mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add support for audio fades and gain automation
This change introduces `GainProcessor` to apply gain automation over an audio stream. The gain automation is defined with a `GainProvider`. `DefaultGainProvider` implements a basic `GainProvider` that allows populating a gain automation line additively by adding fade shapes (`FadeProvider`) at specific positions with specific durations. `DefaultGainProvider` also implements basic equal gain and equal power fades. The current `DefaultGainProvider` implementation does not support adding fades at relative offsets from the stream end. Users must know the stream duration in advance and add fades at absolute positions. PiperOrigin-RevId: 733311314
This commit is contained in:
parent
b36d0483b2
commit
c3d734066d
@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.common.audio;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static androidx.media3.common.util.Util.durationUsToSampleCount;
|
||||
import static androidx.media3.common.util.Util.sampleCountToDurationUs;
|
||||
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.audio.GainProcessor.GainProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.common.collect.RangeMap;
|
||||
import com.google.common.collect.TreeRangeMap;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* Provides gain automation information to be applied on an audio stream.
|
||||
*
|
||||
* <p>The class allows combining multiple {@linkplain FadeProvider fade shapes} into one single
|
||||
* automation line, with common fade shapes already implemented (e.g. {@link #FADE_IN_LINEAR}).
|
||||
*
|
||||
* @see GainProcessor
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class DefaultGainProvider implements GainProvider {
|
||||
|
||||
/** A builder for {@link DefaultGainProvider} instances. */
|
||||
public static final class Builder {
|
||||
private final TreeRangeMap<Long, Function<Pair<Long, Integer>, Float>> gainMap;
|
||||
private final float defaultGain;
|
||||
|
||||
/**
|
||||
* Returns a {@link DefaultGainProvider} builder.
|
||||
*
|
||||
* @param defaultGain Default gain value.
|
||||
*/
|
||||
public Builder(float defaultGain) {
|
||||
gainMap = TreeRangeMap.create();
|
||||
// Add default value for all possible positions.
|
||||
this.defaultGain = defaultGain;
|
||||
gainMap.put(Range.all(), (a) -> GAIN_UNSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@code shape} to be applied between [{@code positionUs}; {@code positionUs} + {@code
|
||||
* durationUs}).
|
||||
*
|
||||
* <p>This fade overwrites the shape of any previously added fade if they overlap.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder addFadeAt(
|
||||
@IntRange(from = 0) long positionUs,
|
||||
@IntRange(from = 1) long durationUs,
|
||||
FadeProvider shape) {
|
||||
checkArgument(positionUs >= 0);
|
||||
checkArgument(durationUs > 1);
|
||||
gainMap.put(
|
||||
Range.closedOpen(positionUs, positionUs + durationUs),
|
||||
(positionSampleRatePair) -> {
|
||||
int sampleRate = positionSampleRatePair.second;
|
||||
long relativeSamplePosition =
|
||||
positionSampleRatePair.first - durationUsToSampleCount(positionUs, sampleRate);
|
||||
return shape.getGainFactorAt(
|
||||
relativeSamplePosition, durationUsToSampleCount(durationUs, sampleRate));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns a new {@link DefaultGainProvider} instance. */
|
||||
public DefaultGainProvider build() {
|
||||
return new DefaultGainProvider(gainMap, defaultGain);
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents a time unit-agnostic fade shape to be applied over an automation. */
|
||||
public interface FadeProvider {
|
||||
|
||||
/**
|
||||
* Returns the gain factor within [0f; 1f] to apply to an audio sample for a specific fade
|
||||
* shape.
|
||||
*
|
||||
* <p>Position and duration are unit agnostic and work as a numerator/denominator pair.
|
||||
*
|
||||
* <p>You can implement a basic linear fade as follows:
|
||||
*
|
||||
* <pre>{@code
|
||||
* @Override
|
||||
* public float getGainFactorAt(long index, long duration) {
|
||||
* return (float) index / duration;
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @param index Position (numerator) between [0; {@code duration}].
|
||||
* @param duration Duration (denominator).
|
||||
*/
|
||||
float getGainFactorAt(@IntRange(from = 0) long index, @IntRange(from = 1) long duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Equal gain fade in.
|
||||
*
|
||||
* <p>Ramps linearly from 0 to 1.
|
||||
*
|
||||
* <p>Summing this with {@link #FADE_OUT_LINEAR} returns a constant gain of 1 for all valid
|
||||
* indexes.
|
||||
*/
|
||||
public static final FadeProvider FADE_IN_LINEAR = (index, duration) -> (float) index / duration;
|
||||
|
||||
/**
|
||||
* Equal gain fade out.
|
||||
*
|
||||
* <p>Ramps linearly from 1 to 0.
|
||||
*
|
||||
* <p>Summing this with {@link #FADE_IN_LINEAR} returns a constant gain of 1 for all valid
|
||||
* indexes.
|
||||
*/
|
||||
public static final FadeProvider FADE_OUT_LINEAR =
|
||||
(index, duration) -> (float) (duration - index) / duration;
|
||||
|
||||
/**
|
||||
* Equal power fade in.
|
||||
*
|
||||
* <p>Ramps from 0 to 1 using an equal power curve.
|
||||
*
|
||||
* <p>Summing this with {@link #FADE_OUT_EQUAL_POWER} returns a constant power of 1 for all valid
|
||||
* indexes.
|
||||
*/
|
||||
public static final FadeProvider FADE_IN_EQUAL_POWER =
|
||||
(index, duration) -> (float) Math.sin((Math.PI / 2.0) * index / duration);
|
||||
|
||||
/**
|
||||
* Equal power fade out.
|
||||
*
|
||||
* <p>Ramps from 1 to 0 using an equal power curve.
|
||||
*
|
||||
* <p>Summing this with {@link #FADE_IN_EQUAL_POWER} returns a constant power of 1 for all valid
|
||||
* indexes.
|
||||
*/
|
||||
public static final FadeProvider FADE_OUT_EQUAL_POWER =
|
||||
(index, duration) -> (float) Math.cos((Math.PI / 2.0) * index / duration);
|
||||
|
||||
private static final float GAIN_UNSET = C.RATE_UNSET;
|
||||
|
||||
/**
|
||||
* {@link RangeMap} for representing a sequence of fades applied at specific time ranges over a
|
||||
* default gain value.
|
||||
*
|
||||
* <p>Keys correspond to the position range in microseconds. Entry values correspond to a generic
|
||||
* {@link Function} that returns a gain value based on a sample position and sample rate.
|
||||
*/
|
||||
// Use TreeRangeMap instead of ImmutableRangeMap to allow overlapping ranges.
|
||||
private final TreeRangeMap<Long, Function<Pair<Long, Integer>, Float>> gainMap;
|
||||
|
||||
private final float defaultGain;
|
||||
|
||||
private DefaultGainProvider(
|
||||
TreeRangeMap<Long, Function<Pair<Long, Integer>, Float>> gainMap, float defaultGain) {
|
||||
this.gainMap = TreeRangeMap.create();
|
||||
this.gainMap.putAll(gainMap);
|
||||
this.defaultGain = defaultGain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getGainFactorAtSamplePosition(
|
||||
@IntRange(from = 0) long samplePosition, @IntRange(from = 1) int sampleRate) {
|
||||
checkState(sampleRate > 0);
|
||||
checkArgument(samplePosition >= 0);
|
||||
|
||||
// gainMap has a default value set for all possible values, so it should never return null.
|
||||
float gain =
|
||||
checkNotNull(gainMap.get(sampleCountToDurationUs(samplePosition, sampleRate)))
|
||||
.apply(Pair.create(samplePosition, sampleRate));
|
||||
if (gain == GAIN_UNSET) {
|
||||
return defaultGain;
|
||||
}
|
||||
return gain;
|
||||
}
|
||||
|
||||
@Override
|
||||
// TODO (b/400418589): Add support for non-default value unity ranges.
|
||||
public long isUnityUntil(
|
||||
@IntRange(from = 0) long samplePosition, @IntRange(from = 1) int sampleRate) {
|
||||
checkState(sampleRate > 0);
|
||||
checkArgument(samplePosition >= 0);
|
||||
|
||||
long positionUs = sampleCountToDurationUs(samplePosition, sampleRate);
|
||||
Entry<Range<Long>, Function<Pair<Long, Integer>, Float>> entry =
|
||||
checkNotNull(gainMap.getEntry(positionUs));
|
||||
|
||||
if (defaultGain != 1f
|
||||
|| entry.getValue().apply(Pair.create(samplePosition, sampleRate)) != GAIN_UNSET) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
if (!entry.getKey().hasUpperBound()) {
|
||||
return C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
return durationUsToSampleCount(entry.getKey().upperEndpoint(), sampleRate);
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.common.audio;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Applies {@linkplain GainProvider gain automation} over an audio stream. */
|
||||
@UnstableApi
|
||||
public final class GainProcessor extends BaseAudioProcessor {
|
||||
|
||||
/** Interface that provides sample-level gain automation to be applied on an audio stream. */
|
||||
public interface GainProvider {
|
||||
/**
|
||||
* Returns a gain factor between [0f; 1f] to apply at the given sample position relative to
|
||||
* {@code sampleRate}.
|
||||
*
|
||||
* <p>Returned values must not change for the same pair of parameter values within the lifetime
|
||||
* of the instance.
|
||||
*/
|
||||
float getGainFactorAtSamplePosition(
|
||||
@IntRange(from = 0) long samplePosition, @IntRange(from = 1) int sampleRate);
|
||||
|
||||
/**
|
||||
* Returns the exclusive upper limit of the range starting at {@code samplePosition} where the
|
||||
* gain value is 1f (unity), or {@link C#TIME_UNSET} if {@code samplePosition} does not
|
||||
* correspond to a gain of 1f.
|
||||
*
|
||||
* <p>If the range continues until the end of the stream, this method returns {@link
|
||||
* C#TIME_END_OF_SOURCE}.
|
||||
*
|
||||
* <p>Returned values must not change for the same pair of parameter values within the lifetime
|
||||
* of the instance.
|
||||
*
|
||||
* @param samplePosition Inclusive starting position of the unity range.
|
||||
* @param sampleRate Sample rate in Hertz related to {@code samplePosition}.
|
||||
*/
|
||||
long isUnityUntil(@IntRange(from = 0) long samplePosition, @IntRange(from = 1) int sampleRate);
|
||||
}
|
||||
|
||||
private final GainProvider gainProvider;
|
||||
private long readFrames;
|
||||
|
||||
public GainProcessor(GainProvider gainProvider) {
|
||||
this.gainProvider = checkNotNull(gainProvider);
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public AudioFormat onConfigure(AudioFormat inputAudioFormat)
|
||||
throws UnhandledAudioFormatException {
|
||||
int encoding = inputAudioFormat.encoding;
|
||||
if (encoding != C.ENCODING_PCM_16BIT && encoding != C.ENCODING_PCM_FLOAT) {
|
||||
throw new UnhandledAudioFormatException(
|
||||
"Invalid PCM encoding. Expected 16 bit PCM or float PCM.", inputAudioFormat);
|
||||
}
|
||||
return inputAudioFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return super.isActive()
|
||||
&& !Objects.equals(inputAudioFormat, AudioFormat.NOT_SET)
|
||||
&& gainProvider.isUnityUntil(/* samplePosition= */ 0, inputAudioFormat.sampleRate)
|
||||
!= C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInput(ByteBuffer inputBuffer) {
|
||||
checkState(
|
||||
!Objects.equals(inputAudioFormat, AudioFormat.NOT_SET),
|
||||
"Audio processor must be configured and flushed before calling queueInput().");
|
||||
|
||||
if (!inputBuffer.hasRemaining()) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkArgument(
|
||||
inputBuffer.remaining() % inputAudioFormat.bytesPerFrame == 0,
|
||||
"Queued an incomplete frame.");
|
||||
|
||||
ByteBuffer buffer = replaceOutputBuffer(inputBuffer.remaining());
|
||||
|
||||
// Each iteration handles one frame.
|
||||
while (inputBuffer.hasRemaining()) {
|
||||
float gain =
|
||||
gainProvider.getGainFactorAtSamplePosition(readFrames, inputAudioFormat.sampleRate);
|
||||
if (gain == 1f) {
|
||||
int oldLimit = inputBuffer.limit();
|
||||
|
||||
long regionEnd = gainProvider.isUnityUntil(readFrames, inputAudioFormat.sampleRate);
|
||||
checkState(regionEnd != C.TIME_UNSET, "Expected a valid end boundary for unity region.");
|
||||
|
||||
// Only set limit if unity does not last until EoS.
|
||||
if (regionEnd != C.TIME_END_OF_SOURCE) {
|
||||
long limitOffsetBytes = (regionEnd - readFrames) * inputAudioFormat.bytesPerFrame;
|
||||
inputBuffer.limit(min(oldLimit, (int) limitOffsetBytes + inputBuffer.position()));
|
||||
}
|
||||
|
||||
readFrames += inputBuffer.remaining() / inputAudioFormat.bytesPerFrame;
|
||||
buffer.put(inputBuffer);
|
||||
inputBuffer.limit(oldLimit);
|
||||
} else {
|
||||
for (int i = 0; i < inputAudioFormat.channelCount; i++) {
|
||||
switch (inputAudioFormat.encoding) {
|
||||
case C.ENCODING_PCM_16BIT:
|
||||
buffer.putShort((short) (inputBuffer.getShort() * gain));
|
||||
break;
|
||||
case C.ENCODING_PCM_FLOAT:
|
||||
buffer.putFloat(inputBuffer.getFloat() * gain);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Unexpected PCM encoding: " + inputAudioFormat.encoding);
|
||||
}
|
||||
}
|
||||
readFrames++;
|
||||
}
|
||||
}
|
||||
buffer.flip();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFlush() {
|
||||
readFrames = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset() {
|
||||
readFrames = 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,316 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.common.audio;
|
||||
|
||||
import static androidx.media3.common.audio.DefaultGainProvider.FADE_IN_EQUAL_POWER;
|
||||
import static androidx.media3.common.audio.DefaultGainProvider.FADE_IN_LINEAR;
|
||||
import static androidx.media3.common.audio.DefaultGainProvider.FADE_OUT_EQUAL_POWER;
|
||||
import static androidx.media3.common.audio.DefaultGainProvider.FADE_OUT_LINEAR;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.audio.DefaultGainProvider.FadeProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link DefaultGainProvider}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DefaultGainProviderTest {
|
||||
|
||||
private static final int SAMPLE_RATE = 50000;
|
||||
|
||||
private static final FadeProvider CONSTANT_VALUE_FADE = (index, duration) -> 0.5f;
|
||||
|
||||
@Test
|
||||
public void getGainFactorAtSamplePosition_withoutFades_returnsDefaultValue() {
|
||||
DefaultGainProvider provider = new DefaultGainProvider.Builder(/* defaultGain= */ 1f).build();
|
||||
assertThat(provider.getGainFactorAtSamplePosition(0, SAMPLE_RATE)).isEqualTo(1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGainFactorAtSamplePosition_withConstantFade_returnsFadeValue() {
|
||||
DefaultGainProvider provider =
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(
|
||||
/* positionUs= */ 0L, /* durationUs= */ C.MICROS_PER_SECOND, CONSTANT_VALUE_FADE)
|
||||
.build();
|
||||
assertThat(provider.getGainFactorAtSamplePosition(/* samplePosition= */ 0, SAMPLE_RATE))
|
||||
.isEqualTo(0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGainFactorAtSamplePosition_withFadeIn_returnsFadeValue() {
|
||||
DefaultGainProvider provider =
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(/* positionUs= */ 0L, /* durationUs= */ C.MICROS_PER_SECOND, FADE_IN_LINEAR)
|
||||
.build();
|
||||
assertThat(provider.getGainFactorAtSamplePosition(/* samplePosition= */ 0, SAMPLE_RATE))
|
||||
.isEqualTo(0f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ SAMPLE_RATE / 4, SAMPLE_RATE))
|
||||
.isEqualTo(0.25f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ SAMPLE_RATE / 2, SAMPLE_RATE))
|
||||
.isEqualTo(0.5f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 3 * SAMPLE_RATE / 4, SAMPLE_RATE))
|
||||
.isEqualTo(0.75f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(/* samplePosition= */ SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGainFactorAtSamplePosition_withNonTrivialFadeDuration_scalesFade() {
|
||||
DefaultGainProvider provider =
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(
|
||||
/* positionUs= */ 0L, /* durationUs= */ 4 * C.MICROS_PER_SECOND, FADE_IN_LINEAR)
|
||||
.build();
|
||||
assertThat(provider.getGainFactorAtSamplePosition(/* samplePosition= */ 0, SAMPLE_RATE))
|
||||
.isEqualTo(0f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(/* samplePosition= */ SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0.25f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 2 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0.5f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 3 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0.75f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 4 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGainFactorAtSamplePosition_withSubsequentSampleRateChange_rescalesFades() {
|
||||
DefaultGainProvider provider =
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(/* positionUs= */ 0L, /* durationUs= */ C.MICROS_PER_SECOND, FADE_IN_LINEAR)
|
||||
.build();
|
||||
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 0, /* sampleRate= */ SAMPLE_RATE))
|
||||
.isEqualTo(0f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ SAMPLE_RATE / 2, /* sampleRate= */ SAMPLE_RATE))
|
||||
.isEqualTo(0.5f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ SAMPLE_RATE, /* sampleRate= */ SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 0, /* sampleRate= */ 2 * SAMPLE_RATE))
|
||||
.isEqualTo(0f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ SAMPLE_RATE, /* sampleRate= */ 2 * SAMPLE_RATE))
|
||||
.isEqualTo(0.5f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 2 * SAMPLE_RATE, /* sampleRate= */ 2 * SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGainFactorAtSamplePosition_afterAddFadeAt_appliesFadeInCorrectly() {
|
||||
DefaultGainProvider provider =
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(
|
||||
5 * C.MICROS_PER_SECOND, /* durationUs= */ 2 * C.MICROS_PER_SECOND, FADE_IN_LINEAR)
|
||||
.build();
|
||||
|
||||
assertThat(provider.getGainFactorAtSamplePosition(/* samplePosition= */ 0, SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 3 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 5 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 6 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0.5f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 7 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGainFactorAtSamplePosition_afterAddFadeAt_appliesFadeOutCorrectly() {
|
||||
DefaultGainProvider provider =
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(
|
||||
/* positionUs= */ 5 * C.MICROS_PER_SECOND,
|
||||
/* durationUs= */ 4 * C.MICROS_PER_SECOND,
|
||||
FADE_OUT_LINEAR)
|
||||
.build();
|
||||
|
||||
assertThat(provider.getGainFactorAtSamplePosition(/* samplePosition= */ 0, SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 3 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 5 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 6 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0.75f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 7 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0.5f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 8 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0.25f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 9 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getGainFactorAtSamplePosition_superposedFades_keepsLastAddedFadeOnTop() {
|
||||
DefaultGainProvider provider =
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(
|
||||
/* positionUs= */ 5 * C.MICROS_PER_SECOND,
|
||||
/* durationUs= */ 5 * C.MICROS_PER_SECOND,
|
||||
FADE_IN_LINEAR)
|
||||
.addFadeAt(
|
||||
/* positionUs= */ 7 * C.MICROS_PER_SECOND,
|
||||
/* durationUs= */ C.MICROS_PER_SECOND,
|
||||
CONSTANT_VALUE_FADE)
|
||||
.build();
|
||||
|
||||
assertThat(provider.getGainFactorAtSamplePosition(/* samplePosition= */ 0, SAMPLE_RATE))
|
||||
.isEqualTo(1f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 5 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 6 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0.2f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 7 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0.5f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ (long) (7.5 * SAMPLE_RATE), SAMPLE_RATE))
|
||||
.isEqualTo(0.5f);
|
||||
assertThat(
|
||||
provider.getGainFactorAtSamplePosition(
|
||||
/* samplePosition= */ 8 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(0.6f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void linearFades_maintainEqualGain() {
|
||||
int duration = 100;
|
||||
for (int i = 0; i <= duration; i++) {
|
||||
float inGain = FADE_IN_LINEAR.getGainFactorAt(/* index= */ i, /* duration= */ duration);
|
||||
float outGain = FADE_OUT_LINEAR.getGainFactorAt(/* index= */ i, /* duration= */ duration);
|
||||
assertThat(inGain + outGain).isWithin(Math.ulp(1.0f)).of(1f);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constantPowerFades_maintainEqualPower() {
|
||||
int duration = 100;
|
||||
for (int i = 0; i <= duration; i++) {
|
||||
float inGain = FADE_IN_EQUAL_POWER.getGainFactorAt(/* index= */ i, /* duration= */ 10);
|
||||
float outGain = FADE_OUT_EQUAL_POWER.getGainFactorAt(/* index= */ i, /* duration= */ 10);
|
||||
assertThat(inGain * inGain + outGain * outGain).isWithin(Math.ulp(1.0f)).of(1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isUnityUntil_withDefaultValueSetToUnity_returnsTimeEndOfStream() {
|
||||
DefaultGainProvider provider = new DefaultGainProvider.Builder(/* defaultGain= */ 1f).build();
|
||||
assertThat(provider.isUnityUntil(/* samplePosition= */ 0, SAMPLE_RATE))
|
||||
.isEqualTo(C.TIME_END_OF_SOURCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isUnityUntil_withDefaultValueSetToZero_returnsTimeUnset() {
|
||||
DefaultGainProvider provider = new DefaultGainProvider.Builder(/* defaultGain= */ 0f).build();
|
||||
assertThat(provider.isUnityUntil(/* samplePosition= */ 0, SAMPLE_RATE)).isEqualTo(C.TIME_UNSET);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isUnityUntil_withMultipleNonUnityRegions_resolvesResultingUnityRegions() {
|
||||
DefaultGainProvider provider =
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(
|
||||
/* positionUs= */ C.MICROS_PER_SECOND,
|
||||
/* durationUs= */ C.MICROS_PER_SECOND,
|
||||
CONSTANT_VALUE_FADE)
|
||||
.addFadeAt(
|
||||
/* positionUs= */ 3 * C.MICROS_PER_SECOND,
|
||||
/* durationUs= */ C.MICROS_PER_SECOND,
|
||||
CONSTANT_VALUE_FADE)
|
||||
.build();
|
||||
assertThat(provider.isUnityUntil(/* samplePosition= */ 0, SAMPLE_RATE)).isEqualTo(SAMPLE_RATE);
|
||||
assertThat(provider.isUnityUntil(/* samplePosition= */ SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(C.TIME_UNSET);
|
||||
assertThat(provider.isUnityUntil(/* samplePosition= */ 2 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(3 * SAMPLE_RATE);
|
||||
assertThat(provider.isUnityUntil(/* samplePosition= */ 3 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(C.TIME_UNSET);
|
||||
assertThat(provider.isUnityUntil(/* samplePosition= */ 4 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(C.TIME_END_OF_SOURCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isUnityUntil_withNonUnityRegionStartingAtUnity_doesNotSkipNonUnityRegion() {
|
||||
DefaultGainProvider provider =
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(
|
||||
/* positionUs= */ C.MICROS_PER_SECOND,
|
||||
/* durationUs= */ C.MICROS_PER_SECOND,
|
||||
FADE_OUT_LINEAR)
|
||||
.build();
|
||||
assertThat(provider.isUnityUntil(/* samplePosition= */ 0, SAMPLE_RATE)).isEqualTo(SAMPLE_RATE);
|
||||
assertThat(provider.isUnityUntil(/* samplePosition= */ SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(C.TIME_UNSET);
|
||||
assertThat(provider.isUnityUntil(/* samplePosition= */ 2 * SAMPLE_RATE, SAMPLE_RATE))
|
||||
.isEqualTo(C.TIME_END_OF_SOURCE);
|
||||
}
|
||||
}
|
@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.common.audio;
|
||||
|
||||
import static androidx.media3.common.audio.AudioProcessor.EMPTY_BUFFER;
|
||||
import static androidx.media3.test.utils.TestUtil.createByteBuffer;
|
||||
import static androidx.media3.test.utils.TestUtil.createFloatArray;
|
||||
import static androidx.media3.test.utils.TestUtil.createShortArray;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.audio.AudioProcessor.AudioFormat;
|
||||
import androidx.media3.common.audio.AudioProcessor.UnhandledAudioFormatException;
|
||||
import androidx.media3.common.audio.DefaultGainProvider.FadeProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link GainProcessor}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class GainProcessorTest {
|
||||
|
||||
private static final FadeProvider CONSTANT_VALUE_FADE = (index, duration) -> 0.5f;
|
||||
|
||||
private static final DefaultGainProvider HUNDRED_US_FADE_IN_PROVIDER =
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(
|
||||
/* positionUs= */ 0L, /* durationUs= */ 100, DefaultGainProvider.FADE_IN_LINEAR)
|
||||
.build();
|
||||
|
||||
private static final AudioFormat MONO_50KHZ_16BIT_FORMAT =
|
||||
new AudioFormat(/* sampleRate= */ 50000, /* channelCount= */ 1, C.ENCODING_PCM_16BIT);
|
||||
private static final AudioFormat MONO_100KHZ_16BIT_FORMAT =
|
||||
new AudioFormat(/* sampleRate= */ 100000, /* channelCount= */ 1, C.ENCODING_PCM_16BIT);
|
||||
|
||||
private static final AudioFormat MONO_50KHZ_FLOAT_FORMAT =
|
||||
new AudioFormat(/* sampleRate= */ 50000, /* channelCount= */ 1, C.ENCODING_PCM_FLOAT);
|
||||
|
||||
@Test
|
||||
public void applyGain_withMutingGainProvider_returnsAllZeroes()
|
||||
throws UnhandledAudioFormatException {
|
||||
GainProcessor processor =
|
||||
new GainProcessor(new DefaultGainProvider.Builder(/* defaultGain= */ 0f).build());
|
||||
processor.configure(MONO_50KHZ_16BIT_FORMAT);
|
||||
processor.flush();
|
||||
|
||||
ByteBuffer input = createByteBuffer(new short[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1});
|
||||
processor.queueInput(input);
|
||||
|
||||
ByteBuffer output = processor.getOutput();
|
||||
assertThat(output.remaining()).isEqualTo(20);
|
||||
while (output.hasRemaining()) {
|
||||
assertThat(output.getShort()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyGain_withFadeIn_returnsScaledSamples() throws UnhandledAudioFormatException {
|
||||
GainProcessor processor = new GainProcessor(HUNDRED_US_FADE_IN_PROVIDER);
|
||||
processor.configure(MONO_50KHZ_16BIT_FORMAT);
|
||||
processor.flush();
|
||||
|
||||
ByteBuffer input = createByteBuffer(new short[] {100, 100, 100, 100, 100, 100, 100});
|
||||
processor.queueInput(input);
|
||||
ByteBuffer output = processor.getOutput();
|
||||
|
||||
short[] outputSamples = createShortArray(output);
|
||||
assertThat(outputSamples).isEqualTo(new short[] {0, 20, 40, 60, 80, 100, 100});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyGain_withFloatSamples_returnsScaledSamples()
|
||||
throws UnhandledAudioFormatException {
|
||||
GainProcessor processor = new GainProcessor(HUNDRED_US_FADE_IN_PROVIDER);
|
||||
processor.configure(MONO_50KHZ_FLOAT_FORMAT);
|
||||
processor.flush();
|
||||
|
||||
ByteBuffer input = createByteBuffer(new float[] {1, 1, 1, 1, 1, 1, 1});
|
||||
processor.queueInput(input);
|
||||
ByteBuffer output = processor.getOutput();
|
||||
|
||||
float[] outputSamples = createFloatArray(output);
|
||||
assertThat(outputSamples).isEqualTo(new float[] {0f, 0.2f, 0.4f, 0.6f, 0.8f, 1f, 1f});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyGain_afterSampleRateChange_stretchesFade() throws UnhandledAudioFormatException {
|
||||
GainProcessor processor = new GainProcessor(HUNDRED_US_FADE_IN_PROVIDER);
|
||||
processor.configure(MONO_50KHZ_16BIT_FORMAT);
|
||||
processor.flush();
|
||||
|
||||
ByteBuffer input = createByteBuffer(new short[] {100, 100, 100, 100, 100, 100, 100});
|
||||
processor.queueInput(input);
|
||||
ByteBuffer output = processor.getOutput();
|
||||
|
||||
short[] outputSamples = createShortArray(output);
|
||||
assertThat(outputSamples).isEqualTo(new short[] {0, 20, 40, 60, 80, 100, 100});
|
||||
|
||||
processor.configure(MONO_100KHZ_16BIT_FORMAT);
|
||||
processor.flush();
|
||||
input.rewind();
|
||||
processor.queueInput(input);
|
||||
output.clear();
|
||||
output = processor.getOutput();
|
||||
|
||||
outputSamples = createShortArray(output);
|
||||
assertThat(outputSamples).isEqualTo(new short[] {0, 10, 20, 30, 40, 50, 60});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyGain_withMultipleQueueInputCalls_appliesGainAtCorrectPosition()
|
||||
throws UnhandledAudioFormatException {
|
||||
GainProcessor processor =
|
||||
new GainProcessor(
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(/* positionUs= */ 100, /* durationUs= */ 100, CONSTANT_VALUE_FADE)
|
||||
.build());
|
||||
processor.configure(MONO_50KHZ_16BIT_FORMAT);
|
||||
processor.flush();
|
||||
|
||||
ByteBuffer input = createByteBuffer(new short[] {100, 100, 100, 100, 100});
|
||||
processor.queueInput(input);
|
||||
ByteBuffer output = processor.getOutput();
|
||||
|
||||
short[] outputSamples = createShortArray(output);
|
||||
assertThat(outputSamples).isEqualTo(new short[] {100, 100, 100, 100, 100});
|
||||
|
||||
input.rewind();
|
||||
processor.queueInput(input);
|
||||
output.clear();
|
||||
output = processor.getOutput();
|
||||
|
||||
outputSamples = createShortArray(output);
|
||||
assertThat(outputSamples).isEqualTo(new short[] {50, 50, 50, 50, 50});
|
||||
|
||||
input.rewind();
|
||||
processor.queueInput(input);
|
||||
output.clear();
|
||||
output = processor.getOutput();
|
||||
|
||||
outputSamples = createShortArray(output);
|
||||
assertThat(outputSamples).isEqualTo(new short[] {100, 100, 100, 100, 100});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyGain_withSingleQueueInputCall_appliesGainAtCorrectPosition()
|
||||
throws UnhandledAudioFormatException {
|
||||
GainProcessor processor =
|
||||
new GainProcessor(
|
||||
new DefaultGainProvider.Builder(/* defaultGain= */ 1f)
|
||||
.addFadeAt(/* positionUs= */ 100, /* durationUs= */ 100, CONSTANT_VALUE_FADE)
|
||||
.build());
|
||||
processor.configure(MONO_50KHZ_16BIT_FORMAT);
|
||||
processor.flush();
|
||||
|
||||
// 15 mono frames set to 100.
|
||||
ByteBuffer input =
|
||||
createByteBuffer(
|
||||
new short[] {
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100
|
||||
});
|
||||
processor.queueInput(input);
|
||||
ByteBuffer output = processor.getOutput();
|
||||
|
||||
short[] outputSamples = createShortArray(output);
|
||||
// 5 frames at unity + 5 frames with gain 0.5 (100 * 0.5 = 50) + 5 frames with at unity.
|
||||
assertThat(outputSamples)
|
||||
.isEqualTo(
|
||||
new short[] {100, 100, 100, 100, 100, 50, 50, 50, 50, 50, 100, 100, 100, 100, 100});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEnded_afterQueueEndOfStreamWithNoPendingOutput_returnsTrue()
|
||||
throws UnhandledAudioFormatException {
|
||||
GainProcessor processor = new GainProcessor(HUNDRED_US_FADE_IN_PROVIDER);
|
||||
processor.configure(MONO_50KHZ_16BIT_FORMAT);
|
||||
processor.flush();
|
||||
|
||||
ByteBuffer input = createByteBuffer(new short[] {100, 100, 100, 100, 100, 100, 100});
|
||||
processor.queueInput(input);
|
||||
processor.queueEndOfStream();
|
||||
|
||||
assertThat(processor.isEnded()).isFalse();
|
||||
processor.getOutput();
|
||||
assertThat(processor.isEnded()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queueInput_beforeConfigureAndFlush_throwsIllegalStateException()
|
||||
throws UnhandledAudioFormatException {
|
||||
GainProcessor processor = new GainProcessor(HUNDRED_US_FADE_IN_PROVIDER);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> processor.queueInput(EMPTY_BUFFER));
|
||||
processor.configure(MONO_50KHZ_16BIT_FORMAT);
|
||||
assertThrows(IllegalStateException.class, () -> processor.queueInput(EMPTY_BUFFER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configure_withUnsupportedEncoding_throwsUnhandledAudioFormatException() {
|
||||
GainProcessor processor = new GainProcessor(HUNDRED_US_FADE_IN_PROVIDER);
|
||||
assertThrows(
|
||||
UnhandledAudioFormatException.class,
|
||||
() ->
|
||||
processor.configure(
|
||||
new AudioFormat(
|
||||
/* sampleRate= */ 50000,
|
||||
/* channelCount= */ 1,
|
||||
C.ENCODING_PCM_16BIT_BIG_ENDIAN)));
|
||||
assertThrows(
|
||||
UnhandledAudioFormatException.class,
|
||||
() ->
|
||||
processor.configure(
|
||||
new AudioFormat(
|
||||
/* sampleRate= */ 50000,
|
||||
/* channelCount= */ 1,
|
||||
C.ENCODING_PCM_24BIT_BIG_ENDIAN)));
|
||||
assertThrows(
|
||||
UnhandledAudioFormatException.class,
|
||||
() ->
|
||||
processor.configure(
|
||||
new AudioFormat(
|
||||
/* sampleRate= */ 50000,
|
||||
/* channelCount= */ 1,
|
||||
C.ENCODING_PCM_32BIT_BIG_ENDIAN)));
|
||||
assertThrows(
|
||||
UnhandledAudioFormatException.class,
|
||||
() ->
|
||||
processor.configure(
|
||||
new AudioFormat(
|
||||
/* sampleRate= */ 50000, /* channelCount= */ 1, C.ENCODING_INVALID)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isActive_withConstantGainProviderAtUnity_returnsFalse()
|
||||
throws UnhandledAudioFormatException {
|
||||
GainProcessor processor =
|
||||
new GainProcessor(new DefaultGainProvider.Builder(/* defaultGain= */ 1).build());
|
||||
processor.configure(MONO_50KHZ_FLOAT_FORMAT);
|
||||
processor.flush();
|
||||
assertThat(processor.isActive()).isFalse();
|
||||
}
|
||||
}
|
@ -1057,6 +1057,9 @@ public final class AndroidTestUtil {
|
||||
public static final AssetInfo WAV_192KHZ_ASSET =
|
||||
new AssetInfo.Builder("asset:///media/wav/sample_192khz.wav").build();
|
||||
|
||||
public static final AssetInfo FLAC_STEREO_ASSET =
|
||||
new AssetInfo.Builder("asset:///media/flac/bear.flac").build();
|
||||
|
||||
/** A {@link GlEffect} that adds delay in the video pipeline by putting the thread to sleep. */
|
||||
public static final class DelayEffect implements GlEffect {
|
||||
private final long delayMs;
|
||||
|
@ -82,6 +82,8 @@ import androidx.media3.common.audio.AudioProcessor;
|
||||
import androidx.media3.common.audio.AudioProcessor.AudioFormat;
|
||||
import androidx.media3.common.audio.ChannelMixingAudioProcessor;
|
||||
import androidx.media3.common.audio.ChannelMixingMatrix;
|
||||
import androidx.media3.common.audio.DefaultGainProvider;
|
||||
import androidx.media3.common.audio.GainProcessor;
|
||||
import androidx.media3.common.audio.SonicAudioProcessor;
|
||||
import androidx.media3.common.audio.SpeedProvider;
|
||||
import androidx.media3.common.util.CodecSpecificDataUtil;
|
||||
@ -113,6 +115,7 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.io.File;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@ -2493,6 +2496,40 @@ public class TransformerEndToEndTest {
|
||||
assertThat(new File(result.filePath).length()).isGreaterThan(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void export_withMutingGainProvider_processesMutedAudio() throws Exception {
|
||||
Transformer transformer = new Transformer.Builder(context).build();
|
||||
DefaultGainProvider provider = new DefaultGainProvider.Builder(/* defaultGain= */ 0f).build();
|
||||
GainProcessor gainProcessor = new GainProcessor(provider);
|
||||
TeeAudioProcessor teeAudioProcessor =
|
||||
new TeeAudioProcessor(
|
||||
new TeeAudioProcessor.AudioBufferSink() {
|
||||
@Override
|
||||
public void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {}
|
||||
|
||||
@Override
|
||||
public void handleBuffer(ByteBuffer buffer) {
|
||||
ShortBuffer samplesBuffer = buffer.asShortBuffer();
|
||||
while (samplesBuffer.hasRemaining()) {
|
||||
assertThat(samplesBuffer.get()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
EditedMediaItem item =
|
||||
new EditedMediaItem.Builder(MediaItem.fromUri(WAV_ASSET.uri))
|
||||
.setEffects(
|
||||
new Effects(ImmutableList.of(gainProcessor, teeAudioProcessor), ImmutableList.of()))
|
||||
.build();
|
||||
|
||||
ExportTestResult result =
|
||||
new TransformerAndroidTestRunner.Builder(context, transformer).build().run(testId, item);
|
||||
|
||||
// Tolerance required due to bug in durationMs (b/355201372).
|
||||
assertThat(result.exportResult.durationMs).isWithin(40).of(1000);
|
||||
assertThat(new File(result.filePath).length()).isGreaterThan(0);
|
||||
}
|
||||
|
||||
private static boolean shouldSkipDeviceForAacObjectHeProfileEncoding() {
|
||||
return Util.SDK_INT < 29;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user