/* * SampleRateConversionProvider.java * * This file is part of Tritonus: http://www.tritonus.org/ */ /* * Copyright (c) 2001,2006,2008 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)); } @Override 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(targetFormat, sourceFormat)) { 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 }; @Override 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; } } @Override 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 (long) (targetFormat.getSampleRate() / sourceFormat.getSampleRate() * sourceLength); } protected static long convertLength(float sourceSR, float targetSR, long sourceLength) { if (sourceLength == AudioSystem.NOT_SPECIFIED) { return sourceLength; } return (long) (targetSR / sourceSR * 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; /** source stream is read in buffers of this size - in samples */ private int sourceBufferSizeSamples; /** 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 synchronized void resizeBuffers() { sourceBufferSizeSamples = (int) AudioUtils.millis2Frames( (long) sourceBufferTime, sourceSampleRate); if (sourceBufferSizeSamples < minimumSamplesInHistory) { sourceBufferSizeSamples = minimumSamplesInHistory; } // we must be able to calculate at least one output sample from // one input buffer block if (sourceBufferSizeSamples < outSamples2inSamples(1)) { sourceBufferSizeSamples = ((int) outSamples2inSamples(1)) + 1; } if (historyBuffer == null) { historyBuffer = new FloatSampleBuffer( getFormat().getChannels(), sourceBufferSizeSamples, sourceSampleRate); historyBuffer.makeSilence(); } // TODO: retain last samples ! historyBuffer.changeSampleCount(sourceBufferSizeSamples, true); if (thisBuffer == null) { thisBuffer = new FloatSampleBuffer(getFormat().getChannels(), sourceBufferSizeSamples, sourceSampleRate); } // TODO: retain last samples and adjust dPos thisBuffer.changeSampleCount(sourceBufferSizeSamples, true); if (TDebug.TraceAudioConverter && DEBUG_STREAM) { TDebug.out("Initialized thisBuffer and historyBuffer with " + sourceBufferSizeSamples + " samples"); } } /** * Maintenance work before reading from the source stream. In * particular, it is ensured that the temporary buffer for reading from * the source buffer is large enough. */ private void beforeReadFromSourceStream() { FloatSampleBuffer lBuffer = thisBuffer; if (lBuffer != null && lBuffer.getSampleCount() != sourceBufferSizeSamples) { lBuffer.changeSampleCount(sourceBufferSizeSamples, false); } } /** * 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(sourceStream.getFormat()); if (byteBuffer == null || byteBuffer.length < byteCount) { byteBuffer = new byte[byteCount]; } if (TDebug.TraceAudioConverter && DEBUG_STREAM) { TDebug.out("in readFromByteSourceStream: trying to read " + byteCount + " bytes = " + (byteCount / sourceStream.getFormat().getFrameSize()) + " samples from source stream"); } // 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()); if (TDebug.TraceAudioConverter && DEBUG_STREAM) { TDebug.out("in readFromByteSourceStream: initialized thisBuffer with " + thisBuffer.getSampleCount() + " samples"); } } } /** pre-condition: sourceInput != null, thisBuffer.getSampleCount()>0 */ private void readFromSourceInput() { if (sourceInput.isDone()) { close(); } else { sourceInput.read(thisBuffer); } } 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; } // reuse history buffer FloatSampleBuffer lBuffer = historyBuffer; historyBuffer = thisBuffer; thisBuffer = lBuffer; beforeReadFromSourceStream(); int oldSampleCount = thisBuffer.getSampleCount(); // ensure that we don't read more than the source stream claimed to // have if (sourceFrameLength != AudioSystem.NOT_SPECIFIED && lBuffer.getSampleCount() + testInFramesRead > sourceFrameLength) { long remaining = sourceFrameLength - testInFramesRead; if (remaining <= 0) { if (TDebug.TraceAudioConverter && DEBUG_STREAM) { TDebug.out("Read more than allowed from source stream:" + " sourceFrameLength=" + sourceFrameLength + " samples, inFramesRead=" + testInFramesRead + " samples."); } close(); return; } if (TDebug.TraceAudioConverter && DEBUG_STREAM) { TDebug.out("Reading from source stream: change from " + lBuffer.getSampleCount() + " samples to" + remaining + " samples"); } lBuffer.changeSampleCount((int) remaining, false); } if (sourceInput != null) { readFromSourceInput(); } else { readFromByteSourceStream(); } int sampleCount = (lBuffer == null) ? 0 : lBuffer.getSampleCount(); testInFramesRead += sampleCount; if (TDebug.TraceAudioConverter && DEBUG_STREAM) { String src = (sourceInput != null) ? "source input" : "source byte stream"; TDebug.out("Read " + sampleCount + " frames from " + src + " (requested=" + oldSampleCount + "). Total=" + testInFramesRead); } double inc = outSamples2inSamples(1.0); if (!thisBufferValid) { thisBufferValid = true; dPos = 0.0; } else { double temp = dPos; dPos -= 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 = 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 = 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.TraceAllExceptions) { TDebug.out("**** INDEX OUT OF BOUNDS ****** inSampleOffset=" + inSampleOffset + " inSamples.length=" + inSamples.length + " outSampleOffset=" + outSampleOffset + " outSamples.length=" + outSamples.length); } if (TDebug.TraceAllExceptions) { aioobe.printStackTrace(); } // 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 */ @SuppressWarnings("cast") 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 && DEBUG_STREAM) { TDebug.out(">SamplerateConverterStream.read(" + count + " samples)"); } FloatSampleBuffer lSourceBuffer = thisBuffer; float[] outSamples; float[] inSamples; float[] history; double increment = outSamples2inSamples(1.0); int writtenSamples = 0; do { // check thisBuffer with samples of source stream int inSampleCount = lSourceBuffer.getSampleCount(); if (((int) dPos) >= inSampleCount || !thisBufferValid) { // need to load new data of sourceStream readFromSourceStream(); if (isClosed()) { break; } lSourceBuffer = thisBuffer; inSampleCount = thisBuffer.getSampleCount(); if (inSampleCount == 0) { // cannot read anything right now break; } } // 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 = lSourceBuffer.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 && DEBUG_STREAM) { 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 // //////////////////////// @Override 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) */ @Override 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. */ @Override 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 (writeBuffer.getSampleCount() == 0 && eofReached) { return -1; } int written = writeBuffer.convertToByteArray(abData, nOffset, getFormat()); return written; } @Override 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); } @Override public int available() throws IOException { if (sourceStream == null) { return -1; } return (int) sourceBytes2targetBytes(sourceStream.available()); } @Override public void mark(int readlimit) { if (sourceStream != null) { sourceStream.mark((int) targetBytes2sourceBytes(readlimit)); } } @Override public synchronized void reset() throws IOException { if (sourceStream != null) { sourceStream.reset(); flush(); } } @Override public boolean markSupported() { if (sourceStream != null) { return sourceStream.markSupported(); } return false; } @Override 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(), AudioUtils.getFrameSize( targetFormat.getChannels(), targetFormat.getSampleSizeInBits()), targetFormat.getSampleRate(), targetFormat.isBigEndian(), targetFormat.properties()); this.sampleRate = targetFormat.getSampleRate(); } public void setSampleRate(float sr) { if (sr > 0) { this.sampleRate = sr; } } @Override public float getSampleRate() { return this.sampleRate; } @Override public float getFrameRate() { return this.sampleRate; } } } /** * SampleRateConversionProvider.java ** */