/*
 * Portable Audio I/O Library
 * Java Binding for PortAudio
 *
 * Based on the Open Source API proposed by Ross Bencina
 * Copyright (c) 2008 Ross Bencina
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
 * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * The text above constitutes the entire PortAudio license; however, 
 * the PortAudio community also makes the following non-binding requests:
 *
 * Any person wishing to distribute modifications to the Software is
 * requested to send the modifications to the original developer so that
 * they can be incorporated into the canonical version. It is also 
 * requested that these non-binding requests be included along with the 
 * license above.
 */

package com.portaudio;

import junit.framework.TestCase;

/**
 * Test the Java bindings for PortAudio.
 * 
 * @author Phil Burk
 * 
 */
public class TestBasic extends TestCase
{

	public void testDeviceCount()
	{
		PortAudio.initialize();
		assertTrue( "version invalid", (PortAudio.getVersion() > 0) );
		System.out.println( "getVersion  = " + PortAudio.getVersion() );
		System.out.println( "getVersionText  = " + PortAudio.getVersionText() );
		System.out.println( "getDeviceCount  = " + PortAudio.getDeviceCount() );
		assertTrue( "getDeviceCount", (PortAudio.getDeviceCount() > 0) );
		PortAudio.terminate();
	}

	public void testListDevices()
	{
		PortAudio.initialize();
		int count = PortAudio.getDeviceCount();
		assertTrue( "getDeviceCount", (count > 0) );
		for( int i = 0; i < count; i++ )
		{
			DeviceInfo info = PortAudio.getDeviceInfo( i );
			System.out.println( "------------------ #" + i );
			System.out.println( "  name              = " + info.name );
			System.out.println( "  hostApi           = " + info.hostApi );
			System.out.println( "  maxOutputChannels = "
					+ info.maxOutputChannels );
			System.out.println( "  maxInputChannels  = "
					+ info.maxInputChannels );
			System.out.println( "  defaultSampleRate = "
					+ info.defaultSampleRate );
			System.out.printf( "  defaultLowInputLatency   = %3d msec\n",
					((int) (info.defaultLowInputLatency * 1000)) );
			System.out.printf( "  defaultHighInputLatency  = %3d msec\n",
					((int) (info.defaultHighInputLatency * 1000)) );
			System.out.printf( "  defaultLowOutputLatency  = %3d msec\n",
					((int) (info.defaultLowOutputLatency * 1000)) );
			System.out.printf( "  defaultHighOutputLatency = %3d msec\n",
					((int) (info.defaultHighOutputLatency * 1000)) );

			assertTrue( "some channels",
					(info.maxOutputChannels + info.maxInputChannels) > 0 );
			assertTrue( "not too many channels", (info.maxInputChannels < 64) );
			assertTrue( "not too many channels", (info.maxOutputChannels < 64) );
		}

		System.out.println( "defaultInput  = "
				+ PortAudio.getDefaultInputDevice() );
		System.out.println( "defaultOutput = "
				+ PortAudio.getDefaultOutputDevice() );

		PortAudio.terminate();
	}

	public void testHostApis()
	{
		PortAudio.initialize();
		int validApiCount = 0;
		for( int hostApiType = 0; hostApiType < PortAudio.HOST_API_TYPE_COUNT; hostApiType++ )
		{
			int hostApiIndex = PortAudio
					.hostApiTypeIdToHostApiIndex( hostApiType );
			if( hostApiIndex >= 0 )
			{
				HostApiInfo info = PortAudio.getHostApiInfo( hostApiIndex );
				System.out.println( "Checking Host API: " + info.name );
				for( int apiDeviceIndex = 0; apiDeviceIndex < info.deviceCount; apiDeviceIndex++ )
				{
					int deviceIndex = PortAudio
							.hostApiDeviceIndexToDeviceIndex( hostApiIndex,
									apiDeviceIndex );
					DeviceInfo deviceInfo = PortAudio
							.getDeviceInfo( deviceIndex );
					assertEquals( "host api must match up", hostApiIndex,
							deviceInfo.hostApi );
				}
				validApiCount++;
			}
		}

		assertEquals( "host api counts", PortAudio.getHostApiCount(),
				validApiCount );
	}

	public void testListHostApis()
	{
		PortAudio.initialize();
		int count = PortAudio.getHostApiCount();
		assertTrue( "getHostApiCount", (count > 0) );
		for( int i = 0; i < count; i++ )
		{
			HostApiInfo info = PortAudio.getHostApiInfo( i );
			System.out.println( "------------------ #" + i );
			System.out.println( "  version             = " + info.version );
			System.out.println( "  name                = " + info.name );
			System.out.println( "  type                = " + info.type );
			System.out.println( "  deviceCount         = " + info.deviceCount );
			System.out.println( "  defaultInputDevice  = "
					+ info.defaultInputDevice );
			System.out.println( "  defaultOutputDevice = "
					+ info.defaultOutputDevice );
			assertTrue( "some devices", info.deviceCount > 0 );
		}

		System.out.println( "------\ndefaultHostApi = "
				+ PortAudio.getDefaultHostApi() );
		PortAudio.terminate();
	}

