WAV音频定长分段切取

作者:Joseph Pan(转载请注明出处:http://www.cnblogs.com/weizhoupan/archive/2011/03/23/1993222.html
 
出于研究项目的需要,我需要设计一个WAV音频文件定长切取的小功能:给定一个WAV文件、一组记录时间信息的数组t_Array以及一个阈值ΔT,要求从这个文件中切取出以t_Array记录的每个时刻t为中心的ΔT长度的片段,合并为一个新的文件;对于相邻的任意两个时刻t1和t2,其以各自为中心切取的片段不能够包含以另一个时刻为起点或终点的片段。即如果t2在t1之后,则以t2为中心切取的片段不能包含t1及t1以前的数据,且以t1为中心切取的片段不能包含t2及t2以后的数据。

一、问题分析(Problem Analysis)

最理想的情况下,相邻的两个时刻tk与tk+1、第一个时刻t0与0时刻、最后一个时刻tn-1与文件末尾的间隔都大于ΔT/2,整个音频序列如图1所示,轴线代表一段WAV音频文件,tk为待切取的中心时刻序列,以每个t为中心,待切取的片段用虚框矩形表示。:

image

图1 理想情况

如图1所示,这种情况下的切取工作只需一次性遍历整个时刻数组,然后以每个时刻为中心切取周围ΔT长度的序列。

然而很多情况下我们并不是这么幸运,为了使得系统更加鲁棒,我们应该考虑以下几种情况:

  1. 存在两个相邻时刻tk与tk+1之间的间隔小于ΔT/2,如图2所示:

image

图2 相邻时刻间隔小于ΔT/2的情况

  1. 第一个时刻t0与0时刻之间的间隔小于ΔT/2,如图3所示:

                      image

图3 t0与0时刻间隔小于ΔT/2的情况

  1. 最后一个时刻tn-1与末尾的间隔小于ΔT/2,如图4所示:

image

图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)

  1. 实验平台:Windows
  2. 开发语言: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中定义如下:

image

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所示:

image

图5 切取阈值选项

以400ms为切取阈值,对一段音频进行定长分段切取,其切取前后的波形图案对比图如图6所示:

image

(a) 切分前的波形图案

image

(b) 切分后的波形图案

图6 切取前后波形图对比

图中的白线表示每一个给定的切取时刻。

posted @ 2011-03-23 23:07  Joseph Pan  阅读(2486)  评论(0编辑  收藏  举报