Android 音量系统分析

最近在处理一个蓝牙设备播放没有声音问题时,发现是设置音量的问题,顺便学习了一下Android系统的音量构架原理及设置方法。这里主要参考了rinswindqin同学写的有关音频及音量分析的文章,加了一些自己的理解及源代码分析。下面以Android 6.0为例来说明。

一、音频流、音频设备、音量三角关系

要了解Android系统的音量构架原理,我们先要了解一下Android系统的音频流有哪些。下面是在AudioSystem.java中定义的音频流格式:

int STREAM_VOICE_CALL = 0;    电话

int STREAM_SYSTEM = 1;   系统

int STREAM_RING = 2;  响铃和消息

int STREAM_MUSIC = 3;   音乐

int STREAM_ALARM = 4;  闹钟

int STREAM_NOTIFICATION = 5;  通知

int STREAM_BLUETOOTH_SCO = 6;  蓝牙

int STREAM_SYSTEM_ENFORCED = 7;   强制系统声音

int STREAM_DTMF = 8;  双音多频

int STREAM_TTS = 9;  语音

总共10种音频流,因Android版本不同可能存在差异。在AudioManager.java中也定义了,但它是引用了AudioSystem.java的定义。

音量与音频流是息息相关的。每种音频流至少对应一种音量,当然也可以多种音频流对应一种音量。在AudioService.java中定义了这种对应关系,具体如下:

private final int[] STREAM_VOLUME_ALIAS_VOICE = new int[] {
    // STREAM_VOICE_CALL
    AudioSystem.STREAM_VOICE_CALL,
    // STREAM_SYSTEM
    AudioSystem.STREAM_RING,
    // STREAM_RING      
    AudioSystem.STREAM_RING,
    // STREAM_MUSIC            
    AudioSystem.STREAM_MUSIC, 
    // STREAM_ALARM       
    AudioSystem.STREAM_ALARM, 
    // STREAM_NOTIFICATION      
    AudioSystem.STREAM_RING,
    // STREAM_BLUETOOTH_SCO       
    AudioSystem.STREAM_BLUETOOTH_SCO,
    // STREAM_SYSTEM_ENFORCED
    AudioSystem.STREAM_RING,
    // STREAM_DTMF    
    AudioSystem.STREAM_RING, 
    // STREAM_TTS     
    AudioSystem.STREAM_MUSIC      
};

从上面定义可以看到系统音频流,响铃与消息音频流,通知音频流,强制声音音频流,DTMF这五种音频流共用一个音量,音乐与语音是共用一个音量。上面这个定义是用于通话的Android平台上的(比如手机),Android还定义了两种,分别用在电视或者机顶盒上的定义:

private final int[] STREAM_VOLUME_ALIAS_TELEVISION = new int[] {
    // STREAM_VOICE_CALL
    AudioSystem.STREAM_MUSIC,
    // STREAM_SYSTEM     
    AudioSystem.STREAM_MUSIC,
    // STREAM_RING     
    AudioSystem.STREAM_MUSIC,
    // STREAM_MUSIC    
    AudioSystem.STREAM_MUSIC,
    // STREAM_ALARM  
    AudioSystem.STREAM_MUSIC,
    // STREAM_NOTIFICATION 
    AudioSystem.STREAM_MUSIC, 
     // STREAM_BLUETOOTH_SCO 
    AudioSystem.STREAM_MUSIC,
    // STREAM_SYSTEM_ENFORCED   
    AudioSystem.STREAM_MUSIC, 
    // STREAM_DTMF  
    AudioSystem.STREAM_MUSIC,
     // STREAM_TTS 
    AudioSystem.STREAM_MUSIC       
};

和是其它设备的定义:

private final int[] STREAM_VOLUME_ALIAS_DEFAULT = new int[] {
        // STREAM_VOICE_CALL
        AudioSystem.STREAM_VOICE_CALL,
        // STREAM_SYSTEM  
        AudioSystem.STREAM_RING,
        // STREAM_RING      
        AudioSystem.STREAM_RING,  
         // STREAM_MUSIC    
        AudioSystem.STREAM_MUSIC,
         // STREAM_ALARM   
        AudioSystem.STREAM_ALARM,  
        // STREAM_NOTIFICATION
        AudioSystem.STREAM_RING,    
         // STREAM_BLUETOOTH_SCO  
        AudioSystem.STREAM_BLUETOOTH_SCO,
        // STREAM_SYSTEM_ENFORCED
        AudioSystem.STREAM_RING,    
        // STREAM_DTMF
        AudioSystem.STREAM_RING, 
        // STREAM_TTS     
        AudioSystem.STREAM_MUSIC       
};

