C#实现WAV音频单声道提取

喜欢买碟或者卡拉ok的朋友一定不会对声道这个术语陌生。通常我们在音像店买回来的VCD或者DVD都是双声道的形式,唱片商在录制唱片时往往提供了两个或多个声道,以保存不同的音频内容,以形成立体声效。左声道保存的大多为一些背景声效,如卡拉OK的消音伴唱。而右声道保存的往往是较为主要的声音,比如主唱的歌声。利用多声道技术,听众可以清晰地分辨出各种乐器来自的方向,从而使音乐更富想象力,更加接近于临场感受。

  有时候我们只需要音频里的单声道内容,比如喜欢用电脑录制卡拉ok的朋友就经常为了找歌曲的伴唱而流连于各大伴奏网站。现在的网络翻唱非常流行,很多网络歌手就是先搜索喜欢的歌曲的伴奏,然后利用Adobe Audition(前身就是大名鼎鼎的CoolEdit)录制自己的演唱,然后加一些简单的降噪和压限处理,最后mix到伴奏的音轨里面。尽管利用Audition也可以完成单声道的提取工作,但是操作起来比较复杂。

  其实对WAV的单声道提取并不困难。关键在于对WAV文件格式的理解。

一、WAV的文件头

  WAV为微软公司(Microsoft)开发的一种声音文件格式,它符合RIFF(Resource Interchange File Format)文件规范。所有的WAV都有一个文件头,这个文件头包含了音频流的编码参数。

偏移地址 字节数 类型 内容
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的文件头

  由表1我们可以得到以下几个重要的信息:

  1. 16H~17H处记录通道数,当值为1时,表示文件为单声道;当值为2时,表示文件为双声道。
  2. 18H~1BH处记录采样频率。它的取值与声卡的支持情况有关。常见的有8000、11025、22050、44100、48000、96000等。其中,44100是大多数歌曲文件采用的标准采样频率。
  3. 22H~23H处记录样本数据位数。即每一个采样的长度。常见的有8位和16位。这里还包含了另外一个信息:若样本的数据位数为n,对于双声道文件,则低n/2位用于存放左声道;高n/2位用于存放右声道。

  根据这三点信息,我们可以自己编程实现单声道的提取。下面我们就来一步步动手实现。由于程序涉及的只是简单的二进制文件读写操作,因此这里只举C#作简单示例,其他语言的处理与之大同小异。

二、文件读取类的编写

  为了方便以后对WAV文件的研究,我们可以先单独写一个WAV文件读取类,专门获取文件头的每一块信息:

