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 =
|
public static final AssetInfo WAV_192KHZ_ASSET =
|
||||||
new AssetInfo.Builder("asset:///media/wav/sample_192khz.wav").build();
|
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. */
|
/** A {@link GlEffect} that adds delay in the video pipeline by putting the thread to sleep. */
|
||||||
public static final class DelayEffect implements GlEffect {
|
public static final class DelayEffect implements GlEffect {
|
||||||
private final long delayMs;
|
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.AudioProcessor.AudioFormat;
|
||||||
import androidx.media3.common.audio.ChannelMixingAudioProcessor;
|
import androidx.media3.common.audio.ChannelMixingAudioProcessor;
|
||||||
import androidx.media3.common.audio.ChannelMixingMatrix;
|
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.SonicAudioProcessor;
|
||||||
import androidx.media3.common.audio.SpeedProvider;
|
import androidx.media3.common.audio.SpeedProvider;
|
||||||
import androidx.media3.common.util.CodecSpecificDataUtil;
|
import androidx.media3.common.util.CodecSpecificDataUtil;
|
||||||
@ -113,6 +115,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ShortBuffer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@ -2493,6 +2496,40 @@ public class TransformerEndToEndTest {
|
|||||||
assertThat(new File(result.filePath).length()).isGreaterThan(0);
|
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() {
|
private static boolean shouldSkipDeviceForAacObjectHeProfileEncoding() {
|
||||||
return Util.SDK_INT < 29;
|
return Util.SDK_INT < 29;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user