1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.hyphonate.megaaudio.recorder;
17 
18 import android.media.AudioDeviceInfo;
19 import android.media.AudioFormat;
20 import android.media.AudioRecord;
21 import android.os.Looper;
22 import android.os.Message;
23 import android.util.Log;
24 
25 import org.hyphonate.megaaudio.common.StreamBase;
26 import org.hyphonate.megaaudio.recorder.sinks.NopAudioSinkProvider;
27 
28 /**
29  * Implementation of abstract Recorder class implemented for the Android Java-based audio record
30  * API, i.e. AudioRecord.
31  */
32 public class JavaRecorder extends Recorder {
33     @SuppressWarnings("unused") private static final String TAG = JavaRecorder.class.getSimpleName();
34     @SuppressWarnings("unused") private static final boolean LOG = true;
35 
36     /** The buffer to receive the recorder samples */
37     private float[] mRecorderBuffer;
38 
39     /** The number of FRAMES of audio data in the record buffer */
40     private int mNumBuffFrames;
41 
42     // Recording state
43     /** <code>true</code> if currently recording audio data */
44     private boolean mRecording = false;
45 
46     /* The AudioRecord for recording the audio stream */
47     private AudioRecord mAudioRecord = null;
48 
49     private AudioSink mAudioSink;
50 
51     private int mInputPreset = INPUT_PRESET_NONE;
52 
53     @Override
getRoutedDeviceId()54     public int getRoutedDeviceId() {
55         if (mAudioRecord != null) {
56             AudioDeviceInfo routedDevice = mAudioRecord.getRoutedDevice();
57             return routedDevice != null ? routedDevice.getId() : ROUTED_DEVICE_ID_INVALID;
58         } else {
59             return ROUTED_DEVICE_ID_INVALID;
60         }
61     }
62 
63     /**
64       * The listener to receive notifications of recording events
65       * @see {@link JavaSinkHandler}
66       */
67     private JavaSinkHandler mListener = null;
68 
JavaRecorder(AudioSinkProvider sinkProvider)69     public JavaRecorder(AudioSinkProvider sinkProvider) {
70         super(sinkProvider);
71     }
72 
73     //
74     // Attributes
75     //
76     /** The buff to receive the recorder samples */
getFloatBuffer()77     public float[] getFloatBuffer() { return mRecorderBuffer; }
78 
79     // JavaRecorder-specific extension
getAudioRecord()80     public AudioRecord getAudioRecord() { return mAudioRecord; }
81 
82     @Override
setInputPreset(int preset)83     public void setInputPreset(int preset) { mInputPreset = preset; }
84 
85     /*
86      * State
87      */
88     @Override
isRecording()89     public boolean isRecording() {
90         return mRecording;
91     }
92 
93     @Override
setupStream(int channelCount, int sampleRate, int numBurstFrames)94     public int setupStream(int channelCount, int sampleRate, int numBurstFrames) {
95         if (LOG) {
96             Log.i(TAG, "setupStream(chans:" + channelCount + ", rate:" + sampleRate +
97                     ", frames:" + numBurstFrames);
98         }
99         mChannelCount = channelCount;
100         mSampleRate = sampleRate;
101 
102         try {
103             int frameSize = calcFrameSizeInBytes(mChannelCount);
104 
105             AudioRecord.Builder builder = new AudioRecord.Builder();
106 
107             builder.setAudioFormat(new AudioFormat.Builder()
108                             .setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
109                             .setSampleRate(mSampleRate)
110                             .setChannelIndexMask(StreamBase.channelCountToIndexMask(mChannelCount))
111                             .build());
112                     // .setBufferSizeInBytes(numBurstFrames * frameSize)
113             if (mInputPreset != Recorder.INPUT_PRESET_NONE) {
114                 builder.setAudioSource(mInputPreset);
115             }
116             mAudioRecord = builder.build();
117             mAudioRecord.setPreferredDevice(mRouteDevice);
118 
119             mNumBuffFrames = mAudioRecord.getBufferSizeInFrames();
120 
121             mRecorderBuffer = new float[mNumBuffFrames * mChannelCount];
122 
123             if (mSinkProvider == null) {
124                 mSinkProvider = new NopAudioSinkProvider();
125             }
126             mAudioSink = mSinkProvider.allocJavaSink();
127             mAudioSink.init(mNumBuffFrames, mChannelCount);
128             mListener = new JavaSinkHandler(this, mAudioSink, Looper.getMainLooper());
129             return OK;
130         } catch (UnsupportedOperationException ex) {
131             if (LOG) {
132                 Log.i(TAG, "Couldn't open AudioRecord: " + ex);
133             }
134             mAudioRecord = null;
135             mNumBuffFrames = 0;
136             mRecorderBuffer = null;
137 
138             return ERROR_UNSUPPORTED;
139         }
140     }
141 
142     @Override
teardownStream()143     public int teardownStream() {
144         stopStream();
145 
146         waitForStreamThreadToExit();
147 
148         if (mAudioRecord != null) {
149             mAudioRecord.release();
150             mAudioRecord = null;
151         }
152 
153         mChannelCount = 0;
154         mSampleRate = 0;
155 
156         //TODO Retrieve errors from above
157         return OK;
158     }
159 
160     @Override
startStream()161     public int startStream() {
162         if (LOG) {
163             Log.i(TAG, "startStream() mAudioRecord:" + mAudioRecord);
164         }
165         if (mAudioRecord == null) {
166             return ERROR_INVALID_STATE;
167         }
168 //        // Routing
169 //        mAudioRecord.setPreferredDevice(mRoutingDevice);
170 
171         if (mListener != null) {
172             mListener.sendEmptyMessage(JavaSinkHandler.MSG_START);
173         }
174 
175 //        if (mAudioSink != null) {
176 //            mAudioSink.init(mNumBuffFrames, mChannelCount);
177 //        }
178         try {
179             mAudioRecord.startRecording();
180         } catch (IllegalStateException ex) {
181             Log.e(TAG, "startRecording exception: " + ex);
182         }
183 
184         waitForStreamThreadToExit(); // just to be sure.
185 
186         mStreamThread = new Thread(new RecorderRunnable(), "JavaRecorder Thread");
187         mRecording = true;
188         mStreamThread.start();
189 
190         return OK;
191     }
192 
193     /**
194      * Marks the stream for stopping on the next callback from the underlying system.
195      *
196      * Returns immediately, though a call to AudioSource.push() may be in progress.
197      */
198     @Override
stopStream()199     public int stopStream() {
200         mRecording = false;
201         return OK;
202     }
203 
204     // @Override
205     // Used in JavaSinkHandler
getDataBuffer()206     public float[] getDataBuffer() {
207         return mRecorderBuffer;
208         // System.arraycopy(mRecorderBuffer, 0, buffer, 0, mNumBuffFrames * mChannelCount);
209     }
210 
211     @Override
getNumBufferFrames()212     public int getNumBufferFrames() {
213         return mNumBuffFrames;
214     }
215 
216     /*
217      * Recorder Thread
218      */
219     /**
220      * Implements the <code>run</code> method for the record thread.
221      * Starts the AudioRecord, then continuously reads audio data
222      * until the flag <code>mRecording</code> is set to false (in the stop() method).
223      */
224     private class RecorderRunnable implements Runnable {
225         @Override
run()226         public void run() {
227             final int numBurstSamples = mNumBuffFrames * mChannelCount;
228             int numReadSamples = 0;
229             while (mRecording) {
230                 numReadSamples = mAudioRecord.read(
231                         mRecorderBuffer, 0, numBurstSamples, AudioRecord.READ_BLOCKING);
232 
233                 if (numReadSamples < 0) {
234                     // error
235                     if (LOG) {
236                         Log.e(TAG, "AudioRecord write error: " + numReadSamples);
237                     }
238                     stopStream();
239                 } else if (numReadSamples < numBurstSamples) {
240                     // got less than requested?
241                     if (LOG) {
242                         Log.e(TAG, "AudioRecord Underflow: " + numReadSamples +
243                                 " vs. " + numBurstSamples);
244                     }
245                     stopStream();
246                 }
247 
248                 if (mListener != null) {
249                     // TODO: on error or underrun we may be send bogus data.
250                     mListener.sendEmptyMessage(JavaSinkHandler.MSG_BUFFER_FILL);
251                 }
252             }
253 
254             if (mListener != null) {
255                 // TODO: on error or underrun we may be send bogus data.
256                 Message message = new Message();
257                 message.what = JavaSinkHandler.MSG_STOP;
258                 message.arg1 = numReadSamples;
259                 mListener.sendMessage(message);
260             }
261             mAudioRecord.stop();
262         }
263     }
264 }
265