WaveAccess
 
        
1 /* WaveAccess
2 * 提供wav文件头的访问
3 * 和文件写入相关操作
4   */
5
6   class WaveAccess
7 {
8
9 private byte [] riff; // 4
10   private byte [] riffSize; // 4
11   private byte [] waveID; // 4
12   private byte [] fmtID; // 4
13   private byte [] notDefinition; // 4
14 private byte [] waveType; // 2
15 private byte [] channel; // 2
16 private byte [] sample; // 4
17 private byte [] send; // 4
18 private byte [] blockAjust; // 2
19 private byte [] bitNum; // 2
20 private byte [] unknown; // 2
21 private byte [] dataID; // 4
22 private byte [] dataLength; // 4
23
24 short [] data;
25 private string longFileName;
26
27 public string LongFileName
28 {
29 get { return longFileName; }
30 }
31
32 public string ShortFileName
33 {
34 get
35 {
36 int pos = LongFileName.LastIndexOf( " \\ " );
37 return LongFileName.Substring(pos + 1 );
38 }
39 }
40
41 public short [] Data
42 {
43 get { return data; }
44 set { data = value; }
45 }
46
47 public string Riff
48 {
49 get { return Encoding.Default.GetString(riff); }
50 set { riff = Encoding.Default.GetBytes(value); }
51 }
52
53 public uint RiffSize
54 {
55 get { return BitConverter.ToUInt32(riffSize, 0 ); }
56 set { riffSize = BitConverter.GetBytes(value); }
57 }
58
59
60 public string WaveID
61 {
62 get { return Encoding.Default.GetString(waveID); }
63 set { waveID = Encoding.Default.GetBytes(value); }
64 }
65
66
67 public string FmtID
68 {
69 get { return Encoding.Default.GetString(fmtID); }
70 set { fmtID = Encoding.Default.GetBytes(value); }
71 }
72
73
74 public int NotDefinition
75 {
76 get { return BitConverter.ToInt32(notDefinition, 0 ); }
77 set { notDefinition = BitConverter.GetBytes(value); }
78 }
79
80
81 public short WaveType
82 {
83 get { return BitConverter.ToInt16(waveType, 0 ); }
84 set { waveType = BitConverter.GetBytes(value); }
85 }
86
87
88 public ushort Channel
89 {
90 get { return BitConverter.ToUInt16(channel, 0 ); }
91 set { channel = BitConverter.GetBytes(value); }
92 }
93
94
95 public uint Sample
96 {
97 get { return BitConverter.ToUInt32(sample, 0 ); }
98 set { sample = BitConverter.GetBytes(value); }
99 }
100
101
102 public uint Send
103 {
104 get { return BitConverter.ToUInt32(send, 0 ); }
105 set { send = BitConverter.GetBytes(value); }
106 }
107
108
109 public ushort BlockAjust
110 {
111 get { return BitConverter.ToUInt16(blockAjust, 0 ); ; }
112 set { blockAjust = BitConverter.GetBytes(value); }
113 }
114
115
116 public ushort BitNum
117 {
118 get { return BitConverter.ToUInt16(bitNum, 0 );}
119 set { bitNum = BitConverter.GetBytes(value); }
120 }
121
122
123 public ushort Unknown
124 {
125 get
126 {
127 if (unknown == null )
128 {
129 return 1 ;
130 }
131 else
132 return BitConverter.ToUInt16(unknown, 0 );
133 }
134
135 set { unknown = BitConverter.GetBytes(value); }
136 }
137
138
139 public string DataID
140 {
141 get { return Encoding.Default.GetString(dataID); }
142 set { dataID = Encoding.Default.GetBytes(value); }
143 }
144
145 public uint DataLength
146 {
147 get { return BitConverter.ToUInt32(dataLength, 0 ); }
148 set { dataLength = BitConverter.GetBytes(value); }
149 }
150
151
152 public WaveAccess() { }
153
154 public WaveAccess( string filepath)
155 {
156 try
157 {
158 riff = new byte [ 4 ];
159 riffSize = new byte [ 4 ];
160 waveID = new byte [ 4 ];
161 fmtID = new byte [ 4 ];
162 notDefinition = new byte [ 4 ];
163 waveType = new byte [ 2 ];
164 channel = new byte [ 2 ];
165 sample = new byte [ 4 ];
166 send = new byte [ 4 ];
167 blockAjust = new byte [ 2 ];
168 bitNum = new byte [ 2 ];
169 unknown = new byte [ 2 ];
170 dataID = new byte [ 4 ]; // 52
171 dataLength = new byte [ 4 ]; // 56 个字节
172
173 longFileName = filepath;
174
175
176 FileStream fs = new FileStream(filepath,FileMode.Open);
177 BinaryReader bread = new BinaryReader(fs);
178 riff = bread.ReadBytes( 4 );
179 riffSize = bread.ReadBytes( 4 );
180 waveID = bread.ReadBytes( 4 );
181 fmtID = bread.ReadBytes( 4 );
182 notDefinition = bread.ReadBytes( 4 );
183 waveType = bread.ReadBytes( 2 );
184 channel = bread.ReadBytes( 2 );
185 sample = bread.ReadBytes( 4 );
186 send = bread.ReadBytes( 4 );
187 blockAjust = bread.ReadBytes( 2 );
188 bitNum = bread.ReadBytes( 2 );
189 if (BitConverter.ToUInt32(notDefinition, 0 ) == 18 )
190 {
191 unknown = bread.ReadBytes( 2 );
192 }
193 dataID = bread.ReadBytes( 4 );
194 dataLength = bread.ReadBytes( 4 );
195 uint length = DataLength / 2 ;
196 data = new short [length];
197 for ( int i = 0 ; i < length; i ++ )
198 {
199 data[i] = bread.ReadInt16(); // 读入2字节有符号整数
200 }
201 fs.Close();
202 bread.Close();
203 }
204 catch (System.Exception ex)
205 {
206 Console.Write(ex.Message);
207 }
208 }
209
210 public short [] GetData( uint begin, uint end )
211 {
212 if ((end - begin) >= Data.Length)
213 return Data;
214 else
215 {
216 uint temp = end - begin + 1 ;
217 short [] dataTemp = new short [temp];
218 uint j = begin;
219 for ( int i = 0 ; i < temp; i ++ )
220 {
221 dataTemp[i] = Data[j];
222 j ++ ;
223 }
224 return dataTemp;
225 }
226
227 }
228
229 ///
230 /// 生成wav文件到系统
231 ///

232 /// 要保存的文件名
233 ///
234 public bool bulidWave( string fileName)
235 {
236 try
237 {
238 FileInfo fi = new FileInfo(fileName);
239 if (fi.Exists)
240 fi.Delete();
241 FileStream fs = new FileStream(fileName, FileMode.CreateNew);
242 BinaryWriter bwriter = new BinaryWriter(fs); // 二进制写入
243 bwriter.Seek( 0 , SeekOrigin.Begin);
244 bwriter.Write(Encoding.Default.GetBytes( this .Riff)); // 不可以直接写入string类型的字符串,字符串会有串结束符,比原来的bytes多一个字节
245 bwriter.Write( this .RiffSize);
246 bwriter.Write(Encoding.Default.GetBytes( this .WaveID));
247 bwriter.Write(Encoding.Default.GetBytes( this .FmtID));
248 bwriter.Write( this .NotDefinition);
249 bwriter.Write( this .WaveType);
250 bwriter.Write( this .Channel);
251 bwriter.Write( this .Sample);
252 bwriter.Write( this .Send);
253 bwriter.Write( this .BlockAjust);
254 bwriter.Write( this .BitNum);
255 if ( this .Unknown != 0 )
256 bwriter.Write( this .Unknown);
257 bwriter.Write(Encoding.Default.GetBytes( this .DataID));
258 bwriter.Write( this .DataLength);
259
260 for ( int i = 0 ; i < this .Data.Length; i ++ )
261 {
262 bwriter.Write( this .Data[i]);
263 }
264
265
266 bwriter.Flush();
267 fs.Close();
268 bwriter.Close();
269 fi = null ;
270 return true ;
271 }
272 catch (System.Exception ex)
273 {
274 Console.Write(ex.Message);
275 return false ;
276 }
277 }
278
279 }