系统通过mStreamVolumeAlias来存储当前是那种平台。

我们知道在使用手机扬声器播放音乐时调整音量后,如果插入耳机,从耳机听到的音量并没有变化。在Android系统中,定义了一系统输入和输出设备,针对每个输入与输出设备的音量也是不一样的。下面是Android系统在audio.h定义的部份音频设备。 输出设备:

AUDIO_DEVICE_OUT_EARPIECE                  = 0x1,// 听筒
AUDIO_DEVICE_OUT_SPEAKER                   = 0x2,// 扬声器
AUDIO_DEVICE_OUT_WIRED_HEADSET             = 0x4,//线控耳机
AUDIO_DEVICE_OUT_WIRED_HEADPHONE           = 0x8,//普通耳机
AUDIO_DEVICE_OUT_BLUETOOTH_SCO             = 0x10,//单声道蓝牙耳机
AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET     = 0x20,//蓝牙电话 
AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT      = 0x40, //车载免提蓝牙设备
AUDIO_DEVICE_OUT_BLUETOOTH_A2DP                 = 0x80, //立体声蓝牙耳机

输入设备,比如:

AUDIO_DEVICE_IN_BUILTIN_MIC     = AUDIO_DEVICE_BIT_IN | 0x4, //手机自带MIC
AUDIO_DEVICE_IN_VOICE_CALL     = AUDIO_DEVICE_BIT_IN| 0x40,//电话MIC

可以说每个音频流对应到每种设备都有一个音量。比如,对于同一个STREAM_MUSIC流,对扬声器和耳机的音量是分开存储的。不考虑相同的情况,音量个数=音频流*音频设备。

二、音量的缓存与持久化

音量的缓存是通过AudioService.java的内部类VolumeStreamState来设置。mStreamType属性指示哪个音频流,通过定义:

private final SparseIntArray mIndexMap = new SparseIntArray(8);

按照device = index的键值对,每个元素对应一个设备的音量,将用于播放这种音频流的设备的音量保存在其中。 在AudioService中定义了所有音频流及所对应的设备的音量,具体如下:

private VolumeStreamState[] mStreamStates;

我们在使用手机调整音量后,关机后再开机,发现音量是我们最后调整的音量。那么这说明音量已经持久化了。音量的持久化在Android 6.0以前是保存到设置数据库setting.db的System表中,具体如下:

上图中后缀为headset的就是耳机相关音量,比如:耳机铃声音量,耳机MIC音量。

在Android 6.0及以后的版本为了加快响应速度,采用了xml的形式来存储的。

在用户做音量调整时,会保存到数据库或者xml中以实现音量的持久化。如果Android系统没有使用过音量,音量的初始值是什么呢?在AudioSystem.java中定义了各种音频流的默认音量,如下:

public static int[] DEFAULT_STREAM_VOLUME = new int[] {
        4,  // STREAM_VOICE_CALL  
        7,  // STREAM_SYSTEM
        5,  // STREAM_RING      
        11, // STREAM_MUSIC     
        6,  // STREAM_ALARM     
        5,  // STREAM_NOTIFICATION 
        7,  // STREAM_BLUETOOTH_SCO 
        7,  // STREAM_SYSTEM_ENFORCED 
        11, // STREAM_DTMF
        11  // STREAM_TTS
};

同样在AudioService.java中定义了每种流的最大音量与最小音量:

/** Maximum volume index values for audio streams */
private static int[] MAX_STREAM_VOLUME = new int[] {
    5,  // STREAM_VOICE_CALL
    7,  // STREAM_SYSTEM
    7,  // STREAM_RING
    15, // STREAM_MUSIC
    7,  // STREAM_ALARM
    7,  // STREAM_NOTIFICATION
    15, // STREAM_BLUETOOTH_SCO
    7,  // STREAM_SYSTEM_ENFORCED
    15, // STREAM_DTMF
    15  // STREAM_TTS
};
/** Minimum volume index values for audio streams */
private static int[] MIN_STREAM_VOLUME = new int[] {
    1,  // STREAM_VOICE_CALL
    0,  // STREAM_SYSTEM
    0,  // STREAM_RING
    0,  // STREAM_MUSIC
    0,  // STREAM_ALARM
    0,  // STREAM_NOTIFICATION
    1,  // STREAM_BLUETOOTH_SCO
    0,  // STREAM_SYSTEM_ENFORCED
    0,  // STREAM_DTMF
    0   // STREAM_TTS
};

