/* $Id$
 *
 * Copyright (c) 1999 Xenonsoft Limited. All Rights Reserved.
 *
 * This software is protected by copyright. You are hereby notified from
 * now by reading this message. This software is also the confidential 
 * and proprietary information of Xenonsoft Limited.  ("Confidential
 * Information"). 
 *
 * This software is distributed under the Xenonsoft Public end user 
 * License ("XPeL"), where the machine-readable source code is provided 
 * under the "Open Source" model.
 * For more information, please read the file "LICENSE-XPL.txt" 
 */

// SoundEngine.java
//

//
// Description:
//	  Sound Engine builds sounds
//	  
// Author:
//	  Peter Pilgrim
//	  
// RCS HEADER ``SoundEngine.java''
// 
// $Author$
// $Date$
// $Source$
// $Revision$    $State$    $Locker$
//
// ================================================================================
// History
//	  $Log$
// 
// 
//

package xenon.soundkit;

import java.util.*;


// TODO: Still to come echo and reverb members!

/**
 * A object that generate sound waves for playing through
 * <b>sun.audio.AudioPlayer</b>. The Sound engine generates an audio
 * buffer, which is fancy name for an array of bytes in mu-law format.
 *
 * The sound engine works int conjunction with the <b>Envelope</b> to
 * shape the sound that is produced.
 *
 * Those of you who are old enough to remember the home computer boom
 * of the mid 1980's may have pleasent memories of the <b>BBC
 * Microcomputer</b>, which manufactured by British company
 * <b>Acorn</b> based Cambridge, England. This popular home computer
 * had to extended basic command SOUND and ENVELOPE. These reserved
 * words generated some fantastic sounds at the time. The SoundEngine
 * sort of emulates the SOUND statement and the corresponding Envelope
 * class emulates the ENVELOPE statement. Note it was never my
 * intention to complete emulate the BBC Sound Chip, however I
 * remembered the beloved simplicity and elegance of the clever
 * design. I thought wouldn't just be great it Java had such applied
 * sound generating facility. 
 *
 * More docu required.
 *
 *
 * @author Peter Pilgrim Thu Jun 17 23:37:02 BST 1999
 * @version $Id$ 
 *
 * */
public class SoundEngine {

    /** minimum volume */
    public final static int	MIN_VOLUME=0;
    /** maximum volume */
    public final static int	MAX_VOLUME=127;
    /** minimum frequency (ideal) */
    public final static int	MIN_FREQUENCY=50;
    /** maximum frequency (ideal) */
    public final static int	MAX_FREQUENCY=22250;
    

    public final static int	DEFAULT_SAMPLING_RATE=8000;
    public final static double	DEFAULT_TIME_DURATION=1.0;
    public final static double	MINIMUM_TIME_DURATION=0.10;
    
    public final static int	HOLD_PITCH_MODULATION=0x0001;
						// Hold pitch modulation 
    public final static int	HOLD_AMPLITUDE_MODULATION=0x0002;
						// Hold amplitude modulation 
    public final static int	NOISE_GATE_FLAG=0x0004;
						// Noise Gate Complete Flag
    public final static int	NOISE_MIX_GATE_FLAG=0x0008;
						// Noise Gate Mix Flag
    public final static int	FM_DELTA_RANDOM_FLAG=0x0010;
						// Frequency Random Waveform
						// Increment flags.
    
    protected final int	ATTACK=1;		// attack stage
    protected final int	DECAY=2;		// decay stage
    protected final int	SUSTAIN=3;		// sustain stage
    protected final int	HOLD=4;			// hold stage
    protected final int	RELEASE=5;		// release stage
    protected final int	AMPLITUDE_DONE=5;	// sound (amplitude) done
    

    protected int	flags = HOLD_PITCH_MODULATION;

    protected int	sampling_rate = DEFAULT_SAMPLING_RATE;
    protected double	time_duration = DEFAULT_TIME_DURATION;
    
    protected int	pitch, duration, volume;
    protected Wave	wave;
    protected Envelope	envelope;    
    protected byte []	buffer;

    protected SoundFilter filter;

    // These tables were snarfed from the Linux kernel 2.0.33
    // `/usr/src/linux/drivers/sound/ulaw.h'
    
