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.BuilderBase;
26 import org.hyphonate.megaaudio.common.StreamBase;
27 import org.hyphonate.megaaudio.common.StreamState;
28 import org.hyphonate.megaaudio.recorder.sinks.NopAudioSinkProvider;
29 
30 /**
31  * Implementation of abstract Recorder class implemented for the Android Java-based audio record
32  * API, i.e. AudioRecord.
33  */
34 public class JavaRecorder extends Recorder {
35     @SuppressWarnings("unused")
36     private static final String TAG = JavaRecorder.class.getSimpleName();
37     @SuppressWarnings("unused")
38     private static final boolean LOG = true;
39 
40     /**
41      * The buffer to receive the recorder samples
42      */
43     private float[] mRecorderBuffer;
44 
45     /* The AudioRecord for recording the audio stream */
46     private AudioRecord mAudioRecord = null;
47 
48     private AudioSink mAudioSink;
49 
50     @Override
getRoutedDeviceId()51     public int getRoutedDeviceId() {
52         if (mAudioRecord != null) {
53             AudioDeviceInfo routedDevice = mAudioRecord.getRoutedDevice();
54             return routedDevice != null
55                     ? routedDevice.getId() : BuilderBase.ROUTED_DEVICE_ID_DEFAULT;
56         } else {
57             return BuilderBase.ROUTED_DEVICE_ID_DEFAULT;
58         }
59     }
60 
61     /**
62      * The listener to receive notifications of recording events
63      */
64     private JavaSinkHandler mListener = null;
65 
JavaRecorder(RecorderBuilder builder, AudioSinkProvider sinkProvider)66     public JavaRecorder(RecorderBuilder builder, AudioSinkProvider sinkProvider) {
67         super(sinkProvider);
68         setupStream(builder);
69     }
70 
71     //
72     // Attributes
73     //
74     @Override
getSharingMode()75     public int getSharingMode() {
76         // JAVA Audio API does not support a sharing mode
77         return BuilderBase.SHARING_MODE_NOTSUPPORTED;
78     }
79 
80     @Override
getChannelCount()81     public int getChannelCount() {
82         return mAudioRecord != null ?  mAudioRecord.getChannelCount() : -1;
83     }
84 
85     @Override
isMMap()86     public boolean isMMap() {
87         // Java Streams are never MMAP
88         return false;
89     }
90 
91     /**
92      * The buff to receive the recorder samples
93      */
getFloatBuffer()94     public float[] getFloatBuffer() {
95         return mRecorderBuffer;
96     }
97 
98     // JavaRecorder-specific extension
getAudioRecord()99     public AudioRecord getAudioRecord() {
100         return mAudioRecord;
101     }
102 
setupStream(RecorderBuilder builder)103     private int setupStream(RecorderBuilder builder) {
104         mChannelCount = builder.getChannelCount();
105         mSampleRate = builder.getSampleRate();
106         mNumExchangeFrames = builder.getNumExchangeFrames();
107         mSharingMode = builder.getSharingMode();
108         mPerformanceMode = builder.getPerformanceMode();
109         mInputPreset = builder.getInputPreset();
110 
111         if (LOG) {
112             Log.i(TAG, "setupStream()");
113             Log.i(TAG, "  chans:" + mChannelCount);
114             Log.i(TAG, "  rate: " + mSampleRate);
115             Log.i(TAG, "  frames: " + mNumExchangeFrames);
116             Log.i(TAG, "  perf mode: " + mPerformanceMode);
117             Log.i(TAG, "  route device: " + builder.getRouteDeviceId());
118             Log.i(TAG, "  preset: " + mInputPreset);
119         }
120 
121         try {
122 //            int bufferSizeInBytes = mNumExchangeFrames * mChannelCount
123 //                    * sampleSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT);
124 //            Log.i(TAG, "  bufferSizeInBytes:" + bufferSizeInBytes);
125 //            Log.i(TAG, "  (in frames)" + (bufferSizeInBytes / 4 / mChannelCount));
126 
127             AudioFormat.Builder formatBuilder = new AudioFormat.Builder();
128             formatBuilder.setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
129                     .setSampleRate(mSampleRate)
130                     .setChannelIndexMask(StreamBase.channelCountToIndexMask(mChannelCount));
131 
132             AudioRecord.Builder recordBuilder = new AudioRecord.Builder();
133             recordBuilder.setAudioFormat(formatBuilder.build())
134                     /*.setBufferSizeInBytes(bufferSizeInBytes)*/;
135             if (mInputPreset != Recorder.INPUT_PRESET_NONE) {
136                 recordBuilder.setAudioSource(mInputPreset);
137             }
138             mAudioRecord = recordBuilder.build();
139             mNumExchangeFrames = mAudioRecord.getBufferSizeInFrames();
140             if (LOG) {
141                 Log.i(TAG, "  mAudioRecord.getBufferSizeInFrames(): "
142                         + mAudioRecord.getBufferSizeInFrames());
143             }
144             mAudioRecord.setPreferredDevice(builder.getRouteDevice());
145 
146             mRecorderBuffer = new float[mNumExchangeFrames * mChannelCount];
147 
148             if (mSinkProvider == null) {
149                 mSinkProvider = new NopAudioSinkProvider();
150             }
151             mAudioSink = mSinkProvider.allocJavaSink();
152             mAudioSink.init(mNumExchangeFrames, mChannelCount);
153             mListener = new JavaSinkHandler(this, mAudioSink, Looper.getMainLooper());
154             return OK;
155         } catch (UnsupportedOperationException ex) {
156             if (LOG) {
157                 Log.e(TAG, "Couldn't open AudioRecord: " + ex);
158             }
159             return ERROR_UNSUPPORTED;
160         } catch (java.lang.IllegalArgumentException ex) {
161             if (LOG) {
162                 Log.e(TAG, "Invalid arguments to AudioRecord.Builder: " + ex);
163             }
164             return ERROR_UNSUPPORTED;
165         }
166     }
167 
168     @Override
teardownStream()169     public int teardownStream() {
170         if (LOG) {
171             Log.i(TAG, "teardownStream()");
172         }
173         stopStream();
174 
175         waitForStreamThreadToExit();
176 
177         if (mAudioRecord != null) {
178             mAudioRecord.release();
179             mAudioRecord = null;
180         }
181 
182         mChannelCount = 0;
183         mSampleRate = 0;
184 
185         //TODO Retrieve errors from above
186         return OK;
187     }
188 
189     @Override
startStream()190     public int startStream() {
191         if (LOG) {
192             Log.i(TAG, "startStream() mAudioRecord:" + mAudioRecord);
193         }
194         if (mAudioRecord == null) {
195             return ERROR_INVALID_STATE;
196         }
197         if (mListener != null) {
198             mListener.sendEmptyMessage(JavaSinkHandler.MSG_START);
199         }
200 
201         try {
202             mAudioRecord.startRecording();
203         } catch (IllegalStateException ex) {
204             Log.e(TAG, "startRecording exception: " + ex);
205         }
206 
207         waitForStreamThreadToExit(); // just to be sure.
208 
209         mStreamThread = new Thread(new RecorderRunnable(), "JavaRecorder Thread");
210         mRecording = true;
211         mStreamThread.start();
212 
213         return OK;
214     }
215 
216     /**
217      * Marks the stream for stopping on the next callback from the underlying system.
218      *
219      * Returns immediately, though a call to AudioSource.push() may be in progress.
220      */
221     @Override
stopStream()222     public int stopStream() {
223         mRecording = false;
224         return OK;
225     }
226 
227     /**
228      * @return See StreamState constants
229      */
getStreamState()230     public int getStreamState() {
231         //TODO - track state so we can return something meaningful here.
232         return StreamState.UNKNOWN;
233     }
234 
235     /**
236      * @return The last error callback result (these must match Oboe). See Oboe constants
237      */
getLastErrorCallbackResult()238     public int getLastErrorCallbackResult() {
239         //TODO - track errors so we can return something meaningful here.
240         return ERROR_UNKNOWN;
241     }
242 
243     // @Override
244     // Used in JavaSinkHandler
getDataBuffer()245     public float[] getDataBuffer() {
246         return mRecorderBuffer;
247     }
248 
249     /*
250      * Recorder Thread
251      */
252     /**
253      * Implements the <code>run</code> method for the record thread.
254      * Starts the AudioRecord, then continuously reads audio data
255      * until the flag <code>mRecording</code> is set to false (in the stop() method).
256      */
257     private class RecorderRunnable implements Runnable {
258         @Override
run()259         public void run() {
260             final int numRecordSamples = mNumExchangeFrames * mChannelCount;
261             if (LOG) {
262                 Log.i(TAG, "numRecordSamples: " + numRecordSamples);
263             }
264 
265             int numReadSamples = 0;
266             while (mRecording) {
267                 numReadSamples = mAudioRecord.read(
268                         mRecorderBuffer, 0, numRecordSamples, AudioRecord.READ_BLOCKING);
269                 if (numReadSamples < 0) {
270                     // error
271                     if (LOG) {
272                         Log.e(TAG, "AudioRecord write error - numReadSamples: " + numReadSamples);
273                     }
274                     stopStream();
275                 } else if (numReadSamples < numRecordSamples) {
276                     // got less than requested?
277                     if (LOG) {
278                         Log.e(TAG, "AudioRecord Underflow: " + numReadSamples +
279                                 " vs. " + numRecordSamples);
280                     }
281                     stopStream();
282                 }
283 
284                 if (mListener != null) {
285                     // TODO: on error or underrun we may be send bogus data.
286                     mListener.sendEmptyMessage(JavaSinkHandler.MSG_BUFFER_FILL);
287                 }
288             }
289 
290             if (mListener != null) {
291                 // TODO: on error or underrun we may be send bogus data.
292                 Message message = new Message();
293                 message.what = JavaSinkHandler.MSG_STOP;
294                 message.arg1 = numReadSamples;
295                 mListener.sendMessage(message);
296             }
297             mAudioRecord.stop();
298         }
299     }
300 }
301