mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Implement Sonic method to get input frame count from output frame count
The static method can estimate the number of input frames needed to get a given number of output frames with a given Sonic configuration. This CL is prework to remove the dependency of `SpeedChangingAudioProcessor#getMediaDurationUs()` on non-static output based heuristics and simplify `SpeedChangingAudioProcessor`. PiperOrigin-RevId: 711446999
This commit is contained in:
parent
01c51d8475
commit
682889f91d
@ -36,6 +36,9 @@ import java.util.Arrays;
|
|||||||
private static final int AMDF_FREQUENCY = 4000;
|
private static final int AMDF_FREQUENCY = 4000;
|
||||||
private static final int BYTES_PER_SAMPLE = 2;
|
private static final int BYTES_PER_SAMPLE = 2;
|
||||||
|
|
||||||
|
private static final float MINIMUM_SPEEDUP_RATE = 1.00001f;
|
||||||
|
private static final float MINIMUM_SLOWDOWN_RATE = 0.99999f;
|
||||||
|
|
||||||
private final int inputSampleRateHz;
|
private final int inputSampleRateHz;
|
||||||
private final int channelCount;
|
private final int channelCount;
|
||||||
private final float speed;
|
private final float speed;
|
||||||
@ -91,7 +94,7 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
BigDecimal length = BigDecimal.valueOf(inputFrameCount);
|
BigDecimal length = BigDecimal.valueOf(inputFrameCount);
|
||||||
BigDecimal framesAfterTimeStretching;
|
BigDecimal framesAfterTimeStretching;
|
||||||
if (speedRate > 1.00001 || speedRate < 0.99999) {
|
if (speedRate > MINIMUM_SPEEDUP_RATE || speedRate < MINIMUM_SLOWDOWN_RATE) {
|
||||||
framesAfterTimeStretching =
|
framesAfterTimeStretching =
|
||||||
length.divide(BigDecimal.valueOf(speedRate), RoundingMode.HALF_EVEN);
|
length.divide(BigDecimal.valueOf(speedRate), RoundingMode.HALF_EVEN);
|
||||||
} else {
|
} else {
|
||||||
@ -143,6 +146,62 @@ import java.util.Arrays;
|
|||||||
return accumulatedError.longValueExact();
|
return accumulatedError.longValueExact();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of input frames required for Sonic to produce the given number of output
|
||||||
|
* frames under the specified parameters.
|
||||||
|
*
|
||||||
|
* <p>This method is the inverse of {@link #getExpectedFrameCountAfterProcessorApplied}.
|
||||||
|
*
|
||||||
|
* @param inputSampleRateHz Input sample rate in Hertz.
|
||||||
|
* @param outputSampleRateHz Output sample rate in Hertz.
|
||||||
|
* @param speed Speed rate.
|
||||||
|
* @param pitch Pitch rate.
|
||||||
|
* @param outputFrameCount Number of output frames to calculate the required input frame count of.
|
||||||
|
*/
|
||||||
|
/* package */ static long getExpectedInputFrameCountForOutputFrameCount(
|
||||||
|
int inputSampleRateHz,
|
||||||
|
int outputSampleRateHz,
|
||||||
|
float speed,
|
||||||
|
float pitch,
|
||||||
|
long outputFrameCount) {
|
||||||
|
float resamplingRate = (float) inputSampleRateHz / outputSampleRateHz;
|
||||||
|
resamplingRate *= pitch;
|
||||||
|
BigDecimal bigResamplingRate = new BigDecimal(String.valueOf(resamplingRate));
|
||||||
|
long framesBeforeResampling =
|
||||||
|
getFrameCountBeforeResamplingForOutputCount(
|
||||||
|
BigDecimal.valueOf(inputSampleRateHz),
|
||||||
|
bigResamplingRate,
|
||||||
|
BigDecimal.valueOf(outputFrameCount));
|
||||||
|
double speedRate = speed / pitch;
|
||||||
|
|
||||||
|
if (speedRate > MINIMUM_SPEEDUP_RATE || speedRate < MINIMUM_SLOWDOWN_RATE) {
|
||||||
|
return BigDecimal.valueOf(framesBeforeResampling)
|
||||||
|
.multiply(BigDecimal.valueOf(speedRate))
|
||||||
|
.setScale(0, RoundingMode.FLOOR)
|
||||||
|
.longValueExact();
|
||||||
|
} else {
|
||||||
|
// If speed is almost 1, then just copy the buffers without modifying them.
|
||||||
|
return framesBeforeResampling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the expected input frame count prior to resampling with Sonic.
|
||||||
|
*
|
||||||
|
* <p>See {@link #getExpectedFrameCountAfterProcessorApplied} for more information.
|
||||||
|
*
|
||||||
|
* @param sampleRate Input sample rate of {@link Sonic} instance.
|
||||||
|
* @param resamplingRate Resampling rate given by {@code (inputSampleRate / outputSampleRate) *
|
||||||
|
* pitch}.
|
||||||
|
* @param outputLength Length of output in frames.
|
||||||
|
*/
|
||||||
|
private static long getFrameCountBeforeResamplingForOutputCount(
|
||||||
|
BigDecimal sampleRate, BigDecimal resamplingRate, BigDecimal outputLength) {
|
||||||
|
BigDecimal denominator = sampleRate.divide(resamplingRate, /* scale */ 0, RoundingMode.FLOOR);
|
||||||
|
BigDecimal numerator = sampleRate.multiply(outputLength);
|
||||||
|
return numerator.divide(denominator, /* scale */ 0, RoundingMode.FLOOR).longValueExact();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Sonic audio stream processor.
|
* Creates a new Sonic audio stream processor.
|
||||||
*
|
*
|
||||||
@ -599,7 +658,7 @@ import java.util.Arrays;
|
|||||||
int originalOutputFrameCount = outputFrameCount;
|
int originalOutputFrameCount = outputFrameCount;
|
||||||
double s = speed / pitch;
|
double s = speed / pitch;
|
||||||
float r = rate * pitch;
|
float r = rate * pitch;
|
||||||
if (s > 1.00001 || s < 0.99999) {
|
if (s > MINIMUM_SPEEDUP_RATE || s < MINIMUM_SLOWDOWN_RATE) {
|
||||||
changeSpeed(s);
|
changeSpeed(s);
|
||||||
} else {
|
} else {
|
||||||
copyToOutput(inputBuffer, 0, inputFrameCount);
|
copyToOutput(inputBuffer, 0, inputFrameCount);
|
||||||
|
@ -17,6 +17,7 @@ package androidx.media3.common.audio;
|
|||||||
|
|
||||||
import static androidx.media3.common.audio.Sonic.calculateAccumulatedTruncationErrorForResampling;
|
import static androidx.media3.common.audio.Sonic.calculateAccumulatedTruncationErrorForResampling;
|
||||||
import static androidx.media3.common.audio.Sonic.getExpectedFrameCountAfterProcessorApplied;
|
import static androidx.media3.common.audio.Sonic.getExpectedFrameCountAfterProcessorApplied;
|
||||||
|
import static androidx.media3.common.audio.Sonic.getExpectedInputFrameCountForOutputFrameCount;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
@ -260,4 +261,122 @@ public class SonicTest {
|
|||||||
// (All calculations are done on BigDecimal rounded to 20 decimal places, unless indicated).
|
// (All calculations are done on BigDecimal rounded to 20 decimal places, unless indicated).
|
||||||
assertThat(error).isEqualTo(305);
|
assertThat(error).isEqualTo(305);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getExpectedInputFrameCountForOutputFrameCount_fasterSpeed_returnsExpectedCount() {
|
||||||
|
long inputSamples =
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount(
|
||||||
|
/* inputSampleRateHz= */ 48_000,
|
||||||
|
/* outputSampleRateHz= */ 48_000,
|
||||||
|
/* speed= */ 5,
|
||||||
|
/* pitch= */ 1,
|
||||||
|
/* outputFrameCount= */ 20);
|
||||||
|
assertThat(inputSamples).isEqualTo(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount_fasterSpeedAndPitch_returnsExpectedCount() {
|
||||||
|
long inputSamples =
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount(
|
||||||
|
/* inputSampleRateHz= */ 48_000,
|
||||||
|
/* outputSampleRateHz= */ 48_000,
|
||||||
|
/* speed= */ 5,
|
||||||
|
/* pitch= */ 5,
|
||||||
|
/* outputFrameCount= */ 20);
|
||||||
|
assertThat(inputSamples).isEqualTo(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getExpectedInputFrameCountForOutputFrameCount_higherPitch_returnsExpectedCount() {
|
||||||
|
long inputSamples =
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount(
|
||||||
|
/* inputSampleRateHz= */ 48_000,
|
||||||
|
/* outputSampleRateHz= */ 48_000,
|
||||||
|
/* speed= */ 1,
|
||||||
|
/* pitch= */ 5,
|
||||||
|
/* outputFrameCount= */ 20);
|
||||||
|
assertThat(inputSamples).isEqualTo(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getExpectedInputFrameCountForOutputFrameCount_slowerSpeed_returnsExpectedCount() {
|
||||||
|
long inputSamples =
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount(
|
||||||
|
/* inputSampleRateHz= */ 48_000,
|
||||||
|
/* outputSampleRateHz= */ 48_000,
|
||||||
|
/* speed= */ 0.25f,
|
||||||
|
/* pitch= */ 1,
|
||||||
|
/* outputFrameCount= */ 100);
|
||||||
|
assertThat(inputSamples).isEqualTo(25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount_slowerSpeedAndPitch_returnsExpectedCount() {
|
||||||
|
long inputSamples =
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount(
|
||||||
|
/* inputSampleRateHz= */ 48_000,
|
||||||
|
/* outputSampleRateHz= */ 48_000,
|
||||||
|
/* speed= */ 0.25f,
|
||||||
|
/* pitch= */ 0.25f,
|
||||||
|
/* outputFrameCount= */ 100);
|
||||||
|
assertThat(inputSamples).isEqualTo(25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getExpectedInputFrameCountForOutputFrameCount_lowerPitch_returnsExpectedCount() {
|
||||||
|
long inputSamples =
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount(
|
||||||
|
/* inputSampleRateHz= */ 48_000,
|
||||||
|
/* outputSampleRateHz= */ 48_000,
|
||||||
|
/* speed= */ 1,
|
||||||
|
/* pitch= */ 0.75f,
|
||||||
|
/* outputFrameCount= */ 100);
|
||||||
|
assertThat(inputSamples).isEqualTo(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount_differentSamplingRates_returnsExpectedCount() {
|
||||||
|
long inputSamples =
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount(
|
||||||
|
/* inputSampleRateHz= */ 48_000,
|
||||||
|
/* outputSampleRateHz= */ 96_000,
|
||||||
|
/* speed= */ 1,
|
||||||
|
/* pitch= */ 1,
|
||||||
|
/* outputFrameCount= */ 100);
|
||||||
|
assertThat(inputSamples).isEqualTo(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount_differentPitchSpeedAndSamplingRates_returnsExpectedCount() {
|
||||||
|
long inputSamples =
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount(
|
||||||
|
/* inputSampleRateHz= */ 48_000,
|
||||||
|
/* outputSampleRateHz= */ 96_000,
|
||||||
|
/* speed= */ 5,
|
||||||
|
/* pitch= */ 2,
|
||||||
|
/* outputFrameCount= */ 40);
|
||||||
|
assertThat(inputSamples).isEqualTo(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount_withPeriodicResamplingRate_adjustsForTruncationError() {
|
||||||
|
float resamplingRate = 0.33f;
|
||||||
|
long outputLength = 81_521_212;
|
||||||
|
long truncationError = 305;
|
||||||
|
|
||||||
|
long inputSamples =
|
||||||
|
getExpectedInputFrameCountForOutputFrameCount(
|
||||||
|
/* inputSampleRateHz= */ 48_000,
|
||||||
|
/* outputSampleRateHz= */ 48_000,
|
||||||
|
/* speed= */ resamplingRate,
|
||||||
|
/* pitch= */ resamplingRate,
|
||||||
|
/* outputFrameCount= */ outputLength - truncationError);
|
||||||
|
|
||||||
|
assertThat(inputSamples).isEqualTo(26_902_000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user