    protected static int ulaw_to_linear[] = {
	// maps a mu-law value to linear DSP value.
	3,    7,   11,   15,   19,   23,   27,   31,
	35,   39,   43,   47,   51,   55,   59,   63,
	66,   68,   70,   72,   74,   76,   78,   80,
	82,   84,   86,   88,   90,   92,   94,   96,
	98,   99,  100,  101,  102,  103,  104,  105,
	106,  107,  108,  109,  110,  111,  112,  113,
	113,  114,  114,  115,  115,  116,  116,  117,
	117,  118,  118,  119,  119,  120,  120,  121,
	121,  121,  122,  122,  122,  122,  123,  123,
	123,  123,  124,  124,  124,  124,  125,  125,
	125,  125,  125,  125,  126,  126,  126,  126,
	126,  126,  126,  126,  127,  127,  127,  127,
	127,  127,  127,  127,  127,  127,  127,  127,
	128,  128,  128,  128,  128,  128,  128,  128,
	128,  128,  128,  128,  128,  128,  128,  128,
	128,  128,  128,  128,  128,  128,  128,  128,
	253,  249,  245,  241,  237,  233,  229,  225,
	221,  217,  213,  209,  205,  201,  197,  193,
	190,  188,  186,  184,  182,  180,  178,  176,
	174,  172,  170,  168,  166,  164,  162,  160,
	158,  157,  156,  155,  154,  153,  152,  151,
	150,  149,  148,  147,  146,  145,  144,  143,
	143,  142,  142,  141,  141,  140,  140,  139,
	139,  138,  138,  137,  137,  136,  136,  135,
	135,  135,  134,  134,  134,  134,  133,  133,
	133,  133,  132,  132,  132,  132,  131,  131,
	131,  131,  131,  131,  130,  130,  130,  130,
	130,  130,  130,  130,  129,  129,  129,  129,
	129,  129,  129,  129,  129,  129,  129,  129,
	128,  128,  128,  128,  128,  128,  128,  128,
	128,  128,  128,  128,  128,  128,  128,  128,
	128,  128,  128,  128,  128,  128,  128,  128,
    };
    
    protected static int linear_to_ulaw[] = {
	// maps a linear DSP value to mu-law value.
	0,    0,    0,    0,    0,    1,    1,    1,
	1,    2,    2,    2,    2,    3,    3,    3,
	3,    4,    4,    4,    4,    5,    5,    5,
	5,    6,    6,    6,    6,    7,    7,    7,
	7,    8,    8,    8,    8,    9,    9,    9,
	9,   10,   10,   10,   10,   11,   11,   11,
	11,   12,   12,   12,   12,   13,   13,   13,
	13,   14,   14,   14,   14,   15,   15,   15,
	15,   16,   16,   17,   17,   18,   18,   19,
	19,   20,   20,   21,   21,   22,   22,   23,
	23,   24,   24,   25,   25,   26,   26,   27,
	27,   28,   28,   29,   29,   30,   30,   31,
	31,   32,   33,   34,   35,   36,   37,   38,
	39,   40,   41,   42,   43,   44,   45,   46,
	47,   49,   51,   53,   55,   57,   59,   61,
	63,   66,   70,   74,   78,   84,   92,  104,
	254,  231,  219,  211,  205,  201,  197,  193,
	190,  188,  186,  184,  182,  180,  178,  176,
	175,  174,  173,  172,  171,  170,  169,  168,
	167,  166,  165,  164,  163,  162,  161,  160,
	159,  159,  158,  158,  157,  157,  156,  156,
	155,  155,  154,  154,  153,  153,  152,  152,
	151,  151,  150,  150,  149,  149,  148,  148,
	147,  147,  146,  146,  145,  145,  144,  144,
	143,  143,  143,  143,  142,  142,  142,  142,
	141,  141,  141,  141,  140,  140,  140,  140,
	139,  139,  139,  139,  138,  138,  138,  138,
	137,  137,  137,  137,  136,  136,  136,  136,
	135,  135,  135,  135,  134,  134,  134,  134,
	133,  133,  133,  133,  132,  132,  132,  132,
	131,  131,  131,  131,  130,  130,  130,  130,
	129,  129,  129,  129,  128,  128,  128,  128,
    };
    
    public SoundEngine( int sampling_rate, double time_duration, 
			int volume, int pitch, int duration,
			Wave wave, Envelope envelope )
    {
	if ( time_duration < MINIMUM_TIME_DURATION ) 
	    throw new RuntimeException( "time duration is less than " + MINIMUM_TIME_DURATION + " second.");
	
	this.time_duration = time_duration;
	this.pitch = pitch;
	this.duration = duration;
	this.volume = volume;
	this.wave = wave;
	this.envelope = envelope;
	
	int buffer_length = (int)( time_duration * sampling_rate );
	buffer = new byte[buffer_length];
    }

