一、问题分析(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[] riffSize; //4
15: private byte[] waveID; //4
16: private byte[] fmtID; //4
17: private byte[] notDefinition; //4
18: private byte[] waveType; //2
19: private byte[] channel; //2
20: private byte[] sample; //4
21: private byte[] send; //4
22: private byte[] blockAjust; //2
23: private byte[] bitNum; //2
24: private byte[] unknown; //2
25: private byte[] dataID; //4
26: private byte[] dataLength; //4
27:
28: short[] data;
29: private string longFileName;
30:
31: public string LongFileName
32: {
33: get { return longFileName; }
34: }
35:
36: public string ShortFileName
37: {
38: get
39: {
40: int pos = LongFileName.LastIndexOf("\\");
41: return LongFileName.Substring(pos + 1);
42: }
43: }
44:
45: public short[] Data
46: {
47: get { return data; }
48: set { data = value; }
49: }
50:
51: public string Riff
52: {
53: get { return Encoding.Default.GetString(riff); }
54: set { riff = Encoding.Default.GetBytes(value); }
55: }
56:
57: public uint RiffSize
58: {
59: get { return BitConverter.ToUInt32(riffSize,0); }
60: set { riffSize = BitConverter.GetBytes(value); }
61: }
62:
63:
64: public string WaveID
65: {
66: get { return Encoding.Default.GetString(waveID); }
67: set { waveID = Encoding.Default.GetBytes(value); }
68: }
69:
70:
71: public string FmtID
72: {
73: get { return Encoding.Default.GetString(fmtID); }
74: set { fmtID = Encoding.Default.GetBytes(value); }
75: }
76:
77:
78: public int NotDefinition
79: {
80: get { return BitConverter.ToInt32(notDefinition,0); }
81: set { notDefinition = BitConverter.GetBytes(value); }
82: }
83:
84:
85: public short WaveType
86: {
87: get { return BitConverter.ToInt16(waveType, 0); }
88: set { waveType = BitConverter.GetBytes(value); }
89: }
90:
91:
92: public ushort Channel
93: {
94: get { return BitConverter.ToUInt16(channel,0); }
95: set { channel = BitConverter.GetBytes(value); }
96: }
97:
98:
99: public uint Sample
100: {
101: get { return BitConverter.ToUInt32(sample,0); }
102: set { sample = BitConverter.GetBytes(value); }
103: }
104:
105:
106: public uint Send
107: {
108: get { return BitConverter.ToUInt32(send, 0); }
109: set { send = BitConverter.GetBytes(value); }
110: }
111:
112:
113: public ushort BlockAjust
114: {
115: get { return BitConverter.ToUInt16(blockAjust, 0); ; }
116: set { blockAjust = BitConverter.GetBytes(value); }
117: }
118:
119:
120: public ushort BitNum
121: {
122: get { return BitConverter.ToUInt16(bitNum, 0);}
123: set { bitNum = BitConverter.GetBytes(value); }
124: }
125:
126:
127: public ushort Unknown
128: {
129: get
130: {
131: if (unknown == null)
132: {
133: return 1;
134: }
135: else
136: return BitConverter.ToUInt16(unknown, 0);
137: }
138:
139: set { unknown = BitConverter.GetBytes(value); }
140: }
141:
142:
143: public string DataID
144: {
145: get { return Encoding.Default.GetString(dataID); }
146: set { dataID = Encoding.Default.GetBytes(value); }
147: }
148:
149: public uint DataLength
150: {
151: get { return BitConverter.ToUInt32(dataLength, 0); }
152: set { dataLength = BitConverter.GetBytes(value); }
153: }
154:
155:
156: public WaveAccess() { }
157:
158: public WaveAccess(string filepath)
159: {
160: try
161: {
162: riff = new byte[4];
163: riffSize = new byte[4];
164: waveID = new byte[4];
165: fmtID = new byte[4];
166: notDefinition = new byte[4];
167: waveType = new byte[2];
168: channel = new byte[2];
169: sample = new byte[4];
170: send = new byte[4];
171: blockAjust = new byte[2];
172: bitNum = new byte[2];
173: unknown = new byte[2];
174: dataID = new byte[4]; //52
175: dataLength = new byte[4]; //56 个字节
176:
177: longFileName = filepath;
178:
179:
180: FileStream fs = new FileStream(filepath,FileMode.Open);
181: BinaryReader bread = new BinaryReader(fs);
182: riff = bread.ReadBytes(4);
183: riffSize = bread.ReadBytes(4);
184: waveID = bread.ReadBytes(4);
185: fmtID = bread.ReadBytes(4);
186: notDefinition = bread.ReadBytes(4);
187: waveType = bread.ReadBytes(2);
188: channel = bread.ReadBytes(2);
189: sample = bread.ReadBytes(4);
190: send = bread.ReadBytes(4);
191: blockAjust = bread.ReadBytes(2);
192: bitNum = bread.ReadBytes(2);
193: if (BitConverter.ToUInt32(notDefinition,0)== 18 )
194: {
195: unknown = bread.ReadBytes(2);
196: }
197: dataID = bread.ReadBytes(4);
198: dataLength = bread.ReadBytes(4);
199: uint length = DataLength/2;
200: data = new short[length];
201: for (int i = 0; i < length; i++)
202: {
203: data[i] = bread.ReadInt16();//读入2字节有符号整数
204: }
205: fs.Close();
206: bread.Close();
207: }
208: catch (System.Exception ex)
209: {
210: Console.Write(ex.Message);
211: }
212: }
213:
214: public short[] GetData(uint begin,uint end )
215: {
216: if ((end - begin) >= Data.Length)
217: return Data;
218: else
219: {
220: uint temp = end - begin+1;
221: short[] dataTemp = new short[temp];
222: uint j = begin;
223: for (int i = 0; i<temp; i++)
224: {
225: dataTemp[i] = Data[j];
226: j++;
227: }
228: return dataTemp;
229: }
230:
231: }
232:
233: /// <summary>
234: /// 生成wav文件到系统
235: /// </summary>
236: /// <param name="fileName">要保存的文件名</param>
237: /// <returns></returns>
238: public bool bulidWave(string fileName)
239: {
240: try
241: {
242: FileInfo fi = new FileInfo(fileName);
243: if (fi.Exists)
244: fi.Delete();
245: FileStream fs = new FileStream(fileName, FileMode.CreateNew);
246: BinaryWriter bwriter = new BinaryWriter(fs); //二进制写入
247: bwriter.Seek(0, SeekOrigin.Begin);
248: bwriter.Write(Encoding.Default.GetBytes(this.Riff)); //不可以直接写入string类型的字符串,字符串会有串结束符,比原来的bytes多一个字节
249: bwriter.Write(this.RiffSize);
250: bwriter.Write(Encoding.Default.GetBytes(this.WaveID));
251: bwriter.Write(Encoding.Default.GetBytes(this.FmtID));
252: bwriter.Write(this.NotDefinition);
253: bwriter.Write(this.WaveType);
254: bwriter.Write(this.Channel);
255: bwriter.Write(this.Sample);
256: bwriter.Write(this.Send);
257: bwriter.Write(this.BlockAjust);
258: bwriter.Write(this.BitNum);
259: if (this.Unknown != 0)
260: bwriter.Write(this.Unknown);
261: bwriter.Write(Encoding.Default.GetBytes(this.DataID));
262: bwriter.Write(this.DataLength);
263:
264: for (int i = 0; i < this.Data.Length; i++)
265: {
266: bwriter.Write(this.Data[i]);
267: }
268:
269:
270: bwriter.Flush();
271: fs.Close();
272: bwriter.Close();
273: fi = null;
274: return true;
275: }
276: catch (System.Exception ex)
277: {
278: Console.Write(ex.Message);
279: return false;
280: }
281: }
282:
283: }
284: }
2.确定每个时刻的索引位置
根据公式(1),我们可以编写函数,计算出时间数组里保存的每个时刻在队列中的索引位置。
1: /// <summary>
2: /// time2index
3: /// 将时间数组转换为索引
4: /// </summary>
5: /// <param name="t_Array">时间数组,格式为(m:s:ms)</param>
6: /// <param name="sample">采样率</param>
7: /// <returns></returns>
8: private double[] time2index(string[] t_Array, int sample)
9: {
10: double[] timeIndex = new double[t_Array.Length];
11: string[] tempStr = new string[3];
12: int [] temp = new int[3];
13: double s;
14:
15: for(int i=0;i<t_Array.Length;i++)
16: {
17: tempStr = t_Array[i].Split(':'); //利用分号将时间字符串划分为m、s和ms
18: for (int j = 0; j < 2; j++)
19: {
20: temp[j] = Convert.ToInt32(tempStr[j]);
21: }
22: s = temp[0] * 60 + temp[1] + temp[2] / 1000.0; //计算出秒数
23: timeIndex[i] = s * sample; //计算索引
24: }
25: return timeIndex;
26: }
需要注意的是:调用这个函数前,需要先保证t_Array的格式一定为(m:s:ms),可以通过设置掩码来进行限制,或者在这之前对t_Array的内容进行一个正则表达式检验。方法有很多,在这里不展开论述。
3.排序
由于输入的t_Array数组所保存的时刻数据不一定就是按照时间顺序的,为了便于算法设计,需要先对保存索引位置的timeIndex数组做一次排序。C#已经有内置的排序工具Sort(),在msdn中定义如下:
3.切割算法
Algorithm Time!通过上面几步,我们已经知道了每一个时刻的索引位置,也对它们进行了排序。下面就回到一开始提出来的问题,如何根据不同情况来作切分?
很明显,现在我们还不知道每一个时刻与它前一个时刻或者后一个时刻(如果有的话)之间的间隔,所以在决定如何切之前,我们要先把每个间隔求出来,然后与ΔT的一半作比较(注意这里的ΔT指的是采样长度,不要混淆),决定切取的长度。遍历一次timeIndex数组(保存索引位置的数组),对于任一切取时刻,我们需要进行两次判断:
第一次,判断与它前一切取时刻之间的间隔,如果前面没有切取时刻,则判断与0时刻的间隔。设该时刻索引位置为t,其上一时刻索引位置为lastT,让lastT的初始值为0,刚好对应0时刻的索引位置,因此不需要单独考虑0时刻的情况。仅需要考虑以下两种情况:
a) t-lastT < ΔT/2,此时只切取从lastT到t之间的歌曲片段。
b) t-lastT >= ΔT/2,此时只复制从t-ΔT/2到t之间的歌曲片段。
第二次,判断与它后一切取时刻之间的间隔,如果后面没有切取时刻,则判断与末尾的间隔。设该时刻索引位置为t,其后一时刻索引位置为nextT,如果t是timeIndex最后一个元素,则nextT的值为整首歌的结束时刻的索引位置,刚好为数据列表的长度-1。
通过每一次判断,我们可以得出切取的起始索引位置beginIndex和要切取的长度dataLth,然后据此进行切取。整个切取的函数如下所示:
1: /// <summary>
2: /// cutting
3: /// 定长分段切取函数
4: /// </summary>
5: /// <param name="from">源文件</param>
6: /// <param name="to">目标文件</param>
7: /// <param name="timeIndex">切取时刻对应的索引位置列表</param>
8: /// <param name="deltaT">切取阈值</param>
9: private void cutting(string from, string to, double[] timeIndex, double deltaT)
10: {
11: WaveAccess wa = new WaveAccess(from); //声明一个文件访问类
12:
13: double dataLth; //数据队列的总长度
14:
15: double halfDelta = deltaT / 2; //DeltaT的一半
16: double lastT = 0, nextT = 0; //上一个切取时刻、下一个切取时刻,初始值为0
17: double ms; //保存切取片段长度
18: double beginIndex = 0; //记录开始切割的索引位置
19:
20: Array.sort(timeIndex); //排序
21:
22: //(1)先复制from文件到to,以使得
23: // 目标文件和源文件保持一样的头文件和数据长度
24: FileInfo fi1 = new FileInfo(from);
25: FileInfo fi2 = new FileInfo(to);
26: // 确保复制前目标文件并不存在
27: fi2.Delete();
28: fi1.CopyTo(to, true);
29:
30: WaveAccess waTemp = new WaveAccess(to); //用于保存切取结果
31:
32:
33: //(2)将waTemp的内容全部置0
34: for (int i = 0; i < waTemp.Data.Length; i++)
35: {
36: waTemp.Data[i] = 0;
37: }
38:
39: //(3)切分文件
40: for (int i = 0; i < timeIndex.Length; i++)
41: {
42: t = timeIndex[i] * 1000; //以毫秒为切取单位
43:
44: if (i == timeIndex.length - 1)
45: {
46: //如果是最后一个切取时刻,
47: //则下一个时刻为结束时刻
48: nextT = wa.Data.Length / wa.Sample * 1000 - 1;
49: }
50: else
51: {
52: nextT = timeIndex[i + 1] * 1000;
53: }
54:
55: //先切分每个时刻左边一段
56: if (halfDelta > (t - lastT))
57: {
58: //复制从lastT到t之间的歌曲片段
59: ms = t - lastT;
60: dataLth = wa.Sample / 1000 * ms;
61: beginIndex = 2 * wa.Sample / 1000 * lastT;
62: }
63: else
64: {
65: //复制只从t-halfdelta到t之间的歌曲片段
66: ms = halfDelta;
67:
68: dataLth = wa.Sample / 1000 * ms;
69: beginIndex = 2 * wa.Sample / 1000 * (t - halfDelta / 2);
70: }
71: for (int j = 0; j < (int)dataLth; j++)
72: {
73: //覆盖数据
74: //Overwrite data
75: waTemp.Data[(int)beginIndex + j] = wa.Data[(int)beginIndex + j];
76: }
77:
78: //切分每个时刻右边一段
79: if (halfDelta > (nextT - t))
80: {
81: //复制从t到nextT的歌曲片段
82:
83: ms = nextT - t;
84: dataLth = wa.Sample / 1000 * ms;
85: beginIndex = 2 * wa.Sample / 1000 * t;
86: }
87: else
88: {
89: //复制只从t到t+halfdelta之间的歌曲片段
90:
91: ms = halfDelta;
92:
93: dataLth = wa.Sample / 1000 * ms;
94: beginIndex = 2 * wa.Sample / 1000 * t;
95:
96: }
97: for (int j = 0; j < (int)dataLth; j++)
98: {
99: //覆盖数据
100: waTemp.Data[(int)beginIndex + j] = wa.Data[(int)beginIndex + j];
101: }
102:
103: lastT = t;
104: }
105: //(4)写入文件
106: waTemp.bulidWave(to);
107: }
四、实验结果
根据前面的分析,编写切取工具,提供切取阈值选项,如图5所示:
图5 切取阈值选项
以400ms为切取阈值,对一段音频进行定长分段切取,其切取前后的波形图案对比图如图6所示:
(a) 切分前的波形图案
(b) 切分后的波形图案
图6 切取前后波形图对比
图中的白线表示每一个给定的切取时刻。