通过以上两个数组来控制各种流音量的最大最小值。

三、音量的设置流程

设置音量通常有以下方法: 通过AudioManager来设置 通过AudioTrack/MediaPlayer来设置

1.通过AudioManager来设置

我们先看一下AudioManager音量的设置过程

图3.1AudioManager音量设置流程

AudioManager只是一个轻量级的封装类,由Context创建,工作在APK进程中,通过IBinder的机制,负责与JAVA层的音频服务AudioService进行交互。

AudioManager类提供了setStreamVolume方法来对一种stream type对应的音量进行设置:

public void setStreamVolume(int streamType, int index, int flags) {
    IAudioService service = getService();
    try {
        service.setStreamVolume(streamType, index, flags, getContext().getOpPackageName());
    } catch (RemoteException e) {
        Log.e(TAG, "Dead object in setStreamVolume", e);
    }
}

从代码中可以看出,setStreamVolume就是通过IPC调用AudioService的方法,用一个类图来表示AudioManager和AudioService的关系:

图3.2 AudioManager & AudioService

AudioManager通过代理对象访问工作在SystemServer中的AudioService服务,调用其setStreamVolume方法来设置音量。

上面说过AudioService通过VolumeStreamState来缓存各种音频流的音量,并且通过mStreamStates来记录各种音频流的音量。设置音量最终是通过 VolumeStreamState. applyDeviceVolume_syncVSS函数调用AudioSystem.setStreamVolumeIndex函数来传入device类型、音量index以及stream类型,告知音频系统,“使用这种device播放这种stream类型的音频播放操作,都将使用这个音量index”。代码如下:

status_t AudioSystem::setStreamVolumeIndex(audio_stream_type_t stream,
                                           int index,
                                           audio_devices_t device)
{
    const sp<IAudioPolicyService>& aps = AudioSystem::get_audio_policy_service();
    if (aps == 0) return PERMISSION_DENIED;
    return aps->setStreamVolumeIndex(stream, index, device);
}

AudioSystem主要对AudioPolicyService进行了封装,所以接下来的操作都是由AudioPolicyService来完成的。

setStreamVolumeIndex是AudioSystem通过IBinder调用了AudioPolicyService的setStreamVolumeIndex函数,AudioPolicyService继承了AudioPolicyClientInterface类,他有一个AudioPolicyInterface类的成员指针mpPolicyManager,实际上就是指向了AudioPolicyManager,最终是调用了AudioPolicyManager的setStreamVolumeIndex函数。(实际上AudioPolicyService是通过成员指针mpPolicyManager访问AudioPolicyManager,而AudioPolicyManager则通过AudioPolicyClientInterface(mpClientInterface)访问AudioPolicyService)。

AudioPolicyManager调用setStreamVolumeIndex后会引发AudioPolicyService执行一个SET_VOLUME的CommandThread,在这个CommandThread中调用了AudioSystem的静态方法setStreamVolume,具体如下:

status_t AudioSystem::setStreamVolume(audio_stream_type_t stream, float value,
        audio_io_handle_t output)
{
    if (uint32_t(stream) >= AUDIO_STREAM_CNT) return BAD_VALUE;
    const sp<IAudioFlinger>& af = AudioSystem::get_audio_flinger();
    if (af == 0) return PERMISSION_DENIED;
    af->setStreamVolume(stream, value, output);
    return NO_ERROR;
}

在这个函数里调用了AudioFlinger的setStreamVolume。在AudioFlinger的setStreamVolume中调用了PlaybackThread的setStreamVolume.

AudioFlinger通过checkPlaybachTread方法,通过AudioPolicy传入IO句柄(audio_io_handle_t),来定位到具体的PlaybackThread,调用其setStreamVolume方法,这个方法将音量值缓存到stream对应的stream_type_t对象中,这样,PlaybackThread便知道每种stream对应的音量了。具体如下:

status_t AudioFlinger::setStreamVolume(audio_stream_type_t stream, float value,
        audio_io_handle_t output)
{
    ......
    if (thread == NULL) {
        for (size_t i = 0; i < mPlaybackThreads.size(); i++) {
            mPlaybackThreads.valueAt(i)->setStreamVolume(stream, value);
        }
    } else {
        thread->setStreamVolume(stream, value);
    }

    return NO_ERROR;
}

在PlaybackThread的setStreamVolume中只是保存当前音量值,然后发送通知在输出音频时按新的音量计算。

2. 通过AudioTrack/MediaPlayer来设置

Android Framework的音频子系统中,每一个音频流对应着一个AudioTrack类的一个实例。每个AudioTrack在创建时会注册到AudioFlinger中,AudioFlinger在AudioPolicy的辅助下,为每个AudioTrack对象建立与某个具体的工作线程的对应关系,并通知这个工作线程创建了一个Track对象与这个AudioTrack进行对应。由AudioFlinger把所有的AudioTrack进行混合(Mixer),然后输送到AudioHardware中进行播放。最多可以创建32个音频流。

AudioMixer进行混音的时候,需要知道每个Track播放音频的音量,这个音量是由stream音量、master音量和track音量相乘出来的,stream音量就是AudioPolicy设置进来的,master volume由用户设置,track volume由调用者通过AudioTrack.setVolume来设置。AudioTrack.setVolume所设置的track volume,是一个取值为0~1.0的浮点数 通常,AudioTrack和AudioFlinger并不在同一个进程中,它们通过android中的binder机制建立联系。

AudioTrack通过setVolume设置音量后,会记录入共享内存中,然后由AudioFlinger去读取。

四、小结

整个Android音量设置还是比较复杂,其中包括持久化及各个模块的缓存及同步更新。所涉及到的音频系统子模块包括AudioService、AudioPolicy和AudioFlinger,每个子模块都用各自的数据结构缓存了stream音量,持久化在设置数据库的system表中或者XML中。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ml

HDUOJ-----2068RPG的错排

RPG的错排 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Ja...

2795
来自专栏CDA数据分析师

10个令人相见恨晚的R语言包

新媒体管家 ? 大约3年前我开始使用R,起初进展很慢,与我习惯的语言相比,语法更加直观也比较简单,而且需要一段时间才能习惯于细微的差别。我还不清楚语言的力量与社...

19410
来自专栏Kirito的技术分享

中文文案排版指南

目录 空格 中英文之间需要增加空格 中文与数字之间需要增加空格 数字与单位之间需要增加空格 全角标点与其他字符之间不加空格 -ms-text-autospac...

4308
来自专栏吉浦迅科技

[教程] 系列报道——PyOpenCL介绍

OpenCL一直被软件工程师诟病说很难学习,但我觉得这是不公平的。OpenCL API的通用性,导致了它比较繁琐。一旦你写了一些OpenCL代码,你就会意识到很...

5967
来自专栏数据结构与算法

BZOJ3573: [Hnoi2014]米特运输(树上乱搞)

Description 米特是D星球上一种非常神秘的物质,蕴含着巨大的能量。在以米特为主要能源的D星上,这种米特能源的运输和储 存一直是一个大问题。D星上有N个...

3637
来自专栏Nian糕的私人厨房

JavaScript 展开全文和收起全文

我们在浏览文章列表页的时候,往往只会看到一部分的摘要,在摘要下面会有一个展开全文的按钮,点开后就能看到完整内容,而原来的展开全文按钮此时变成了收齐全文的按钮,同...

984
来自专栏数据小魔方

这么牛X的包,一般人我不告诉他!!!

本文将给大家介绍一个ggplot2灰常牛X的可视化扩展包,我将该包主页的包用法介绍整理成中文,分享给大家。 包名叫geofacet,有经验的charter大概...

3335
来自专栏安智客

密码发展史之古典密码

密码(Cryptology)是一种用来混淆的技术,它希望将正常的、可识别的信息转变为无法识别的信息。密码学是一个即古老又新兴的学科,密码学一词源自希腊文“kry...

2247
来自专栏点滴积累

ANSJ中文分词使用方法

一、前言 之前做solr索引的时候就使用了ANSJ进行中文分词,用着挺好,然而当时没有写博客记录的习惯。最近又尝试了好几种JAVA下的中文分词库,个人感觉还是A...

3809
来自专栏数据结构与算法

BZOJ4668: 冷战(并查集)

• 1 u v, Reddington 需要知道 u 号军工厂及 v 号军工厂最早在加入第

742

扫码关注云+社区