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.media.audiofx.AcousticEchoCanceler;
15 import android.media.audiofx.AudioEffect;
16 import android.media.audiofx.AudioEffect.Descriptor;
17 import android.media.audiofx.AutomaticGainControl;
18 import android.media.audiofx.NoiseSuppressor;
19 import android.os.Build;
20 
21 import org.webrtc.Logging;
22 
23 import java.util.List;
24 
25 import java.util.UUID;
26 
27 // This class wraps control of three different platform effects. Supported
28 // effects are: AcousticEchoCanceler (AEC), AutomaticGainControl (AGC) and
29 // NoiseSuppressor (NS). Calling enable() will active all effects that are
30 // supported by the device if the corresponding |shouldEnableXXX| member is set.
31 class WebRtcAudioEffects {
32   private static final boolean DEBUG = false;
33 
34   private static final String TAG = "WebRtcAudioEffects";
35 
36   // UUIDs for Software Audio Effects that we want to avoid using.
37   // The implementor field will be set to "The Android Open Source Project".
38   private static final UUID AOSP_ACOUSTIC_ECHO_CANCELER =
39       UUID.fromString("bb392ec0-8d4d-11e0-a896-0002a5d5c51b");
40   private static final UUID AOSP_AUTOMATIC_GAIN_CONTROL =
41       UUID.fromString("aa8130e0-66fc-11e0-bad0-0002a5d5c51b");
42   private static final UUID AOSP_NOISE_SUPPRESSOR =
43       UUID.fromString("c06c8400-8e06-11e0-9cb6-0002a5d5c51b");
44 
45   // Static Boolean objects used to avoid expensive queries more than once.
46   // The first result is cached in these members and then reused if needed.
47   // Each member is null until it has been evaluated/set for the first time.
48   private static Boolean canUseAcousticEchoCanceler = null;
49   private static Boolean canUseAutomaticGainControl = null;
50   private static Boolean canUseNoiseSuppressor = null;
51 
52   // Contains the audio effect objects. Created in enable() and destroyed
53   // in release().
54   private AcousticEchoCanceler aec = null;
55   private AutomaticGainControl agc = null;
56   private NoiseSuppressor ns = null;
57 
58   // Affects the final state given to the setEnabled() method on each effect.
59   // The default state is set to "disabled" but each effect can also be enabled
60   // by calling setAEC(), setAGC() and setNS().
61   // To enable an effect, both the shouldEnableXXX member and the static
62   // canUseXXX() must be true.
63   private boolean shouldEnableAec = false;
64   private boolean shouldEnableAgc = false;
65   private boolean shouldEnableNs = false;
66 
67   // Checks if the device implements Acoustic Echo Cancellation (AEC).
68   // Returns true if the device implements AEC, false otherwise.
isAcousticEchoCancelerSupported()69   public static boolean isAcousticEchoCancelerSupported() {
70     return WebRtcAudioUtils.runningOnJellyBeanOrHigher()
71         && AcousticEchoCanceler.isAvailable();
72   }
73 
74   // Checks if the device implements Automatic Gain Control (AGC).
75   // Returns true if the device implements AGC, false otherwise.
isAutomaticGainControlSupported()76   public static boolean isAutomaticGainControlSupported() {
77     return WebRtcAudioUtils.runningOnJellyBeanOrHigher()
78         && AutomaticGainControl.isAvailable();
79   }
80 
81   // Checks if the device implements Noise Suppression (NS).
82   // Returns true if the device implements NS, false otherwise.
isNoiseSuppressorSupported()83   public static boolean isNoiseSuppressorSupported() {
84     return WebRtcAudioUtils.runningOnJellyBeanOrHigher()
85         && NoiseSuppressor.isAvailable();
86   }
87 
88   // Returns true if the device is blacklisted for HW AEC usage.
isAcousticEchoCancelerBlacklisted()89   public static boolean isAcousticEchoCancelerBlacklisted() {
90     List<String> blackListedModels =
91         WebRtcAudioUtils.getBlackListedModelsForAecUsage();
92     boolean isBlacklisted = blackListedModels.contains(Build.MODEL);
93     if (isBlacklisted) {
94       Logging.w(TAG, Build.MODEL + " is blacklisted for HW AEC usage!");
95     }
96     return isBlacklisted;
97   }
98 
99   // Returns true if the device is blacklisted for HW AGC usage.
isAutomaticGainControlBlacklisted()100   public static boolean isAutomaticGainControlBlacklisted() {
101    List<String> blackListedModels =
102         WebRtcAudioUtils.getBlackListedModelsForAgcUsage();
103     boolean isBlacklisted = blackListedModels.contains(Build.MODEL);
104     if (isBlacklisted) {
105       Logging.w(TAG, Build.MODEL + " is blacklisted for HW AGC usage!");
106     }
107     return isBlacklisted;
108   }
109 
110   // Returns true if the device is blacklisted for HW NS usage.
isNoiseSuppressorBlacklisted()111   public static boolean isNoiseSuppressorBlacklisted() {
112     List<String> blackListedModels =
113         WebRtcAudioUtils.getBlackListedModelsForNsUsage();
114     boolean isBlacklisted = blackListedModels.contains(Build.MODEL);
115     if (isBlacklisted) {
116       Logging.w(TAG, Build.MODEL + " is blacklisted for HW NS usage!");
117     }
118     return isBlacklisted;
119   }
120 
121   // Returns true if the platform AEC should be excluded based on its UUID.
122   // AudioEffect.queryEffects() can throw IllegalStateException.
123   @TargetApi(18)
isAcousticEchoCancelerExcludedByUUID()124   private static boolean isAcousticEchoCancelerExcludedByUUID() {
125     for (Descriptor d : AudioEffect.queryEffects()) {
126       if (d.type.equals(AudioEffect.EFFECT_TYPE_AEC) &&
127           d.uuid.equals(AOSP_ACOUSTIC_ECHO_CANCELER)) {
128         return true;
129       }
130     }
131     return false;
132   }
133 
134   // Returns true if the platform AGC should be excluded based on its UUID.
135   // AudioEffect.queryEffects() can throw IllegalStateException.
136   @TargetApi(18)
isAutomaticGainControlExcludedByUUID()137   private static boolean isAutomaticGainControlExcludedByUUID() {
138     for (Descriptor d : AudioEffect.queryEffects()) {
139       if (d.type.equals(AudioEffect.EFFECT_TYPE_AGC) &&
140           d.uuid.equals(AOSP_AUTOMATIC_GAIN_CONTROL)) {
141         return true;
142       }
143     }
144     return false;
145   }
146 
147   // Returns true if the platform NS should be excluded based on its UUID.
148   // AudioEffect.queryEffects() can throw IllegalStateException.
149   @TargetApi(18)
isNoiseSuppressorExcludedByUUID()150   private static boolean isNoiseSuppressorExcludedByUUID() {
151     for (Descriptor d : AudioEffect.queryEffects()) {
152       if (d.type.equals(AudioEffect.EFFECT_TYPE_NS) &&
153           d.uuid.equals(AOSP_NOISE_SUPPRESSOR)) {
154         return true;
155       }
156     }
157     return false;
158   }
159 
160   // Returns true if all conditions for supporting the HW AEC are fulfilled.
161   // It will not be possible to enable the HW AEC if this method returns false.
canUseAcousticEchoCanceler()162   public static boolean canUseAcousticEchoCanceler() {
163     if (canUseAcousticEchoCanceler == null) {
164       canUseAcousticEchoCanceler = new Boolean(
165           isAcousticEchoCancelerSupported()
166           && !WebRtcAudioUtils.useWebRtcBasedAcousticEchoCanceler()
167           && !isAcousticEchoCancelerBlacklisted()
168           && !isAcousticEchoCancelerExcludedByUUID());
169       Logging.d(TAG, "canUseAcousticEchoCanceler: "
170           + canUseAcousticEchoCanceler);
171     }
172     return canUseAcousticEchoCanceler;
173   }
174 
175   // Returns true if all conditions for supporting the HW AGC are fulfilled.
176   // It will not be possible to enable the HW AGC if this method returns false.
canUseAutomaticGainControl()177   public static boolean canUseAutomaticGainControl() {
178     if (canUseAutomaticGainControl == null) {
179       canUseAutomaticGainControl = new Boolean(
180           isAutomaticGainControlSupported()
181           && !WebRtcAudioUtils.useWebRtcBasedAutomaticGainControl()
182           && !isAutomaticGainControlBlacklisted()
183           && !isAutomaticGainControlExcludedByUUID());
184       Logging.d(TAG, "canUseAutomaticGainControl: "
185           + canUseAutomaticGainControl);
186     }
187     return canUseAutomaticGainControl;
188   }
189 
190   // Returns true if all conditions for supporting the HW NS are fulfilled.
191   // It will not be possible to enable the HW NS if this method returns false.
canUseNoiseSuppressor()192   public static boolean canUseNoiseSuppressor() {
193     if (canUseNoiseSuppressor == null) {
194       canUseNoiseSuppressor = new Boolean(
195           isNoiseSuppressorSupported()
196           && !WebRtcAudioUtils.useWebRtcBasedNoiseSuppressor()
197           && !isNoiseSuppressorBlacklisted()
198           && !isNoiseSuppressorExcludedByUUID());
199       Logging.d(TAG, "canUseNoiseSuppressor: " + canUseNoiseSuppressor);
200     }
201     return canUseNoiseSuppressor;
202   }
203 
create()204   static WebRtcAudioEffects create() {
205     // Return null if VoIP effects (AEC, AGC and NS) are not supported.
206     if (!WebRtcAudioUtils.runningOnJellyBeanOrHigher()) {
207       Logging.w(TAG, "API level 16 or higher is required!");
208       return null;
209     }
210     return new WebRtcAudioEffects();
211   }
212 
WebRtcAudioEffects()213   private WebRtcAudioEffects() {
214     Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo());
215   }
216 
217   // Call this method to enable or disable the platform AEC. It modifies
218   // |shouldEnableAec| which is used in enable() where the actual state
219   // of the AEC effect is modified. Returns true if HW AEC is supported and
220   // false otherwise.
setAEC(boolean enable)221   public boolean setAEC(boolean enable) {
222     Logging.d(TAG, "setAEC(" + enable + ")");
223     if (!canUseAcousticEchoCanceler()) {
224       Logging.w(TAG, "Platform AEC is not supported");
225       shouldEnableAec = false;
226       return false;
227     }
228     if (aec != null && (enable != shouldEnableAec)) {
229       Logging.e(TAG, "Platform AEC state can't be modified while recording");
230       return false;
231     }
232     shouldEnableAec = enable;
233     return true;
234   }
235 
236   // Call this method to enable or disable the platform AGC. It modifies
237   // |shouldEnableAgc| which is used in enable() where the actual state
238   // of the AGC effect is modified. Returns true if HW AGC is supported and
239   // false otherwise.
setAGC(boolean enable)240   public boolean setAGC(boolean enable) {
241     Logging.d(TAG, "setAGC(" + enable + ")");
242     if (!canUseAutomaticGainControl()) {
243       Logging.w(TAG, "Platform AGC is not supported");
244       shouldEnableAgc = false;
245       return false;
246     }
247     if (agc != null && (enable != shouldEnableAgc)) {
248       Logging.e(TAG, "Platform AGC state can't be modified while recording");
249       return false;
250     }
251     shouldEnableAgc = enable;
252     return true;
253   }
254 
255   // Call this method to enable or disable the platform NS. It modifies
256   // |shouldEnableNs| which is used in enable() where the actual state
257   // of the NS effect is modified. Returns true if HW NS is supported and
258   // false otherwise.
setNS(boolean enable)259   public boolean setNS(boolean enable) {
260     Logging.d(TAG, "setNS(" + enable + ")");
261     if (!canUseNoiseSuppressor()) {
262       Logging.w(TAG, "Platform NS is not supported");
263       shouldEnableNs = false;
264       return false;
265     }
266     if (ns != null && (enable != shouldEnableNs)) {
267       Logging.e(TAG, "Platform NS state can't be modified while recording");
268       return false;
269     }
270     shouldEnableNs = enable;
271     return true;
272   }
273 
enable(int audioSession)274   public void enable(int audioSession) {
275     Logging.d(TAG, "enable(audioSession=" + audioSession + ")");
276     assertTrue(aec == null);
277     assertTrue(agc == null);
278     assertTrue(ns == null);
279 
280     // Add logging of supported effects but filter out "VoIP effects", i.e.,
281     // AEC, AEC and NS.
282     for (Descriptor d : AudioEffect.queryEffects()) {
283       if (effectTypeIsVoIP(d.type) || DEBUG) {
284         Logging.d(TAG, "name: " + d.name + ", "
285             + "mode: " + d.connectMode + ", "
286             + "implementor: " + d.implementor + ", "
287             + "UUID: " + d.uuid);
288       }
289     }
290 
291     if (isAcousticEchoCancelerSupported()) {
292       // Create an AcousticEchoCanceler and attach it to the AudioRecord on
293       // the specified audio session.
294       aec = AcousticEchoCanceler.create(audioSession);
295       if (aec != null) {
296         boolean enabled = aec.getEnabled();
297         boolean enable = shouldEnableAec && canUseAcousticEchoCanceler();
298         if (aec.setEnabled(enable) != AudioEffect.SUCCESS) {
299           Logging.e(TAG, "Failed to set the AcousticEchoCanceler state");
300         }
301         Logging.d(TAG, "AcousticEchoCanceler: was "
302             + (enabled ? "enabled" : "disabled")
303             + ", enable: " + enable + ", is now: "
304             + (aec.getEnabled() ? "enabled" : "disabled"));
305       } else {
306         Logging.e(TAG, "Failed to create the AcousticEchoCanceler instance");
307       }
308     }
309 
310     if (isAutomaticGainControlSupported()) {
311       // Create an AutomaticGainControl and attach it to the AudioRecord on
312       // the specified audio session.
313       agc = AutomaticGainControl.create(audioSession);
314       if (agc != null) {
315         boolean enabled = agc.getEnabled();
316         boolean enable = shouldEnableAgc && canUseAutomaticGainControl();
317         if (agc.setEnabled(enable) != AudioEffect.SUCCESS) {
318           Logging.e(TAG, "Failed to set the AutomaticGainControl state");
319         }
320         Logging.d(TAG, "AutomaticGainControl: was "
321             + (enabled ? "enabled" : "disabled")
322             + ", enable: " + enable + ", is now: "
323             + (agc.getEnabled() ? "enabled" : "disabled"));
324       } else {
325         Logging.e(TAG, "Failed to create the AutomaticGainControl instance");
326       }
327     }
328 
329     if (isNoiseSuppressorSupported()) {
330       // Create an NoiseSuppressor and attach it to the AudioRecord on the
331       // specified audio session.
332       ns = NoiseSuppressor.create(audioSession);
333       if (ns != null) {
334         boolean enabled = ns.getEnabled();
335         boolean enable = shouldEnableNs && canUseNoiseSuppressor();
336         if (ns.setEnabled(enable) != AudioEffect.SUCCESS) {
337           Logging.e(TAG, "Failed to set the NoiseSuppressor state");
338         }
339         Logging.d(TAG, "NoiseSuppressor: was "
340             + (enabled ? "enabled" : "disabled")
341             + ", enable: " + enable + ", is now: "
342             + (ns.getEnabled() ? "enabled" : "disabled"));
343       } else {
344         Logging.e(TAG, "Failed to create the NoiseSuppressor instance");
345       }
346     }
347   }
348 
349   // Releases all native audio effect resources. It is a good practice to
350   // release the effect engine when not in use as control can be returned
351   // to other applications or the native resources released.
release()352   public void release() {
353     Logging.d(TAG, "release");
354     if (aec != null) {
355       aec.release();
356       aec = null;
357     }
358     if (agc != null) {
359       agc.release();
360       agc = null;
361     }
362     if (ns != null) {
363       ns.release();
364       ns = null;
365     }
366   }
367 
368   // Returns true for effect types in |type| that are of "VoIP" types:
369   // Acoustic Echo Canceler (AEC) or Automatic Gain Control (AGC) or
370   // Noise Suppressor (NS). Note that, an extra check for support is needed
371   // in each comparison since some devices includes effects in the
372   // AudioEffect.Descriptor array that are actually not available on the device.
373   // As an example: Samsung Galaxy S6 includes an AGC in the descriptor but
374   // AutomaticGainControl.isAvailable() returns false.
375   @TargetApi(18)
effectTypeIsVoIP(UUID type)376   private boolean effectTypeIsVoIP(UUID type) {
377     if (!WebRtcAudioUtils.runningOnJellyBeanMR2OrHigher())
378       return false;
379 
380     return (AudioEffect.EFFECT_TYPE_AEC.equals(type)
381         && isAcousticEchoCancelerSupported())
382         || (AudioEffect.EFFECT_TYPE_AGC.equals(type)
383         && isAutomaticGainControlSupported())
384         || (AudioEffect.EFFECT_TYPE_NS.equals(type)
385         && isNoiseSuppressorSupported());
386   }
387 
388   // Helper method which throws an exception when an assertion has failed.
assertTrue(boolean condition)389   private static void assertTrue(boolean condition) {
390     if (!condition) {
391       throw new AssertionError("Expected condition to be true");
392     }
393   }
394 }
395