AudioStreamBasicDescription
core Audio 将音频数据视为数据包流。而流的内容是由AudioStreamBasicDescription来确定。ASBD(AudioStreamBasicDescription) 比较重要,我们可以深入研究一下。
struct AudioStreamBasicDescription {
Float64 mSampleRate;
UInt32 mFormatID;
UInt32 mFormatFlags;
UInt32 mBytesPerPacket;
UInt32 mFramesPerPacket;
UInt32 mBytesPerFrame;
UInt32 mChannelsPerFrame;
UInt32 mBitsPerChannel;
UInt32 mReserved;
};
typedef struct
AudioStreamBasicDescription AudioStreamBasicDescription;
这个结构体包含数据包数据布局的元数据:采样率,一个信道的bt数(一个信道的一次采样用多少个数据来表示,一帧中的信道数等等。它本身并不包含音频数据,这是只是描述音频数据布局相关的东西。
移动音频被称为流式,所以内存中的一堆音频数据被称为音频流。不要和网上的音频流混淆,这是同一个比喻的不同用法。
关于流的 ABSD的重要一点就是它的格式是其格式的实现细节:没有必要比较 AAC 数据格式和MP3 中ABSD 中 mFramesPerPacket 这个值,因为他们的数据包完全不同。
当你读取音频数据时,核心音频的各个部分会自动填充ASBD的值。cor Audio 甚至会在你写数据的时候填充 ASBD,如果你想压缩数据,但是你不知道,你可以传0,让Core Audio 自己琢磨。
- mSampleRate 每秒未压缩的数据的样本数 = 每秒的帧数 ,可以× mFramesPerPacket 就可以确定数据包的长度
- mFormatID 是作为格式名称的四字符代码,不命名格式,这个就没意义,所以这个值必须指定。
- mFormatFlags 是格式的子类型或者叫偏好面板。这是 UInt32 类型,你可以通过设置或者检查其中一位。你可以通过文档搜素AudioStreamBasicDescription Flags去查找。没有标记的格式可以设为 0.
- mBytesPerPacket 如果比特率不变,可以设置这个值。如果是Variable bit rate 那么就不行,就不能用这个来描述,要将这个值设为0,并且用AudioStreamPacketDescriptions 来描述。
- mBytesPerFrame 单个帧字节数,用来表示时间中每个时刻的数字总数,当一帧中不包含一个采样通道时,如压缩,这个值为 0
- mChannelsPerFrame 每一个帧有多少个通道,不考虑压缩的情况,这个值不可能是0,因为根据定义,没有通道的音频流是空的。
- mBitsPerChannel 采样频率的位深,与每帧字节一样,它代表数据的实际结构。压缩格式他的帧中没有包含采样通道,把他设为0。
- mReserved 用于数据对齐,将结构填充为8 的偶数,他始终是0
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'
};
对于压缩数据,他的包结构不能从上面的结构中完全表示,需要通过AudioStreamPacketDescriptions,
AudioStreamPacketDescription
struct AudioStreamPacketDescription {
SInt64 mStartOffset;
UInt32 mVariableFramesInPacket;
UInt32 mDataByteSize;
};
typedef struct AudioStreamPacketDescription
AudioStreamPacketDescription;
- mStartOffset 表示数据包相对于缓冲区中其他数据包的位置,以字节为单位。这个很重要,因为不同大小的包不能像在线性PCM格式中那样使用隐含 的X轴值 (简单来说不能用 偏移量 = 时间 * 采样率 * 帧长 + 通道数 是找不到样本的) ? 这里感觉计算偏移的公式有问题。
- mVariableFramesInPacket 表示数据包中的帧数,这个只有数据包使用了可变帧速率时使用。如果是固定的帧速率,那么这个值应该为0,应该设置的是AudioStreamBasicDescription 中mFramesPerPacket这个值。
- mDataByteSize 这个数据包大小,有多少个字节
总结
通过上面两个结构体,就基本可以处理任何格式的音频数据,但是对于特定格式的数据,我们使用Core Audio 的 Audio File Services, Audio File Stream Services, Audio Conversion Services, and Audio Queue Services 来支撑。cookie 是一块透明的数据,他的内容可以按照特定的格式进行编码和解码。这就意味着如果你打开本地或者网络的压缩数据,你需要先检查 magic cookie 属性,如果有的话,你就把他当做 unType的数据来读,然后把他传给 core Audio而不用关心里面发了什么。
例如:确定格式
正如你所看到的,core Audio 表达音频数据的方式并不是那么简单说“ an MP3” "Audible 书",有很大的不同在文件格式,文件中的音频数据,和描述音频数据的数据,这涉及到三个不同的描述。对于所有的音频流都需要 AudioStreamBasicDescription,magic cookie 确定压缩格式, 对于 压缩格式的每个数据包都需要 AudioStreamPacketDescription。
正如我们所说的,很多格式看起来是任意的,所以你就可以通过代码告诉什么可以进入,什么不可以进入给定的格式。Audio File Services 有一个 AudioFileGetGlobalInfo 来描述core Audio 一般对音频的处理。查看Audio File Global Info Properties ,里面有很多属性。kAudioFileGlobalInfo_AvailableStreamDescriptionsForFormat 用这个跟你可以查看那些不能进入的 audio fIles
通过下面的代码
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
AudioFileTypeAndFormatID fileTypeAndFormat;
//文件格式
fileTypeAndFormat.mFileType = kAudioFileCAFType;
// formatID
fileTypeAndFormat.mFormatID = kAudioFormatLinearPCM;
OSStatus audioErr = noErr;
UInt32 infoSize = 0;
audioErr = AudioFileGetGlobalInfoSize
(kAudioFileGlobalInfo_AvailableStreamDescriptionsForFormat,
sizeof (fileTypeAndFormat),
&fileTypeAndFormat,
&infoSize);
if (audioErr != noErr) {
UInt32 err4cc = CFSwapInt32HostToBig(audioErr);
NSLog (@"%4.4s", (char*)&err4cc);
}
assert (audioErr == noErr);
//分配asbds 大小
AudioStreamBasicDescription *asbds = malloc (infoSize);
//多少数据infoSize
audioErr = AudioFileGetGlobalInfo
(kAudioFileGlobalInfo_AvailableStreamDescriptionsForFormat,
sizeof (fileTypeAndFormat),
&fileTypeAndFormat,
&infoSize,
asbds);
assert (audioErr == noErr);
int asbdCount = infoSize / sizeof (AudioStreamBasicDescription);
for (int i=0; i<asbdCount; i++) {
//将数据转化为大端模式
UInt32 format4cc = CFSwapInt32HostToBig(asbds[i].mFormatID);
NSLog (@"%d: mFormatId: %4.4s, mFormatFlags: %d, mBitsPerChannel: %d",
i,
(char*)&format4cc,
asbds[i].mFormatFlags,
asbds[i].mBitsPerChannel);
}
free (asbds);
[pool drain];
return 0;
}
其中将文件的格式设置为
fileTypeAndFormat.mFileType = kAudioFileAIFFType;
上面的代码打印输出为
0: mFormatId: lpcm, mFormatFlags: 14, mBitsPerChannel: 8
1: mFormatId: lpcm, mFormatFlags: 14, mBitsPerChannel: 16
2: mFormatId: lpcm, mFormatFlags: 14, mBitsPerChannel: 24
3: mFormatId: lpcm, mFormatFlags: 14, mBitsPerChannel: 32
可以看到mFormatFlags 的值为 14 ,0×2 + 0×4 + 0×8 = 14,所以kAudioFileAIFFType类型数据必须是大端模式
其中将文件的格式设置为
fileTypeAndFormat.mFileType = kAudioFileWAVEType;
上面的代码打印输出为
0: mFormatId: lpcm, mFormatFlags: 8, mBitsPerChannel: 8
1: mFormatId: lpcm, mFormatFlags: 12, mBitsPerChannel: 16
2: mFormatId: lpcm, mFormatFlags: 12, mBitsPerChannel: 24
3: mFormatId: lpcm, mFormatFlags: 12, mBitsPerChannel: 32
4: mFormatId: lpcm, mFormatFlags: 9, mBitsPerChannel: 32
5: mFormatId: lpcm, mFormatFlags: 9, mBitsPerChannel: 64
可以看到所有mFormatFlags mFormatFlags 都不含 0x2,说明是小端模式,里面还有一个情况就是 kAudioFormatFlagIsFloat = 0x1 有值,说明支持浮点型。
其中将文件的格式设置为
fileTypeAndFormat.mFileType = kAudioFileCAFType;
上面的代码打印输出为
0: mFormatId: lpcm, mFormatFlags: 14, mBitsPerChannel: 8
1: mFormatId: lpcm, mFormatFlags: 14, mBitsPerChannel: 16
2: mFormatId: lpcm, mFormatFlags: 14, mBitsPerChannel: 24
3: mFormatId: lpcm, mFormatFlags: 14, mBitsPerChannel: 32
4: mFormatId: lpcm, mFormatFlags: 11, mBitsPerChannel: 32
5: mFormatId: lpcm, mFormatFlags: 11, mBitsPerChannel: 64
6: mFormatId: lpcm, mFormatFlags: 12, mBitsPerChannel: 16
7: mFormatId: lpcm, mFormatFlags: 12, mBitsPerChannel: 24
8: mFormatId: lpcm, mFormatFlags: 12, mBitsPerChannel: 32
9: mFormatId: lpcm, mFormatFlags: 9, mBitsPerChannel: 32
10: mFormatId: lpcm, mFormatFlags: 9, mBitsPerChannel: 64
可以看到所有mFormatFlags mFormatFlags 整数和浮点型都支持,他支持 AIFF 和wav所有格式除了kAudioFileWAVEType中 mFormatFlags: 8, mBitsPerChannel: 8 的情况。
继续下面的修改
//文件格式
fileTypeAndFormat.mFileType = kAudioFileCAFType;
// formatID
fileTypeAndFormat.mFormatID = kAudioFormatMPEG4AAC;
运行代码可以看到下面的结果,这意味着AAC 是CAF文件的有效负载, mBitsPerChannel为0 是因为这种情况使用的是可变比特率。
0: mFormatId: aac , mFormatFlags: 0, mBitsPerChannel: 0
AAC可以用不同的格式来携带数据,你可能已经习惯了.M4a 文件,它有文件格式常量表示kAudioFileM4AType。AAC 并不是 mp3 的有效负载。MP3 只能携带mp3 数据。
fileTypeAndFormat.mFileType = kAudioFileMP3Type;
fileTypeAndFormat.mFormatID = kAudioFormatMPEG4AAC;
运行上面的数据就崩溃了,控制台打印的数据如下:
2022-07-20 17:18:01.182268+0800 CH03_CAStreamFormatTester[3716:186454] fmt?
这个fmt? 定义在 AudioFile.h
中kAudioFileUnsupportedDataFormatError ,这里就是说你不能把 AAC 的数据放在 MP3 文件中,如果你这样做就会返回一个 error。通常 some file formats只能使用一种audio data format,而CAF 可以让你获取几乎任何东西。
这就是文件格式和数据格式的不同以及两者是怎么配合的。
系统规定的mFormatFlags 的值
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
};
Canonical Formats
支持这么多格式,什么是最好的格式?这是一个愚蠢的问题,之所以存在这么多的格式,是以为每种格式都是为不同的用途而设计的。在压缩格式中,AAC在高压缩率下的音频保真度(尤其是音乐)非常好,但是你要达到最高的保真度,你可以使用 Apple Lossless ,他可以完美地再现源音频。在带宽规模的另一端,iLBC (Internet Low-Bandwidth Codec) 是为了潜在不可靠的互联网链接上的语音进行优化,使其非常适合 Voip或 游戏内聊天的目的。
在给定的任务每个平台都有一个最有效率的format(格式),在core Audio 中我们称他为 canonical ,因为他形成了 其他别的音频的基准。
AudioStreamBasicDescription 的默认值是Canonical Formats,所有格式的转化都是要先转化为canonical,再从canonical 转化为其他。
每个平台都有两种canonical formats,AudioSampleType用在I/O操作,AudioUnitSampleType 在 MAC OS Snow Leopard 这一个版本的系统和 ios 引入的,它主要用在 audio units 和 数字信号处理。
在 Mac OS X, AudioSampleType 和 AudioUnitSampleType 都是 32位的,channels 必须是非交错的。你可以找到 CoreAudioTypes.h 中的定义。
kAudioFormatFlagsCanonical = kAudioFormatFlagIsFloat |
kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked,
kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsFloat |
kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked |
kAudioFormatFlagIsNonInterleaved,
在ios 中,AudioSampleType 是16位的整形,AudioUnitSampleType 是 8.24定点整数。在 小数点左边有8位,右边有24位。
kAudioFormatFlagsCanonical = kAudioFormatFlagIsSignedInteger
|
kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked,
kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsSignedInteger
|
kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked |
kAudioFormatFlagIsNonInterleaved | (kAudioUnitSampleFractionBits
<<
kLinearPCMFormatFlagsSampleFractionShift),
core Audio 使用音频转换器在格式和编解码器之间转换数据,以及各种压缩方式之间转化数据。更简单的框架,如音频服务任务,在幕后使用这些工具将文件包含的任何数据转化为规范格式。当你向audio engines (Audio Units and OpenAL) 提供样本数据时,最好使用 canonical formats,因为他可以节省一些数据转化,然后更多的CPU资源到其他地方。
Processing Audio with Audio Units
用音频单元处理音频
core audio 大量的工作都是在音频单元上的,Audio Queue and OpenAL engines 也在音频单元上实现的。
每个音频单元都是以特定的方式处理样本的buffer,从麦克风捕获数据,下一个工作流可能是加一些效果在样本上,接下来可能又是和另外的资源进行混合。音频单元通过音频流连接就像专业音频设备经过电缆连接在一起一样。音频单元有很多元素,这些可能有输入或者输出范围去表明是接收数据还是产生数据。为了连接两个单元你需要设置一个连接输入元素的单元和输出单元的元素。
MAC OSX 提供了很多中音频单元,这些音频单元可以通过图表 连接在一个工作流中,音频数据包通过graph通过 audio unit,audio unit 对数据进行处理。你最有可能遇到的是下面几种units:
Effect units :进行数字处理,他们以某种方式改变音频数据,他们类似于 hardware effects boxes and 外部信号处理器。
Instrument units :生成代表音符的音频数据,通常来自于 MIDI输入,它本身可以来自乐器或者软件合成器。
Generator units:生成音频数据的,但不包括通过MIDI源,一些通过编程的方式生成的信号,或者是从音频文件或者网络流中获取的数据。
I/O units :提供输入或者输出硬件的接口,如麦克风或者扬声器。这些通常是在硬件抽象层(HAL)上实现的,这是core Audio 和 I/O 以及驱动程序之间可以操作的部分。
Converter units: 可以在Canonical Formats 和其他格式进行转化的东西,除此之外还可以合并或者拆分流,改变时间和音调。
Mixer units:合并 audio tracks,也可以进行拆分,一个输入变成多个输出。
Panner units:使用立体声混音创建平移效果。
Offline effect units : 对于无法实时被处理的数据进行的一些操作
因为它运行在手机上, CPU 不够强大,对耗电量比较敏感,iOS 只提供上面单元中的一部分。实际上 ios 4 不包含 instrument, generator, or panner units。
为了理解工作单元是如何连接工作的,我们可以假设的应用程序,它从输入设备(例如麦克风)捕获音频,并将该音频与其他一些源(节奏和纯音乐)(例如文件播放)混合。例如,这可以是一个唱歌的应用程序,让用户可以跟着喜欢的歌曲一起唱歌。
这种可以看做四个units组成,一个Generator units开始播放来自文件或网络的音频,以及一个 I/O units 从输入硬件获取音频。然后将这两个单元的输出接到 mixer unit 的输入,混合后你就可以再把 I/O 单元的输出 接到硬件。例如麦克风或者耳机上。该过程涉及更多,正如您将在第 7 章“音频单元:生成器、效果和渲染”和第 8 章“音频单元:输入和混合”中看到的那样,但这是基本流程。
在 Mac OS X 上,您可以编写自己的音频单元并将它们部署到您的应用程序中。您还可以提供 GUI 来调整自定义单位的设置并使它们可用于其他应用程序。这在 iOS 上是不可能的,因为应用程序是“沙盒化”的,您无法为其他应用程序提供调用功能。第 7 章更多地讨论了音频单元。
拉的模式
但如果你有一堆units,它们如何协同工作?您是否只是创建一堆units并将数据推送到其中?In Core Audio’s pull architecture,工作原理恰恰相反,在 a pull architecture,framework 会告诉开发者不用调用它,框架会告诉你的。你如果有一串audio units,最后一个(可能是将音频发送到扬声器或耳机的 I/O 单元)从连接到其输入元素的单元中pull,实际上是说,“给我一些数据播放。”这些单元调用它们更上游的单元。
回调有时在 Core Audio 中被称为 procs。在 Audio File Services API 中,接收数据的回调 是一个read proc,产生数据的回调是 write proc。
core audio 的别的部分使用不同的技术,例如:当音频通过代码获取采样样本时,他被成为渲染回调,这是不同的概念,但是相同的思路。尽管概念简单,但是回调可能比较不好搞。在下一章你将创建第一个回调,在那里你创建一个Audio Queue ,他将会捕获到数据进行回调,有趣的是,callback 通常遵循 Core Audio 面向属性的设计模式:不是调用“设置音频单元回调”函数来设置回调,而是在音频单元上设置回调属性。
Core Audio 广泛使用回调是很方便。例如,许多 API 使您能够将函数注册为属性侦听器。当属性发生变化时,被监视的 API 调用注册的函数。例如,在 iPhone 上,一个属性表示音频到当前输出设备的路径:扬声器、耳机或电话听筒。如果用户拔出耳机,此路径属性会发生变化,并且应用程序会收到回调,这使代码有机会采取行动,例如暂停播放或录制。(其实就是可以监听到拔出耳机,然后就可以做其他操作)
总结
本章建立在第 2 章“声音的故事”的理论基础之上,已帮助您了解数字音频的概念是如何在 Core Audio 的基本数据类型中表示的。在您可能遇到的所有可能的数字音频格式中,您可以使用 AudioStreamBasicDescriptions、AudioStreamPacketDescriptions 和magic cookies (告诉 Core Audio 如何理解每个音频数据缓冲区)。
您还可以通过使用Audio File Services 检查支持的音频文件类型并告诉您可以放入哪些类型的数据,了解 Core Audio 的内置格式支持有哪些。最后,您通过了解音频单元了解了处理以及他们的联系,并深入研究 Core Audio 如何通过这些单元提取音频。
有了这些材料和第 1 章对 Core Audio 编程的常用习语(属性、四字符代码等)的讨论,您现在已经准备好深入研究 Core Audio 提供的 API 的细节。对于许多人来说,音频框架最自然的用途是用文件播放音频并将音频录制到文件中。您已经在前三章的示例中使用了音频文件服务;在下一章中,您将把它与音频队列服务结合起来播放和录制音频。