diff options
Diffstat (limited to 'src/com/android')
-rw-r--r-- | src/com/android/fmradio/AudioRecorder.java | 331 | ||||
-rw-r--r-- | src/com/android/fmradio/FmRecorder.java | 92 | ||||
-rw-r--r-- | src/com/android/fmradio/FmService.java | 65 |
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(); + } + } + } } /** |