Commit abcd177e by andrewlewis Committed by Oliver Woodman

Remove some unused Sonic functionality.

Also move it closer to the ExoPlayer code style.

Note: This change is intended to be purely cosmetic.

Issue: #26

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=151307575
parent 9d20a8d4
...@@ -16,329 +16,129 @@ ...@@ -16,329 +16,129 @@
*/ */
package com.google.android.exoplayer2.audio; package com.google.android.exoplayer2.audio;
import com.google.android.exoplayer2.util.Assertions;
import java.util.Arrays;
/** /**
* Sonic audio time/pitch stretching library. Based on https://github.com/waywardgeek/sonic. * Sonic audio stream processor for time/pitch stretching.
* <p>
* Based on https://github.com/waywardgeek/sonic.
*/ */
/* package */ final class Sonic { /* package */ final class Sonic {
private static final int SONIC_MIN_PITCH = 65; private static final boolean USE_CHORD_PITCH = false;
private static final int SONIC_MAX_PITCH = 400; private static final int MINIMUM_PITCH = 65;
/* This is used to down-sample some inputs to improve speed */ private static final int MAXIMUM_PITCH = 400;
private static final int SONIC_AMDF_FREQ = 4000; private static final int AMDF_FREQUENCY = 4000;
private final int sampleRate;
private final int numChannels;
private final int minPeriod;
private final int maxPeriod;
private final int maxRequired;
private final short[] downSampleBuffer;
private int inputBufferSize;
private short[] inputBuffer; private short[] inputBuffer;
private int outputBufferSize;
private short[] outputBuffer; private short[] outputBuffer;
private int pitchBufferSize;
private short[] pitchBuffer; private short[] pitchBuffer;
private short[] downSampleBuffer;
private float speed;
private float volume;
private float pitch;
private float rate;
private int oldRatePosition; private int oldRatePosition;
private int newRatePosition; private int newRatePosition;
private boolean useChordPitch; private float speed;
private int quality; private float pitch;
private int numChannels;
private int inputBufferSize;
private int pitchBufferSize;
private int outputBufferSize;
private int numInputSamples; private int numInputSamples;
private int numOutputSamples; private int numOutputSamples;
private int numPitchSamples; private int numPitchSamples;
private int minPeriod;
private int maxPeriod;
private int maxRequired;
private int remainingInputToCopy; private int remainingInputToCopy;
private int sampleRate;
private int prevPeriod; private int prevPeriod;
private int prevMinDiff; private int prevMinDiff;
private int minDiff; private int minDiff;
private int maxDiff; private int maxDiff;
// Resize the array. /**
private short[] resize(short[] oldArray, int newLength) { * Creates a new Sonic audio stream processor.
newLength *= numChannels; *
short[] newArray = new short[newLength]; * @param sampleRate The sample rate of input audio.
int length = Math.min(oldArray.length, newLength); * @param numChannels The number of channels in the input audio.
*/
System.arraycopy(oldArray, 0, newArray, 0, length); public Sonic(int sampleRate, int numChannels) {
return newArray; this.sampleRate = sampleRate;
} this.numChannels = numChannels;
minPeriod = sampleRate / MAXIMUM_PITCH;
// Move samples from one array to another. May move samples down within an array, but not up. maxPeriod = sampleRate / MINIMUM_PITCH;
private void move(short[] dest, int destPos, short[] source, int sourcePos, int numSamples) {
System.arraycopy(
source, sourcePos * numChannels, dest, destPos * numChannels, numSamples * numChannels);
}
// Scale the samples by the factor.
private void scaleSamples(short[] samples, int position, int numSamples, float volume) {
int fixedPointVolume = (int) (volume * 4096.0f);
int start = position * numChannels;
int stop = start + numSamples * numChannels;
for (int xSample = start; xSample < stop; xSample++) {
int value = (samples[xSample] * fixedPointVolume) >> 12;
if (value > 32767) {
value = 32767;
} else if (value < -32767) {
value = -32767;
}
samples[xSample] = (short) value;
}
}
// Get the speed of the stream.
public float getSpeed() {
return speed;
}
// Set the speed of the stream.
public void setSpeed(float speed) {
this.speed = speed;
}
// Get the pitch of the stream.
public float getPitch() {
return pitch;
}
// Set the pitch of the stream.
public void setPitch(float pitch) {
this.pitch = pitch;
}
// Get the rate of the stream.
public float getRate() {
return rate;
}
// Set the playback rate of the stream. This scales pitch and speed at the same time.
public void setRate(float rate) {
this.rate = rate;
this.oldRatePosition = 0;
this.newRatePosition = 0;
}
// Get the vocal chord pitch setting.
public boolean getChordPitch() {
return useChordPitch;
}
// Set the vocal chord mode for pitch computation. Default is off.
public void setChordPitch(boolean useChordPitch) {
this.useChordPitch = useChordPitch;
}
// Get the quality setting.
public int getQuality() {
return quality;
}
// Set the "quality". Default 0 is virtually as good as 1, but very much faster.
public void setQuality(int quality) {
this.quality = quality;
}
// Get the scaling factor of the stream.
public float getVolume() {
return volume;
}
// Set the scaling factor of the stream.
public void setVolume(float volume) {
this.volume = volume;
}
// Allocate stream buffers.
private void allocateStreamBuffers(int sampleRate, int numChannels) {
minPeriod = sampleRate / SONIC_MAX_PITCH;
maxPeriod = sampleRate / SONIC_MIN_PITCH;
maxRequired = 2 * maxPeriod; maxRequired = 2 * maxPeriod;
downSampleBuffer = new short[maxRequired];
inputBufferSize = maxRequired; inputBufferSize = maxRequired;
inputBuffer = new short[maxRequired * numChannels]; inputBuffer = new short[maxRequired * numChannels];
outputBufferSize = maxRequired; outputBufferSize = maxRequired;
outputBuffer = new short[maxRequired * numChannels]; outputBuffer = new short[maxRequired * numChannels];
pitchBufferSize = maxRequired; pitchBufferSize = maxRequired;
pitchBuffer = new short[maxRequired * numChannels]; pitchBuffer = new short[maxRequired * numChannels];
downSampleBuffer = new short[maxRequired];
this.sampleRate = sampleRate;
this.numChannels = numChannels;
oldRatePosition = 0; oldRatePosition = 0;
newRatePosition = 0; newRatePosition = 0;
prevPeriod = 0; prevPeriod = 0;
}
// Create a sonic stream.
public Sonic(int sampleRate, int numChannels) {
allocateStreamBuffers(sampleRate, numChannels);
speed = 1.0f; speed = 1.0f;
pitch = 1.0f; pitch = 1.0f;
volume = 1.0f;
rate = 1.0f;
oldRatePosition = 0;
newRatePosition = 0;
useChordPitch = false;
quality = 0;
}
// Get the sample rate of the stream.
public int getSampleRate() {
return sampleRate;
}
// Set the sample rate of the stream. This will cause samples buffered in the stream to be lost.
public void setSampleRate(int sampleRate) {
allocateStreamBuffers(sampleRate, numChannels);
}
// Get the number of channels.
public int getNumChannels() {
return numChannels;
}
// Set the num channels of the stream. This will cause samples buffered in the stream to be lost.
public void setNumChannels(int numChannels) {
allocateStreamBuffers(sampleRate, numChannels);
}
// Enlarge the output buffer if needed.
private void enlargeOutputBufferIfNeeded(int numSamples) {
if (numOutputSamples + numSamples > outputBufferSize) {
outputBufferSize += (outputBufferSize >> 1) + numSamples;
outputBuffer = resize(outputBuffer, outputBufferSize);
}
} }
// Enlarge the input buffer if needed. /**
private void enlargeInputBufferIfNeeded(int numSamples) { * Sets the output speed.
if (numInputSamples + numSamples > inputBufferSize) { */
inputBufferSize += (inputBufferSize >> 1) + numSamples; public void setSpeed(float speed) {
inputBuffer = resize(inputBuffer, inputBufferSize); this.speed = speed;
}
} }
// Add the input samples to the input buffer. /**
private void addFloatSamplesToInputBuffer(float[] samples, int numSamples) { * Gets the output speed.
if (numSamples == 0) { */
return; public float getSpeed() {
} return speed;
enlargeInputBufferIfNeeded(numSamples);
int xBuffer = numInputSamples * numChannels;
for (int xSample = 0; xSample < numSamples * numChannels; xSample++) {
inputBuffer[xBuffer++] = (short) (samples[xSample] * 32767.0f);
}
numInputSamples += numSamples;
} }
// Add the input samples to the input buffer. /**
private void addShortSamplesToInputBuffer(short[] samples, int numSamples) { * Sets the output pitch.
if (numSamples == 0) { */
return; public void setPitch(float pitch) {
} this.pitch = pitch;
enlargeInputBufferIfNeeded(numSamples);
move(inputBuffer, numInputSamples, samples, 0, numSamples);
numInputSamples += numSamples;
} }
// Add the input samples to the input buffer. /**
private void addUnsignedByteSamplesToInputBuffer(byte[] samples, int numSamples) { * Gets the output pitch.
short sample; */
public float getPitch() {
enlargeInputBufferIfNeeded(numSamples); return pitch;
int xBuffer = numInputSamples * numChannels;
for (int xSample = 0; xSample < numSamples * numChannels; xSample++) {
sample = (short) ((samples[xSample] & 0xff) - 128); // Convert from unsigned to signed
inputBuffer[xBuffer++] = (short) (sample << 8);
}
numInputSamples += numSamples;
} }
// Add the input samples to the input buffer. They must be 16-bit little-endian encoded in a byte /**
// array. * Writes {@code numBytes} from {@code buffer} as input.
private void addBytesToInputBuffer(byte[] inBuffer, int numBytes) { *
* @param buffer A buffer containing input data.
* @param numBytes The number of bytes of input data to read from {@code buffer}.
*/
public void writeBytesToStream(byte[] buffer, int numBytes) {
int numSamples = numBytes / (2 * numChannels); int numSamples = numBytes / (2 * numChannels);
short sample; short sample;
enlargeInputBufferIfNeeded(numSamples); enlargeInputBufferIfNeeded(numSamples);
int xBuffer = numInputSamples * numChannels; int xBuffer = numInputSamples * numChannels;
for (int xByte = 0; xByte + 1 < numBytes; xByte += 2) { for (int xByte = 0; xByte + 1 < numBytes; xByte += 2) {
sample = (short) ((inBuffer[xByte] & 0xff) | (inBuffer[xByte + 1] << 8)); sample = (short) ((buffer[xByte] & 0xff) | (buffer[xByte + 1] << 8));
inputBuffer[xBuffer++] = sample; inputBuffer[xBuffer++] = sample;
} }
numInputSamples += numSamples; numInputSamples += numSamples;
processStreamInput();
} }
// Remove input samples that we have already processed. /**
private void removeInputSamples(int position) { * Reads up to {@code maxBytes} of output into {@code buffer}.
int remainingSamples = numInputSamples - position; *
* @param buffer The buffer into which output will be written.
move(inputBuffer, 0, inputBuffer, position, remainingSamples); * @param maxBytes The maximum number of bytes to write.
numInputSamples = remainingSamples; * @return The number of bytes read from the stream.
} */
public int readBytesFromStream(byte[] buffer, int maxBytes) {
// Just copy from the array to the output buffer
private void copyToOutput(short[] samples, int position, int numSamples) {
enlargeOutputBufferIfNeeded(numSamples);
move(outputBuffer, numOutputSamples, samples, position, numSamples);
numOutputSamples += numSamples;
}
// Just copy from the input buffer to the output buffer. Return num samples copied.
private int copyInputToOutput(int position) {
int numSamples = remainingInputToCopy;
if (numSamples > maxRequired) {
numSamples = maxRequired;
}
copyToOutput(inputBuffer, position, numSamples);
remainingInputToCopy -= numSamples;
return numSamples;
}
// Read data out of the stream. Sometimes no data will be available, and zero
// is returned, which is not an error condition.
public int readFloatFromStream(float[] samples, int maxSamples) {
int numSamples = numOutputSamples;
int remainingSamples = 0;
if (numSamples == 0) {
return 0;
}
if (numSamples > maxSamples) {
remainingSamples = numSamples - maxSamples;
numSamples = maxSamples;
}
for (int xSample = 0; xSample < numSamples * numChannels; xSample++) {
samples[xSample++] = (outputBuffer[xSample]) / 32767.0f;
}
move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples);
numOutputSamples = remainingSamples;
return numSamples;
}
// Read short data out of the stream. Sometimes no data will be available, and zero
// is returned, which is not an error condition.
public int readShortFromStream(short[] samples, int maxSamples) {
int numSamples = numOutputSamples;
int remainingSamples = 0;
if (numSamples == 0) {
return 0;
}
if (numSamples > maxSamples) {
remainingSamples = numSamples - maxSamples;
numSamples = maxSamples;
}
move(samples, 0, outputBuffer, 0, numSamples);
move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples);
numOutputSamples = remainingSamples;
return numSamples;
}
// Read unsigned byte data out of the stream. Sometimes no data will be available, and zero
// is returned, which is not an error condition.
public int readBytesFromStream(byte[] outBuffer, int maxBytes) {
int maxSamples = maxBytes / (2 * numChannels); int maxSamples = maxBytes / (2 * numChannels);
int numSamples = numOutputSamples; int numSamples = numOutputSamples;
int remainingSamples = 0; int remainingSamples = 0;
...@@ -352,23 +152,24 @@ package com.google.android.exoplayer2.audio; ...@@ -352,23 +152,24 @@ package com.google.android.exoplayer2.audio;
} }
for (int xSample = 0; xSample < numSamples * numChannels; xSample++) { for (int xSample = 0; xSample < numSamples * numChannels; xSample++) {
short sample = outputBuffer[xSample]; short sample = outputBuffer[xSample];
outBuffer[xSample << 1] = (byte) (sample & 0xff); buffer[xSample << 1] = (byte) (sample & 0xff);
outBuffer[(xSample << 1) + 1] = (byte) (sample >> 8); buffer[(xSample << 1) + 1] = (byte) (sample >> 8);
} }
move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples); System.arraycopy(outputBuffer, numSamples * numChannels, outputBuffer, 0,
remainingSamples * numChannels);
numOutputSamples = remainingSamples; numOutputSamples = remainingSamples;
return 2 * numSamples * numChannels; return 2 * numSamples * numChannels;
} }
// Force the sonic stream to generate output using whatever data it currently /**
// has. No extra delay will be added to the output, but flushing in the middle of * Forces generating output using whatever data has been queued already. No extra delay will be
// words could introduce distortion. * added to the output, but flushing in the middle of words could introduce distortion.
*/
public void flushStream() { public void flushStream() {
int remainingSamples = numInputSamples; int remainingSamples = numInputSamples;
float s = speed / pitch; float s = speed / pitch;
float r = rate * pitch;
int expectedOutputSamples = int expectedOutputSamples =
numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / r + 0.5f); numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / pitch + 0.5f);
// Add enough silence to flush both input and pitch buffers. // Add enough silence to flush both input and pitch buffers.
enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired); enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired);
...@@ -376,7 +177,7 @@ package com.google.android.exoplayer2.audio; ...@@ -376,7 +177,7 @@ package com.google.android.exoplayer2.audio;
inputBuffer[remainingSamples * numChannels + xSample] = 0; inputBuffer[remainingSamples * numChannels + xSample] = 0;
} }
numInputSamples += 2 * maxRequired; numInputSamples += 2 * maxRequired;
writeShortToStream(null, 0); processStreamInput();
// Throw away any extra samples we generated due to the silence we added. // Throw away any extra samples we generated due to the silence we added.
if (numOutputSamples > expectedOutputSamples) { if (numOutputSamples > expectedOutputSamples) {
numOutputSamples = expectedOutputSamples; numOutputSamples = expectedOutputSamples;
...@@ -387,22 +188,59 @@ package com.google.android.exoplayer2.audio; ...@@ -387,22 +188,59 @@ package com.google.android.exoplayer2.audio;
numPitchSamples = 0; numPitchSamples = 0;
} }
// Return the number of samples in the output buffer /**
* Returns the number of output samples that can be read with
* {@link #readBytesFromStream(byte[], int)}.
*/
public int samplesAvailable() { public int samplesAvailable() {
return numOutputSamples; return numOutputSamples;
} }
// If skip is greater than one, average skip samples together and write them to // Internal methods.
// the down-sample buffer. If numChannels is greater than one, mix the channels
// together as we down sample. private void enlargeOutputBufferIfNeeded(int numSamples) {
if (numOutputSamples + numSamples > outputBufferSize) {
outputBufferSize += (outputBufferSize / 2) + numSamples;
outputBuffer = Arrays.copyOf(outputBuffer, outputBufferSize * numChannels);
}
}
private void enlargeInputBufferIfNeeded(int numSamples) {
if (numInputSamples + numSamples > inputBufferSize) {
inputBufferSize += (inputBufferSize / 2) + numSamples;
inputBuffer = Arrays.copyOf(inputBuffer, inputBufferSize * numChannels);
}
}
private void removeProcessedInputSamples(int position) {
int remainingSamples = numInputSamples - position;
System.arraycopy(inputBuffer, position * numChannels, inputBuffer, 0,
remainingSamples * numChannels);
numInputSamples = remainingSamples;
}
private void copyToOutput(short[] samples, int position, int numSamples) {
enlargeOutputBufferIfNeeded(numSamples);
System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels,
numSamples * numChannels);
numOutputSamples += numSamples;
}
private int copyInputToOutput(int position) {
int numSamples = Math.min(maxRequired, remainingInputToCopy);
copyToOutput(inputBuffer, position, numSamples);
remainingInputToCopy -= numSamples;
return numSamples;
}
private void downSampleInput(short[] samples, int position, int skip) { private void downSampleInput(short[] samples, int position, int skip) {
// If skip is greater than one, average skip samples together and write them to the down-sample
// buffer. If numChannels is greater than one, mix the channels together as we down sample.
int numSamples = maxRequired / skip; int numSamples = maxRequired / skip;
int samplesPerValue = numChannels * skip; int samplesPerValue = numChannels * skip;
int value;
position *= numChannels; position *= numChannels;
for (int i = 0; i < numSamples; i++) { for (int i = 0; i < numSamples; i++) {
value = 0; int value = 0;
for (int j = 0; j < samplesPerValue; j++) { for (int j = 0; j < samplesPerValue; j++) {
value += samples[position + i * samplesPerValue + j]; value += samples[position + i * samplesPerValue + j];
} }
...@@ -411,14 +249,13 @@ package com.google.android.exoplayer2.audio; ...@@ -411,14 +249,13 @@ package com.google.android.exoplayer2.audio;
} }
} }
// Find the best frequency match in the range, and given a sample skip multiple.
// For now, just find the pitch of the first channel.
private int findPitchPeriodInRange(short[] samples, int position, int minPeriod, int maxPeriod) { private int findPitchPeriodInRange(short[] samples, int position, int minPeriod, int maxPeriod) {
// Find the best frequency match in the range, and given a sample skip multiple. For now, just
// find the pitch of the first channel.
int bestPeriod = 0; int bestPeriod = 0;
int worstPeriod = 255; int worstPeriod = 255;
int minDiff = 1; int minDiff = 1;
int maxDiff = 0; int maxDiff = 0;
position *= numChannels; position *= numChannels;
for (int period = minPeriod; period <= maxPeriod; period++) { for (int period = minPeriod; period <= maxPeriod; period++) {
int diff = 0; int diff = 0;
...@@ -428,7 +265,7 @@ package com.google.android.exoplayer2.audio; ...@@ -428,7 +265,7 @@ package com.google.android.exoplayer2.audio;
diff += sVal >= pVal ? sVal - pVal : pVal - sVal; diff += sVal >= pVal ? sVal - pVal : pVal - sVal;
} }
// Note that the highest number of samples we add into diff will be less than 256, since we // Note that the highest number of samples we add into diff will be less than 256, since we
// skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples // skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples
// without overflow. // without overflow.
if (diff * bestPeriod < minDiff * period) { if (diff * bestPeriod < minDiff * period) {
minDiff = diff; minDiff = diff;
...@@ -441,13 +278,14 @@ package com.google.android.exoplayer2.audio; ...@@ -441,13 +278,14 @@ package com.google.android.exoplayer2.audio;
} }
this.minDiff = minDiff / bestPeriod; this.minDiff = minDiff / bestPeriod;
this.maxDiff = maxDiff / worstPeriod; this.maxDiff = maxDiff / worstPeriod;
return bestPeriod; return bestPeriod;
} }
// At abrupt ends of voiced words, we can have pitch periods that are better /**
// approximated by the previous pitch period estimate. Try to detect this case. * Returns whether the previous pitch period estimate is a better approximation, which can occur
private boolean prevPeriodBetter(int minDiff, int maxDiff, boolean preferNewPeriod) { * at the abrupt end of voiced words.
*/
private boolean previousPeriodBetter(int minDiff, int maxDiff, boolean preferNewPeriod) {
if (minDiff == 0 || prevPeriod == 0) { if (minDiff == 0 || prevPeriod == 0) {
return false; return false;
} }
...@@ -468,18 +306,14 @@ package com.google.android.exoplayer2.audio; ...@@ -468,18 +306,14 @@ package com.google.android.exoplayer2.audio;
return true; return true;
} }
// Find the pitch period. This is a critical step, and we may have to try
// multiple ways to get a good answer. This version uses AMDF. To improve
// speed, we down sample by an integer factor get in the 11KHz range, and then
// do it again with a narrower frequency range without down sampling
private int findPitchPeriod(short[] samples, int position, boolean preferNewPeriod) { private int findPitchPeriod(short[] samples, int position, boolean preferNewPeriod) {
// Find the pitch period. This is a critical step, and we may have to try multiple ways to get a
// good answer. This version uses AMDF. To improve speed, we down sample by an integer factor
// get in the 11 kHz range, and then do it again with a narrower frequency range without down
// sampling.
int period; int period;
int retPeriod; int retPeriod;
int skip = 1; int skip = sampleRate > AMDF_FREQUENCY ? sampleRate / AMDF_FREQUENCY : 1;
if (sampleRate > SONIC_AMDF_FREQ && quality == 0) {
skip = sampleRate / SONIC_AMDF_FREQ;
}
if (numChannels == 1 && skip == 1) { if (numChannels == 1 && skip == 1) {
period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod); period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod);
} else { } else {
...@@ -487,8 +321,8 @@ package com.google.android.exoplayer2.audio; ...@@ -487,8 +321,8 @@ package com.google.android.exoplayer2.audio;
period = findPitchPeriodInRange(downSampleBuffer, 0, minPeriod / skip, maxPeriod / skip); period = findPitchPeriodInRange(downSampleBuffer, 0, minPeriod / skip, maxPeriod / skip);
if (skip != 1) { if (skip != 1) {
period *= skip; period *= skip;
int minP = period - (skip << 2); int minP = period - (skip * 4);
int maxP = period + (skip << 2); int maxP = period + (skip * 4);
if (minP < minPeriod) { if (minP < minPeriod) {
minP = minPeriod; minP = minPeriod;
} }
...@@ -503,7 +337,7 @@ package com.google.android.exoplayer2.audio; ...@@ -503,7 +337,7 @@ package com.google.android.exoplayer2.audio;
} }
} }
} }
if (prevPeriodBetter(minDiff, maxDiff, preferNewPeriod)) { if (previousPeriodBetter(minDiff, maxDiff, preferNewPeriod)) {
retPeriod = prevPeriod; retPeriod = prevPeriod;
} else { } else {
retPeriod = period; retPeriod = period;
...@@ -513,93 +347,44 @@ package com.google.android.exoplayer2.audio; ...@@ -513,93 +347,44 @@ package com.google.android.exoplayer2.audio;
return retPeriod; return retPeriod;
} }
// Overlap two sound segments, ramp the volume of one down, while ramping the
// other one from zero up, and add them, storing the result at the output.
private static void overlapAdd(int numSamples, int numChannels, short[] out, int outPos,
short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) {
for (int i = 0; i < numChannels; i++) {
int o = outPos * numChannels + i;
int u = rampUpPos * numChannels + i;
int d = rampDownPos * numChannels + i;
for (int t = 0; t < numSamples; t++) {
out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * t) / numSamples);
o += numChannels;
d += numChannels;
u += numChannels;
}
}
}
// Overlap two sound segments, ramp the volume of one down, while ramping the
// other one from zero up, and add them, storing the result at the output.
private static void overlapAddWithSeparation(int numSamples, int numChannels, int separation,
short[] out, int outPos, short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) {
for (int i = 0; i < numChannels; i++) {
int o = outPos * numChannels + i;
int u = rampUpPos * numChannels + i;
int d = rampDownPos * numChannels + i;
for (int t = 0; t < numSamples + separation; t++) {
if (t < separation) {
out[o] = (short) (rampDown[d] * (numSamples - t) / numSamples);
d += numChannels;
} else if (t < numSamples) {
out[o] =
(short) ((rampDown[d] * (numSamples - t) + rampUp[u] * (t - separation))
/ numSamples);
d += numChannels;
u += numChannels;
} else {
out[o] = (short) (rampUp[u] * (t - separation) / numSamples);
u += numChannels;
}
o += numChannels;
}
}
}
// Just move the new samples in the output buffer to the pitch buffer
private void moveNewSamplesToPitchBuffer(int originalNumOutputSamples) { private void moveNewSamplesToPitchBuffer(int originalNumOutputSamples) {
int numSamples = numOutputSamples - originalNumOutputSamples; int numSamples = numOutputSamples - originalNumOutputSamples;
if (numPitchSamples + numSamples > pitchBufferSize) { if (numPitchSamples + numSamples > pitchBufferSize) {
pitchBufferSize += (pitchBufferSize >> 1) + numSamples; pitchBufferSize += (pitchBufferSize / 2) + numSamples;
pitchBuffer = resize(pitchBuffer, pitchBufferSize); pitchBuffer = Arrays.copyOf(pitchBuffer, pitchBufferSize * numChannels);
} }
move(pitchBuffer, numPitchSamples, outputBuffer, originalNumOutputSamples, numSamples); System.arraycopy(outputBuffer, originalNumOutputSamples * numChannels, pitchBuffer,
numPitchSamples * numChannels, numSamples * numChannels);
numOutputSamples = originalNumOutputSamples; numOutputSamples = originalNumOutputSamples;
numPitchSamples += numSamples; numPitchSamples += numSamples;
} }
// Remove processed samples from the pitch buffer.
private void removePitchSamples(int numSamples) { private void removePitchSamples(int numSamples) {
if (numSamples == 0) { if (numSamples == 0) {
return; return;
} }
move(pitchBuffer, 0, pitchBuffer, numSamples, numPitchSamples - numSamples); System.arraycopy(pitchBuffer, numSamples * numChannels, pitchBuffer, 0,
(numPitchSamples - numSamples) * numChannels);
numPitchSamples -= numSamples; numPitchSamples -= numSamples;
} }
// Change the pitch. The latency this introduces could be reduced by looking at
// past samples to determine pitch, rather than future.
private void adjustPitch(int originalNumOutputSamples) { private void adjustPitch(int originalNumOutputSamples) {
int period; // Latency due to pitch changes could be reduced by looking at past samples to determine pitch,
int newPeriod; // rather than future.
int separation;
int position = 0;
if (numOutputSamples == originalNumOutputSamples) { if (numOutputSamples == originalNumOutputSamples) {
return; return;
} }
moveNewSamplesToPitchBuffer(originalNumOutputSamples); moveNewSamplesToPitchBuffer(originalNumOutputSamples);
int position = 0;
while (numPitchSamples - position >= maxRequired) { while (numPitchSamples - position >= maxRequired) {
period = findPitchPeriod(pitchBuffer, position, false); int period = findPitchPeriod(pitchBuffer, position, false);
newPeriod = (int) (period / pitch); int newPeriod = (int) (period / pitch);
enlargeOutputBufferIfNeeded(newPeriod); enlargeOutputBufferIfNeeded(newPeriod);
if (pitch >= 1.0f) { if (pitch >= 1.0f) {
overlapAdd(newPeriod, numChannels, outputBuffer, numOutputSamples, pitchBuffer, position, overlapAdd(newPeriod, numChannels, outputBuffer, numOutputSamples, pitchBuffer, position,
pitchBuffer, position + period - newPeriod); pitchBuffer, position + period - newPeriod);
} else { } else {
separation = newPeriod - period; int separation = newPeriod - period;
overlapAddWithSeparation(period, numChannels, separation, outputBuffer, numOutputSamples, overlapAddWithSeparation(period, numChannels, separation, outputBuffer, numOutputSamples,
pitchBuffer, position, pitchBuffer, position); pitchBuffer, position, pitchBuffer, position);
} }
...@@ -609,7 +394,6 @@ package com.google.android.exoplayer2.audio; ...@@ -609,7 +394,6 @@ package com.google.android.exoplayer2.audio;
removePitchSamples(position); removePitchSamples(position);
} }
// Interpolate the new output sample.
private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) { private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) {
short left = in[inPos * numChannels]; short left = in[inPos * numChannels];
short right = in[inPos * numChannels + numChannels]; short right = in[inPos * numChannels + numChannels];
...@@ -618,27 +402,23 @@ package com.google.android.exoplayer2.audio; ...@@ -618,27 +402,23 @@ package com.google.android.exoplayer2.audio;
int rightPosition = (oldRatePosition + 1) * newSampleRate; int rightPosition = (oldRatePosition + 1) * newSampleRate;
int ratio = rightPosition - position; int ratio = rightPosition - position;
int width = rightPosition - leftPosition; int width = rightPosition - leftPosition;
return (short) ((ratio * left + (width - ratio) * right) / width); return (short) ((ratio * left + (width - ratio) * right) / width);
} }
// Change the rate.
private void adjustRate(float rate, int originalNumOutputSamples) { private void adjustRate(float rate, int originalNumOutputSamples) {
if (numOutputSamples == originalNumOutputSamples) {
return;
}
int newSampleRate = (int) (sampleRate / rate); int newSampleRate = (int) (sampleRate / rate);
int oldSampleRate = sampleRate; int oldSampleRate = sampleRate;
int position; // Set these values to help with the integer math.
// Set these values to help with the integer math
while (newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) { while (newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) {
newSampleRate >>= 1; newSampleRate /= 2;
oldSampleRate >>= 1; oldSampleRate /= 2;
}
if (numOutputSamples == originalNumOutputSamples) {
return;
} }
moveNewSamplesToPitchBuffer(originalNumOutputSamples); moveNewSamplesToPitchBuffer(originalNumOutputSamples);
// Leave at least one pitch sample in the buffer // Leave at least one pitch sample in the buffer.
for (position = 0; position < numPitchSamples - 1; position++) { for (int position = 0; position < numPitchSamples - 1; position++) {
while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) { while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) {
enlargeOutputBufferIfNeeded(1); enlargeOutputBufferIfNeeded(1);
for (int i = 0; i < numChannels; i++) { for (int i = 0; i < numChannels; i++) {
...@@ -651,20 +431,16 @@ package com.google.android.exoplayer2.audio; ...@@ -651,20 +431,16 @@ package com.google.android.exoplayer2.audio;
oldRatePosition++; oldRatePosition++;
if (oldRatePosition == oldSampleRate) { if (oldRatePosition == oldSampleRate) {
oldRatePosition = 0; oldRatePosition = 0;
if (newRatePosition != newSampleRate) { Assertions.checkState(newRatePosition == newSampleRate);
System.out.printf("Assertion failed: newRatePosition != newSampleRate\n");
assert false;
}
newRatePosition = 0; newRatePosition = 0;
} }
} }
removePitchSamples(position); removePitchSamples(numPitchSamples - 1);
} }
// Skip over a pitch period, and copy period/speed samples to the output
private int skipPitchPeriod(short[] samples, int position, float speed, int period) { private int skipPitchPeriod(short[] samples, int position, float speed, int period) {
// Skip over a pitch period, and copy period/speed samples to the output.
int newSamples; int newSamples;
if (speed >= 2.0f) { if (speed >= 2.0f) {
newSamples = (int) (period / (speed - 1.0f)); newSamples = (int) (period / (speed - 1.0f));
} else { } else {
...@@ -678,10 +454,9 @@ package com.google.android.exoplayer2.audio; ...@@ -678,10 +454,9 @@ package com.google.android.exoplayer2.audio;
return newSamples; return newSamples;
} }
// Insert a pitch period, and determine how much input to copy directly.
private int insertPitchPeriod(short[] samples, int position, float speed, int period) { private int insertPitchPeriod(short[] samples, int position, float speed, int period) {
// Insert a pitch period, and determine how much input to copy directly.
int newSamples; int newSamples;
if (speed < 0.5f) { if (speed < 0.5f) {
newSamples = (int) (period * speed / (1.0f - speed)); newSamples = (int) (period * speed / (1.0f - speed));
} else { } else {
...@@ -689,129 +464,92 @@ package com.google.android.exoplayer2.audio; ...@@ -689,129 +464,92 @@ package com.google.android.exoplayer2.audio;
remainingInputToCopy = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed)); remainingInputToCopy = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed));
} }
enlargeOutputBufferIfNeeded(period + newSamples); enlargeOutputBufferIfNeeded(period + newSamples);
move(outputBuffer, numOutputSamples, samples, position, period); System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels,
period * numChannels);
overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples, overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples,
position + period, samples, position); position + period, samples, position);
numOutputSamples += period + newSamples; numOutputSamples += period + newSamples;
return newSamples; return newSamples;
} }
// Resample as many pitch periods as we have buffered on the input. Return 0 if
// we fail to resize an input or output buffer. Also scale the output by the volume.
private void changeSpeed(float speed) { private void changeSpeed(float speed) {
int numSamples = numInputSamples;
int position = 0;
int period;
int newSamples;
if (numInputSamples < maxRequired) { if (numInputSamples < maxRequired) {
return; return;
} }
int numSamples = numInputSamples;
int position = 0;
do { do {
if (remainingInputToCopy > 0) { if (remainingInputToCopy > 0) {
newSamples = copyInputToOutput(position); position += copyInputToOutput(position);
position += newSamples;
} else { } else {
period = findPitchPeriod(inputBuffer, position, true); int period = findPitchPeriod(inputBuffer, position, true);
if (speed > 1.0) { if (speed > 1.0) {
newSamples = skipPitchPeriod(inputBuffer, position, speed, period); position += period + skipPitchPeriod(inputBuffer, position, speed, period);
position += period + newSamples;
} else { } else {
newSamples = insertPitchPeriod(inputBuffer, position, speed, period); position += insertPitchPeriod(inputBuffer, position, speed, period);
position += newSamples;
} }
} }
} while (position + maxRequired <= numSamples); } while (position + maxRequired <= numSamples);
removeInputSamples(position); removeProcessedInputSamples(position);
} }
// Resample as many pitch periods as we have buffered on the input. Scale the output by the
// volume.
private void processStreamInput() { private void processStreamInput() {
// Resample as many pitch periods as we have buffered on the input.
int originalNumOutputSamples = numOutputSamples; int originalNumOutputSamples = numOutputSamples;
float s = speed / pitch; float s = speed / pitch;
float r = rate;
if (!useChordPitch) {
r *= pitch;
}
if (s > 1.00001 || s < 0.99999) { if (s > 1.00001 || s < 0.99999) {
changeSpeed(s); changeSpeed(s);
} else { } else {
copyToOutput(inputBuffer, 0, numInputSamples); copyToOutput(inputBuffer, 0, numInputSamples);
numInputSamples = 0; numInputSamples = 0;
} }
if (useChordPitch) { if (USE_CHORD_PITCH) {
if (pitch != 1.0f) { if (pitch != 1.0f) {
adjustPitch(originalNumOutputSamples); adjustPitch(originalNumOutputSamples);
} }
} else if (r != 1.0f) { } else if (!USE_CHORD_PITCH && pitch != 1.0f) {
adjustRate(r, originalNumOutputSamples); adjustRate(pitch, originalNumOutputSamples);
}
if (volume != 1.0f) {
// Adjust output volume.
scaleSamples(outputBuffer, originalNumOutputSamples,
numOutputSamples - originalNumOutputSamples, volume);
} }
} }
// Write floating point data to the input buffer and process it. private static void overlapAdd(int numSamples, int numChannels, short[] out, int outPos,
public void writeFloatToStream(float[] samples, int numSamples) { short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) {
addFloatSamplesToInputBuffer(samples, numSamples); for (int i = 0; i < numChannels; i++) {
processStreamInput(); int o = outPos * numChannels + i;
} int u = rampUpPos * numChannels + i;
int d = rampDownPos * numChannels + i;
// Write the data to the input stream, and process it. for (int t = 0; t < numSamples; t++) {
public void writeShortToStream(short[] samples, int numSamples) { out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * t) / numSamples);
addShortSamplesToInputBuffer(samples, numSamples); o += numChannels;
processStreamInput(); d += numChannels;
} u += numChannels;
}
// Simple wrapper around sonicWriteFloatToStream that does the unsigned byte to short }
// conversion for you.
public void writeUnsignedByteToStream(byte[] samples, int numSamples) {
addUnsignedByteSamplesToInputBuffer(samples, numSamples);
processStreamInput();
}
// Simple wrapper around sonicWriteBytesToStream that does the byte to 16-bit LE conversion.
public void writeBytesToStream(byte[] inBuffer, int numBytes) {
addBytesToInputBuffer(inBuffer, numBytes);
processStreamInput();
}
// This is a non-stream oriented interface to just change the speed of a sound sample
public static int changeFloatSpeed(float[] samples, int numSamples, float speed, float pitch,
float rate, float volume, boolean useChordPitch, int sampleRate, int numChannels) {
Sonic stream = new Sonic(sampleRate, numChannels);
stream.setSpeed(speed);
stream.setPitch(pitch);
stream.setRate(rate);
stream.setVolume(volume);
stream.setChordPitch(useChordPitch);
stream.writeFloatToStream(samples, numSamples);
stream.flushStream();
numSamples = stream.samplesAvailable();
stream.readFloatFromStream(samples, numSamples);
return numSamples;
} }
/* This is a non-stream oriented interface to just change the speed of a sound sample */ private static void overlapAddWithSeparation(int numSamples, int numChannels, int separation,
public int sonicChangeShortSpeed(short[] samples, int numSamples, float speed, float pitch, short[] out, int outPos, short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) {
float rate, float volume, boolean useChordPitch, int sampleRate, int numChannels) { for (int i = 0; i < numChannels; i++) {
Sonic stream = new Sonic(sampleRate, numChannels); int o = outPos * numChannels + i;
int u = rampUpPos * numChannels + i;
stream.setSpeed(speed); int d = rampDownPos * numChannels + i;
stream.setPitch(pitch); for (int t = 0; t < numSamples + separation; t++) {
stream.setRate(rate); if (t < separation) {
stream.setVolume(volume); out[o] = (short) (rampDown[d] * (numSamples - t) / numSamples);
stream.setChordPitch(useChordPitch); d += numChannels;
stream.writeShortToStream(samples, numSamples); } else if (t < numSamples) {
stream.flushStream(); out[o] =
numSamples = stream.samplesAvailable(); (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * (t - separation))
stream.readShortFromStream(samples, numSamples); / numSamples);
return numSamples; d += numChannels;
u += numChannels;
} else {
out[o] = (short) (rampUp[u] * (t - separation) / numSamples);
u += numChannels;
}
o += numChannels;
}
}
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment