java wav 切割_WAV音频定长分段切取
出于研究項目的需要,我需要設計一個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音频定长分段切取的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java设置断点,在Java中设置断点
- 下一篇: 多线程锁,线程池,消费者生产者模型