summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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();
+ }
+ }
+ }
}
/**