/*
 *	SampleRateConversionProvider.java
 *
 *	This file is part of Tritonus: http://www.tritonus.org/
 */

/*
 *  Copyright (c) 2001,2006 by Florian Bomers <http://www.bomers.de>
 *
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

/*
|<---            this code is formatted to fit into 80 columns             --->|
*/

package org.tritonus.sampled.convert;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;

import org.tritonus.share.TDebug;
import org.tritonus.share.sampled.AudioFormats;
import org.tritonus.share.sampled.FloatSampleBuffer;
import org.tritonus.share.sampled.FloatSampleInput;
import org.tritonus.share.sampled.AudioUtils;
import org.tritonus.share.sampled.convert.TSimpleFormatConversionProvider;
import org.tritonus.share.ArraySet;

/**
 * This provider converts sample rate of 2 PCM streams. <br>
 * It does:
 * <ul><li>conversion of different sample rates
 * <li>conversion of unsigned/signed (only 8bit unsigned supported)
 * <li>conversion of big/small endian
 * <li>8,16,24,32 bit conversion
 * </ul>
 * It does NOT:
 * <ul><li>change channel count
 * <li>accept a stream where the sample rates are equal. This case should be handled
 * by the PCM2PCM converter
 * </ul>
 *
 * @author Florian Bomers
 */

