package org.devel.jsyn;

import java.io.IOException;

import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;

import com.jsyn.JSyn;
import com.jsyn.Synthesizer;
import com.jsyn.devices.javasound.MidiDeviceTools;
import com.jsyn.instruments.SubtractiveSynthVoice;
import com.jsyn.midi.MessageParser;
import com.jsyn.midi.MidiConstants;
import com.jsyn.unitgen.LineOut;
import com.jsyn.unitgen.PowerOfTwo;
import com.jsyn.unitgen.SineOscillator;
import com.jsyn.unitgen.UnitOscillator;
import com.jsyn.util.VoiceAllocator;
import com.softsynth.shared.time.TimeStamp;

/**
 * Connect a USB MIDI Keyboard to the internal MIDI Synthesizer
 * using JavaSound.
 * 
 * @author Phil Burk (C) 2010 Mobileer Inc
 * 
 */
public class UseMidiKeyboard
{
	private static final int MAX_VOICES = 8;
	private Synthesizer synth;
	private VoiceAllocator allocator;
	private LineOut lineOut;
	private double vibratoRate = 5.0;
	private double vibratoDepth = 0.0;

	private UnitOscillator lfo;
	private PowerOfTwo powerOfTwo;
	private MessageParser messageParser;
	private SubtractiveSynthVoice[] voices;

	public static void main( String[] args )
	{
		UseMidiKeyboard app = new UseMidiKeyboard();
		try
		{
			app.test();
		} catch( MidiUnavailableException e )
		{
			e.printStackTrace();
		} catch( IOException e )
		{
			e.printStackTrace();
		} catch( InterruptedException e )
		{
			e.printStackTrace();
		}
	}

	// Write a Receiver to get the messages from a Transmitter.
	class CustomReceiver implements Receiver
	{
		public void close()
		{
			System.out.print( "Closed." );
		}

		public void send( MidiMessage message, long timeStamp )
		{
			byte[] bytes = message.getMessage();
			messageParser.parse( bytes );
		}
	}

	public int test() throws MidiUnavailableException, IOException,
			InterruptedException
	{
		setupSynth();

		messageParser = new MyParser();

		int result = 2;
		// I know that my keyboard has "Usb in the description".
		// There is another device that does not have Usb that I want to avoid.
		MidiDevice keyboard = MidiDeviceTools.findKeyboard( "usb" );
		Receiver receiver = new CustomReceiver();
		// Just use default synthesizer.
		if( keyboard != null )
		{
			// If you forget to open them you will hear no sound.
			keyboard.open();
			// Put the receiver in the transmitter.
			// This gives fairly low latency playing.
			keyboard.getTransmitter().setReceiver( receiver );
			System.out.println( "Play MIDI keyboard: " + keyboard.getDeviceInfo().getDescription() );
			result = 0;
		}
		else
		{
			System.out.println( "Could not find a keyboard." );
		}
		return result;
	}

	class MyParser extends MessageParser
	{
		@Override
		public void controlChange( int channel, int index, int value )
		{
			// Mod Wheel
			if( index == 1 )
			{
				vibratoDepth = 0.1 * value / 128.0;
				// System.out.println( "vibratoDepth = " + vibratoDepth );
				lfo.amplitude.set( vibratoDepth );
			}
			// 102 is the index of the first knob on my Axiom 25
			else if( index == 102 )
			{
				final double bump = 0.95;
				if( value < 64 )
				{
					vibratoRate *= bump;
				}
				else
				{
					vibratoRate *= 1.0 / bump;
				}
				System.out.println( "vibratoRate = " + vibratoRate );
				lfo.frequency.set( vibratoRate );
			}

		}

		@Override
		public void noteOff( int channel, int noteNumber, int velocity )
		{
			allocator.noteOff( noteNumber, synth.createTimeStamp() );
		}

		@Override
		public void noteOn( int channel, int noteNumber, int velocity )
		{
			double frequency = convertPitchToFrequency( noteNumber );
			double amplitude = velocity / (4 * 128.0);
			TimeStamp timeStamp = synth.createTimeStamp();
			allocator.noteOn( noteNumber, frequency, amplitude, timeStamp  );
		}

		public void pitchBend( int channel, int bend )
		{
			double fraction = (bend - MidiConstants.PITCH_BEND_CENTER) / ((double)MidiConstants.PITCH_BEND_CENTER);
			System.out.println( "bend = " + bend + ", fraction = " + fraction );
		}
	}

	/**
	 * Calculate frequency in Hertz based on MIDI pitch. Middle C is 60.0. You
	 * can use fractional pitches so 60.5 would give you a pitch half way
	 * between C and C#.
	 */
	double convertPitchToFrequency( double pitch )
	{
		final double concertA = 440.0;
		return concertA * Math.pow( 2.0, ((pitch - 69) * (1.0 / 12.0)) );
	}

	private void setupSynth()
	{
		synth = JSyn.createSynthesizer();

		// Add an output.
		synth.add( lineOut = new LineOut() );

		synth.add( powerOfTwo = new PowerOfTwo() );
		synth.add( lfo = new SineOscillator() );
		// Sums pitch modulation.
		lfo.output.connect( powerOfTwo.input );
		lfo.amplitude.set( vibratoDepth );
		lfo.frequency.set( vibratoRate );

		voices = new SubtractiveSynthVoice[MAX_VOICES];
		for( int i = 0; i < MAX_VOICES; i++ )
		{
			SubtractiveSynthVoice voice = new SubtractiveSynthVoice();
			synth.add( voice );
			powerOfTwo.output.connect( voice.pitchModulation );
			voice.getOutput().connect( 0, lineOut.input, 0 );
			voice.getOutput().connect( 0, lineOut.input, 1 );
			voices[i] = voice;
		}
		allocator = new VoiceAllocator( voices );

		// Start synthesizer using default stereo output at 44100 Hz.
		synth.start();
		// We only need to start the LineOut. It will pull data from the
		// oscillator.
		lineOut.start();

		// Get synthesizer time in seconds.
		double timeNow = synth.getCurrentTime();

		// Advance to a near future time so we have a clean start.
		double time = timeNow + 0.5;

	}

}
