车机 Android 调节音量的三种方式及底层代码逻辑

这篇具有很好参考价值的文章主要介绍了车机 Android 调节音量的三种方式及底层代码逻辑。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

注意:本文基于 Android 12/S 进行分析
Qidi 2023.07.20 (MarkDown & EnterpriseArchitect & Haroopad)


0. 车机环境下音量调节的特殊性

车机环境下的音频使用场景,相较于原始 Android 的音频使用场景,存在这些特殊性:

  • 使用专门的 aDSP 芯片进行音效处理;
  • 需要播放/控制原始 Android 预设之外的音源(AudioUsage);
  • 音源间交互行为更加复杂(AudioFocus);
  • 需要响应更复杂的电源模式变化。

其中第一、二点会直接影响用户从 APP 层调节音量的方式,以及 AudioHAL 的实现。

0.1 在 aDSP 芯片中进行音效处理

众所周知,Android 在 AudioFlinger::MixerThread 里已经实现了一套调节音量的逻辑,这势必对 aDSP 中的音量调节效果造成影响。为了使送入 aDSP 的音频信号完整,就要禁用这部分音量调节功能。

在 Android 框架代码中,可以将 frameworks/base/core/res/res/values/config.xml 中的 config_useFixedVolume 属性通过 overlay 的方式(参考 前文中的操作)设置为 true,来禁用 AudioFlinger 中的音量调节。

<resources>
    <bool name="config_useFixedVolume">true</bool>
    ......
</resources>

AudioService.java 在各音量函数入口会检查该属性值,从而跳过设置音量到 AudioFlinger::MixerThread 的逻辑。相应代码片段如下:

public class AudioService ... {
	......
    public AudioService(Context context, AudioSystemAdapter audioSystem,
            SystemServerAdapter systemServer) {
        ......
        mUseFixedVolume = mContext.getResources().getBoolean(
                com.android.internal.R.bool.config_useFixedVolume);
        ......
    }
    private void setStreamVolume(int streamType, int index, int flags, String callingPackage,
            String caller, int uid, boolean hasModifyAudioSettings) {
        ......
        if (mUseFixedVolume) {
            return;
        }
        ......
    }
    ......
}

0.2 调节 Android 预设之外的音源音量

APP 要播放声音和控制音量,通常需要指定 AudioUsage但在车机系统上,很多音源在原始 Android 框架里是没有对应的 AudioUsage 的,比如 ECall、Chime,这样的音源一般称之为“外部音源”。 对于这些 Android 预设之外的音源,APP 自然无法通过 AudioManager.setStreamVolume() 等 API 在 AudioFlinger::MixerThread 调节音量,所以我们需要想办法把音量调节请求发送到 AudioHAL 进行处理,或由 AudioHAL 再转发给 aDSP 进行处理。

这就需要 AudioHAL 实现 IDevice::setAudioPortConfig() 接口。Android 12 在 hardware/interfaces/audio/7.0/IDevice.hal 中对该接口的描述如下:

    /**
     * Set audio port configuration.
     *
     * @param config audio port configuration.
     * @return retval operation completion status.
     */
    setAudioPortConfig(AudioPortConfig config) generates (Result retval);

1. 通过 AudioManager 调节音量

1.1 混音音源

“混音音源” 指数据要经过 MixerThread 的音源。对于这些音源,为了让 APP 能使用 AudioManager 的 API 将音量调节命令发送到 aDSP 中,根据上一节说明,我们需要将 config.xml 中的 config_useFixedVolume 属性值配置为 false。
此外,通过阅读 Android 框架代码,发现还需要在 audio_policy_configuration.xml 中给 devicePortgain 节点加上 useForVolume 属性。 如下:

    <devicePort tagName="Speaker" role="sink" type="AUDIO_DEVICE_OUT_SPEAKER" address="">
        <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
                 samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
        <gains>
            <gain name="gain_1" mode="AUDIO_GAIN_MODE_JOINT"
                  minValueMB="-8400"
                  maxValueMB="4000"
                  defaultValueMB="0"
                  stepValueMB="100"
                  useForVolume="true"/>
        </gains>
    </devicePort>

