mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
parent
afd601f670
commit
d6e4642bcf
@ -18,6 +18,8 @@
|
||||
* Extractors:
|
||||
* DataSource:
|
||||
* Audio:
|
||||
* Do not bypass `SonicAudioProcessor` when `SpeedChangingAudioProcessor`
|
||||
is configured with default parameters.
|
||||
* Video:
|
||||
* Text:
|
||||
* Metadata:
|
||||
|
@ -15,8 +15,11 @@
|
||||
*/
|
||||
package androidx.media3.common.audio;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static java.lang.Math.abs;
|
||||
|
||||
import androidx.annotation.FloatRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
@ -44,6 +47,8 @@ public class SonicAudioProcessor implements AudioProcessor {
|
||||
*/
|
||||
private static final int MIN_BYTES_FOR_DURATION_SCALING_CALCULATION = 1024;
|
||||
|
||||
private final boolean shouldBeActiveWithDefaultParameters;
|
||||
|
||||
private int pendingOutputSampleRate;
|
||||
private float speed;
|
||||
private float pitch;
|
||||
@ -64,6 +69,17 @@ public class SonicAudioProcessor implements AudioProcessor {
|
||||
|
||||
/** Creates a new Sonic audio processor. */
|
||||
public SonicAudioProcessor() {
|
||||
this(/* keepActiveWithDefaultParameters= */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@link SonicAudioProcessor}.
|
||||
*
|
||||
* <p>If {@code keepActiveWithDefaultParameters} is set to {@code true}, then {@link #isActive()}
|
||||
* returns {@code true} when parameters have been configured to default values that result in
|
||||
* no-op processing.
|
||||
*/
|
||||
/* package */ SonicAudioProcessor(boolean keepActiveWithDefaultParameters) {
|
||||
speed = 1f;
|
||||
pitch = 1f;
|
||||
pendingInputAudioFormat = AudioFormat.NOT_SET;
|
||||
@ -74,6 +90,7 @@ public class SonicAudioProcessor implements AudioProcessor {
|
||||
shortBuffer = buffer.asShortBuffer();
|
||||
outputBuffer = EMPTY_BUFFER;
|
||||
pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE;
|
||||
shouldBeActiveWithDefaultParameters = keepActiveWithDefaultParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,7 +100,8 @@ public class SonicAudioProcessor implements AudioProcessor {
|
||||
*
|
||||
* @param speed The target factor by which playback should be sped up.
|
||||
*/
|
||||
public final void setSpeed(float speed) {
|
||||
public final void setSpeed(@FloatRange(from = 0f, fromInclusive = false) float speed) {
|
||||
checkArgument(speed > 0f);
|
||||
if (this.speed != speed) {
|
||||
this.speed = speed;
|
||||
pendingSonicRecreation = true;
|
||||
@ -97,7 +115,8 @@ public class SonicAudioProcessor implements AudioProcessor {
|
||||
*
|
||||
* @param pitch The target pitch.
|
||||
*/
|
||||
public final void setPitch(float pitch) {
|
||||
public final void setPitch(@FloatRange(from = 0f, fromInclusive = false) float pitch) {
|
||||
checkArgument(pitch > 0f);
|
||||
if (this.pitch != pitch) {
|
||||
this.pitch = pitch;
|
||||
pendingSonicRecreation = true;
|
||||
@ -113,6 +132,7 @@ public class SonicAudioProcessor implements AudioProcessor {
|
||||
* @see #configure(AudioFormat)
|
||||
*/
|
||||
public final void setOutputSampleRateHz(int sampleRateHz) {
|
||||
checkArgument(sampleRateHz == SAMPLE_RATE_NO_CHANGE || sampleRateHz > 0);
|
||||
pendingOutputSampleRate = sampleRateHz;
|
||||
}
|
||||
|
||||
@ -196,9 +216,13 @@ public class SonicAudioProcessor implements AudioProcessor {
|
||||
@Override
|
||||
public final boolean isActive() {
|
||||
return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE
|
||||
&& (Math.abs(speed - 1f) >= CLOSE_THRESHOLD
|
||||
|| Math.abs(pitch - 1f) >= CLOSE_THRESHOLD
|
||||
|| pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate);
|
||||
&& (shouldBeActiveWithDefaultParameters || !areParametersSetToDefaultValues());
|
||||
}
|
||||
|
||||
private boolean areParametersSetToDefaultValues() {
|
||||
return abs(speed - 1f) < CLOSE_THRESHOLD
|
||||
&& abs(pitch - 1f) < CLOSE_THRESHOLD
|
||||
&& pendingOutputAudioFormat.sampleRate == pendingInputAudioFormat.sampleRate;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -24,6 +24,7 @@ import static androidx.media3.common.util.Util.sampleCountToDurationUs;
|
||||
import static java.lang.Math.min;
|
||||
import static java.lang.Math.round;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.media3.common.C;
|
||||
@ -99,11 +100,12 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
|
||||
public SpeedChangingAudioProcessor(SpeedProvider speedProvider) {
|
||||
this.speedProvider = speedProvider;
|
||||
lock = new Object();
|
||||
sonicAudioProcessor = new SynchronizedSonicAudioProcessor(lock);
|
||||
sonicAudioProcessor =
|
||||
new SynchronizedSonicAudioProcessor(lock, /* keepActiveWithDefaultParameters= */ true);
|
||||
pendingCallbackInputTimesUs = new LongArrayQueue();
|
||||
pendingCallbacks = new ArrayDeque<>();
|
||||
speedAdjustedTimeAsyncInputTimeUs = C.TIME_UNSET;
|
||||
resetState();
|
||||
resetState(/* shouldResetSpeed= */ true);
|
||||
}
|
||||
|
||||
/** Returns the estimated number of samples output given the provided parameters. */
|
||||
@ -174,19 +176,11 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
|
||||
}
|
||||
|
||||
long startPosition = inputBuffer.position();
|
||||
if (isUsingSonic()) {
|
||||
sonicAudioProcessor.queueInput(inputBuffer);
|
||||
if (bytesToNextSpeedChange != C.LENGTH_UNSET
|
||||
&& (inputBuffer.position() - startPosition) == bytesToNextSpeedChange) {
|
||||
sonicAudioProcessor.queueEndOfStream();
|
||||
endOfStreamQueuedToSonic = true;
|
||||
}
|
||||
} else {
|
||||
ByteBuffer buffer = replaceOutputBuffer(/* size= */ inputBuffer.remaining());
|
||||
if (inputBuffer.hasRemaining()) {
|
||||
buffer.put(inputBuffer);
|
||||
}
|
||||
buffer.flip();
|
||||
sonicAudioProcessor.queueInput(inputBuffer);
|
||||
if (bytesToNextSpeedChange != C.LENGTH_UNSET
|
||||
&& (inputBuffer.position() - startPosition) == bytesToNextSpeedChange) {
|
||||
sonicAudioProcessor.queueEndOfStream();
|
||||
endOfStreamQueuedToSonic = true;
|
||||
}
|
||||
long bytesRead = inputBuffer.position() - startPosition;
|
||||
checkState(
|
||||
@ -204,9 +198,11 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
// Not using BaseAudioProcessor's buffers.
|
||||
@SuppressLint("MissingSuperCall")
|
||||
@Override
|
||||
public ByteBuffer getOutput() {
|
||||
ByteBuffer output = isUsingSonic() ? sonicAudioProcessor.getOutput() : super.getOutput();
|
||||
ByteBuffer output = sonicAudioProcessor.getOutput();
|
||||
processPendingCallbacks();
|
||||
return output;
|
||||
}
|
||||
@ -218,13 +214,13 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
|
||||
|
||||
@Override
|
||||
protected void onFlush() {
|
||||
resetState();
|
||||
resetState(/* shouldResetSpeed= */ false);
|
||||
sonicAudioProcessor.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReset() {
|
||||
resetState();
|
||||
resetState(/* shouldResetSpeed= */ true);
|
||||
sonicAudioProcessor.reset();
|
||||
}
|
||||
|
||||
@ -351,10 +347,8 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
|
||||
if (newSpeed != currentSpeed) {
|
||||
updateSpeedChangeArrays(timeUs);
|
||||
currentSpeed = newSpeed;
|
||||
if (isUsingSonic()) {
|
||||
sonicAudioProcessor.setSpeed(newSpeed);
|
||||
sonicAudioProcessor.setPitch(newSpeed);
|
||||
}
|
||||
sonicAudioProcessor.setSpeed(newSpeed);
|
||||
sonicAudioProcessor.setPitch(newSpeed);
|
||||
// Invalidate any previously created buffers in SonicAudioProcessor and the base class.
|
||||
sonicAudioProcessor.flush();
|
||||
endOfStreamQueuedToSonic = false;
|
||||
@ -378,45 +372,40 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
|
||||
}
|
||||
|
||||
private long getPlayoutDurationUsAtCurrentSpeed(long mediaDurationUs) {
|
||||
return isUsingSonic()
|
||||
? sonicAudioProcessor.getPlayoutDuration(mediaDurationUs)
|
||||
: mediaDurationUs;
|
||||
return sonicAudioProcessor.getPlayoutDuration(mediaDurationUs);
|
||||
}
|
||||
|
||||
private long getMediaDurationUsAtCurrentSpeed(long playoutDurationUs) {
|
||||
return isUsingSonic()
|
||||
? sonicAudioProcessor.getMediaDuration(playoutDurationUs)
|
||||
: playoutDurationUs;
|
||||
return sonicAudioProcessor.getMediaDuration(playoutDurationUs);
|
||||
}
|
||||
|
||||
private void updateLastProcessedInputTime() {
|
||||
synchronized (lock) {
|
||||
if (isUsingSonic()) {
|
||||
// TODO - b/320242819: Investigate whether bytesRead can be used here rather than
|
||||
// sonicAudioProcessor.getProcessedInputBytes().
|
||||
long currentProcessedInputDurationUs =
|
||||
Util.scaleLargeTimestamp(
|
||||
/* timestamp= */ sonicAudioProcessor.getProcessedInputBytes(),
|
||||
/* multiplier= */ C.MICROS_PER_SECOND,
|
||||
/* divisor= */ (long) inputAudioFormat.sampleRate * inputAudioFormat.bytesPerFrame);
|
||||
lastProcessedInputTimeUs =
|
||||
inputSegmentStartTimesUs.get(inputSegmentStartTimesUs.size() - 1)
|
||||
+ currentProcessedInputDurationUs;
|
||||
} else {
|
||||
lastProcessedInputTimeUs = sampleCountToDurationUs(framesRead, inputAudioFormat.sampleRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isUsingSonic() {
|
||||
synchronized (lock) {
|
||||
return currentSpeed != 1f;
|
||||
// TODO - b/320242819: Investigate whether bytesRead can be used here rather than
|
||||
// sonicAudioProcessor.getProcessedInputBytes().
|
||||
long currentProcessedInputDurationUs =
|
||||
Util.scaleLargeTimestamp(
|
||||
/* timestamp= */ sonicAudioProcessor.getProcessedInputBytes(),
|
||||
/* multiplier= */ C.MICROS_PER_SECOND,
|
||||
/* divisor= */ (long) inputAudioFormat.sampleRate * inputAudioFormat.bytesPerFrame);
|
||||
lastProcessedInputTimeUs =
|
||||
inputSegmentStartTimesUs.get(inputSegmentStartTimesUs.size() - 1)
|
||||
+ currentProcessedInputDurationUs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets internal fields to their default value.
|
||||
*
|
||||
* <p>When setting {@code shouldResetSpeed} to {@code true}, {@link #sonicAudioProcessor}'s speed
|
||||
* and pitch must also be updated.
|
||||
*
|
||||
* @param shouldResetSpeed Whether {@link #currentSpeed} should be reset to its default value.
|
||||
*/
|
||||
@EnsuresNonNull({"inputSegmentStartTimesUs", "outputSegmentStartTimesUs"})
|
||||
@RequiresNonNull("lock")
|
||||
private void resetState(@UnknownInitialization SpeedChangingAudioProcessor this) {
|
||||
private void resetState(
|
||||
@UnknownInitialization SpeedChangingAudioProcessor this, boolean shouldResetSpeed) {
|
||||
synchronized (lock) {
|
||||
inputSegmentStartTimesUs = new LongArray();
|
||||
outputSegmentStartTimesUs = new LongArray();
|
||||
@ -425,7 +414,9 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
|
||||
lastProcessedInputTimeUs = 0;
|
||||
lastSpeedAdjustedInputTimeUs = 0;
|
||||
lastSpeedAdjustedOutputTimeUs = 0;
|
||||
currentSpeed = 1f;
|
||||
if (shouldResetSpeed) {
|
||||
currentSpeed = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
framesRead = 0;
|
||||
|
@ -26,9 +26,9 @@ import java.nio.ByteBuffer;
|
||||
private final Object lock;
|
||||
private final SonicAudioProcessor sonicAudioProcessor;
|
||||
|
||||
public SynchronizedSonicAudioProcessor(Object lock) {
|
||||
public SynchronizedSonicAudioProcessor(Object lock, boolean keepActiveWithDefaultParameters) {
|
||||
this.lock = lock;
|
||||
sonicAudioProcessor = new SonicAudioProcessor();
|
||||
sonicAudioProcessor = new SonicAudioProcessor(keepActiveWithDefaultParameters);
|
||||
}
|
||||
|
||||
public final void setSpeed(float speed) {
|
||||
|
@ -86,11 +86,19 @@ public final class SonicAudioProcessorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isNotActiveWithNoChange() throws Exception {
|
||||
public void isActive_withDefaultParameters_returnsFalse() throws Exception {
|
||||
sonicAudioProcessor.configure(AUDIO_FORMAT_44100_HZ);
|
||||
assertThat(sonicAudioProcessor.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isActive_keepActiveWithDefaultParameters_returnsTrue() throws Exception {
|
||||
SonicAudioProcessor processor =
|
||||
new SonicAudioProcessor(/* keepActiveWithDefaultParameters= */ true);
|
||||
processor.configure(AUDIO_FORMAT_44100_HZ);
|
||||
assertThat(processor.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doesNotSupportNon16BitInput() throws Exception {
|
||||
try {
|
||||
|
@ -603,6 +603,84 @@ public class SpeedChangingAudioProcessorTest {
|
||||
assertThat(outputFrameCount).isWithin(1).of(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_withInitialSpeedSetToDefault_returnsToInitialSpeedAfterFlush()
|
||||
throws AudioProcessor.UnhandledAudioFormatException {
|
||||
SpeedProvider speedProvider =
|
||||
TestSpeedProvider.createWithFrameCounts(
|
||||
AUDIO_FORMAT,
|
||||
/* frameCounts= */ new int[] {1000, 1000},
|
||||
/* speeds= */ new float[] {1, 2}); // 1000, 500.
|
||||
SpeedChangingAudioProcessor speedChangingAudioProcessor =
|
||||
getConfiguredSpeedChangingAudioProcessor(speedProvider);
|
||||
// 1500 input frames falls in the middle of the 2x region.
|
||||
ByteBuffer input = getInputBuffer(1500);
|
||||
int outputFrameCount = 0;
|
||||
|
||||
while (input.hasRemaining()) {
|
||||
speedChangingAudioProcessor.queueInput(input);
|
||||
outputFrameCount +=
|
||||
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
||||
}
|
||||
speedChangingAudioProcessor.flush();
|
||||
outputFrameCount +=
|
||||
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
||||
assertThat(outputFrameCount).isEqualTo(1250);
|
||||
input.rewind();
|
||||
|
||||
// After flush, SpeedChangingAudioProcessor's position should go back to the beginning and use
|
||||
// the first speed region. This means that even if we flushed during 2x, the initial 1000
|
||||
// samples fed to SpeedChangingAudioProcessor after the flush should be output at 1x.
|
||||
while (input.hasRemaining()) {
|
||||
speedChangingAudioProcessor.queueInput(input);
|
||||
outputFrameCount +=
|
||||
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
||||
}
|
||||
speedChangingAudioProcessor.queueEndOfStream();
|
||||
outputFrameCount +=
|
||||
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
||||
assertThat(outputFrameCount).isWithin(1).of(2500); // 1250 * 2.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_withInitialSpeedSetToNonDefault_returnsToInitialSpeedAfterFlush()
|
||||
throws AudioProcessor.UnhandledAudioFormatException {
|
||||
SpeedProvider speedProvider =
|
||||
TestSpeedProvider.createWithFrameCounts(
|
||||
AUDIO_FORMAT,
|
||||
/* frameCounts= */ new int[] {1000, 1000},
|
||||
/* speeds= */ new float[] {2, 4}); // 500, 250.
|
||||
SpeedChangingAudioProcessor speedChangingAudioProcessor =
|
||||
getConfiguredSpeedChangingAudioProcessor(speedProvider);
|
||||
// 1500 input frames falls in the middle of the 2x region.
|
||||
ByteBuffer input = getInputBuffer(1500);
|
||||
int outputFrameCount = 0;
|
||||
|
||||
while (input.hasRemaining()) {
|
||||
speedChangingAudioProcessor.queueInput(input);
|
||||
outputFrameCount +=
|
||||
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
||||
}
|
||||
speedChangingAudioProcessor.flush();
|
||||
outputFrameCount +=
|
||||
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
||||
assertThat(outputFrameCount).isWithin(1).of(625);
|
||||
input.rewind();
|
||||
|
||||
// After flush, SpeedChangingAudioProcessor's position should go back to the beginning and use
|
||||
// the first speed region. This means that even if we flushed during 4x, the initial 1000
|
||||
// samples fed to SpeedChangingAudioProcessor after the flush should be output at 2x.
|
||||
while (input.hasRemaining()) {
|
||||
speedChangingAudioProcessor.queueInput(input);
|
||||
outputFrameCount +=
|
||||
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
||||
}
|
||||
speedChangingAudioProcessor.queueEndOfStream();
|
||||
outputFrameCount +=
|
||||
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
||||
assertThat(outputFrameCount).isWithin(2).of(1250); // 625 * 2.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSampleCountAfterProcessorApplied_withConstantSpeed_outputsExpectedSamples() {
|
||||
SpeedProvider speedProvider =
|
||||
|
Loading…
x
Reference in New Issue
Block a user