Android适配音频外设&音频基础知识

监听USB音频外设的插拔/开关机

Android系统监听音频设备变化的方法

对于Android常见的音频设备,有下面这个回调,可以监听到音频设备列表的变化。但是经过测试,音频外设插拔时是没有这些回调事件发出来的。

image.png

如何监听USB音频外设的插拔

比较容易想到通过监听USB设备连接状态的广播,实现感知USB音频外设的插拔/开关机。代码如下:

   fun initUsbAudioDeviceList(): ArrayList<AudioInfo> {
        val iFilter = IntentFilter()
//        iFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
        iFilter.addAction(AudioManager.ACTION_HEADSET_PLUG)
        iFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
        iFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)

        val mBroadcastReceiver = USBBroadCastReceiver()
        context.registerReceiver(
            mBroadcastReceiver, iFilter
        )
        return updateUsbAudioDevices()
    }

切换音频设备

Android应用层如何切换音频设备

Android应用层切换音频路由的方法

Android系统内部有自己的音频路由策略,对应用层开放的,能够改变系统音频路由设备的方法,有以下两个,在AudioManager中:

/**
 * Sets the speakerphone on or off.
 * <p>
 * This method should only be used by applications that replace the platform-wide
 * management of audio settings or the main telephony application.
 *
 * @param on set <var>true</var> to turn on speakerphone;
 *           <var>false</var> to turn it off
 */
public void setSpeakerphoneOn(boolean on){
    final IAudioService service = getService();
    try {
        service.setSpeakerphoneOn(on);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}
/*
 * @see #stopBluetoothSco()
 * @see #ACTION_SCO_AUDIO_STATE_UPDATED
 */
public void startBluetoothSco(){
    final IAudioService service = getService();
    try {
        service.startBluetoothSco(mICallBack,
                getContext().getApplicationInfo().targetSdkVersion);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

Android切换音频路由方法使用:

切换到蓝牙耳机

audioManager.setSpeakerphoneOn(false);
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);

切换到有线耳机

audioManager.setSpeakerphoneOn(false);
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);

切换到听筒

audioManager.setSpeakerphoneOn(false);
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);

切换到扬声器

audioManager.setSpeakerphoneOn(true);
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);

Android系统音频路由介绍

系统会根据不同的音频流类型选择不同的音频路由策略,然后根据音频路由策略和当前的硬件状态路由到合适的设备进行播放。

音频流类型

AudioSystem.java

/** 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) */
@UnsupportedAppUsage
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;

一般通话音频播放是STREAM_VOICE_CALL,铃声或者提示音播放是STREAM_MUSIC。

音频流类型与路由策略的对应关系

AudioPolicyManagerBase::routing_strategy AudioPolicyManagerBase::getStrategy(
        AudioSystem::stream_type stream) {
    // stream to strategy mapping
    switch (stream) {
    case AudioSystem::VOICE_CALL:
    case AudioSystem::BLUETOOTH_SCO:
        return STRATEGY_PHONE;
    case AudioSystem::RING:
    case AudioSystem::ALARM:
        return STRATEGY_SONIFICATION;
    case AudioSystem::NOTIFICATION:
        return STRATEGY_SONIFICATION_RESPECTFUL;
    case AudioSystem::DTMF:
        return STRATEGY_DTMF;
    default:
        ALOGE("unknown stream type");
    case AudioSystem::SYSTEM:
        // NOTE: SYSTEM stream uses MEDIA strategy because muting music and switching outputs
        // while key clicks are played produces a poor result
    case AudioSystem::TTS:
    case AudioSystem::MUSIC:
        return STRATEGY_MEDIA;
    case AudioSystem::ENFORCED_AUDIBLE:
        return STRATEGY_ENFORCED_AUDIBLE;
    }
}

根据音频路由策略选择设备

