01|iOS平台音频渲染(一):使用AudioQueue渲染音频

2022-07-25 16:53:00

你好,我是展晓凯。

记得在开篇的时候我说过,我们最后的目标之一就是要实现一个视频播放器项目。而想要实现这个项目,需要我们先掌握音频渲染、视频渲染以及音视频同步等知识。所以今天我们就来迈出第一步——音频的渲染。

音频渲染相关的技术框架比较多,平台不同,需要用到的技术框架也不同。这节课我们就先来看一下iOS平台都有哪些音频框架可供我们选择,以及怎么在iOS平台做音频渲染。

我们先看一下图1,iOS平台的音频框架,里面比较高层次的音频框架有Media Player、AV Foundation、OpenAL和Audio Toolbox(AudioQueue),这些框架都封装了AudioUnit,然后提供了更高层次的、功能更精简、职责更加单一的API接口。这里你先简单地了解一下这些音频框架之间的关系,以及AudioUnit在整个音频体系中的作用,下节课我会给你详细地讲解AudioUnit框架。

如果我们想要低开销地实现录制或播放音频的功能,就需要用到iOS音频框架中一个非常重要的接口——AudioQueue它是实现录制与播放功能最简单的API接口,作为开发者的我们无需知道太多内部细节,就可以简单地完成播放PCM数据的功能,可以说是非常方便了。

在实际学习AudioQueue框架之前,我会先把AudioSession给你讲清楚,因为AudioSession是我们与系统对话的重要窗口,它能够向系统描述应用需要的音频能力,所以需要在学会使用AudioSession基础上,再去学习具体的框架。

AVAudioSession

在iOS的音视频开发中,使用具体API之前都会先创建一个会话,而音频这里的会话就是AVAudioSession,它以单例的形式存在,用于管理与获取iOS设备音频的硬件信息。我们可以使用以下代码来获取AudioSession的实例:

AVAudioSession *audioSession = [AVAudioSession sharedInstance];

基本设置

获得AudioSession实例之后,就可以设置以何种方式使用音频硬件做哪些处理了,基本的设置如下所示:

  1. 根据我们需要硬件设备提供的能力来设置类别:
[audioSession setCategory:AVAudioSessionCategoryPlayback error:&error]; 
  1. 设置I/O的Buffer,Buffer越小说明延迟越低:
NSTimeInterval bufferDuration = 0.002;
[audioSession setPreferredIOBufferDuration:bufferDuration error:&error]; 
  1. 设置采样频率,让硬件设备按照设置的采样率来采集或者播放音频:
double hwSampleRate = 44100.0;
[audioSession setPreferredSampleRate:hwSampleRate error:&error];
  1. 当设置完毕所有参数之后就可以激活AudioSession了,代码如下:
[audioSession setActive:YES error:&error];

经过上述几个简单的调用,我们就完成了对AVAudioSession的设置。当我们使用具体API的时候,系统就会按照上述设置的参数进行播放或者回调给开发者进行处理。

深入理解AudioSession

除了上述基本的设置之外,我们再从以下几个层面深入理解一下AudioSession,在AVAudioSession设置Category的时候是有很多细节的,分别是Category和CategoryOptions,在某些场景下,它可能会产生奇效。

  1. Category是向系统描述应用需要的能力,常用的分类如下:
  1. CategoryOptions是向系统设置类别的可选项,具体分类如下:
  1. 监听音频焦点抢占,一般在检测到音频被打断的时候处理一些自己业务上的操作,比如暂停播放音频等,代码如下:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionInterruptionNoti:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
 
- (void)audioSessionInterruptionNoti:(NSNotification *)noti {   
AVAudioSessionInterruptionType type = [noti.userInfo[AVAudioSessionInterruptionTypeKey] intValue];
if (type == AVAudioSessionInterruptionTypeBegan) {
//Do Something
}
}
  1. 监听声音硬件路由变化,当检测到插拔耳机或者接入蓝牙设备的时候,业务需要做一些自己的操作,代码如下:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification object:nil];
 