    /** Set new sound paremeters: pitch, duration, volume */
    public SoundEngine setSound( int volume, int pitch, int duration )
    {
	this.pitch = pitch;
	this.duration = duration;
	this.volume = volume;

	return (this);
    }

    /** Set new sound paremeters: pitch, duration, volume
	and waveform and envelope. 
    */
    public SoundEngine setSound( int pitch, int duration, int volume,
				 Wave wave, Envelope envelope )
    {
	this.pitch = pitch;
	this.duration = duration;
	this.volume = volume;
	this.wave = wave;
	this.envelope = envelope;

	return (this);
    }
    
    /** Generate new wave form (sound audio) data in the sound buffer. */
    public void makeWave()
    {
	int j, counter;

	// Create the mu-Law buffer for this engine
	int buffer_length = buffer.length;
	byte zero_amplitude = (byte)(linear_to_ulaw[128] - 128);
	for ( j=0; j<buffer.length; ++j ) buffer[j] = zero_amplitude;

	// Create the linear sound data buffer (also used byte filters)
	SoundFilterDataBuffer data_buffer = 
	    new SoundFilterDataBuffer( buffer_length, -127, 127 );
	
	//
	// Waveform variables
	//
	int wavelength = wave.getLength();
	int waveincrement_start = 
	    (int)( pitch * wavelength / (double)sampling_rate );
	if ( waveincrement_start < 1 )
	    waveincrement_start = 1;
	int waveincrement = waveincrement_start;
	int waveincrement_extra = 0;
	int waveindex = 0;

	// HACK: A proper resampling algorithm is required especially
	// for sampled audio clips. Setting the wave increment to 1
	// and ignoring pitch modulation will do for now.
	boolean ignorePitchInfo = ( wave instanceof AudioClipWave );
	if ( ignorePitchInfo )
	    waveincrement_start = waveincrement = 1;

	// Pitch change variables.

	int pitch_change_counter = 
	    envelope.pitch_step_unit * sampling_rate / 40;
	int pitch_mod_index = 0;
	int pitch_step = envelope.pitch_step[pitch_mod_index];

	// Amplitude change variables.
	int amplitude_change_counter = 
	    envelope.amplitude_step_unit * sampling_rate / 40;
	int amplitude = 0;
	int amplitude_mode = ATTACK;
	int amplitude_delta = envelope.attack_delta;	
	int amplitude_step = envelope.attack_step;

	// Calculate the sound buffer sample clip length
	int duration_clip_length = sampling_rate * duration / 80;
	double value;

	// System.out.println( "pitch:"+ pitch );
	// System.out.println( "volume:"+ volume );
	// System.out.println( "duration:"+ duration );
	// System.out.println( "clip length:"+ duration_clip_length+"  (estimated)" );
	
	if ( duration_clip_length < 1 ) 
	    duration_clip_length = 1;
	data_buffer.setClipLength( duration_clip_length );
	
	for ( j=0; j<buffer.length; ++j ) {
	    if ( !ignorePitchInfo ) {
		if ( ( FM_DELTA_RANDOM_FLAG & flags) != 0 ) {
		    // Frequency modulate the waveform so by
		    // randomly setting a waveform index. So that
		    // we get a distinctive warble when we want to
		    // get a noise gate sound.
		    //
		    // Randomly vary the waveform increment FIXME:
		    waveincrement_extra = (int) ( Math.random() * 40.0 );
		}
		if (j % pitch_change_counter == 0 ) {
		    //
		    // Frequency modulation.
		    // Modulate the sound by the envelope pitch
		    // rate of change parameters
		    //
		    if ( --pitch_step == 0 ) {
			//
			// When the current pitch step counter reaches
			// reset modify the pitch by changing the waveform
			// delta increment.
			//
			++pitch_mod_index;
			if ( pitch_mod_index == 3 ) {
			    pitch_mod_index = 0;
			    if ( (flags & HOLD_PITCH_MODULATION ) != 0 )
				// pitch returns to the original after pi[3] !!
				waveincrement = waveincrement_start;
			}
			else {
			    waveincrement += envelope.pitch_delta[pitch_mod_index];
			    if (waveincrement < -wavelength/2 )
				waveincrement = wavelength/2 ;
			    else if (waveincrement > wavelength/2 )
				waveincrement = -wavelength/2 ;
			}
			pitch_step = envelope.pitch_step[pitch_mod_index];
		    }
		}
	    }
	    
	    if (j % amplitude_change_counter == 0 ) {
		//
		// Amplitude modulation.
		//
		amplitude += amplitude_delta;

		// Make sure amplitude is clipped.
		if ( amplitude < 0 ) {
		    amplitude = 0;
		    if ( amplitude_mode == RELEASE ) {
			// System.out.println("Done (1)");
			amplitude_delta = 0;
			amplitude_step = 123456;
			amplitude_mode = AMPLITUDE_DONE;
		    }
		}
		if ( amplitude > 127 )
		    amplitude = 127;

		if ( --amplitude_step < 1 ) {
		    switch ( amplitude_mode ) {
		      case ATTACK:
			  // System.out.println("Decay"); 
			  amplitude_mode = DECAY;
			  amplitude_delta = envelope.decay_delta;	  
			  amplitude_step = envelope.decay_step;
			  break;
			  
		      case DECAY:
			  // System.out.println("Sustain");
			  amplitude_mode = SUSTAIN;
			  amplitude_delta = envelope.sustain_delta;
			  amplitude_step = envelope.sustain_step;
			  break;
			  
		      case SUSTAIN:
			  // System.out.println("Hold");
			  amplitude_mode = HOLD;
			  amplitude_delta = 0;
			  amplitude_step = 123456;
			  break;
			  
		      case HOLD:
			  // System.out.println("Release (1)");
			  amplitude_mode = RELEASE;
			  if ( (flags & HOLD_AMPLITUDE_MODULATION ) != 0 ) {
			      amplitude_delta = 0;
			      amplitude_step = 123456;
			  }
			  else {
			      amplitude_delta = envelope.release_delta;
			      amplitude_step = envelope.release_step;
			  }
			  break;
			  
		      case RELEASE:
		      default:
			  // System.out.println("Done (2)");
			  amplitude = 0;
			  amplitude_delta = 0;			  
			  amplitude_step = 123456;
			  amplitude_mode = AMPLITUDE_DONE;
			  break;
		    }
		}
	    }
	    
	    //
	    // Calculate the discrete wave point value at this time coordinate
	    //
	    if ( (NOISE_GATE_FLAG & flags) != 0 ) {
		//
		// Excuse me, I have only a NOVICE understanding of digital
		// signal processing. A digital filter is basically
		//
		//
		//    	 '
		//    	X    =  A * X  +  B * X
		//    	 i           i         j
		//
		//
		// Where X is buffer of audio data
		// and A and B are mixing amplitude coefficients
		// and i and j indices in the audio buffer X
		// such  j = i - D, where D is a delay offset back
		// in time to earlier element int the audio buffer's history.
		// By definition D > 0 is always true.
		//
		// What follows below is a pseudo random digital filter
		// for what I call my basic NOISE GATE, and it works for me.
		// So int antifact `D = RND(f) + K'.
		// If you can improve on it, don't forget to courtesy
		// mail me your change, so that we all can learn as well.
		//
		// Peter Pilgrim Thu Jun 17 23:14:32 BST 1999
		//
		double history = 5.0 + waveincrement * 0.10;
		int sfdb_len = data_buffer.filter_buffer.length;
		int q = ( sfdb_len + j - ((int)(10.0+Math.random() * history)) )
		    % sfdb_len;
		int histValue = (q>=0 ? data_buffer.filter_buffer[q] : 0 );
		value = 0.69 * wave.elementAt(waveindex) * volume * amplitude / 127.0 + 0.31*histValue;
	    }
	    else if ( (NOISE_MIX_GATE_FLAG & flags) != 0 )
		value = (Math.random() - 0.5 + wave.elementAt(waveindex) ) * 0.5 * volume * amplitude / 127.0;
	    else
		value = wave.elementAt(waveindex) * volume * amplitude / 127.0;
	    if (value < -127.0) value=-127.0;
	    else if (value > 127.0) value=127.0;

	    data_buffer.filter_buffer[j] = (int)value;
	    
	    //
	    // Decrement the sound byte length, (the number of bytes
	    // that go to make the core of the sound. Once this point
	    // jump to the RELEASE phase.
	    //
	    --duration_clip_length;
	    if ( duration_clip_length == 0 && 
		 amplitude_mode != RELEASE && amplitude_mode != AMPLITUDE_DONE) 
	    {
		// System.out.println("Release (2)");
		amplitude_mode = RELEASE;
		if ( (flags & HOLD_AMPLITUDE_MODULATION ) != 0 ) {
		    amplitude_delta = 0;
		    amplitude_step = 123456;
		}
		else {
		    amplitude_delta = envelope.release_delta;
		    amplitude_step = envelope.release_step;
		}
	    }

	    waveindex = (waveindex + waveincrement + 
			 waveincrement_extra + wavelength) % wavelength;
	}

	// Set the clip length of the sound buffer to last index
	data_buffer.setClipLength( j );

	// System.out.println( 
	//      "data_buffer{ filter_length:"+data_buffer.filter_buffer.length+
	//      ", filter_buffer:"+data_buffer.filter_buffer+" }" );

	if ( filter != null ) {
	    filter.filterLinearData( this, data_buffer );
	}

	// Convert linear data to mu-Law for Sun Audio Player
	cvtLinearDataToMuLaw8( data_buffer, buffer );
    }
    
