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.content.Context; 14 import android.media.AudioAttributes; 15 import android.media.AudioFormat; 16 import android.media.AudioManager; 17 import android.media.AudioTrack; 18 import android.os.Build; 19 import android.os.Process; 20 import androidx.annotation.Nullable; 21 import java.lang.Thread; 22 import java.nio.ByteBuffer; 23 import org.webrtc.ContextUtils; 24 import org.webrtc.Logging; 25 import org.webrtc.ThreadUtils; 26 27 public class WebRtcAudioTrack { 28 private static final boolean DEBUG = false; 29 30 private static final String TAG = "WebRtcAudioTrack"; 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 // The AudioTrackThread is allowed to wait for successful call to join() 43 // but the wait times out afther this amount of time. 44 private static final long AUDIO_TRACK_THREAD_JOIN_TIMEOUT_MS = 2000; 45 46 // By default, WebRTC creates audio tracks with a usage attribute 47 // corresponding to voice communications, such as telephony or VoIP. 48 private static final int DEFAULT_USAGE = AudioAttributes.USAGE_VOICE_COMMUNICATION; 49 private static int usageAttribute = DEFAULT_USAGE; 50 51 // This method overrides the default usage attribute and allows the user 52 // to set it to something else than AudioAttributes.USAGE_VOICE_COMMUNICATION. 53 // NOTE: calling this method will most likely break existing VoIP tuning. 54 // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 55 @SuppressWarnings("NoSynchronizedMethodCheck") setAudioTrackUsageAttribute(int usage)56 public static synchronized void setAudioTrackUsageAttribute(int usage) { 57 Logging.w(TAG, "Default usage attribute is changed from: " 58 + DEFAULT_USAGE + " to " + usage); 59 usageAttribute = usage; 60 } 61 62 private final long nativeAudioTrack; 63 private final AudioManager audioManager; 64 private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); 65 66 private ByteBuffer byteBuffer; 67 68 private @Nullable AudioTrack audioTrack; 69 private @Nullable AudioTrackThread audioThread; 70 71 // Samples to be played are replaced by zeros if `speakerMute` is set to true. 72 // Can be used to ensure that the speaker is fully muted. 73 private static volatile boolean speakerMute; 74 private byte[] emptyBytes; 75 76 // Audio playout/track error handler functions. 77 public enum AudioTrackStartErrorCode { 78 AUDIO_TRACK_START_EXCEPTION, 79 AUDIO_TRACK_START_STATE_MISMATCH, 80 } 81 82 @Deprecated 83 public static interface WebRtcAudioTrackErrorCallback { onWebRtcAudioTrackInitError(String errorMessage)84 void onWebRtcAudioTrackInitError(String errorMessage); onWebRtcAudioTrackStartError(String errorMessage)85 void onWebRtcAudioTrackStartError(String errorMessage); onWebRtcAudioTrackError(String errorMessage)86 void onWebRtcAudioTrackError(String errorMessage); 87 } 88 89 // TODO(henrika): upgrade all clients to use this new interface instead. 90 public static interface ErrorCallback { onWebRtcAudioTrackInitError(String errorMessage)91 void onWebRtcAudioTrackInitError(String errorMessage); onWebRtcAudioTrackStartError(AudioTrackStartErrorCode errorCode, String errorMessage)92 void onWebRtcAudioTrackStartError(AudioTrackStartErrorCode errorCode, String errorMessage); onWebRtcAudioTrackError(String errorMessage)93 void onWebRtcAudioTrackError(String errorMessage); 94 } 95 96 private static @Nullable WebRtcAudioTrackErrorCallback errorCallbackOld; 97 private static @Nullable ErrorCallback errorCallback; 98 99 @Deprecated setErrorCallback(WebRtcAudioTrackErrorCallback errorCallback)100 public static void setErrorCallback(WebRtcAudioTrackErrorCallback errorCallback) { 101 Logging.d(TAG, "Set error callback (deprecated"); 102 WebRtcAudioTrack.errorCallbackOld = errorCallback; 103 } 104 setErrorCallback(ErrorCallback errorCallback)105 public static void setErrorCallback(ErrorCallback errorCallback) { 106 Logging.d(TAG, "Set extended error callback"); 107 WebRtcAudioTrack.errorCallback = errorCallback; 108 } 109 110 /** 111 * Audio thread which keeps calling AudioTrack.write() to stream audio. 112 * Data is periodically acquired from the native WebRTC layer using the 113 * nativeGetPlayoutData callback function. 114 * This thread uses a Process.THREAD_PRIORITY_URGENT_AUDIO priority. 115 */ 116 private class AudioTrackThread extends Thread { 117 private volatile boolean keepAlive = true; 118 AudioTrackThread(String name)119 public AudioTrackThread(String name) { 120 super(name); 121 } 122 123 @Override run()124 public void run() { 125 Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); 126 Logging.d(TAG, "AudioTrackThread" + WebRtcAudioUtils.getThreadInfo()); 127 assertTrue(audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING); 128 129 // Fixed size in bytes of each 10ms block of audio data that we ask for 130 // using callbacks to the native WebRTC client. 131 final int sizeInBytes = byteBuffer.capacity(); 132 133 while (keepAlive) { 134 // Get 10ms of PCM data from the native WebRTC client. Audio data is 135 // written into the common ByteBuffer using the address that was 136 // cached at construction. 137 nativeGetPlayoutData(sizeInBytes, nativeAudioTrack); 138 // Write data until all data has been written to the audio sink. 139 // Upon return, the buffer position will have been advanced to reflect 140 // the amount of data that was successfully written to the AudioTrack. 141 assertTrue(sizeInBytes <= byteBuffer.remaining()); 142 if (speakerMute) { 143 byteBuffer.clear(); 144 byteBuffer.put(emptyBytes); 145 byteBuffer.position(0); 146 } 147 int bytesWritten = audioTrack.write(byteBuffer, sizeInBytes, AudioTrack.WRITE_BLOCKING); 148 if (bytesWritten != sizeInBytes) { 149 Logging.e(TAG, "AudioTrack.write played invalid number of bytes: " + bytesWritten); 150 // If a write() returns a negative value, an error has occurred. 151 // Stop playing and report an error in this case. 152 if (bytesWritten < 0) { 153 keepAlive = false; 154 reportWebRtcAudioTrackError("AudioTrack.write failed: " + bytesWritten); 155 } 156 } 157 // The byte buffer must be rewinded since byteBuffer.position() is 158 // increased at each call to AudioTrack.write(). If we don't do this, 159 // next call to AudioTrack.write() will fail. 160 byteBuffer.rewind(); 161 162 // TODO(henrika): it is possible to create a delay estimate here by 163 // counting number of written frames and subtracting the result from 164 // audioTrack.getPlaybackHeadPosition(). 165 } 166 167 // Stops playing the audio data. Since the instance was created in 168 // MODE_STREAM mode, audio will stop playing after the last buffer that 169 // was written has been played. 170 if (audioTrack != null) { 171 Logging.d(TAG, "Calling AudioTrack.stop..."); 172 try { 173 audioTrack.stop(); 174 Logging.d(TAG, "AudioTrack.stop is done."); 175 } catch (IllegalStateException e) { 176 Logging.e(TAG, "AudioTrack.stop failed: " + e.getMessage()); 177 } 178 } 179 } 180 181 // Stops the inner thread loop which results in calling AudioTrack.stop(). 182 // Does not block the calling thread. stopThread()183 public void stopThread() { 184 Logging.d(TAG, "stopThread"); 185 keepAlive = false; 186 } 187 } 188 WebRtcAudioTrack(long nativeAudioTrack)189 WebRtcAudioTrack(long nativeAudioTrack) { 190 threadChecker.checkIsOnValidThread(); 191 Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo()); 192 this.nativeAudioTrack = nativeAudioTrack; 193 audioManager = 194 (AudioManager) ContextUtils.getApplicationContext().getSystemService(Context.AUDIO_SERVICE); 195 if (DEBUG) { 196 WebRtcAudioUtils.logDeviceInfo(TAG); 197 } 198 } 199 initPlayout(int sampleRate, int channels, double bufferSizeFactor)200 private int initPlayout(int sampleRate, int channels, double bufferSizeFactor) { 201 threadChecker.checkIsOnValidThread(); 202 Logging.d(TAG, 203 "initPlayout(sampleRate=" + sampleRate + ", channels=" + channels 204 + ", bufferSizeFactor=" + bufferSizeFactor + ")"); 205 final int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8); 206 byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * (sampleRate / BUFFERS_PER_SECOND)); 207 Logging.d(TAG, "byteBuffer.capacity: " + byteBuffer.capacity()); 208 emptyBytes = new byte[byteBuffer.capacity()]; 209 // Rather than passing the ByteBuffer with every callback (requiring 210 // the potentially expensive GetDirectBufferAddress) we simply have the 211 // the native class cache the address to the memory once. 212 nativeCacheDirectBufferAddress(byteBuffer, nativeAudioTrack); 213 214 // Get the minimum buffer size required for the successful creation of an 215 // AudioTrack object to be created in the MODE_STREAM mode. 216 // Note that this size doesn't guarantee a smooth playback under load. 217 final int channelConfig = channelCountToConfiguration(channels); 218 final int minBufferSizeInBytes = (int) (AudioTrack.getMinBufferSize(sampleRate, channelConfig, 219 AudioFormat.ENCODING_PCM_16BIT) 220 * bufferSizeFactor); 221 Logging.d(TAG, "minBufferSizeInBytes: " + minBufferSizeInBytes); 222 // For the streaming mode, data must be written to the audio sink in 223 // chunks of size (given by byteBuffer.capacity()) less than or equal 224 // to the total buffer size `minBufferSizeInBytes`. But, we have seen 225 // reports of "getMinBufferSize(): error querying hardware". Hence, it 226 // can happen that `minBufferSizeInBytes` contains an invalid value. 227 if (minBufferSizeInBytes < byteBuffer.capacity()) { 228 reportWebRtcAudioTrackInitError("AudioTrack.getMinBufferSize returns an invalid value."); 229 return -1; 230 } 231 232 // Ensure that prevision audio session was stopped correctly before trying 233 // to create a new AudioTrack. 234 if (audioTrack != null) { 235 reportWebRtcAudioTrackInitError("Conflict with existing AudioTrack."); 236 return -1; 237 } 238 try { 239 // Create an AudioTrack object and initialize its associated audio buffer. 240 // The size of this buffer determines how long an AudioTrack can play 241 // before running out of data. 242 // As we are on API level 21 or higher, it is possible to use a special AudioTrack 243 // constructor that uses AudioAttributes and AudioFormat as input. It allows us to 244 // supersede the notion of stream types for defining the behavior of audio playback, 245 // and to allow certain platforms or routing policies to use this information for more 246 // refined volume or routing decisions. 247 audioTrack = createAudioTrack(sampleRate, channelConfig, minBufferSizeInBytes); 248 } catch (IllegalArgumentException e) { 249 reportWebRtcAudioTrackInitError(e.getMessage()); 250 releaseAudioResources(); 251 return -1; 252 } 253 254 // It can happen that an AudioTrack is created but it was not successfully 255 // initialized upon creation. Seems to be the case e.g. when the maximum 256 // number of globally available audio tracks is exceeded. 257 if (audioTrack == null || audioTrack.getState() != AudioTrack.STATE_INITIALIZED) { 258 reportWebRtcAudioTrackInitError("Initialization of audio track failed."); 259 releaseAudioResources(); 260 return -1; 261 } 262 logMainParameters(); 263 logMainParametersExtended(); 264 return minBufferSizeInBytes; 265 } 266 startPlayout()267 private boolean startPlayout() { 268 threadChecker.checkIsOnValidThread(); 269 Logging.d(TAG, "startPlayout"); 270 assertTrue(audioTrack != null); 271 assertTrue(audioThread == null); 272 273 // Starts playing an audio track. 274 try { 275 audioTrack.play(); 276 } catch (IllegalStateException e) { 277 reportWebRtcAudioTrackStartError(AudioTrackStartErrorCode.AUDIO_TRACK_START_EXCEPTION, 278 "AudioTrack.play failed: " + e.getMessage()); 279 releaseAudioResources(); 280 return false; 281 } 282 if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) { 283 reportWebRtcAudioTrackStartError( 284 AudioTrackStartErrorCode.AUDIO_TRACK_START_STATE_MISMATCH, 285 "AudioTrack.play failed - incorrect state :" 286 + audioTrack.getPlayState()); 287 releaseAudioResources(); 288 return false; 289 } 290 291 // Create and start new high-priority thread which calls AudioTrack.write() 292 // and where we also call the native nativeGetPlayoutData() callback to 293 // request decoded audio from WebRTC. 294 audioThread = new AudioTrackThread("AudioTrackJavaThread"); 295 audioThread.start(); 296 return true; 297 } 298 stopPlayout()299 private boolean stopPlayout() { 300 threadChecker.checkIsOnValidThread(); 301 Logging.d(TAG, "stopPlayout"); 302 assertTrue(audioThread != null); 303 logUnderrunCount(); 304 audioThread.stopThread(); 305 306 Logging.d(TAG, "Stopping the AudioTrackThread..."); 307 audioThread.interrupt(); 308 if (!ThreadUtils.joinUninterruptibly(audioThread, AUDIO_TRACK_THREAD_JOIN_TIMEOUT_MS)) { 309 Logging.e(TAG, "Join of AudioTrackThread timed out."); 310 WebRtcAudioUtils.logAudioState(TAG); 311 } 312 Logging.d(TAG, "AudioTrackThread has now been stopped."); 313 audioThread = null; 314 releaseAudioResources(); 315 return true; 316 } 317 318 // Get max possible volume index for a phone call audio stream. getStreamMaxVolume()319 private int getStreamMaxVolume() { 320 threadChecker.checkIsOnValidThread(); 321 Logging.d(TAG, "getStreamMaxVolume"); 322 assertTrue(audioManager != null); 323 return audioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL); 324 } 325 326 // Set current volume level for a phone call audio stream. setStreamVolume(int volume)327 private boolean setStreamVolume(int volume) { 328 threadChecker.checkIsOnValidThread(); 329 Logging.d(TAG, "setStreamVolume(" + volume + ")"); 330 assertTrue(audioManager != null); 331 if (audioManager.isVolumeFixed()) { 332 Logging.e(TAG, "The device implements a fixed volume policy."); 333 return false; 334 } 335 audioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, volume, 0); 336 return true; 337 } 338 339 /** Get current volume level for a phone call audio stream. */ getStreamVolume()340 private int getStreamVolume() { 341 threadChecker.checkIsOnValidThread(); 342 Logging.d(TAG, "getStreamVolume"); 343 assertTrue(audioManager != null); 344 return audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL); 345 } 346 logMainParameters()347 private void logMainParameters() { 348 Logging.d(TAG, "AudioTrack: " 349 + "session ID: " + audioTrack.getAudioSessionId() + ", " 350 + "channels: " + audioTrack.getChannelCount() + ", " 351 + "sample rate: " + audioTrack.getSampleRate() + ", " 352 // Gain (>=1.0) expressed as linear multiplier on sample values. 353 + "max gain: " + AudioTrack.getMaxVolume()); 354 } 355 356 // Creates and AudioTrack instance using AudioAttributes and AudioFormat as input. 357 // It allows certain platforms or routing policies to use this information for more 358 // refined volume or routing decisions. createAudioTrack( int sampleRateInHz, int channelConfig, int bufferSizeInBytes)359 private static AudioTrack createAudioTrack( 360 int sampleRateInHz, int channelConfig, int bufferSizeInBytes) { 361 Logging.d(TAG, "createAudioTrack"); 362 // TODO(henrika): use setPerformanceMode(int) with PERFORMANCE_MODE_LOW_LATENCY to control 363 // performance when Android O is supported. Add some logging in the mean time. 364 final int nativeOutputSampleRate = 365 AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_VOICE_CALL); 366 Logging.d(TAG, "nativeOutputSampleRate: " + nativeOutputSampleRate); 367 if (sampleRateInHz != nativeOutputSampleRate) { 368 Logging.w(TAG, "Unable to use fast mode since requested sample rate is not native"); 369 } 370 if (usageAttribute != DEFAULT_USAGE) { 371 Logging.w(TAG, "A non default usage attribute is used: " + usageAttribute); 372 } 373 // Create an audio track where the audio usage is for VoIP and the content type is speech. 374 return new AudioTrack( 375 new AudioAttributes.Builder() 376 .setUsage(usageAttribute) 377 .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) 378 .build(), 379 new AudioFormat.Builder() 380 .setEncoding(AudioFormat.ENCODING_PCM_16BIT) 381 .setSampleRate(sampleRateInHz) 382 .setChannelMask(channelConfig) 383 .build(), 384 bufferSizeInBytes, 385 AudioTrack.MODE_STREAM, 386 AudioManager.AUDIO_SESSION_ID_GENERATE); 387 } 388 logBufferSizeInFrames()389 private void logBufferSizeInFrames() { 390 if (Build.VERSION.SDK_INT >= 23) { 391 Logging.d(TAG, "AudioTrack: " 392 // The effective size of the AudioTrack buffer that the app writes to. 393 + "buffer size in frames: " + audioTrack.getBufferSizeInFrames()); 394 } 395 } 396 getBufferSizeInFrames()397 private int getBufferSizeInFrames() { 398 if (Build.VERSION.SDK_INT >= 23) { 399 return audioTrack.getBufferSizeInFrames(); 400 } 401 return -1; 402 } 403 logBufferCapacityInFrames()404 private void logBufferCapacityInFrames() { 405 if (Build.VERSION.SDK_INT >= 24) { 406 Logging.d(TAG, 407 "AudioTrack: " 408 // Maximum size of the AudioTrack buffer in frames. 409 + "buffer capacity in frames: " + audioTrack.getBufferCapacityInFrames()); 410 } 411 } 412 logMainParametersExtended()413 private void logMainParametersExtended() { 414 logBufferSizeInFrames(); 415 logBufferCapacityInFrames(); 416 } 417 418 // Prints the number of underrun occurrences in the application-level write 419 // buffer since the AudioTrack was created. An underrun occurs if the app does 420 // not write audio data quickly enough, causing the buffer to underflow and a 421 // potential audio glitch. 422 // TODO(henrika): keep track of this value in the field and possibly add new 423 // UMA stat if needed. logUnderrunCount()424 private void logUnderrunCount() { 425 if (Build.VERSION.SDK_INT >= 24) { 426 Logging.d(TAG, "underrun count: " + audioTrack.getUnderrunCount()); 427 } 428 } 429 430 // Helper method which throws an exception when an assertion has failed. assertTrue(boolean condition)431 private static void assertTrue(boolean condition) { 432 if (!condition) { 433 throw new AssertionError("Expected condition to be true"); 434 } 435 } 436 channelCountToConfiguration(int channels)437 private int channelCountToConfiguration(int channels) { 438 return (channels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO); 439 } 440 nativeCacheDirectBufferAddress(ByteBuffer byteBuffer, long nativeAudioRecord)441 private native void nativeCacheDirectBufferAddress(ByteBuffer byteBuffer, long nativeAudioRecord); 442 nativeGetPlayoutData(int bytes, long nativeAudioRecord)443 private native void nativeGetPlayoutData(int bytes, long nativeAudioRecord); 444 445 // Sets all samples to be played out to zero if `mute` is true, i.e., 446 // ensures that the speaker is muted. setSpeakerMute(boolean mute)447 public static void setSpeakerMute(boolean mute) { 448 Logging.w(TAG, "setSpeakerMute(" + mute + ")"); 449 speakerMute = mute; 450 } 451 452 // Releases the native AudioTrack resources. releaseAudioResources()453 private void releaseAudioResources() { 454 Logging.d(TAG, "releaseAudioResources"); 455 if (audioTrack != null) { 456 audioTrack.release(); 457 audioTrack = null; 458 } 459 } 460 reportWebRtcAudioTrackInitError(String errorMessage)461 private void reportWebRtcAudioTrackInitError(String errorMessage) { 462 Logging.e(TAG, "Init playout error: " + errorMessage); 463 WebRtcAudioUtils.logAudioState(TAG); 464 if (errorCallbackOld != null) { 465 errorCallbackOld.onWebRtcAudioTrackInitError(errorMessage); 466 } 467 if (errorCallback != null) { 468 errorCallback.onWebRtcAudioTrackInitError(errorMessage); 469 } 470 } 471 reportWebRtcAudioTrackStartError( AudioTrackStartErrorCode errorCode, String errorMessage)472 private void reportWebRtcAudioTrackStartError( 473 AudioTrackStartErrorCode errorCode, String errorMessage) { 474 Logging.e(TAG, "Start playout error: " + errorCode + ". " + errorMessage); 475 WebRtcAudioUtils.logAudioState(TAG); 476 if (errorCallbackOld != null) { 477 errorCallbackOld.onWebRtcAudioTrackStartError(errorMessage); 478 } 479 if (errorCallback != null) { 480 errorCallback.onWebRtcAudioTrackStartError(errorCode, errorMessage); 481 } 482 } 483 reportWebRtcAudioTrackError(String errorMessage)484 private void reportWebRtcAudioTrackError(String errorMessage) { 485 Logging.e(TAG, "Run-time playback error: " + errorMessage); 486 WebRtcAudioUtils.logAudioState(TAG); 487 if (errorCallbackOld != null) { 488 errorCallbackOld.onWebRtcAudioTrackError(errorMessage); 489 } 490 if (errorCallback != null) { 491 errorCallback.onWebRtcAudioTrackError(errorMessage); 492 } 493 } 494 } 495