audio_devices_t AudioPolicyManagerBase::getDeviceForStrategy(routing_strategy strategy,
                                                             bool fromCache)
{
    uint32_t device = AUDIO_DEVICE_NONE;
    if (fromCache) {
        ALOGVV("getDeviceForStrategy() from cache strategy %d, device %x",
              strategy, mDeviceForStrategy[strategy]);
        return mDeviceForStrategy[strategy];
    }
    switch (strategy) {
    case STRATEGY_SONIFICATION_RESPECTFUL:
        ...
        break;
    case STRATEGY_DTMF:
       ...
        // when in call, DTMF and PHONE strategies follow the same rules
        // FALL THROUGH
    case STRATEGY_PHONE:
        // for phone strategy, we first consider the forced use and then the available devices by order
        // of priority
        ...

    case STRATEGY_SONIFICATION:
        ...
        // FALL THROUGH
    case STRATEGY_ENFORCED_AUDIBLE:
       ...
        // The second device used for sonification is the same as the device used by media strategy
        // FALL THROUGH
    case STRATEGY_MEDIA: {
        ...
         break;
    default:
        ALOGW("getDeviceForStrategy() unknown strategy: %d", strategy);
        break;
    }
    ALOGVV("getDeviceForStrategy() strategy %d, device %x", strategy, device);
    return device;
}

STRATEGY_MEDIA:

 case STRATEGY_MEDIA: {
        uint32_t device2 = AUDIO_DEVICE_NONE;
        if (strategy != STRATEGY_SONIFICATION) {
            // no sonification on remote submix (e.g. WFD)
            device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_REMOTE_SUBMIX;
        }
        if ((device2 == AUDIO_DEVICE_NONE) &&
                mHasA2dp && (mForceUse[AudioSystem::FOR_MEDIA] != AudioSystem::FORCE_NO_BT_A2DP) &&
                (getA2dpOutput() != 0) && !mA2dpSuspended) {
            device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_BLUETOOTH_A2DP;
            if (device2 == AUDIO_DEVICE_NONE) {
                device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES;
            }
            if (device2 == AUDIO_DEVICE_NONE) {
                device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER;
            }
        }
        if (device2 == AUDIO_DEVICE_NONE) {
            device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_WIRED_HEADPHONE;
        }
        if (device2 == AUDIO_DEVICE_NONE) {
            device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_WIRED_HEADSET;
        }
        if (device2 == AUDIO_DEVICE_NONE) {
            device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_USB_ACCESSORY;
        }
        if (device2 == AUDIO_DEVICE_NONE) {
            device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_USB_DEVICE;
        }
        if (device2 == AUDIO_DEVICE_NONE) {
            device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET;
        }
        if ((device2 == AUDIO_DEVICE_NONE) && (strategy != STRATEGY_SONIFICATION)) {
            // no sonification on aux digital (e.g. HDMI)
            device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_AUX_DIGITAL;
        }
        if ((device2 == AUDIO_DEVICE_NONE) &&
                (mForceUse[AudioSystem::FOR_DOCK] == AudioSystem::FORCE_ANALOG_DOCK)) {
            device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET;
        }
        if (device2 == AUDIO_DEVICE_NONE) {
            device2 = mAvailableOutputDevices & AUDIO_DEVICE_OUT_SPEAKER;
        }
        // device is DEVICE_OUT_SPEAKER if we come from case STRATEGY_SONIFICATION or
        // STRATEGY_ENFORCED_AUDIBLE, AUDIO_DEVICE_NONE otherwise
        device |= device2;
        if (device) break;
        device = mDefaultOutputDevice;
        if (device == AUDIO_DEVICE_NONE) {
            ALOGE("getDeviceForStrategy() no device found for STRATEGY_MEDIA");
        }
        } break;

STRATEGY_PHONE:

 case STRATEGY_PHONE:
        // for phone strategy, we first consider the forced use and then the available devices by order
        // of priority
        switch (mForceUse[AudioSystem::FOR_COMMUNICATION]) {
        case AudioSystem::FORCE_BT_SCO:
            if (!isInCall() || strategy != STRATEGY_DTMF) {
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT;
                if (device) break;
            }
            device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET;
            if (device) break;
            device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_BLUETOOTH_SCO;
            if (device) break;
            // if SCO device is requested but no SCO device is available, fall back to default case
            // FALL THROUGH
        default:    // FORCE_NONE
            // when not in a phone call, phone strategy should route STREAM_VOICE_CALL to A2DP
            if (mHasA2dp && !isInCall() &&
                    (mForceUse[AudioSystem::FOR_MEDIA] != AudioSystem::FORCE_NO_BT_A2DP) &&
                    (getA2dpOutput() != 0) && !mA2dpSuspended) {
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_BLUETOOTH_A2DP;
                if (device) break;
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES;
                if (device) break;
            }
            device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_WIRED_HEADPHONE;
            if (device) break;
            device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_WIRED_HEADSET;
            if (device) break;
            if (mPhoneState != AudioSystem::MODE_IN_CALL) {
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_USB_ACCESSORY;
                if (device) break;
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_USB_DEVICE;
                if (device) break;
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET;
                if (device) break;
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_AUX_DIGITAL;
                if (device) break;
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET;
                if (device) break;
            }
            device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_EARPIECE;
            if (device) break;
            device = mDefaultOutputDevice;
            if (device == AUDIO_DEVICE_NONE) {
                ALOGE("getDeviceForStrategy() no device found for STRATEGY_PHONE");
            }
            break;
        case AudioSystem::FORCE_SPEAKER:
            // when not in a phone call, phone strategy should route STREAM_VOICE_CALL to
            // A2DP speaker when forcing to speaker output
            if (mHasA2dp && !isInCall() &&
                    (mForceUse[AudioSystem::FOR_MEDIA] != AudioSystem::FORCE_NO_BT_A2DP) &&
                    (getA2dpOutput() != 0) && !mA2dpSuspended) {
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER;
                if (device) break;
            }
            if (mPhoneState != AudioSystem::MODE_IN_CALL) {
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_USB_ACCESSORY;
                if (device) break;
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_USB_DEVICE;
                if (device) break;
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET;
                if (device) break;
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_AUX_DIGITAL;
                if (device) break;
                device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET;
                if (device) break;
            }
            device = mAvailableOutputDevices & AUDIO_DEVICE_OUT_SPEAKER;
            if (device) break;
            device = mDefaultOutputDevice;
            if (device == AUDIO_DEVICE_NONE) {
                ALOGE("getDeviceForStrategy() no device found for STRATEGY_PHONE, FORCE_SPEAKER");
            }
            break;
        }
    break;

可以看出,一般情况下(以上的 default: // FORCE_NONE逻辑),STRATEGY_MEDIA和STRATEGY_PHONE这两种路由策略,一般情况下,都会按照优先级顺序 蓝牙耳机 > 有线耳机 > USB外接音频设备和其他外界设备 > 默认设置(听筒/扬声器)来选择当前的音频输出设备。

而STRATEGY_PHONE路由策略下,选择音频设备逻辑,还会受mForceUse[AudioSystem::FOR_COMMUNICATION]的影响。且force use逻辑的优先级高于默认的根据设备可用性按顺序选择的逻辑。在FORCE_BT_SCO和FORCE_SPEAKER情况下,会优先选择 蓝牙a2dp扬声器/扬声器 播放。

那么mForceUse中的设置又是哪来的呢?AudioManager-->setSpeakerphoneOn这个方法,就是会影响mForceUse中数据的其中一个方法,下面以这个方法为例分析。

举例setSpeakerphoneOn如何影响音频路由

该方法调用链路:

image1.png

AudioPolicyManagerBase::setForceUse源码:

void AudioPolicyManagerBase::setForceUse(AudioSystem::force_use usage, AudioSystem::forced_config config)
{
    ALOGV("setForceUse() usage %d, config %d, mPhoneState %d", usage, config, mPhoneState);
    bool forceVolumeReeval = false;
    switch(usage) {
    case AudioSystem::FOR_COMMUNICATION:
        if (config != AudioSystem::FORCE_SPEAKER && config != AudioSystem::FORCE_BT_SCO &&
            config != AudioSystem::FORCE_NONE) {
            ALOGW("setForceUse() invalid config %d for FOR_COMMUNICATION", config);
            return;
        }
        forceVolumeReeval = true;
        mForceUse[usage] = config;
        break;
    case AudioSystem::FOR_MEDIA:
        if (config != AudioSystem::FORCE_HEADPHONES && config != AudioSystem::FORCE_BT_A2DP &&
            config != AudioSystem::FORCE_WIRED_ACCESSORY &&
            config != AudioSystem::FORCE_ANALOG_DOCK &&
            config != AudioSystem::FORCE_DIGITAL_DOCK && config != AudioSystem::FORCE_NONE &&
            config != AudioSystem::FORCE_NO_BT_A2DP) {
            ALOGW("setForceUse() invalid config %d for FOR_MEDIA", config);
            return;
        }
        mForceUse[usage] = config;
        break;
    case AudioSystem::FOR_RECORD:
        if (config != AudioSystem::FORCE_BT_SCO && config != AudioSystem::FORCE_WIRED_ACCESSORY &&
            config != AudioSystem::FORCE_NONE) {
            ALOGW("setForceUse() invalid config %d for FOR_RECORD", config);
            return;
        }
        mForceUse[usage] = config;
        break;
    case AudioSystem::FOR_DOCK:
        if (config != AudioSystem::FORCE_NONE && config != AudioSystem::FORCE_BT_CAR_DOCK &&
            config != AudioSystem::FORCE_BT_DESK_DOCK &&
            config != AudioSystem::FORCE_WIRED_ACCESSORY &&
            config != AudioSystem::FORCE_ANALOG_DOCK &&
            config != AudioSystem::FORCE_DIGITAL_DOCK) {
            ALOGW("setForceUse() invalid config %d for FOR_DOCK", config);
        }
        forceVolumeReeval = true;
        mForceUse[usage] = config;
        break;
    case AudioSystem::FOR_SYSTEM:
        if (config != AudioSystem::FORCE_NONE &&
            config != AudioSystem::FORCE_SYSTEM_ENFORCED) {
            ALOGW("setForceUse() invalid config %d for FOR_SYSTEM", config);
        }
        forceVolumeReeval = true;
        mForceUse[usage] = config;
        break;
    default:
        ALOGW("setForceUse() invalid usage %d", usage);
        break;
    }
    // check for device and output changes triggered by new force usage
   ...
    }
}

这里将设置保存到mForceUse 后,在路由策略选择音频设备方法AudioPolicyManagerBase::getDeviceForStrategy中,会消费这个数据,如果是强制切换到扬声器,便会优先路由到扬声器或者外接音频设备。

类似的,AudioManager的setBluetoothScoOn方法,也是通过设置mForceUse,实现对音频设备的强制切换。

如何在系统内置音频设备和外接的音频设备之间实现切换?

根据Android系统默认的音频路由策略,当Android设备外接了音频设备时,外接音频设备的优先级大于默认的设备,因此会优先路由到外接的设备;
如果在外接了音频的情况下想切回到自带的扬声器播放,可以尝试AudioManager的setSpeakerPhoneOn方法,如果这个方法无效,基本只能依赖厂商修改固件中的音频路由规则来解决。

调节音量

Android调节音量方式

AudioManager->setStreamVolume


public void setStreamVolume(int streamType, int index, int flags) {
    final IAudioService service = getService();
    try {
        service.setStreamVolume(streamType, index, flags, getContext().getOpPackageName());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

AudioManager会调用到AudioService中去执行音量的调节,在AudioService中,有一个内部类VolumeStreamState,这个类包含了一个流类型的所有音量信息;在AudioService中维护了一个VolumeStreamState类型的数组mStreamStates,对不同流类型的音量分别管理。AudioService负责保存音量信息,并将音量传递给AudioFlinger,使得音量设置对硬件生效。

那么不同流类型的音量控制一定是分开的吗?也不一定。Android系统中与音量控制别名的设计,比如

音量控制别名数组mStreamVolumeAlias,存储了与AudioSystem类中11种流类型对应音量控制流的别名,在进行音量调节操作之前,会线通过这个别名数组拿到对应的StreamType,然后在进行调节。因此,对于不同的流类型,但是别名相同的,音量调节会同步影响,而不是完全隔离。那么会中通话音量和铃声播放音量是不是同步生效取决于固件中音量控制的逻辑。


/* mStreamVolumeAlias[] indicates for each stream if it uses the volume settings
 * of another stream: This avoids multiplying the volume settings for hidden
 * stream types that follow other stream behavior for volume settings
 * NOTE: do not create loops in aliases!
 * Some streams alias to different streams according to device category (phone or tablet) or
 * use case (in call vs off call...). See updateStreamVolumeAlias() for more details.
 *  mStreamVolumeAlias contains STREAM_VOLUME_ALIAS_VOICE aliases for a voice capable device
 *  (phone), STREAM_VOLUME_ALIAS_TELEVISION for a television or set-top box and
 *  STREAM_VOLUME_ALIAS_DEFAULT for other devices (e.g. tablets).*/
private final int[] STREAM_VOLUME_ALIAS_VOICE = new int[] {
    AudioSystem.STREAM_VOICE_CALL,      // STREAM_VOICE_CALL
    AudioSystem.STREAM_RING,            // STREAM_SYSTEM
    AudioSystem.STREAM_RING,            // STREAM_RING
    AudioSystem.STREAM_MUSIC,           // STREAM_MUSIC
    AudioSystem.STREAM_ALARM,           // STREAM_ALARM
    AudioSystem.STREAM_RING,            // STREAM_NOTIFICATION
    AudioSystem.STREAM_BLUETOOTH_SCO,   // STREAM_BLUETOOTH_SCO
    AudioSystem.STREAM_RING,            // STREAM_SYSTEM_ENFORCED
    AudioSystem.STREAM_RING,            // STREAM_DTMF
    AudioSystem.STREAM_MUSIC,           // STREAM_TTS
    AudioSystem.STREAM_MUSIC            // STREAM_ACCESSIBILITY
};
private final int[] STREAM_VOLUME_ALIAS_TELEVISION = new int[] {
    AudioSystem.STREAM_MUSIC,       // 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        // STREAM_ACCESSIBILITY
};
private final int[] STREAM_VOLUME_ALIAS_DEFAULT = new int[] {
    AudioSystem.STREAM_VOICE_CALL,      // STREAM_VOICE_CALL
    AudioSystem.STREAM_RING,            // STREAM_SYSTEM
    AudioSystem.STREAM_RING,            // STREAM_RING
    AudioSystem.STREAM_MUSIC,           // STREAM_MUSIC
    AudioSystem.STREAM_ALARM,           // STREAM_ALARM
    AudioSystem.STREAM_RING,            // STREAM_NOTIFICATION
    AudioSystem.STREAM_BLUETOOTH_SCO,   // STREAM_BLUETOOTH_SCO
    AudioSystem.STREAM_RING,            // STREAM_SYSTEM_ENFORCED
    AudioSystem.STREAM_RING,            // STREAM_DTMF
    AudioSystem.STREAM_MUSIC,           // STREAM_TTS
    AudioSystem.STREAM_MUSIC            // STREAM_ACCESSIBILITY
};
protected static int[] mStreamVolumeAlias;
private void setStreamVolume(int streamType, int index, int flags, String callingPackage,
        String caller, int uid) {
    if (DEBUG_VOL) {
        Log.d(TAG, "setStreamVolume(stream=" + streamType+", index=" + index
                + ", calling=" + callingPackage + ")");
    }
    if (mUseFixedVolume) {
        return;
    }

    ensureValidStreamType(streamType);
    int streamTypeAlias = mStreamVolumeAlias[streamType];
    VolumeStreamState streamState = mStreamStates[streamTypeAlias];
    ...
  }

VOLUME_CHANGED_ACTION广播是如何触发的?

调节音量到触发VOLUME_CHANGED_ACTION广播的流程如下:

image2.png

也就是说,只有通过AudioManager.setStreamVolume 这个方法的调用调节了设备内置的/外接的音频设备的音量,才会触发这个广播的发送。

播放音频

MediaPlayer

可以播放一些比较长的音乐;MediaPlayer启动延迟高,不适合播放短小间隔小的音频,比如字母与数字单音频组成的提示音;有播放状态的回调。

if (mMediaPlayer != null) {
    mMediaPlayer.stop();
    mMediaPlayer.reset();
} else {
    mMediaPlayer = new MediaPlayer();
}
try {
    AssetFileDescriptor afd =
        ContextUtil.getContext().getResources().openRawResourceFd(resId);
    mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    mMediaPlayer.setLooping(true);
    mMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
        afd.getLength());
    mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mp) {
            mMediaPlayer.start();
        }
    });
    mMediaPlayer.prepareAsync();
} catch (Exception e) {
    Logger.e("CallingRingPlayer", "start MediaPlayer error:" + e.getMessage());
}

