Android音频输出——外放、耳机、听筒

最近一直在做项目,好久没写博客了
由于项目周期拖得很长,之前写的代码现在再回去看有点吃力
代码写了不少,到头来却感觉脑子里空空的,想是自己疏忽了对所做东西的总结吧
也许编程学习就是 学习理论 —— 项目实战 —— 总结 这么一个过程,当然总结完没事还是要翻翻的

问题来源

项目要用到环信即时通讯云来实现单聊和群聊,为了方便起见,我使用的是环信官方提供的EaseUI(基于3.1.5版本的环信SDK),然而这个库始终使用外放模式来播放对方发送过来的语音,这显然不符合实际场景的需求,咨询客服对方回应音频输出模式的切换与环信无关,需要开发者自己去实现,略坑 (┬_┬)

场景需求

在即时通讯场景中,收到对方语音时,用户可以选择直接外放,也可以选择插入耳机收听。播放过程中如果没有插入耳机,用户把手机贴近耳朵,音频会自动从外放切换到听筒播放并熄屏防误触;此时用户再拿开手机,音频又会切换回外放并点亮屏幕,微信使用的就是这种策略。

需求分析

从上面场景中可以分析出我们要解决的主要问题

  • 设置音频输出模式: 外放、耳机、听筒
  • 屏幕操作:亮屏 <——> 息屏
  • 监测耳机的插入与拔出
  • 监测用户是否靠近听筒

解决问题

1. 设置音频输出模式

Android系统的音频模式由AudioManager来管理,它有一个的setMode()方法用于设置音频模式,但首先需要申请android.permission.MODIFY_AUDIO_SETTINGS权限,setMode()一般使用以下几种参数:

  • MODE_NORMAL : 普通模式
  • MODE_RINGTONE : 铃声模式
  • MODE_IN_CALL : 呼叫模式
  • MODE_IN_COMMUNICATION : 通话模式,包括音/视频、VoIP通话(3.0加入的,与通话模式类似)

很明显,这里我们应该设置通话模式MODE_IN_COMMUNICATION
在通话模式下,要设置外放、耳机、听筒三种音频输出模式需要使用AudioManager的setSpeakerphoneOn()设置是否外放,对于耳机和听筒模式我们把他设置成false
当我们开始播放语音时,我们还要用MediaPlayer将要播放的音频源的StreamType设置成STREAM_VOICE_CALL,核心代码如下:

audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
if (audioManager.isWiredHeadsetOn()) {  // 开始播放语音时,已插入耳机
    audioManager.setSpeakerphoneOn(false);
} else {    // 未插入耳机
    audioManager.setSpeakerphoneOn(true);
}
2. 屏幕操作

Android系统中硬件的工作状态PowerManager来管理,PowerManager通过不同的WakeLock来控制CPU、屏幕、键盘等硬件的工作状态。使用前仍然需要申请权限android.Manifest.permission.DEVICE_POWERandroid.permission.WAKE_LOCK

powerManager = (PowerManager) getSystemService(POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);

上面代码中newWakeLock的第一个参数代表控制级别,可选值如下

  • PARTIAL_WAKE_LOCK : CPU运行,屏幕和键盘可能关闭
  • SCREEN_DIM_WAKE_LOCK : 屏幕亮,键盘灯可能关闭
  • SCREEN_BRIGHT_WAKE_LOCK : 屏幕全亮,键盘灯可能关闭
  • FULL_WAKE_LOCK : 屏幕和键盘灯全亮
  • PROXIMITY_SCREEN_OFF_WAKE_LOCK : 屏幕关闭,键盘灯关闭,CPU运行
  • DOZE_WAKE_LOCK : 屏幕灰显,CPU延缓工作

这里我们选取的是PROXIMITY_SCREEN_OFF_WAKE_LOCK,通过WakeLock的acquire()release()方法即可实现上锁(熄屏)和解锁(亮屏)。

/**
 * 熄屏
 */
private void setScreenOff() {
    if (wakeLock == null) {
        wakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
    }
    wakeLock.acquire();
}

/**
 * 亮屏
 */
private void setScreenOn() {
    if (wakeLock != null) {
        wakeLock.setReferenceCounted(false);
        wakeLock.release();
        wakeLock = null;
    }
}
3. 监测耳机的插入与拔出

