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 android.annotation.TargetApi; 14 import android.content.Context; 15 import android.media.AudioFormat; 16 import android.media.AudioRecord; 17 import android.media.MediaRecorder.AudioSource; 18 import android.os.Process; 19 import java.lang.System; 20 import java.nio.ByteBuffer; 21 import java.util.concurrent.TimeUnit; 22 import org.webrtc.ContextUtils; 23 import org.webrtc.Logging; 24 import org.webrtc.ThreadUtils; 25 26 public class WebRtcAudioRecord { 27 private static final boolean DEBUG = false; 28 29 private static final String TAG = "WebRtcAudioRecord"; 30 31 // Default audio data format is PCM 16 bit per sample. 32 // Guaranteed to be supported by all devices. 33 private static final int BITS_PER_SAMPLE = 16; 34 35 // Requested size of each recorded buffer provided to the client. 36 private static final int CALLBACK_BUFFER_SIZE_MS = 10; 37 38 // Average number of callbacks per second. 39 private static final int BUFFERS_PER_SECOND = 1000 / CALLBACK_BUFFER_SIZE_MS; 40 41 // We ask for a native buffer size of BUFFER_SIZE_FACTOR * (minimum required 42 // buffer size). The extra space is allocated to guard against glitches under 43 // high load. 44 private static final int BUFFER_SIZE_FACTOR = 2; 45 46 // The AudioRecordJavaThread is allowed to wait for successful call to join() 47 // but the wait times out afther this amount of time. 48 private static final long AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS = 2000; 49 50 private static final int DEFAULT_AUDIO_SOURCE = getDefaultAudioSource(); 51 private static int audioSource = DEFAULT_AUDIO_SOURCE; 52 53 private final long nativeAudioRecord; 54 55 private WebRtcAudioEffects effects = null; 56 57 private ByteBuffer byteBuffer; 58 59 private AudioRecord audioRecord = null; 60 private AudioRecordThread audioThread = null; 61 62 private static volatile boolean microphoneMute = false; 63 private byte[] emptyBytes; 64 65 // Audio recording error handler functions. 66 public enum AudioRecordStartErrorCode { 67 AUDIO_RECORD_START_EXCEPTION, 68 AUDIO_RECORD_START_STATE_MISMATCH, 69 } 70 71 public static interface WebRtcAudioRecordErrorCallback { onWebRtcAudioRecordInitError(String errorMessage)72 void onWebRtcAudioRecordInitError(String errorMessage); onWebRtcAudioRecordStartError(AudioRecordStartErrorCode errorCode, String errorMessage)73 void onWebRtcAudioRecordStartError(AudioRecordStartErrorCode errorCode, String errorMessage); onWebRtcAudioRecordError(String errorMessage)74 void onWebRtcAudioRecordError(String errorMessage); 75 } 76 77 private static WebRtcAudioRecordErrorCallback errorCallback = null; 78 setErrorCallback(WebRtcAudioRecordErrorCallback errorCallback)79 public static void setErrorCallback(WebRtcAudioRecordErrorCallback errorCallback) { 80 Logging.d(TAG, "Set error callback"); 81 WebRtcAudioRecord.errorCallback = errorCallback; 82 } 83 84 /** 85 * Audio thread which keeps calling ByteBuffer.read() waiting for audio 86 * to be recorded. Feeds recorded data to the native counterpart as a 87 * periodic sequence of callbacks using DataIsRecorded(). 88 * This thread uses a Process.THREAD_PRIORITY_URGENT_AUDIO priority. 89 */ 90 private class AudioRecordThread extends Thread { 91 private volatile boolean keepAlive = true; 92 AudioRecordThread(String name)93 public AudioRecordThread(String name) { 94 super(name); 95 } 96 97 @Override run()98 public void run() { 99 Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); 100 Logging.d(TAG, "AudioRecordThread" + WebRtcAudioUtils.getThreadInfo()); 101 assertTrue(audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING); 102 103 long lastTime = System.nanoTime(); 104 while (keepAlive) { 105 int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity()); 106 if (bytesRead == byteBuffer.capacity()) { 107 if (microphoneMute) { 108 byteBuffer.clear(); 109 byteBuffer.put(emptyBytes); 110 } 111 nativeDataIsRecorded(bytesRead, nativeAudioRecord); 112 } else { 113 String errorMessage = "AudioRecord.read failed: " + bytesRead; 114 Logging.e(TAG, errorMessage); 115 if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) { 116 keepAlive = false; 117 reportWebRtcAudioRecordError(errorMessage); 118 } 119 } 120 if (DEBUG) { 121 long nowTime = System.nanoTime(); 122 long durationInMs = TimeUnit.NANOSECONDS.toMillis((nowTime - lastTime)); 123 lastTime = nowTime; 124 Logging.d(TAG, "bytesRead[" + durationInMs + "] " + bytesRead); 125 } 126 } 127 128 try { 129 if (audioRecord != null) { 130 audioRecord.stop(); 131 } 132 } catch (IllegalStateException e) { 133 Logging.e(TAG, "AudioRecord.stop failed: " + e.getMessage()); 134 } 135 } 136 137 // Stops the inner thread loop and also calls AudioRecord.stop(). 138 // Does not block the calling thread. stopThread()139 public void stopThread() { 140 Logging.d(TAG, "stopThread"); 141 keepAlive = false; 142 } 143 } 144 WebRtcAudioRecord(long nativeAudioRecord)145 WebRtcAudioRecord(long nativeAudioRecord) { 146 Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo()); 147 this.nativeAudioRecord = nativeAudioRecord; 148 if (DEBUG) { 149 WebRtcAudioUtils.logDeviceInfo(TAG); 150 } 151 effects = WebRtcAudioEffects.create(); 152 } 153 enableBuiltInAEC(boolean enable)154 private boolean enableBuiltInAEC(boolean enable) { 155 Logging.d(TAG, "enableBuiltInAEC(" + enable + ')'); 156 if (effects == null) { 157 Logging.e(TAG, "Built-in AEC is not supported on this platform"); 158 return false; 159 } 160 return effects.setAEC(enable); 161 } 162 enableBuiltInNS(boolean enable)163 private boolean enableBuiltInNS(boolean enable) { 164 Logging.d(TAG, "enableBuiltInNS(" + enable + ')'); 165 if (effects == null) { 166 Logging.e(TAG, "Built-in NS is not supported on this platform"); 167 return false; 168 } 169 return effects.setNS(enable); 170 } 171 initRecording(int sampleRate, int channels)172 private int initRecording(int sampleRate, int channels) { 173 Logging.d(TAG, "initRecording(sampleRate=" + sampleRate + ", channels=" + channels + ")"); 174 if (audioRecord != null) { 175 reportWebRtcAudioRecordInitError("InitRecording called twice without StopRecording."); 176 return -1; 177 } 178 final int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8); 179 final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND; 180 byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer); 181 Logging.d(TAG, "byteBuffer.capacity: " + byteBuffer.capacity()); 182 emptyBytes = new byte[byteBuffer.capacity()]; 183 // Rather than passing the ByteBuffer with every callback (requiring 184 // the potentially expensive GetDirectBufferAddress) we simply have the 185 // the native class cache the address to the memory once. 186 nativeCacheDirectBufferAddress(byteBuffer, nativeAudioRecord); 187 188 // Get the minimum buffer size required for the successful creation of 189 // an AudioRecord object, in byte units. 190 // Note that this size doesn't guarantee a smooth recording under load. 191 final int channelConfig = channelCountToConfiguration(channels); 192 int minBufferSize = 193 AudioRecord.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT); 194 if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) { 195 reportWebRtcAudioRecordInitError("AudioRecord.getMinBufferSize failed: " + minBufferSize); 196 return -1; 197 } 198 Logging.d(TAG, "AudioRecord.getMinBufferSize: " + minBufferSize); 199 200 // Use a larger buffer size than the minimum required when creating the 201 // AudioRecord instance to ensure smooth recording under load. It has been 202 // verified that it does not increase the actual recording latency. 203 int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity()); 204 Logging.d(TAG, "bufferSizeInBytes: " + bufferSizeInBytes); 205 try { 206 audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, 207 AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes); 208 } catch (IllegalArgumentException e) { 209 reportWebRtcAudioRecordInitError("AudioRecord ctor error: " + e.getMessage()); 210 releaseAudioResources(); 211 return -1; 212 } 213 if (audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { 214 reportWebRtcAudioRecordInitError("Failed to create a new AudioRecord instance"); 215 releaseAudioResources(); 216 return -1; 217 } 218 if (effects != null) { 219 effects.enable(audioRecord.getAudioSessionId()); 220 } 221 logMainParameters(); 222 logMainParametersExtended(); 223 return framesPerBuffer; 224 } 225 startRecording()226 private boolean startRecording() { 227 Logging.d(TAG, "startRecording"); 228 assertTrue(audioRecord != null); 229 assertTrue(audioThread == null); 230 try { 231 audioRecord.startRecording(); 232 } catch (IllegalStateException e) { 233 reportWebRtcAudioRecordStartError(AudioRecordStartErrorCode.AUDIO_RECORD_START_EXCEPTION, 234 "AudioRecord.startRecording failed: " + e.getMessage()); 235 return false; 236 } 237 if (audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { 238 reportWebRtcAudioRecordStartError( 239 AudioRecordStartErrorCode.AUDIO_RECORD_START_STATE_MISMATCH, 240 "AudioRecord.startRecording failed - incorrect state :" 241 + audioRecord.getRecordingState()); 242 return false; 243 } 244 audioThread = new AudioRecordThread("AudioRecordJavaThread"); 245 audioThread.start(); 246 return true; 247 } 248 stopRecording()249 private boolean stopRecording() { 250 Logging.d(TAG, "stopRecording"); 251 assertTrue(audioThread != null); 252 audioThread.stopThread(); 253 if (!ThreadUtils.joinUninterruptibly(audioThread, AUDIO_RECORD_THREAD_JOIN_TIMEOUT_MS)) { 254 Logging.e(TAG, "Join of AudioRecordJavaThread timed out"); 255 } 256 audioThread = null; 257 if (effects != null) { 258 effects.release(); 259 } 260 releaseAudioResources(); 261 return true; 262 } 263 logMainParameters()264 private void logMainParameters() { 265 Logging.d(TAG, "AudioRecord: " 266 + "session ID: " + audioRecord.getAudioSessionId() + ", " 267 + "channels: " + audioRecord.getChannelCount() + ", " 268 + "sample rate: " + audioRecord.getSampleRate()); 269 } 270 271 @TargetApi(23) logMainParametersExtended()272 private void logMainParametersExtended() { 273 if (WebRtcAudioUtils.runningOnMarshmallowOrHigher()) { 274 Logging.d(TAG, "AudioRecord: " 275 // The frame count of the native AudioRecord buffer. 276 + "buffer size in frames: " + audioRecord.getBufferSizeInFrames()); 277 } 278 } 279 280 // Helper method which throws an exception when an assertion has failed. assertTrue(boolean condition)281 private static void assertTrue(boolean condition) { 282 if (!condition) { 283 throw new AssertionError("Expected condition to be true"); 284 } 285 } 286 channelCountToConfiguration(int channels)287 private int channelCountToConfiguration(int channels) { 288 return (channels == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO); 289 } 290 nativeCacheDirectBufferAddress(ByteBuffer byteBuffer, long nativeAudioRecord)291 private native void nativeCacheDirectBufferAddress(ByteBuffer byteBuffer, long nativeAudioRecord); 292 nativeDataIsRecorded(int bytes, long nativeAudioRecord)293 private native void nativeDataIsRecorded(int bytes, long nativeAudioRecord); 294 295 @SuppressWarnings("NoSynchronizedMethodCheck") setAudioSource(int source)296 public static synchronized void setAudioSource(int source) { 297 Logging.w(TAG, "Audio source is changed from: " + audioSource 298 + " to " + source); 299 audioSource = source; 300 } 301 getDefaultAudioSource()302 private static int getDefaultAudioSource() { 303 return AudioSource.VOICE_COMMUNICATION; 304 } 305 306 // Sets all recorded samples to zero if |mute| is true, i.e., ensures that 307 // the microphone is muted. setMicrophoneMute(boolean mute)308 public static void setMicrophoneMute(boolean mute) { 309 Logging.w(TAG, "setMicrophoneMute(" + mute + ")"); 310 microphoneMute = mute; 311 } 312 313 // Releases the native AudioRecord resources. releaseAudioResources()314 private void releaseAudioResources() { 315 if (audioRecord != null) { 316 audioRecord.release(); 317 audioRecord = null; 318 } 319 } 320 reportWebRtcAudioRecordInitError(String errorMessage)321 private void reportWebRtcAudioRecordInitError(String errorMessage) { 322 Logging.e(TAG, "Init recording error: " + errorMessage); 323 if (errorCallback != null) { 324 errorCallback.onWebRtcAudioRecordInitError(errorMessage); 325 } 326 } 327 reportWebRtcAudioRecordStartError( AudioRecordStartErrorCode errorCode, String errorMessage)328 private void reportWebRtcAudioRecordStartError( 329 AudioRecordStartErrorCode errorCode, String errorMessage) { 330 Logging.e(TAG, "Start recording error: " + errorCode + ". " + errorMessage); 331 if (errorCallback != null) { 332 errorCallback.onWebRtcAudioRecordStartError(errorCode, errorMessage); 333 } 334 } 335 reportWebRtcAudioRecordError(String errorMessage)336 private void reportWebRtcAudioRecordError(String errorMessage) { 337 Logging.e(TAG, "Run-time recording error: " + errorMessage); 338 if (errorCallback != null) { 339 errorCallback.onWebRtcAudioRecordError(errorMessage); 340 } 341 } 342 } 343