public class SampleRateConversionProvider
extends TSimpleFormatConversionProvider {

	// only used as abbreviation
	public static AudioFormat.Encoding PCM_SIGNED=AudioFormat.Encoding.PCM_SIGNED;
	public static AudioFormat.Encoding PCM_UNSIGNED=AudioFormat.Encoding.PCM_UNSIGNED;

	private static final boolean DEBUG_STREAM=false;
	private static final boolean DEBUG_STREAM_PROBLEMS=false;

	private static final int ALL=AudioSystem.NOT_SPECIFIED;
	private static final AudioFormat[]	OUTPUT_FORMATS = {
	    // Encoding, SampleRate, sampleSizeInBits, channels, frameSize, frameRate, bigEndian
	    new AudioFormat(PCM_SIGNED, ALL, 8, ALL, ALL, ALL, false),
	    new AudioFormat(PCM_SIGNED, ALL, 8, ALL, ALL, ALL, true),
	    new AudioFormat(PCM_UNSIGNED, ALL, 8, ALL, ALL, ALL, false),
	    new AudioFormat(PCM_UNSIGNED, ALL, 8, ALL, ALL, ALL, true),
	    new AudioFormat(PCM_SIGNED, ALL, 16, ALL, ALL, ALL, false),
	    new AudioFormat(PCM_SIGNED, ALL, 16, ALL, ALL, ALL, true),
	    new AudioFormat(PCM_SIGNED, ALL, 24, ALL, ALL, ALL, false),
	    new AudioFormat(PCM_SIGNED, ALL, 24, ALL, ALL, ALL, true),
	    new AudioFormat(PCM_SIGNED, ALL, 32, ALL, ALL, ALL, false),
	    new AudioFormat(PCM_SIGNED, ALL, 32, ALL, ALL, ALL, true),
	};

	/**	Constructor.
	 */
	public SampleRateConversionProvider() {
		super(Arrays.asList(OUTPUT_FORMATS),
		      Arrays.asList(OUTPUT_FORMATS));
	}

	public AudioInputStream getAudioInputStream(AudioFormat targetFormat, AudioInputStream sourceStream) {
		AudioFormat sourceFormat=sourceStream.getFormat();

		// the non-conversion case
		if (AudioFormats.matches(sourceFormat, targetFormat)) {
			return sourceStream;
		}

		targetFormat=replaceNotSpecified(sourceFormat, targetFormat);
		// do not support NOT_SPECIFIED as sample rates
		if (targetFormat.getSampleRate()!=AudioSystem.NOT_SPECIFIED
			&& sourceFormat.getSampleRate()!=AudioSystem.NOT_SPECIFIED
			&& targetFormat.getChannels()!=AudioSystem.NOT_SPECIFIED
			&& sourceFormat.getChannels()!=AudioSystem.NOT_SPECIFIED
			&& targetFormat.getSampleSizeInBits()!=AudioSystem.NOT_SPECIFIED
			&& sourceFormat.getSampleSizeInBits()!=AudioSystem.NOT_SPECIFIED
			&& isConversionSupported(sourceFormat, targetFormat)) {
			return new SampleRateConverterStream(sourceStream, targetFormat);
		}
		throw new IllegalArgumentException("format conversion not supported");
	}

	// replaces the sample rate and frame rate.
	// Should only be used with PCM_SIGNED or PCM_UNSIGNED
	private static AudioFormat replaceSampleRate(AudioFormat format, float newSampleRate) {
		if (format.getSampleRate()==newSampleRate) {
			return format;
		}
		return new AudioFormat(format.getEncoding(), newSampleRate, format.getSampleSizeInBits(),
			format.getChannels(), format.getFrameSize(), newSampleRate, format.isBigEndian());
	}

	private static final float[] commonSampleRates={
		8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 56000, 64000, 88200, 96000, 192000
	};

	public AudioFormat[] getTargetFormats(AudioFormat.Encoding targetEncoding,
	                                      AudioFormat sourceFormat)
	{
		if (TDebug.TraceAudioConverter) {
			TDebug.out(">SampleRateConversionProvider.getTargetFormats(AudioFormat.Encoding, AudioFormat):");
			TDebug.out("checking out possible target formats");
			TDebug.out("from: " + sourceFormat);
			TDebug.out("to  : " + targetEncoding);
		}
		float sourceSampleRate=sourceFormat.getSampleRate();
		// a trick: set sourceFormat's sample rate to -1 so that
		//          replaceNotSpecified does not replace the sample rate.
		//          we want to convert that !
		sourceFormat=replaceSampleRate(sourceFormat, AudioSystem.NOT_SPECIFIED);
		if (isConversionSupported(targetEncoding, sourceFormat)) {
			ArraySet<AudioFormat> result=new ArraySet<AudioFormat>();
			Iterator<AudioFormat> iterator=getCollectionTargetFormats().iterator();
			while (iterator.hasNext()) {
				AudioFormat targetFormat = iterator.next();
				targetFormat=replaceNotSpecified(sourceFormat, targetFormat);
				if (isConversionSupported(targetFormat, sourceFormat)) {
					result.add(targetFormat);
				}
			}
			// for convenience, add some often used sample rates as output
			// this may help applications that do not handle NOT_SPECIFIED
			if (result.size()>0
				&& sourceSampleRate!=AudioSystem.NOT_SPECIFIED) {
				int count=result.size();
				for (int i=0; i<count; i++) {
					AudioFormat format = result.get(i);
					for (int j=0; j<commonSampleRates.length; j++) {
						if (!doMatch(sourceSampleRate, commonSampleRates[j])) {
							result.add(replaceSampleRate(format, commonSampleRates[j]));
						}
					}
				}
			}
			if (TDebug.TraceAudioConverter) {
				TDebug.out("<found "+result.size()+" matching formats.");
			}
			return result.toArray(EMPTY_FORMAT_ARRAY);
		} else {
			if (TDebug.TraceAudioConverter) {
				TDebug.out("<returning empty array.");
			}
			return EMPTY_FORMAT_ARRAY;
		}
	}


	// overriden
	public boolean isConversionSupported(AudioFormat targetFormat,
	                                     AudioFormat sourceFormat) {
		// do not match when targetSampleRate set and sourceSamplerate set and NOT both the same
		boolean result=(targetFormat.getSampleRate()==AudioSystem.NOT_SPECIFIED
				|| targetFormat.getSampleRate()==AudioSystem.NOT_SPECIFIED
				|| !doMatch(targetFormat.getSampleRate(), sourceFormat.getSampleRate())
			&& doMatch(targetFormat.getChannels(), sourceFormat.getChannels()))
			&& AudioUtils.containsFormat(sourceFormat, getCollectionSourceFormats().iterator())
			&& AudioUtils.containsFormat(targetFormat, getCollectionTargetFormats().iterator());
		if (TDebug.TraceAudioConverter) {
			TDebug.out(">SampleRateConverter: isConversionSupported(AudioFormat, AudioFormat):");
			TDebug.out("checking if conversion possible");
			TDebug.out("from: " + sourceFormat);
			TDebug.out("to  : " + targetFormat);
			TDebug.out("< result : " + result);
		}
		return result;
	}

	protected static long convertLength(AudioFormat sourceFormat, AudioFormat targetFormat, long sourceLength) {
		if (sourceLength==AudioSystem.NOT_SPECIFIED) {
			return sourceLength;
		}
		return Math.round(targetFormat.getSampleRate()/sourceFormat.getSampleRate()*sourceLength);
	}

	protected static long convertLength(float sourceSR, float targetSR, long sourceLength) {
		if (sourceLength==AudioSystem.NOT_SPECIFIED) {
			return sourceLength;
		}
		return Math.round(targetSR/sourceSR*sourceLength);
		//return (long) (targetFormat.getSampleRate()/sourceFormat.getSampleRate()*sourceLength);
	}

	/**
	 * SampleRateConverterStream
	 */
	// at the moment, there are so many special things to care
	// about, and new things in an AIS, that I derive directly from
	// AudioInputStream.
	// I cannot use TAsynchronousFilteredAudioInputStream because
	// - it doesn't allow convenient use of a history. The history will be needed
	//   especially when performing filtering
	// - it doesn't work on FloatSampleBuffer (yet)
	// - each sample must be calculated one-by-one. The asynchronous
	//   difficulty isn't overcome by using a TCircularBuffer
	// I cannot use TSynchronousFilteredAudioInputStream because
	// - it doesn't handle different sample rates
	// Later we can make a base class for this, e.g. THistoryAudioInputStream

	// TODO: when target sample rate is < source sample rate (or only slightly above),
	// this stream calculates ONE sample too much.
	public static class SampleRateConverterStream extends AudioInputStream implements FloatSampleInput {

		/** the current working buffer with samples of the sourceStream */
		private FloatSampleBuffer thisBuffer=null;
		/** used when read(byte[],int,int) is called */
		private FloatSampleBuffer writeBuffer=null;
		private byte[] byteBuffer; // used for reading samples of sourceStream
		private AudioInputStream sourceStream;
		private FloatSampleInput sourceInput;
		private float sourceSampleRate;
		private float targetSampleRate;
		private long sourceFrameLength;
		/** index in thisBuffer */
		private double dPos;
		/** Conversion algorithm */
		public static final int SAMPLE_AND_HOLD=1;
		/** Conversion algorithm */
		public static final int LINEAR_INTERPOLATION=2;
		/** Conversion algorithm */
		public static final int RESAMPLE=3;
		
		private boolean eofReached = false;

		/** source stream is read in buffers of this size - in milliseconds */
		private int sourceBufferTime;

		/** the current conversion algorithm */
		private int conversionAlgorithm=LINEAR_INTERPOLATION;
		//private int conversionAlgorithm=SAMPLE_AND_HOLD;

		// History support
		/** the buffer with history samples */
		private FloatSampleBuffer historyBuffer=null;
		/** the minimum number of samples that must be present in the history buffer */
		private int minimumSamplesInHistory=1;

		/** force to discard current contents in thisBuffer if true */
		private boolean thisBufferValid=false;

		public SampleRateConverterStream(AudioInputStream sourceStream, AudioFormat targetFormat) {
			// clean up targetFormat:
			// - ignore frame rate totally
			// - recalculate frame size
			super (sourceStream, new SRCAudioFormat(targetFormat),
			           convertLength(sourceStream.getFormat(), targetFormat, sourceStream.getFrameLength()));
			if (TDebug.TraceAudioConverter) {
				TDebug.out("SampleRateConverterStream: <init>");
			}
			this.sourceStream=sourceStream;
			if (sourceStream instanceof FloatSampleInput) {
				sourceInput = (FloatSampleInput) sourceStream; 
			} else {
				this.sourceInput = null;
			}
			sourceSampleRate=sourceStream.getFormat().getSampleRate();
			targetSampleRate=targetFormat.getSampleRate();
			sourceFrameLength = sourceStream.getFrameLength();
			dPos=0;
			// use a buffer size of 100ms
			sourceBufferTime=100;
			resizeBuffers();
			flush(); // force read of source stream next time read is called
		}

		public SampleRateConverterStream(FloatSampleInput sourceInput, AudioFormat targetFormat, long frameLength) {
			// clean up targetFormat:
			// - ignore frame rate totally
			// - recalculate frame size
			super (new ByteArrayInputStream(new byte[0]), 
					new SRCAudioFormat(targetFormat),
			           convertLength(sourceInput.getSampleRate(), 
			        		   targetFormat.getSampleRate(), 
			        		   frameLength));
			if (TDebug.TraceAudioConverter) {
				TDebug.out("SampleRateConverterStream: <init>");
			}
			this.sourceStream = null;
			this.sourceInput = sourceInput;;
			sourceSampleRate = sourceInput.getSampleRate();
			targetSampleRate = targetFormat.getSampleRate();
			sourceFrameLength = frameLength;
			dPos=0;
			// use a buffer size of 100ms
			sourceBufferTime=100;
			resizeBuffers();
			flush(); // force read of source stream next time read is called
		}

		
		/**
		 * Assures that both historyBuffer and working buffer
		 * <ul><li>exist
		 * <li>have about <code>sourceBufferTime</code> ms samples
		 * <li>that both have at least <code>minimumSamplesInHistory</code>
		 * samples
		 * </ul>
		 * This method must be called when anything is changed that
		 * may change the size of the buffers.
		 */
		private void resizeBuffers() {
			int bufferSize=(int) AudioUtils.millis2Frames(
				(long) sourceBufferTime, sourceSampleRate);
			if (bufferSize<minimumSamplesInHistory) {
				bufferSize=minimumSamplesInHistory;
			}
			// we must be able to calculate at least one output sample from
			// one input buffer block
			if (bufferSize<outSamples2inSamples(1)) {
				bufferSize=((int) outSamples2inSamples(1)) + 1;
			}
			if (historyBuffer==null) {
				historyBuffer=new FloatSampleBuffer(getFormat().getChannels(),
					bufferSize, sourceSampleRate);
				historyBuffer.makeSilence();
			}
			// TODO: retain last samples !
			historyBuffer.changeSampleCount(bufferSize, true);
			if (thisBuffer==null) {
				thisBuffer=new FloatSampleBuffer(getFormat().getChannels(),
					bufferSize, sourceSampleRate);
			}
			// TODO: retain last samples and adjust dPos
			thisBuffer.changeSampleCount(bufferSize, true);
		}

		/**
		 * Reads from a source stream that cannot handle float buffers.
		 * After this method has been called, it is to be checked whether
		 * we are closed !
		 * Precondition: sourceStream!=null
		 */
		private void readFromByteSourceStream() {
			int byteCount=thisBuffer.getByteArrayBufferSize(getFormat());
			if (byteBuffer==null || byteBuffer.length<byteCount) {
				byteBuffer=new byte[byteCount];
			}
			// finally read it
			int bytesRead=0;
			int thisRead;
			do {
				try {
					thisRead = sourceStream.read(byteBuffer, bytesRead,
							byteCount - bytesRead);
				} catch (IOException ioe) {
					thisRead = -1;
				}
				if (thisRead > 0) {
					bytesRead += thisRead;
				}
			} while (bytesRead < byteCount && thisRead > 0);
			if (bytesRead==0) {
				// sourceStream is closed. We don't accept 0 bytes read from source stream
				close();
			} else {
				thisBuffer.initFromByteArray(byteBuffer, 0, bytesRead, sourceStream.getFormat());
			}
		}

		/** pre-condition: sourceInput != null */
		private void readFromSourceInput() {
			sourceInput.read(thisBuffer);
			if (sourceInput.isDone()) {
				close();
			}
		}

private long testInFramesRead=0;
private long testOutFramesReturned=0;

		/**
		 * fills thisBuffer with new samples.
		 * It sets the history buffer to the last buffer.
		 * thisBuffer's sampleCount will be the number of samples read.
		 * Calling methods MUST check whether this stream is closed upon
		 * completion of this method. If the stream is closed, the contents
		 * of <code>thisBuffer</code> are not valid.
		 */
		private void readFromSourceStream() {
			if (isClosed()) {
				return;
			}
			int oldSampleCount=thisBuffer.getSampleCount();
			// reuse history buffer
			FloatSampleBuffer newBuffer=historyBuffer;
			historyBuffer=thisBuffer;
			thisBuffer=newBuffer;
			if (sourceFrameLength!=AudioSystem.NOT_SPECIFIED
				&& thisBuffer.getSampleCount()+testInFramesRead>sourceFrameLength) {
					if (sourceFrameLength-testInFramesRead<=0) {
						close();
						return;
					}
					thisBuffer.changeSampleCount((int) (sourceFrameLength-testInFramesRead), false);
			}

			if (sourceInput != null) {
				readFromSourceInput();
			} else {
				readFromByteSourceStream();
			}
			if (TDebug.TraceAudioConverter) {
				testInFramesRead+=thisBuffer.getSampleCount();
				if (DEBUG_STREAM) {
					TDebug.out("Read "+thisBuffer.getSampleCount()+" frames from source stream. Total="+testInFramesRead);
				}
			}
			double inc=outSamples2inSamples(1.0);
			if (!thisBufferValid) {
				thisBufferValid=true;
				dPos=0.0;
			} else {
				double temp = dPos;
				dPos -= (double) oldSampleCount;
				if (DEBUG_STREAM) {
					TDebug.out("new dPos: "+temp+" - "+oldSampleCount+" = "+dPos);
				}
				if ((dPos>inc || dPos<-inc) && ((int) dPos)!=0) {
					// hard-reset dPos if - why ever - it got out of bounds
					if (DEBUG_STREAM_PROBLEMS) {
						TDebug.out("Need to hard reset dPos="+dPos+" !");
					}
					dPos=0.0;
				}
			}
		}

		protected void convertSampleAndHold1(
				float[] inSamples, double inSampleOffset, int inSampleCount, double increment,
				float[] outSamples, int outSampleOffset, int outSampleCount, float[] history, int historyLength) {
			if (DEBUG_STREAM) {
				TDebug.out("convertSampleAndHold1(inSamples["+inSamples.length+"], "
					+((int) inSampleOffset)+" to "+((int) (inSampleOffset+increment*(outSampleCount-1)))+", "
					+"outSamples["+outSamples.length+"], "+outSampleOffset+" to "+(outSampleOffset+outSampleCount-1)+")");
				System.out.flush();
			}
			for (int i=0; i<outSampleCount; i++) {
				int iInIndex=(int) (inSampleOffset+increment*i);
				if (iInIndex<0) {
					outSamples[i+outSampleOffset]=history[iInIndex+historyLength];
					if (DEBUG_STREAM) {
						TDebug.out("convertSampleAndHold: using history["+(iInIndex+historyLength)+" because inIndex="+iInIndex);
					}
				}
				else if (iInIndex>=inSampleCount) {
					if (DEBUG_STREAM_PROBLEMS) {
						TDebug.out("convertSampleAndHold: INDEX OUT OF BOUNDS outSamples["+i+"]=inSamples[roundDown("+inSampleOffset+")="+iInIndex+"];");
					}
				} else {
					outSamples[i+outSampleOffset]=inSamples[iInIndex];
					//outSamples[i]=inSamples[roundDown(inSampleOffset)];
				}
				//inSampleOffset+=increment; <- this produces too much rounding errors...
			}
		}

		/**
		 * optimized version
		 * @param inSamples
		 * @param inSampleOffset
		 * @param inSampleCount
		 * @param increment
		 * @param outSamples
		 * @param outSampleOffset
		 * @param outSampleCount
		 * @param history
		 * @param historyLength
		 */
		private void convertSampleAndHold2(
				float[] inSamples, double inSampleOffset, int inSampleCount, double increment,
				float[] outSamples, int outSampleOffset, int outSampleCount, float[] history, int historyLength) {
			if (DEBUG_STREAM) {
				TDebug.out("convertSampleAndHold2(inSamples["+inSamples.length+"], "
					+((int) inSampleOffset)+" to "+((int) (inSampleOffset+increment*(outSampleCount-1)))+", "
					+"outSamples["+outSamples.length+"], "+outSampleOffset+" to "+(outSampleOffset+outSampleCount-1)+")");
				System.out.flush();
			}
			int endSampleOffset = outSampleOffset + outSampleCount;
			// first go through the history
			double dHistoryLength = (double) historyLength;
			while (inSampleOffset < 0.0d && outSampleOffset < endSampleOffset) {
				double dInIndex = (inSampleOffset + dHistoryLength);
				outSamples[outSampleOffset] = history[(int) dInIndex];
				inSampleOffset += increment;
				outSampleOffset++;
			}

			// then go through the remaining new samples
			while (outSampleOffset < endSampleOffset) {
				outSamples[outSampleOffset] = inSamples[(int) inSampleOffset];
				
				inSampleOffset += increment;
				outSampleOffset++;
			}
		}

		protected void convertLinearInterpolation1(
				float[] inSamples, double inSampleOffset, int inSampleCount, double increment,
				float[] outSamples, int outSampleOffset, int outSampleCount, float[] history, int historyLength) {
			if (DEBUG_STREAM) {
				TDebug.out("convertLinearInterpolate1(inSamples["+inSamples.length+"], "
					+((int) inSampleOffset)+" to "+((int) (inSampleOffset+increment*(outSampleCount-1)))+", "
					+"outSamples["+outSamples.length+"], "+outSampleOffset+" to "+(outSampleOffset+outSampleCount-1)+")");
				System.out.flush();
			}
			for (int i=0; i<outSampleCount; i++) {
				try {
					double dInIndex=inSampleOffset+increment*i-1;
					int iInIndex=(int) Math.floor(dInIndex);
					double factor=1.0d-(dInIndex-iInIndex);
					float value=0;
					for (int x=0; x<2; x++) {
						if (iInIndex>=inSampleCount) {
							// we clearly need more samples !
							if (DEBUG_STREAM_PROBLEMS) {
								TDebug.out("linear interpolation: INDEX OUT OF BOUNDS iInIndex="+iInIndex+" inSampleCount="+inSampleCount);
							}
						}
						else if (iInIndex<0) {
							int histIndex=iInIndex+historyLength;
							if (histIndex>=0) {
								value+=history[histIndex]*factor;
								if (DEBUG_STREAM) {
									TDebug.out("linear interpolation: using history["+iInIndex+"]");
								}
							}
							else if (DEBUG_STREAM_PROBLEMS) {
								TDebug.out("linear interpolation: history INDEX OUT OF BOUNDS iInIndex="+iInIndex+" histIndex="+histIndex+" history length="+historyLength);
							}
						} else {
							value+=inSamples[iInIndex]*factor;
						}
						factor=1-factor;
						iInIndex++;
					}
					outSamples[i+outSampleOffset]=value;
					//outSamples[i]=inSamples[roundDown(inSampleOffset)];
				} catch (ArrayIndexOutOfBoundsException aioobe) {
					if (DEBUG_STREAM_PROBLEMS) {
						TDebug.out("**** REAL INDEX OUT OF BOUNDS ****** outSamples["+i+"]=inSamples[roundDown("+inSampleOffset+")="+((int) inSampleOffset)+"];");
					}
					//throw aioobe;
				}
				//inSampleOffset+=increment; <- this produces too much rounding errors...
			}
		}

		/**
		 * optimized version of the linear interpolator
		 * @param inSamples
		 * @param inSampleOffset
		 * @param inSampleCount
		 * @param increment
		 * @param outSamples
		 * @param outSampleOffset
		 * @param outSampleCount
		 * @param history
		 * @param historyLength
		 */
		private void convertLinearInterpolation2(float[] inSamples,
				double inSampleOffset, int inSampleCount, double increment,
				float[] outSamples, int outSampleOffset, int outSampleCount,
				float[] history, int historyLength) {
			// cast results:
			// (int) -1.7d=-1 (int) -1.5d=-1 (int) -1.2d=-1 (int) -1.0d=-1 (int)
			// -0.7d=0 (int) -0.5d=0 (int) -0.2d=0
			if (DEBUG_STREAM) {
				TDebug.out("convertLinearInterpolate2(inSamples["
						+ inSamples.length
						+ "], "
						+((int) inSampleOffset)+" to "+((int) (inSampleOffset+increment*(outSampleCount-1)))+", "
						+ "outSamples["
						+ outSamples.length + "], " + outSampleOffset + " to "
						+ (outSampleOffset + outSampleCount - 1) + ")");
				System.out.flush();
			}

			try {
				int endSampleOffset = outSampleOffset + outSampleCount;

				// first go through the history
				double dHistoryLength = (double) historyLength;
				while (inSampleOffset < 0.0d && outSampleOffset < endSampleOffset) {
					double dInIndex = (inSampleOffset + dHistoryLength);
					int histIndex = (int) dInIndex;
					float factor = (float) (dInIndex - histIndex);

					outSamples[outSampleOffset] = (history[histIndex - 1] * (1.0f - factor))
							+ (history[histIndex] * factor);

					inSampleOffset += increment;
					outSampleOffset++;
				}

				// then the transition area: last sample is in history, new sample in inSamples
				while (inSampleOffset < 1.0d && outSampleOffset < endSampleOffset) {
					float factor = (float) inSampleOffset;
					outSamples[outSampleOffset] = (history[historyLength - 1] * (1.0f - factor))
							+ (inSamples[0] * factor);

					inSampleOffset += increment;
					outSampleOffset++;
				}

				// then go through the remaining new samples
				while (outSampleOffset < endSampleOffset) {
					int iInIndex = (int) inSampleOffset;
					float factor = (float) (inSampleOffset - iInIndex);

					outSamples[outSampleOffset] = (inSamples[iInIndex-1] * (1.0f - factor))
							+ (inSamples[iInIndex] * factor);

					inSampleOffset += increment;
					outSampleOffset++;
				}

			} catch (ArrayIndexOutOfBoundsException aioobe) {
				if (DEBUG_STREAM_PROBLEMS) {
					TDebug.out("**** INDEX OUT OF BOUNDS ****** inSampleOffset="
							+ inSampleOffset
							+ "  inSamples.length="
							+ inSamples.length
							+ "  outSampleOffset="
							+ outSampleOffset
					+ "  outSamples.length="
					+ outSamples.length);
				}
				//throw aioobe;
			}
		}

		private double inSamples2outSamples(double inSamples) {
			return inSamples*targetSampleRate/sourceSampleRate;
		}

		private double outSamples2inSamples(double outSamples) {
			return outSamples*sourceSampleRate/targetSampleRate;
		}

		
		// interface FloatSampleInput
		
		/*
		 * (non-Javadoc)
		 * 
		 * @see org.tritonus.share.sampled.FloatSampleInput#getChannels()
		 */
		public int getChannels() {
			return getFormat().getChannels();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.tritonus.share.sampled.FloatSampleInput#getSampleRate()
		 */
		public float getSampleRate() {
			return getFormat().getSampleRate();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.tritonus.share.sampled.FloatSampleInput#isDone()
		 */
		public boolean isDone() {
			return isClosed();
		}

		public void read(FloatSampleBuffer outBuffer) {
			read(outBuffer, 0, outBuffer.getSampleCount());
		}

		/**
		 * Main read method. It blocks until all samples are converted or
		 * the source stream is at its end or closed.<br>
		 * The sourceStream's sample rate is converted following
		 * the current setting of <code>conversionAlgorithm</code>.
		 * At most outBuffer.getSampleCount() are converted. In general,
		 * if outBuffer.getSampleCount()) is less
		 * after processing this function, then it is an indicator
		 * that it was the last block to be processed.
		 *
		 * @see #setConversionAlgorithm(int)
		 * @param outBuffer the buffer that the converted samples will be written to.
		 * @throws IllegalArgumentException when outBuffer's channel count does not match
		 */
		public void read(FloatSampleBuffer outBuffer, int offset, int count) {
			if (isClosed() || count == 0) {
				outBuffer.setSampleCount(offset, true);
				return;
			}
			if (outBuffer.getChannelCount()!=thisBuffer.getChannelCount()) {
				throw new IllegalArgumentException("passed buffer has different channel count");
			}
			if (TDebug.TraceAudioConverter) {
				TDebug.out(">SamplerateConverterStream.read("+count+" samples)");
			}
			float[] outSamples;
			float[] inSamples;
			float[] history;
			double increment=outSamples2inSamples(1.0);
			int writtenSamples=0;
			do {
				// check thisBuffer with samples of source stream
				int inSampleCount=thisBuffer.getSampleCount();
				if (((int) dPos) >= inSampleCount || !thisBufferValid) {
					// need to load new data of sourceStream
					readFromSourceStream();
					if (isClosed()) {
						break;
					}
					inSampleCount=thisBuffer.getSampleCount();
				}
				// calculate number of samples to write
				int writeCount=count-writtenSamples;
				// check whether this exceeds the current in-buffer
				if (((int) (outSamples2inSamples((double) writeCount)+dPos))>=inSampleCount) {
					int lastOutIndex=((int) (inSamples2outSamples(((double) inSampleCount)-dPos)))+1;
					// normally, the above formula gives the exact writeCount.
					// but due to rounding issues, sometimes it has to be decremented once.
					// so we need to iterate to get the last index and then increment it once to make
					// it the writeCount (=the number of samples to write)
					while ((int) (outSamples2inSamples((double) lastOutIndex)+dPos)>=inSampleCount) {
						lastOutIndex--;
						if (DEBUG_STREAM) {
							TDebug.out("--------- Decremented lastOutIndex="+lastOutIndex);
						}
					}
					if (DEBUG_STREAM_PROBLEMS) {
						int testLastOutIndex=writeCount-1;
						if (DEBUG_STREAM_PROBLEMS) {
							while ((int) (outSamples2inSamples((double) testLastOutIndex)+dPos)>=inSampleCount) {
								testLastOutIndex--;
							}
						}
						if (testLastOutIndex!=lastOutIndex) {
							TDebug.out("lastOutIndex wrong: lastOutIndex="+lastOutIndex+" testLastOutIndex="+testLastOutIndex+" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
						}
					}
					writeCount=lastOutIndex+1;
				}
				// finally do the actual conversion - separated per channel
				for (int channel=0; channel<outBuffer.getChannelCount(); channel++) {
					inSamples=thisBuffer.getChannel(channel);
					outSamples=outBuffer.getChannel(channel);
					history=historyBuffer.getChannel(channel);
					switch (conversionAlgorithm) {
						case SAMPLE_AND_HOLD: 
							convertSampleAndHold2(inSamples, dPos, inSampleCount, increment,
								outSamples, writtenSamples + offset, writeCount, history, historyBuffer.getSampleCount()); 
							break;
						case LINEAR_INTERPOLATION: 
							convertLinearInterpolation2(inSamples, dPos, inSampleCount, increment,
								outSamples, writtenSamples + offset, writeCount, history, historyBuffer.getSampleCount()); 
							break;
					}
				}
				writtenSamples+=writeCount;
				// adjust new position
				dPos+=outSamples2inSamples((double) writeCount);
			} while (!isClosed() && writtenSamples<outBuffer.getSampleCount());
			if (writtenSamples<count) {
				outBuffer.changeSampleCount(writtenSamples+offset, true);
			}
			if (TDebug.TraceAudioConverter) {
				testOutFramesReturned+=outBuffer.getSampleCount();
				TDebug.out("< return "+outBuffer.getSampleCount()+"frames. Total="+testOutFramesReturned+" frames. Read total "+testInFramesRead+" frames from source stream");
			}
		}


		//////////////////// utility methods ////////////////////////

		protected double sourceFrames2targetFrames(double sourceFrames) {
			return targetSampleRate/sourceSampleRate*sourceFrames;
		}

		protected double targetFrames2sourceFrames(double targetFrames) {
			return sourceSampleRate/targetSampleRate*targetFrames;
		}

		protected long sourceBytes2targetBytes(long sourceBytes) {
			long sourceFrames=sourceBytes/getSourceFrameSize();
			long targetFrames=(long) sourceFrames2targetFrames(sourceFrames);
			return targetFrames*getFrameSize();
		}

		protected long targetBytes2sourceBytes(long targetBytes) {
			long targetFrames=targetBytes/getFrameSize();
			long sourceFrames=(long) targetFrames2sourceFrames(targetFrames);
			return sourceFrames*getSourceFrameSize();
		}

		public int getFrameSize() {
			return getFormat().getFrameSize();
		}

		public int getSourceFrameSize() {
			return sourceStream!=null?sourceStream.getFormat().getFrameSize():1;
		}

		//////////////////// methods overwritten of AudioInputStream ////////////////////////

		public int read() throws IOException {
			if (getFormat().getFrameSize() != 1) {
				throw new IOException("frame size must be 1 to read a single byte");
			}
			// very ugly, but efficient. Who uses this method anyway ?
			byte[] temp = new byte[1];
			int result = read(temp);
			if (result <= 0) {
				return -1;
			}
			return temp[0] & 0xFF;
		}

		/**
		 * @see #read(byte[], int, int)
		 */
		public int read(byte[] abData) throws IOException {
			return read(abData, 0, abData.length);
		}

		/**
		 * Read nLength bytes that will be the converted samples
		 * of the original inputStream.
		 * When nLength is not an integral number of frames,
		 * this method may read less than nLength bytes.
		 */
		public int read(byte[] abData, int nOffset, int nLength)
		throws IOException {
			if (isClosed()) {
				return -1;
			}
			int frameCount=nLength/getFrameSize();
			if (writeBuffer==null) {
				writeBuffer=new FloatSampleBuffer(getFormat().getChannels(), frameCount, getFormat().getSampleRate());
			} else {
				writeBuffer.changeSampleCount(frameCount, false);
			}
			read(writeBuffer);
			if (eofReached) {
				return -1;
			}
			int written=writeBuffer.convertToByteArray(abData, nOffset, getFormat());
			return written;
		}

		public synchronized long skip(long nSkip) throws IOException {
			// only returns integral frames
			long sourceSkip = targetBytes2sourceBytes(nSkip);
			long sourceSkipped = sourceStream!=null?sourceStream.skip(sourceSkip):0;
			flush();
			return sourceBytes2targetBytes(sourceSkipped);
		}


		public int available() throws IOException {
			if (sourceStream==null) {
				return -1;
			}
			return (int) sourceBytes2targetBytes(sourceStream.available());
		}


		public void mark(int readlimit) {
			if (sourceStream!=null) {
				sourceStream.mark((int) targetBytes2sourceBytes(readlimit));
			}
		}

		public synchronized void reset() throws IOException {
			if (sourceStream!=null) {
				sourceStream.reset();
				flush();
			}
		}

		public boolean markSupported() {
			if (sourceStream!=null) {
				return sourceStream.markSupported();
			}
			return false;
		}

		public void close() {
			if (isClosed()) {
				return;
			}
			if (sourceStream != null) {
				try {
					sourceStream.close();
				} catch (IOException ioe) {
				}
			}
			eofReached = true;
			// clean memory, this will also be an indicator that
			// the stream is closed
			thisBuffer=null;
			historyBuffer=null;
			byteBuffer=null;
		}

		///////////////////////////// additional methods /////////////////////////////

		public boolean isClosed() {
			return eofReached || (thisBuffer==null);
		}

		/**
		 * Flushes the internal buffers
		 */
		public synchronized void flush() {
			if (!isClosed()) {
				thisBufferValid=false;
				historyBuffer.makeSilence();
			}
		}

		/////////////////////////// Properties ///////////////////////////////////////

		public synchronized void setTargetSampleRate(float sr) {
			if (sr>0) {
				targetSampleRate=sr;
				//((SRCAudioFormat) getFormat()).setSampleRate(sr);
				resizeBuffers();
			}
		}

		public synchronized void setConversionAlgorithm(int algo) {
			if ((algo==SAMPLE_AND_HOLD || algo==LINEAR_INTERPOLATION)
				&& (algo!=conversionAlgorithm)) {
					conversionAlgorithm=algo;
					resizeBuffers();
			}
		}

		public synchronized float getTargetSampleRate() {
			return targetSampleRate;
		}

		public synchronized int getConversionAlgorithm() {
			return conversionAlgorithm;
		}

	}


	/**
	   Obviously, this class is used to be able to set the
	   frame rate/sample rate after the AudioFormat object
	   has been created. It assumes the PCM case where the
	   frame rate is always in sync with the sample rate.
	   (MP)
	*/
	public static class SRCAudioFormat extends AudioFormat {
		private float sampleRate;

		public SRCAudioFormat(AudioFormat targetFormat) {
			super(targetFormat.getEncoding(),
				targetFormat.getSampleRate(),
				targetFormat.getSampleSizeInBits(),
				targetFormat.getChannels(),
				targetFormat.getChannels()*targetFormat.getSampleSizeInBits()/8,
				targetFormat.getSampleRate(),
				targetFormat.isBigEndian());
			this.sampleRate=targetFormat.getSampleRate();
		}

		public void setSampleRate(float sr) {
			if (sr>0) {
				this.sampleRate=sr;
			}
		}

		public float getSampleRate() {
			return this.sampleRate;
		}

		public float getFrameRate() {
			return this.sampleRate;
		}
	}
}

/*** SampleRateConversionProvider.java ***/
