• 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.media.audiofx.AcousticEchoCanceler;
14 import android.media.audiofx.AudioEffect;
15 import android.media.audiofx.AudioEffect.Descriptor;
16 import android.media.audiofx.NoiseSuppressor;
17 import android.os.Build;
18 import androidx.annotation.Nullable;
19 import java.util.List;
20 import java.util.UUID;
21 import org.webrtc.Logging;
22 
23 // This class wraps control of three different platform effects. Supported
24 // effects are: AcousticEchoCanceler (AEC) and NoiseSuppressor (NS).
25 // Calling enable() will active all effects that are
26 // supported by the device if the corresponding `shouldEnableXXX` member is set.
27 public class WebRtcAudioEffects {
28   private static final boolean DEBUG = false;
29 
30   private static final String TAG = "WebRtcAudioEffects";
31 
32   // UUIDs for Software Audio Effects that we want to avoid using.
33   // The implementor field will be set to "The Android Open Source Project".
34   private static final UUID AOSP_ACOUSTIC_ECHO_CANCELER =
35       UUID.fromString("bb392ec0-8d4d-11e0-a896-0002a5d5c51b");
36   private static final UUID AOSP_NOISE_SUPPRESSOR =
37       UUID.fromString("c06c8400-8e06-11e0-9cb6-0002a5d5c51b");
38 
39   // Contains the available effect descriptors returned from the
40   // AudioEffect.getEffects() call. This result is cached to avoid doing the
41   // slow OS call multiple times.
42   private static @Nullable Descriptor[] cachedEffects;
43 
44   // Contains the audio effect objects. Created in enable() and destroyed
45   // in release().
46   private @Nullable AcousticEchoCanceler aec;
47   private @Nullable NoiseSuppressor ns;
48 
49   // Affects the final state given to the setEnabled() method on each effect.
50   // The default state is set to "disabled" but each effect can also be enabled
51   // by calling setAEC() and setNS().
52   // To enable an effect, both the shouldEnableXXX member and the static
53   // canUseXXX() must be true.
54   private boolean shouldEnableAec;
55   private boolean shouldEnableNs;
56 
57   // Checks if the device implements Acoustic Echo Cancellation (AEC).
58   // Returns true if the device implements AEC, false otherwise.
isAcousticEchoCancelerSupported()59   public static boolean isAcousticEchoCancelerSupported() {
60     // Note: we're using isAcousticEchoCancelerEffectAvailable() instead of
61     // AcousticEchoCanceler.isAvailable() to avoid the expensive getEffects()
62     // OS API call.
63     return isAcousticEchoCancelerEffectAvailable();
64   }
65 
66   // Checks if the device implements Noise Suppression (NS).
67   // Returns true if the device implements NS, false otherwise.
isNoiseSuppressorSupported()68   public static boolean isNoiseSuppressorSupported() {
69     // Note: we're using isNoiseSuppressorEffectAvailable() instead of
70     // NoiseSuppressor.isAvailable() to avoid the expensive getEffects()
71     // OS API call.
72     return isNoiseSuppressorEffectAvailable();
73   }
74 
75   // Returns true if the device is blacklisted for HW AEC usage.
isAcousticEchoCancelerBlacklisted()76   public static boolean isAcousticEchoCancelerBlacklisted() {
77     List<String> blackListedModels = WebRtcAudioUtils.getBlackListedModelsForAecUsage();
78     boolean isBlacklisted = blackListedModels.contains(Build.MODEL);
79     if (isBlacklisted) {
80       Logging.w(TAG, Build.MODEL + " is blacklisted for HW AEC usage!");
81     }
82     return isBlacklisted;
83   }
84 
85   // Returns true if the device is blacklisted for HW NS usage.
isNoiseSuppressorBlacklisted()86   public static boolean isNoiseSuppressorBlacklisted() {
87     List<String> blackListedModels = WebRtcAudioUtils.getBlackListedModelsForNsUsage();
88     boolean isBlacklisted = blackListedModels.contains(Build.MODEL);
89     if (isBlacklisted) {
90       Logging.w(TAG, Build.MODEL + " is blacklisted for HW NS usage!");
91     }
92     return isBlacklisted;
93   }
94 
95   // Returns true if the platform AEC should be excluded based on its UUID.
96   // AudioEffect.queryEffects() can throw IllegalStateException.
isAcousticEchoCancelerExcludedByUUID()97   private static boolean isAcousticEchoCancelerExcludedByUUID() {
98     for (Descriptor d : getAvailableEffects()) {
99       if (d.type.equals(AudioEffect.EFFECT_TYPE_AEC)
100           && d.uuid.equals(AOSP_ACOUSTIC_ECHO_CANCELER)) {
101         return true;
102       }
103     }
104     return false;
105   }
106 
107   // Returns true if the platform NS should be excluded based on its UUID.
108   // AudioEffect.queryEffects() can throw IllegalStateException.
isNoiseSuppressorExcludedByUUID()109   private static boolean isNoiseSuppressorExcludedByUUID() {
110     for (Descriptor d : getAvailableEffects()) {
111       if (d.type.equals(AudioEffect.EFFECT_TYPE_NS) && d.uuid.equals(AOSP_NOISE_SUPPRESSOR)) {
112         return true;
113       }
114     }
115     return false;
116   }
117 
118   // Returns true if the device supports Acoustic Echo Cancellation (AEC).
isAcousticEchoCancelerEffectAvailable()119   private static boolean isAcousticEchoCancelerEffectAvailable() {
120     return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_AEC);
121   }
122 
123   // Returns true if the device supports Noise Suppression (NS).
isNoiseSuppressorEffectAvailable()124   private static boolean isNoiseSuppressorEffectAvailable() {
125     return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_NS);
126   }
127 
128   // Returns true if all conditions for supporting the HW AEC are fulfilled.
129   // It will not be possible to enable the HW AEC if this method returns false.
canUseAcousticEchoCanceler()130   public static boolean canUseAcousticEchoCanceler() {
131     boolean canUseAcousticEchoCanceler = isAcousticEchoCancelerSupported()
132         && !WebRtcAudioUtils.useWebRtcBasedAcousticEchoCanceler()
133         && !isAcousticEchoCancelerBlacklisted() && !isAcousticEchoCancelerExcludedByUUID();
134     Logging.d(TAG, "canUseAcousticEchoCanceler: " + canUseAcousticEchoCanceler);
135     return canUseAcousticEchoCanceler;
136   }
137 
138   // Returns true if all conditions for supporting the HW NS are fulfilled.
139   // It will not be possible to enable the HW NS if this method returns false.
canUseNoiseSuppressor()140   public static boolean canUseNoiseSuppressor() {
141     boolean canUseNoiseSuppressor = isNoiseSuppressorSupported()
142         && !WebRtcAudioUtils.useWebRtcBasedNoiseSuppressor() && !isNoiseSuppressorBlacklisted()
143         && !isNoiseSuppressorExcludedByUUID();
144     Logging.d(TAG, "canUseNoiseSuppressor: " + canUseNoiseSuppressor);
145     return canUseNoiseSuppressor;
146   }
147 
create()148   public static WebRtcAudioEffects create() {
149     return new WebRtcAudioEffects();
150   }
151 
WebRtcAudioEffects()152   private WebRtcAudioEffects() {
153     Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo());
154   }
155 
156   // Call this method to enable or disable the platform AEC. It modifies
157   // `shouldEnableAec` which is used in enable() where the actual state
158   // of the AEC effect is modified. Returns true if HW AEC is supported and
159   // false otherwise.
setAEC(boolean enable)160   public boolean setAEC(boolean enable) {
161     Logging.d(TAG, "setAEC(" + enable + ")");
162     if (!canUseAcousticEchoCanceler()) {
163       Logging.w(TAG, "Platform AEC is not supported");
164       shouldEnableAec = false;
165       return false;
166     }
167     if (aec != null && (enable != shouldEnableAec)) {
168       Logging.e(TAG, "Platform AEC state can't be modified while recording");
169       return false;
170     }
171     shouldEnableAec = enable;
172     return true;
173   }
174 
175   // Call this method to enable or disable the platform NS. It modifies
176   // `shouldEnableNs` which is used in enable() where the actual state
177   // of the NS effect is modified. Returns true if HW NS is supported and
178   // false otherwise.
setNS(boolean enable)179   public boolean setNS(boolean enable) {
180     Logging.d(TAG, "setNS(" + enable + ")");
181     if (!canUseNoiseSuppressor()) {
182       Logging.w(TAG, "Platform NS is not supported");
183       shouldEnableNs = false;
184       return false;
185     }
186     if (ns != null && (enable != shouldEnableNs)) {
187       Logging.e(TAG, "Platform NS state can't be modified while recording");
188       return false;
189     }
190     shouldEnableNs = enable;
191     return true;
192   }
193 
enable(int audioSession)194   public void enable(int audioSession) {
195     Logging.d(TAG, "enable(audioSession=" + audioSession + ")");
196     assertTrue(aec == null);
197     assertTrue(ns == null);
198 
199     if (DEBUG) {
200       // Add logging of supported effects but filter out "VoIP effects", i.e.,
201       // AEC, AEC and NS. Avoid calling AudioEffect.queryEffects() unless the
202       // DEBUG flag is set since we have seen crashes in this API.
203       for (Descriptor d : AudioEffect.queryEffects()) {
204         if (effectTypeIsVoIP(d.type)) {
205           Logging.d(TAG, "name: " + d.name + ", "
206                   + "mode: " + d.connectMode + ", "
207                   + "implementor: " + d.implementor + ", "
208                   + "UUID: " + d.uuid);
209         }
210       }
211     }
212 
213     if (isAcousticEchoCancelerSupported()) {
214       // Create an AcousticEchoCanceler and attach it to the AudioRecord on
215       // the specified audio session.
216       aec = AcousticEchoCanceler.create(audioSession);
217       if (aec != null) {
218         boolean enabled = aec.getEnabled();
219         boolean enable = shouldEnableAec && canUseAcousticEchoCanceler();
220         if (aec.setEnabled(enable) != AudioEffect.SUCCESS) {
221           Logging.e(TAG, "Failed to set the AcousticEchoCanceler state");
222         }
223         Logging.d(TAG, "AcousticEchoCanceler: was " + (enabled ? "enabled" : "disabled")
224                 + ", enable: " + enable + ", is now: "
225                 + (aec.getEnabled() ? "enabled" : "disabled"));
226       } else {
227         Logging.e(TAG, "Failed to create the AcousticEchoCanceler instance");
228       }
229     }
230 
231     if (isNoiseSuppressorSupported()) {
232       // Create an NoiseSuppressor and attach it to the AudioRecord on the
233       // specified audio session.
234       ns = NoiseSuppressor.create(audioSession);
235       if (ns != null) {
236         boolean enabled = ns.getEnabled();
237         boolean enable = shouldEnableNs && canUseNoiseSuppressor();
238         if (ns.setEnabled(enable) != AudioEffect.SUCCESS) {
239           Logging.e(TAG, "Failed to set the NoiseSuppressor state");
240         }
241         Logging.d(TAG, "NoiseSuppressor: was " + (enabled ? "enabled" : "disabled") + ", enable: "
242                 + enable + ", is now: " + (ns.getEnabled() ? "enabled" : "disabled"));
243       } else {
244         Logging.e(TAG, "Failed to create the NoiseSuppressor instance");
245       }
246     }
247   }
248 
249   // Releases all native audio effect resources. It is a good practice to
250   // release the effect engine when not in use as control can be returned
251   // to other applications or the native resources released.
release()252   public void release() {
253     Logging.d(TAG, "release");
254     if (aec != null) {
255       aec.release();
256       aec = null;
257     }
258     if (ns != null) {
259       ns.release();
260       ns = null;
261     }
262   }
263 
264   // Returns true for effect types in `type` that are of "VoIP" types:
265   // Acoustic Echo Canceler (AEC) or Automatic Gain Control (AGC) or
266   // Noise Suppressor (NS). Note that, an extra check for support is needed
267   // in each comparison since some devices includes effects in the
268   // AudioEffect.Descriptor array that are actually not available on the device.
269   // As an example: Samsung Galaxy S6 includes an AGC in the descriptor but
270   // AutomaticGainControl.isAvailable() returns false.
effectTypeIsVoIP(UUID type)271   private boolean effectTypeIsVoIP(UUID type) {
272     return (AudioEffect.EFFECT_TYPE_AEC.equals(type) && isAcousticEchoCancelerSupported())
273         || (AudioEffect.EFFECT_TYPE_NS.equals(type) && isNoiseSuppressorSupported());
274   }
275 
276   // Helper method which throws an exception when an assertion has failed.
assertTrue(boolean condition)277   private static void assertTrue(boolean condition) {
278     if (!condition) {
279       throw new AssertionError("Expected condition to be true");
280     }
281   }
282 
283   // Returns the cached copy of the audio effects array, if available, or
284   // queries the operating system for the list of effects.
getAvailableEffects()285   private static @Nullable Descriptor[] getAvailableEffects() {
286     if (cachedEffects != null) {
287       return cachedEffects;
288     }
289     // The caching is best effort only - if this method is called from several
290     // threads in parallel, they may end up doing the underlying OS call
291     // multiple times. It's normally only called on one thread so there's no
292     // real need to optimize for the multiple threads case.
293     cachedEffects = AudioEffect.queryEffects();
294     return cachedEffects;
295   }
296 
297   // Returns true if an effect of the specified type is available. Functionally
298   // equivalent to (NoiseSuppressor`AutomaticGainControl`...).isAvailable(), but
299   // faster as it avoids the expensive OS call to enumerate effects.
isEffectTypeAvailable(UUID effectType)300   private static boolean isEffectTypeAvailable(UUID effectType) {
301     Descriptor[] effects = getAvailableEffects();
302     if (effects == null) {
303       return false;
304     }
305     for (Descriptor d : effects) {
306       if (d.type.equals(effectType)) {
307         return true;
308       }
309     }
310     return false;
311   }
312 }
313