既然是Overview,那么这一章文字的内容会占绝大部分比例。
CoreAudio是一堆框架的集合,他们通常被分为两组:audio engines,用来处理音频流;helper APIs,提供了便捷的方法:从engine中获取音频数据或者将音频数据写入engine或用音频数据做一些其他的事。
Audio Units(音频单元)
每一个单元从某处(硬件、另一个音频单元、回调函数等等)接收一个音频数据的buffer,对它执行某些工作(比如施加特效),然后将buffer传递给另一个单元。一个单元能够潜在地拥有许多输入和输出,这样它就可以将多条音频流混缩成一条输出
一个在音频单元之上的抽象类,使用AQ能轻易的播放和录制音频,并且不需要担心当直接操作时间约束的I/O的音频单元时会出现的线程编程挑战(因为AQ都已经帮你搞定了:P)。使用音队进行录音需要设置一个回调函数,每当来自输入设备的新数据可用的时候重复地接收这些数据的buffer;要使用音队播放音频,则需要使用音频数据来填充buffer然后将这些buffer传递给音队
用来创建3D音频(环绕音)的具有工业标准的API,并且它的设计和OpenGL图形标准很相似。当你使用它的时候你会觉得它非常的酷炫
Audio File Services
这个API抽象出了各种音频文件的容器格式的详细内容,也就是说,你不需要通过写代码具体的指明你需要访问哪种音频格式的特性(比如AIFF,WAV,MP3),这个API可以让你直接获取或者设置该音频数据包含的格式,然后对它们进行读写
如果你的音频来自网络,这个API能帮助你找出这个音频在网络流中的格式。所以你可以将音频流提供给其中一个播放引擎或者用其他感兴趣的方式处理它。
音频可以以多种格式存在。而当一个音频被送达音频引擎(audio engine)的时候,它应该是未经压缩的可播放的格式(LPCM)。这个API能够帮助你在编码后的音频格式(比如AAC、MP3)和无损原始样本(直接通过音频单元获取的)之间进行转换
Audio Converter Services和Audio File Stream Services的结合,它能让你在对音频文件进行读取或者写入的同时能进行格式转换。举个栗子:从一个文件中读取AAC格式的数据然后在内存中将它转换成非压缩的PCM格式,这样的操作在Extended Audio File Services中可以只用一次调用完成。
大多数CoreAudio框架都涉及处理你从其他资源获取或者从输入设备中抓取的音频采样。通过Core MIDI框架,你可以通过描述音符以及它们是如何被播放出来的(比如,它们听起来像钢琴还是夏威夷四弦琴)来迅速地合成音频。
Audio Session Services
这个仅iOS支持的框架允许你的App使用其他系统整合它用的音频资源。举个栗子,你使用这个API声明了一个音频“类目”,它决定了iPod音频是否能够在你的app播放时继续播放,以及铃声/静音开关是否能够使你的app变成静音。
当你开发应用程序的时候,你会用一些有趣的方式将这些API联合起来。举个栗子,你可以使用Audio File Stream Services来从一个网络无线电流获取音频数据然后使用OpenAL把这段音频放到3D环境中一个指定的位置。
我们通过调用C函数来调用CA框架,所以得时刻准备好处理C编程问题和事件,比如指针、手动内存管理、结构体、枚举。
在C语言中没有类,对象,封装等主要的特性,然而和苹果的其他基于C语言的框架一样,CA提供了大量的这些现今的特性,甚至用C语言的语法呈现了这些特性。
苹果的模型C框架是Core Foundation,它是Foundation框架的底层,是最基础的OC框架,几乎所有的Mac和iPhone应用都会用到。
我们通过类来认识Foundation框架,比如NSString, NSURL, NSDate,NSObject。
而在许多情况下,OC类通过调用CF来组装它们的功能,CF提供了不透明类型(指向数据结构的指针,而这些数据结构真正的成员被隐藏了)以及作用于这些对象之上的函数。比如,一个NSString在字面上是等同于CFStringRef的(你可以在他们之间任意转换),并且它的length方法也和CFStringGetLength()函数是等价的,这个函数会将一个CFStringRef作为它的对象。通过一致的函数命名规范将这些不透明类型联结起来,CF就这样提供了高可管理的C API,它们和你在Cocoa中使用的异常相似。
CA的设计是(和CF)非常相似的,CA的许多重要对象(比如audio queues 和audio files)被当做不透明对象,传递给比如AudioQueueStart(),AudioFileOpenURL()这样的意料之中命名的函数。CA并没有明确地构建在CF之上——AudioQueueRef严格上来说并不是一个CF不透明类型。然而,它确实用到了CF的重要的对象,比如CFStringRef和CFURLRef,它们可以随意在你的Cocoa编码中与NSString和NSURL相互转换。
好,现在让咱们实际写点东西来感受下Core Audio的代码。Audio engine APIs有大量的移动部件,因此会更加的复杂。所以我们将会以使用一个简单的helper APIs作为开始。在这个示例中,我们将会去设法从一个音频文件中获取元数据(关于这个音频的信息)。
注意:Core Audio并没有默认被包含到Xcode的命令行可执行工程中,所以我们需要手动在Build phase中添加framework:AudioToolbox.framework。然后将AudioToolbox.h import到代码中。我们将手动将音频文件路径作为工程的参数添加进工程中,通过argv参数获取。
打开Edit Scheme
选择Run下的Arguments栏,点击加号输入路径。如果路径中有空格,要用双引号将整个路径引起来
#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>
int main(int argc, const char * argv[]) {
// argv表示传递给这个程序的参数数组,是一系列字符串
// argc表示argv中元素的个数
// 默认情况下argv只有一个元素:该程序本身的路径。而我们可以手动为其添加参数
@autoreleasepool {
if (argc < 2) {
printf("Usage: no file !\n");
return -1;
}
printf("%s",argv[0]); // 1
// 如果提供了路径,需要将它从c字符串转换成NSString或者CFStringRef这种苹果大多数框架都回用的。
// stringByExpandingTildeInPath表示字符串会自动在前面加上~来代表根目录
NSString * audioFilePath = [[NSString stringWithUTF8String:argv[1]] stringByExpandingTildeInPath]; // 2
// AudioFile API使用的是URL来代表文件路径,所以把字符串再转换成URL
NSURL * audioURL = [NSURL fileURLWithPath:audioFilePath]; // 3
// CoreAuido使用AudioFileID类型来指代音频文件对象
AudioFileID audioFile; // 4
// 大部分CoreAuido调用函数成功或失败的信号通过一个OSStatus类型的返回值来确认。
// 除了noErr信号外的信号都代表发生了错误。
// 我们应该在【所有的】CoreAuido函数调用后检查这个返回值因为前面已经发生错误了,后续的调用就显得毫无意义。
// 比如,如果我们不能成功创建一个AuidoFileID对象,那么我们想从这个对象代表的那个文件中获取音频属性就完全是徒劳的。
OSStatus theErr = noErr; // 5
// 来到了我们第一次调用的CoreAudio函数:AuidoFileOpenURL。它有4个参数:CFURLRef,文件权限的flag,文件类型提示和一个接收创建的AudioFileID对象的指针。
// 第一个参数:我们可以直接通过强制类型转换把一个NSURL转换成CFURLRef(当然我们需要加上关键字__bridge。)
// 第二个参数:文件操作权限,我们这里只需要读取数据的权限,所以传递一个枚举值(苹果一贯的命名规范)。
// 第三个参数:我们不需要提供任何文件类型提示,所以传0,这样CoreAudio就会自己来解决(其实这个参数我都没搞懂是什么意思,反正一般传0就对了)
// 第四个参数:传一个接收AudioFileID对象的指针,传入我们之前声明的AudioFileID类型的变量地址就行了。
theErr = AudioFileOpenURL((__bridge CFURLRef)audioURL, kAudioFileReadPermission, 0, &audioFile); // 6
// 如果上面的调用失败了,那么直接终止程序,因为以后的所有操作都没意义了。
assert(theErr == noErr); // 7
// 为了拿到这个文件的元数据,我们将会被要求提供一个元数据属性,kAudioFilePropertyInfoDictionary。但是这个调用需要为返回的元数据分配内存。所以我们声明这么一个变量来接收我们需要分配的内存的size。
UInt32 dictionarySize = 0; // 8
// 为了得到我们需要分配多少内存,我们调用AudioFileGetPropertyInfo函数,传入你想要拿到数据的那个文件的AudioFileID、你想要啥子信息、一个用来接收结果的指针、以及一个指向一个标识变量的指针用来指示这个属性是否是可写的(我们对此毫不在乎,所以传0)。
theErr = AudioFileGetPropertyInfo(audioFile, kAudioFilePropertyInfoDictionary, &dictionarySize, 0); // 9
assert(theErr == noErr); // 10
// 为了从一个音频文件获取属性(这里我们要的是这个音频文件的元数据)的调用,需要基于这个属性本身填充各种类型(这里是一个字典)。有的属性是数字,有的是字符串。文档和CoreAudio头文件描述了这些值。我们在第二个参数传入kAudioFilePropertyInfoDictionary就可以得到一个字典。所以我们声明这么一个变量它是CFDictionaryRef类型的对象(它可以随意转换成NSDictionary)。
CFDictionaryRef dictionary; // 11
// 啊,我们终于到了最终的时刻了,终于要开始获取属性了。调用AudioFileGetProperty函数,传入AudioFileID、一个常量(枚举类型,表示属性类型)、一个指向你准备好用来接收的size的指针、一个用来接收最终结果的指针(就是一个字典了)
theErr = AudioFileGetProperty(audioFile, kAudioFilePropertyInfoDictionary, &dictionarySize, &dictionary); // 12
assert(theErr == noErr); // 13
// 我们来看看得到了什么。对任意的CoreFoundation或者Cocoa 对象都可以使用"%@"在格式化字符串里面来获取一个字典的字符串表示。
NSLog(@"dictionary : %@", dictionary); // 14
// Core Foundation没有提供自动内存释放,所以CFDictionaryRef对象在传入AudioFileGetProperty函数后它的retain count是1。我们用CFRelease函数来释放我们对这个对象的兴趣。
CFRelease(dictionary); // 15
// AudioFileID同样需要被清空。但是它本身并不是一个CoreFoundation对象,因此它不能通过调用CFRelease释放。取而代之的,它有自己的自杀方法:AudioFileClose()。
theErr = AudioFileClose(audioFile); // 16
assert(theErr == noErr); // 17
// 结束了。我们用了二十多行代码,但是实际上都是为了调那么三个函数:打开一个文件、为元数据分配容器、获取元数据。
}
return 0;
}
来运行一下,看看我们打印出了什么:
/Users/dreamhack/Library/Developer/Xcode/DerivedData/MyFirstCoreAudioProj-ckhojjygpvgkcgcwmohckworckis/Build/Products/Debug/MyFirstCoreAudioProj2015-04-03 11:21:38.851 MyFirstCoreAudioProj[636:303] dictionary : {
album = “If I Didn’t Have You (Bernadette’s Song - From the Big Bang Theory) [MusiCares\U00ae Version] - Single”;
“approximate duration in seconds” = “139.572”;
artist = “Simon Helberg, Johnny Galecki, Jim Parsons, Kaley Cuoco, Kunal Nayyar & Mayim Bialik”;
comments = “(MusiCares\U00ae Version)”;
“encoding application” = “iTunes 11.1.2”;
genre = Soundtrack;
title = “If I Didn’t Have You (Bernadette’s Song - From the Big Bang Theory) [MusiCares\U00ae Version]”;
“track number” = “1/1”;
year = 2013;
}
你要随时做好准备应对未知结果,比如对MP3和AAC文件的不同级别的元数据支持。掌握CoreAudio并不仅仅是理解这些API,还包括提高实现的意识,这个库究竟是怎样工作的,它好在哪里,它的短处在哪。
CoreAudio并不止是你调用它的语法,也包括它的语义。在某些情况下,语法正确的代码可能会在实践中出错,因为它违反了隐式协议、在不同的硬件中表现得不一样、又或者它在一个时间约束的回调中占用了太多的CPU时间。成功的CoreAudio程序猿在当事情并没有像他们预期的那样或者第一次运行时并不足够好的时候并不会鲁莽的继续下去。你必须尝试找出究竟发生了什么并想一个更好的方法。
在上面的例子中,CoreAudio的调用全部都是关于从音频文件对象中获取属性。随时准备和执行属性获取器和设置器的调用在CoreAudio中是日常规范,是非常必要的。
因为CoreAudio是一个属性驱动的API。属性是键值对,而键是枚举整型数。值可以是API定义的任何类型。每一个CoreAudio的API都通过它的属性列表来交流它的功能和状态。比如,如果你查看AudioFileGetProperty()函数,你会在文档中找到一个音频文件属性列表的链接:
kAudioFilePropertyFileFormat = ‘ffmt’,
kAudioFilePropertyDataFormat = ‘dfmt’,
kAudioFilePropertyIsOptimized = ‘optm’,
kAudioFilePropertyMagicCookieData = ‘mgic’,
kAudioFilePropertyAudioDataByteCount = ‘bcnt’
…
这些键是32位整数值,你可以在文档和头文件中读到。你可以看到,这4位字符编码被单引号引了起来代表C字符。假设fmt 是“format”的简写,你会发现ffmt 是”file format”的编码而dfmt 则意为“data format”。这样的编码贯穿于整个CoreAudio作为属性的键有时也作为错误状态码。如果你企图写入一个CoreAudio不认识的文件格式,你将会得到fmt? 响应,也就是kAudioFileUnsupportedDataFormatError。
你通过这些API获取或者设置的值取决于属性的设置。通过kAudioFilePropertyInfoDictionary属性将得到一个CFDictionaryRef的指针,但是如果传入的是kAudioFilePropertyEstimatedDuration,你需要准备接收一个NSTimeInterval(实际上就是一个double)的指针。这是及其强大的,因为只需要少量的函数就可以支持潜在无限多的属性。然而,建立这样的调用需要涉及额外的工作,代表性地,你不得不调用“get property info”来为接收属性值分配内存或者检查属性是否是可写的。
另一个需要注意的是CoreAudio函数参数的命名规范。我们来看看AudioFileGetProperty()的定义:
OSStatus AudioFileGetProperty (
AudioFileID inAudioFile,
AudioFilePropertyID inPropertyID,
UInt32 *ioDataSize,
void *outPropertyData
);
注意到这些参数的命名:使用in,out或者io指示一个参数被这个函数是否只用作输入(前两个参数,指定了要使用哪个文件和你想要的属性),是否只用作输出(第四个参数,outPropertyData 用属性的值填充一个指针指向的内容),是否既用作输入也用作输出(第三个参数,ioDataSize,接收你分配给outPropertyData的内存缓冲区的大小,然后写回实际上被写入缓冲区的字节数)。这种命名模式贯穿整个CoreAudio,特别是一个参数用指针来填充其值的时候。
这一节总览了Core Audio许多不同的部分,尝试了使用Audio File Services来编程获取一个本地磁盘的音频文件的元数据属性。我们看到了CoreAudio如何将属性作为一个关键的语义来和它不同的API协同工作。我们同样看到了CoreAudio如何使用4字符编码来指定属性键和错误信号。
当然你现在还不需要真正的去处理音频本身。如果你想那样的话,你首先需要理解声音在数字形态下是如何被表示以及处理的。接下来为了与音频数据工作,准备刻苦钻研CoreAudio的API吧。