mirror of
https://github.com/samsonjs/media.git
synced 2026-03-26 09:35:47 +00:00
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:
parent
68add98c23
commit
ded2b2eb2a
3 changed files with 396 additions and 35 deletions
|
|
@ -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++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue