写点什么

android 端音频采集与播放

用户头像
floer rivor
关注
发布于: 2021 年 05 月 19 日

概述

上一篇文章讲到了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、启动

 

void    start()
复制代码

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

发布于: 2021 年 05 月 19 日阅读数: 55
用户头像

floer rivor

关注

还未添加个人签名 2021.04.24 加入

还未添加个人简介

评论

发布
暂无评论
android端音频采集与播放