音视频基础能力之 Android 音频篇 (四):音频路由

1,123 阅读11分钟

涉及硬件的音视频能力,比如采集、渲染、硬件编码、硬件解码,通常是与客户端操作系统强相关的,就算是跨平台的多媒体框架也必须使用平台原生语言的模块来支持这些功能。

本系列文章将详细讲述移动端音视频的采集、渲染、硬件编码、硬件解码这些涉及硬件的能力该如何实现。本文为该系列文章的第 4 篇,将详细讲述在 Android 平台下影响音频路由相关的知识点。

一、前言

咋回事,怎么听不到对方的声音了?

我这明明播放了音乐啊,怎么什么声音都没有?

相信做过音视频业务的同学都遇到过类似的问题,当然出现此类问题的原因比较多,例如:音频设备故障,网络、音频路由等,其他的我们先暂时搁置一旁,今天着重讲讲音频路由相关的知识点。

音频路由所产生的音频采集、播放异常(故障) 对业务产生的影响持续时间比较差,且难于排查。主要原因是开发者不仅需要对 Android 平台音频路由相关的知识点非常熟悉,还需要很多一些特殊机型、特殊场景下的经验值。本文会详细介绍下影响 Android 音频路由相关的应用层知识点。

二、影响音频路由的参数

在 Android 平台中有三个非常重要的参数,audioSource、streamType、audioMode 这三种参数分别表示 采集音频源、播放音频流类型、音频场景,他们很大程度上决定着系统路由(指的是输入设备和输出设备)的选择和优先级。除了在 commuication 的模式下,可以允许开发者控制扬声器和听筒的切换,大部分场景音频路由都是由系统根据这三个参数来决定的。

2.1 audioSource

PS: 如果您阅读过之前有关音频采集的文章,应该对这个参数很熟悉。

表示采集源类型,通常会影响到系统选择采集设备的优先级,常见的类型值:

  • Default(0): 默认的 audio source,Android 系统来决定,一般取值为 MIC(1)。
  • MIC(1): 麦克风采集,适用于对音质效果比较好的场景。
  • VOICE_UPLINK(2) VOICE_DOWN_LINK(3) VOICE_CALL(4) 系统应用才可用,需要申请 CAPTURE_AUDIO_OUTPUT 权限,用于实现通话录音功能。分别录制上行、下行、上下行。
  • CAMCORDER(5) 采集的音频会跟随着视频的方向来调整音频的空间方向;还会根据视频的画面做一些同步和匹配。以提供更具沉浸感和真实感的视听体验,如拍摄 vlog、微电影、纪录片等。
  • VOICE_RECOGNITION(6) 语音识别相关的应用场景,会对采集的音频做一些优化处理,通常过滤掉环境的底噪。
  • VOICE_COMMUNICATION(7) 语音通信场景像VoIP,采集的音频源会经过回声消除、自动增益等算法,来提升语音通信的质量。

从上面的描述来看,audioSource 不仅会影响优先级,还会根据音频源的用途做一些额外的音频处理,但这些通常和路由没什么关系,普通开发者这里只需要知道就行。

以下是 audiosource 为 VOICE_COMMUNICATION 的情况下,Android 音频路由选择的代码逻辑。

