出于研究项目的需要,我需要设计一个WAV音频文件定长切取的小功能:给定一个WAV文件、一组记录时间信息的数组t_Array以及一个阈值ΔT,要求从这个文件中切取出以t_Array记录的每个时刻t为中心的ΔT长度的片段,合并为一个新的文件;对于相邻的任意两个时刻t
1和t
2,其以各自为中心切取的片段不能够包含以另一个时刻为起点或终点的片段。即如果t
2在t
1之后,则以t
2为中心切取的片段不能包含t
1及t
1以前的数据,且以t
1为中心切取的片段不能包含t
2及t
2以后的数据。
一、问题分析(Problem Analysis)
最理想的情况下,相邻的两个时刻tk与tk+1、第一个时刻t0与0时刻、最后一个时刻tn-1与文件末尾的间隔都大于ΔT/2,整个音频序列如图1所示,轴线代表一段WAV音频文件,tk为待切取的中心时刻序列,以每个t为中心,待切取的片段用虚框矩形表示。:
图1 理想情况
如图1所示,这种情况下的切取工作只需一次性遍历整个时刻数组,然后以每个时刻为中心切取周围ΔT长度的序列。
然而很多情况下我们并不是这么幸运,为了使得系统更加鲁棒,我们应该考虑以下几种情况:
存在两个相邻时刻tk与tk+1之间的间隔小于ΔT/2,如图2所示:
图2 相邻时刻间隔小于ΔT/2的情况
第一个时刻t0与0时刻之间的间隔小于ΔT/2,如图3所示:
图3 t0与0时刻间隔小于ΔT/2的情况
最后一个时刻tn-1与末尾的间隔小于ΔT/2,如图4所示:
图4 tn-1与结尾间隔小于ΔT/2的情况
这些情况如果不处理,就会造成莫名其妙的错误。比如,假设t1与t2之间间隔小于ΔT/2,按照顺序我们先切取出了t1周围的片段,但是这将会将t2时刻开始的序列也包含进来。正确的处理应该是只切取到t2时刻的前一个数据。而对于t2,则只从t1的下一个数据开始切取。
二、预备知识
如果读者有看过我之前写的一篇博文《C#实现WAV音频单声道提取》,那就会对WAV文件头格式有个初步的认识。但为了实现我们这次的切取目的,我还需要针对文件头再进行简要介绍。WAV文件头如表1所示。
偏移地址
字节数
类型
内容
00H~03H
4
字符
资源交换文件标志(RIFF)
04H~07H
4
长整数
从下个地址开始到文件尾的总字节数
08H~0BH
4
字符
WAV文件标志(WAVE)
0CH~0FH
4
字符
波形格式标志(FMT)
10H~13H
4
整数
过滤字节(一般为00000010H)
14H~15H
2
整数
格式种类(值为1,表示数据PCMμ律编码的数据)
16H~17H
2
整数
通道数,单声道为1,双声音为2
18H~1BH
4
长整数
采样频率
1CH~1FH
4
长整数
波形数据传输速率(每秒平均字节数)
20H~21H
2
整数
数据的调整数(按字节计算)
22H~23H
2
整数
样本数据位数
表1 WAV文件头
在偏移地址为18H~1BH处存放的是采样率。由于现实生活中的声音是连续型的模拟信号,而计算机只能表达离散的信号。因此在录制音频的时候就涉及到AD转换,即模拟信号到离散信号的转换,这个转换过程可以简单概括为一个采样过程。单位时间采的样本数越多,则越接近模拟信号,还原度也就越高。“单位时间采的样本数”就是采样率(也称为采样速率或者采样频率)。常见的音频采样率有8000、11025、22050、44100、48000、96000等。其中,44100是大多数歌曲文件采用的标准采样频率。
根据采样率信息,我们可以计算出计算任一时刻在数据队列中的索引位置。即:
k = s * t (1)
其中,k为该时刻在数据队列中的索引位置,而s和t分别为采样率和时间。
三、环境和工具(Environment & Tools)
实验平台:Windows
开发语言:C#
四、编程实现
1. 文件读取类
还记得我在上一篇博文《C#实现WAV音频单声道提取》里提过的WaveAccess类吗?现在它又再次派上用场了!我们可以利用它来得到关键的采样信息!
WaveAccess.cs:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.IO;
6: using System.Windows.Forms;
7:
8: namespace SingleChannleExtractor
9: {
10: public class WaveAccess
11: {
12:
13: private byte[] riff; //4
14: private byte[] riffSiz