1 /*
2  *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
3  *
4  *  Use of this source code is governed by a BSD-style license
5  *  that can be found in the LICENSE file in the root of the source
6  *  tree. An additional intellectual property rights grant can be found
7  *  in the file PATENTS.  All contributing project authors may
8  *  be found in the AUTHORS file in the root of the source tree.
9  */
10 
11 package org.webrtc.voiceengine;
12 
13 import java.lang.System;
14 import java.nio.ByteBuffer;
15 import java.util.concurrent.TimeUnit;
16 
17 import android.content.Context;
18 import android.media.AudioFormat;
19 import android.media.AudioRecord;
20 import android.media.MediaRecorder.AudioSource;
21 import android.os.Build;
22 import android.os.Process;
23 import android.os.SystemClock;
24 
25 import org.webrtc.Logging;
26 
27 class  WebRtcAudioRecord {
28   private static final boolean DEBUG = false;
29 
30   private static final String TAG = "WebRtcAudioRecord";
31 
32   // Default audio data format is PCM 16 bit per sample.
33   // Guaranteed to be supported by all devices.
34   private static final int BITS_PER_SAMPLE = 16;
35 
36   // Requested size of each recorded buffer provided to the client.
37   private static final int CALLBACK_BUFFER_SIZE_MS = 10;
38 
39   // Average number of callbacks per second.
40   private static final int BUFFERS_PER_SECOND = 1000 / CALLBACK_BUFFER_SIZE_MS;
41 
42   // We ask for a native buffer size of BUFFER_SIZE_FACTOR * (minimum required
43   // buffer size). The extra space is allocated to guard against glitches under
44   // high load.
45   private static final int BUFFER_SIZE_FACTOR = 2;
46 
47   private final long nativeAudioRecord;
48   private final Context context;
49 
50   private WebRtcAudioEffects effects = null;
51 
52   private ByteBuffer byteBuffer;
53 
54   private AudioRecord audioRecord = null;
55   private AudioRecordThread audioThread = null;
56 
57   /**
58    * Audio thread which keeps calling ByteBuffer.read() waiting for audio
59    * to be recorded. Feeds recorded data to the native counterpart as a
60    * periodic sequence of callbacks using DataIsRecorded().
61    * This thread uses a Process.THREAD_PRIORITY_URGENT_AUDIO priority.
62    */
63   private class AudioRecordThread extends Thread {
64     private volatile boolean keepAlive = true;
65 
AudioRecordThread(String name)66     public AudioRecordThread(String name) {
67       super(name);
68     }
69 
70     @Override
run()71     public void run() {
72       Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
73       Logging.d(TAG, "AudioRecordThread" + WebRtcAudioUtils.getThreadInfo());
74       assertTrue(audioRecord.getRecordingState()
75           == AudioRecord.RECORDSTATE_RECORDING);
76 
77       long lastTime = System.nanoTime();
78       while (keepAlive) {
79         int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity());
80         if (bytesRead == byteBuffer.capacity()) {
81           nativeDataIsRecorded(bytesRead, nativeAudioRecord);
82         } else {
83           Logging.e(TAG,"AudioRecord.read failed: " + bytesRead);
84           if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {
85             keepAlive = false;
86           }
87         }
88         if (DEBUG) {
89           long nowTime = System.nanoTime();
90           long durationInMs =
91               TimeUnit.NANOSECONDS.toMillis((nowTime - lastTime));
92           lastTime = nowTime;
93           Logging.d(TAG, "bytesRead[" + durationInMs + "] " + bytesRead);
94         }
95       }
96 
97       try {
98         audioRecord.stop();
99       } catch (IllegalStateException e) {
100         Logging.e(TAG,"AudioRecord.stop failed: " + e.getMessage());
101       }
102     }
103 
joinThread()104     public void joinThread() {
105       keepAlive = false;
106       while (isAlive()) {
107         try {
108           join();
109         } catch (InterruptedException e) {
110           // Ignore.
111         }
112       }
113     }
114   }
115 
WebRtcAudioRecord(Context context, long nativeAudioRecord)116   WebRtcAudioRecord(Context context, long nativeAudioRecord) {
117     Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo());
118     this.context = context;
119     this.nativeAudioRecord = nativeAudioRecord;
120     if (DEBUG) {
121       WebRtcAudioUtils.logDeviceInfo(TAG);
122     }
123     effects = WebRtcAudioEffects.create();
124   }
125 
enableBuiltInAEC(boolean enable)126   private boolean enableBuiltInAEC(boolean enable) {
127     Logging.d(TAG, "enableBuiltInAEC(" + enable + ')');
128     if (effects == null) {
129       Logging.e(TAG,"Built-in AEC is not supported on this platform");
130       return false;
131     }
132     return effects.setAEC(enable);
133   }
134 
enableBuiltInAGC(boolean enable)135   private boolean enableBuiltInAGC(boolean enable) {
136     Logging.d(TAG, "enableBuiltInAGC(" + enable + ')');
137     if (effects == null) {
138       Logging.e(TAG,"Built-in AGC is not supported on this platform");
139       return false;
140     }
141     return effects.setAGC(enable);
142   }
143 
enableBuiltInNS(boolean enable)144   private boolean enableBuiltInNS(boolean enable) {
145     Logging.d(TAG, "enableBuiltInNS(" + enable + ')');
146     if (effects == null) {
147       Logging.e(TAG,"Built-in NS is not supported on this platform");
148       return false;
149     }
150     return effects.setNS(enable);
151   }
152 
initRecording(int sampleRate, int channels)153   private int initRecording(int sampleRate, int channels) {
154     Logging.d(TAG, "initRecording(sampleRate=" + sampleRate + ", channels=" +
155         channels + ")");
156     if (!WebRtcAudioUtils.hasPermission(
157         context, android.Manifest.permission.RECORD_AUDIO)) {
158       Logging.e(TAG,"RECORD_AUDIO permission is missing");
159       return -1;
160     }
161     if (audioRecord != null) {
162       Logging.e(TAG,"InitRecording() called twice without StopRecording()");
163       return -1;
164     }
165     final int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8);
166     final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND;
167     byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);
168     Logging.d(TAG, "byteBuffer.capacity: " + byteBuffer.capacity());
169     // Rather than passing the ByteBuffer with every callback (requiring
170     // the potentially expensive GetDirectBufferAddress) we simply have the
171     // the native class cache the address to the memory once.
172     nativeCacheDirectBufferAddress(byteBuffer, nativeAudioRecord);
173 
174     // Get the minimum buffer size required for the successful creation of
175     // an AudioRecord object, in byte units.
176     // Note that this size doesn't guarantee a smooth recording under load.
177     int minBufferSize = AudioRecord.getMinBufferSize(
178           sampleRate,
179           AudioFormat.CHANNEL_IN_MONO,
180           AudioFormat.ENCODING_PCM_16BIT);
181     if (minBufferSize == AudioRecord.ERROR
182         || minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
183       Logging.e(TAG, "AudioRecord.getMinBufferSize failed: " + minBufferSize);
184       return -1;
185     }
186     Logging.d(TAG, "AudioRecord.getMinBufferSize: " + minBufferSize);
187 
188     // Use a larger buffer size than the minimum required when creating the
189     // AudioRecord instance to ensure smooth recording under load. It has been
190     // verified that it does not increase the actual recording latency.
191     int bufferSizeInBytes =
192         Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity());
193     Logging.d(TAG, "bufferSizeInBytes: " + bufferSizeInBytes);
194     try {
195       audioRecord = new AudioRecord(AudioSource.VOICE_COMMUNICATION,
196                                     sampleRate,
197                                     AudioFormat.CHANNEL_IN_MONO,
198                                     AudioFormat.ENCODING_PCM_16BIT,
199                                     bufferSizeInBytes);
200     } catch (IllegalArgumentException e) {
201       Logging.e(TAG,e.getMessage());
202       return -1;
203     }
204     if (audioRecord == null ||
205         audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
206       Logging.e(TAG,"Failed to create a new AudioRecord instance");
207       return -1;
208     }
209     Logging.d(TAG, "AudioRecord "
210         + "session ID: " + audioRecord.getAudioSessionId() + ", "
211         + "audio format: " + audioRecord.getAudioFormat() + ", "
212         + "channels: " + audioRecord.getChannelCount() + ", "
213         + "sample rate: " + audioRecord.getSampleRate());
214     if (effects != null) {
215       effects.enable(audioRecord.getAudioSessionId());
216     }
217     // TODO(phoglund): put back audioRecord.getBufferSizeInFrames when
218     // all known downstream users supports M.
219     // if (WebRtcAudioUtils.runningOnMOrHigher()) {
220       // Returns the frame count of the native AudioRecord buffer. This is
221       // greater than or equal to the bufferSizeInBytes converted to frame
222       // units. The native frame count may be enlarged to accommodate the
223       // requirements of the source on creation or if the AudioRecord is
224       // subsequently rerouted.
225 
226       // Logging.d(TAG, "bufferSizeInFrames: "
227       //     + audioRecord.getBufferSizeInFrames());
228     //}
229     return framesPerBuffer;
230   }
231 
startRecording()232   private boolean startRecording() {
233     Logging.d(TAG, "startRecording");
234     assertTrue(audioRecord != null);
235     assertTrue(audioThread == null);
236     try {
237       audioRecord.startRecording();
238     } catch (IllegalStateException e) {
239       Logging.e(TAG,"AudioRecord.startRecording failed: " + e.getMessage());
240       return false;
241     }
242     if (audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
243       Logging.e(TAG,"AudioRecord.startRecording failed");
244       return false;
245     }
246     audioThread = new AudioRecordThread("AudioRecordJavaThread");
247     audioThread.start();
248     return true;
249   }
250 
stopRecording()251   private boolean stopRecording() {
252     Logging.d(TAG, "stopRecording");
253     assertTrue(audioThread != null);
254     audioThread.joinThread();
255     audioThread = null;
256     if (effects != null) {
257       effects.release();
258     }
259     audioRecord.release();
260     audioRecord = null;
261     return true;
262   }
263 
264   // Helper method which throws an exception  when an assertion has failed.
assertTrue(boolean condition)265   private static void assertTrue(boolean condition) {
266     if (!condition) {
267       throw new AssertionError("Expected condition to be true");
268     }
269   }
270 
nativeCacheDirectBufferAddress( ByteBuffer byteBuffer, long nativeAudioRecord)271   private native void nativeCacheDirectBufferAddress(
272       ByteBuffer byteBuffer, long nativeAudioRecord);
273 
nativeDataIsRecorded(int bytes, long nativeAudioRecord)274   private native void nativeDataIsRecorded(int bytes, long nativeAudioRecord);
275 }
276