case AUDIO_SOURCE_VOICE_COMMUNICATION:
    
    //如果是 in_call 状态,指定可用设备列表 (primary)。
    if ((getPhoneState() == AUDIO_MODE_IN_CALL) &&
            (availableOutputDevices.getDevice(AUDIO_DEVICE_OUT_TELEPHONY_TX,
                    String8(""), AUDIO_FORMAT_DEFAULT)) == nullptr) {
        LOG_ALWAYS_FATAL_IF(availablePrimaryDevices.isEmpty(), "Primary devices not found");
        availableDevices = availablePrimaryDevices;
    }

		//优先选择带有语音通信的蓝牙
    if (audio_is_bluetooth_out_sco_device(commDeviceType)) {
        // if SCO device is requested but no SCO device is available, fall back to default case
        device = availableDevices.getDevice(
                AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET, String8(""), AUDIO_FORMAT_DEFAULT);
        if (device != nullptr) {
            break;
        }
    }
    
    switch (commDeviceType) {
    //支持低功耗协议的蓝牙设备
    case AUDIO_DEVICE_OUT_BLE_HEADSET:
        device = availableDevices.getDevice(
                AUDIO_DEVICE_IN_BLE_HEADSET, String8(""), AUDIO_FORMAT_DEFAULT);
        break;
    //外放(喇叭)
    case AUDIO_DEVICE_OUT_SPEAKER:
        device = availableDevices.getFirstExistingDevice({
                AUDIO_DEVICE_IN_BACK_MIC, AUDIO_DEVICE_IN_BUILTIN_MIC,
                AUDIO_DEVICE_IN_USB_DEVICE, AUDIO_DEVICE_IN_USB_HEADSET});
        break;
    default:    // FORCE_NONE
        //如果是其他的 audiosource,则按照以下顺序来选择
        device = availableDevices.getFirstExistingDevice({
                AUDIO_DEVICE_IN_WIRED_HEADSET, AUDIO_DEVICE_IN_USB_HEADSET,
                AUDIO_DEVICE_IN_USB_DEVICE, AUDIO_DEVICE_IN_BLUETOOTH_BLE,
                AUDIO_DEVICE_IN_BUILTIN_MIC});
        break;

    }
    break;

2.2 streamType

streamType 用于对播放的音频流进行分类,主要包括以下几种常见类型:

  • STREAM_VOICE_CALL(0) 用于语音通话。
  • STREAM_SYSTEM(1) 用于系统声音,如按键声、通知音效等。
  • STREAM_RING(2) 铃声、短信通知、提醒事项。
  • STREAM_MUSIC(3) 用于音乐和其他媒体播放。
  • STREAM_ALARM(4) 闹钟
  • STREAM_NOTIFICATION(5) 应用的通知音量,如微信消息提醒。

streamType 的作用就是标识每条播放音频流的类型,用途如下:

  1. 对这些流进行单独的处理,例如对 STREAM_VOICE_CALL 进行音频处理,开启回声消除和自动增益,以保证通话质量。

  2. 让用户单独控制每种类型音频流的音量值大小。

image.png

  1. 音频播放场景毕竟和音频采集有所不同,比如正在开会,你把手机设置为静音模式,这时候还有来电的声音是不是有些不妥?所以还有更重要的作用,就是根据系统当前的状态来控制每种类型的音频流是否参与混音以及播放。

以下是通话模式下,Android 音频输出设备选择的代码逻辑。

  //系统底层会对 streamType 再一次的分类,
  //STRATEGY_PHONE 是 STREAM_VOICE_CALL 和 STREAM_BLUETOOTH_SCO 的集合
  //这里在不赘述
  case STRATEGY_PHONE: {
      //为了用户体验,会优先选择最后使用过的可插拔音频输出设备。
      devices = availableOutputDevices.getFirstDevicesFromTypes(
                      getLastRemovableMediaDevices(GROUP_NONE, {
                          //要排除这两种设备,因为 Dialer 系统应用会显示的使用它们
                          AUDIO_DEVICE_OUT_HEARING_AID,
                          AUDIO_DEVICE_OUT_BLE_HEADSET
                          }));
      if (!devices.isEmpty()) break;
      
      //如果最近没有使用过可插拔的音频输出设备,按照以下顺序来选择输出设备。
      devices = availableOutputDevices.getFirstDevicesFromTypes({
              AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET, AUDIO_DEVICE_OUT_EARPIECE,
              AUDIO_DEVICE_OUT_SPEAKER});
  } break;

2.3 audioMode

  • MODE_NORMAL(0) 正常模式,也是系统默认的音频模式。
  • MODE_RINGTONE(1) 铃声模式
  • MODE_IN_CALL(2) 来电模式
  • MODE_IN_COMMUNICATION(3) 通话模式

