喜欢买碟或者卡拉ok的朋友一定不会对声道这个术语陌生。通常我们在音像店买回来的VCD或者DVD都是双声道的形式,唱片商在录制唱片时往往提供了两个或多个声道,以保存不同的音频内容,以形成立体声效。左声道保存的大多为一些背景声效,如卡拉OK的消音伴唱。而右声道保存的往往是较为主要的声音,比如主唱的歌声。利用多声道技术,听众可以清晰地分辨出各种乐器来自的方向,从而使音乐更富想象力,更加接近于临场感受。 有时候我们只需要音频里的单声道内容,比如喜欢用电脑录制卡拉ok的朋友就经常为了找歌曲的伴唱而流连于各大伴奏网站。现在的网络翻唱非常流行,很多网络歌手就是先搜索喜欢的歌曲的伴奏,然后利用Adobe Audition(前身就是大名鼎鼎的CoolEdit)录制自己的演唱,然后加一些简单的降噪和压限处理,最后mix到伴奏的音轨里面。尽管利用Audition也可以完成单声道的提取工作,但是操作起来比较复杂。 其实对WAV的单声道提取并不困难。关键在于对WAV文件格式的理解。 一、WAV的文件头 WAV为微软公司(Microsoft)开发的一种声音文件格式,它符合RIFF(Resource Interchange File Format)文件规范。所有的WAV都有一个文件头,这个文件头包含了音频流的编码参数。
表1 WAV的文件头 由表1我们可以得到以下几个重要的信息:
根据这三点信息,我们可以自己编程实现单声道的提取。下面我们就来一步步动手实现。由于程序涉及的只是简单的二进制文件读写操作,因此这里只举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位内容提取出来并合并成一个文件,再修改头文件相关数据,从而达到需要的结果。 点击下载本例源码 |