在开始播放语音时,我们通过audioManager.isWiredHeadsetOn()可以判断耳机是否插入,这个在上面的代码中已经实现了。但这显然是不够的,我们还需要在播放语音的过程中实时监测耳机的插拔,并及时切换音频输出模式。
当用户插入或者拔出耳机系统会发出Action为Intent.ACTION_HEADSET_PLUG的广播(据说该广播会有一点延迟,如果不希望有任何延迟可以选择监听 AudioManager.ACTION_AUDIO_BECOMING_NOISY,但它只针对有线耳机拔出或者无线耳机断开),并且该广播不能使用静态广播接收者来处理,直接在AndroidManifest.xml中添加一个<receiver>标签是无效的,需要我们自己写一个BroadcastReceiver来接收

public class HeadsetPlugReceiver extends BroadcastReceiver {

    private AudioManager audioManager;

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
            audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
            int state = intent.getIntExtra("state", 0);
            if (state == 0) {   // 耳机拔出
                audioManager.setSpeakerphoneOn(true);
            } else if (state == 1) {    // 耳机插入
                audioManager.setSpeakerphoneOn(false);
            }
        }
    }
}

然后,在需要监测耳机插拔的Activity的onCreate()中注册该BroadcastReceiver

/**
 * 注册监测耳机插拔的BroadcastReceiver
 */
private void registerHeadsetPlugReceiver() {
    headsetPlugReceiver = new HeadsetPlugReceiver();
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(Intent.ACTION_HEADSET_PLUG);
    registerReceiver(headsetPlugReceiver, intentFilter);
}
4. 监测用户是否靠近听筒

现在几乎每个手机都有距离感应器,距离传感器就是位于手机上方的一个小孔儿,用来在用户通话的时候检测人脸位置并及时关闭屏幕以防误操作同时切换听筒输出音频。

Android的距离感应器由SensorManager管理。首先我们要让需要监测用户是否贴近听筒的Activity去实现SensorEventListener接口,并重写该接口的方法如下:

@Override
public void onSensorChanged(SensorEvent event) {
    if (audioManager.isWiredHeadsetOn()) {      // 如果耳机已插入,设置距离传感器失效
        return;
    }
    if (EaseChatRowVoicePlayClickListener.isPlaying) {  // 如果音频正在播放
        float distance = event.values[0];
        if (distance >= sensor.getMaximumRange()) {     // 用户远离听筒,音频外放,亮屏
            audioManager.setSpeakerphoneOn(true);
            setScreenOn();
            Toast.makeText(ChatActivity.this, "已切换为扬声器播放模式", Toast.LENGTH_SHORT).show();
        } else {    // 用户贴近听筒,切换音频到听筒输出,并且熄屏防误触
            audioManager.setSpeakerphoneOn(false);
            setScreenOff();
        }
    } else {    // 音频播放完了
        if (wakeLock != null && wakeLock.isHeld()) {    // 还没有release点亮屏幕
            wakeLock.release();
            wakeLock = null;
        }
    }
}


@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {

}

然后,在该Activity的onCreate()中注册该Listener

/**
 * 注册距离感应器监听器,监测用户是否靠近手机听筒
 */
private void registerProximitySensorListener() {
    sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
    sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);
}

最后不要忘了在onDestroy()中注销HeadsetPlugReceiver和SensorEventListener

@Override
protected void onDestroy() {
    unregisterReceiver(headsetPlugReceiver);
    sensorManager.unregisterListener(this);
    super.onDestroy();
}

结束

这是我第一次在简书上写博客,如果看着难受,欢迎提出意见 _

参考资料
http://www.devwiki.net/2015/09/20/Android-Music-Play-Mode/
http://stackoverflow.com/questions/31871328/android-5-0-audiomanager-setmode-not-working

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,077评论 25 707
  • Media Playback Android多媒体框架包涵了对播放多种通用媒体的类型的支持,所以你可以很容易的集成...
    VegetableAD阅读 882评论 0 0
  • 首先#pragma在本质上是声明,常用的功能就是注释,尤其是给Code分段注释;而且它还有另一个强大的功能是处理编...
    CoderZb阅读 463评论 0 0
  • 救赎是你我生命的交换吗 那是一厢情愿,为成就第九条尾巴 救赎 和自我 不是孪生兄弟,但他们同在 孤独的时刻会走出一...
    木島阅读 620评论 1 1