因为 SwAudioOutputDescriptor::setVolume() 函数中会判断这个属性值。只有当 useForVolume 属性值为 true 时,才会调用 AudioFlinger::setAudioPortConfig()。相应代码片段如下:

bool SwAudioOutputDescriptor::setVolume(float volumeDb,
                                        VolumeSource vs, const StreamTypeVector &streamTypes,
                                        const DeviceTypeSet& deviceTypes,
                                        uint32_t delayMs,
                                        bool force)
{
    ......
    for (const auto& devicePort : devices()) {
        if (isSingleDeviceType(deviceTypes, devicePort->type()) &&
                devicePort->hasGainController(true) && isActive(vs)) {
            ......

            audio_port_config config = {};
            devicePort->toAudioPortConfig(&config);
            config.config_mask = AUDIO_PORT_CONFIG_GAIN;
            config.gain.values[0] = gainValueMb;
            return mClientInterface->setAudioPortConfig(&config, 0) == NO_ERROR;
        }
    }
    ......
}

如此修改后,对 “混音音源” 调节音量的命令,就会同时发送给 MixerThread 和 AudioHAL。 时序图如下:
车机 Android 调节音量的三种方式及底层代码逻辑

AudioHAL 可以直接进行音量调节处理,或者将命令转发给 aDSP 进行处理。

1.2 非混音音源

“非混音音源” 指数据不经过 MixerThread,而是送到 DirectOutputThreadOffloadThreadMmapThread 的音源。
为了拉起这些线程,我们(AudioHAL 开发人员)需要在 audio_policy_configuration.xml 里给对应的 mixPort 分别配置下列 flags 属性:

  • AUDIO_OUTPUT_FLAG_DIRECT
  • AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD
  • AUDIO_OUTPUT_FLAG_MMAP_NOIRQ

并且由于 AudioPolicyManager 使用 “优先比对 flags 是否匹配” 的策略来选择播放线程,所以 APP 开发人员创建 AudioTrack 时,也要进行以下操作,才能保证数据不被写到 MixerThread 线程上:

  • 设置音频格式为 non-linear PCM 格式之一,比如 ENCODING_MP3ENCODING_AAC_LCENCODING_IEC61937 等(框架代码会据此自动添加 AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD 标记位。代码片段如下);
status_t AudioTrack::set(...)
{
	......
    // force direct flag if format is not linear PCM
    // or offload was requested
    if ((flags & AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD)
            || !audio_is_linear_pcm(format)) {
        ALOGV( (flags & AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD)
                    ? "%s(): Offload request, forcing to Direct Output"
                    : "%s(): Not linear PCM, forcing to Direct Output",
                    __func__);
        flags = (audio_output_flags_t)
                // FIXME why can't we allow direct AND fast?
                ((flags | AUDIO_OUTPUT_FLAG_DIRECT) & ~AUDIO_OUTPUT_FLAG_FAST);
    }
    ......
}
  • 或者,设置数据传输模式为 MODE_STREAM,并通过 AudioTrack.Builder.setOffloadedPlayback(true) 显式设置播放模式为 offload(框架代码据此会自动添加 AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD 标记位。代码片段如下);
static jint android_media_AudioTrack_setup(...)
{
		......
        switch (memoryMode) {
        case MODE_STREAM:
            status = lpTrack->set(......,
                                  offload ? AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD
                                          : AUDIO_OUTPUT_FLAG_NONE,
                                  ......
                                  );
            break;
        ......
        }
        ......
}
  • 或者,在其使用的 AudioAttributes 变量里设置 AUDIO_OUTPUT_FLAG_HW_AV_SYNC 标记位(框架代码据此会自动添加 AUDIO_OUTPUT_DIRECT 标记位。代码片段如下)。
