iOS CoreAudio学习笔记(二)—— The Story of Sound

来源:互联网 发布:心电图数据怎么看 编辑:程序博客网 时间:2024/06/02 03:12

在上一章,我们初次尝试了CoreAudio API:它提供了什么以及怎样调用它的函数。现在是时候往回一步来看看一张更大的图:一开始CoreAudio访问的问题。

这一章将介绍基础的声音科学,它是什么,它怎样工作。事实证明,计算机的数字化天性使它们并不那么适合处理连续的模拟信号。这引导了对信号采样的思想,或者将平滑的声波斩为频率足够大的离散值,而人耳无法注意到差别。这一章覆盖了这些采样在数字化形态中是怎样被表示和整理分类的。

  • Making Waves
  • Digital Audio
  • DIY Samples
  • Buffers
  • Audio Formats
  • 总结

Making Waves

如果你在人行道上猛推一个人,那个人就会动一下而已。但是如果你在一个拥挤的地方猛推一个人,比如在音乐会上,他会碰到他前面的人然后弹回他原来的位置。由于你推的力在人群中传递,这将会触发一个连锁反应,最终穿过房间打到你的朋友。你刚刚通过一群生气的代理推了你的朋友。

在科学术语里面,一群足够近的相互反弹的物体被叫做介质,能量可以通过它们传递。你推的那个人的运动(一开始向前,然后向后)称为一个循环。完成一个循环的时间叫做周期。如果你反复地推一个人,人们相互反弹的这种模式就是一个压缩的波。

这个波有两个属性。你推这个波的力形成了波的振幅。你推这个波的速度形成了波的频率。你越频繁地推,这个波的频率就会越高。如果你改变你推的振幅和频率,你的朋友将会感受到这样的变化。你不再只是生产波,而是在通过介质传递数据。

当你对某人说话,你的声带来回移动或者振动,推动空气中的原子。这些原子相互反弹就像音乐会里面的人一样,直到它们击中你朋友的鼓膜。扬声器和麦克风就是这样做的。声音就是通过介质传递的能量而已。

要录音的话,你所需做的就是描述声音使膜振动的方式。要播放声音的话,你需要像提供的描述那样让一个膜振动。一种描述声音的方式是基于时间画出膜的位置,如图所示。
与振动的膜位置相关的声波

Figure 2.1

图中央的横线代表膜静止时的状态。顶部和底部代表膜位移的最大值。随着时间从左到右移动,膜的位置一上一下。y轴代表振幅最大值的百分比,正值和负值来回变化的频率代表声音的频率。

一种表示这段声波的方法是把它的图像雕刻成一些物理对象。这种技术叫做模拟记录法,它的好处在于它将产生一个非常精确的复制品。而它的缺点在于录音会像它描述的声波那样不精确。这样的不精确来源自复制的同时播放。它同样不兼容计算机,因为计算机需要的东西要有精确的数值计算来描述。

Digital Audio

对于计算机,你需要把波通过数字来表示。你可以沿着波的路径绘制一系列(x,y)坐标点来近似这段波。如果你提供了足够多的点,你将会得到一个不错的展现。一个标准叫做脉冲编码调制(pulse code modulation, PCM) ,它每隔一段规律的时间记录一次y值,意思是x值(时间)是隐式的。其最常见的形式是线性脉冲编码调制,你指定一个代表y值最大值百分比的值。举个栗子,如果一个图表示的值从0到1,给出的一个点的y值是最大值的一半,那么你指定的值就是0.5。这种在规律的时间间隔的时候指定一个值的处理叫做采样。每个值是它本身的一个采样。

CD音质的音频采样率为44.1 kHz,也就是每秒44100个采样。一种看清这个的方法是认识到CD音质的音频每秒有44100个少于23微秒的间隙。没有任何数据存在于这些间隙中,在这段时间发生在声波上的任何东西都消失了。下图阐述了采样率是如何影响数据精确地模拟声波曲线的能力的。三张图都代表了Figure2.1的波,但是削减了采样率。如你所见,使用更少的采样会使得声波表示得更不精确。事实上,t=2.5的峰值和t=2.1的谷值以及t=4.4的数据在最后一张图中完全遗失了。
a
b
c