SoundPool

适合一些短促、比较小的音频,比如通知铃声、字母或数字的单音频等;可以批量预加载资源,得到资源id,根据资源id来播放音频(启动延迟短);播放完成后没有回调。

//初始化
SoundPool.Builder builder = new SoundPool.Builder();
builder.setMaxStreams(maxStreams);
AudioAttributes.Builder attrBuilder = new AudioAttributes.Builder();
attrBuilder.setLegacyStreamType(M_STREAM_TYPE);
builder.setAudioAttributes(attrBuilder.build());
mSound = builder.build();

//进程启动时预先load音频文件
mSound.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
    Logger.i(TAG, "ring file load success");
});
mInMeetingToneId = mSound.load(context, R.raw.ring_in_meeting, 1);
mCountdownToneId = mSound.load(context, R.raw.countdown_audio, 1);
mRemindToneId = mSound.load(context, R.raw.remind_audio, 1);
mOutMeetingToneId =
    mSound.load(context, R.raw.ring_out_meeting, 1);

 //play
 mSound.play(mInMeetingToneId, 1, 1, Integer.MAX_VALUE, 0, 1);

AudioTrack

AudioTrack 有两种数据加载模式(MODE_STREAM 和 MODE_STATIC), 对应着两种完全不同的使用场景。

  • MODE_STREAM:在这种模式下,通过 write 一次次把音频数据写到 AudioTrack 中。这和平时通过 write 调用往文件中写数据类似,但这种方式每次都需要把数据从用户提供的 Buffer 中拷贝到 AudioTrack 内部的 Buffer 中,在一定程度上会使引起延时。为解决这一问题,AudioTrack 就引入了第二种模式。

  • MODE_STATIC:在这种模式下,只需要在 play 之前通过一次 write 调用,把所有数据传递到 AudioTrack 中的内部缓冲区,后续就不必再传递数据了。这种模式适用于像铃声这种内存占用较小、延时要求较高的文件。但它也有一个缺点,就是一次 write 的数据不能太多,否则系统无法分配足够的内存来存储全部数据。

