| /* |
| * 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++); |
| } |
| } |
| } |