	public void testCheckFormat()
	{
		PortAudio.initialize();
		StreamParameters streamParameters = new StreamParameters();
		streamParameters.device = PortAudio.getDefaultOutputDevice();
		int result = PortAudio
				.isFormatSupported( null, streamParameters, 44100 );
		System.out.println( "isFormatSupported returns " + result );
		assertEquals( "default output format", 0, result );
		// Try crazy channelCount
		streamParameters.channelCount = 8765;
		result = PortAudio.isFormatSupported( null, streamParameters, 44100 );
		System.out.println( "crazy isFormatSupported returns " + result );
		assertTrue( "default output format", (result < 0) );
		PortAudio.terminate();
	}

	static class SineOscillator
	{
		double phase = 0.0;
		double phaseIncrement = 0.01;

		SineOscillator(double freq, int sampleRate)
		{
			phaseIncrement = freq * Math.PI * 2.0 / sampleRate;
		}

		double next()
		{
			double value = Math.sin( phase );
			phase += phaseIncrement;
			if( phase > Math.PI )
			{
				phase -= Math.PI * 2.0;
			}
			return value;
		}
	}

	public void testStreamError()
	{
		PortAudio.initialize();
		StreamParameters streamParameters = new StreamParameters();
		streamParameters.sampleFormat = PortAudio.FORMAT_FLOAT_32;
		streamParameters.channelCount = 2;
		streamParameters.device = PortAudio.getDefaultOutputDevice();
		int framesPerBuffer = 256;
		int flags = 0;
		BlockingStream stream = PortAudio.openStream( null, streamParameters,
				44100, framesPerBuffer, flags );

		// Try to write data to a stopped stream.
		Throwable caught = null;
		try
		{
			float[] buffer = new float[framesPerBuffer
					* streamParameters.channelCount];
			stream.write( buffer, framesPerBuffer );
		} catch( Throwable e )
		{
			caught = e;
			e.printStackTrace();
		}

		assertTrue( "caught no exception", (caught != null) );
		assertTrue( "exception should say stream is stopped", caught
				.getMessage().contains( "stopped" ) );

		// Try to write null data.
		caught = null;
		try
		{
			stream.write( (float[]) null, framesPerBuffer );
		} catch( Throwable e )
		{
			caught = e;
			e.printStackTrace();
		}
		assertTrue( "caught no exception", (caught != null) );
		assertTrue( "exception should say stream is stopped", caught
				.getMessage().contains( "null" ) );

		// Try to write short data to a float stream.
		stream.start();
		caught = null;
		try
		{
			short[] buffer = new short[framesPerBuffer
					* streamParameters.channelCount];
			stream.write( buffer, framesPerBuffer );
		} catch( Throwable e )
		{
			caught = e;
			e.printStackTrace();
		}

		assertTrue( "caught no exception", (caught != null) );
		assertTrue( "exception should say tried to", caught.getMessage()
				.contains( "Tried to write short" ) );

		stream.close();

		PortAudio.terminate();
	}

