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中添加一个标签是无效的,需要我们自己写一个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音频输出——外放、耳机、听筒_第1张图片

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

你可能感兴趣的:(Android音频输出——外放、耳机、听筒)