blob: a9afa4719880c6827945784817773b097d810742 [file] [log] [blame]
/*
* Copyright (C) 2018 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 com.google.android.exoplayer2.audio;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Audio processor that outputs its input unmodified and also outputs its input to a given sink.
* This is intended to be used for diagnostics and debugging.
*
* <p>This audio processor can be inserted into the audio processor chain to access audio data
* before/after particular processing steps have been applied. For example, to get audio output
* after playback speed adjustment and silence skipping have been applied it is necessary to pass a
* custom {@link com.google.android.exoplayer2.audio.DefaultAudioSink.AudioProcessorChain} when
* creating the audio sink, and include this audio processor after all other audio processors.
*/
public final class TeeAudioProcessor extends BaseAudioProcessor {
/** A sink for audio buffers handled by the audio processor. */
public interface AudioBufferSink {
/** Called when the audio processor is flushed with a format of subsequent input. */
void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding);
/**
* Called when data is written to the audio processor.
*
* @param buffer A read-only buffer containing input which the audio processor will handle.
*/
void handleBuffer(ByteBuffer buffer);
}
private final AudioBufferSink audioBufferSink;
/**
* Creates a new tee audio processor, sending incoming data to the given {@link AudioBufferSink}.
*
* @param audioBufferSink The audio buffer sink that will receive input queued to this audio
* processor.
*/
public TeeAudioProcessor(AudioBufferSink audioBufferSink) {
this.audioBufferSink = Assertions.checkNotNull(audioBufferSink);
}
@Override
public AudioFormat onConfigure(AudioFormat inputAudioFormat) {
// This processor is always active (if passed to the sink) and outputs its input.
return inputAudioFormat;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
int remaining = inputBuffer.remaining();
if (remaining == 0) {
return;
}
audioBufferSink.handleBuffer(inputBuffer.asReadOnlyBuffer());
replaceOutputBuffer(remaining).put(inputBuffer).flip();
}
@Override
protected void onFlush() {
flushSinkIfActive();
}
@Override
protected void onQueueEndOfStream() {
flushSinkIfActive();
}
@Override
protected void onReset() {
flushSinkIfActive();
}
private void flushSinkIfActive() {
if (isActive()) {
audioBufferSink.flush(
inputAudioFormat.sampleRate, inputAudioFormat.channelCount, inputAudioFormat.encoding);
}
}
/**
* A sink for audio buffers that writes output audio as .wav files with a given path prefix. When
* new audio data is handled after flushing the audio processor, a counter is incremented and its
* value is appended to the output file name.
*
* <p>Note: if writing to external storage it's necessary to grant the {@code
* WRITE_EXTERNAL_STORAGE} permission.
*/
public static final class WavFileAudioBufferSink implements AudioBufferSink {
private static final String TAG = "WaveFileAudioBufferSink";
private static final int FILE_SIZE_MINUS_8_OFFSET = 4;
private static final int FILE_SIZE_MINUS_44_OFFSET = 40;
private static final int HEADER_LENGTH = 44;
private final String outputFileNamePrefix;
private final byte[] scratchBuffer;
private final ByteBuffer scratchByteBuffer;
private int sampleRateHz;
private int channelCount;
@C.PcmEncoding private int encoding;
@Nullable private RandomAccessFile randomAccessFile;
private int counter;
private int bytesWritten;
/**
* Creates a new audio buffer sink that writes to .wav files with the given prefix.
*
* @param outputFileNamePrefix The prefix for output files.
*/
public WavFileAudioBufferSink(String outputFileNamePrefix) {
this.outputFileNamePrefix = outputFileNamePrefix;
scratchBuffer = new byte[1024];
scratchByteBuffer = ByteBuffer.wrap(scratchBuffer).order(ByteOrder.LITTLE_ENDIAN);
}
@Override
public void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {
try {
reset();
} catch (IOException e) {
Log.e(TAG, "Error resetting", e);
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
this.encoding = encoding;
}
@Override
public void handleBuffer(ByteBuffer buffer) {
try {
maybePrepareFile();
writeBuffer(buffer);
} catch (IOException e) {
Log.e(TAG, "Error writing data", e);
}
}
private void maybePrepareFile() throws IOException {
if (randomAccessFile != null) {
return;
}
RandomAccessFile randomAccessFile = new RandomAccessFile(getNextOutputFileName(), "rw");
writeFileHeader(randomAccessFile);
this.randomAccessFile = randomAccessFile;
bytesWritten = HEADER_LENGTH;
}
private void writeFileHeader(RandomAccessFile randomAccessFile) throws IOException {
// Write the start of the header as big endian data.
randomAccessFile.writeInt(WavUtil.RIFF_FOURCC);
randomAccessFile.writeInt(-1);
randomAccessFile.writeInt(WavUtil.WAVE_FOURCC);
randomAccessFile.writeInt(WavUtil.FMT_FOURCC);
// Write the rest of the header as little endian data.
scratchByteBuffer.clear();
scratchByteBuffer.putInt(16);
scratchByteBuffer.putShort((short) WavUtil.getTypeForPcmEncoding(encoding));
scratchByteBuffer.putShort((short) channelCount);
scratchByteBuffer.putInt(sampleRateHz);
int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount);
scratchByteBuffer.putInt(bytesPerSample * sampleRateHz);
scratchByteBuffer.putShort((short) bytesPerSample);
scratchByteBuffer.putShort((short) (8 * bytesPerSample / channelCount));
randomAccessFile.write(scratchBuffer, 0, scratchByteBuffer.position());
// Write the start of the data chunk as big endian data.
randomAccessFile.writeInt(WavUtil.DATA_FOURCC);
randomAccessFile.writeInt(-1);
}
private void writeBuffer(ByteBuffer buffer) throws IOException {
RandomAccessFile randomAccessFile = Assertions.checkNotNull(this.randomAccessFile);
while (buffer.hasRemaining()) {
int bytesToWrite = Math.min(buffer.remaining(), scratchBuffer.length);
buffer.get(scratchBuffer, 0, bytesToWrite);
randomAccessFile.write(scratchBuffer, 0, bytesToWrite);
bytesWritten += bytesToWrite;
}
}
private void reset() throws IOException {
@Nullable RandomAccessFile randomAccessFile = this.randomAccessFile;
if (randomAccessFile == null) {
return;
}
try {
scratchByteBuffer.clear();
scratchByteBuffer.putInt(bytesWritten - 8);
randomAccessFile.seek(FILE_SIZE_MINUS_8_OFFSET);
randomAccessFile.write(scratchBuffer, 0, 4);
scratchByteBuffer.clear();
scratchByteBuffer.putInt(bytesWritten - 44);
randomAccessFile.seek(FILE_SIZE_MINUS_44_OFFSET);
randomAccessFile.write(scratchBuffer, 0, 4);
} catch (IOException e) {
// The file may still be playable, so just log a warning.
Log.w(TAG, "Error updating file size", e);
}
try {
randomAccessFile.close();
} finally {
this.randomAccessFile = null;
}
}
private String getNextOutputFileName() {
return Util.formatInvariant("%s-%04d.wav", outputFileNamePrefix, counter++);
}
}
}