package kr.util.audio;

import java.util.List;


/**
 * Plays a single sin wave at any tone.
 */
public class MidiAudioClip extends AudioClip  
{
	private int rate;
	private int midiOffset = 0; // offset used to start a new frequency at the beginning of the cycle (to prevent clicking noises)
	private static final double [] MIDI_FREQ;
	private static final byte [] SIN_WAVE_BANK;
	
	public double baseFreq;
	private double currFreqRatio;
	private double newFreqRatio;

	//the current value allowed in the line before we cut the note and start another
	private static final int TRANSISTION_SLOP_VALUE = 5;
	

	static
	{
		MIDI_FREQ = new double[128];
		for(int i = 0; i < MIDI_FREQ.length; i++)
		{
			MIDI_FREQ[i] = 55. * Math.pow(2, (i-33.)/12.);
		}
		
		SIN_WAVE_BANK = new byte[4096]; 
		
		for(int i = 0; i < SIN_WAVE_BANK.length; i++)
		{
			SIN_WAVE_BANK[i] = (byte)Math.round(127 * Math.sin(2 * Math.PI * i / SIN_WAVE_BANK.length));
		}
	}
	
	public static double getFreqForMidi(int midi)
	{
		return MIDI_FREQ[midi];
	}

	public MidiAudioClip(int rate)
	{
		this.rate = rate;
		this.baseFreq = (double)rate/SIN_WAVE_BANK.length;
	}


	@Override
	public int getNumChannels() {
		return 1;
	}

	@Override
	public int length() {
		return Integer.MAX_VALUE;
	}

	@Override
	public int getRate() {
		return rate;
	}
	
	public void playMidi(byte note)
	{
		if(note == -1)
			playFreq(0);
		else
			playFreq(MIDI_FREQ[note]);
	}
	
	public void playFreq(double freq)
	{
		double choosenFreqRatio = freq/baseFreq;
		if(choosenFreqRatio == newFreqRatio || newFreqRatio == - 1 && choosenFreqRatio == currFreqRatio) return;
		
		//if we are currently playing, we transistion to the next frequency at the cycle end of the current frequency
		if(currFreqRatio != 0)
			newFreqRatio = freq / baseFreq;
		else
		{
			//transistion right away
			midiOffset = -1; //signify to make zero the next read index
			this.currFreqRatio = freq / baseFreq;
			newFreqRatio = -1;
		}
	}
	
	public double getFreqRatio()
	{
		return currFreqRatio;
	}
	
	@Override
	public byte byteAt(int index) {
		
		
		//double val = index * currFreqRatio / SIN_WAVE_BANK.length;
		//return SIN_WAVE_BANK[((int)Math.floor((val - Math.floor(val))*SIN_WAVE_BANK.length))];
		if(midiOffset == -1) //we just started playing the note, so make the index the current offset 
			midiOffset = (int) ((SIN_WAVE_BANK.length - (int)Math.round(index * currFreqRatio) % SIN_WAVE_BANK.length)/currFreqRatio);
		
		int val = (int)Math.round((index + midiOffset) * currFreqRatio);
		
		byte data = SIN_WAVE_BANK[val % SIN_WAVE_BANK.length];

		if(newFreqRatio != -1 && data > -TRANSISTION_SLOP_VALUE && data < TRANSISTION_SLOP_VALUE) 
		{
			currFreqRatio = newFreqRatio;
			newFreqRatio = -1;
			midiOffset = (int) ((SIN_WAVE_BANK.length - (int)Math.round(index * currFreqRatio) % SIN_WAVE_BANK.length)/currFreqRatio);
			
		}

		return data;
	}

	public byte cosByteAt(int index) {
		int val = (int)Math.round(index * currFreqRatio);
		
		return SIN_WAVE_BANK[(val + SIN_WAVE_BANK.length / 4) % SIN_WAVE_BANK.length];
	}
	
	
	@Override
	public void copyTo(int offset, int length, byte[] dest, int destOffset) {
		//PERF: can do this faster
		if(length==0) return;
		
		if(midiOffset == -1) //we just started playing the note, so make the index the current offset 
			midiOffset = offset;
		
		offset += midiOffset;

		if(newFreqRatio != -1)
		{
			int v1 = (int)(offset * currFreqRatio) / SIN_WAVE_BANK.length;
			int v2 = (int)((offset + length) * currFreqRatio) / SIN_WAVE_BANK.length;
			
			//if we've crossed the zero line, only then do we switch notes to prevent a clicking sound
			if(v1 < v2)
			{
				//write the end of the last cycle
				double temp = newFreqRatio;
				newFreqRatio = -1;
				int writtenLength = (int) ((v1 + 1) * SIN_WAVE_BANK.length / currFreqRatio) - offset;
				copyTo(offset, (int) writtenLength, dest, destOffset);
				
				//update the offset and the length, and write the new cycle with the new note
				offset += writtenLength;
				destOffset += writtenLength;
				
				length -= writtenLength;
				
				//set the new midi offset so the new note will start at zero again
				midiOffset = (int) ((SIN_WAVE_BANK.length - (int)((offset - midiOffset) * currFreqRatio) % SIN_WAVE_BANK.length) / currFreqRatio);
				
				currFreqRatio = temp;
			}
		}
		
		int x0,x1;
		
		//dest is represented by the line in the horizontal direction
		x0 = destOffset;
		x1 = destOffset+length-1;
		double y = offset * currFreqRatio;
		int y0 = (int)Math.floor(y) % SIN_WAVE_BANK.length;
		double fract = y - Math.floor(y);
				
		//
		// uses a line drawing algorithm to get the data
		//
		
        int dy = (int)Math.round(currFreqRatio * SIN_WAVE_BANK.length);
        int dx = SIN_WAVE_BANK.length;
        int stepx, stepy;

        dy <<= 1;                                                  // dy is now 2*dy
        dx <<= 1;                                                  // dx is now 2*dx
        int fraction = (int)Math.round(2*dx*(fract+.5) + 2*dy);         // fraction is (fract + .5 + dy/dx) multiplied by 2 * dx
        int stepY = dy/dx + 1;
        dx *= stepY;
        
        if(x0 < 0 || y0 < 0)
        	x0 = x0 * 1;
        dest[x0] = SIN_WAVE_BANK[y0];
        while (x0 != x1) {
            while (fraction >= 0) {
                y0 = (y0 + stepY) % SIN_WAVE_BANK.length;
                fraction -= dx;                                // same as fraction -= 2*dx
            }
            x0 ++;
            fraction += dy;                                    // same as fraction -= 2*dy
            dest[x0] = SIN_WAVE_BANK[y0];
        }
	}
	
	/**
	 * The number of bytes that will constitute a single cycle at a given note
	 */
	public double getCycleBytesForNote(int note)
	{
		return SIN_WAVE_BANK.length * baseFreq / MIDI_FREQ[note]; 
	}

	/**
	 * Returns the greatest index <= indexFromMicros that is the first value in the bank
	 */
	public int getFloorCycle(int index) {
		int val = (int)Math.round(index * currFreqRatio);
		val -= val % SIN_WAVE_BANK.length;
		
		return (int)Math.round(val / currFreqRatio); 
	}

	public int roundToCycles(int frames, int note) {
		return (int) (Math.round(frames / getCycleBytesForNote(note)) * getCycleBytesForNote(note));
	}

}
