summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKhalid Zubair <kzubair@cyngn.com>2016-06-27 14:29:45 -0700
committerKhalid Zubair <kzubair@cyngn.com>2016-06-28 11:12:53 -0700
commit3c0c276699c9ba975e370c3c7711677b71772854 (patch)
tree2aba79805447209c0f94e23baddf60e8a5680708
parenta7db4dc6a01e5f756257907e3000538134497b7c (diff)
downloadandroid_packages_apps_FMRadio-3c0c276699c9ba975e370c3c7711677b71772854.tar.gz
android_packages_apps_FMRadio-3c0c276699c9ba975e370c3c7711677b71772854.tar.bz2
android_packages_apps_FMRadio-3c0c276699c9ba975e370c3c7711677b71772854.zip
Handle recording without using MediaRecorder
MediaRecorder creates a new AudioRecord for the tuner internally and causes two audio input devices to be in-use during recording. Replace MediaRecorder with a custom recorder that gets fed by the same AudioRecord instance used for playback when SW rendering is in affect. This change helps workaround some bugs in the Audio HAL during fm recording (two tuner input devices) and concurrent mic recording. Force SW rendering when recording starts so that the recorder works and attempt to start the audio patch when recording ends. In onAudioPatchListUpdate there is no need to call initAudioRecordSink() before calling startRender() because startRender() will call initAudioRecordSink(). CYNGNOS-2819, KIPPER-687, FEIJ-1372 Change-Id: Iddd9f325892ca4482c3977dcadc627352e6f5bb2
-rw-r--r--src/com/android/fmradio/AudioRecorder.java331
-rw-r--r--src/com/android/fmradio/FmRecorder.java92
-rw-r--r--src/com/android/fmradio/FmService.java65
3 files changed, 413 insertions, 75 deletions
diff --git a/src/com/android/fmradio/AudioRecorder.java b/src/com/android/fmradio/AudioRecorder.java
new file mode 100644
index 0000000..a779abb
--- /dev/null
+++ b/src/com/android/fmradio/AudioRecorder.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2016 The CyanogenMod 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.android.fmradio;
+
+import android.media.AudioFormat;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.concurrent.Semaphore;
+
+class AudioRecorder extends HandlerThread implements Handler.Callback {
+ public static final int AUDIO_RECORDER_ERROR_INTERNAL = -100;
+ public static final int AUDIO_RECORDER_WARN_DISK_LOW = 100;
+ private static final boolean TRACE = false;
+ private static final String TAG = "AudioRecorder";
+ private static final int MSG_INIT = 100;
+ private static final int MSG_ENCODE = 101;
+ private static final int MSG_STOP = 999;
+ private static final long DISK_LOW_THRESHOLD = 10 * 1024 * 1024;
+ private AudioFormat mInputFormat;
+ private Handler mHandler;
+ private File mFilePath;
+ private MediaMuxer mMuxer;
+ private MediaCodec mCodec;
+ private MediaFormat mRequestedFormat;
+ private LinkedList<Sample> mQueue = new LinkedList<>();
+ private MediaFormat mOutFormat;
+ private int mMuxerTrack;
+ private float mRate; // bytes per us
+ private long mInputBufferPosition;
+ private int mInputBufferIndex = -1;
+ /** This semaphore is initialized when stopRecording() is called and blocks
+ until recording is stopped. */
+ private Semaphore mFinalSem;
+ private boolean mFinished;
+ private Handler mCallbackHandler;
+ private Callback mCallback;
+
+ AudioRecorder(AudioFormat format, File filePath) {
+ super("AudioRecorder Thread");
+ mFilePath = filePath;
+ mInputFormat = format;
+
+ start();
+
+ mHandler = new Handler(getLooper(), this);
+ mHandler.obtainMessage(MSG_INIT).sendToTarget();
+ }
+
+ public void setCallback(Callback callback) {
+ mCallback = callback;
+ mCallbackHandler = new Handler(Looper.getMainLooper());
+ }
+
+ /**
+ * Encode bytes of audio to file
+ *
+ * @param bytes - PCM inbut buffer
+ */
+ public void encode(byte[] bytes) {
+ if (mFinished) {
+ Log.w(TAG, "encode() called after stopped");
+ return;
+ }
+ Sample s = new Sample();
+ s.bytes = bytes;
+ mHandler.obtainMessage(MSG_ENCODE, s).sendToTarget();
+ }
+
+ /**
+ * Stop the current recording.
+ * Blocks until the recording finishes cleanly.
+ */
+ public void stopRecording() {
+ if (mFinished) {
+ Log.w(TAG, "stopRecording() called after stopped");
+ return;
+ }
+
+ mFinished = true;
+ Log.d(TAG, "Stopping");
+ Semaphore done = new Semaphore(0);
+ mHandler.obtainMessage(MSG_STOP, done).sendToTarget();
+
+ try {
+ // block until done
+ done.acquire();
+ } catch (InterruptedException ex) {
+ Log.e(TAG, "interrupted waiting for encoding to finish", ex);
+ } finally {
+ quitSafely();
+ }
+ }
+
+ private void init() {
+ Log.i(TAG, "Starting AudioRecorder with format=" + mInputFormat + ". Saving to: " + mFilePath);
+ calculateInputRate();
+
+ mRequestedFormat = new MediaFormat();
+ mRequestedFormat.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
+ mRequestedFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
+ mRequestedFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, mInputFormat.getChannelCount());
+ mRequestedFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, mInputFormat.getSampleRate());
+ mRequestedFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
+
+ try {
+ mCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
+ } catch (IOException ex) {
+ onError("failed creating encoder", ex);
+ return;
+ }
+ mCodec.setCallback(new AudioRecorderCodecCallback(), new Handler(getLooper()));
+ mCodec.configure(mRequestedFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+ mCodec.start();
+
+ try {
+ mMuxer = new MediaMuxer(mFilePath.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+ } catch (IOException ex) {
+ onError("failed creating muxer", ex);
+ return;
+ }
+
+ mOutFormat = mCodec.getOutputFormat();
+ mMuxerTrack = mMuxer.addTrack(mOutFormat);
+ mMuxer.start();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.what == MSG_INIT) {
+ init();
+ } else if (msg.what == MSG_STOP) {
+ mFinalSem = (Semaphore) msg.obj;
+ if (mInputBufferIndex >= 0) {
+ processInputBuffer();
+ }
+ } else if (msg.what == MSG_ENCODE) {
+ mQueue.addLast((Sample) msg.obj);
+ if (mInputBufferIndex >= 0) {
+ processInputBuffer();
+ }
+ }
+ return true;
+ }
+
+ private void processInputBuffer() {
+ Sample s = mQueue.peekFirst();
+ if (s == null) { // input available?
+ if (mFinalSem != null) {
+ // input queue is exhausted and stopRecording() is waiting for
+ // encoding to finish. signal end-of-stream on the input.
+ Log.d(TAG, "Input EOS");
+ mCodec.queueInputBuffer(
+ mInputBufferIndex, 0, 0,
+ getPresentationTimestampUs(mInputBufferPosition),
+ MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ }
+ return;
+ }
+
+ ByteBuffer b = mCodec.getInputBuffer(mInputBufferIndex);
+ assert b != null;
+ int sz = Math.min(b.capacity(), s.bytes.length - s.offset);
+ long ts = getPresentationTimestampUs(mInputBufferPosition);
+ if (TRACE)
+ Log.v(TAG, String.format("processInputBuffer (len=%d) ts=%.3f", sz, ts * 1e-6));
+
+ b.put(s.bytes, s.offset, sz);
+ mCodec.queueInputBuffer(mInputBufferIndex, 0, sz, ts, 0);
+
+ mInputBufferPosition += sz;
+ s.offset += sz;
+
+ // done with this sample?
+ if (s.offset >= s.bytes.length) {
+ mQueue.pop();
+ }
+
+ // done with this buffer
+ mInputBufferIndex = -1;
+ }
+
+ private void processOutputBuffer(int index, MediaCodec.BufferInfo info) {
+ ByteBuffer outputBuffer = mCodec.getOutputBuffer(index);
+ assert outputBuffer != null;
+
+ if (TRACE)
+ Log.v(TAG, String.format("processOutputBuffer (len=%d) ts=%.3f",
+ outputBuffer.limit(), info.presentationTimeUs * 1e-6));
+
+ mMuxer.writeSampleData(mMuxerTrack, outputBuffer, info);
+ mCodec.releaseOutputBuffer(index, false);
+ if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ Log.d(TAG, "Output EOS");
+ finish();
+ } else if (mFilePath.getFreeSpace() < DISK_LOW_THRESHOLD) {
+ onDiskLow();
+ }
+ }
+
+ private void onDiskLow() {
+ mCallbackHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mCallback != null) {
+ mCallback.onError(AUDIO_RECORDER_WARN_DISK_LOW);
+ }
+ }
+ });
+ }
+
+ private void onError(String s, Exception e) {
+ Log.e(TAG, s, e);
+ mFinished = true;
+ stopAndRelease();
+ mCallbackHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ quitSafely();
+ if (mCallback != null) {
+ mCallback.onError(AUDIO_RECORDER_ERROR_INTERNAL);
+ }
+ }
+ });
+ }
+
+ private void finish() {
+ assert mFinalSem != null;
+ stopAndRelease();
+ mFinalSem.release();
+ }
+
+ private void stopAndRelease() {
+ // can fail early on before codec/muxer are created
+ if (mCodec != null) {
+ mCodec.stop();
+ mCodec.release();
+ }
+
+ if (mMuxer != null) {
+ mMuxer.stop();
+ mMuxer.release();
+ }
+ }
+
+ private void calculateInputRate() {
+ int bits_per_sample;
+ switch (mInputFormat.getEncoding()) {
+ case AudioFormat.ENCODING_PCM_8BIT:
+ bits_per_sample = 8;
+ break;
+ case AudioFormat.ENCODING_PCM_16BIT:
+ bits_per_sample = 16;
+ break;
+ case AudioFormat.ENCODING_PCM_FLOAT:
+ bits_per_sample = 32;
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected encoding: " + mInputFormat.getEncoding());
+ }
+
+ mRate = bits_per_sample;
+ mRate *= mInputFormat.getSampleRate();
+ mRate *= mInputFormat.getChannelCount();
+ mRate *= 1e-6; // -> us
+ mRate /= 8; // -> bytes
+
+ Log.v(TAG, "Rate: " + mRate);
+ }
+
+ private long getPresentationTimestampUs(long position) {
+ return (long) (position / mRate);
+ }
+
+ public interface Callback {
+ void onError(int what);
+ }
+
+ class AudioRecorderCodecCallback extends MediaCodec.Callback {
+
+ @Override
+ public void onInputBufferAvailable(MediaCodec codec, int index) {
+ mInputBufferIndex = index;
+ processInputBuffer();
+ }
+
+ @Override
+ public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
+ processOutputBuffer(index, info);
+ }
+
+ @Override
+ public void onError(MediaCodec codec, MediaCodec.CodecException e) {
+ AudioRecorder.this.onError("Encoder error", e);
+ }
+
+ @Override
+ public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
+ }
+ }
+
+ private class Sample {
+ byte bytes[];
+ int offset;
+ }
+}
diff --git a/src/com/android/fmradio/FmRecorder.java b/src/com/android/fmradio/FmRecorder.java
index 18a9d00..389fbc2 100644
--- a/src/com/android/fmradio/FmRecorder.java
+++ b/src/com/android/fmradio/FmRecorder.java
@@ -21,8 +21,8 @@ import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.media.AudioFormat;
import android.media.MediaPlayer;
-import android.media.MediaRecorder;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Environment;
@@ -41,7 +41,7 @@ import java.util.Locale;
* This class provider interface to recording, stop recording, save recording
* file, play recording file
*/
-public class FmRecorder implements MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener {
+public class FmRecorder implements AudioRecorder.Callback {
private static final String TAG = "FmRecorder";
// file prefix
public static final String RECORDING_FILE_PREFIX = "FM";
@@ -82,7 +82,15 @@ public class FmRecorder implements MediaRecorder.OnErrorListener, MediaRecorder.
// listener use for notify service the record state or error state
private OnRecorderStateChangedListener mStateListener = null;
// recorder use for record file
- private MediaRecorder mRecorder = null;
+ private AudioRecorder mRecorder = null;
+ // take this lock before manipulating mRecorder
+ private final Object mRecorderLock = new Object();
+ // format of input audio
+ private AudioFormat mInputFormat = null;
+
+ FmRecorder(AudioFormat in) {
+ mInputFormat = in;
+ }
/**
* Start recording the voice of FM, also check the pre-conditions, if not
@@ -146,31 +154,20 @@ public class FmRecorder implements MediaRecorder.OnErrorListener, MediaRecorder.
}
// set record parameter and start recording
try {
- mRecorder = new MediaRecorder();
- mRecorder.setOnErrorListener(this);
- mRecorder.setOnInfoListener(this);
- mRecorder.setAudioSource(MediaRecorder.AudioSource.RADIO_TUNER);
- mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
- mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
- final int samplingRate = 44100;
- mRecorder.setAudioSamplingRate(samplingRate);
- final int bitRate = 128000;
- mRecorder.setAudioEncodingBitRate(bitRate);
- final int audiochannels = 2;
- mRecorder.setAudioChannels(audiochannels);
- mRecorder.setOutputFile(mRecordFile.getAbsolutePath());
- mRecorder.prepare();
- mRecordStartTime = SystemClock.elapsedRealtime();
- mRecorder.start();
- mIsRecordingFileSaved = false;
+ synchronized(mRecorderLock) {
+ if (mRecorder != null) {
+ mRecorder.stopRecording();
+ }
+
+ mRecorder = new AudioRecorder(mInputFormat, mRecordFile);
+ mRecorder.setCallback(this);
+ mRecordStartTime = SystemClock.elapsedRealtime();
+ mIsRecordingFileSaved = false;
+ }
} catch (IllegalStateException e) {
Log.e(TAG, "startRecording, IllegalStateException while starting recording!", e);
setError(ERROR_RECORDER_INTERNAL);
return;
- } catch (IOException e) {
- Log.e(TAG, "startRecording, IOException while starting recording!", e);
- setError(ERROR_RECORDER_INTERNAL);
- return;
}
setState(STATE_RECORDING);
}
@@ -297,17 +294,9 @@ public class FmRecorder implements MediaRecorder.OnErrorListener, MediaRecorder.
void onRecorderError(int error);
}
- /**
- * When recorder occur error, release player, notify error message, and
- * update FM recorder state to idle
- *
- * @param mr The current recorder
- * @param what The error message type
- * @param extra The error message extra
- */
@Override
- public void onError(MediaRecorder mr, int what, int extra) {
- Log.e(TAG, "onError, what = " + what + ", extra = " + extra);
+ public void onError(int what) {
+ Log.e(TAG, "onError, what = " + what);
stopRecorder();
setError(ERROR_RECORDER_INTERNAL);
if (STATE_RECORDING == mInternalState) {
@@ -315,23 +304,11 @@ public class FmRecorder implements MediaRecorder.OnErrorListener, MediaRecorder.
}
}
- @Override
- public void onInfo(MediaRecorder mr, int what, int extra) {
- Log.d(TAG, "onInfo: what=" + what + ", extra=" + extra);
- if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED ||
- what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
- onError(mr, what, extra);
- }
- }
-
/**
* Reset FM recorder
*/
public void resetRecorder() {
- if (mRecorder != null) {
- mRecorder.release();
- mRecorder = null;
- }
+ stopRecorder();
mRecordFile = null;
mRecordStartTime = 0;
mRecordTime = 0;
@@ -523,17 +500,18 @@ public class FmRecorder implements MediaRecorder.OnErrorListener, MediaRecorder.
}
private void stopRecorder() {
- synchronized (this) {
+ synchronized (mRecorderLock) {
if (mRecorder != null) {
- try {
- mRecorder.stop();
- } catch (IllegalStateException ex) {
- Log.e(TAG, "stopRecorder, IllegalStateException ocurr " + ex);
- setError(ERROR_RECORDER_INTERNAL);
- } finally {
- mRecorder.release();
- mRecorder = null;
- }
+ mRecorder.stopRecording();
+ mRecorder = null;
+ }
+ }
+ }
+
+ public void encode(byte bytes[]) {
+ synchronized (mRecorderLock) {
+ if (mRecorder != null) {
+ mRecorder.encode(bytes);
}
}
}
diff --git a/src/com/android/fmradio/FmService.java b/src/com/android/fmradio/FmService.java
index 25ad3e9..d197be4 100644
--- a/src/com/android/fmradio/FmService.java
+++ b/src/com/android/fmradio/FmService.java
@@ -510,6 +510,10 @@ public class FmService extends Service implements FmRecorder.OnRecorderStateChan
if (isRender()) {
mAudioTrack.write(tmpBuf, 0, tmpBuf.length);
}
+
+ if (mFmRecorder != null) {
+ mFmRecorder.encode(tmpBuf);
+ }
} else {
// Earphone mode will come here and wait.
mCurrentFrame = 0;
@@ -1110,12 +1114,17 @@ public class FmService extends Service implements FmRecorder.OnRecorderStateChan
}
if (mFmRecorder == null) {
- mFmRecorder = new FmRecorder();
+ mFmRecorder = new FmRecorder(mAudioRecord.getFormat());
mFmRecorder.registerRecorderStateListener(FmService.this);
}
if (isSdcardReady(sRecordingSdcard)) {
mFmRecorder.startRecording(mContext);
+ if (mAudioPatch != null) {
+ Log.d(TAG, "Switching to SW rendering on recording start");
+ releaseAudioPatch();
+ startRender();
+ }
} else {
onRecorderError(FmRecorder.ERROR_SDCARD_NOT_PRESENT);
}
@@ -1377,7 +1386,6 @@ public class FmService extends Service implements FmRecorder.OnRecorderStateChan
// Need to recreate AudioRecord and AudioTrack for this case.
if (isPatchMixerToDeviceRemoved(patches)) {
Log.d(TAG, "onAudioPatchListUpdate reinit for BT or WFD connected");
- initAudioRecordSink();
startRender();
return;
}
@@ -1669,32 +1677,44 @@ public class FmService extends Service implements FmRecorder.OnRecorderStateChan
}
startAudioTrack();
- ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>();
- mAudioManager.listAudioPatches(patches);
- if (mAudioPatch == null) {
- if (isPatchMixerToEarphone(patches)) {
- int status;
- stopAudioTrack();
- stopRender();
- status = createAudioPatch();
- if (status != AudioManager.SUCCESS){
- Log.d(TAG, "enableFmAudio: fallback as createAudioPatch failed");
- startRender();
- }
- } else {
- startRender();
- }
- }
+ startPatchOrRender();
} else {
releaseAudioPatch();
stopRender();
}
}
+ private void startPatchOrRender() {
+ ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>();
+ mAudioManager.listAudioPatches(patches);
+ if (mAudioPatch == null) {
+ if (isPatchMixerToEarphone(patches)) {
+ int status;
+ stopAudioTrack();
+ stopRender();
+ status = createAudioPatch();
+ if (status != AudioManager.SUCCESS){
+ Log.d(TAG, "startPatchOrRender: fallback as createAudioPatch failed");
+ startRender();
+ }
+ } else {
+ if (!isRendering()) {
+ startRender();
+ }
+ }
+ }
+ }
+
// Make sure patches count will not be 0
private boolean isPatchMixerToEarphone(ArrayList<AudioPatch> patches) {
int deviceCount = 0;
int deviceEarphoneCount = 0;
+
+ if (getRecorderState() == FmRecorder.STATE_RECORDING) {
+ // force software rendering when recording
+ return false;
+ }
+
if (mContext.getResources().getBoolean(R.bool.config_useSoftwareRenderingForAudio)) {
Log.w(TAG, "FIXME: forcing isPatchMixerToEarphone to return false. "
+ "Software rendering will be used.");
@@ -1897,6 +1917,15 @@ public class FmService extends Service implements FmRecorder.OnRecorderStateChan
bundle.putInt(FmListener.CALLBACK_FLAG, FmListener.LISTEN_RECORDSTATE_CHANGED);
bundle.putInt(FmListener.KEY_RECORDING_STATE, state);
notifyActivityStateChanged(bundle);
+
+ if (state == FmRecorder.STATE_IDLE) { // stopped recording?
+ if (mPowerStatus == POWER_UP) { // playing?
+ if (mAudioPatch == null) {
+ // maybe switch to patch if possible
+ startPatchOrRender();
+ }
+ }
+ }
}
/**