// 初始化AudioTrack 
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, // 指定在流的类型  // STREAM_ALARM:警告声  // STREAM_MUSCI:音乐声,例如music等  // STREAM_RING:铃声  // STREAM_SYSTEM:系统声音  // STREAM_VOCIE_CALL:电话声音  

                samplerate,// 设置音频数据的采样率  AudioFormat.CHANNEL_CONFIGURATION_STEREO,// 设置输出声道为双声道立体声  AudioFormat.ENCODING_PCM_16BIT,// 设置音频数据块是8位还是16位  
                mAudioMinBufSize, AudioTrack.MODE_STREAM);// 设置模式类型,在这里设置为流类型  // AudioTrack中有MODE_STATIC和MODE_STREAM两种分类。  // STREAM方式表示由用户通过write方式把数据一次一次得写到audiotrack中。  // 这种方式的缺点就是JAVA层和Native层不断地交换数据,效率损失较大。  // 而STATIC方式表示是一开始创建的时候,就把音频数据放到一个固定的buffer,然后直接传给audiotrack,  // 后续就不用一次次得write了。AudioTrack会自己播放这个buffer中的数据。  // 这种方法对于铃声等体积较小的文件比较合适。  

mAudioTrack.play();  // 启动
mAudioTrack.write();//数据写入audiotrack中// 停止与释放资源
mAudioTrack.stop();
mAudioTrack.release();

