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 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 channelCount;
|
||||
private final float speed;
|
||||
@ -91,7 +94,7 @@ import java.util.Arrays;
|
||||
|
||||
BigDecimal length = BigDecimal.valueOf(inputFrameCount);
|
||||
BigDecimal framesAfterTimeStretching;
|
||||
if (speedRate > 1.00001 || speedRate < 0.99999) {
|
||||
if (speedRate > MINIMUM_SPEEDUP_RATE || speedRate < MINIMUM_SLOWDOWN_RATE) {
|
||||
framesAfterTimeStretching =
|
||||
length.divide(BigDecimal.valueOf(speedRate), RoundingMode.HALF_EVEN);
|
||||
} else {
|
||||
@ -143,6 +146,62 @@ import java.util.Arrays;
|
||||
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.
|
||||
*
|
||||
@ -599,7 +658,7 @@ import java.util.Arrays;
|
||||
int originalOutputFrameCount = outputFrameCount;
|
||||
double s = speed / pitch;
|
||||
float r = rate * pitch;
|
||||
if (s > 1.00001 || s < 0.99999) {
|
||||
if (s > MINIMUM_SPEEDUP_RATE || s < MINIMUM_SLOWDOWN_RATE) {
|
||||
changeSpeed(s);
|
||||
} else {
|
||||
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.getExpectedFrameCountAfterProcessorApplied;
|
||||
import static androidx.media3.common.audio.Sonic.getExpectedInputFrameCountForOutputFrameCount;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
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).
|
||||
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