車機 Android 調節音量的三種方式及底層代碼邏輯

来源:https://www.cnblogs.com/qidi-huang/archive/2023/12/13/three_ways_to_set_volume_in_android_automotive_operating_system.html
-Advertisement-
Play Games

車機環境下的音頻使用場景,相較於原始 Android 的音頻使用場景,存在這些特殊性: + **使用專門的 aDSP 晶元進行音效處理;** + **需要播放/控制原始 Android 預設之外的音源(AudioUsage);** + **音源間交互行為更加複雜(AudioFocus);** + ... ...


註意:本文基於 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。 時序圖如下:
image

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。

時序圖如下:
image

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(),此圖略去):
image


以上就是車機 Android 環境下的三種音量調節方式,及底層代碼邏輯。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 背景: 使用com.github.docker-java庫可以很方便地在Java中操作Docker。下麵是一個詳細的教程,包括創建鏡像、創建容器、啟動容器、停止容器和刪除容器的步驟以及每一步的說明。 前提: 首先,在你的Java項目中添加com.github.docker-java庫的依賴。你可以在 ...
  • 概述 ThreadLocal 意為本地線程變數,即該變數只屬於當前線程,對其他線程隔離 我們知道,一個普通變數如果被多線程訪問會存在存線上程安全問題,這時我們可以使用 Synchronize 來保證該變數某一時刻只能有一個線程訪問,從而解決併發安全問題 但如果這個變數並不需要被共用,那麼就可以使用 ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹`LineEdit`單行輸入框組件的常用方法及靈活運用。在Qt中,`QLineEdit`是一個用於輸入單行文本的控... ...
  • 在任何系統中,日誌都是非常重要的組成部分,它是反映系統運行情況的重要依據,也是排查問題時的必要線索。絕大多數人都認可日誌的重要性,但是又有哪些場景可能導致性能問題?今天就讓我們來聊聊Java日誌性能那些事。 ...
  • 一:背景 1. 講故事 在高級調試的旅行中,發現有不少人對符號表不是很清楚,其實簡而言之符號表中記錄著一些程式的生物特征,比如哪個地址是函數(簽名信息),哪個地址是全局變數,靜態變數,行號是多少,數據類型是什麼 等等,目的就是輔助我們可視化的調試,如果沒有這些輔助我們看到的都是一些無意義的彙編代碼, ...
  • 下麵針對 ASP.NET Core 中修改預設埠的五種常用方法的詳細示例,分別對應 appsettings.json 配置 Kestrel 的 Endpoint、使用 UseUrls 方法、命令行參數方法、host.json 配置方法和使用 Docker 的方式。 方法一:appsettings. ...
  • VS2022之後,其實還挺好用的,但個人還是習慣VS+Resharper的強強組合,尤其是Ctrl+N快捷鍵的全局搜,比VS自帶的Ctrl+T好用太多了,Ctrl+B還能直接查看反編譯之後的dll的方法。下麵是常用VS快捷鍵,收藏記錄下。 1、項目快捷鍵CTRL + SHIFT + B生成解決方案C ...
  • 哈嘍大家好,我是鹹魚 參加過校招面試的小伙伴們肯定對下麵這道面試題很熟悉:“當你在瀏覽器輸入一段網址後會發生什麼?”。這道面試題可以說是很經典了,因為其涉及大量網路協議,可以非常直觀的看出小伙伴們對電腦網路體系的整體把握程度 但如果問題換成:“當你打開終端並輸入 ls 時會發生什麼?”,有多少小伙 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...