• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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