出于研究项目的需要,我需要设计一个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[] 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

224: {

225: dataTemp[i] = Data[j];

226: j++;

227: }

228: return dataTemp;

229: }

230:

231: }

232:

233: ///

234: /// 生成wav文件到系统

235: ///

236: /// 要保存的文件名

237: ///

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: ///

2: /// time2index

3: /// 将时间数组转换为索引

4: ///

5: /// 时间数组,格式为(m:s:ms)

6: /// 采样率

7: ///

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

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: ///

2: /// cutting

3: /// 定长分段切取函数

4: ///

5: /// 源文件

6: /// 目标文件

7: /// 切取时刻对应的索引位置列表

8: /// 切取阈值

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 切取前后波形图对比

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

java wav 切割_WAV音频定长分段切取相关推荐

  1. java wav 切割_java切割音频文件

    工具: 一个jar包即可:jave-1.0.2.jar 可以切割wav格式的音频文件 完整工程目录 就一个jar包,一个main类 代码: package com.zit; import java.i ...

  2. mp3、amr、wav三种音频格式时长获取

    在平时开发过程中可能遇到需要上传相关音频文件到后台,并且计算各种音频文件的时长,因此对三种音频格式(mp3.amr和wav)的时长计算进行了简单的调研,现将相关实现记录一下,也方便需要的朋友查看. ( ...

  3. java socket发送定长报文_一个基于TCP协议的Socket通信实例

    原标题:一个基于TCP协议的Socket通信实例 1. 前言 一般接口对接多以http/https或webservice的方式,socket方式的对接比较少并且会有一些难度.正好前段时间完成了一个so ...

  4. JAVA线程池_并发队列工作笔记0003---线程池的分类_可缓存线程池_定长线程池_定时线程池_单例线程池

    技术交流QQ群[JAVA,C++,Python,.NET,BigData,AI]:170933152 这里说线程池的分类 有可缓存类型, 定长类型, 定时类型, 单例类型, 这里我这次用Executo ...

  5. java在linux转化mp3,linux下微信/qq的aud、silk音频文件格式与mp3格式互转,获取音频时长的方法,附问题分析和java代码。...

    安装ffmpeg ffmpeg -codecs -可以看出ffmpeg默认的MP3格式无法编码为其它格式[D.A.L mp3]D=解码 E=编码 A=AudioCodec L=有损压缩 需要安装lam ...

  6. 获取MP3音频时长 | Java工具类

    获取mp3格式音频时长. Maven依赖 <dependency><groupId>org</groupId><artifactId>jaudiotag ...

  7. linux下微信/qq的aud、silk音频文件格式与mp3格式互转,获取音频时长的方法,附问题分析和java代码。

    安装ffmpeg ffmpeg -codecs -可以看出ffmpeg默认的MP3格式无法编码为其它格式[D.A.L mp3]D=解码 E=编码 A=AudioCodec L=有损压缩 需要安装lam ...

  8. 统计多个wav文件的总音频时长

    最近需要做一件事,统计一个文件夹下多个wav文件的总音频文件. 应该有多种途径,我这里就只说我的做法. 主要工作分成了两步:1.提取单个文件的音频时长信息   2.累加音频时长 1.提取单个文件的音频 ...

  9. java socket发送定长报文_定长消息报文的组包与解包简单封装(Java实现)

    报文 组包 解包 在实际项目中经常会碰到不同系统之间的数据交换,有些是用webservice.有些则是使用发socket消息的方式,将需要发送的消息组装成特定格式的字符串或Xml格式的文件,再通过so ...

最新文章

  1. 免费教材丨第48期:业界大牛中文教学视频《深度学习:进阶》第25-28讲
  2. hash_map 桶扩张逻辑
  3. 用 SpringBoot,亲自打造一个在线题库系统
  4. install npm 到某个文件下执行_如何将npm安装到指定目录?
  5. Linux文件系统十问
  6. Java加密与解密的艺术~DES实现
  7. 智能汽车产业148页深度研究报告:汽车智能开启,拥抱产业变革.pdf(附下载链接)...
  8. C语言如何捕获按下方向键
  9. python 函数篇(2)
  10. Office 2007简体中文版售价抢先看
  11. 接入淘宝客+拼多多(多多客)+京东进行优惠券推广
  12. win10 远程桌面卡顿_win10系统使用远程桌面卡顿的设置教程
  13. 苹果手机没声音了显示耳机模式_别再认为音质不好是耳机的问题,也有可能是你手机没打开这个模式...
  14. Scala基础语法学习
  15. 齐河木凡装饰工程有限公司简介
  16. nginx、php-fpm、mysql用户权限解析
  17. Linux网络设计之实现io_uring用户态接口
  18. ubuntu20.04启动时黑屏
  19. HP刀片服务器系统Flex,深入解析Flex System新一代刀片系统
  20. Ardupilot chibios IO固件,IO与FMU通信,固件下载(3)

热门文章

  1. WebClient UI view controller所有可用的属性列表
  2. IBASE change related BDOC generation
  3. 什么是SAP Commerce Cloud OCC
  4. SAP C4C客户主数据重复检查和清洗实现
  5. Kubernetes里的secret最基本的用法
  6. Netweaver和CloudFoundry的服务器日志
  7. 自定义背景android,Android自定义Button并设置不同背景图片的方法
  8. wps 模拟分析 规划求解_【网友来稿】利用Excel求解线性规划问题
  9. TCP 三次握手四次挥手
  10. 三菱d700变频器模拟量控制_PLC和变频器:开关量控制和模拟量控制什么区别?...