善恶众相

  • 首页

  • 分类

  • 归档

从数据到声音

发表于 2019-07-15 更新于 2019-10-18 分类于 Linux
声音是如何用一个数组去表示的呢?表示声音的这个数组又是如何去拼凑而成的呢?正是为了搞清这两个问题才有了这篇文章。

本文源于对于声波支付的调研,后来通过东拼西凑整合了一个SinVoice项目,本文对该项目的发声部分进行深入的解析,虽然和声波支付相去甚远,但从中我也学到了不少有关音频的入门知识,当然由于本人知识有限存在纰漏的地方望大家请谅解^_^。

基础知识

声音三特征

  • 音调:就是高音(青藏高原)和低音(赵忠祥),与振动的频率有关,频率越高,音调越高;频率越低,音调越低
  • 响度:就是音量,由声源振动的幅度决定的,振幅越大,响度越大;振幅越小,响度越小
  • 音色:跟材料和结构有关.当材料的结构发生变化时,音色发生变化

概念的理解很重要

  • 采样频率[Hz]:计算机对模拟信号进行取样时,(每秒钟采样点个数)每秒从连续信号中提取并组成离散信号的采样点个数
  • 频率[Hz]:自然声音一秒钟完成周期性变化的次数,描述周期运动频繁程度的量
  • 采样间隔[s]:
    • 采样频率的倒数,即每个采样点之间的时间间隔
    • 频率的倒数,即完成一次周期性变化所需时间
  • 采样精度[Bit]:表示每个样本点所需的比特位数,如:8bit(0255级),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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 根据频率和持续时间生成采样点数据
*
* @param genRate 生成的声波的频率
* @param dur 声波播放时长
*
*/
- (void)gen:(int)genRate dur:(int)dur
{
int buffSize = 0;
int n = BITS_16/2; // 振幅
int totalCount = (dur*44100)/1000; // 持续时间为dur毫秒的声音的采样点个数
double per = (genRate/(double)44100)*2*M_PI; // 频率为genRate的波采样点之间角度间隔
double d = 0;

for (int i = 0; i < totalCount; i++) {
int outPrint = (int)(sin(d)*n);
_playState.mCode[_playState.mCodeLength + buffSize++] = (SignedByte)outPrint;
_playState.mCode[_playState.mCodeLength + buffSize++] = (SignedByte)(outPrint >> 8);

d+=per;
}
_playState.mCodeLength += totalCount * 2;
}

下面我们逐一对一下几句代码进行解释:

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
2
3
4
5
6
7
8
for (int i = 0; i < totalCount; i++) {
int outPrint = (int)(sin(d)*n);
_playState.mCode[_playState.mCodeLength + buffSize++] = (SignedByte)outPrint;
_playState.mCode[_playState.mCodeLength + buffSize++] = (SignedByte)(outPrint >> 8);

d+=per;
}
_playState.mCodeLength += totalCount * 2;

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
- (void)play:(NSString *)msg
{
[self _setupAudioFormat];
_playState.mCurrentPacket = 0;
_playState.mCodeLength = 0;
[self encodeMessage:msg];
[self _deriveBufferSize:1000];

OSStatus status = noErr;
/*!
@function AudioQueueNewOutput
@abstract 创建一个新的audio queue用于播放音频数据,
调用了AudioQueueStart()才会开始播放
当第一个buffer中的数据播放完之后,才会调用callback

@param inFormat
指向描述将被播放的音频数据格式的结构体指针,对于线性PCM来说,只有隔行扫描的格式能够被支持。
@param inCallbackProc
指向一个回调函数,当audio queue播放玩一个buffer后会将该buffer传给这个回调函数
@param inUserData
自定义结构体的指针,会传递给回调函数
@param inCallbackRunLoop
回调函数将被在执行在inCallbackRunLoop指定的事件循环中
如果值为NULL,回调函数将被安排在audio queue的内部线程中执行
@param inCallbackRunLoopMode
指定RunLoopMode,默认为kCFRunLoopCommonModes
@param inFlags
保留,传0
@param outAQ
返回的时候,该变量会指向新创建的audio queue对象

@result An OSStatus result code.
*/
status = AudioQueueNewOutput(&_playState.mDataFormat,
HandleOutputBuffer,
&_playState,
CFRunLoopGetCurrent(),
kCFRunLoopCommonModes,
0,
&_playState.mQueue);

ADAssert(noErr == status, @"Could not create queue.");

_playState.mIsRunning = YES;
//向buffer aqueue中添加三个audio queue buffer准备开始播放
for (int i = 0; i < kNumberBuffers; i++) {
// 为mBuffers[i]申请bufferByteSize大小的内存
status = AudioQueueAllocateBuffer(_playState.mQueue, _playState.bufferByteSize, &_playState.mBuffers[i]);
ADAssert(noErr == status, @"Could not allocate buffers.");
HandleOutputBuffer(&_playState, _playState.mQueue, _playState.mBuffers[i]);
}

status = AudioQueueStart(_playState.mQueue, NULL);
ADAssert(noErr == status, @"Could not start playing.");
}

/**
* AudioQueueOutputCallback
*
* @param inUserData AudioQueueNewOutput()函数中自定义的结构体指针inUserData
* @param inAQ 指向audio queue对象的指针
* @param inBuffer 等待填充数据并放置进buffer queue的buffer
*/
void HandleOutputBuffer(void * inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer) {
AQPlayState * pPlayState = (AQPlayState *)inUserData;

if ( ! pPlayState->mIsRunning) {
return;
}

// inBuffer表示已经播放完的audio queue buffer(已经开始播放后)
// 等待往里面添加下面要播放的audio data后,重新放到buffer queue的队尾
UInt32 numBytesToPlay = inBuffer->mAudioDataBytesCapacity;
UInt32 numPackets = numBytesToPlay/pPlayState->mDataFormat.mBytesPerPacket;

SInt8 * buffer = (SInt8 *)inBuffer->mAudioData;

// 每个buffer中有添加 44100个采样点数据
for(long i = (long)pPlayState->mCurrentPacket; i < pPlayState->mCurrentPacket + numPackets; i++) {
long idx = i % pPlayState->mCodeLength;
// 循环播放code
buffer[i-pPlayState->mCurrentPacket] = pPlayState->mCode[idx];
}

inBuffer->mAudioDataByteSize = numPackets;
AudioQueueEnqueueBuffer(pPlayState->mQueue, inBuffer, 0, NULL);
pPlayState->mCurrentPacket += numPackets;
}
文章分享
代码的编译与执行
  • 文章目录
  • 站点概览
Zrongl

Zrongl

23 日志
3 分类
GitHub E-Mail
  1. 1. 基础知识
    1. 1.1. 声音三特征
    2. 1.2. 概念的理解很重要
    3. 1.3. 香农采样定理的理解
  2. 2. 声音数据生成
  3. 3. AudioQueue播放声音数据
© 2019 Zrongl
不争无尤
|
主题 – NexT.Mist v7.3.0
0%