Audio 是整个 Android 平台非常重要的一个组成部分,负责音频数据的采集和输出、音频流的控制、音频设备的管理、音量调节等,主要包括如下部分:
与 Audio 强相关的有 MultiMedia,MultiMedia 负责音视频的编解码,MultiMedia 将解码后的数据通过 AudioTrack 输出,而 AudioRecord 采集的录音数据交由 MultiMedia 进行编码。
AudioTrack Java API 两种数据传输模式:
Transfer Mode | Description |
---|---|
MODE_STATIC | 应用进程将回放数据一次性付给 AudioTrack,适用于数据量小、时延要求高的场景 |
MODE_STREAM | 用进程需要持续调用 write() 写数据到 FIFO,写数据时有可能遭遇阻塞(等待 AudioFlinger::PlaybackThread 消费之前的数据),基本适用所有的音频场景 |
audio playback sample
int minBuffSize = AudioTrack.getMinBufferSize(22050, CHANNEL_OUT_STEREO, ENCODING_PCM_16BIT);
AudioTrack track = new AudioTrack(STREAM_MUSIC, 22050, CHANNEL_OUT_STEREO, ENCODING_PCM_16BIT, minBuffSize, MODE_STREAM);
byte data[] = new byte[minBuffSize/2];
track.write(data, 0, data.length);
track.play();
float maxVol = AudioTrack.getMaxVolume();
track.release();
最小缓冲区的大小 = 最低帧数 * 声道数 * 采样深度,(采样深度以字节为单位)
AudioTrack Native API 四种数据传输模式:
Transfer Mode | Description |
---|---|
TRANSFER_CALLBACK | 在 AudioTrackThread 线程中通过 audioCallback 回调函数主动从应用进程那里索取数据,ToneGenerator 采用这种模式 |
TRANSFER_OBTAIN | 应用进程需要调用 obtainBuffer()/releaseBuffer() 填充数据,目前我还没有见到实际的使用场景 |
TRANSFER_SYNC | 应用进程需要持续调用 write() 写数据到 FIFO,写数据时有可能遭遇阻塞(等待 AudioFlinger::PlaybackThread 消费之前的数据),基本适用所有的音频场景;对应于 AudioTrack Java API 的 MODE_STREAM 模式 |
TRANSFER_SHARED | 应用进程将回放数据一次性付给 AudioTrack,适用于数据量小、时延要求高的场景;对应于 AudioTrack Java API 的 MODE_STATIC 模式 |
AudioTrack Native API 音频流类型:
Stream Type | Description |
---|---|
AUDIO_STREAM_VOICE_CALL | 电话语音 |
AUDIO_STREAM_SYSTEM | 系统声音 |
AUDIO_STREAM_RING | 铃声声音,如来电铃声、闹钟铃声等 |
AUDIO_STREAM_MUSIC | 音乐声音 |
AUDIO_STREAM_ALARM | 警告音 |
AUDIO_STREAM_NOTIFICATION | 通知音 |
AUDIO_STREAM_DTMF | DTMF 音(拨号盘按键音) |
AudioTrack Native API 输出标识:
AUDIO_OUTPUT_FLAG | Description |
---|---|
AUDIO_OUTPUT_FLAG_DIRECT | 表示音频流直接输出到音频设备,不需要软件混音,一般用于 HDMI 设备声音输出 |
AUDIO_OUTPUT_FLAG_PRIMARY | 表示音频流需要输出到主输出设备,一般用于铃声类声音 |
AUDIO_OUTPUT_FLAG_FAST | 表示音频流需要快速输出到音频设备,一般用于按键音、游戏背景音等对时延要求高的场景 |
AUDIO_OUTPUT_FLAG_DEEP_BUFFER | 表示音频流输出可以接受较大的时延,一般用于音乐、视频播放等对时延要求不高的场景 |
AUDIO_OUTPUT_FLAG_COMPRESS_OFFLOAD | 表示音频流没有经过软件解码,需要输出到硬件解码器,由硬件解码器进行解码 |
我们根据不同的播放场景,使用不同的输出标识,如按键音、游戏背景音对输出时延要求很高,那么就需要置
AUDIO_OUTPUT_FLAG_FAST,具体可以参考 ToneGenerator、SoundPool 和 OpenSL ES。
首先要了解音频领域中,帧(frame)的概念:帧表示一个完整的声音单元,所谓的声音单元是指一个采样样本;
如果是双声道,那么一个完整的声音单元就是 2 个样本,如果是 5.1 声道,那么一个完整的声音单元就是 6 个样本了。
帧的大小(一个完整的声音单元的数据量)等于声道数乘以采样深度,即 frameSize = channelCount * bytesPerSample
。
帧的概念非常重要,无论是框架层还是内核层,都是以帧为单位去管理音频数据缓冲区的。
其次还得了解音频领域中,传输延迟(latency)的概念:传输延迟表示一个周期的音频数据的传输时间。
可能有些读者一脸懵逼,一个周期的音频数据,这又是啥?我们再引入周期(period)的概念:
Linux ALSA 把数据缓冲区划分为若干个块,dma 每传输完一个块上的数据即发出一个硬件中断,cpu 收到中断信号后,
再配置 dma 去传输下一个块上的数据;一个块即是一个周期,周期大小(periodSize)即是一个数据块的帧数。
再回到传输延迟(latency),传输延迟等于周期大小除以采样率,即 latency = periodSize / sampleRate
。
最后了解下音频重采样:音频重采样是指这样的一个过程——把一个采样率的数据转换为另一个采样率的数据。
Android 原生系统上,音频硬件设备一般都工作在一个固定的采样率上(如 48 KHz),因此所有音轨数据都需要重采样
到这个固定的采样率上,然后再输出。为什么这么做?系统中可能存在多个音轨同时播放,而每个音轨的采样率可能是不一致的;
比如在播放音乐的过程中,来了一个提示音,这时需要把音乐和提示音混音并输出到硬件设备,而音乐的采样率和提示音的
采样率不一致,问题来了,如果硬件设备工作的采样率设置为音乐的采样率的话,那么提示音就会失真;
因此最简单见效的解决方法是:硬件设备工作的采样率固定一个值,所有音轨在 AudioFlinger 都重采样到这个采样率上,
混音后输出到硬件设备,保证所有音轨听起来都不失真。
重采样计算的:afFrameCount 是硬件设备处理单个数据块的帧数,afSampleRate 是硬件设备配置的采样率,sampleRate 是音轨的采样率,
如果要把音轨数据重采样到 afSampleRate 上,那么反推算出应用程序最少传入的帧数为 afFrameCount * sampleRate / afSampleRate,
而为了播放流畅,实际上还要大一点,所以再乘以一个系数(可参照 framebuffer 双缓冲,一个缓冲缓存当前的图像,一个缓冲准备下一幅的图像,
这样图像切换更流畅),然后就得出一个可以保证播放流畅的最低帧数
minFrameCount = (afFrameCount * sampleRate / afSampleRate) * minBufCount
。
几个重要的类
AudioFlinger 对外提供的主要的服务接口如下:
Interface | Description |
---|---|
sampleRate | 获取硬件设备的采样率 |
format | 获取硬件设备的音频格式 |
frameCount | 获取硬件设备的周期帧数 |
latency | 获取硬件设备的传输延迟 |
setMasterVolume | 调节主输出设备的音量 |
setMasterMute | 静音主输出设备 |
setStreamVolume | 调节指定类型的音频流的音量,这种调节不影响其他类型的音频流的音量 |
setStreamMute | 静音指定类型的音频流 |
setVoiceVolume | 调节通话音量 |
setMicMute | 静音麦克风输入 |
setMode | 切换音频模式:音频模式有 4 种,分别是 Normal、Ringtone、Call、Communicatoin |
setParameters | 设置音频参数:往下调用 HAL 层相应接口,常用于切换音频通道 |
getParameters | 获取音频参数:往下调用 HAL 层相应接口 |
openOutput | 打开输出流:打开输出流设备,并创建 PlaybackThread 对象 |
closeOutput | 关闭输出流:移除并销毁 PlaybackThread 上面挂着的所有的 Track,退出 PlaybackThread,关闭输出流设备 |
openInput | 打开输入流:打开输入流设备,并创建 RecordThread 对象 |
closeInput | 关闭输入流:退出 RecordThread,关闭输入流设备 |
createTrack | 新建输出流管理对象: 找到对应的 PlaybackThread,创建输出流管理对象 Track,然后创建并返回该 Track 的代理对象 TrackHandle |
openRecord | 新建输入流管理对象:找到 RecordThread,创建输入流管理对象 RecordTrack,然后创建并返回该 RecordTrack 的代理对象 RecordHandle |
可以归纳出 AudioFlinger 响应的服务请求主要有:
从 Audio HAL 中,我们通常看到如下 4 种输出流设备,分别对应着不同的播放场景:
其中 primary_out 设备是必须声明支持的,而且系统启动时就已经打开 primary_out 设备并创建好对应的 MixerThread 实例。其他类型的输出流设备并非必须声明支持的,主要是看硬件上有无这个能力。
可能有人产生这样的疑问:既然 primary_out 设备一直保持打开,那么能耗岂不是很大?这里阐释一个概念:输出流设备属于逻辑设备,并不是硬件设备。所以即使输出流设备一直保持打开,只要硬件设备不工作,那么就不会影响能耗。那么硬件设备什么时候才会打开呢?答案是 PlaybackThread 将音频数据写入到输出流设备时。
最后看个金典的架构