    /**
     * Sets the sound <i>primary</i> filter for the linear sound data
     *
     * @param f the new filter
     */
    public void setFilter( SoundFilter f )
    {
	filter = f;
    }
    
    /**
     * Gets the sound <i>primary</i> filter for the linear sound data
     */
    public SoundFilter getFilter()
    {
	return (filter);
    }
    

    /**
     * convert linear sound data to mu-Law data requiring
     * for JDK 1.1 <code>sun.audio.AudioPlayer</code>
     *
     * @param data_buffer the <i>input</i> linear data buffer object
     * @param muLaw_buffer the <i>output</i> mu-law buffer
     *
     */
    protected void cvtLinearDataToMuLaw8( 
	SoundFilterDataBuffer data_buffer, 
	byte [] muLaw_buffer )
    {
	//
	// This next statement line writes a waveform linear
	// amplitude to the audio buffer which is wrong because
	// the `sun.audio.AudioData' expects mu-law companding 
	// logarithmic amplitude.
	//
	for ( int j=0; j<muLaw_buffer.length; ++j ) {
	    muLaw_buffer[j] = (byte)
		(linear_to_ulaw[(int)data_buffer.filter_buffer[j] + 128 ] - 128);
	    // (int)value + 128
	    // bias the waveform from -128 < y < 128 to 0 <= y << 256
	    // to read the array. The elements of the array is already
	    // in unsigned char values, 0 <= e < 256 therefore substract
	    // by 128 brings the element's value into signed range.
	    // -128 < e < 128.

	    // if ( j % 250 == 0 ) System.out.println(" [" + j + ", " + waveindex + "]\t value:" + value + "  \tamplitude:" + amplitude + " \tbyte:" + buffer[j] );
	    
	}
	
    }
    
