01 前言
大家好,本文是 iOS/Android 音视频开发专题 的第七篇,该专题中 AVPlayer 项目代码将在 Github 进行托管,你可在微信公众号(GeekDev)后台回复 资料 获取项目地址。
在上篇文章 OpenGL ES 实现播放视频帧 中我们已经知道如何使用 GLSurfaceView 将解码后的视频渲染到屏幕上,但是,我们的播放器还不具备音频播放的功能,在本篇文章中我们将使用 AudioTrack 播放解码后的音频数据(PCM)。
本期内容:
- PCM 介绍
- AudioTrack API 介绍
- 使用 MediaCodec 解码及播放音频轨道
- 结束语
02 PCM 介绍
PCM (Pulse-code modulation 脉冲编码调制)是一种将模拟信号转为数字信号的方法。由于计算机只能识别数字信号,也就是一堆二进制序列,所以麦克风采集到的模拟信号会被模数转换器转换,生成数字信号。最常见的方式就是经过 PCM A/D 转换。
A/D 转换涉及到采样,量化和编码。
采样:由于存储空间有限,我们需要对模拟信号进行采样存储。采样就是从模拟信号进行抽样,抽样就涉及到采样频率,采样频率是每秒钟对声音样本的采样次数,采样率越高,声音质量越高,越能还原真实的声音。因此,我们一般称模拟信号是连续信号,数字信号为离散,不连续信号。
根据奈奎斯特理论,采样频率不低于音频信号最高频率的2倍,就可以无损的还原真实声音。
而由于人耳能听到的频率范围在 20Hz~20kHz,所以,为了保证声音不失真,采样频率我们一般设定为 40kHz 以上。常用的采样频率有 22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz。目前在 Android 设备中,只有 44.1kHz 是所有设备都支持的采样频率。
量化:模拟信号经过采样成为离散信号,离散信号经过量化成为数字信号。量化是将经过采样得到的离散数据转换成二进制数的过程,量化深度表示每个采样点用多少比特表示,在计算机中音频的量化深度一般为4、8、16、32位(bit)等。
量化深度的大小影响到声音的质量,显然,位数越多,量化后的波形越接近原始波形,声音的质量越高,而需要的存储空间也越多;位数越少,声音的质量越低,需要的存储空间越少。CD音质采用的是16 bits,移动通信 8bits。
另外,WAV 文件其实就是 PCM 格式,因为播放 PCM 裸流时,我们需要知道 PCM 的采样率, 声道数, 位宽等信息,WAV 只是在文件头前添加了这部分描述信息,所以 WAV 文件可以直接播放。
PCM 是音频处理中频繁接触的格式,通常我们对音频的处理都是基于 PCM 流,如常见的音量调节, 变声, 变调等特性。
03 AudioTrack API 介绍
在 Android 中,如果你想要播放一个音频文件,我们一般优先选用 MediaPlayer,使用 MediaPlayer 时你不需要关心文件的具体格式,也不需要对文件进行解码,使用 MediaPlayer 提供的 API,我们就可以开发出一个简单的音频播放器。
AudioTrack 是播放音频的另外一种方式 「如果你感兴趣还可以了解下 SoundPool」, 并且只能用于播放 PCM 数据。
AudioTrack API 概述 :
- AudioTrack 初始化
/**
* Class constructor.
* @param streamType 流类型
* @link AudioManager#STREAM_VOICE_CALL, 语音通话
* @link AudioManager#STREAM_SYSTEM, 系统声音 如低电量
* @link AudioManager#STREAM_RING, 来电铃声
* @link AudioManager#STREAM_MUSIC, 音乐播放器
* @link AudioManager#STREAM_ALARM, 警告音
* @link AudioManager#STREAM_NOTIFICATION 通知
*
* @param sampleRateInHz 采样率
*
* @param channelConfig 声道类型
* @link AudioFormat#CHANNEL_OUT_MONO 单声道
* @link AudioFormat#CHANNEL_OUT_STEREO 双声道
* @param audioFormat
* @link AudioFormat#ENCODING_PCM_16BIT,
* @link AudioFormat#ENCODING_PCM_8BIT,
* @link AudioFormat#ENCODING_PCM_FLOAT
* @param bufferSizeInBytes 缓冲区大小
* @param mode 模式
* @link #MODE_STATIC 静态模式 通过 write 将数据一次写入,适合较小文件
* @link #MODE_STREAM 流式模式 通过 write 分批写入,适合较大文件
*/
public AudioTrack (int streamType, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes, int mode)
初始化 AudioTrack 时的 bufferSizeInBytes 参数,可以通过 getMinBufferSize 计算算出合适的预估缓冲区大小,一般为 getMinBufferSize 的整数倍。
- 写入数据
/**
* @param audioData 保存要播放的数据的数组
* @param offsetInBytes 在要写入数据的audioData中以字节表示的偏移量
* @param sizeInBytes 在偏移量之后写入audioData的字节数。
**/
public int write(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes)
- 开始播放
public void play()
如果 AudioTrack 创建时的模式为 MODE_STATIC 时,调用 play 之前必须保证 write 方法已被调用。
- 暂停播放
public void pause()
暂停播放数据,尚未播放的数据不会被丢弃,再次调用 play 时将继续播放。
- 停止播放
public void stop()
停止播放数据,尚未播放的数据将会被丢弃。
- 刷新缓冲区数据
public void flush()
刷新当前排队等待播放的数据,已写入当未播放的数据将被丢弃,缓冲区将被清理。
04 MediaCodec 解码并播放音频轨道
如果我们要播放一个音频轨道,需要将音轨解码后才可以播放,之前我们一直在说如何解码视频,如果你看过 AVPlayer Demo ,你一定对如何创建视频轨道解码器很熟悉了,如果我们要解码一个音频轨道,只需要改下 mimeType 即可。创建一个音频轨道解码如下:
private void doDecoder() {
// step 1:创建一个媒体分离器
MediaExtractor extractor = new MediaExtractor();
// step 2:为媒体分离器装载媒体文件路径
// 指定文件路径
Uri videoPathUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.demo_video);
try
{
extractor.setDataSource(this, videoPathUri, null);
}
catch(IOException e) {
e.printStackTrace();
}
// step 3:获取并选中指定类型的轨道
// 媒体文件中的轨道数量 (一般有视频,音频,字幕等)
int trackCount = extractor.getTrackCount();
// mime type 指示需要分离的轨道类型 指定为音频轨道
String extractMimeType = "audio/";
MediaFormat trackFormat = null;
// 记录轨道索引id,MediaExtractor 读取数据之前需要指定分离的轨道索引
int trackID = -1;
for(int i = 0; i < trackCount; i++) {
trackFormat = extractor.getTrackFormat(i);
if(trackFormat.getString(MediaFormat.KEY_MIME).startsWith(extractMimeType))
{
trackID = i;
break;
}
}
// 媒体文件中存在视频轨道
// step 4:选中指定类型的轨道
if(trackID != -1)
extractor.selectTrack(trackID);
// step 5:根据 MediaFormat 创建解码器
MediaCodec mediaCodec = null;
try
{
mediaCodec = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME));
mediaCodec.configure(trackFormat,null,null,0);
mediaCodec.start();
}
catch(IOExceptione) {
e.printStackTrace();
}
while (mRuning) {
// step 6: 向解码器喂入数据
boolean ret = feedInputBuffer(extractor,mediaCodec);
// step 7: 从解码器吐出数据
boolean decRet = drainOutputBuffer(mediaCodec);
if(!ret && !decRet) break;
}
// step 8: 释放资源
// 释放分离器,释放后 extractor 将不可用
extractor.release();
// 释放解码器
mediaCodec.release();
new Handler(LoopergetMainLooper()).post(new Runnable() {
@Override
public void run() {
mPlayButton.setEnabled(true);
mInfoTextView.setText("解码完成");
}
});
}
解码音频时我们将 extractMimeType 设定为 "audio/" ,其它代码与解码视频时相同。
接着我们监听到 INFO_OUTPUT_FORMAT_CHANGED 状态时,获取该音频轨道的格式信息, MediaFormat 提供了足够的信息可以让我们初始化 AudioTrack。
// 从 MediaCodec 吐出解码后的音频信息
private boolean drainOutputBuffer(MediaCodec mediaCodec) {
if (mediaCodec == null) return false;
final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int outIndex = mediaCodec.dequeueOutputBuffer(info, 0);
if ((info.flags & BUFFER_FLAG_END_OF_STREAM) != 0)
{
mediaCodec.releaseOutputBuffer(outIndex, false);
return false ;
}
switch (outIndex) {
case
INFO_OUTPUT_BUFFERS_CHANGED: return true
case
INFO_TRY_AGAIN_LATER: return true;
case
INFO_OUTPUT_FORMAT_CHANGED: {
MediaFormat outputFormat = mediaCodec.getOutputFormat();
int sampleRate = 44100;
if (outputFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE))
sampleRate = outputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
if
(outputFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT))
channelConfig = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
if (outputFormat.containsKey("bit-width"))
audioFormat = outputFormat.getInteger("bit-width") == 8 ? AudioFormat.ENCODING_PCM_8BIT : AudioFormat.ENCODING_PCM_16BIT;
mBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat) * 2;
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,sampleRate,channelConfig,audioFormat,mBufferSize,AudioTrack.MODE_STREAM);
mAudioTrack.play();
return true;
}
default:
{
if (outIndex >= 0 && info.size > 0)
{
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
bufferInfo.presentationTimeUs = info.presentationTimeUs;
bufferInfo.size = info.size;
bufferInfo.flags = info.flags;
bufferInfo.offset = info.offset;
ByteBuffer outputBuffer = mediaCodec.getOutputBuffers()[outIndex];
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
byte[] audioData = new byte[bufferInfo.size];
outputBuffer.get(audioData);
// 写入解码后的音频数据
mAudioTrack.write(audioData,bufferInfo.offset,Math.min(bufferInfo.size, mBufferSize));
// 释放
mediaCodec.releaseOutputBuffer(outIndex, false);
return true;
}
}
}
当我们通过 INFO_OUTPUT_FORMAT_CHANGED 获取到 MediaFormat 并初始化 AudioTrack 后,就可以通过 write 方法写入解码后的音频数据。
详见: DemoAudioTrackPlayerActivity
05 结束语
关注 GeekDev 公众号获取首发内容。如果你想了解更多信息,可关注微信公众号 (GeekDev) 并回复 资料 获取。
往期内容:
iOS/Android 音视频开发专题介绍
iOS/Android 音视频概念介绍
MediaCodec/OpenMAX/StageFright 介绍
使用 MediaCodec 解码音视频
OpenGL ES for Android 世界
OpenGL ES 与 GlSurfaceView 渲染音视频
下期预告:
《 AVPlayer 添加音效 》