ffmpeg开发播放器学习笔记 - 解码音频,使用AudioQueue 播放

1,479 阅读15分钟

该节是ffmpeg开发播放器学习笔记的第六节《ffmpeg解码音频,使用AudioQueue 播放》

ffmpeg音频解码后的数据是PCM(Pulse Code Modulation,脉冲编码调制)音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准数字音频数据。对于我们最常说的“无损音频”来说,一般都是指传统CD格式中的16bit/44.1kHz采样率的文件格式,而知所以称为无损压缩,也是因为其包含了20Hz-22.05kHz这个完全覆盖人耳可闻范围的频响频率而得名,当然现在的各种PCM格式编码高码率文件已经层出不穷非常常见,但是就像上文中所说的,高码率并不能有效地提升PCM编码采样率的频响范围,而只能增加其采样点来得到更加类似模拟录音的平滑波形。
PCM音频格式的播放在macOS/iOS平台可以使用AudioQueueu与AudioUint来播放,本节采用了AudioQueue。

✅ 第一节 - Hello FFmpeg
✅ 第二节 - 软解视频流,渲染 RGB24
✅ 第三节 - 认识YUV
✅ 第四节 - 硬解码,OpenGL渲染YUV
✅ 第五节 - Metal 渲染YUV
🔔 第六节 - 解码音频,使用AudioQueue 播放
📗 第七节 - 音视频同步
📗 第八节 - 完善播放控制
📗 第九节 - 倍速播放
📗 第十节 - 增加视频过滤效果
📗 第十一节 - 音频变声

该节 Demo 地址: github.com/czqasngit/f…
实例代码提供了Objective-CSwift两种实现,为了方便说明,文章引用的是Objective-C代码,因为Swift代码指针看着不简洁。
该节最终效果如下图:

目标

  • 了解PCM
  • 了解ffmpeg解码同时播放音频与视频的流程
  • 了解AudioQueue播放流程
  • 完成ffmpeg解码后的音频与视频同时播放

了解PCM

采样率和采样大小

声音其实是一种能量波,因此也有频率和振幅的特征,频率对应于时间轴线,振幅对应于电平轴线。波是无限光滑的,弦线可以看成由无数点组成,由于存储空间是相对有限的,数字编码过程中,必须对弦线的点进行采样。采样的过程就是抽取某点的频率值,很显然,在一秒中内抽取的点越多,获取得频率信息更丰富,为了复原波形,一次振动中,必须有2个点的采样,人耳能够感觉到的最高频率为20kHz,因此要满足人耳的听觉要求,则需要至少每秒进行40k次采样,用40kHz表达,这个40kHz就是采样率。我们常见的CD,采样率为44.1kHz。光有频率信息是不够的,我们还必须获得该频率的能量值并量化,用于表示信号强度。量化电平数为2的整数次幂,我们常见的CD位16bit的采样大小,即2的16次方。采样大小相对采样率更难理解,因为要显得抽象点,举个简单例子:假设对一个波进行8次采样,采样点分别对应的能量值分别为A1-A8,但我们只使用2bit的采样大小,结果我们只能保留A1-A8中4个点的值而舍弃另外4个。如果我们进行3bit的采样大小,则刚好记录下8个点的所有信息。采样率和采样大小的值越大,记录的波形更接近原始信号。

有损和无损

根据采样率和采样大小可以得知,相对自然界的信号,音频编码最多只能做到无限接近,至少目前的技术只能这样了,相对自然界的信号,任何数字音频编码方案都是有损的,因为无法完全还原。在计算机应用中,能够达到最高保真水平的就是PCM编码,被广泛用于素材保存及音乐欣赏,CD、DVD以及我们常见的WAV文件中均有应用。因此,PCM约定俗成了无损编码,因为PCM代表了数字音频中最佳的保真水准,并不意味着PCM就能够确保信号绝对保真,PCM也只能做到最大程度的无限接近。我们而习惯性的把MP3列入有损音频编码范畴,是相对PCM编码的。强调编码的相对性的有损和无损,是为了告诉大家,要做到真正的无损是困难的,就像用数字去表达圆周率,不管精度多高,也只是无限接近,而不是真正等于圆周率的值。

使用音频压缩技术的原因

要算一个PCM音频流的码率是一件很轻松的事情,采样率值×采样大小值×声道数 bps。一个采样率为44.1KHz,采样大小为16bit,双声道的PCM编码的WAV文件,它的数据速率则为 44.1K×16×2 =1411.2 Kbps。我们常说128K的MP3,对应的WAV的参数,就是这个1411.2 Kbps,这个参数也被称为数据带宽,它和ADSL中的带宽是一个概念。将码率除以8,就可以得到这个WAV的数据速率,即176.4KB/s。这表示存储一秒钟采样率为44.1KHz,采样大小为16bit,双声道的PCM编码的音频信号,需要176.4KB的空间,1分钟则约为10.34M,这对大部分用户是不可接受的,尤其是喜欢在电脑上听音乐的朋友,要降低磁盘占用,只有2种方法,降低采样指标或者压缩。降低指标是不可取的,因此专家们研发了各种压缩方案。由于用途和针对的目标市场不一样,各种音频压缩编码所达到的音质和压缩比都不一样,在后面的文章中我们都会一一提到。有一点是可以肯定的,他们都压缩过。

频率与采样率的关系

采样率表示了每秒对原始信号采样的次数,我们常见到的音频文件采样率多为44.1KHz,这意味着什么呢?假设我们有2段正弦波信号,分别为20Hz和20KHz,长度均为一秒钟,以对应我们能听到的最低频和最高频,分别对这两段信号进行40KHz的采样,我们可以得到一个什么样的结果呢?结果是:20Hz的信号每次振动被采样了40K/20=2000次,而20K的信号每次振动只有2次采样。显然,在相同的采样率下,记录低频的信息远比高频的详细。这也是为什么有些音响发烧友指责CD有数码声不够真实的原因,CD的44.1KHz采样也无法保证高频信号被较好记录。要较好的记录高频信号,看来需要更高的采样率,于是有些朋友在捕捉CD音轨的时候使用48KHz的采样率,这是不可取的!这其实对音质没有任何好处,对抓轨软件来说,保持和CD提供的44.1KHz一样的采样率才是最佳音质的保证之一,而不是去提高它。较高的采样率只有相对模拟信号的时候才有用,如果被采样的信号是数字的,请不要去尝试提高采样率。 流特征 随着网络的发展,人们对在线收听音乐提出了要求,因此也要求音频文件能够一边读一边播放,而不需要把这个文件全部读出后然后回放,这样就可以做到不用下载就可以实现收听了;也可以做到一边编码一边播放,正是这种特征,可以实现在线的直播,架设自己的数字广播电台成为了现实。

编码格式

PCM编码

PCM 脉冲编码调制是Pulse Code Modulation的缩写。前面的文字我们提到了PCM大致的工作流程,我们不需要关心PCM最终编码采用的是什么计算方式,我们只需要知道PCM编码的音频流的优点和缺点就可以了。PCM编码的最大的优点就是音质好,最大的缺点就是体积大。我们常见的Audio CD就采用了PCM编码,一张光盘的容量只能容纳72分钟的音乐信息。

WAV格式

这是一种古老的音频文件格式,由微软开发。WAV是一种文件格式,符合RIFF (Resource Interchange File Format) 规范。所有的WAV都有一个文件头,这个文件头包含了音频流的编码参数。WAV对音频流的编码没有硬性规定,除了PCM之外,还有几乎所有支持ACM规范的编码都可以为WAV的音频流进行编码。很多朋友没有这个概念,我们拿AVI做个示范,因为AVI和WAV在文件结构上是非常相似的,不过AVI多了一个视频流而已。我们接触到的AVI有很多种,因此我们经常需要安装一些Decode才能观看一些AVI,我们接触到比较多的DivX就是一种视频编码,AVI可以采用DivX编码来压缩视频流,当然也可以使用其他的编码压缩。同样,WAV也可以使用多种音频编码来压缩其音频流,不过我们常见的都是音频流被PCM编码处理的WAV,但这不表示WAV只能使用PCM编码,MP3编码同样也可以运用在WAV中,和AVI一样,只要安装好了相应的Decode,就可以欣赏这些WAV了。 在Windows平台下,基于PCM编码的WAV是被支持得最好的音频格式,所有音频软件都能完美支持,由于本身可以达到较高的音质的要求,因此,WAV也是音乐编辑创作的首选格式,适合保存音乐素材。因此,基于PCM编码的WAV被作为了一种中介的格式,常常使用在其他编码的相互转换之中,例如MP3转换成WMA。

MP3编码