static inline void audio_flags_to_audio_output_flags(
                                           const audio_flags_mask_t audio_flags,
                                           audio_output_flags_t *flags)
{
    if ((audio_flags & AUDIO_FLAG_HW_AV_SYNC) != 0) {
        *flags = (audio_output_flags_t)(*flags |
            AUDIO_OUTPUT_FLAG_HW_AV_SYNC | AUDIO_OUTPUT_FLAG_DIRECT);
    }
    if ((audio_flags & AUDIO_FLAG_LOW_LATENCY) != 0) {
        *flags = (audio_output_flags_t)(*flags | AUDIO_OUTPUT_FLAG_FAST);
    }
    // check deep buffer after flags have been modified above
    if (*flags == AUDIO_OUTPUT_FLAG_NONE && (audio_flags & AUDIO_FLAG_DEEP_BUFFER) != 0) {
        *flags = AUDIO_OUTPUT_FLAG_DEEP_BUFFER;
    }
}

因为 “非混音音源” 数据不参与 AudioMixer 混音,所以理论上来说,在非车机环境上调节这些音源音量的代码,可以不加修改地直接在车机环境上使用。 APP 通过 AudioManager API 调节这些音源的音量,对 aDSP 接收到的数据没有副作用。

通过 AudioManager API 调节 “非混音音源” 的音量,其 Java 层的处理逻辑与调节 “混音音源” 音量的逻辑相同,故可参考上个时序图;其 Native 层的处理逻辑与通过 AudioTrack API 调节音量的处理逻辑相同,故可参考下一节的时序图。此处省略时序图绘制。


2. 通过 AudioTrack 调节音量

除了 AudioManager,当 APP 直接使用 AudioTrack 播放声音时,也可以通过 AudioTrack.setVolume() 来调节音量。

基本步骤有两个。第一步,新的音量值通过 AudioTrackClientProxyaudio_track_cblk_t 结构体的形式被存储到共享内存里;第二步,在 PlaybackThreadDirectOutputThread 等线程的 threadLoop() 函数中,通过 AudioTrackServerProxy 读取 audio_track_cblk_t 中的音量值。根据线程类型不同,音量值会被送给 AudioMixer 进行混音,或者通过 StreamOut::setVolume() 发给 AudioHAL。

时序图如下:
车机 Android 调节音量的三种方式及底层代码逻辑

PS: 不知道大家是怎么理解 cblk 这个字串的含义的。虽然没有官方说明,但我认为它应该是 Control Block 的意思。


3. 通过 CarAudioManager 调节音量

车机 Android 上还有个特有的组件可用于音量调节,就是 CarAudioManager。APP 通过 CarAudioManager.setGroupVolume() 接口可以设置指定音量组的音量。底层实现这个功能的接口仍然是 IDevice::setAudioPortConfig()

要使用 CarAudioManager API 调节音量,必须将 packages/services/Car/service/res/values/config.xml 中的 audioUseDynamicRouting 属性通过 overlay 方式设置为 true。 如下:

<resources>
    <bool name="audioUseDynamicRouting">true</bool>
    ......
</resources>

否则,代码会回滚为使用 AudioManager.setStreamVolume() 进行调节。相应代码如下:

public class CarAudioService extends ICarAudio.Stub implements CarServiceBase {
	......
    public CarAudioService(Context context) {
        ......

        mUseDynamicRouting = mContext.getResources().getBoolean(R.bool.audioUseDynamicRouting);
        ......
    }
    ......
    @Override
    public void setGroupVolume(int zoneId, int groupId, int index, int flags) {
        enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
        callbackGroupVolumeChange(zoneId, groupId, flags);
        // For legacy stream type based volume control
        if (!mUseDynamicRouting) {
            mAudioManager.setStreamVolume(
                    CarAudioDynamicRouting.STREAM_TYPES[groupId], index, flags);
            return;
        }
        synchronized (mImplLock) {
            CarVolumeGroup group = getCarVolumeGroupLocked(zoneId, groupId);
            group.setCurrentGainIndex(index);
        }
    }
    ......
}

时序图如下(AudioSystem 之后会经过 AudioFlinger 调用到 AudioHAL 实现的 IDevice::setAudioPortConfig(),此图略去):
车机 Android 调节音量的三种方式及底层代码逻辑


以上就是车机 Android 环境下的三种音量调节方式,及底层代码逻辑。文章来源地址https://www.toymoban.com/news/detail-753435.html

