本文用到的 音频文件+逻辑分析仪软件+i2s数据波形
一、pcm
与pcm相关的几个参数:
1. PCM数据常用量化指标
- 采样率(Sample rate):每秒钟采样多少次,以Hz为单位。采样率表示音频信号每秒的数字快照数。该速率决定了音频文件的频率范围。采样率越高,数字波形的形状越接近原始模拟波形。低采样率会限制可录制的频率范围,这可导致录音表现原始声音的效果不佳。
根据 奈奎斯特采样定理,为了重现给定频率,采样率必须至少是该频率的两倍。例如,CD 的采样率为每秒 44,100 个采样,因此可重现最高为 22,050 Hz 的频率,此频率刚好超过人类的听力极限 20,000 Hz。
-
位深度(Bit-depth):表示用多少个二进制位来描述采样数据,一般为16bit。位深度决定动态范围。采样声波时,为每个采样指定最接近原始声波振幅的振幅值。较高的位深度可提供更多可能的振幅值,产生更大的动态范围、更低的噪声基准和更高的保真度。
-
字节序:表示音频PCM数据存储的字节序是大端存储(big-endian)还是小端存储(little-endian),为了数据处理效率的高效,通常为小端存储。
-
声道数(channel number):当前PCM文件中包含的声道数,是单声道(mono)、双声道(stereo)?此外还有5.1声道等。
-
采样数据是否有符号(Sign):要表达的就是字面上的意思,需要注意的是,使用有符号的采样数据不能用无符号的方式播放。
以FFmpeg中常见的PCM数据格式s16le为例:
- 它描述的是有符号16位小端PCM数据
s表示有符号,16表示位深,le表示小端存储。
2. PCM数据流
PCM (Pulse Code Modulation) 也被称为脉冲编码调制。PCM 音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准的数字音频数据。
PCM 音频数据的存储
如果是单声道的音频文件,采样数据按时间的先后顺序依次存入(有的时候也会采用 LRLRLR 方式存储,只是另一个声道的数据为 0),如果是双声道的话通常按照 LRLRLR 的方式存储,存储的时候还和机器的大小端有关。
小端模式如下图所示:
PCM 音频数据是未经压缩的数据,所以通常都比较大,常见的 MP3 格式都是经过压缩的,128Kbps 的 MP3 压缩率可以达到 1:11
PCM 音频数据的参数
一般我们描述 PCM 音频数据的参数的时候有如下描述方式:
- 44100HZ 16bit stereo:
每秒钟有 44100 次采样, 采样数据用 16 位(2 字节)记录, 双声道(立体声)
44100Hz 指的是采样率,它的意思是每秒取样 44100 次。采样率越大,存储数字音频所占的空间就越大。
16bit 指的是采样精度,意思是原始模拟信号被采样后,每一个采样点在计算机中用 16 位(两个字节)来表示。采样精度越高越能精细地表示模拟信号的差异。
Stereo 指的是声道数,也即采样时用到的麦克风的数量,麦克风越多就越能还原真实的采样环境(当然麦克风的放置位置也是有规定的)。
其他格式例子:
- 22050HZ 8bit mono:
每秒钟有 22050 次采样, 采样数据用 8 位(1 字节)记录, 单声道
- 48000HZ 32bit 51ch:
每秒钟有 48000 次采样, 采样数据用 32 位(4 字节浮点型)记录, 5.1 声道
二、WAV文件
WAV 是 Microsoft 和 IBM 为 PC 开发的一种声音文件格式,它符合 RIFF(Resource Interchange File Format)文件规范,用于保存 Windows 平台的音频信息资源,被 Windows 平台及其应用程序所广泛支持。
1. wav文件头
WAVE 文件通常只是一个具有单个 “WAVE” 块的 RIFF 文件,该块由两个子块(”fmt” 子数据块和 ”data” 子数据块),它的标准格式如下图所示:
图片来源:
http://soundfile.sapp.org/doc/WaveFormat/
该格式的实质就是在 PCM 文件的前面加了一个文件头,各字段含义如下:
偏移与大小名称说明0 4ChunkID包含 ASCII 形式的字母“RIFF”(0x52494646 大端形式)。4 4ChunkSize36 + SubChunk2Size,或更准确地说:4 + (8 + SubChunk1Size) + (8 + SubChunk2Size)这是此数字之后的块的其余部分的大小。这是整个文件的大小(以字节为单位)减去未包含在此计数中的两个字段的 8 字节:ChunkID 和 ChunkSize。8 4格式包含字母“WAVE”(0x57415645 大端形式)。12 4Subchunk1ID包含字母“fmt”(0x666d7420 大端格式)。16 4Subchunk1Size16 用于 PCM。这是该数字之后的其余子块的大小。20 2AudioFormatPCM = 1(即线性量化)1 以外的值表示某种形式的压缩。22 2NumChannelsMono = 1、Stereo = 2 等24 4SampleRate8000、44100 等28 4ByteRate== SampleRate * NumChannels * BitsPerSample/832 2BlockAlign== NumChannels * BitsPerSample/8 1 的字节数样本包括所有通道。34 2BitsPerSample8 位 = 8,16 位 = 16,等等2ExtraParamSize如果是 PCM,则不存在XExtraParams用于额外参数的空间36 4Subchunk2ID包含字母“数据”(0x64617461 大端形式)。40 4Subchunk2Size== NumSamples * NumChannels * BitsPerSample/8 这是数据中的字节数。您还可以将其视为该数字后面的子块的读取大小。44 *Data实际的声音数据。
2. wav文件头结构体
wav文件头信息对应结构体:
typedef struct { char ChunkID[4]; //内容为"RIFF" unsigned long ChunkSize; //存储文件的字节数(不包含ChunkID和ChunkSize这8个字节) char Format[4]; //内容为"WAVE“} WAVE_HEADER;typedef struct { char Subchunk1ID[4]; //内容为"fmt" unsigned long Subchunk1Size; //存储该子块的字节数(不含前面的Subchunk1ID和Subchunk1Size这8个字节) unsigned short AudioFormat; //存储音频文件的编码格式,例如若为PCM则其存储值为1。 unsigned short NumChannels; //声道数,单声道(Mono)值为1,双声道(Stereo)值为2,等等 unsigned long SampleRate; //采样率,如8k,44.1k等 unsigned long ByteRate; //每秒存储的bit数,其值 = SampleRate * NumChannels * BitsPerSample / 8 unsigned short BlockAlign; //块对齐大小,其值 = NumChannels * BitsPerSample / 8 unsigned short BitsPerSample; //每个采样点的bit数,一般为8,16,32等。} WAVE_FMT;typedef struct { char Subchunk2ID[4]; //内容为“data” unsigned long Subchunk2Size; //接下来的正式的数据部分的字节数,其值 = NumSamples * NumChannels * BitsPerSample / 8} WAVE_DATA;
3. WAV 文件头解析实例
下面通过提供给大家的音频文件《xiaoniao.wav》来详细讲解wav文件格式,该音频文件格式为:S16_LE
peng@ubuntu:~/test$ ls -l xiaoniao.wav -rwxrw-rw- 1 peng peng 1764448 May 10 20:41 xiaoniao.wav
用ue打开该文件,自动显示为十六进制数字,
文件头信息解析如下图:
数据是小端,比如采样率4个字段是 44 AC 00 00实际数据是0x0000ac44,转换成10进制是44100
读者对照结构体,可以解析出改文件的所有信息。
三、i2s音频波形分析
wav文件格式我们搞清楚了,那么它和i2s是什么关系呢?
1. 嵌入式设备音频架构
一个典型的嵌入式设备的音频架构大致如下【以rk3568为例】,
当我们使用aplay工具播放wav文件时:
- 解析wav文件头,读取相应信息
- 然后通过i2s控制器驱动,将pcm音频流通过i2s接口发送给codec rk809,
- codec rk809会将pcm音频流进行DAC转换成对应的模拟信号,并通过耳机/喇叭播放出去。
2. 播放命令
播放命令:
root@ATK-DLRK356X:/sdcard# aplay -v xiaoniao.wavPlaying WAVE 'xiaoniao.wav' : Signed 16 bit Little Endian, Rate 44100 Hz, StereoALSA <-> PulseAudio PCM I/O PluginIts setup is: stream : PLAYBACK access : RW_INTERLEAVED format : S16_LE subformat : STD channels : 2 rate : 44100 exact rate : 44100 (44100/1) msbits : 16 buffer_size : 22050 period_size : 5512 period_time : 125000 tstamp_mode : NONE tstamp_type : GETTIMEOFDAY period_step : 1 avail_min : 5512 period_event : 0 start_threshold : 22050 stop_threshold : 22050 silence_threshold: 0 silence_size : 0 boundary : 6206523236469964800
3.波形分析
现在我在图中i2s控制器与codec之间位置用逻辑分析仪抓取了i2s数据波形,
【该操作需要飞线,建议找硬件工程师帮忙】
波形文件aplay_xiaoniao.kvdat,
一口君实际测试的i2s控制器为24位小端格式。
由上图可知:
- xiaomiao.wav文件为s16_le格式,所以i2s控制器依次每次读取data后面2个字节的数据
- 根据帧时钟,依次在左右声道时隙,将pcm数据放到数据线中。
- 因为控制器是24位,所以各channel会有24个bit的时钟周期;
- 根据i2s协议,默认有效数据靠左,并且空1个bit的位置;多出来的8个bit位置默认补充填0。
5. codec就会通过该波形提取对应的pcm数据,做出相应处理之后就可以播放出去了。
四、如何在各种音频格式之间进行转换
处于测试需要,我们还需要经常转换文件格式,可以通过FFmpeg工具
1. FFmpeg
对于其他格式的音频文件,一般用FFmpeg软件进行转换,先在当前的设备安装好FFmpeg软件,然后用命令行就可以进行转换了,常用的示范如下:
- 将mp4视频提取wav格式:
ffmpeg -i D:\input.mp4 -vn -acodec pcm_s16le -ar 44100 -ac 2 D:\output.wav
- 将wav格式转变为pcm格式:
ffmpeg -i D:\output.wav -f s16le -acodec pcm_s16le D:\output.pcm
- 将pcm格式转变为wav格式:
ffmpeg -f s16le -ar 44100 -ac 2 -i D:\output.pcm c:\output.wav
“注意上面的命令中指定的采样率为44.1k ,双声道,存储格式是s16le
”
2. 编写代码实现PCM → WAV 代码
下面是一个实现将pcm文件转换成wav文件的代码实例:
int simplest_pcm16le_to_wave( const char *pcmpath, int channels, int sample_rate, const char *wavepath ){ // 省去错误判断 short pcmData; FILE* fp = fopen( pcmpath, "rb" ); FILE* fpout = fopen( wavepath, "wb+" ); // 填充 WAVE_HEADER WAVE_HEADER pcmHEADER; memcpy( pcmHEADER.ChunkID, "RIFF", strlen( "RIFF" ) ); memcpy( pcmHEADER.Format, "WAVE", strlen( "WAVE" ) ); fseek( fpout, sizeof( WAVE_HEADER ), 1 ); //填充 WAVE_FMT WAVE_FMT pcmFMT; pcmFMT.SampleRate = sample_rate; pcmFMT.ByteRate = sample_rate * sizeof( pcmData ); pcmFMT.BitsPerSample = 8 * sizeof( pcmData ); memcpy( pcmFMT.Subchunk1ID, "fmt ", strlen( "fmt " ) ); pcmFMT.Subchunk1Size = 16; pcmFMT.BlockAlign = channels * sizeof( pcmData ); pcmFMT.NumChannels = channels; pcmFMT.AudioFormat = 1; fwrite( &pcmFMT, sizeof( WAVE_FMT ), 1, fpout ); //填充 WAVE_DATA; WAVE_DATA pcmDATA; memcpy( pcmDATA.Subchunk2ID, "data", strlen( "data" ) ); pcmDATA.Subchunk2Size = 0; fseek( fpout, sizeof( WAVE_DATA ), SEEK_CUR ); fread( &m_pcmData, sizeof( short ), 1, fp ); while ( !feof( fp ) ) { pcmDATA.dwSize += 2; fwrite( &m_pcmData, sizeof( short ), 1, fpout ); fread( &m_pcmData, sizeof( short ), 1, fp ); } int headerSize = sizeof( pcmHEADER.Format ) + sizeof( WAVE_FMT ) + sizeof( WAVE_DATA ); // 36 pcmHEADER.ChunkSize = headerSize + pcmDATA.Subchunk2Size; rewind( fpout ); fwrite( &pcmHEADER, sizeof( WAVE_HEADER ), 1, fpout ); fseek( fpout, sizeof( WAVE_FMT ), SEEK_CUR ); fwrite( &pcmDATA, sizeof( WAVE_DATA ), 1, fpout ); fclose( fp ); fclose( fpout ); return 0;}
大家可以用我提供的sound.pcm、xiaoniao.wav语音文件,测试一下。