Skip to content

Instantly share code, notes, and snippets.

@saiday
Created October 29, 2022 13:25
Show Gist options
  • Save saiday/39700858a5c30ac26ac6656a96349c3a to your computer and use it in GitHub Desktop.
Save saiday/39700858a5c30ac26ac6656a96349c3a to your computer and use it in GitHub Desktop.
package com.streetvoice.streetvoice.player;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.Arrays;
import java.util.LinkedList;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;
public class SimpleEncoder {
private Boolean VERBOSE = false;
private String TAG = "SimpleEncoder";
private int DECODE_INPUT_SIZE = 524288; // 524288 Bytes = 0.5 MB
private int BUFFER_OVERFLOW_SAFE_GATE = 5000;
// Analog audio is recorded by sampling it 44,100 times per second, and then these samples are used to reconstruct the audio signal when playing it back.
// ref:https://en.wikipedia.org/wiki/44,100_Hz#Related_rates
public static final int DEFAULT_SAMPLE_RATE = 44100;
private ProgressListener mProgressListener = null;
private File mInputFile = null;
// Member variables representing frame data
private String mFileType;
private int mFileSize;
private int mAvgBitRate; // Average bit rate in kbps.
private int mSampleRate;
private int mChannels;
private long mDuration;
private int mNumSamples; // total number of samples per channel in audio file
private ByteBuffer mDecodedBytes; // Raw audio data
private ShortBuffer mDecodedSamples; // shared buffer with mDecodedBytes.
// mDecodedSamples has the following format:
// {s1c1, s1c2, ..., s1cM, s2c1, ..., s2cM, ..., sNc1, ..., sNcM}
// where sicj is the ith sample of the jth channel (a sample is a signed short)
// M is the number of channels (e.g. 2 for stereo) and N is the number of samples per channel.
private MediaCodec mAudioDecoder = null;
private MediaFormat mDecoderOutputAudioFormat = null;
private boolean mAudioExtractorDone = false;
private boolean mAudioInputBufferEOF = false;
private boolean mAudioDecoderDone = false;
private MediaExtractor mAudioExtractor = null;
private int mAudioExtractedFrameCount = 0;
private int mAudioDecodedFrameCount = 0;
private int mAudioExtractedTotalSize = 0;
private int decodedSamplesSize = 0; // size of the output buffer containing decoded samples.
private byte[] decodedSamples = null;
private LinkedList<Integer> mPendingAudioDecoderOutputBufferIndices;
private LinkedList<MediaCodec.BufferInfo> mPendingAudioDecoderOutputBufferInfos;
// Progress listener interface.
public interface ProgressListener {
/**
* Will be called by the SoundFile class periodically
* with values between 0.0 and 1.0. Return true to continue
* loading the file or recording the audio, and false to cancel or stop recording.
*/
boolean reportProgress(double fractionComplete);
}
// Custom exception for invalid inputs.
public class InvalidInputException extends Exception {
public InvalidInputException(String message) {
super(message);
}
}
public static String[] getSupportedExtensions() {
return new String[]{"mp3", "wav", "3gpp", "3gp", "amr", "aac", "m4a", "ogg"};
}
// Create and return a SimpleEncoder object using the file fileName.
public static SimpleEncoder create(String fileName,
ProgressListener progressListener)
throws FileNotFoundException,
java.io.IOException, com.streetvoice.streetvoice.utils.soundfile.SoundFile.InvalidInputException, InvalidInputException {
// First check that the file exists and that its extension is supported.
File f = new File(fileName);
if (!f.exists()) {
throw new java.io.FileNotFoundException(fileName);
}
String name = f.getName().toLowerCase();
String[] components = name.split("\\.");
if (components.length < 2) {
return null;
}
if (!Arrays.asList(getSupportedExtensions()).contains(components[components.length - 1])) {
return null;
}
SimpleEncoder simpleEncoder = new SimpleEncoder();
simpleEncoder.setProgressListener(progressListener);
simpleEncoder.ReadFile(f);
return simpleEncoder;
}
public String getFiletype() {
return mFileType;
}
public int getFileSizeBytes() {
return mFileSize;
}
public int getAvgBitrateKbps() {
return mAvgBitRate;
}
public int getSampleRate() {
return mSampleRate;
}
public int getChannels() {
return mChannels;
}
public long getDuration() {
return mDuration;
}
public int getNumSamples() {
return mNumSamples; // Number of samples per channel.
}
public ShortBuffer getSamples() {
return mDecodedSamples;
}
private SimpleEncoder() {
}
private void setProgressListener(ProgressListener progressListener) {
mProgressListener = progressListener;
}
private void logState() {
if (VERBOSE) {
Log.d(TAG, String.format(
"loop: "
+ "{"
+ "extracted:%d(done:%b) "
+ "decoded:%d(done:%b) ",
mAudioExtractedFrameCount, mAudioExtractorDone,
mAudioDecodedFrameCount, mAudioDecoderDone
));
}
}
private void decodeAudio() {
if (mPendingAudioDecoderOutputBufferIndices.size() == 0) {
return;
}
int decoderIndex = mPendingAudioDecoderOutputBufferIndices.poll();
MediaCodec.BufferInfo info = mPendingAudioDecoderOutputBufferInfos.poll();
int size = info.size;
long presentationTime = info.presentationTimeUs;
if (VERBOSE) {
Log.d(TAG, "audio decoder: processing pending buffer: "
+ decoderIndex);
Log.d(TAG, "audio decoder: pending buffer of size " + size);
Log.d(TAG, "audio decoder: pending buffer for time " + presentationTime);
}
if (size >= 0) {
ByteBuffer decoderOutputBuffer = mAudioDecoder.getOutputBuffer(decoderIndex).duplicate();
if (decodedSamplesSize < info.size) {
decodedSamplesSize = info.size;
decodedSamples = new byte[decodedSamplesSize];
}
decoderOutputBuffer.get(decodedSamples, 0, info.size);
mAudioDecoder.releaseOutputBuffer(decoderIndex, false);
// Check if buffer is big enough. Resize it if it's too small.
if (mDecodedBytes.remaining() < info.size) {
// Getting a rough estimate of the total size, allocate 20% more, and
// make sure to allocate at least 5MB more than the initial size.
int position = mDecodedBytes.position();
int newSize = (int) ((position * (1.0 * mFileSize / mAudioExtractedTotalSize)) * 1.2);
if (newSize - position < info.size + 5 * (1 << 20)) {
newSize = position + info.size + 5 * (1 << 20);
}
ByteBuffer newDecodedBytes = null;
// Try to allocate memory. If we are OOM, try to run the garbage collector.
int retry = 10;
while (retry > 0) {
try {
newDecodedBytes = ByteBuffer.allocate(newSize);
break;
} catch (OutOfMemoryError oome) {
// setting android:largeHeap="true" in <application> seem to help not
// reaching this section.
retry--;
}
}
if (retry == 0) {
// Failed to allocate memory... Stop reading more data and finalize the
// instance with the data decoded so far.
Log.e(TAG, "Failed to allocate memory... Stop reading more data");
}
//ByteBuffer newDecodedBytes = ByteBuffer.allocate(newSize);
mDecodedBytes.rewind();
newDecodedBytes.put(mDecodedBytes);
mDecodedBytes = newDecodedBytes;
mDecodedBytes.position(position);
}
mDecodedBytes.put(decodedSamples, 0, info.size);
}
if ((info.flags
& MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(TAG, "audio decoder: EOS");
synchronized (this) {
mAudioDecoderDone = true;
notifyAll();
}
}
logState();
}
/**
* Creates a decoder for the given format.
*
* @param inputFormat the format of the stream to decode
*/
private MediaCodec createAudioDecoder(MediaFormat inputFormat) throws IOException {
inputFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, DECODE_INPUT_SIZE); // huge throughput
MediaCodec decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME));
decoder.setCallback(new MediaCodec.Callback() {
public void onError(MediaCodec codec, MediaCodec.CodecException exception) {
Log.e(TAG, exception.toString());
}
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
mDecoderOutputAudioFormat = codec.getOutputFormat();
if (VERBOSE) {
Log.d(TAG, "audio decoder: output format changed: "
+ mDecoderOutputAudioFormat);
}
}
public void onInputBufferAvailable(MediaCodec codec, int index) {
ByteBuffer decoderInputBuffer = codec.getInputBuffer(index);
while (!mAudioExtractorDone && !mAudioInputBufferEOF) {
int bufferChunkSize = 0;
long presentationTime = 0;
while (true) {
ByteBuffer tempBuffer = ByteBuffer.allocate(1 << 10);
int size = mAudioExtractor.readSampleData(tempBuffer, 0);
if (size > 0) {
bufferChunkSize += size;
decoderInputBuffer.put(tempBuffer);
mAudioExtractedTotalSize += size;
presentationTime += mAudioExtractor.getSampleTime();
if (VERBOSE) {
Log.d(TAG, "audio extractor: returned buffer of size " + size);
Log.d(TAG, "audio extractor: returned buffer for time " + presentationTime);
}
}
mAudioExtractorDone = !mAudioExtractor.advance() && size == -1;
mAudioExtractedFrameCount++;
if (bufferChunkSize > (DECODE_INPUT_SIZE - BUFFER_OVERFLOW_SAFE_GATE) || size == -1 || mAudioDecoderDone) {
break;
}
}
if (bufferChunkSize >= 0) {
codec.queueInputBuffer(
index,
0,
bufferChunkSize,
presentationTime,
mAudioExtractor.getSampleFlags());
} else if (mAudioExtractorDone) {
if (VERBOSE) {
Log.d(TAG, "audio extractor: EOS");
}
codec.queueInputBuffer(
index,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
}
if (mProgressListener != null) {
if (!mProgressListener.reportProgress((float) (mAudioExtractedTotalSize) / mFileSize)) {
// We are asked to stop reading the file. Returning immediately. The
// SoundFile object is invalid and should NOT be used afterward!
synchronized (this) {
mAudioDecoderDone = true;
notifyAll();
}
}
}
logState();
if (bufferChunkSize >= 0)
break;
}
}
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
if (VERBOSE) {
Log.d(TAG, "audio decoder: returned output buffer: " + index);
}
if (VERBOSE) {
Log.d(TAG, "audio decoder: returned buffer of size " + info.size);
}
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
if (VERBOSE) {
Log.d(TAG, "audio decoder: codec config buffer");
}
codec.releaseOutputBuffer(index, false);
return;
}
if (VERBOSE) {
Log.d(TAG, "audio decoder: returned buffer for time "
+ info.presentationTimeUs);
}
mPendingAudioDecoderOutputBufferIndices.add(index);
mPendingAudioDecoderOutputBufferInfos.add(info);
mAudioDecodedFrameCount++;
logState();
decodeAudio();
}
});
decoder.configure(inputFormat, null, null, 0);
decoder.start();
return decoder;
}
private void ReadFile(File inputFile)
throws FileNotFoundException,
java.io.IOException, InvalidInputException {
mAudioExtractor = new MediaExtractor();
MediaFormat format = null;
int i;
mInputFile = inputFile;
String[] components = mInputFile.getPath().split("\\.");
mFileType = components[components.length - 1];
mFileSize = (int) mInputFile.length();
mAudioExtractor.setDataSource(mInputFile.getPath());
int numTracks = mAudioExtractor.getTrackCount();
// find and select the first audio track present in the file.
for (i = 0; i < numTracks; i++) {
format = mAudioExtractor.getTrackFormat(i);
if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) {
mAudioExtractor.selectTrack(i);
break;
}
}
if (i == numTracks) {
throw new InvalidInputException("No audio track found in " + mInputFile);
}
mChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
mSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
mDuration = format.getLong(MediaFormat.KEY_DURATION);
// Set the size of the decoded samples buffer to 1MB (~6sec of a stereo stream at 44.1kHz).
// For longer streams, the buffer size will be increased later on, calculating a rough
// estimate of the total size needed to store all the samples in order to resize the buffer
// only once.
mDecodedBytes = ByteBuffer.allocate(1 << 20);
Log.i(TAG, "start decoding");
mPendingAudioDecoderOutputBufferIndices = new LinkedList<Integer>();
mPendingAudioDecoderOutputBufferInfos = new LinkedList<MediaCodec.BufferInfo>();
mAudioDecoder = createAudioDecoder(format);
synchronized (this) {
while (!mAudioDecoderDone) {
try {
wait();
} catch (InterruptedException ie) {
}
}
}
Log.i(TAG, "all set");
mNumSamples = mDecodedBytes.position() / (mChannels * 2); // One sample = 2 bytes.
mDecodedBytes.rewind();
mDecodedBytes.order(ByteOrder.LITTLE_ENDIAN);
mDecodedSamples = mDecodedBytes.asShortBuffer();
mAvgBitRate = (int) ((mFileSize * 8) * ((float) mSampleRate / mNumSamples) / 1000);
mAudioExtractor.release();
mAudioExtractor = null;
mAudioDecoder.stop();
mAudioDecoder.release();
mAudioDecoder = null;
Log.i(TAG, "all done");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment