本文源于对于声波支付的调研,后来通过东拼西凑整合了一个SinVoice项目,本文对该项目的发声部分进行深入的解析,虽然和声波支付相去甚远,但从中我也学到了不少有关音频的入门知识,当然由于本人知识有限存在纰漏的地方望大家请谅解^_^。
基础知识
声音三特征
- 音调:就是高音(青藏高原)和低音(赵忠祥),与振动的频率有关,频率越高,音调越高;频率越低,音调越低
- 响度:就是音量,由声源振动的幅度决定的,振幅越大,响度越大;振幅越小,响度越小
- 音色:跟材料和结构有关.当材料的结构发生变化时,音色发生变化
概念的理解很重要
- 采样频率[Hz]:计算机对模拟信号进行取样时,(每秒钟采样点个数)每秒从连续信号中提取并组成离散信号的采样点个数
- 频率[Hz]:自然声音一秒钟完成周期性变化的次数,描述周期运动频繁程度的量
- 采样间隔[s]:
- 采样频率的倒数,即每个采样点之间的时间间隔
- 频率的倒数,即完成一次周期性变化所需时间
- 采样精度[Bit]:表示每个样本点所需的比特位数,如:8bit(0
255级),16bit(065535级),采样精度越大,所能表现的声音越丰富,区分相似声音的能力越强。

如上图:我们可以看到对同一声波(振幅和频率相同)进行采样时,采样精度越高,模拟出来的声波越接近原始声波。要强调的一点是,采样精度表达的是振幅的平分份数,假设该声波的振幅为n,对于采样点s9而言,当采样精度为4bit时,s9的幅值为n*15/16;当采样精度为2x4bit时,s9的幅值为n*31/32;所以采样精度越高采样点幅值越接近原始声波的幅值。
- 比特率[Bps]:每秒传输的比特数模拟信号转换为数字信号后,单位时间内的二进制数据量
比特率 = 采样率 x 采样精度 x 声道数
香农采样定理的理解
为了不失真地恢复模拟信号,采样频率(Fs)应该不小于模拟信号频谱中最高频率(Fmax)的2倍:
Fs ≥ 2Fmax
设想有一个频率为F的自然声波(模拟信号),如果对该模拟信号进行采样时,采样频率也是F的话,也就是说采样间隔为1/F
,如下图:

如上图,采样频率是F时模拟出来的只是一条直线,而不是一条波。
当采样频率变为声波频率的2倍时,如下图:

我们看出采样频率为2F时,模拟出来的是一条锯齿波,虽然跟原本的采样对象有些差距,但是波形是相似的。
而人耳能听到的声音范围在20Hz~20000Hz,所以根据香农采样定理,我们的采样对象的频率范围是20Hz~20000Hz,最高频率的2倍就是40000Hz,为了留一点安全系数,再考虑到工程上的习惯,最终选择了44.1kHz这个数值作为标准采样频率。当然生活中常见声音的范围一般在200Hz~6000Hz之间,所以为了节省空间或宽带也存在采样率为22500Hz的情况。
声音数据生成
项目中SinPlayer.m 文件主要负责生成声音数据并进行播放,我们的声音数组就是在gen:dur:
方法中生成的:
1 | /** |
下面我们逐一对一下几句代码进行解释:
1 | int totalCount = (dur*44100)/1000; |
44100是我们将要对我们产生的声音进行采样的频率,所以每一毫秒采样点的数量为44100/1000,我们想要使得频率为genRate
的声波持续dur毫秒的话,就需要准备(dur*44100)/1000个采样点数据,对这么多个采样点数据进行播放的持续时间就是dur
毫秒;
1 | double per = (genRate/(double)44100)*2*M_PI; |
根据频率的定义:频率表示一秒钟内完成周期性变化的次数,而一个周期的角度变化是2π,即:360°,所以对于频率44100的声波在每一个采样时间间隔1/44100内对应的角度变化为360°,对于频率为genRate
的声波每一个采样时间间隔1/44100内对应的角度变化则为(gentRate/44100)*2π。
为了便于理解,请看下图:

各个采样点的数据有了,接下来就是对其进行存储,如下图:

通过之前的属性设置代码得知,我们生成声音的采样精度是16bit,因此每个采样点数据位数为16位,而mCode类型是char*占8bit大小,利用mCode存储采样数据时,需要将第一个采样点数据的低8位和高8位分别存储到mCode[0]和mCode[1]的位置,之后的以此类推,这就是下面代码中实现的,pcm音频数据存储过程:
1 | for (int i = 0; i < totalCount; i++) { |
AudioQueue播放声音数据
有关AudioQueue的使用,苹果文档已经说的很明白了,如图:

播放的过程:
- 1、利用AudioQueueNewOutput()方法创建audio queue,设置回调函数及其传入参数,调用自定义的callback函数向buffer中填充数据,并将填充后的buffer,放置到buffer queue中,准备播放
- 2、调用AudioQueueStart(),开始播放
- 3、audio queue播放buffer queue第一个被填充的buffer中的数据
- 4、audio queue返回已播放完的buffer以供复用,并播放下一个buffer中的数据
- 5、audio queue将播放完的buffer返回给callback函数,并继续向该buffer中添加要播放的数据
- 6、callback函数调用AudioQueueEnqueueBuffer()方法将填充好数据的buffer放置到待播放buffer队列中
从这个过程中可以看出我们需要自定义一个函数来实现向buffer中装填上面我们准备好的音频数据,然后将其放置到buffer queue中等待进行播放,代码如下:
1 | - (void)play:(NSString *)msg |