audio_unit_base - ShenYj/ShenYj.github.io GitHub Wiki
- 引言
- AudioStreamPacketDescription
- Audio File Stream Services
- Audio File Service
- Audio Converter Services
- Extended Audio File Service
- Audio Queue Services
- OSStatus
初次接触 Audio Unit 面对很多的类型、接口容易混淆,这篇笔记主要用于整理下编解码中可能接触到的一些工具类、以及部分函数的简单介绍
篇幅很长,但不涉及较深的技术原理,多为开发中涉及到的知识点介绍
iOS一个经典的音频播放流程, 以MP3为例
- 读取MP3文件
- 解析采样率、码率、时长等信息,分离MP3中的音频帧
- 对分离出来的音频帧解码得到PCM数据
- 对PCM数据进行音效处理(均衡器、混响器等,非必须)
- 把PCM数据解码成音频信号
- 把音频信号交给硬件播放
- 重复1-6步直到播放完成
在iOS系统中apple对上述的流程进行了封装并提供了不同层次的接口,下面对其中的中高层接口进行功能说明
- Audio File Services:读写音频数据,可以完成播放流程中的第2步
- Audio File Stream Services:对音频进行解码,可以完成播放流程中的第2步
- Audio Converter services:音频数据转换,可以完成播放流程中的第3步;
- Audio Processing Graph Services:音效处理模块,可以完成播放流程中的第4步;
- Audio Unit Services:播放音频数据:可以完成播放流程中的第5步、第6步;
- Extended Audio File Services:Audio File Services和Audio Converter services的结合体;
- AVAudioPlayer/AVPlayer(AVFoundation):高级接口,可以完成整个音频播放的过程(包括本地文件和网络流播放,第4步除外);
- Audio Queue Services:高级接口,可以进行录音和播放,可以完成播放流程中的第3、5、6步;
在iOS平台做音视频开发久了就会知道,不论音频还是视频的API都会接触到很多 StreamBasic Description,该 Description 就是用来描述音视频具体格式的
-
struct AudioStreamBasicDescription { Float64 mSampleRate; AudioFormatID mFormatID; AudioFormatFlags mFormatFlags; UInt32 mBytesPerPacket; UInt32 mFramesPerPacket; UInt32 mBytesPerFrame; UInt32 mChannelsPerFrame; UInt32 mBitsPerChannel; UInt32 mReserved; }; typedef struct AudioStreamBasicDescription AudioStreamBasicDescription;
- mSampleRate: 每秒钟的 sample frame 的个数(帧数), 注意不是 sample 的个数
- mFormatID: 用来指定音频的编码格式, 比如 mp3 kAudioFormatMPEGLayer3
- mFormatFlags: 音频格式标志,用于指示mFormatID所指定的格式
- mBytesPerPacket: 数据包中的字节数
- mFramesPerPacket: 每个数据包中的样本帧数
- mBytesPerFrame: 单个样本数据帧中的字节数
- mChannelsPerFrame: 每个数据帧中的通道数
- mBitsPerChannel: 数据帧中每个通道的样本数据位数
- mReserved: 填充结构以强制实现偶数 8 字节对齐
-
AudioFormatID
CF_ENUM(AudioFormatID) { kAudioFormatLinearPCM = 'lpcm', kAudioFormatAC3 = 'ac-3', kAudioFormat60958AC3 = 'cac3', kAudioFormatAppleIMA4 = 'ima4', kAudioFormatMPEG4AAC = 'aac ', kAudioFormatMPEG4CELP = 'celp', kAudioFormatMPEG4HVXC = 'hvxc', kAudioFormatMPEG4TwinVQ = 'twvq', kAudioFormatMACE3 = 'MAC3', kAudioFormatMACE6 = 'MAC6', kAudioFormatULaw = 'ulaw', kAudioFormatALaw = 'alaw', kAudioFormatQDesign = 'QDMC', kAudioFormatQDesign2 = 'QDM2', kAudioFormatQUALCOMM = 'Qclp', kAudioFormatMPEGLayer1 = '.mp1', kAudioFormatMPEGLayer2 = '.mp2', kAudioFormatMPEGLayer3 = '.mp3', kAudioFormatTimeCode = 'time', kAudioFormatMIDIStream = 'midi', kAudioFormatParameterValueStream = 'apvs', kAudioFormatAppleLossless = 'alac', kAudioFormatMPEG4AAC_HE = 'aach', kAudioFormatMPEG4AAC_LD = 'aacl', kAudioFormatMPEG4AAC_ELD = 'aace', kAudioFormatMPEG4AAC_ELD_SBR = 'aacf', kAudioFormatMPEG4AAC_ELD_V2 = 'aacg', kAudioFormatMPEG4AAC_HE_V2 = 'aacp', kAudioFormatMPEG4AAC_Spatial = 'aacs', kAudioFormatMPEGD_USAC = 'usac', kAudioFormatAMR = 'samr', kAudioFormatAMR_WB = 'sawb', kAudioFormatAudible = 'AUDB', kAudioFormatiLBC = 'ilbc', kAudioFormatDVIIntelIMA = 0x6D730011, kAudioFormatMicrosoftGSM = 0x6D730031, kAudioFormatAES3 = 'aes3', kAudioFormatEnhancedAC3 = 'ec-3', kAudioFormatFLAC = 'flac', kAudioFormatOpus = 'opus', kAudioFormatAPAC = 'apac', };
-
AudioFormatFlags
CF_ENUM(AudioFormatFlags) { kAudioFormatFlagIsFloat = (1U << 0), // 0x1 kAudioFormatFlagIsBigEndian = (1U << 1), // 0x2 kAudioFormatFlagIsSignedInteger = (1U << 2), // 0x4 kAudioFormatFlagIsPacked = (1U << 3), // 0x8 kAudioFormatFlagIsAlignedHigh = (1U << 4), // 0x10 kAudioFormatFlagIsNonInterleaved = (1U << 5), // 0x20 kAudioFormatFlagIsNonMixable = (1U << 6), // 0x40 kAudioFormatFlagsAreAllClear = 0x80000000, kLinearPCMFormatFlagIsFloat = kAudioFormatFlagIsFloat, kLinearPCMFormatFlagIsBigEndian = kAudioFormatFlagIsBigEndian, kLinearPCMFormatFlagIsSignedInteger = kAudioFormatFlagIsSignedInteger, kLinearPCMFormatFlagIsPacked = kAudioFormatFlagIsPacked, kLinearPCMFormatFlagIsAlignedHigh = kAudioFormatFlagIsAlignedHigh, kLinearPCMFormatFlagIsNonInterleaved = kAudioFormatFlagIsNonInterleaved, kLinearPCMFormatFlagIsNonMixable = kAudioFormatFlagIsNonMixable, kLinearPCMFormatFlagsSampleFractionShift = 7, kLinearPCMFormatFlagsSampleFractionMask = (0x3F << kLinearPCMFormatFlagsSampleFractionShift), kLinearPCMFormatFlagsAreAllClear = kAudioFormatFlagsAreAllClear, kAppleLosslessFormatFlag_16BitSourceData = 1, kAppleLosslessFormatFlag_20BitSourceData = 2, kAppleLosslessFormatFlag_24BitSourceData = 3, kAppleLosslessFormatFlag_32BitSourceData = 4 };
举个栗子
asbd.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
示例代码中的第一个参数指定每个sample的表示格式是Float格式,这点类似于之前讲解的每个sample都是使用两个字节(SInt16)来表示;
然后是后面的参数NonInterleaved,字面理解这个单词的意思是非交错的,其实对于音频来讲就是左右声道是非交错存放的,实际的音频数据会存储在一个AudioBufferList结构中的变量mBuffers中
- 如果mFormatFlags指定的是NonInterleaved,那么左声道就会在mBuffers[0]里面,右声道就会在mBuffers[1]里面;
- 而如果mFormatFlags指定的是Interleaved的话,那么左右声道就会交错排列在mBuffers[0]里面,理解这一点对于后续的开发将是十分重要的
声的三要素:
-
频率: 频率是指声波的频率,即声音的音调,人类听觉的频率(音调)范围为[20Hz,20kHz
-
振幅: 振幅是指声波的响度,通俗地讲就是声音的高低,可以理解为音量
-
波形: 波形是指声音的音色,在同样的频率和振幅下,钢琴和小提琴的声音听起来是完全不同的,因为它们的音色不同。波形决定了其所代表声音的音色,音色不同是因为它们的介质所产生的波形不同
音频的基础概念:
-
音频流
连续的表示声音的数据流,可以是pcm也是编码后的数据流,例如aac
-
声道
是指声音在录制或播放时在不同空间位置采集或回放的相互独立的音频信号,所以声道数也就是声音录制时的音源数量或回放时相应的扬声器数量。
-
采样率
简单来讲就是每秒获取声音样本的次数。声音是一种能量波,其具有音频频率和振幅的特征。采样的过程,其实就是抽取某点的频率值。如果在1s内抽取的点越多,获得的信息也就越多,采样率越高,声音的质量就越好,但并不是说采样率越高就越好,因为人耳的听觉范围为[20Hz,20kHz]。一般来讲,44100Hz的采样率已经能够满足基本要求了,更高的采样频率还有48 000Hz。
-
采样点
音频流中单个声道的一个采样点的数字表示。例如采样率16000,表示1秒内单个声道会有16000个采样点。
采样数跟采样率、采样时间、采样位数和声道数有关系,即采样数等于采样率、采样时间、采样位数和声道数这几个参数的乘积。例如采样率为44 100Hz,采样时间为1s,采样位数为16b,声道数为2,那么采样数就等于44 100×1×16×2=1 411 200。
-
量化
每个采样又该如何表示呢?这就涉及量化了。量化是指在幅度轴上对信号进行数字化。如果用16比特的二进制信号来表示一个采样,则一个采样所表示的范围为[-32 768,32 767]。
采样位数也叫采样大小、量化位数、量化深度、采样位深、采样位宽。采样位数表示每个采样点用多少比特表示,音频的量化深度一般为8b、16b、32b等
量化深度的大小会影响声音的质量,位数越多,量化后的波形越接近原始波形,声音的质量越高,而需要的存储空间也越大;位数越少,声音的质量越低,需要的存储空间越小 -
帧
同一时间点的多个声道的采样集合构成一帧。对于一个双声道立体声的pcm数据,一帧有两个采样,一个来自左声道,一个来自右声道。
音频跟视频不太一样,视频的每一帧就是一副图像,但是音频是流式的,本身没有一帧的概念。对于音频来讲,确实没有办法明确定义出一帧。例如对于PCM流来讲,采样率为44 100Hz,采样位数为16b,通道数为2,那么1s音频数据的大小是固定的,共44 100×16b×2÷8=176 400B。通常情况下,可以规定一帧音频音频跟视频不太一样,视频的每一帧就是一副图像,但是音频是流式的,本身没有一帧的概念。对于音频来讲,确实没有办法明确定义出一帧。例如对于PCM流来讲,采样率为44 100Hz,采样位数为16b,通道数为2,那么1s音频数据的大小是固定的,共44 100×16b×2÷8=176 400B。
通常情况下,可以规定一帧音频的概念,例如规定每20ms的音频是一帧帧长由编码格式决定,PCM没有帧长的概念,开发者可自行决定帧长。为了和主流音频编码格式的帧长保持一致,推荐采用20ms为帧长。
-
包
一个或多个连续帧的集合。pcm中一个包中只有一帧,在压缩格式中,一个包里面会有多个帧。例如aac,一个包里一般有1024个帧。
-
比特率(码率)
是指音频每秒传送的比特数,单位为b/s。
比特率越大表示单位时间内采样的数据越多,传输的数据量就越大。
例如对于PCM流,采样率为44 100Hz,采样大小为16b,声道数为2,那么比特率为44 100×16×2=1 411 200b/s。一个音频文件的总大小,可以根据采样率、采样位数、声道数、采样时间来计算
即文件大小 = 采样率 × 采样时间 × 采样位数 × 声道数 ÷ 8。 -
单个音频包的持续时间
packetDuration = framesPerPacket / sampleRate * 1000
,也就是说单个包的持续时间(毫秒) = 单个包的帧数 / 采样频率 * 1000例如,对于一个采样率44.1KHz的MP3文件,一个音频包有1152个音频帧(固定的)
根据公式计算可得 packetDuration = 1152 / 44.1KHz * 1000 = 26.1224...(约等于26ms),即一个MP3音频包的持续时间是26ms。
这个知识点在seek时会发挥比较大的作用,可以用于求出seek后对应的音频包位置。
ADIF:Audio Data Interchange Format
ADIF特征可以准确地找到这个音频数据的开始,不可以在音频数据流的中间位置进行解码,即它的解码必须在明确定义的开始处进行,故这种格式常用在磁盘文件中。
ADTS:Audio Data Transport Stream
ADTS是一个有同步字的比特流,解码可以在这个流中的任何位置开始,它的特征类似于MP3数据流格式。简单来讲,ADTS可以在任意帧解码,也就是说它的每帧都有头信息;
ADIF只有一个统一的头,所以必须得到所有的数据后解码。这两种文件格式的Header的格式也是不同的,一般编码后的和抽取出的都是ADTS格式的音频流。
封装适用性:由于ADTS的每个数据块都包含同步信息,因此更适合流式传输。而ADIF则更适合文件存储,因为它提供了整个文件的元数据。
ADIF和ADTS主要适用于 AAC 编码格式的音频,MP3有自己独立的一套封装格式,这里的目的是掌握这个原理、了解这个概念
VBR(Variable Bitrate)动态比特率 也就是没有固定的比特率,压缩软件在压缩时根据音频数据即时确定使用什么比特率,这是以质量为前提兼顾文件大小的方式,推荐编码模式;
CBR(Constant Bitrate),常数比特率 指文件从头到尾都是一种位速率。相对于VBR和ABR来讲,它压缩出来的文件体积很大,而且音质相对于VBR和ABR不会有明显的提高
用来读取采样率、码率、时长等基本信息以及分离音频帧
不仅限于网络流,本地文件同样可以用它来读取信息和分离音频帧。AudioFileStreamer的主要数据是文件数据而不是文件路径,所以数据的读取需要使用者自行实现
核心函数
-
实例化一个流解析器,最关键的两个参数分别是
inPropertyListenerProc
和inPacketsProc
, 前一个是当通过数据流解析到一个属性时就会被触发的回调,后一个每当解析出一个数据包就会被触发 -
当得到一块流数据需要解析的时候,就会调用这个函数
-
关闭解析器
AudioFile的 功能远比AudioFileStream强大,除了共同的解析音频数据分离音频帧之外,它还可以读取音频数据,甚至可以写音频(生成音频文件),而AudioFileStream本身没有读取音频数据的功能。看起来选择AudioFile就OK了,不料AudioFile却需要AudioFileStream来保证数据的完整性,否则会大大增加出错的可能性
核心函数
-
创建一个新的音频文件,或初始化由URL指定的现有文件
需要注意的是先创建子目录. 若果直接调用AudioFileCreateWithURL创建一个不存在的目录创建文件会失败
iOS中 CAF 格式文件可以存放任意类型音频数据
通过参数 kAudioFileFlags_EraseFile 创建时将清空现有文件的内容,如果不设置并且文件已经存在则会创建失败 -
AudioFileInitializeWithCallbacks
删除现有文件的内容并将回调分配给音频文件对象
-
将音频数据字节写入音频文件。
-
基于一个本地文件路径的打开方式
-
这个方法自由度较高,数据源是通过
inReadFunc
回调来获取的, 无论是本地文件、内存文件还是网络流只要在 AudioFIle 需要数据时 (open和read时) 通过回调提供即可AudioFile 的 open方法调用过程中就会对音频格式信息进行解析,只有符合要求的音频格式才能被成功打开否则open方法就会返回错误码(换句话说,open方法一旦调用成功就相当于AudioFileStream调用AudioFileStreamParseBytes()后返回 ReadyToProducePackets一样,只要open成功就可以开始读取音频数据,所以在open方法调用的过程中就需要提供一部分音频数据来进行解析。 这也是前面提到的 AudioFile 需要 AudioFileStream 来保证数据完整性,否则就是持续的收集 buffer 多次失败中尝试成功 Open 为止
-
从音频文件中读取音频数据字节
-
读取音频文件中的音频数据包
-
关闭音频文件
在使用 AudioFileOpenxxx
函数时,要确保音频文件的格式信息完整,否则将会 Open 失败, 因为 Open 成功 等同于 AudioFileStream 解析信息后返回了 ReadyToProducePackets ,这就意味着 AudioFile 并不能独立用于音频流的读取,在流播放时首先需要使用 AudioStreamFile来得到ReadyToProducePackets标志位来保证信息完整
这一部分的解释可以参考文章: iOS 音频流播(三)
- 其他函数介绍
AudioFileGetPropertyInfo 方法用来获取某个属性对应的数据的大小(outDataSize)以及该属性是否可以被write(isWritable),而AudioFileGetProperty则用来获取属性对应的数据。对于一些大小可变的属性需要先使用AudioFileGetPropertyInfo获取数据大小才能取获取数据(例如formatList),而有些确定类型单个属性则不必先调用AudioFileGetPropertyInfo直接调用AudioFileGetProperty即可。
关键函数
-
创建基于指定音频格式的新的音频转换对象。
-
使用指定的编解码器创建一个新的音频转换器对象。
对比上面的函数,多了一个参数 编码器
-
重置音频转换器对象,清除并刷新其缓冲区
-
释放转码器
-
AudioConverter
提供了三个函数用于编解码函数前两个函数功能类似,都只支持PCM之间的转换,并且两种PCM的采样率必须一致。也就是说无法从PCM转换成其他压缩格式或者从压缩格式转换成PCM
Extended Audio File Services 是Audio File Services 和 Audio Converter Services 的结合,提供统一的接口进行处理
ExtAudioFile具有几下特点:
- ExtAudioFile 是AudioUnit的一个组件,它提供了将原始音频数据编码为WAV,caff等编码格式的音频数据,同时提供写入文件的接口
- 同时它还提供了从文件中读取数据解码为PCM音频数据的功能
- 编码和解码支持硬编解码和软编解码
- 不能操作PCM裸数据
- 对应的数据结构对象为 ExtAudioFileRef
- 该对象具有编码和封装两大功能
目前想要研究的音频流的解析、帧分离、编码/解码、播放PCM裸流,以及效果器等
在拿到帧分离后的数据后,到这里就可以结合 Audio Queue 来进行播放了
Audio Unit 开发时,很多接口最终返回值类型均为 OSStatus,通常只要是 noErr 就可以确定是没有问题,如果有问题,这里有个网站可以帮助我们快速排查 https://www.osstatus.com