三、单声道提取

  前面提到,若样本的数据位数为n,则对单声道的提取,其实就是提取出n/2的数据。对于任意一位数据,其在新的数据队列中的索引k’与其在源数据队列中的索引k满足如下的映射关系:

 
       
k = 2 * k’ – k’mod(n / 2 ) + n / 2

  但这里有个问题,加入只是将高或者低n/2的数据提取出来合为一个新的文件,则样本的数据位数和文件长度都需要修改为原先的一半,如果没有进行修改,播放速度将变为原来的两倍。

  另外一种解决思路是将我们需要的那n/2的数据提取出来,然后覆盖另外n/2的数据。这样,头文件就不需要进行修改,因为没有任何属性发生了改变,只是文件的内容发生了变化。

  根据上面的思想,对整个WAV文件作一次遍历,每次读入n位数据,如果是要提取左声道,则取出低n/2位数,覆盖高n/2位数;如果是要提取右声道,则取出高n/2位数,覆盖低n/2位数。据此编写singleChannelExtract函数如下:

singleChannelExtract
 
        
1 ///
2 /// singleChannelExtract
3 /// 用于提取单声道
4 ///

5 /// 源文件路径
6 /// 目的文件路径
7 /// 0:提取左声道;1:提取右声道
8 private void singleChannelExtract( string from, string to, int flag)
9 {
10 // 判断是否为双声道
11 WaveAccess wa = new WaveAccess(from);
12
13 if (wa.Channel == 2 )
14 {
15 // 获取位数
16 ushort bitNum = wa.BitNum;
17 int pos1 = 0 , pos2 = 0 ;
18 int lth = ( int )(wa.Data.Length * 2 / bitNum);
19
20 for ( int i = 0 ; i < wa.Data.Length; i += ( int )(bitNum / 2 ))
21 {
22 for ( int j = 0 ; j < ( int )(bitNum);j ++ )
23 {
24 for ( int k = 0 ; k < bitNum / 2 ; k ++ )
25 {
26 // 判断要提取的声道类型
27 switch (flag)
28 {
29 case 0 : // 提取左声道
30 wa.Data[( int )(k + bitNum / 2 )] = wa.Data[k];
31 break ;
32 case 1 : // 提取右声道
33 wa.Data[k] = wa.Data[( int )(k + bitNum / 2 )];
34 break ;
35 default :
36 MessageBox.Show( " Sorry. Only left or right channel is supported for now! " );
37 return ;
38 }
39 }
40 }
41 }
42
43 /* **写入文件 ** */
44 wa.bulidWave(to);
45 wa = null ;
46 MessageBox.Show( " Done! " );
47 return ;
48 }
49 else
50 {
51 MessageBox.Show( " The song already has single channel! " );
52 return ;
53 }

四、实验结果

  根据上面的分析,完成单声道提取器如图1所示。

  

         图1 单声道提取器

  运行该程序后,点击“open...”按钮,打开文件打开对话框,选中要进行单声道提取的文件。完成后,Format Info栏将显示该wav文件的信息。之后,单击“Extract!”按钮,弹出一个文件保存对话框,选择要保存的路径点确定,开始提取单声道,完成后将提示“Done!”。

  图2和图3分别给出了对一段音频进行右声道提取前和提取后的结果。

  

                        图2 提取右声道前

  

                        图3 提取右声道后

  注意图3中原来的左声道内容已经被右声道的内容覆盖,因此此时虽然还是双声道,但两个声道的内容是一样的,因此在使用上与单声道并没有区别。但如果只想保留一个声道,则可以根据前面的阐述,将每个采样值n/2位内容提取出来并合并成一个文件,再修改头文件相关数据,从而达到需要的结果。

点击下载本例源码

你可能感兴趣的:(Windows)