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 android.support.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     if (Build.VERSION.SDK_INT < 18)
99       return false;
100     for (Descriptor d : getAvailableEffects()) {
101       if (d.type.equals(AudioEffect.EFFECT_TYPE_AEC)
102           && d.uuid.equals(AOSP_ACOUSTIC_ECHO_CANCELER)) {
103         return true;
104       }
105     }
106     return false;
107   }
108 
109   // Returns true if the platform NS should be excluded based on its UUID.
110   // AudioEffect.queryEffects() can throw IllegalStateException.
isNoiseSuppressorExcludedByUUID()111   private static boolean isNoiseSuppressorExcludedByUUID() {
112     if (Build.VERSION.SDK_INT < 18)
113       return false;
114     for (Descriptor d : getAvailableEffects()) {
115       if (d.type.equals(AudioEffect.EFFECT_TYPE_NS) && d.uuid.equals(AOSP_NOISE_SUPPRESSOR)) {
116         return true;
117       }
118     }
119     return false;
120   }
121 
122   // Returns true if the device supports Acoustic Echo Cancellation (AEC).
isAcousticEchoCancelerEffectAvailable()123   private static boolean isAcousticEchoCancelerEffectAvailable() {
124     if (Build.VERSION.SDK_INT < 18)
125       return false;
126     return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_AEC);
127   }
128 
129   // Returns true if the device supports Noise Suppression (NS).
isNoiseSuppressorEffectAvailable()130   private static boolean isNoiseSuppressorEffectAvailable() {
131     if (Build.VERSION.SDK_INT < 18)
132       return false;
133     return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_NS);
134   }
135 
136   // Returns true if all conditions for supporting the HW AEC are fulfilled.
137   // It will not be possible to enable the HW AEC if this method returns false.
canUseAcousticEchoCanceler()138   public static boolean canUseAcousticEchoCanceler() {
139     boolean canUseAcousticEchoCanceler = isAcousticEchoCancelerSupported()
140         && !WebRtcAudioUtils.useWebRtcBasedAcousticEchoCanceler()
141         && !isAcousticEchoCancelerBlacklisted() && !isAcousticEchoCancelerExcludedByUUID();
142     Logging.d(TAG, "canUseAcousticEchoCanceler: " + canUseAcousticEchoCanceler);
143     return canUseAcousticEchoCanceler;
144   }
145 
146   // Returns true if all conditions for supporting the HW NS are fulfilled.
147   // It will not be possible to enable the HW NS if this method returns false.
canUseNoiseSuppressor()148   public static boolean canUseNoiseSuppressor() {
149     boolean canUseNoiseSuppressor = isNoiseSuppressorSupported()
150         && !WebRtcAudioUtils.useWebRtcBasedNoiseSuppressor() && !isNoiseSuppressorBlacklisted()
151         && !isNoiseSuppressorExcludedByUUID();
152     Logging.d(TAG, "canUseNoiseSuppressor: " + canUseNoiseSuppressor);
153     return canUseNoiseSuppressor;
154   }
155 
create()156   public static WebRtcAudioEffects create() {
157     return new WebRtcAudioEffects();
158   }
159 
WebRtcAudioEffects()160   private WebRtcAudioEffects() {
161     Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo());
162   }
163 
164   // Call this method to enable or disable the platform AEC. It modifies
165   // |shouldEnableAec| which is used in enable() where the actual state
166   // of the AEC effect is modified. Returns true if HW AEC is supported and
167   // false otherwise.
setAEC(boolean enable)168   public boolean setAEC(boolean enable) {
169     Logging.d(TAG, "setAEC(" + enable + ")");
170     if (!canUseAcousticEchoCanceler()) {
171       Logging.w(TAG, "Platform AEC is not supported");
172       shouldEnableAec = false;
173       return false;
174     }
175     if (aec != null && (enable != shouldEnableAec)) {
176       Logging.e(TAG, "Platform AEC state can't be modified while recording");
177       return false;
178     }
179     shouldEnableAec = enable;
180     return true;
181   }
182 
183   // Call this method to enable or disable the platform NS. It modifies
184   // |shouldEnableNs| which is used in enable() where the actual state
185   // of the NS effect is modified. Returns true if HW NS is supported and
186   // false otherwise.
setNS(boolean enable)187   public boolean setNS(boolean enable) {
188     Logging.d(TAG, "setNS(" + enable + ")");
189     if (!canUseNoiseSuppressor()) {
190       Logging.w(TAG, "Platform NS is not supported");
191       shouldEnableNs = false;
192       return false;
193     }
194     if (ns != null && (enable != shouldEnableNs)) {
195       Logging.e(TAG, "Platform NS state can't be modified while recording");
196       return false;
197     }
198     shouldEnableNs = enable;
199     return true;
200   }
201 
enable(int audioSession)202   public void enable(int audioSession) {
203     Logging.d(TAG, "enable(audioSession=" + audioSession + ")");
204     assertTrue(aec == null);
205     assertTrue(ns == null);
206 
207     if (DEBUG) {
208       // Add logging of supported effects but filter out "VoIP effects", i.e.,
209       // AEC, AEC and NS. Avoid calling AudioEffect.queryEffects() unless the
210       // DEBUG flag is set since we have seen crashes in this API.
211       for (Descriptor d : AudioEffect.queryEffects()) {
212         if (effectTypeIsVoIP(d.type)) {
213           Logging.d(TAG, "name: " + d.name + ", "
214                   + "mode: " + d.connectMode + ", "
215                   + "implementor: " + d.implementor + ", "
216                   + "UUID: " + d.uuid);
217         }
218       }
219     }
220 
221     if (isAcousticEchoCancelerSupported()) {
222       // Create an AcousticEchoCanceler and attach it to the AudioRecord on
223       // the specified audio session.
224       aec = AcousticEchoCanceler.create(audioSession);
225       if (aec != null) {
226         boolean enabled = aec.getEnabled();
227         boolean enable = shouldEnableAec && canUseAcousticEchoCanceler();
228         if (aec.setEnabled(enable) != AudioEffect.SUCCESS) {
229           Logging.e(TAG, "Failed to set the AcousticEchoCanceler state");
230         }
231         Logging.d(TAG, "AcousticEchoCanceler: was " + (enabled ? "enabled" : "disabled")
232                 + ", enable: " + enable + ", is now: "
233                 + (aec.getEnabled() ? "enabled" : "disabled"));
234       } else {
235         Logging.e(TAG, "Failed to create the AcousticEchoCanceler instance");
236       }
237     }
238 
239     if (isNoiseSuppressorSupported()) {
240       // Create an NoiseSuppressor and attach it to the AudioRecord on the
241       // specified audio session.
242       ns = NoiseSuppressor.create(audioSession);
243       if (ns != null) {
244         boolean enabled = ns.getEnabled();
245         boolean enable = shouldEnableNs && canUseNoiseSuppressor();
246         if (ns.setEnabled(enable) != AudioEffect.SUCCESS) {
247           Logging.e(TAG, "Failed to set the NoiseSuppressor state");
248         }
249         Logging.d(TAG, "NoiseSuppressor: was " + (enabled ? "enabled" : "disabled") + ", enable: "
250                 + enable + ", is now: " + (ns.getEnabled() ? "enabled" : "disabled"));
251       } else {
252         Logging.e(TAG, "Failed to create the NoiseSuppressor instance");
253       }
254     }
255   }
256 
257   // Releases all native audio effect resources. It is a good practice to
258   // release the effect engine when not in use as control can be returned
259   // to other applications or the native resources released.
release()260   public void release() {
261     Logging.d(TAG, "release");
262     if (aec != null) {
263       aec.release();
264       aec = null;
265     }
266     if (ns != null) {
267       ns.release();
268       ns = null;
269     }
270   }
271 
272   // Returns true for effect types in |type| that are of "VoIP" types:
273   // Acoustic Echo Canceler (AEC) or Automatic Gain Control (AGC) or
274   // Noise Suppressor (NS). Note that, an extra check for support is needed
275   // in each comparison since some devices includes effects in the
276   // AudioEffect.Descriptor array that are actually not available on the device.
277   // As an example: Samsung Galaxy S6 includes an AGC in the descriptor but
278   // AutomaticGainControl.isAvailable() returns false.
effectTypeIsVoIP(UUID type)279   private boolean effectTypeIsVoIP(UUID type) {
280     if (Build.VERSION.SDK_INT < 18)
281       return false;
282 
283     return (AudioEffect.EFFECT_TYPE_AEC.equals(type) && isAcousticEchoCancelerSupported())
284         || (AudioEffect.EFFECT_TYPE_NS.equals(type) && isNoiseSuppressorSupported());
285   }
286 
287   // Helper method which throws an exception when an assertion has failed.
assertTrue(boolean condition)288   private static void assertTrue(boolean condition) {
289     if (!condition) {
290       throw new AssertionError("Expected condition to be true");
291     }
292   }
293 
294   // Returns the cached copy of the audio effects array, if available, or
295   // queries the operating system for the list of effects.
getAvailableEffects()296   private static @Nullable Descriptor[] getAvailableEffects() {
297     if (cachedEffects != null) {
298       return cachedEffects;
299     }
300     // The caching is best effort only - if this method is called from several
301     // threads in parallel, they may end up doing the underlying OS call
302     // multiple times. It's normally only called on one thread so there's no
303     // real need to optimize for the multiple threads case.
304     cachedEffects = AudioEffect.queryEffects();
305     return cachedEffects;
306   }
307 
308   // Returns true if an effect of the specified type is available. Functionally
309   // equivalent to (NoiseSuppressor|AutomaticGainControl|...).isAvailable(), but
310   // faster as it avoids the expensive OS call to enumerate effects.
isEffectTypeAvailable(UUID effectType)311   private static boolean isEffectTypeAvailable(UUID effectType) {
312     Descriptor[] effects = getAvailableEffects();
313     if (effects == null) {
314       return false;
315     }
316     for (Descriptor d : effects) {
317       if (d.type.equals(effectType)) {
318         return true;
319       }
320     }
321     return false;
322   }
323 }
324