AudioTrack只能播放已经解码的PCM流,不能多种格式的声音文件,例如MP3,AAC,WAV,OGG,MIDI等。因为wav格式的音频文件大部分都是PCM流,AudioTrack不创建解码器,所以可播放不需要解码的wav文件。

一般rtc的通话语音使用的这种方式。

Android音频架构

AudioRcorder和AudioTrack是Audio系统对外提供API类,AudioRcorder主要用于完成音频数据的采集,而AudioTrack则是负责音频数据的输出。AudioFlinger管理着系统中的输入输出音频流,并承担着音频数据的混合,通过读写Audio硬件实现音频数据的输入输出功能;AudioPolicyService是Audio系统的策略控制中心,掌管系统中声音设备的选择和切换、音量控制等。

Android音频控制架构图如下:

image3.png

在Android音频框架中,主要以下面部分组成:

Application:音频应用,如音乐播放器,录音机,收音机等。

Framework java层:

  • AudioTrack:负责回放数据的输出,属于应用框架 API 类

  • AudioRecord:负责录音数据的采集,属于应用框架 API 类

  • AudioSystem: 负责音频事务的综合管理,属于应用框架 API 类

Framework Libraries:

  • AudioTrack:负责回放数据的输出,属于本地框架 API 类

  • AudioRecord:负责录音数据的采集,属于本地框架 API 类

  • AudioSystem: 负责音频事务的综合管理,属于本地框架 API 类

  • AudioPolicyService:音频策略的制定者,负责音频设备切换的策略抉择、音量调节策略等

  • AudioFlinger:音频策略的执行者,负责输入输出流设备的管理及音频流数据的处理传输

