最近遇到很多声音方面的问题,仔细的研究了一轮。之前也看过书,但是实际上还是有很多问题。关于耳机、蓝牙、外放、声音通道等等的处理。
值得一提的是,Android中系统承包了音频的控制,所以设置对媒体通道,也就完成了80%的功能。
这也是我请教别人我遇到的问题,别人总是莫名其妙的原因,因为系统把事情都做好了。
如果你发现自己在声音的处理上需要多很多事情,估计是你没使用对方法。
如何切换音频通道?
有几个关键的设置:
STREAM_VOCIE_CALL
CONTENT_TYPE_SPEECH
USAGE_VOICE_COMMUNICATION
网上很多资料都是。
public class TestActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
}
也不是不能这样设置,起码Android原生的系统是没有任何问题的。
但是小米等这些第三方的系统,会有莫名其妙的问题。
如何监听耳机、蓝牙、外放,并处理切声音通道的切换?
无需处理,系统已经实现
音频焦点如何处理?
一个场景是,我们希望在播放的时候,把别的软件的声音给停掉,避免声音重叠。
可以这样做,每次打开都请求一下焦点,离开应用都释放焦点。
PS:这个的实现靠应用的自觉性,对主流的播放器是有效的。
public class AudioFocusHandler {
private Handler handler = new Handler();
private final AudioManager.OnAudioFocusChangeListener mChangeListener = focusChange -> {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
handler.post(this::release);
}
};
public void request() {
AudioManager am = (AudioManager) CGApp.getInstance().getSystemService(Context.AUDIO_SERVICE);
if (am == null) return;
int result;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN
).setAudioAttributes(playbackAttributes
).setAcceptsDelayedFocusGain(true
).setOnAudioFocusChangeListener(mChangeListener, handler
).build();
result = am.requestAudioFocus(focusRequest);
} else {
result = am.requestAudioFocus(mChangeListener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
}
Lg.d(AudioFocusHandler.class.getSimpleName(), "is granted:",
AudioManager.AUDIOFOCUS_REQUEST_GRANTED == result);
}
public void release() {
AudioManager am = (AudioManager) CGApp.getInstance().getSystemService(Context.AUDIO_SERVICE);
if (am == null) return;
int result = am.abandonAudioFocus(mChangeListener);
Lg.d(AudioFocusHandler.class.getSimpleName(), "is release:",
AudioManager.AUDIOFOCUS_REQUEST_GRANTED == result);
}
}
音量的调节与控制如何处理?
无需处理,系统已经实现
一些背景知识的节选
下面内容节选自《音视频开发进阶指南:基于Android和iOS平台的实践》
强烈建议买这本书,如果你要做音视频开发的话。
由于AudioTrack是Android SDK层提供的最底层的音频播放API,因此只允许输入裸数据。和MediaPlayer相比,对于一个压缩的音频文件(比如MP3、AAC等文件),它需要自行实现解码操作和缓冲区控制。因为这里只涉及AudioTrack的音频渲染端(解码部分已经在前面章节中介绍过了,对于缓冲区的控制机制,后续章节将会详细讲解),所以本节只介绍如何使用AudioTrack渲染音频PCM数据。
首先来看一下AudioTrack的工作流程,具体如下。
1)根据音频参数信息,配置出一个AudioTrack的实例。
2)调用play方法,将AudioTrack切换到播放状态。
3)启动播放线程,循环向AudioTrack的缓冲区中写入音频数据。
4)当数据写完或者停止播放的时候,停止播放线程,并且释放所有资源。
根据AudioTrack的上述工作流程,本节将以4个小部分分别介绍每个流程的详细步骤。
1.配置AudioTrack
先来看一下AudioTrack的参数配置,要想构造出一个AudioTrack类型的实例,必须先了解其构造函数原型,代码如下所示:
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig,
int audioFormat, int bufferSizeInBytes, int mode);
其中构造函数的参数说明如下。
streamType, Android手机上提供了多重音频管理策略(读者按一下手机侧边的按键,可以看到有多个音量管理,这其实就是不同音频策略的音量控制展示),当系统有多个进程需要播放音频的时候,管理策略会决定最终的呈现效果,该参数的可选值将以常量的形式定义在类AudioManager中,主要包括以下内容。
STREAM_VOCIE_CALL:电话声音
STREAM_SYSTEM:系统声音
STREAM_RING:铃声
STREAM_MUSCI:音乐声
STREAM_ALARM:警告声
STREAM_NOTIFICATION:通知声
sampleRateInHz,采样率,即播放的音频每秒钟会有多少次采样,可选用的采样频率列表为:8000、16000、22050、24000、32000、44100、48000等,大家可以根据自己的应用场景进行合理的选择。
channelConf ig,声道数(通道数)的配置,可选值以常量的形式配置在类AudioFormat中,常用的是CHANNEL_IN_MONO(单声道)、CHANNEL_IN_STEREO(双声道),因为现在大多数手机的麦克风都是伪立体声的采集,为了性能考虑,笔者建议使用单声道进行采集,而转变为立体声的过程可以在声音的特效处理阶段来完成。
audioFormat,该参数是用来配置“数据位宽”的,即采样格式,可选值以常量的形式定义在类AudioFormat中,分别为ENCODING_PCM_16BIT(16bit)、ENCODING_PCM_8BIT(8bit),注意,前者是可以兼容所有Android手机的。
bufferSizeInBytes,其配置的是AudioTrack内部的音频缓冲区的大小,AudioTrack类提供了一个帮助开发者确定bufferSizeInBytes的函数,其原型具体如下:
int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);
在实际开发中,强烈建议由该函数计算出需要传入的bufferSizeInBytes,而不是自己手动计算。
mode, AudioTrack提供了两种播放模式,可选的值以常量的形式定义在类AudioTrack中,一个是MODE_STATIC,需要一次性将所有的数据都写入播放缓冲区中,简单高效,通常用于播放铃声、系统提醒的音频片段;另一个是MODE_STREAM,需要按照一定的时间间隔不间断地写入音频数据,理论上它可以应用于任何音频播放的场景。
2.将AudioTrack切换到播放状态
首先判断AudioTrack实例是否初始化成功,如果当前状态处于初始化成功的状态,那么就调用它的play方法,并切换到播放状态,代码如下:
if (null ! = audioTrack && audioTrack.getState() ! = AudioTrack.STATE_UNINITIALIZED){
audioTrack.play();
}
3.开启播放线程
首先创建一个播放线程,代码如下:
playerThread = new Thread(new PlayerThread(), "playerThread");
playerThread.start();
接下来看看该线程中执行的任务,代码如下:
class PlayerThread implements Runnable {
private short[] samples;
public void run() {
samples = new short[minBufferSize];
while(! isStop) {
int actualSize = decoder.readSamples(samples);
audioTrack.write(samples, actualSize);
}
}
}
线程中的minBufferSize是在初始化AudioTrack的时候获得的缓冲区大小,会对其进行换算,即以2个字节表示一个采样的大小,也就是2倍的关系(因为初始化的时候是以字节为单位的); decoder是一个解码器,假设已经初始化成功,最后将调用write方法把从解码器中获得的PCM采样数据写入AudioTrack的缓冲区中,注意此方法是阻塞的方法,比如:一般要写入200ms的音频数据需要执行接近200ms的时间。
4.销毁资源
首先停止AudioTrack,代码如下:
if (null ! = audioTrack && audioTrack.getState() ! = AudioTrack.STATE_UNINITIALIZED)
{
audioTrack.stop();
}
然后停止线程:
isStop = true;
if (null ! = playerThread) {
playerThread.join();
playerThread = null;
}
最后释放AudioTrack:
audioTrack.release();
具体实例请参看代码仓库中的AudioPlayer项目的AudioTrack部分,需要把项目中resource目录下的音频文件放入目标设备的sdcard根目录下。