MP3作为目前最为普及的音频压缩格式,为大家所大量接受,各种与MP3相关的软件产品层出不穷,而且更多的硬件产品也开始支持MP3,我们能够买到的VCD/DVD播放机都很多都能够支持MP3,还有更多的便携的MP3播放器等等,虽然几大音乐商极其反感这种开放的格式,但也无法阻止这种音频压缩的格式的生存与流传。MP3发展已经有10个年头了,他是MPEG(MPEG:Moving Picture Experts Group) Audio Layer-3的简称,是MPEG1的衍生编码方案,1993年由德国Fraunhofer IIS研究院和汤姆生公司合作发展成功。MP3可以做到12:1的惊人压缩比并保持基本可听的音质,在当年硬盘天价的日子里,MP3迅速被用户接受,随着网络的普及,MP3被数以亿计的用户接受。MP3编码技术的发布之初其实是非常不完善的,由于缺乏对声音和人耳听觉的研究,早期的mp3编码器几乎全是以粗暴方式来编码,音质破坏严重。随着新技术的不断导入,mp3编码技术一次一次的被改良,其中有2次重大技术上的改进。

除了以上编码,还有其它的有兴趣可以自己去google一下。

了解ffmpeg解码同时播放音频与视频的流程

本节采用了解码、播放音频、渲染视频各自己在单独的线程中进行,共享数据缓冲区。解码线程利用ffmpeg从数据流中读取数据,并将音频与视频数据帧进行解码转码后分别存储到音频与视频缓冲区。
音频播放与视频渲染线程从音频解码线程缓冲区请求数据,流程图如下:

  • 解码线程维护两个缓冲区: 音频与视频缓冲区
  • 音频或视频缓冲区缓冲数据未填满时通知解码线程开始进行解码
  • 缓冲区数据填满后解码数据暂停,并通知音频和视频可以继续播放(如果此时音频或视频缓冲区线程暂停)
  • 音频播放线程从音频缓冲区获取数据,如果缓冲区数据不够则暂停音频缓冲区并通知解码线程继续解码(如果解码线程暂停)
  • 视频渲染线程从视频缓冲区获取数据,如果缓冲区数据不够则暂停视频缓冲区并通知解码线程继续解码(如果解码线程暂停)

了解AudioQueue播放流程

AudioQueue是macOS/iOS平台的可直接播放PCM数据的音频播放库之后,提供了C语言接口,利用AudioQueu可以很方便的播放PCM数据。

1.初始化AudioQueue

初始化AudioQueue时需要提供一个目标PCM数据的描述,这个描述的结体构是这样的:

AudioStreamBasicDescription asbd;
asbd.mSampleRate = audioInformation.rate;
asbd.mFormatID = kAudioFormatLinearPCM;
asbd.mChannelsPerFrame = 2;
asbd.mFramesPerPacket = 1;
asbd.mBitsPerChannel = 16;
asbd.mBytesPerFrame = 4;
asbd.mBytesPerPacket = 4;
/// kLinearPCMFormatFlagIsSignedInteger: 存储的数据类型
/// kAudioFormatFlagIsPacked: 数据交叉排列
asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
asbd.mReserved = NO;

mSampleRate: 采样率,描述了音频采样时的频率(一秒时间对音频波进行多少次数据采集)
mFormatID: 播放的数据格式
mChannelsPerFrame: 一帧音频有多少个通道数(单声道、双声道)
mFramesPerPacket: 一个数据包包含多少个音频帧,PCM数据这个值是1
mBitsPerChannel: 一个数据通道占多少位数据
mBytesPerFrame: 一帧音频数据占多少个字节数
mBytesPerPacket: 一个数据久占多少个字节数
mFormatFlags: 数据格式的具体描述
mReserved: 强制8位对齐,这里必须设置成0
这个结构体具体描述的PCM数据的读取方式,这很重要,数据读取失败播放出来的效果会很奇怪。
有了这个数据结构,就可以调用如下接口完成AudioQueue的初始化:

OSStatus status = AudioQueueNewOutput(&asbd, _AudioQueueOutputCallback, (__bridge void *)self, NULL, NULL, 0, &audioQueue);
NSAssert(status == errSecSuccess, @"Initialize audioQueue Failed");
  • 第一个参数是PCM数据描述的结构体指针
  • 第二个参数是一个回调函数,在实际播放过程中需要重复利用AudioQueueBuffer以降低重复初始化AudioQueueBuffer的开销
  • 第三个参数是回调函数中的上下文参数,在C语言的函数指针中这样的设计很常见,回调函数需要知道上下文并正确关联对应的对象
  • 最后一个参数则是初始化完成的AudioQueue对象 其中第二个参数的函数签名是这样的:
static void _AudioQueueOutputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
    FFAudioQueuePlayer *player = (__bridge FFAudioQueuePlayer *)inUserData;
    [player reuseAudioQueueBuffer:inBuffer];
}

2.初始化AudioQueueBuffer

AudioQueueBuffer是具体需要播放的音频数据的载体,AudioQueueBuffer可以携带了多个音频帧,完成AudioQueue的数据化之后就可以利用如下函数初始化足够可重复利用的AudioQueueBuffer,这里MAX_BUFFER_COUNT设置为3。

for(NSInteger i = 0; i < MAX_BUFFER_COUNT; i ++) {
    AudioQueueBufferRef audioQueueBuffer = NULL;
    status = AudioQueueAllocateBuffer(self->audioQueue, audioInformation.buffer_size, &audioQueueBuffer);
    NSAssert(status == errSecSuccess, @"Initialize AudioQueueBuffer Failed");
    CFArrayAppendValue(buffers, audioQueueBuffer);
}

AudioQueueBuffer对象需要保存并引用,防止被释放。其次在后期的播放控制中也需要重新利用这些对象。

3.启动并播放数据

AudioQueueStart(audioQueue, NULL);

首先调用上面的代码启动播放器

AudioQueueBufferRef aqBuffer;
aqBuffer->mAudioDataByteSize = (int)length;
memcpy(aqBuffer->mAudioData, data, length);
AudioQueueEnqueueBuffer(self->audioQueue, aqBuffer, 0, NULL);
AudioQueueEnqueueBuffer(self->audioQueue, aqBuffer, 0, NULL);

设置好需要播放的数据大小与具体的数据之后,将AudioQueueBuffer对象放AudioQueue播放队列即可播放出声音。

完成ffmpeg解码后的音频与视频同时播放

通过以上三步即可完成音频数据的播放,但是在结合了ffmpeg之后除了音频还需要播放视频。

1.完善解码

解码在单独的线程中进行,这里为了方便说明流程逻辑使用的是伪代码(完整的代码请看示例工程),大致逻辑是这样的:

dispatch_async(decode_dispatch_queue, ^{
    while (true) {
        /// Video与Audio缓冲帧都超过最大数时暂停解码线程,等待唤醒
        if((不需要解码音频 && 不需要解码视频) {
             sleep_for_wait();
        }
        AVFrame *frame = decode();
        if(is_audio(frame)) {
          audio_enqueue(frame);
          notify_audio_play_thread_play_if_wait();
        } else if(is_video(frame)) {
          video_enqueue(frame);
          notify_video_render_thread_render_if_wait();
        } else if(decode_complete) {
          /// 跳出while循环
          break;
        }
    }
    NSLog(@"Decode completed, read end of file.");
});

2.完善视频渲染

dispatch_async(video_render_dispatch_queue, ^{
    if(!has_enough_video_frame() && !isDecodeComplete) {
        notify_decode_thread_keep_decode();
        sleep_video_render_thread_for_wait();
    }
    AVFrame* frame = video_dequeue();
    if(obj) {
        video_render(frame);
        if(low_max_cache_video_frame()) {
          notify_decode_thread_keep_decode();
        }
    } else {
        if(isDecodeComplete) {
            stop_video_render();
        }
    }
});

3.完善音频播放

dispatch_async(audio_play_dispatch_queue, ^{
    if(!has_enough_audio_frame() && !isDecodeComplete) {
        notify_decode_thread_keep_decode();
        sleep_audio_play_thread_for_wait();
    }
    AVFrame* frame = audio_dequeue();
    if(obj) {
        audio_play(frame);
        if(low_max_cache_audio_frame()) {
          notify_decode_thread_keep_decode();
        }
    } else {
        if(isDecodeComplete) {
            stop_audio_play();
        }
    }
});

解码线程扮演了生产者的角色,在数据不够的时候生产数据。音频播放与视频渲染扮演了消费者的角色,有足够的数据后消费数据。数据缓冲区保持一个合理的动态平衡。

到此,播放器已经完成了视频的渲染与音频的播放。但是这还不够,音频与视频在单独的线程中不会是一种非常理想的同步播放的状态,下一节将解决播放同步的问题。

总结

  • 了解PCM是什么以及模拟音频信号转换成数字信号的过程与常用的编码格式
  • 了解ffmpeg解码同时播放音频与视频的流程,对播放器有了一个大致的认识
  • 了解AudioQueue播放流程
  • 完成ffmpeg解码后的音频与视频同时播放