音频播放数据流向图如下:

image4.png

其他问题解决分享:

问题一:连接音频外设时,偶现入会提示音播放不出来

原因:会中只有自己一个人或者会中其他人都mute的情况下,有新的人入会容易出现, 因为这时候系统的音频通道被关闭。被关闭的原因是Google 考虑手机低功耗设计了策略:如果3s没有app播放音频就会关闭硬件。下次有app播放才会打开硬件,重新打开的时候需要重新初始化音频播放通道,包括USB驱动、音频驱动、音频应用软件等的初始化,这个耗时较长,导致初始化期间的音频播放不出来,而入会提示音时长总共就只有一两百ms,便完全听不到了。

如何修复:

预先播放一次音量为0的音频,300ms后(一般音频链路已经初始化完成),再次正常播放需要播放的音频。

    mSound.play(mInMeetingToneId, 0, 0, Integer.MAX_VALUE, 0, 1);
    mHandler.postDelayed(
        () -> mSound.play(mInMeetingToneId, 1, 1, Integer.MAX_VALUE, 0, 1), 300);

如果是系统应用的话,也可设置系统属性,修改standby的时长解决 ro.audio.flinger_standbytime_ms。

总结下适配音频外设坑比较多的原因:

(一)Android原生支持耳机这样的音频外设,可以调节音量,需要考虑Android主机和耳机之间的互操作性和一致性。在业务适配场景下,除了保证Android设备和外界设备之间的互操作性之外,还需要考虑外界设备状态和业务一致性。

比如:

  • 设置页面/会中页面的音量显示、音频外设实际的音量是否一致

  • 开启麦克风的时候,业务的UI展示和音频外设实际的设置是否一致

  • 音频设备切换的时候,业务展示选择设备的与系统实际路由的是否一

(二)外接音频设备之后音频播放链路更长,可能产生延时导致体验问题

(三)外接音频设备(甚至可能多个),支持随意切换,一般需要厂商修改固件支持

参考:

从上往下认识安卓音频框架

深入剖析Android音频之AudioTrack - mfmdaoyou - 博客园

Android P Audio系统笔记:AudioPolicy& AudioFlinger初始化 | 航行学园

Android 音频系统:从 AudioTrack 到 AudioFlinger_zhuyong006的博客-CSDN博客

Android音频路由策略_zhuyong006的博客-CSDN博客_android 音频路由

Android深入浅出之Audio第三部分Audio Policy[1] - innost - 博客园

Android音频子系统,Audiopolicyservice音频策略的制定(五)_lin-0410的博客-CSDN博客

Android 音量系统分析 - 掘金

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容