- (void)audioRouteChangeListenerCallback:(NSNotification*)notification {
NSDictionary *interuptionDict = notification.userInfo;
NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
if (routeChangeReason==AVAudioSessionRouteChangeReasonNewDeviceAvailable || routeChangeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable || routeChangeReason == AVAudioSessionRouteChangeReasonWakeFromSleep ) {
//Do Something
} else if (
routeChangeReason == AVAudioSessionRouteChangeReasonCategoryChange ||
routeChangeReason == AVAudioSessionRouteChangeReasonOverride) {
//Do Something
}
}
  1. 申请录音权限,首先判断授权状态,如果没有询问过,就询问用户授权,如果拒绝了就引导用户进入设置页面手动打开,代码如下:
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
    if (status == AVAuthorizationStatusNotDetermined) {
        [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
            //granted代表是否授权
        }];
    } else if (status == AVAuthorizationStatusRestricted || status == AVAuthorizationStatusDenied) {// 未授权
        //引导用户跳入设置页面
    } else {
        // 已授权
    }

注意:从iOS 10开始,所有访问任何设备麦克风的应用都必须静态声明其意图。为此,应用程序现在必须在其Info.plist文件中包含NSMicrophoneUsageDescription键,并为此密钥提供目的字符串。当系统提示用户允许访问时,这个字符串将显示为警报的一部分。如果应用程序尝试访问任何设备的麦克风而没有此键和值,则应用程序将终止。

现在,音频渲染第一步——会话创建就完成了,接下来就可以进入音频渲染框架的学习了,我们就先来看AudioQueue渲染音频的部分。

AudioQueue详解

iOS为开发者在AudioToolbox这个framework中提供了一个名为AudioQueueRef的类,AudioQueue内部会完成以下职责:

接下来让我们一起看一下AudioQueue播放音频的结构图:

图片

AudioQueue暴露给开发者的接口如下:

了解了AudioQueue的内部职责和暴露给开发者的接口之后,就让我们一起看一下AudioQueue的运行流程吧!

AudioQueue运行流程

AudioQueue的整体运行流程分为启动和运行阶段,启动阶段主要是应用程序配置和启动AudioQueue;运行阶段主要是AudioQueue开始播放之后回调给应用程序填充buffer,并重新入队,3个buffer周而复始地运行起来;直到应用程序调用AudioQueue的Pause或者Stop等接口。下图是一个详细的运行流程:

图片

启动阶段

  1. 配置AudioQueue:
AudioQueueNewInput(&dataformat, playCallback, (__bridge void *)self, NULL, NULL, 0, &queueRef);

dataformat就是音频格式,后面我们会重点讲解,playCallback是当AudioQueue需要我们填充数据时的回调方法,函数返回值为OSStatus类型,如果为noErr则说明配置成功。

  1. 分配3个Buffer,并且依次灌到AudioQueue中:
for (int i = 0; i < kNumberBuffers; i++) {
  AudioQueueAllocateBuffer(queueRef, bufferBytesSize, &buffers[i]);
  AudioQueueEnqueueBuffer(queueRef, buffers[i], 0, NULL);
}

Buffer类型为AudioQueueBufferRef,是AudioQueue对外提供的数据封装,具体每个Buffer的大小是如何决定的,我会在后面与dataformat一起讲解。

  1. 调用Play方法进行播放:
AudioQueueStart(queueRef, NULL)

运行阶段

启动完毕后,接下来就到运行阶段了,运行阶段主要分为4步:

  1. AudioQueue启动之后会播放第一个buffer;
  2. 当播放完第一个buffer之后,会继续播放第二个buffer,但是与此同时将第一个buffer回调给业务层由开发者进行填充,填充完毕重新入队;
  3. 第二个buffer播放完毕后,会继续播放第三个buffer,与此同时会将第二个buffer回调给业务层由开发者进行填充,填充完毕重新入队;
  4. 第三个buffer播放完毕后,会继续循环播放队列中的第一个buffer,也会将第三个buffer回调给业务层由开发者进行填充,填充完毕重新入队。
static void playCallback(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
  KSAudioPlayer *player = (__bridge KSAudioPlayer *)aqData;
  //TODO: Fill Data
  AudioQueueEnqueueBuffer(player->queueRef, inBuffer, numPackets, player.mPacketDescs);
}

这样一来,整个AudioQueue的运行流程就讲解完了,还记得我们在前面说过AudioQueue内部会进行调用编解码器进行音频格式转换吗?接下来我们就详细介绍一下AudioQueue中的Codec运行流程。

AudioQueue中Codec运行流程

值得一提的是,AudioQueue的强大之处在于开发者可以不用关心播放的数据的编解码格式,它内部会帮助开发者将Codec的事情做好,所以这部分的流程我们是有必要单拎出来看一下的。

图片