	public void checkBlockingWriteFloat( int deviceId, double sampleRate )
	{
		StreamParameters streamParameters = new StreamParameters();
		streamParameters.channelCount = 2;
		streamParameters.device = deviceId;
		streamParameters.suggestedLatency = PortAudio
				.getDeviceInfo( streamParameters.device ).defaultLowOutputLatency;
		System.out.println( "suggestedLatency = "
				+ streamParameters.suggestedLatency );

		int framesPerBuffer = 256;
		int flags = 0;
		BlockingStream stream = PortAudio.openStream( null, streamParameters,
				(int) sampleRate, framesPerBuffer, flags );
		assertTrue( "got default stream", stream != null );

		assertEquals( "stream isStopped", true, stream.isStopped() );
		assertEquals( "stream isActive", false, stream.isActive() );

		int numFrames = 80000;
		double expected = ((double)numFrames) / sampleRate;
		stream.start();
		long startTime = System.currentTimeMillis();
		double startStreamTime = stream.getTime();
		assertEquals( "stream isStopped", false, stream.isStopped() );
		assertEquals( "stream isActive", true, stream.isActive() );

		writeSineData( stream, framesPerBuffer, numFrames, (int) sampleRate );
		
		StreamInfo streamInfo = stream.getInfo();
		System.out.println( "inputLatency of a stream = "+ streamInfo.inputLatency );
		System.out.println( "outputLatency of a stream = "+streamInfo.outputLatency );
		System.out.println( "sampleRate of a stream = "+ streamInfo.sampleRate );
		
		assertEquals( "inputLatency of a stream ", 0.0, streamInfo.inputLatency, 0.000001 );
		assertTrue( "outputLatency of a stream ",(streamInfo.outputLatency > 0) );
		assertEquals( "sampleRate of a stream ", sampleRate, streamInfo.sampleRate, 3 );

		double endStreamTime = stream.getTime();
		stream.stop();
		long stopTime = System.currentTimeMillis();

		System.out.println( "startStreamTime = " + startStreamTime );
		System.out.println( "endStreamTime = " + endStreamTime );
		double elapsedStreamTime = endStreamTime - startStreamTime;
		System.out.println( "elapsedStreamTime = " + elapsedStreamTime );
		assertTrue( "elapsedStreamTime: " + elapsedStreamTime,
				(elapsedStreamTime > 0.0) );
		assertEquals( "elapsedStreamTime: ", expected, elapsedStreamTime, 0.10 );

		assertEquals( "stream isStopped", true, stream.isStopped() );
		assertEquals( "stream isActive", false, stream.isActive() );
		stream.close();

		double elapsed = (stopTime - startTime) / 1000.0;
		assertEquals( "elapsed time to play", expected, elapsed, 0.20 );
	}

	public void testBlockingWriteFloat()
	{
		PortAudio.initialize();
		checkBlockingWriteFloat( PortAudio.getDefaultOutputDevice(), 44100 );
		PortAudio.terminate();
	}

	public void ZtestWriteEachHostAPI()
	{
		PortAudio.initialize();
		for( int hostApiIndex = 0; hostApiIndex < PortAudio.getHostApiCount(); hostApiIndex++ )
		{
			HostApiInfo hostInfo = PortAudio.getHostApiInfo( hostApiIndex );
			System.out.println( "-------------\nWriting using Host API: " + hostInfo.name );
			int deviceId = hostInfo.defaultOutputDevice;
			System.out.println( "   Device ID  =" + deviceId );
			DeviceInfo deviceInfo = PortAudio.getDeviceInfo( deviceId );
			System.out.println( "   sampleRate =" + deviceInfo.defaultSampleRate );
			checkBlockingWriteFloat( deviceId,
					(int) deviceInfo.defaultSampleRate );
			System.out.println( "Finished with " + hostInfo.name );
		}
		PortAudio.terminate();
	}

	private void writeSineData( BlockingStream stream, int framesPerBuffer,
			int numFrames, int sampleRate )
	{
		float[] buffer = new float[framesPerBuffer * 2];
		SineOscillator osc1 = new SineOscillator( 200.0, sampleRate );
		SineOscillator osc2 = new SineOscillator( 300.0, sampleRate );
		int framesLeft = numFrames;
		while( framesLeft > 0 )
		{
			int index = 0;
			int framesToWrite = (framesLeft > framesPerBuffer) ? framesPerBuffer
					: framesLeft;
			for( int j = 0; j < framesToWrite; j++ )
			{
				buffer[index++] = (float) osc1.next();
				buffer[index++] = (float) osc2.next();
			}
			stream.write( buffer, framesToWrite );
			framesLeft -= framesToWrite;
		}
	}

	private void writeSineDataShort( BlockingStream stream,
			int framesPerBuffer, int numFrames )
	{
		short[] buffer = new short[framesPerBuffer * 2];
		SineOscillator osc1 = new SineOscillator( 200.0, 44100 );
		SineOscillator osc2 = new SineOscillator( 300.0, 44100 );
		int framesLeft = numFrames;
		while( framesLeft > 0 )
		{
			int index = 0;
			int framesToWrite = (framesLeft > framesPerBuffer) ? framesPerBuffer
					: framesLeft;
			for( int j = 0; j < framesToWrite; j++ )
			{
				buffer[index++] = (short) (osc1.next() * 32767);
				buffer[index++] = (short) (osc2.next() * 32767);
			}
			stream.write( buffer, framesToWrite );
			framesLeft -= framesToWrite;
		}
	}