Figure 2.2 某采样率下的声波模拟

你可以通过增加更多的采样模拟的更加精确,但是知道存在一些“足够好”的粒度是很有帮助的。找到一个足够好的模拟的关键在于你听到的不是个别的采样,而是通过空气推出的声波。你听到的是频率,振动的重复形式,无论是从吉他和弦还是喉咙发出来的。你需要弄明白需要多快的采样来再现这些振动频率。

事实上你可以从奈奎斯特-香农采样定律(Nyquist-Shannon Sampling Theorem)中找到这个数字,这个定律说,如果你有一个一个函数表示没有频率能高过B Hz,那么你可以用间隔1/(2B)秒分开的点精确地呈现它。对于音频,这就意味着要再现任何频率,你需要用两倍或更高频率采样信号。

这就解释了为什么CD音质的音频的采样率为44.1 kHz。它的一半是22.05Hz,超出了绝大部分人类可以听到的频率。感知到高频率声音的能力随着年龄的增长而恶化。年轻人差不多能听到20kHz,而一个中年人可能只听得到14或者15kHz。所以通过44.1kHz的采样,你可能会丢失一些信息,但是不用担心,真正重要的其实是听众在音频信号中感知到振动频率的能力。

每一个采样代表了波的振幅,或者膜的位移,作为最大可能值的百分比。使用百分比的好处在于硬件独立性。一半就是一半,直接忽视什么被折半了。

不像整数那样越大的数需要更多的数字来表达,分数需要更多的数字来表达更小的数。写100比写10需要更多的数字,但是写1/100要比写1/10需要更多的数字。一个采样拥有多少数字叫做它的位深(像素深度)。如果两个声音的差距(最大可能位移的百分比)比采样拥有的数字还小,那么这个差距就将会丢失。

位深(每个采样拥有的bit量)乘以采样率(每秒采样的量)得到比特率(每秒bit量)。这就描述了音频1秒钟需要多少个bit。更高的比特率会提供更高质量的录音,但是那也意味着硬件需要存储和处理更多的bit。

数字高保真的根本问题在于通过给定限制的硬件找到最好的近似值。每一个不同的格式都是一系列不同的妥协解决这个问题的方法。不仅在数字音频中,更广泛地,举个栗子,在数码相片中同样有这么多问题,对于相同的格式字母汤,每种格式提供自己的解决方案。

在数字图像中,采样率被转换成了像素值,而位深则转换成了每像素的颜色值。增加一个bit将会得到两倍的收益。在图2.3中两个相邻图像的差距为1bit。
数码相片的位深和采样率

Figure 2.3

一个琐碎的实现细节是,这个比喻只适用于灰度图像。计算机不能像人类看见的那样确切地显示颜色像素。每一个颜色像素点由红、绿和蓝组成。每一点又需要它们自己的数据集,叫做通道。大多数图像格式将每一个通道的采样结合成一坨表示单个像素。

数字音频也存在这些问题。一段单声道声波就像一张灰度图一样,但是许多声音系统都有多道发声器。就像像素需要红色、蓝色和绿色作为通道一样,立体声需要左声道和右声道。环绕声增加了额外的通道。一个典型的5.1环绕声信号有6个通道:左右声道来处理前和后;一个中心通道;一个无方向通道来处理低频效果,比如贝斯。

和它们的图形同胞一样,音频格式把每个通道的一个采样结合成一坨,称为帧。鉴于一个像素代表在空间中一个区域里面所有的颜色通道,一帧代表在时间中一个时刻所有的音频通道。所以对于单声道而言,一帧只有一个采样;而对于立体声则有两个采样。如果你把多通道的声音放入一个流,则它们被称为交错模式 。对于播放这是很常见的:因为你想要同时读取所有的通道,有理的做法就是安排数据简单地这样做。然而,当处理音频(比如添加效果或者做一些其他的信号处理的时候)时,你可能想要非交错模式的音频,那么你就可以分离地关注每一个通道了。

一些音频格式将许多帧结合成分组。这一概念完全是一个给定的音频格式创建和代表在该格式的一个不可分割的单元。LPCM不使用分组,但是压缩的音频格式则会使用,因为它们使用随一组采样的数学技术来获得它们的近似值。一个给定的采样可以经常被预测成某个音阶,通过那些围绕着它(音阶)的,所以压缩格式能使用采样组,安排为帧,来通过比无损的源LPCM更少的数据生成相似的(如果不是完全相同的话)波。

我们之前提到了比特率,在某个格式中用来表示音频一个给定时间的周期的数据量。对于PCM,比率是常量:CD音质的音频的比特率为1411200bits每秒或者1411kbps,因为它有2通道16位*44100每秒的采样。PCM拥有一个常量比特率因为对于一个给定通道数量、位深和采样率的组合,数据率永远不会改变。有损格式经常使用可变比特率* ,意味着需要压缩的数据的任意特定部分的数据量会发生变化。CoreAudio支持可变比特率格式:对于任意给定帧的数据量可能是不同的,而分组则保留相同大小。CoreAudio同样支持可变分组率,这样甚至分组率也可能改变。然而,目前没有可变分组率格式被广泛使用。

关心这个区别的一个原因在于恒定比特率(CBR)数据有时采用比可变比特率(VBR)更简单的API调用。举个栗子,当你从一个文件或者流里面读音频数据的时候,VBR数据提供给你一个数据块和一个分组描述的数组来帮你弄清楚哪些采样在什么时候去了。而对于CBR这就不是必要的了,每一帧的数据量都是不变的,所以你可以通过帧大小乘以时间得出一个给定时间的采样。

DIY Samples

我们已经谈论了大量关于采样和音频波。你自己来写一些东西可能有助于理解音频采样。

我们将再次使用Audio File Services,这一次创建一个文件然后写一些原生的采样到里面去。创建一个命令行工具工程取名为CAToneFileGenerator。添加AudioToolbox.framework到工程里面然后重写main.m:

#import <Foundation/Foundation.h>#import <AudioToolbox/AudioToolbox.h>// 把文件名define到一个宏里面,这样一会可以改名字。// 这里我们创建一个方波,是最简单的一种波的形式#define FILENAME_FORMAT @"%0.3f-square.aif"#define SAMPLE_RATE 44100               // define一个44100采样每秒的采样率#define DURATION    5.0                 // define你想要创建多少秒的音频int main(int argc, const char * argv[]) {    @autoreleasepool {        if (argc < 2) {            printf ("Usage: CAToneFileGenerator n\n(where n is tone in Hz)");            return -1;        }        // 和上次一样,我们需要一个命令行参数        // 这一次是一个浮点型数作为你想要生成的音的频率        // 如果你想要运行这个程序,就要到Scheme Editor里面去像上次那样设置参数。        // 你可以把音符频率设置成261.626,这是钢琴上中央C的频率,或者440,是在C之上的A(叫做中央A)        double hz = atof(argv[1]);           assert(hz > 0);        NSLog(@"generating %f hz tone", hz);        // 这两行代码生成一个文件路径,使用了我们的宏和频率来生成名字文件名,比如261.626-square.aif        // 然后它们来生成一个NSURL因为Audio File Services函数要的是URL而不是文件路径        NSString * fileName = [NSString stringWithFormat:FILENAME_FORMAT,hz];          NSString * filePath = [[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:fileName];        NSLog(@"%@",filePath);        NSURL * fileURL = [NSURL fileURLWithPath:filePath];        // prepare the format        // 要创建一个音频文件,你必须要提供一个这个文件包含的音频的描述。        // 你要用到的可能是CoreAudio最重要和最常见的数据结构,AudioStreamBasicDescription        // 这个结构体定义了一个音频流最普遍的特征:它有多少声道,它在什么格式下,比特率等等        AudioStreamBasicDescription asbd;          // 在有些情况下,CoreAudio会为一个AudioStreamBasicDescription填充一些区域而你在编程的时候完全不知道        // 要这样做,这些区域必须被初始化为0。作为一次普通的实践,请一直要在设置它们任何一个之前使用memset()把ASBD的区域清空        memset(&asbd, 0, sizeof(asbd));         // 接下来的8行代码使用ASBD各自的区域来描述你要写入文件的数据。        // 这里,它们描述了一个流:        // 只有一个声道(单声道)的PCM,数据率为44100        // 使用16位采样(再说一次,和CD一样),所以每一帧为2个字节(1声道*2字节的采样数据)        // LPCM不使用分组(它们只对可变比特率格式有用)所以bytesPerFrame和bytesPerPackt相等。        // 其他针对一个音符的区域是mFormatFlags,它的内容是不同的,基于你用的格式        // 对于PCM,你必须表明你的采样是大端模式(字节或者文字的的高位在数字上对其意义的影响更大)亦或相反。        // 这里你要写入一个AIFF文件,它可以只取大端模式的PCM,所以你需要在你的ASBD中设置它。        // 你同样需要表明采样的数值格式(kAudioFormatFlagIsSignedInteger)        // 以及,你传入的第三个标识来表明你的采样值使用每一个字节的所有可用位(kAudioFormatFlagIsPacked)。        // mFormatFlags是一个bit区域,所以你可以使用算术或运算符(|)把这些标识结合到一起        asbd.mSampleRate = SAMPLE_RATE;        asbd.mFormatID = kAudioFormatLinearPCM;        asbd.mFormatFlags = kAudioFormatFlagIsBigEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;        asbd.mBitsPerChannel = 16;        asbd.mChannelsPerFrame = 1;        asbd.mFramesPerPacket = 1;        asbd.mBytesPerFrame = 2;        asbd.mBytesPerPacket = 2;        // set up the file        AudioFileID audioFile;        OSStatus audioErr = noErr;        // 现在你可以让CoreAudio创建一个AudioFileID了,准备用来写在你设置好的URL        // AudioFileCreateWithURL()函数接受一个URL(注意到你再次使用桥接来从一个Cocoa NSURL转换到CoreFoundation CFURLRef)        // 一个用来描述AIFF文件格式的常量        // 一个指向描述音频数据的AudioStreamBasicDescription的指针        // 一个行为标识(在这里,表明你希望如果已存在同名的文件就把它覆盖掉)        // 一个用来填充我们创建的AudioFileID的指针        audioErr = AudioFileCreateWithURL((__bridge CFURLRef)fileURL,                                          kAudioFileAIFFType,                                          &asbd,                                          kAudioFileFlags_EraseFile,                                          &audioFile);                assert(audioErr == noErr);        // start writing samples        // 你马上就可以准备完毕写采样了。        // 在我们进入这个写采样的循环之前,你要计算一下在每秒SAMPLE_RATE个采样下对于DURATION秒的声音需要多少采样        // 随着一个计数变量,sampleCount        // 你定义了bytesToWrite作为局部变量,只因为写采样的调用需要一个纸箱UInt32的指针。        // 你不能就直接把这个值放进参数        long maxSampleCount = SAMPLE_RATE * DURATION;           long sampleCount = 0;        UInt32 bytesToWrite = 2;        // 你需要跟踪在一个波长里面有多少采样        // 然后你就可以计算组成一个波需要多少采样值        double wavelengthInSamples = SAMPLE_RATE / hz;          while (sampleCount < maxSampleCount) {            for (int i = 0; i < wavelengthInSamples; i++) {                // Square wave                SInt16 sample;                // 对于第一个例子,你将会写最简单的波之一,方波。其采样是非常简单的                // 对于波长的前半部分,你要提供一个最大值,对于波长的剩下部分,你提供一个最小值                // 所以仅有两个可能的采样值可能会呗用到:一个高的和一个低的。                // 对于16位有符号整数,你要使用C常量来代表最大值和最小值:SHRT_MAX和SHRT_MIN                if (i < wavelengthInSamples/2) {                           // 你在ASBD中声明了将大端有符号整数作为音频格式,所以你不得不在这个格式中小心地持有者2字节的采样                    // 现代Mac运行中小端模式的Intel CPU上,而且iPhone的ARM处理器也是小端的                    // 所以你需要把CPU表示的字符切换为大端模式。CoreFoundation函数CFSwapInt16HostToBig()会帮到你。                    // 这个调用同样可以在大端模式的CPU上,比如老Mac上的PowerPC,因为它会意识到主机的格式是大端模式然后就什么也不做                    sample = CFSwapInt16HostToBig(SHRT_MAX);                } else {                    sample = CFSwapInt16HostToBig(SHRT_MIN);                }                // 已经计算好了你的采样,使用AudioFileWriteBytes()把它写进文件。                // 这个调用接受5个参数:AudioFileID用来写入、缓存标识、你要写的音频数据的偏移量                // 你要写的字符数和一个指向被写入字符的指针。                // 你可以使用这个函数因为你拥有常量比特率数据。                // 在更多的一般情况下,比如写一个有损格式,你必须使用更负责的AudioFileWritePackets()                audioErr = AudioFileWriteBytes(audioFile, false, sampleCount*2, &bytesToWrite, &sample);                    assert(audioErr == noErr);                // 增量sampleCount,然后你就一点点地将新数据写进文件                sampleCount ++;                   }        }        // 最后调用AudioFileClose()来完成并关闭文件        audioErr = AudioFileClose(audioFile);         assert(audioErr == noErr);        NSLog(@"wrote %ld samples",sampleCount);    }    return 0;

编译并执行这个程序。为了找到我们写入的文件,打开Organizer,进入Project选项,选择CAToneFileGenerator工程,然后点击Derived Data path右边的圆形箭头。这回打开一个工程元数据和成品的Finder窗口,在里面你可以找到路径Build/Products/Debug。在这个文件夹中,你应该可以看到CAToneFileGenerator的可执行文件和一个声音文件,名字代表你设置的命令行参数代表的频率,比如880.000-square.aif。你可以用QuickTime Player、iTunes或者直接在Finder里面选中它然后按空格 —— 但是在你那样做之前,请把音量调到最小!方波对于我们的耳朵来说是非常重的。

这就带来了一个重要的观点。波重复的频率就是你感知到的音调:一个声音多高或者多低。但是那并不是故事的全部。波的形状赋予了声音它的角色,它的音色。

考虑一下三种最基础的波形,你可以简单地通过程序来创建:

  • 方波,如你所见,只是在两个值之间循环交替。
  • 锯齿波在一个波长中从最大值到最小值拥有线性的增量,然后在下一个波到来时重置为最小值。
  • 正弦波是准许你三角函数属性的曲线。它们听起来更加自然一点,因为正弦函数可以表示简单的谐波运动,这类似于自然现象,比如琴弦的振动。

下面三个图展示了三种波:
方波

方波

锯齿波

锯齿波

正弦波

正弦波

修改程序来生成这些不同的波类型是小菜一碟。我们先拿锯齿波开刀。首先改变#define文件名格式,这样你就可以区分多个文件:

#define FILENAME_FORMAT @"%0.3f-saw.aif"

然后重写循环:

for (int i=0; i<wavelengthInSamples; i++) {    // 锯齿波    SInt16 sample = CFSwapInt16HostToBig (((i / wavelengthInSamples) * SHRT_MAX *2) - SHRT_MAX);    assert (audioErr == noErr);    sampleCount++; }

唯一的不同在于采样的计算。这个方法用i除以波长然后使用一些缩放所以值会均匀地从SHRT_MIN增加到SHRT_MAX。

在你编译并执行这个版本后,你可以比较方波的声音和锯齿波的声音。你可以听到它们拥有相同的音调,但是声音略有不同。

如果你有音频编辑器来降低原生采样等级,你同样可以用它来检查这些文件。

我们再次来调整代码,这一次来生成正弦波。这是经典参考信号。在电视中,”酒吧和音”过去常常使用一个工程师能用示波器验证的1000Hz的音来校验设备。首先修改文件名格式:

#define FILENAME_FORMAT @"%0.3f-sine.aif"

然后重写for循环。你可以重用锯齿波的for循环然后只改变计算采样的那一行:

sample = CFSwapInt16HostToBig ((SInt16) SHRT_MAX * sin (2 * M_PI * (i / wavelengthInSamples)));

Yeah,欢迎回到高中三角几何。将相位(i / wavelengthInSamples)转换成了正弦函数的弧度(通过乘以2π),用结果乘以SHRT_MAX(因为正弦函数返回的值在-1.0到1.0之间),并且将整个结果转换成16位值,然后你就可以在大端小端之间转换然后写进文件。

注意 如果你使用浮点型采样声明你的ASBD,你就不必这样放大。然后,CoreAudio的iOS4版本只能在PCM中用整型采样,并且我们想让代码在两个平台之间是便携的。

尝试一下 — 你会听到比无论是方波还是锯齿波都要更加令人愉悦的音,尽管你同样能够清楚的听见,对于一个给定的频率,它们播放的是同样的音调。

你可以尝试把频率增高然后看看你能否识别出什么时候你到达了一个点,你无法再听到任何音。你应该可以轻易地听到在10000Hz左右的音,但是你可能无法听到20000Hz或者更高的音。实际上,你停应高的能力随着年龄的增长而恶化,所以如果你现在能听到15000Hz,你可能在10年后就听不到了。

考虑这个例子结束的点。当频率是22050的时候会发生什么?那正好是采样率的一半。在这个例子的任何一个版本中,wavelengthInSamples都将是2。意味着只有两个采样来表示波;对于方波,你通过SHRT_MIN获得一个,通过SHRT_MAX获得另一个。但那是重复的东西,至少这是有模式的。在更高的频率,每一个波你有的采样少于两个。并且,因此,没有重复的模式。这是另一种方法你可以思考尼奎斯特和它看似随意的概念 — 在你想要再现的最高频率的两倍处采样。

Buffers

你可能注意到将所有采样写进你的文件花了几秒钟的时间。运行5秒来生成一个5秒的音频可并不高效!

一次读一个采样或者写一个采样是非常低效的。如果你重新思考AudioFileWriteBytes,你可能会想起最后的两个参数是要写进文件字节数以及一个指向采样的指针。不要一次写一个采样到文件里,会在这个简单的例子中一个函数调用的开销超过200000次。你本可以创建一个内存缓冲来持有一堆采样然后将这些采样集体写入文件。

这并不仅仅是写文件的问题。同样也是当你在运行时生成声音时的问题。一言以蔽之,计算机不同的部分计算速度是不同的。音频硬件生产或者消耗音频数据的分组所花的时间要比把分组移入移出内存所花的时间要少得多。这种放缓被称为冯诺依曼瓶颈。

当音频硬件没事可做的时候,它会产生糟糕的噪音。为了避免这样的事情发生,你可以使用缓冲使音频分组来回穿梭。许多的大缓冲意味着计算机慢的部分中的短暂停顿能够在快的部分注意到之前被解决:当一个音频数据的缓冲耗尽时,另一个(如期而至)到达了。

除了进一步增加问题的复杂性,缓冲可能会在你真正想要得到硬件注意的时候使其变得困难。把它想象成数据级别的官僚制度 —— 有些是必要的,但是太多的话又会让一切变得太长。

技术术语叫做延时,是从初始化一个动作到看到这个动作的结果的延迟。举个栗子,iPhone和iPod Touch的硬件延时在15到30毫秒,取决于模型。当你把一个采样的缓冲从你代码中推入前一章的音频引擎中的一个,在第一个采样走出扬声器或者耳机之前,有几毫秒的时间消逝了。

缓冲和延时是一对微秒的平衡:大的缓冲是用来对付高延时的,但是如果你在低延时的探求上赌运气的话(强行写出低延时的代码),你在冒险使你的缓冲耗尽并听见静音或者噪音。

Audio Formats

在例子中,你使用了一个AudioStreamBasicDescription来描述你代码生成的音频流的格式:16位整型采样,单声道,44100的采样率等等。但是这并不是故事的全部。然后你把音频放进了一个AIFF文件。你可能会从管理你自己的iTunes收藏中知道许多不同的文件格式存在:AIFF,WAV,MP3,M4A,等等。

注意 描述音频和把音频数据存进文件系统是完全不同的问题。数据格式解决第一个问题,文件格式解决后面一个。

考虑一个音频文件作为一个持有音频数据的容器。一些文件格式被定制为指定的数据格式,比如MP3;一个.mp3文件不可能包含PCM或者Windows Media数据 —— 只有MP3数据。正如AIFF文件掌握PCM音频数据,除非它是大端模式的,相反地,WAV文件中的PCM必须是小端模式。其他文件格式是更不可知的并且处理几个数据格式。举个栗子,MP4文件格式能包含一部分数据格式的数据,包括AAC,PCM和AC3。

CoreAudio支持的内容最不可知的文件格式是它自己的CoreAudio格式,缩写为CAF并且文件扩展名表示为.caf。一个CAF文件包含任意CoreAudio支持的音频格式:MP3,AAC,Apple Lossless,你随便取名。这使得CAF成为一个极佳的作为一个你应用内部的音频的容器格式的选择,比如背景音乐或者音效。

CAF还采用了一些技巧来提高性能。举个栗子,考虑MP3音频:因为它有可变的比特率,在一个.mp3文件中跳到任何点都需要从当前播放位置解压缩所有的数据直到它到达你想跳的时间。你没法知道文件的哪一部分代表目标时间。这就需要大量的I/O和CPU成本并且旺旺需要较长时间来执行。CAF建立了一个内部的时间映射到采样的浏览表,所以它可以几乎瞬间跳跃。

注意 格式经常是专有的以及广泛可变的。CoreAudio通过完整的抽象掉它们来处理这个实现细节。
在这个例子中,你可以用kAudioFileM4AType或者kAudioFileCAFType来代替格式常量kAudioFileAIFFType,改变文件名来让后缀适配格式,并且不必改变你代码的任意东西。CoreAudio来负责解决怎么把你的PCM数据放进指定格式中,只要音频格式和文件格式是兼容的。

总结

这一章走了很长一段路。我们从现实世界出发,谈论了自然源如何生成声音,声音作为压力的波是如何穿过空气的,你的耳朵是怎样抓住声音的。然后我们看到如何在一个声波的数字表示中模拟它们,这样的数字表示对于计算机来讲处理起来更加顺手;如何存储在数字媒体上以及通过网络发送。要具体化这些概念,你再次使用CoreAudio的Audio File Services,通过一个一个的采样把自己的声波写进一个磁盘上的文件。你简要地考虑了音频流格式和音频文件格式的区别。最后你看见了CoreAudio是如何抽象出多种文件格式的不同的,这意味着你读写.aif,.caf和.m4a文件或多或少是用的相同的方法。

伴随这种数字音频的概念中的基础,是时候真正的呈现CoreAudio了 —— 它是怎样表示和处理音频的。

0 0
原创粉丝点击