在前面两节中,我们根据耳麦的拔插,修改了系统源码,在andriod设备状态栏中显示或者隐藏状态栏的信息,并且分析了插拔耳麦导致系统的整个流程。
实际上,我们更加关注四声音通道的切换,比如说我们插上耳麦,声音就从耳机里面播放出来,这个过程是怎样的?以耳机为例,声音通道的切换有两种方式,一种是在驱动程序中切换,不如说,插上耳机之后,会有一个中断,在内核中断程序之中,设置声卡,让声音从耳机之中输出出来。第二种方式是,把输出通道的选择权,交给安卓系统。显然,第二种方式才是正统的方式,驱动程序值需要提供切换的能力就可以了,其到底切不切换应该由应用程序决定。该小节要讲解的就是第二种方式,在安卓系统之中,他是怎么去切换通道的。
我们先回顾一下,前面章节中对android音频系统的讲解,之前我们提到了一个/system/etc/audio_policy.conf与/vendor/etc/audio_policy.conf配置文件,该问题提供了这个单板能支持声卡的一些信息:
/*描述多个modules*/
audio_hw_modules {
primary { //一个modules对应一个厂家提供的so文件
outputs { //一个modules可以有多个output
primary {//一个output表明他的参数(primary代表默认设备)
......
}
spdif_passthrough {
......
}
hdmi {
......
}
}
inputs {//一个modules可以有多个input
primary {
}
}
}
a2dp {
outputs {
a2dp {
......
}
}
}
usb {
outputs {
usb_accessory {
......
}
usb_device {
......
}
}
inputs {
usb_device {
......
}
}
}
r_submix {
outputs {
submix {
......
}
}
inputs {
submix {
......
}
}
}
}
其中primary,a2dp,usb,r_submix节点间我们称为modle,。每个modle会对应一个库文件,以后会使用这个库操作声卡,在modle中存在一个outputs与inputs结点,在outputs中存在primary ,其中包含了:
audio_hw_modules {
primary { //一个modules对应一个厂家提供的so文件
outputs { //一个modules可以有多个output
primary {//一个output表明他的参数(primary代表默认设备)
sampling_rates 44100|48000 //采样率
channel_masks AUDIO_CHANNEL_OUT_STEREO //通道
formats AUDIO_FORMAT_PCM_16_BIT //格式
/*有哪些device,如喇叭,耳机等等*/
devices AUDIO_DEVICE_OUT_EARPIECE|AUDIO_DEVICE_OUT_SPEAKER|AUDIO_DEVICE_OUT_WIRED_HEADSET|AUDIO_DEVICE_OUT_WIRED_HEADPHONE|AUDIO_DEVICE_OUT_ALL_SCO|AUDIO_DEVICE_OUT_AUX_DIGITAL|AUDIO_DEVICE_OUT_SPDIF
flags AUDIO_OUTPUT_FLAG_PRIMARY
}
spdif_passthrough {
sampling_rates 44100|48000
channel_masks AUDIO_CHANNEL_OUT_STEREO
formats AUDIO_FORMAT_PCM_16_BIT
devices AUDIO_DEVICE_OUT_SPDIF
flags AUDIO_OUTPUT_FLAG_DIRECT
}
hdmi {
sampling_rates 44100|48000|192000
channel_masks AUDIO_CHANNEL_OUT_5POINT1|AUDIO_CHANNEL_OUT_STEREO|AUDIO_CHANNEL_OUT_7POINT1
formats AUDIO_FORMAT_PCM_16_BIT
devices AUDIO_DEVICE_OUT_AUX_DIGITAL
flags AUDIO_OUTPUT_FLAG_DIRECT
}
}
inputs {//一个modules可以有多个input
primary {
sampling_rates 8000|11025|16000|22050|32000|44100|48000
channel_masks AUDIO_CHANNEL_IN_MONO|AUDIO_CHANNEL_IN_STEREO
formats AUDIO_FORMAT_PCM_16_BIT
devices AUDIO_DEVICE_IN_BUILTIN_MIC|AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET|AUDIO_DEVICE_IN_WIRED_HEADSET
}
}
}
---------------------------------------------------------------------------------------
usb {
outputs {
usb_accessory {
sampling_rates 44100
channel_masks AUDIO_CHANNEL_OUT_STEREO
formats AUDIO_FORMAT_PCM_16_BIT
devices AUDIO_DEVICE_OUT_USB_ACCESSORY
}
usb_device {
sampling_rates dynamic
channel_masks dynamic
formats dynamic
devices AUDIO_DEVICE_OUT_USB_DEVICE
}
}
inputs {
usb_device {
sampling_rates dynamic
channel_masks AUDIO_CHANNEL_IN_STEREO
formats AUDIO_FORMAT_PCM_16_BIT
devices AUDIO_DEVICE_IN_USB_DEVICE
}
}
}
sampling_rates ,channel_masks ,formats ,devices其中的devices表示他支持的输出方式(耳机喇叭等等)。android系统在启动的时候,就是根据配置文件打开modle对应的库,然后创建一个线程与其对应。
一般来说,对于每一个output都有一个playBackThread与他对应。
前面提到配置文件中有多个modle,一个moudle代表一个声卡,根据上面贴出,我们来说说USB声卡与我们板载primary声卡的对比:
每个声卡的ouput都对应一个playBackThread,其中primary声卡有喇叭(speaker),耳麦插孔(headset)。
假设在系统启动的时候,并没有接入USB声卡,那么应用程序,他想播放声音的时候,会创建一个AudioTrack(其会传入声音的类型),然后会找到对应的播放线程,在线程的内部创建一个Track(属于mTracks数组),AudioTrack与Track之间建立共享内存,APP通过AudioTrack传入声音,然后在经过共享内存传递到对应playBackThread的Track中。
假设同样的原理,有多个APP,图示关系如下:
可以看到每个应用程序APP都在playBackThread中有一个与其对应的Track。显然,每个AudioTrack不停的把数据写入自己对应的Track之中,playBackThread把这些数据去出来之后,然后混合在一起发送给硬件,这些内容都在之前的小节讲解过,不在进行深入解答。
既然每个output都对应一个playBackThread线程,就可能存在多个playBackThread,那么AudioTrack应该把数据发送给那个线程呢?在前面的视频我们讲解过:
APP在构建AudioTrack的时候会传入StreamType的参数,先把StreamType进行归类(Strategy),然后根据Strategy找出device,在根据device找出outputs(即有可能找到多个output:对应多个线程),然后从多个线程中选择一个线程(ouput):
到前面的只是回顾到此结束,现在我们开始讲解本节的内容。
我们怎么切换声音通道呢?其主要文件为AudioPolicyManager.cpp,下面我们对源码进行情景分析:
情景一,接上USB声卡
从配置文件中,我们可以看到usb中对应的modle中存在一个outputs其中包含了两个output(usb_accessory,usb_device)。
1.假设插上了USB声卡之后,会创建两个playBackThread(每个output对应一个)。
2.之前与板载声卡建立联系的playBackThread线程,要进行切换,切换成USB创建的playBackThread,然后在新的playBackThread中,每个APP创建对应的Track。
3.插上USB声卡之后,是从耳机还是喇叭播放?这些也需要重新做一些设置。
情景二,在primary中插上耳机
1.这种情况下他们所涉及的线程并没有改变,所以不需要重新去创建output与playBackThread
2.不需要切换output通道。
3.在原来的ouput中选择Device(headset)即可。
从上可以看出他们的流程都是差不多的,首先需要判断一下,是否需要创建output,播放的声音是否需要新的线程进行处理,在output中学着device。
下面是总结的流程:
5.4 切换声音通道流程
A. 在驱动程序中切换:
比如: 插上耳麦发生中断, 在中断处理程序中设置声卡让声音从耳机中输出
B. 把输出通道的选择权交给android系统
驱动程序上报音频拔插事件,
该事件为某个device插入或拔出,
Android系统需要切换声音通道,
过程为(核心文件为frameworks/av/services/audiopolicy/AudioPolicyManager.cpp):
(核心函数为 setDeviceConnectionState)
a. checkOutputsForDevice
针对该device, 打开新的output, 创建新的playbackthread.
方法:
从audio_policy.conf中确定"本该有多少个output"可以支持它,
mOutputs表示"已经打开的output",
两者对比即可确定"尚未打开的output"
b. checkOutputForAllStrategies / checkOutputForStrategy
对所有的strategy分组声音,
判断是否需要迁移到新的output, 如果需要则迁移对应Track到新的output
方法:
b.1 判断是否需要迁移
对于该strategy, 得到它的oldDevice, 进而得到它的outputs (srcOutputs);
对于该strategy, 得到它的newDevice, 进而得到它的outputs (dstOutputs);
如果这2个srcOutputs、dstOutputs不相同, 表示需要迁移
b.2 如果迁移:
把对应的Track设置为invalidate状态即可,
App写AudioTrack时发现它是invalidate状态, 就会重新创建新的Track
audio_devices_t oldDevice = getDeviceForStrategy(strategy, true /*fromCache*/);
audio_devices_t newDevice = getDeviceForStrategy(strategy, false /*fromCache*/);
SortedVector<audio_io_handle_t> srcOutputs = getOutputsForDevice(oldDevice, mPreviousOutputs);
SortedVector<audio_io_handle_t> dstOutputs = getOutputsForDevice(newDevice, mOutputs);
引起通道切换的原因,就是设备的拔插,上小节我们讲解到其会调用到AudioManager.java文件:
onSetWiredDeviceConnectionState(connectState.mType, connectState.mState,connectState.mAddress, connectState.mName, connectState.mCaller);
函数,不过我们跳过了很多东西,值分析了其中的和插拔相关的部分,现在我们再来分析:
onSetWiredDeviceConnectionState(connectState.mType, connectState.mState,connectState.mAddress, connectState.mName, connectState.mCaller);
handleDeviceConnection(state == 1, device, address, deviceName)
.........
根据前面贴出的时序图,最终会到达AudioPolicyManager.cpp文件中的(假设我们插入设备output_device):
status_t AudioPolicyManager::setDeviceConnectionStateInt(audio_devices_t device,audio_policy_dev_state_t state,const char *device_address,const char *device_name)
/*判断上报的是否为output_device*/
audio_is_output_device(device)
/*根据device在可用的设备列表中查找*/
sp<HwModule> module = mHwModules.getModuleForDevice(device);
/*代表存在直接返回,否则代表为新添加的*/
if (index >= 0) {
return INVALID_OPERATION;
/*添加到可用设备*/
index = mAvailableInputDevices.add(devDesc);
/*判断是否为输入设备*/
audio_is_input_device(device)
/*检查这个device是否存在outputs,如果没有这会创建outpus,与线程*/
checkOutputsForDevice(devDesc, state, outputs, devDesc->mAddress)
配置文件每个output都会被描绘成一个profile,也就是说checkOutputsForDevice会检测所有的profile(output),查找每个profile是否都存在对应的线程,如果没有则进行创建:
status_t AudioPolicyManager::checkOutputsForDevice(const sp<DeviceDescriptor> devDesc,audio_policy_dev_state_t state,SortedVector<audio_io_handle_t>& outputs,const String8 address)
// first list already open outputs that can be routed to this device
/*找出这个支持这个device的profile*/
for (size_t i = 0; i < mOutputs.size(); i++) {
/*看看支持的profile有没有对应的output*/
for (size_t i = 0; i < mHwModules.size(); i++)
/*如果没有output,则open其output,创建播放线程。*/
status_t status = mpClientInterface->openOutput(profile->getModuleHandle(), &output,&config, &desc->mDevice,address,&desc->mLatency,desc->mFlags);
我们再次回的setDeviceConnectionStateInt:
status_t AudioPolicyManager::setDeviceConnectionStateInt(audio_devices_t device,audio_policy_dev_state_t state,const char *device_address,const char *device_name)
.......//前面已分析
checkOutputsForDevice(devDesc, state, outputs, devDesc->mAddress)
// checkA2dpSuspend must run before checkOutputForAllStrategies so that A2DP
// output is suspended before any tracks are moved to it
/*蓝牙部分*/
checkA2dpSuspend();
/*检测该内声音是否需要从原来的ouput切换到新的output*/
checkOutputForAllStrategies();
checkOutputForStrategy()
/*获得老的Device*/
audio_devices_t oldDevice = getDeviceForStrategy(strategy, true /*fromCache*/);
/*获得新的Device*/
audio_devices_t newDevice = getDeviceForStrategy(strategy, false /*fromCache*/);
/*得到其中的Outputs进行比较,确定是否需要切换*/
SortedVector<audio_io_handle_t> srcOutputs = getOutputsForDevice(oldDevice, mPreviousOutputs);
SortedVector<audio_io_handle_t> dstOutputs = getOutputsForDevice(newDevice, mOutputs);
/*需要切换,让现在播放的无效*,其实设置了一个标志位/
mpClientInterface->invalidateStream((audio_stream_type_t)i);
设置标志位之后,其怎么进行切换的呢?
在APP重新写入数据的事就,检测到标志位的变化,就会进行相应的操作,具体流程可以根据图示流程详细分析。