SilentAudioStream.java
/*
* Copyright 2023 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
*
* http://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.camera.video.internal.audio;
import static androidx.camera.video.internal.audio.AudioUtils.frameCountToDurationNs;
import static androidx.camera.video.internal.audio.AudioUtils.frameCountToSize;
import static androidx.camera.video.internal.audio.AudioUtils.sizeToFrameCount;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.Logger;
import java.nio.ByteBuffer;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* An AudioStream that only outputs silent audio.
*
* <p>This class is not thread safe, it should be used on the same thread.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class SilentAudioStream implements AudioStream {
private static final String TAG = "SilentAudioStream";
private final AtomicBoolean mIsStarted = new AtomicBoolean(false);
private final AtomicBoolean mIsReleased = new AtomicBoolean(false);
private final int mBytesPerFrame;
private final int mSampleRate;
@Nullable
private byte[] mZeroBytes;
private long mCurrentReadTimeNs;
@Nullable
private AudioStreamCallback mAudioStreamCallback;
@Nullable
private Executor mCallbackExecutor;
/**
* Constructs the instance.
*
* @param audioSettings the audio settings.
*/
public SilentAudioStream(@NonNull AudioSettings audioSettings) {
mBytesPerFrame = audioSettings.getBytesPerFrame();
mSampleRate = audioSettings.getSampleRate();
}
@Override
public void setCallback(@Nullable AudioStreamCallback callback, @Nullable Executor executor) {
checkState(!mIsStarted.get(), "AudioStream can not be started when setCallback.");
checkNotReleasedOrThrow();
checkArgument(callback == null || executor != null,
"executor can't be null with non-null callback.");
mAudioStreamCallback = callback;
mCallbackExecutor = executor;
}
@Override
public void start() {
checkNotReleasedOrThrow();
if (mIsStarted.getAndSet(true)) {
return;
}
mCurrentReadTimeNs = currentSystemTimeNs();
notifySilenced();
}
@Override
public void stop() {
checkNotReleasedOrThrow();
mIsStarted.set(false);
}
@Override
public void release() {
mIsReleased.getAndSet(true);
}
@NonNull
@Override
public PacketInfo read(@NonNull ByteBuffer byteBuffer) {
checkNotReleasedOrThrow();
checkStartedOrThrow();
long requiredFrameCount = sizeToFrameCount(byteBuffer.remaining(), mBytesPerFrame);
int requiredSize = (int) frameCountToSize(requiredFrameCount, mBytesPerFrame);
if (requiredSize <= 0) {
return PacketInfo.of(0, mCurrentReadTimeNs);
}
long requiredDurationNs = frameCountToDurationNs(requiredFrameCount, mSampleRate);
long nextReadTimeNs = mCurrentReadTimeNs + requiredDurationNs;
blockUntilSystemTimeReached(nextReadTimeNs);
writeSilenceToBuffer(byteBuffer, requiredSize);
PacketInfo packetInfo = PacketInfo.of(requiredSize, mCurrentReadTimeNs);
mCurrentReadTimeNs = nextReadTimeNs;
return packetInfo;
}
private void writeSilenceToBuffer(@NonNull ByteBuffer byteBuffer, int sizeInBytes) {
checkState(sizeInBytes <= byteBuffer.remaining());
if (mZeroBytes == null || mZeroBytes.length < sizeInBytes) {
mZeroBytes = new byte[sizeInBytes];
}
int originalPosition = byteBuffer.position();
byteBuffer.put(mZeroBytes, 0, sizeInBytes)
.limit(originalPosition + sizeInBytes)
.position(originalPosition);
}
private void notifySilenced() {
AudioStreamCallback callback = mAudioStreamCallback;
Executor executor = mCallbackExecutor;
if (callback != null && executor != null) {
executor.execute(() -> callback.onSilenceStateChanged(true));
}
}
private void checkNotReleasedOrThrow() {
checkState(!mIsReleased.get(), "AudioStream has been released.");
}
private void checkStartedOrThrow() {
checkState(mIsStarted.get(), "AudioStream has not been started.");
}
private static long currentSystemTimeNs() {
return System.nanoTime();
}
// To avoid writing silence too fast, delay a while if the current system time haven't reach
// the next read time.
private static void blockUntilSystemTimeReached(long nextReadTimeNs) {
long requiredBlockTimeNs = nextReadTimeNs - currentSystemTimeNs();
if (requiredBlockTimeNs > 0L) {
try {
Thread.sleep(TimeUnit.NANOSECONDS.toMillis(requiredBlockTimeNs));
} catch (InterruptedException e) {
Logger.w(TAG, "Ignore interruption", e);
}
}
}
}