	public void testBlockingWriteShort()
	{
		PortAudio.initialize();

		StreamParameters streamParameters = new StreamParameters();
		streamParameters.sampleFormat = PortAudio.FORMAT_INT_16;
		streamParameters.channelCount = 2;
		streamParameters.device = PortAudio.getDefaultOutputDevice();
		streamParameters.suggestedLatency = PortAudio
				.getDeviceInfo( streamParameters.device ).defaultLowOutputLatency;
		System.out.println( "suggestedLatency = "
				+ streamParameters.suggestedLatency );

		int framesPerBuffer = 256;
		int flags = 0;
		BlockingStream stream = PortAudio.openStream( null, streamParameters,
				44100, framesPerBuffer, flags );
		assertTrue( "got default stream", stream != null );

		int numFrames = 80000;
		stream.start();
		long startTime = System.currentTimeMillis();
		writeSineDataShort( stream, framesPerBuffer, numFrames );
		stream.stop();
		long stopTime = System.currentTimeMillis();
		stream.close();

		double elapsed = (stopTime - startTime) / 1000.0;
		double expected = numFrames / 44100.0;
		assertEquals( "elapsed time to play", expected, elapsed, 0.20 );
		PortAudio.terminate();
	}

	public void testRecordPlayFloat() throws InterruptedException
	{
		checkRecordPlay( PortAudio.FORMAT_FLOAT_32 );
	}

	public void testRecordPlayShort() throws InterruptedException
	{
		checkRecordPlay( PortAudio.FORMAT_INT_16 );
	}

	public void checkRecordPlay( int sampleFormat ) throws InterruptedException
	{
		int framesPerBuffer = 256;
		int flags = 0;
		int sampleRate = 44100;
		int numFrames = sampleRate * 3;
		float[] floatBuffer = null;
		short[] shortBuffer = null;

		PortAudio.initialize();
		StreamParameters inParameters = new StreamParameters();
		inParameters.sampleFormat = sampleFormat;
		inParameters.device = PortAudio.getDefaultInputDevice();

		DeviceInfo info = PortAudio.getDeviceInfo( inParameters.device );
		inParameters.channelCount = (info.maxInputChannels > 2) ? 2
				: info.maxInputChannels;
		System.out.println( "channelCount = " + inParameters.channelCount );
		inParameters.suggestedLatency = PortAudio
				.getDeviceInfo( inParameters.device ).defaultLowInputLatency;

		if( sampleFormat == PortAudio.FORMAT_FLOAT_32 )
		{
			floatBuffer = new float[numFrames * inParameters.channelCount];
		}
		else if( sampleFormat == PortAudio.FORMAT_INT_16 )
		{
			shortBuffer = new short[numFrames * inParameters.channelCount];
		}
		// Record a few seconds of audio.
		BlockingStream inStream = PortAudio.openStream( inParameters, null,
				sampleRate, framesPerBuffer, flags );

		System.out.println( "RECORDING - say something like testing 1,2,3..." );
		inStream.start();

		if( sampleFormat == PortAudio.FORMAT_FLOAT_32 )
		{
			inStream.read( floatBuffer, numFrames );
		}
		else if( sampleFormat == PortAudio.FORMAT_INT_16 )
		{
			inStream.read( shortBuffer, numFrames );
		}
		Thread.sleep( 100 );
		int availableToRead = inStream.getReadAvailable();
		System.out.println( "availableToRead =  " + availableToRead );
		assertTrue( "getReadAvailable ", availableToRead > 0 );

		inStream.stop();
		inStream.close();
		System.out.println( "Finished recording. Begin Playback." );

		// Play back what we recorded.
		StreamParameters outParameters = new StreamParameters();
		outParameters.sampleFormat = sampleFormat;
		outParameters.channelCount = inParameters.channelCount;
		outParameters.device = PortAudio.getDefaultOutputDevice();
		outParameters.suggestedLatency = PortAudio
				.getDeviceInfo( outParameters.device ).defaultLowOutputLatency;

		BlockingStream outStream = PortAudio.openStream( null, outParameters,
				sampleRate, framesPerBuffer, flags );
		assertTrue( "got default stream", outStream != null );

		assertEquals( "inStream isActive", false, inStream.isActive() );

		outStream.start();
		Thread.sleep( 100 );
		int availableToWrite = outStream.getWriteAvailable();
		System.out.println( "availableToWrite =  " + availableToWrite );
		assertTrue( "getWriteAvailable ", availableToWrite > 0 );

		System.out.println( "inStream = " + inStream );
		System.out.println( "outStream = " + outStream );
		assertEquals( "inStream isActive", false, inStream.isActive() );
		assertEquals( "outStream isActive", true, outStream.isActive() );
		if( sampleFormat == PortAudio.FORMAT_FLOAT_32 )
		{
			outStream.write( floatBuffer, numFrames );
		}
		else if( sampleFormat == PortAudio.FORMAT_INT_16 )
		{
			outStream.write( shortBuffer, numFrames );
		}
		outStream.stop();

		outStream.close();
		PortAudio.terminate();
	}
}
