前言:音频输出的方式有很多种,外放即扬声器(Speaker)、听筒(Telephone Receiver)、有线耳机(WiredHeadset)、蓝牙音箱(Bluetooth A2DP)等,电话免提、插拔耳机、连接断开蓝牙设备等操作系统都会自动切换Audio音频到相应的输出设备上。

我们知道,音频输出通道切换有些是系统自动切换的,但是有些自动切换并不是我们想要的,如:音乐App在听歌时,需要对听歌时拔出耳机的操作进行阻止(暂停播放)而不是突然切换到外放,又如通话时插入耳机并按下免提,希望声音既可以从耳机输出,又可以从扬声器输出;

我们可以不用系统的自动切换,而自己设置音频输出通道的切换吗?

答案是可以的,即可以在应用层修改,也可以在Framework修改,今天讲的就是Framework层的修改,也就是修改AudioService;

1.先看下android中有哪些音频输出通道,常用的加了注释:

AudioSystem.java
public static final int FORCE_NONE = 0;//默认通道
public static final int FORCE_SPEAKER = 1;//扬声器通道
public static final int FORCE_HEADPHONES = 2;//耳机通道
//下面两个是蓝牙耳机通道
public static final int FORCE_BT_SCO = 3;//是一种双向的音频数据的传输链路,只能用于普通语音的传输,不能用于播放音乐
public static final int FORCE_BT_A2DP = 4;//是一种单向的高品质音频数据传输链路,通常用于播放立体声音乐
public static final int FORCE_WIRED_ACCESSORY = 5;//有线设备通道,如有线耳机
public static final int FORCE_BT_CAR_DOCK = 6;
public static final int FORCE_BT_DESK_DOCK = 7;
public static final int FORCE_ANALOG_DOCK = 8;
public static final int FORCE_DIGITAL_DOCK = 9;
public static final int FORCE_NO_BT_A2DP = 10;
public static final int FORCE_SYSTEM_ENFORCED = 11;
public static final int FORCE_HDMI_SYSTEM_AUDIO_ENFORCED = 12;
public static final int FORCE_ENCODED_SURROUND_NEVER = 13;
public static final int FORCE_ENCODED_SURROUND_ALWAYS = 14;
public static final int NUM_FORCE_CONFIG = 15;
public static final int FORCE_DEFAULT = FORCE_NONE;

常用的也就是:扬声器,有线耳机,听筒,蓝牙耳机等;

2.播放模式

在使用音频输出通道时,需要指定播放模式:

AudioSystem.java
/* modes for setPhoneState, must match AudioSystem.h audio_mode */
public static final int MODE_INVALID            = -2;
public static final int MODE_CURRENT            = -1;
public static final int MODE_NORMAL             = 0;//待机模式,既不是铃声模式也不是通话模式,如music
public static final int MODE_RINGTONE           = 1;//铃声模式
public static final int MODE_IN_CALL            = 2;//音频通话模式
public static final int MODE_IN_COMMUNICATION   = 3;//通信模式,包括音/视频,VoIP通话.(3.0加入的,与通话模式类似)
public static final int NUM_MODES               = 4;

我们指定音频播放模式时,会通知HAL,我们当前音频所处于的状态,以便可以适当地传送音频。

3.流类型

设置播放模式的时候,需要考虑流类型,常用的流类型有:

/** Used to identify the default audio stream volume */
public static final int STREAM_DEFAULT = -1;
/** Used to identify the volume of audio streams for phone calls */
public static final int STREAM_VOICE_CALL = 0;
/** Used to identify the volume of audio streams for system sounds */
public static final int STREAM_SYSTEM = 1;
/** Used to identify the volume of audio streams for the phone ring and message alerts */
public static final int STREAM_RING = 2;
/** Used to identify the volume of audio streams for music playback */
public static final int STREAM_MUSIC = 3;
/** Used to identify the volume of audio streams for alarms */
public static final int STREAM_ALARM = 4;
/** Used to identify the volume of audio streams for notifications */
public static final int STREAM_NOTIFICATION = 5;
/** Used to identify the volume of audio streams for phone calls when connected on bluetooth */
public static final int STREAM_BLUETOOTH_SCO = 6;
/** Used to identify the volume of audio streams for enforced system sounds in certain
* countries (e.g camera in Japan) */
public static final int STREAM_SYSTEM_ENFORCED = 7;
/** Used to identify the volume of audio streams for DTMF tones */
public static final int STREAM_DTMF = 8;
/** Used to identify the volume of audio streams exclusively transmitted through the
*  speaker (TTS) of the device */
public static final int STREAM_TTS = 9;
/** Used to identify the volume of audio streams for accessibility prompts */
public static final int STREAM_ACCESSIBILITY = 10;
/**

4.音频输出通道,播放模式和流类型的关系

音频通道是与播放模式一起用的,而播放模式与音频流类型有关系;
(1)音频通道是指声音从哪里出来,这个容易理解;
(2)播放模式,也叫音频状态,手机有4种音频状态:待机状态,音视频通话状态,视频/VoIP通话状态与响铃状态,这4种状态对底层的音频输出设备的选择影响很大,相应的情景下就得使用相应的模式,如视频情景的播放模式就是MODE_IN_COMMUNICATION,或者,播放音乐情景的播放模式就是MODE_NORMAL,什么样的情形就得用什么样的播放模式,不能搞混,比如MODE_IN_CALL,就只能由通话时才能使用;
(3)音频流类型,我们操作手机的音频时需要指定操作的是哪一个流,虽然手机的中音频流类型有很多,但是一旦进入到属性里,android就会将其整理成几种类型,这才是实际的类型,与上面的播放模式对应;

5.源码分析

上面介绍了音频输出通道,播放模式和流类型,接下来就从源码framework层的角度来分析了;

AudioManager.java
public void setMode(int mode) {final IAudioService service = getService();try {//调用AudioService的setMode()方法service.setMode(mode, mICallBack, mApplicationContext.getOpPackageName());} catch (RemoteException e) {throw e.rethrowFromSystemServer();}
}
AudioService.java
public void (int mode, IBinder cb, String callingPackage) {//应用设置音频模式,需要android.Manifest.permission.MODIFY_AUDIO_SETTINGS权限if (!checkAudioSettingsPermission("setMode()")) {return;}//如果要使用的音频模式是MODE_IN_CALL,那得有android.Manifest.permission.MODIFY_PHONE_STATE权限;//所以,除非是要通话,一般都很少用MODE_IN_CALL,语音视频等用的是MODE_IN_COMMUNICATIONif ( (mode == AudioSystem.MODE_IN_CALL) &&(mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE)!= PackageManager.PERMISSION_GRANTED)) {return;}if (mode < AudioSystem.MODE_CURRENT || mode >= AudioSystem.NUM_MODES) {return;}int newModeOwnerPid = 0;synchronized(mSetModeDeathHandlers) {if (mode == AudioSystem.MODE_CURRENT) {mode = mMode;}//如果此次设置的音频播放模式和上一次的不同,那就返回这次使用新音频播放模式的进程的pidnewModeOwnerPid = mode, cb, Binder.getCallingPid(), callingPackage);}//如果进入了RINGTONE, IN_CALL 或者IN_COMMUNICATION模式,清除掉当前更改音频模式的应用进程的蓝牙SCO连接if (newModeOwnerPid != 0) {disconnectBluetoothSco(newModeOwnerPid);}
}

setMode()中的重点是setModeInt()方法,真正的逻辑都在这里:

AudioService.java
private int setModeInt(int mode, IBinder cb, int pid, String caller) {int newModeOwnerPid = 0;if (cb == null) {return newModeOwnerPid;}SetModeDeathHandler hdlr = null;Iterator iter = mSetModeDeathHandlers.iterator();//循环遍历mSetModeDeathHandlers,找到与传递进来的相同pid的SetModeDeathHandler,并赋值给hdlr,//相同的pid也就是相同的应用,即找到相同的应用while (iter.hasNext()) {SetModeDeathHandler h = (SetModeDeathHandler)iter.next();if (h.getPid() == pid) {hdlr = h;// Remove from client list so that it is re-inserted at top of listiter.remove();hdlr.getBinder().unlinkToDeath(hdlr, 0);break;}}//下面就是设置新的音频播放模式int status = AudioSystem.AUDIO_STATUS_OK;//实际的播放模式int actualMode;do {//初始化实际的播放模式,与传递进来的mode相同actualMode = mode;//如果设置的模式是正常的播放模式,那就从mSetModeDeathHandlers列表的顶端获取一个模式给actualMode,//最近一次设置非正常音频模式的应用都会被放在mSetModeDeathHandlers的顶端if (mode == AudioSystem.MODE_NORMAL) {// get new mode from client at top the list if anyif (!mSetModeDeathHandlers.isEmpty()) {hdlr = mSetModeDeathHandlers.get(0);cb = hdlr.getBinder();actualMode = hdlr.getMode();}} else {//新建hdlrif (hdlr == null) {hdlr = new SetModeDeathHandler(cb, pid);}// Register for client death notificationtry {cb.linkToDeath(hdlr, 0);} catch (RemoteException e) {// Client has died!}//将hdlr加到mSetModeDeathHandlers中,并放到首位,也就是最后一个调用setMode()的进程位于列表的顶部mSetModeDeathHandlers.add(0, hdlr);//设置当前进程的音频播放模式,hdlr.setMode()会将mode设置给mMode,这个要注意,要不然很容易跟下面的"actualMode != mMode"混淆hdlr.setMode(mode);}if (actualMode != mMode) {//通过AudioSystem将当前的音频模式设置到底层去,status返回设置的结果status = AudioSystem.setPhoneState(actualMode);if (status == AudioSystem.AUDIO_STATUS_OK) {if (DEBUG_MODE) { Log.v(TAG, " mode successfully set to " + actualMode); }//如果设置成功,保存当前的音频播放模式mMode = actualMode;} else {//如果设置不成功,从mSetModeDeathHandlers中删除该应用if (hdlr != null) {mSetModeDeathHandlers.remove(hdlr);cb.unlinkToDeath(hdlr, 0);}mode = AudioSystem.MODE_NORMAL;}} else {status = AudioSystem.AUDIO_STATUS_OK;}} while (status != AudioSystem.AUDIO_STATUS_OK && !mSetModeDeathHandlers.isEmpty());if (status == AudioSystem.AUDIO_STATUS_OK) {if (actualMode != AudioSystem.MODE_NORMAL) {if (mSetModeDeathHandlers.isEmpty()) {Log.e(TAG, "setMode() different from MODE_NORMAL with empty mode client stack");} else {//如果这个进程设置的音频模式为非正常模式,那就返回这个进程的pidnewModeOwnerPid = mSetModeDeathHandlers.get(0).getPid();}}//下面的代码用来设置当前音频流类型的音量int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE);int device = getDeviceForStream(streamType);int index = mStreamStates[mStreamVolumeAlias[streamType]].getIndex(device);setStreamVolumeInt(mStreamVolumeAlias[streamType], index, device, true, caller);updateStreamVolumeAlias(true /*updateVolumes*/, caller);}return newModeOwnerPid;
}

