概述
上一篇文章讲到了ios端音频播放和采集,在 android 端使用音频播放采集的逻辑也是一样的,只是调用的 api 不同,以及 ios 和 android 在音频硬件上处理的不同。android 端音频播放采集可以使用 java 层的 api,也可以使用 openSL ES 这 c 层面的 api。java 层的 api 在处理音频时,都会通过 jni 调用 native 层的 c 语言接口,而 openSL ES 支持在 native 层直接处理音频数据。考虑到 android 机型的碎片化,各个机型的采集播放时延不尽相同,为了达到业务要求的时延,可能会 java 层的 api 和 openSL ES 搭配起来混合使用,这一点在 webrtc 中也有体现,在这里不做过多介绍了。这次先介绍 java 层的音频采集播放,关于 openSL ES 在后面会补上。在 android 端,常用的音频相关 API 为:
1)AudioRecord:音频 PCM 数据采集
2)AudioTrack:音频 PCM 数据播放
3)MediaCodec:音频编解码
4)MediaExtractor:音频数据的提取
5)AudioManager:音频音量与铃声控制
还有一些较高级的 API 如 MediaPlayer 在这里就不做介绍了。
音频采集 AudioRecord
使用 AudioRecord 的步骤为
1)配置一个 AudioRecord 对象
AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
复制代码
其中比较重要的是 audioSource 和 bufferSizeInBytes 两个参数。audioSource 为采集数据来源,一般设置为 MediaRecorder.AudioSource.MIC,bufferSizeInBytes 为 AudioRecord 内部缓冲区大小,该缓冲区很重要,设置过小导致会出现声音不连续,过大会导致延时过大,一般由以下函数得到:
AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
复制代码
然后判断当前 AudioRecord 是否初始化成功
if (mAudioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
Log.e(TAG, "AudioRecord initialize fail !");
return false;
}
复制代码
2)开启 AudioRecord 采集声音
mAudioRecord.startRecording();
复制代码
3)读取 AudioRecord 采集到的声音
AudioRecord 采集声音应该放到一个采集线程中,以防阻塞主线程。线程函数实现为
private class AudioCaptureRunnable implements Runnable {
@Override
public void run() {
try {
while (!mIsLoopExit) {
byte[] buffer = new byte[mMinBufferSize];
int ret = mAudioRecord.read(byte, 0, mMinBufferSize);
if (ret == AudioRecord.ERROR_INVALID_OPERATION) {
Log.e(TAG, "Error ERROR_INVALID_OPERATION");
} else if (ret == AudioRecord.ERROR_BAD_VALUE) {
Log.e(TAG, "Error ERROR_BAD_VALUE");
} else {
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
复制代码
4)停止录音
首先判断当前状态,然后释放 audioRecord.停止并且采集线程。注意这里应该先停止线程再进行释放,不然会出现释放 audioRecord 后线程还在使用 audiorecord 引起程序崩溃。
mIsLoopExit = true;
if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
mAudioRecord.stop();
}
mAudioRecord.release();
复制代码
当 android 多个 app 进行录音时,在andriod开发文档上是这样描述的:在 Android 10 之前,输入音频流一次只能由一个应用捕获。如果已有应用在录制或侦听音频,则您的应用可以创建一个 AudioRecord
对象,但系统会在您用 AudioRecord.startRecording() 时返回错误,并且不会开始录制。之前的行为是“先到先得”。应用开始捕获音频后,所有其他应用在捕获应用停止之前均无法访问音频输入。Android 10 (API 级别 29) 或更高版本 采用优先级方案,可以在运行的应用之间切换输入音频流。在大多数情况下,如果新应用获取音频输入,则之前的捕获应用将继续运行,但会受到静默处理。在某些情况下,系统可以继续向这两个应用传送音频。
音频播放 AudioTrack
使用 AudioTrack 的步骤基本和 AudioRecord 一样,为
1)初始化 AudioTrack
AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)
复制代码
同样 bufferSizeInBytes 通过 getMinBufferSize 得到
AudioTrack.getMinBufferSize(sampleRateInHz,channelConfig,audioFormat);
复制代码
判断是否初始化成功
if (mAudioTrack.getState() == AudioTrack.STATE_UNINITIALIZED) {
Log.e(TAG, "AudioTrack initialize fail !");
return false;
}
复制代码
2)在播放线程中向 AudioTrack 写入数据
private class AudioPlayerRunnable implements Runnable {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
try {
while (!mIsLoopExit) {
// mByteBuffer为要写入的数据
mByteBuffer.rewind();
int ret = mAudioTrack.write(mByteBuffer, num, AudioTrack.WRITE_BLOCKING);
if ( ret < 0) {
Log.e(TAG, "audiotrack write data fial %d :"+ ret);
mIsLoopExit = true;
continue;
}
mAudioTrack.play();
Log.d(TAG , "OK, Played "+ret+" bytes !");
mByteBuffer.clear();
}
} catch (Exception e)
{
e.printStackTrace();
}
}
}
复制代码
3)释放 AudioTrack
mIsLoopExit = false;
if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
mAudioTrack.stop();
}
mAudioTrack.release();
复制代码
需要注意的一点事,android 由于不同厂商有不同的硬件,AudioTrack 和 AudioRecord 的每次回调时间在每个机型上总不是一样的。 AudioTrack 和 AudioRecord 不能保证每一次回调间隔为 10ms 左右,android 中 AudioTrack 和 AudioRecord 的回调时间可能前 9 次都是间隔 1ms 甚至 0ms,在第 10 次的回调时间可能为 100ms,那么这 10 次的平均间隔为 10ms。
音频编解码 MediaCodec
MediaCodec 是一个 Codec,通过硬件加速解码和编码。它为芯片厂商和应用开发者搭建了一个统一接口。MediaCodec 在android开发者文档中有很详细的讲解,我这里当一下搬运工。MediaCodec 采用异步方式处理数据,并且使用了一组输入输出 buffer。
上示图中的处理过程如下:
1、 使用者 Client 从 MediaCodec 请求一个空的输入 ByteBuffer(dequeueInputBuffer 和 getInputBuffer),填充满数据后将它传递给 MediaCodec 处理(queueInputBuffer)
2、 MediaCodec 处理完这些数据并将处理结果输出至一个空的输出 ByteBuffer 中。(异步处理,没有对应的函数)
3、 使用者从 MediaCodec 获取输出 buffer(dequeueOutputBuffer),并取得其中数据(getOutputBuffer),使用完输出 buffer 的数据之后,将其释放回编解码器(releaseOutputBuffer)
MediaCodec 的生命周期有三种状态:Stopped、Executing、Released。每种状态之间的切换为
MediaCodec 有同步和异步的 API,
同步 API 处理流程为:
- 创建并配置 MediaCodec 对象。
- 循环直到完成:
- 如果输入 buffer 准备好了:
- 读取一段输入,将其填充到输入 buffer 中
- 如果输出 buffer 准备好了:
- 从输出 buffer 中获取数据进行处理。
- 处理完毕后,release MediaCodec 对象。
异步 API 处理流程为:
- 创建并配置 MediaCodec 对象。
- 给 MediaCodec 对象设置回调 MediaCodec.Callback
- 在 onInputBufferAvailable 回调中:
- 读取一段输入,将其填充到输入 buffer 中
- 在 onOutputBufferAvailable 回调中:
- 从输出 buffer 中获取数据进行处理。
- 处理完毕后,release MediaCodec 对象。
Mecodec 使用流程及其主要 API
1、创建
static MediaCodec createDecoderByType (String type);
static MediaCodec createEncoderByType(String type);
static MediaCodec createByCodecName(String name);
复制代码
2、配置
//第二个参数是设置surface,用来在其上绘制解码器解码出的数据;第三个参数于数据加密有关;第四个参数上1表示编码器,0是否表示解码器呢
void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags);
复制代码
3、启动
4、输入数据
1)寻找输入可用的 buffer
int dequeueInputBuffer(long timeoutUs);
复制代码
2)给可用 buffer 输入数据
void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags)
复制代码
5、输出数据
1)新建一个输出数据缓存对象
MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();
复制代码
2)寻找输出可用 buffer
int dequeueOutputBuffer(MediaCodec.BufferInfo info, long timeoutUs)
复制代码
3)向申请好的输出数据缓存对象输出输出
ByteBuffer getOutputBuffer(int index);
int dequeueOutputBuffer(MediaCodec.BufferInfo info, long timeoutUs)
复制代码
4)拷贝走数据后释放输出的 bytebuffer
void releaseOutputBuffer(int index, boolean render)
复制代码
6、释放 MediaCodec
void flush();
void stop();
void release();
复制代码
支持的编解码类型
MediaCodecList 可用于获取设备支持的编解码器的名字、能力,以查找合适的编解码器。
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);//REGULAR_CODECS参考api说明
MediaCodecInfo[] codecs = list.getCodecInfos();
Log.d(TAG, "Decoders: ");
for (MediaCodecInfo codec : codecs) {
if (codec.isEncoder())
continue;
Log.d(TAG, codec.getName());
}
Log.d(TAG, "Encoders: ");
for (MediaCodecInfo codec : codecs) {
if (codec.isEncoder())
Log.d(TAG, codec.getName());
}
复制代码
MediaExtractor
MediaExtractor 多用于从数据源中提取出解复用,编码后的媒体数据,MediaExtractor 可以设置源为本地文件和网络文件。MediaExtractor 一般和 MediaCodec 配合起来使用。
MediaExtractor 的常用 API 为
setDataSource(String path):即可以设置本地文件又可以设置网络文件
getTrackCount():得到源文件通道数
getTrackFormat(int index):获取指定(index)的通道格式
//选定特定的轨道,会影响 readSampleData(ByteBuffer, int), getSampleTrackIndex() and getSampleTime()的输出,这三个函数输出的是选定轨道的信息
selectTrack(int index)
unselectTrack(int index)
getSampleTime():返回当前的时间戳
readSampleData(ByteBuffer byteBuf, int offset):把指定通道中的数据按偏移量读取到ByteBuffer中;
advance():读取下一帧数据
release(): 读取结束后释放资源
seekTo(long timeUs, int mode): seek到指定位置
复制代码
我们使用 MediaExtractor 的 API 常用流程为,代码中有纤细的注释
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(...); //设置源
int numTracks = extractor.getTrackCount(); //获取track个数
for (int i = 0; i < numTracks; ++i) {
MediaFormat format = extractor.getTrackFormat(i); //得到track格式
String mime = format.getString(MediaFormat.KEY_MIME);
if (weAreInterestedInThisTrack) {
extractor.selectTrack(i); //选取感兴趣的track
}
}
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
while (extractor.readSampleData(inputBuffer, ...) >= 0) { //读取数据
int trackIndex = extractor.getSampleTrackIndex();
long presentationTimeUs = extractor.getSampleTime();
...
extractor.advance(); //读取下一帧
}
extractor.release(); //释放
extractor = null;
复制代码
AudioManager
AudioManager 类提供了访问音量和振铃器 mode 控制。使用 Context.getSystemService(Context.AUDIO_SERVICE)来得到这个类的一个实例。
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
复制代码
常用的方法有:
setSpeakerphoneOn(boolean on):设置是否打开扩音器
setMicrophoneMute(boolean on):设置是否让麦克风静音
adjustVolume(int direction, int flags): 控制手机音量,调大或者调小一个单位,根据第一个参数进行判断 AudioManager.ADJUST_LOWER,可调小一个单位; AudioManager.ADJUST_RAISE,可调大一个单位
setMode( ):设置声音模式 有下述几种模式:
MODE_NORMAL(普通,livebroast),
MODE_RINGTONE(铃声),
MODE_IN_CALL(打电话),
MODE_IN_COMMUNICATION(通话,voip)
android 监控音频状态变化
监控音量变化
当 android 音量变化时,会发出一条系统广播。该广播为 AudioManager.VOLUME_CHANGED_ACTION, 监听该广播可以通知程序音量发生变化。AudioManager.VOLUME_CHANGED_ACTION 被隐藏,所以直接用 "android.media.VOLUME_CHANGED_ACTION"
监控路由状态改变
耳机的插拔会产生 Intent.actiion.ACTION_HEADSET_PLUG 系统广播,通过监听该广播监听耳机的插拔。
蓝牙 耳机的连接需要监听 BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED,BluetoothAdapter.ACTION_STATE_CHANGED,AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED,
BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED4 个系统广播。
这里介绍一下蓝牙链路分两种同步链路(SCO)和异步链路(ACL)。
A2DP(Advanced Audio Distribution Profile 高级音频传输模型)是跑在 ACL 链路上去高品质音频协议。A2DP 定义了 ACL(Asynchronous Connectionless 异步无连接)信道上传送单声道或立体声等高质量音 A2DP 功能频信息的协议和过程。
蓝牙物理链路 SCO(Synchronous Connection Oriented)主要用来传输对时间要求很高的数据通信。
BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:监听蓝牙耳机连接状态。共四种状态
public static final int STATE_DISCONNECTED = 0;
/** The profile is in connecting state */
public static final int STATE_CONNECTING = 1;
/** The profile is in connected state */
public static final int STATE_CONNECTED = 2;
/** The profile is in disconnecting state */
public static final int STATE_DISCONNECTING = 3;
复制代码
BluetoothAdapter.ACTION_STATE_CHANGED:监听手机蓝牙的打开状态。共四种状态:
/**
* Indicates the local Bluetooth adapter is off.
*/
public static final int STATE_OFF = 10;
/**
* Indicates the local Bluetooth adapter is turning on. However local
* clients should wait for {@link #STATE_ON} before attempting to
* use the adapter.
*/
public static final int STATE_TURNING_ON = 11;
/**
* Indicates the local Bluetooth adapter is on, and ready for use.
*/
public static final int STATE_ON = 12;
/**
* Indicates the local Bluetooth adapter is turning off. Local clients
* should immediately attempt graceful disconnection of any remote links.
*/
复制代码
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED:监听蓝牙设备是否打开 SCO。共四种状态:
/**
* Value for extra EXTRA_SCO_AUDIO_STATE or EXTRA_SCO_AUDIO_PREVIOUS_STATE
* indicating that the SCO audio channel is not established
*/
public static final int SCO_AUDIO_STATE_DISCONNECTED = 0;
/**
* Value for extra {@link #EXTRA_SCO_AUDIO_STATE} or {@link #EXTRA_SCO_AUDIO_PREVIOUS_STATE}
* indicating that the SCO audio channel is established
*/
public static final int SCO_AUDIO_STATE_CONNECTED = 1;
/**
* Value for extra EXTRA_SCO_AUDIO_STATE or EXTRA_SCO_AUDIO_PREVIOUS_STATE
* indicating that the SCO audio channel is being established
*/
public static final int SCO_AUDIO_STATE_CONNECTING = 2;
/**
* Value for extra EXTRA_SCO_AUDIO_STATE indicating that
* there was an error trying to obtain the state
*/
public static final int SCO_AUDIO_STATE_ERROR = -1;
复制代码
BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED:监听 A2DP 下音频状态变化。
小结
本文介绍了 android 音频采集播放常用的 api,调用流程大致为 MediaExtractor 解复用音频数据,MediaCodec 进行编解码,AudioTrack、AudioRecord 进行播放,采集。AudioManager 对音量等进行一些控制。本文主要介绍了这些类常用的 api,关于开发中对于这些类的深入使用功能以及使用中避免踩到那些坑,会慢慢进行补充。
参考
https://www.runoob.com/w3cnote/android-tutorial-audiomanager.html
https://developer.android.com/reference/android/media/MediaCodec.html
https://blog.csdn.net/pashanhu6402/article/details/73549469
https://blog.51cto.com/ticktick/1750593
AudioRecord中buffer设置大小:http://www.voidcn.com/article/p-abhwtdhp-bwp.html
开发者文档-android共享音频输入
开发者文档:https://developer.android.com/reference/android/media/AudioTrack
评论