如图所示,主要分为3个步骤:

  1. 开发者配置AudioQueue的时候告诉AudioQueue具体编码格式;
  2. 开发者在回调函数中按照原始格式填充buffer;
  3. AudioQueue会自己采用合适的Codec将压缩数据解码成PCM进行播放。

介绍完Codec相关的流程,你可能还有一个疑问,就是数据格式以及音频数据到底应该如何设置以及填充呢?这个其实就是之前我们说要重点讲解的音频格式(dataformat),接下来我们就一起来学习一下吧!

iOS平台的音频格式

iOS平台的音频格式是ASBD(AudioStreamBasicDescription),用来描述音频数据的表示方式,结构体如下:

struct AudioStreamBasicDescription
{
    AudioFormatID       mFormatID;
    Float64             mSampleRate;    
    UInt32              mChannelsPerFrame;    
    UInt32              mFramesPerPacket;
    AudioFormatFlags    mFormatFlags;
    UInt32              mBytesPerPacket;
    UInt32              mBytesPerFrame;
    UInt32              mBitsPerChannel;
    UInt32              mReserved;
};
typedef struct AudioStreamBasicDescription  AudioStreamBasicDescription;

针对结构体中每个字段,我们需要配上一个实际的案例来逐个讲解一下,先看下面这个格式的配置:

UInt32 bytesPerSample = sizeof(Float32);
AudioStreamBasicDescription asbd;
bzero(&asbd, sizeof(asbd)); 
asbd.mFormatID = kAudioFormatLinearPCM;
asbd.mSampleRate = _sampleRate; 
asbd.mChannelsPerFrame = channels; 
asbd.mFramesPerPacket = 1; 
asbd.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
asbd.mBitsPerChannel = 8 * bytesPerSample;
asbd.mBytesPerFrame = bytesPerSample;
asbd.mBytesPerPacket = bytesPerSample; 

如果要播放的是一个MP3或者M4A的文件,这个ASBD应该如何确定呢?请看下面这个代码:

// 打开文件
NSURL *fileURL = [NSURL URLWithString:filePath];
OSStatus status = AudioFileOpenURL((__bridge CFURLRef)fileURL, kAudioFileReadPermission, kAudioFileCAFType, &_mAudioFile);
if (status != noErr) {
  NSLog(@"open file error");
}    
// 获取文件格式
UInt32 dataFromatSize = sizeof(dataFormat);
AudioFileGetProperty(_mAudioFile, kAudioFilePropertyDataFormat, &dataFromatSize, &dataFormat);

第一步是用AudioFile打开文件,如果打开成功的话,直接获取出这个AudioFile的DataFormat就好了,是不是很简单呢?对于填充数据也比较简单,直接从AudioFile中读取原始数据就可以了。

学到这里可能你会有疑问,绕了一大圈,用AudioQueue就直接播放了一个音频文件,那我直接使用AVAudioPlayer或者AVPlayer来播放这个音频文件不更简单吗?的确是的,但我更想通过这个例子来告诉你:这就是iOS给开发者提供的强大的多媒体处理能力,而AudioQueue更适合开发者在一些更底层的数据处理的场景下使用。

小结

最后,我们可以一起来回顾一下。

这节课我们对iOS的音频框架有了一个大致的了解。其中最重要的两个就是AudioQueue和AudioUnit。AudioQueue使用起来非常方便,它是实现录制与播放功能最简单的API接口,就算你不知道内部的细节,也可以简单地完成播放PCM数据的功能。所以如果你的输入是PCM,比如视频播放器场景、RTC等需要业务自己Mix或者处理PCM的场景,那么使用AudioQueue是非常适合的一种方式。

AudioUnit是iOS中最底层的音频框架,对音频能够实现更高程度的控制,所以也是我们的必学内容之一,下节课我会详细地讲一讲怎么使用AudioUnit实现音频的渲染,期待一下吧!

今天我通过代码带你创建并设置了AVAudioSession,还带你详细了解了AudioQueue的运行流程以及iOS平台的音频格式ASBD,希望你学完之后可以自己动手练一练,把今天学习的内容内化到自己的知识网络中。

思考题

学而不思则罔,最后我给你留一道思考题:你思考一下AudioQueue相比于AVPlayer或者AVAudioPlayer,它的灵活性或者说好处在哪儿呢?欢迎在评论区分享你的思考,也欢迎你把这节课分享给更多对音视频感兴趣的朋友,我们一起交流、共同进步。下节课再见!