AudioService用mMode来保存当前的音频播放模式;

(2)设置音频音频输出管道

设置音频输出管道的方法有两个分别是setSpeakerphoneOn()和setBluetoothScoOn(),我们来就看下最常用的设置扬声器播放;

AudioManager.java
public void setSpeakerphoneOn(boolean on){final IAudioService service = getService();try {//调用AudioService的setSpeakerphoneOn()service.setSpeakerphoneOn(on);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}

进入到AudioService:

AudioService.java
public void setSpeakerphoneOn(boolean on){//需要权限android.Manifest.permission.MODIFY_AUDIO_SETTINGSif (!checkAudioSettingsPermission("setSpeakerphoneOn()")) {return;}// for logging onlyfinal String eventSource = new StringBuilder("setSpeakerphoneOn(").append(on).append(") from u/pid:").append(Binder.getCallingUid()).append("/").append(Binder.getCallingPid()).toString();if (on) {//开启扬声器if (mForcedUseForComm == AudioSystem.FORCE_BT_SCO) {sendMsg(mAudioHandler, MSG_SET_FORCE_USE, SENDMSG_QUEUE,AudioSystem.FOR_RECORD, AudioSystem.FORCE_NONE,eventSource, 0);}//进入扬声器播放的标志AudioSystem.FORCE_SPEAKERmForcedUseForComm = AudioSystem.FORCE_SPEAKER;} else if (mForcedUseForComm == AudioSystem.FORCE_SPEAKER){//取消扬声器mForcedUseForComm = AudioSystem.FORCE_NONE;}mForcedUseForCommExt = mForcedUseForComm;//此时是语音模式AudioSystem.FOR_COMMUNICATION,mForcedUseForComm表示当前是哪种音频通道sendMsg(mAudioHandler, MSG_SET_FORCE_USE, SENDMSG_QUEUE,AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, eventSource, 0);
}private static void sendMsg(Handler handler, int msg,int existingMsgPolicy, int arg1, int arg2, Object obj, int delay) {//existingMsgPolicy是SENDMSG_QUEUEif (existingMsgPolicy == SENDMSG_REPLACE) {handler.removeMessages(msg);} else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {return;}synchronized (mLastDeviceConnectMsgTime) {long time = SystemClock.uptimeMillis() + delay;//传递进来的参数封装,交给mAudioHandler处理;//arg1是AudioSystem.FOR_COMMUNICATION, arg2是mForcedUseForComm, //obj是eventSource,eventSource就是调用扬声器的进程的信息handler.sendMessageAtTime(handler.obtainMessage(msg, arg1, arg2, obj), time);if (msg == MSG_SET_WIRED_DEVICE_CONNECTION_STATE ||msg == MSG_SET_A2DP_SRC_CONNECTION_STATE ||msg == MSG_SET_A2DP_SINK_CONNECTION_STATE) {mLastDeviceConnectMsgTime = time;}}
}

进入到mAudioHandler,调用setForceUse(msg.arg1, msg.arg2, (String) msg.obj);

private void setForceUse(int usage, int config, String eventSource) {synchronized (mConnectedDevices) {setForceUseInt_SyncDevices(usage, config, eventSource);}
}private void setForceUseInt_SyncDevices(int usage, int config, String eventSource) {if (usage == AudioSystem.FOR_MEDIA) {sendMsg(mAudioHandler, MSG_REPORT_NEW_ROUTES,SENDMSG_NOOP, 0, 0, null, 0);}mForceUseLogger.log(new ForceUseEvent(usage, config, eventSource));//将语音模式AudioSystem.FOR_COMMUNICATION,音频通道mForcedUseForComm交给AudioSystem,//AudioSystem会将其设置到HAL底层AudioSystem.setForceUse(usage, config);
}

可以看到,AudioService用mForcedUseForComm和mForcedUseForCommExt保存了当前的音频通道,AudioService在调用AudioSystem.setForceUse(usage, config)方法时,会将相应的音频播放模式和音频通道设置到底层,从这里可以就看出,为什么在调用setSpeakerphoneOn()时要结合setMode()一起使用了;

6.疑惑:

前面介绍中就说了android手机中有很多的音频输出通道,为啥AudioService只提供了setSpeakerphoneOn()和setBluetoothScoOn这两个手动切换音频输出通道的方法呢?
之所以AudioService只提供这两个方法,是因为有些切换是系统自动完成的,比如有线耳机,蓝牙耳机的插入和拔出等,这些音频外设的切换在应用层是无法处理的;
有线耳机和蓝牙耳机均属于音频外设,关于音频外设的内容,可以看下一个章节;

7.听筒,扬声器,有线耳机这三个输出设备的切换

(1)听筒通道

听筒模式一般只会在通话或者语音过程中才会用到,所以,要使用听筒模式,必须得指定播放模式为MODE_IN_CALL或者是MODE_IN_COMMUNICATION;

(2)扬声器通道

在不插入音频外设如耳机的情况下,手机中的输出设备只有听筒和扬声器,要想在听筒和扬声器中切换是比较容易的,无非就是setSpeakerphoneOn()方法调用以及指定播放模式为MODE_IN_CALL或者是MODE_IN_COMMUNICATION;

(3)有线耳机

耳机是音频外设,此时手机中的音频输出设备有3个,除了耳机还有听筒和扬声器;

那底层是怎样选择一个设备进行音频输出的呢?

这就和音频系统中的音频路由策略有关,底层在播放音频时会选择一个设备,这个逻辑跟设备的优先级有关,代码在AudoPolicyServcie中,有时间在剖析这个具体原理;

所以,当手机中的音频输出设备有耳机,听筒和扬声器时,会根据设备的优先级来进行选择;

从测试的结果来看,3个当中,耳机的优先级最高,其次是听筒;

8.总结:

手机的输出设备大体可以分为3类:听筒,扬声器和外设,AudioServcie只提供了手动切换扬声器和蓝牙sro的接口,其余的则由系统根据设备的优先级进行自动切换,听筒设备只能在通话模式或者通信模式下才能使用;

手机音频的播放模式也可以分为4类:待机状态,音视频通话状态,视频/VoIP通话状态与响铃状态,这几个状态会影响系统对音频输出设备的选择;

在AudioService中,无论是手动切换输出设备,还是设置音频的播放模式,都是通过AudioSystem将值设置到AudioFlinger和AudioPolicyManager中,再由AudioFlinger和AudioPolicyManager将其设置到底层中;

android音频系统(6):AudioService之音频输出通道切换相关推荐

  1. android 调用系统录制视频和音频

    1.录制视频 请先申请相机权限 <uses-permission android:name="android.permission.CAMERA" /> public ...

  2. 相声评书段子有声书音频系统开发

    相声评书段子有声书音频系统开发 速接入音频,探索知识新商机精彩内容涵盖有声书,相声段子,音乐,新闻,综艺娱乐.儿童.情感生活.评书; 使用本模块需要申请喜m拉y开放平台,基础设置页有申请入口. 应用优 ...

  3. matlab暂停音频,matlab 中的实时音频

    matlab 中的实时音频 音频系统工具箱™针对实时音频处理进行了优化. audioDeviceReader, audioDeviceWriter, audioPlayerRecorder,dsp.A ...

  4. windows系统可以用android,支持Windows和Android双系统的一体机你见过吗?

    黑板对于屏幕前每个人相信都是印象深刻,因为它是你学生时代的见证.然而如今,随着技术的升级传统意义上的黑板也开始走向智能化时代. 方成板书教学记忆一体机的问世,可谓是应需而生,很快就成为国内众多地区,中 ...

  5. android音频系统(4):AudioService之音量管理

    前言:AudioService这个系统服务包含或者使用了几乎所有与音频有关的内容,AudioService是音频系统在java层的大本营: android音频系统,分为两个部分:数据流和策略: 数据流 ...

  6. android音频系统(5):AudioService之音频焦点

    前言:上一节我们分析了AudioService对音量的管理,这一节来看下AudioService对音频焦点的处理,也就是音频系统中的AudioFocus机制,它用来处理多个音频不合理的同时播放的糟糕后 ...

  7. Android音频系统之四AudioPolicy

    4.1 AudioPolicy的诞生 AudioPolicyService是Android音频系统的两大服务之一,另一个服务是AudioFlinger,这两大服务都在系统启动时有MediaSever加 ...

  8. android音频系统(7):通话过程中的音频输出设备切换

    前言:由于通话比较特殊,Android对于通话过程中音频输出设备的切换做了特殊处理,它在上层也是通过切换音频播放状态来完成切换操作的,android用CallAudioState来封装通话过程中的音频 ...

  9. 【Android】Audio音频输出通道切换 - 蓝牙bluetooth、外放

    参考: [Android]Audio音频输出通道切换 - 蓝牙.外放 Android Audio 音频输出通道切换 为什么 iOS 或 Android 设备连接蓝牙设备后不能通过蓝牙设备接电话? xq ...

  10. 【Android】Audio音频输出通道切换 - 蓝牙、外放

    手机音频的输出有外放(Speaker).听筒(Telephone Receiver).有线耳机(WiredHeadset).蓝牙音箱(Bluetooth A2DP)等输出设备.在平时,电话免提.插拔耳 ...

最新文章

  1. form表单自动填充
  2. 天梯赛 L1-025 正整数A+B (15 分)
  3. 怎样在wp7中检测“主题背景”
  4. C#三层架构之第三次课 业务逻辑层
  5. 分页查询时如何优化MySQL的性能?
  6. BCD与ASCII码互转-C语言实现
  7. microsoft visual studio遇到了问题,需要关闭
  8. BZOJ2151 种树
  9. matlab 仿真逆变电路,逆变电路的MATLAB仿真研究论文.doc
  10. webMethods入门简介
  11. Python3爬取美女妹子图片
  12. 前端开发和后端开发的区别
  13. 做了个抓取全网群二维码和个人二维码的平台
  14. CART回归树算法过程
  15. arista eos系统从零开始研究(1)
  16. 某计算机系统中有k台打印机,第三章复习题(2)
  17. 年年立计划却年年倒?用对计划管理工具是关键
  18. 就业技术书文件表格_《就业规划书》模板
  19. try catch 与 finally
  20. 基于短周期价量特征的多因子选股体系的实现(三)----因子计算

热门文章

  1. html和css实现 字体变色 旋转 图标渐变
  2. 新版个人所得税python123_个人所得税目前的主要征收方式有( )。
  3. p9plus升级鸿蒙教程,华为P9 Plus(VIE-AL10 全网通 EMUI 5.0)一键ROOT图文详解教程
  4. java 什么是过滤器_java中的过滤器是什么
  5. 微信OAuth2接口40163错误怎么解决?
  6. 关于fixed元素的【子父div】宽度问题
  7. 项目管理的四大模型,PM必须懂!
  8. 黏性流体运动的纳维-斯托克斯方程
  9. HTML table border 属性
  10. 联想拯救者Y7000关闭触摸板