刚才前文提了,audioMode 会和音频路由相关。但更为准确的说法是,audioMode 是一种全局的设置,不仅仅会影响到音频路由。接下来,我们举个例子来说明这个参数实际发挥的作用。

比如,你正带着蓝牙耳机接收 phone A 的音乐,这时候你用 phone B 拨打 phone A 的电话。这时候系统会切入到 MODE_RINGTONE 模式 (大概率应该是系统应用电话设置的),这时候的变化是,播放的音乐突然暂停了,然后来电的响铃声从耳机和扬声器同时播放出来了。这时候你按物理按键只能调节铃声的音量了 (对应着 STREAM_RING 这种类型的 streamType)。

从这个例子的现象,我们可以从中 Get 到:

  • 音频模式,会让手机聚焦某种场景的作用。比如,进入了响铃模式,系统会让所有适用于 MODE_RINGTONE 的输出设备播放来电的音频流,而不仅仅从蓝牙设备输出响铃的音频流。
  • 控制当前所有播放的音频流,只让 streamType 为 STREAM_RING 的音频流播放出来。
  • 物理音量键在 MODE_RINGTONE 模式下,只能调节 STREAM_RING streamType 类型的音量。

三、音频焦点介绍

当两个或更多个应用向同一输出流播放音频,系统会将所有的音频流混合在一起播放。这会为用户带来一定的烦恼,因为有时用户并不希望如此。所以,Android 引入了 “音频焦点” 的概念,它是官方为应用设计的一个协商机制。同一个时刻,只有一个应用才可以获取音频焦点,获取音频焦点之后你才可以播放音频流。

在 Android 12 之前,这个协商机制并不是强制的。虽然比较注重体验的应用都会遵守这个机制,但是还是会有很多应用不会遵守,这还多少还是会影响到用户体验,所以在 Android 12 之后,系统收回了这部分的权利,由系统来接管。

下面介绍下不同版本关于音频焦点的适配代码逻辑。

3.1 Android 7.1 及以下版本用法

step1) 调用 requestAudioFocus 请求音频焦点,聊下三个参数的具体用法。

  1. AudioManager.OnAudioFocusChangeListener 音频焦点变化回调接口
  2. streamType 播放的音频流类型,见上一章节。
  3. durationHint
    • AUDIOFOCUS_GAIN 表示你要播放一段音频,你希望之前的持有者停止播放音频。
    • AUDIOFOCUS_GAIN_TRANSIENT 请求瞬时焦点,并且希望之前的持有者暂停播放音频。
    • AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 表示您要播放音频,允许之前的持有者继续播放音频,但是需要降低音频。例如:导航应用和音乐应用同时播放音频的场景。

以下是 Android 官方提供的代码:


AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioManager.OnAudioFocusChangeListener afChangeListener;

...
// Request audio focus for playback
int result = audioManager.requestAudioFocus(afChangeListener,
                             // Use the music stream.
                             AudioManager.STREAM_MUSIC,
                             // Request permanent focus.
                             AudioManager.AUDIOFOCUS_GAIN);

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}

private Handler handler = new Handler();
AudioManager.OnAudioFocusChangeListener afChangeListener =
  new AudioManager.OnAudioFocusChangeListener() {
    public void onAudioFocusChange(int focusChange) {
      if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
        // Permanent loss of audio focus
        // Pause playback immediately
        mediaController.getTransportControls().pause();
        // Wait 30 seconds before stopping playback
        handler.postDelayed(delayedStopRunnable,
          TimeUnit.SECONDS.toMillis(30));
      }
      else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
        // Pause playback
      } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
        // Lower the volume, keep playing
      } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
        // Your app has been granted audio focus again
        // Raise volume to normal, restart playback if necessary
      }
    }
  };

step2) 放弃音频焦点

audioManager.abandonAudioFocus(afChangeListener);

3.2 Android 8.0 到 11 的版本适配