到了这里,关于车机 Android 调节音量的三种方式及底层代码逻辑的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android studio(一)按钮监听的三种方式

    第一种方法:public class Activity2 extends AppCompatActivity implements View.OnClickListener 设置监听后,用id进行分别寻址 第二种方法:将implements写在监听类上 第三种方法:由于新版似乎不允许button根据id用switch语句,所以可以改用textview 在对应的布局文件里也需要为textview添加一个属性

    2024年02月02日
    浏览(41)
  • 终止python代码运行的三种方式

    一 sys.exit() 执行这条语句会直接退出程序,也是常用的方法,不需要考虑平台等因素的影响。它通常是退出 Python 程序的首选方法。 该方法包含一个参数status,默认为0,表示正常退出,或1,表示异常退出。 此方法引发 SystemExit 异常(这是唯一不被视为错误的异常)。当这个

    2024年02月05日
    浏览(42)
  • Android使用WebView与Native交互的三种方式 ( 附源码 )

    javascript.html: jsToAndroid.html jsToAndroid2.html 1.JS与Native的交互 一.Android调用JS的方法 目前学习了俩种方法:1. 调用webview的loadUrl 2.调用webview的evaluateJavascript 方法说明: 1. webView.loadUrl(“javascript:callJS()”); 参数是一个字符串,说明调用了javascript中的 callJS方法 webview.evaluateJavascript(

    2024年02月11日
    浏览(60)
  • 【算法与数据结构】二叉树的三种遍历代码实现(下)—— 非递归方式实现(大量图解)

     上篇: 【算法与数据结构】二叉树的三种遍历代码实现(上)—— 用递归序知识点讲解_Hacynn的博客-CSDN博客 https://blog.csdn.net/zzzzzhxxx/article/details/133609612?spm=1001.2014.3001.5502 目录 前言 1、先序遍历 1.1、详细图解描述 1.2、先序遍历非递归代码实现  2、中序遍历 2.1、详细图解描

    2024年02月08日
    浏览(39)
  • 解析音频输出调节音量的原理以及调节的方法

    通常,音量调整可以在音频流水线的两个主要阶段进行:数字域和模拟域。当我们在系统设置中调整音量时,通常是在模拟域(即音频设备)进行调整。在这种情况下,所有的音频输出(包括来自不同应用的音频)都会受到影响。 如果你只想改变特定音频的音量,你可以在数

    2024年02月05日
    浏览(52)
  • python调节电脑音量

    本文使用创作助手 可以使用Python中的win32api和win32con库来调节电脑音量。以下是一个简单的示例代码: 这个示例代码将音量设置为50%。使用 set_volume 函数可以将音量设置在0到100之间的任何值。

    2024年04月12日
    浏览(32)
  • JavaScript的三种引用方式

    1.1、标签引用(或嵌入式) 使用 script 标签将 JavaScript 代码嵌入到 HTML 页面中。可以放置在 head 或 body 中。 显示效果: 1.2、 文件引用 (外链式) 将 JavaScript 代码编写在一个独立的 .js 文件中,并通过 script 标签的 src 属性引入到 HTML 页面中。 显示效果: 1.3、行内式 直接在

    2024年02月02日
    浏览(49)
  • 线程创建的三种方式

    目录 1. Thread类 2. Runnable接口 3. Callable接口 4. 线程的生命周期 新建  就绪 运行 阻塞 等待 结束 继承Thread类的方式创建线程 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务 创建Thread类的子类,即创建了线程对象 调用线程对象的

    2024年02月09日
    浏览(44)
  • Debezium的三种部署方式

    debezium 有下面三种部署方式,其中最常用的就是 kafka connect。 kafka connect 一般情况下,我们通过 kafka connect 来部署 debezium,kafka connect 是一个框架和运行时: source connectors:像 debezium 这样将记录发送到 kafka 的source connector sink connectors:将记录从 kafka topic传播到其他系统的 sin

    2024年02月10日
    浏览(44)
  • 单点登录的三种方式

    因为一个项目种有多个服务组成,每个服务都是独立的,如果登录的时候在一个服务种,那么其他的服务是显示不了的,所以就有了单点登录。 所谓单点登录就是一处登录,处处登录。 第一种方式:session广播机制实现 第一种当时的的原理是session的复制,就是在多个服务中

    2024年02月15日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包