Allow outputting audio to a WAV file

Add TeeAudioProcessor that doesn't modify the input audio but writes it to an
AudioBufferSink, and WavFileAudioBufferSink for writing audio to a .wav file.

This is intended to be used for diagnostics and debugging.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=206717458
This commit is contained in:
andrewlewis 2018-07-31 00:54:38 -07:00 committed by Oliver Woodman
parent 68add98c23
commit ded2b2eb2a
3 changed files with 396 additions and 35 deletions

View file

@ -0,0 +1,304 @@
/*
* 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 android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
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 implements AudioProcessor {
/** 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.Encoding 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;
private int sampleRateHz;
private int channelCount;
private @C.Encoding int encoding;
private boolean isActive;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
/**
* 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);
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
}
@Override
public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
throws UnhandledFormatException {
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
this.encoding = encoding;
boolean wasActive = isActive;
isActive = true;
return !wasActive;
}
@Override
public boolean isActive() {
return isActive;
}
@Override
public int getOutputChannelCount() {
return channelCount;
}
@Override
public int getOutputEncoding() {
return encoding;
}
@Override
public int getOutputSampleRateHz() {
return sampleRateHz;
}
@Override
public void queueInput(ByteBuffer buffer) {
int remaining = buffer.remaining();
if (remaining == 0) {
return;
}
audioBufferSink.handleBuffer(buffer.asReadOnlyBuffer());
if (this.buffer.capacity() < remaining) {
this.buffer = ByteBuffer.allocateDirect(remaining).order(ByteOrder.nativeOrder());
} else {
this.buffer.clear();
}
this.buffer.put(buffer);
this.buffer.flip();
outputBuffer = this.buffer;
}
@Override
public void queueEndOfStream() {
inputEnded = true;
}
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && buffer == EMPTY_BUFFER;
}
@Override
public void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
audioBufferSink.flush(sampleRateHz, channelCount, encoding);
}
@Override
public void reset() {
flush();
buffer = EMPTY_BUFFER;
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
encoding = Format.NO_VALUE;
}
/**
* 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;
private @C.Encoding int encoding;
private @Nullable 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, 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.getTypeForEncoding(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 {
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++);
}
}
}

View file

@ -0,0 +1,86 @@
/*
* 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 com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util;
/** Utilities for handling WAVE files. */
public final class WavUtil {
/** Four character code for "RIFF". */
public static final int RIFF_FOURCC = Util.getIntegerCodeForString("RIFF");
/** Four character code for "WAVE". */
public static final int WAVE_FOURCC = Util.getIntegerCodeForString("WAVE");
/** Four character code for "fmt ". */
public static final int FMT_FOURCC = Util.getIntegerCodeForString("fmt ");
/** Four character code for "data". */
public static final int DATA_FOURCC = Util.getIntegerCodeForString("data");
/** WAVE type value for integer PCM audio data. */
private static final int TYPE_PCM = 0x0001;
/** WAVE type value for float PCM audio data. */
private static final int TYPE_FLOAT = 0x0003;
/** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */
private static final int TYPE_A_LAW = 0x0006;
/** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */
private static final int TYPE_MU_LAW = 0x0007;
/** WAVE type value for extended WAVE format. */
private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
/** Returns the WAVE type value for the given {@code encoding}. */
public static int getTypeForEncoding(@C.PcmEncoding int encoding) {
switch (encoding) {
case C.ENCODING_PCM_8BIT:
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_24BIT:
case C.ENCODING_PCM_32BIT:
return TYPE_PCM;
case C.ENCODING_PCM_A_LAW:
return TYPE_A_LAW;
case C.ENCODING_PCM_MU_LAW:
return TYPE_MU_LAW;
case C.ENCODING_PCM_FLOAT:
return TYPE_FLOAT;
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
throw new IllegalArgumentException();
}
}
/** Returns the PCM encoding for the given WAVE {@code type} value. */
public static @C.PcmEncoding int getEncodingForType(int type, int bitsPerSample) {
switch (type) {
case TYPE_PCM:
case TYPE_WAVE_FORMAT_EXTENSIBLE:
return Util.getPcmEncoding(bitsPerSample);
case TYPE_FLOAT:
return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID;
case TYPE_A_LAW:
return C.ENCODING_PCM_A_LAW;
case TYPE_MU_LAW:
return C.ENCODING_PCM_MU_LAW;
default:
return C.ENCODING_INVALID;
}
}
private WavUtil() {
// Prevent instantiation.
}
}

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.wav;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.audio.WavUtil;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray;
@ -29,17 +30,6 @@ import java.io.IOException;
private static final String TAG = "WavHeaderReader";
/** Integer PCM audio data. */
private static final int TYPE_PCM = 0x0001;
/** Float PCM audio data. */
private static final int TYPE_FLOAT = 0x0003;
/** 8-bit ITU-T G.711 A-law audio data. */
private static final int TYPE_A_LAW = 0x0006;
/** 8-bit ITU-T G.711 mu-law audio data. */
private static final int TYPE_MU_LAW = 0x0007;
/** Extended WAVE format. */
private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
/**
* Peeks and returns a {@code WavHeader}.
*
@ -58,21 +48,21 @@ import java.io.IOException;
// Attempt to read the RIFF chunk.
ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
if (chunkHeader.id != Util.getIntegerCodeForString("RIFF")) {
if (chunkHeader.id != WavUtil.RIFF_FOURCC) {
return null;
}
input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0);
int riffFormat = scratch.readInt();
if (riffFormat != Util.getIntegerCodeForString("WAVE")) {
if (riffFormat != WavUtil.WAVE_FOURCC) {
Log.e(TAG, "Unsupported RIFF format: " + riffFormat);
return null;
}
// Skip chunks until we find the format chunk.
chunkHeader = ChunkHeader.peek(input, scratch);
while (chunkHeader.id != Util.getIntegerCodeForString("fmt ")) {
while (chunkHeader.id != WavUtil.FMT_FOURCC) {
input.advancePeekPosition((int) chunkHeader.size);
chunkHeader = ChunkHeader.peek(input, scratch);
}
@ -93,28 +83,9 @@ import java.io.IOException;
+ blockAlignment);
}
@C.PcmEncoding int encoding;
switch (type) {
case TYPE_PCM:
case TYPE_WAVE_FORMAT_EXTENSIBLE:
encoding = Util.getPcmEncoding(bitsPerSample);
break;
case TYPE_FLOAT:
encoding = bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID;
break;
case TYPE_A_LAW:
encoding = C.ENCODING_PCM_A_LAW;
break;
case TYPE_MU_LAW:
encoding = C.ENCODING_PCM_MU_LAW;
break;
default:
Log.e(TAG, "Unsupported WAV format type: " + type);
return null;
}
@C.PcmEncoding int encoding = WavUtil.getEncodingForType(type, bitsPerSample);
if (encoding == C.ENCODING_INVALID) {
Log.e(TAG, "Unsupported WAV bit depth " + bitsPerSample + " for type " + type);
Log.e(TAG, "Unsupported WAV format: " + bitsPerSample + " bit/sample, type " + type);
return null;
}