    /** returns the sampling rate */
    public int	getSamplingRate()
    {
	return (sampling_rate);
    }
    
    /** returns the time duration */
    public double	getTimeDuration()
    {
	return (time_duration);
    }
    
    /** set the sound engine flags, returns the `this' reference 
     * to SoundEngine object.
     */
    public SoundEngine	setFlags( int flags)
    {
	this.flags = flags;	
	return (this);
    }
    
    /** returns the sound engine option flags */
    public int	getFlags()
    {
	return (flags);
    }
    
    /** returns the pitch parameter */
    public int	getPitch()
    {
	return (pitch);
    }
    
    /** returns the duration parameter */
    public int	getDuration()
    {
	return (duration);
    }
    
    /** returns the volume parameter */
    public int	getVolume()
    {
	return (volume);
    }
    
    /** returns the waveform parameter */
    public Wave	getWave()
    {
	return (wave);
    }
    
    /** returns the volume parameter */
    public Envelope getEnvelope()
    {
	return (envelope);
    }

    /** returns the sound buffer generated by the method `makeWave'
     * You can pass this directly to the `AudioDate()' constructor.
     * @see makeWave
     * @see AudioData
     * NOTE sun.audio.* is an undocumented API.
     */
    public byte[] getSoundData()
    {
	return buffer;
    }
    
    /** dispose of this SoundEngine */
    public void dispose() 
    {
	// Help the garbage collector
	buffer = null;
	wave = null;
	envelope = null;
    }
	
}

// fini