这个阶段的 Android 系统版本,官方做了一些变更,但是接管的权利还是在开发者的手里。首先初始化的方式有所变动,提供了 Builder 来构建这块的参数 (请阅读下代码)。 下面介绍下接口的用法:

  • setAudioAttributes 设置应用的流类型、音频流使用的场景。
  • setOnAudioFocusChangeListener 这个和之前的接口相同的用法。
  • setWillPauseWhenDuckd 如果其他应用请求抢占音频焦点的方式是AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK ****这种类型的话,系统会自动帮你降低音量,不会再回调 onAudioFocusChange ,但是你想做一些暂停等其他的处理,你可以设置 setWillPauseWhenDuckd(true) 让回调正常工作。
  • setAcceptDelayedFocusGain 如果正在使用应用优先级较高,不能立马请求到音频焦点。你可以通过此接口 setAcceptDelayedFocusGain(true) 来开启异步请求音频焦点。
// initializing variables for audio focus and playback management
audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);
playbackAttributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_GAME)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();
focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(playbackAttributes)
        .setAcceptsDelayedFocusGain(true)
        .setOnAudioFocusChangeListener(afChangeListener, handler)
        .build();
final Object focusLock = new Object();

boolean playbackDelayed = false;
boolean playbackNowAuthorized = false;

// requesting audio focus and processing the response
int res = audioManager.requestAudioFocus(focusRequest);
synchronized(focusLock) {
    if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
        playbackNowAuthorized = false;
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
        playbackNowAuthorized = true;
        playbackNow();
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
        playbackDelayed = true;
        playbackNowAuthorized = false;
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
@Override
public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false;
                    resumeOnFocusGain = false;
                }
                playbackNow();
            }
            break;
        case AudioManager.AUDIOFOCUS_LOSS:
            synchronized(focusLock) {
                resumeOnFocusGain = false;
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying();
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // ... pausing or ducking depends on your app
            break;
        }
    }
}

3.3 Android 12 以上版本更变

Android 12 之后,系统开始接管音频焦点这块的功能,也是为了用户更好的音频体验。下面列举下三种场景的音频焦点变更的变化。

3.3.1 永久失去音频焦点

A 应用当前获取了音频焦点,正在播放音频流。

B 应用通过 AudioManager.AUDIOFOCUS_GAIN 方式来抢占音频焦点。

那么 A 应用将被系统强制停止播放,如果你想增加一些淡出的效果,配置必须满足以下条件:

  1. setUsage 需设置 AudioAttributes.USAGE_MEDIA 或者 AudioAttributes.USAGE_GAME
  2. setContentType 不能设置 AudioAttributes.CONTENT_TYPE_SPEECH

3.3.2 自动降低音量

当前抢占音频焦点的应用 A 配置如下:

  1. setContentType 并未设置 AudioAttributes.CONTENT_TYPE_SPEECH
  2. 应用并未设置 AudioFocusRequest.setWillPauseWhenDucked(true)

应用 B 通过 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 来抢占音频焦点。

那么当应用 B 抢占了音频焦点之后,系统会自动的将应用 A 音量降低。

3.3.2 来电静音

如果您的应用请求音频焦点的代码中里 setUsage 设置 AudioAttributes.USAGE_MEDIA 或者 AudioAttributes.USAGE_GAME 。来电时,系统会自动暂停应用的音频流,待通话结束后自动开启。

最后

以上就是本文的所有内容了,介绍了影响 Android 平台音频路由的相关知识点,相信对您有所帮助。 本文是音视频基础能力 - Android 音频篇的第四篇,后续精彩内容,敬请期待。往期精彩内容,可参考:

# 音视频基础能力之 Android 音频篇(一): 音频采集

# 音视频基础能力之 Andoid 音频篇(二):音频录制

# 音视频基础能力之 Android 音频篇 (三):高性能音频采集

打个广告,欢迎关注我们运营的公众号 声知视界,会定期推送移动端、音视频领域的相关的科普类文章。