// inspired by: https://github.com/Kickflip/kickflip-android-sdk/blob/e35e0a5bb7161ccffebd564ec1a76a0e2c053fc8/sdk/src/main/java/io/kickflip/sdk/av/MicrophoneEncoder.java package io.cine.android.streaming; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaCodec; import android.media.MediaRecorder; import android.os.Trace; import android.util.Log; import java.nio.ByteBuffer; /** * Created by davidbrodsky on 1/23/14. * * @hide */ public class MicrophoneEncoder implements Runnable { protected static final int SAMPLES_PER_FRAME = 1024; // AAC frame size. Audio encoder input size is a multiple of this protected static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; private static final boolean TRACE = false; private static final boolean VERBOSE = false; private static final String TAG = "MicrophoneEncoder"; private final Object mReadyFence = new Object(); // Synchronize audio thread readiness private final Muxer mMuxer; private final Object mRecordingFence = new Object(); // Variables recycled between calls to sendAudioToEncoder MediaCodec mMediaCodec; int audioInputBufferIndex; int audioInputLength; long audioAbsolutePtsUs; long startPTS = 0; long totalSamplesNum = 0; private boolean mThreadReady; // Is audio thread ready private boolean mThreadRunning; // Is audio thread running private AudioRecord mAudioRecord; private AudioEncoderCore mEncoderCore; private boolean mRecordingRequested; public MicrophoneEncoder(Muxer muxer) { mMuxer = muxer; init(); } private void init() { mMediaCodec = null; mThreadReady = false; mThreadRunning = false; mRecordingRequested = false; } private void reset() { audioInputBufferIndex = 0; audioInputLength = 0; audioAbsolutePtsUs = 0; } private void setupAudioRecord() { AudioEncoderConfig config = getAudioEncoderConfig(); int minBufferSize = AudioRecord.getMinBufferSize(config.getSampleRate(), config.getChannelConfig(), AUDIO_FORMAT); mAudioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, // source config.getSampleRate(), // sample rate, hz config.getChannelConfig(), // channels AUDIO_FORMAT, // audio format minBufferSize * 4); // buffer size (bytes) } private AudioEncoderConfig getAudioEncoderConfig() { return mMuxer.getConfig().getAudioEncoderConfig(); } public void startRecording() { if (VERBOSE) Log.i(TAG, "startRecording"); reset(); mEncoderCore = new AudioEncoderCore(mMuxer); startThread(); synchronized (mRecordingFence) { totalSamplesNum = 0; startPTS = 0; mRecordingRequested = true; mRecordingFence.notify(); } } public void stopRecording() { Log.i(TAG, "stopRecording"); synchronized (mRecordingFence) { mRecordingRequested = false; } } public boolean isRecording() { return mRecordingRequested; } private void startThread() { synchronized (mReadyFence) { if (mThreadRunning) { Log.w(TAG, "Audio thread running when start requested"); return; } Thread audioThread = new Thread(this, "MicrophoneEncoder"); audioThread.setPriority(Thread.MAX_PRIORITY); audioThread.start(); while (!mThreadReady) { try { mReadyFence.wait(); } catch (InterruptedException e) { // ignore } } } } @Override public void run() { setupAudioRecord(); synchronized (mReadyFence) { mThreadReady = true; mReadyFence.notify(); } synchronized (mRecordingFence) { while (!mRecordingRequested) { try { mRecordingFence.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } mAudioRecord.startRecording(); mMediaCodec = mEncoderCore.getMediaCodec(); if (VERBOSE) Log.i(TAG, "Begin Audio transmission to encoder. encoder : " + mEncoderCore.mEncoder); while (mRecordingRequested) { if (TRACE) Trace.beginSection("drainAudio"); mEncoderCore.drainEncoder(false); if (TRACE) Trace.endSection(); if (TRACE) Trace.beginSection("sendAudio"); sendAudioToEncoder(false); if (TRACE) Trace.endSection(); } mThreadReady = false; if (VERBOSE) Log.i(TAG, "Exiting audio encode loop. Draining Audio Encoder"); if (TRACE) Trace.beginSection("sendAudio"); sendAudioToEncoder(true); if (TRACE) Trace.endSection(); mAudioRecord.stop(); if (TRACE) Trace.beginSection("drainAudioFinal"); mEncoderCore.drainEncoder(true); if (TRACE) Trace.endSection(); mEncoderCore.release(); mThreadRunning = false; } private void sendAudioToEncoder(boolean endOfStream) { // send current frame data to encoder try { ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); audioInputBufferIndex = mMediaCodec.dequeueInputBuffer(-1); if (audioInputBufferIndex >= 0) { ByteBuffer inputBuffer = inputBuffers[audioInputBufferIndex]; inputBuffer.clear(); audioInputLength = mAudioRecord.read(inputBuffer, SAMPLES_PER_FRAME * 2); audioAbsolutePtsUs = (System.nanoTime()) / 1000L; // We divide audioInputLength by 2 because audio samples are // 16bit. audioAbsolutePtsUs = getJitterFreePTS(audioAbsolutePtsUs, audioInputLength / 2); if (audioInputLength == AudioRecord.ERROR_INVALID_OPERATION) Log.e(TAG, "Audio read error: invalid operation"); if (audioInputLength == AudioRecord.ERROR_BAD_VALUE) Log.e(TAG, "Audio read error: bad value"); // if (VERBOSE) // Log.i(TAG, "queueing " + audioInputLength + " audio bytes with pts " + audioAbsolutePtsUs); if (endOfStream) { if (VERBOSE) Log.i(TAG, "EOS received in sendAudioToEncoder"); mMediaCodec.queueInputBuffer(audioInputBufferIndex, 0, audioInputLength, audioAbsolutePtsUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM); } else { mMediaCodec.queueInputBuffer(audioInputBufferIndex, 0, audioInputLength, audioAbsolutePtsUs, 0); } } } catch (Throwable t) { Log.e(TAG, "_offerAudioEncoder exception"); t.printStackTrace(); } } /** * Ensures that each audio pts differs by a constant amount from the previous one. * * @param bufferPts presentation timestamp in us * @param bufferSamplesNum the number of samples of the buffer's frame * @return */ private long getJitterFreePTS(long bufferPts, long bufferSamplesNum) { AudioEncoderConfig config = getAudioEncoderConfig(); long correctedPts = 0; long bufferDuration = (1000000 * bufferSamplesNum) / (config.getSampleRate()); bufferPts -= bufferDuration; // accounts for the delay of acquiring the audio buffer if (totalSamplesNum == 0) { // reset startPTS = bufferPts; totalSamplesNum = 0; } correctedPts = startPTS + (1000000 * totalSamplesNum) / (config.getSampleRate()); if (bufferPts - correctedPts >= 2 * bufferDuration) { // reset startPTS = bufferPts; totalSamplesNum = 0; correctedPts = startPTS; } totalSamplesNum += bufferSamplesNum; return correctedPts; } }