/*
* Copyright 2015 Constant Innovations Inc
*
* 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.constantinnovationsinc.livemultimedia.encoders;
import android.app.Application;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.media.MediaRecorder;
import android.media.MediaPlayer.TrackInfo;
import android.util.Log;
import android.os.Build;
import android.os.Environment;
import com.constantinnovationsinc.livemultimedia.app.MultimediaApp;
import java.util.LinkedList;
import java.util.List;
import java.nio.ByteBuffer;
import java.io.File;
import java.io.IOException;
import java.nio.ByteOrder;
public class AudioEncoder implements Runnable{
private static final String TAG = AudioEncoder.class.getName();
private static final File OUTPUT_FILENAME_DIR = Environment.getExternalStorageDirectory();
// parameters for the audio encoder
private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm";
// Audio Encoder and Configuration
//
private MediaCodec mAudioEncoder;
private MediaCodec.BufferInfo mAudioBufferInfo;
private TrackInfo mAudioTrackInfo;
private MediaFormat mAudioFormat; // Configured with the options below
private boolean mMuxerStarted;
private TrackIndex mAudioTrackIndex = new TrackIndex();
@SuppressWarnings("all")
private final int kAACProfiles[] = {
2 /* OMX_AUDIO_AACObjectLC */,
5 /* OMX_AUDIO_AACObjectHE */,
39 /* OMX_AUDIO_AACObjectELD */
};
@SuppressWarnings("all")
private final int kSampleRates[] = { 8000, 11025, 22050, 44100, 48000 };
@SuppressWarnings("all")
private final int kBitRates[] = { 64000, 128000 };
private static final int kNumInputBytes = 256 * 1024;
private static final long kTimeoutUs = 10000;
private AudioRecord mRecorder = null;
private Boolean mAudioRecordingStopped = false;
private int mAudioFrames = 0;
private int mAudioFramesMax = 270;
public MultimediaApp mApp = null;
public MediaMuxer mAudioMuxer = null;
public AudioEncoder(Application app) {
mApp = (MultimediaApp)app;
}
@Override
public void run() {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
recordAudio();
}
public synchronized int getRemainingAudioFramesCount() {
return mApp.capacity();
}
public synchronized ByteBuffer getNextAudioFrame() {
return mApp.pullAudioData();
}
public synchronized void prepare( ) {
Log.w(TAG, "******Begin Audio Encoding***********");
}
private synchronized void recordAudio() {
try {
short[][] buffers = new short[256][160];
int ix = 0;
int minBufferSize = AudioRecord.getMinBufferSize(8000,
AudioFormat.CHANNEL_IN_STEREO,
AudioFormat.ENCODING_PCM_16BIT);
mRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC,
8000,
AudioFormat.CHANNEL_IN_STEREO,
AudioFormat.ENCODING_PCM_16BIT,
minBufferSize * 10);
mRecorder.startRecording();
while(!mAudioRecordingStopped ) {
short[] buffer = buffers[ix++ % buffers.length];
int readStatus = mRecorder.read(buffer, 0, buffer.length);
if (readStatus == AudioRecord.ERROR_BAD_VALUE) {
Log.e(TAG, "Error reading audio data");
mAudioRecordingStopped = true;
}
if (readStatus == AudioRecord.ERROR_INVALID_OPERATION) {
Log.e(TAG, "Error Invalid operation");
mAudioRecordingStopped = true;
}
// must convert short[] tp byte[]
ByteBuffer audioBuffer = ByteBuffer.allocate(buffer.length * 2);
audioBuffer.order(ByteOrder.LITTLE_ENDIAN);
audioBuffer.asShortBuffer().put(buffer);
if (mApp.capacity() > 0) {
mApp.saveAudioData(audioBuffer);
mAudioFrames++;
}
}
} catch(IllegalArgumentException ex) {
Log.w(TAG,"Error reading voice audio " + ex.toString());
} catch (IllegalStateException ex) {
Log.w(TAG, "Error StateException audio " + ex.toString());
}finally {
mAudioRecordingStopped = true;
}
}
public synchronized void release() {
if (mAudioEncoder != null) {
mAudioEncoder.stop();
mAudioEncoder.release();
}
if (mRecorder != null) {
mRecorder.stop();
mRecorder.release();
mRecorder = null;
}
mAudioEncoder = null;
}
/*************************************************************
* Checks if external storage is available for read and write
* @return can I write to a sd card
**************************************************************/
private synchronized boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
return Environment.MEDIA_MOUNTED.equals(state);
}
public synchronized void createAudioMuxer() throws IllegalStateException{
if (Thread.currentThread().isInterrupted()) {
release();
}
if ( isExternalStorageWritable()) {
File encodedFile = new File(OUTPUT_FILENAME_DIR, "/movies/EncodedAudio.mp4");
if (encodedFile.exists()) {
boolean result = encodedFile.delete();
if (!result)
throw new IllegalStateException("Unable to delete video file");
}
String outputPath = encodedFile.toString();
int format = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
try {
mAudioMuxer = new MediaMuxer(outputPath, format);
} catch (IOException e) {
Log.e(TAG, "Audio temp Muxer failed to create!!");
}
}
}
public synchronized void createAudioFormat() {
if (Thread.currentThread().isInterrupted()) {
release();
}
mAudioFormat = new MediaFormat();
mAudioFormat.setString(MediaFormat.KEY_MIME, AUDIO_MIME_TYPE);
mAudioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
mAudioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
mAudioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
mAudioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
mAudioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16384);
}
public synchronized MediaFormat getAudioFormat() {
return mAudioFormat;
}
@SuppressWarnings("deprecation")
private synchronized List<String> getEncoderNamesForType(String mime) {
LinkedList<String> names = new LinkedList<>();
MediaCodecInfo[] codecInfo = new MediaCodecInfo[50];
MediaCodecInfo info = null;
int codecTotalNum;
MediaCodecList codecList = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 &&
Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT_WATCH) {
for (int i= 0; i < MediaCodecList.getCodecCount(); i++) {
codecInfo[i] = MediaCodecList.getCodecInfoAt(i);
}
codecTotalNum = MediaCodecList.getCodecCount();
} else if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
codecInfo = codecList.getCodecInfos();
codecTotalNum = codecInfo.length;
} else {
Log.e(TAG, "Unknown Android version something wrong!");
return names;
}
for (int i = 0; i < codecTotalNum; ++i) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 &&
Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT_WATCH) {
info = MediaCodecList.getCodecInfoAt(i);
} else if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
info = codecInfo[i];
}
if (info == null) {
Log.e(TAG, "Null Encode or Decoder something wrong!");
return names;
}
if (!info.isEncoder()) {
Log.d(TAG, "skipping '" + info.getName() + "'." + " since its not an encoder");
continue;
}
if (!info.getName().startsWith("OMX.")) {
// Unfortunately for legacy reasons, "AACEncoder", a
// non OMX component had to be in this list for the video
// editor code to work... but it cannot actually be instantiated
// using MediaCodec.
Log.d(TAG, "skipping '" + info.getName() + "'." + " since its an OMX component");
continue;
}
String[] supportedTypes = info.getSupportedTypes();
for (String type : supportedTypes) {
if (type.equalsIgnoreCase(mime)) {
names.push(info.getName());
break;
}
}
}
return names;
}
private synchronized int queueInputBuffer( MediaCodec codec, ByteBuffer[] inputBuffers, int index, byte[] audioBuffer) {
ByteBuffer buffer = inputBuffers[index];
buffer.clear();
int size = buffer.limit();
byte[] data = new byte[size];
/// ******** fill with audio data ************
if (data.length >= audioBuffer.length) {
System.arraycopy(audioBuffer, 0, data, 0, audioBuffer.length);
}
buffer.put(data);
// ******************************************
codec.queueInputBuffer(index, 0 /* offset */, size, 0 /* timeUs */, 0);
return size;
}
private synchronized void dequeueOutputBuffer(
MediaCodec codec, ByteBuffer[] outputBuffers,
int index, MediaCodec.BufferInfo info) {
codec.releaseOutputBuffer(index, false /* render */);
}
private class TrackIndex {
int index = 0;
}
/*****************************************************************
* Generates the presentation time for frame N, in microseconds.
* @param frameIndex the index of teh frame
* @return long the new time
****************************************************************/
private synchronized static long computePresentationTime(int frameIndex) {
return 132 + frameIndex * 1000000 / 30;
}
}