概述
在 NAudio 中, 常用型別有 WaveIn, WaveOut, WaveStream, WaveFileWriter, WaveFileReader, AudioFileReader 以及介面: IWaveProvider, ISampleProvider, IWaveIn, IWavePlayer
- WaveIn 表示波形輸入, 繼承了 IWaveIn, 例如麥克風輸入, 或者計算機正在播放的音訊流.
- WaveOut 表示波形輸出, 繼承了 IWavePlayer, 用來播放波形音樂, 以 IWaveProvider 作為播放源播放音訊, 通過擴充方法也支援以 ISampleProvider 作為播放源播放音訊
- WaveStream 表示波形流, 它繼承了 IWaveProvider, 可以用來作為播放源.
- WaveFileReader 繼承了 WaveStream, 用來讀取波形檔案
- WaveFileWriter 繼承了Stream, 用來寫入檔案, 常用於儲存音訊錄製的資料.
- AudioFileReader 通用的音訊檔案讀取器, 可以讀取波形檔案, 也可以讀取其他型別的音訊檔案例如 Aiff, MP3
- IWaveProvider 波形提供者, 上面已經提到, 是音訊播放的提供者, 通過擴充方法可以轉換為 ISampleProvider
- ISampleProvider 取樣提供者, 上面已經提到, 通過擴充方法可以作為 WaveOut 的播放源
播放音訊
常用的播放音訊方式有兩種, 播放波形音樂, 以及播放 MP3 音樂
-
播放波形音樂:
// NAudio 中, 通過 WaveFileReader 來讀取波形資料, 在例項化時, 你可以指定檔名或者是輸入流, 這意味著你可以讀取記憶體流中的音訊資料 // 但是需要注意的是, 不可以讀取來自網路流的音訊, 因為網路流不可以進行 Seek 操作. // 此處, 假設 ms 為一個 MemoryStream, 記憶體有音訊資料. WaveFileReader reader = new WaveFileReader(ms); WaveOut wout = new WaveOut(); wout.Init(reader); // 通過 IWaveProvider 為音訊輸出初始化 wout.Play(); // 至此, wout 將從指定的 reader 中提供的資料進行播放
-
播放 MP3 音樂:
// 播放 MP3 音樂其實與播放波形音樂沒有太大區別, 只不過將 WaveFileReader 換成了 Mp3FileReader 罷了 // 另外, 也可以使用通用的 Reader, MediaFoundationReader, 它既可以讀取波形音樂, 也可以讀取 MP3 // 此處, 假設 ms 為一個 MemoryStream, 記憶體有音訊資料. Mp3FileReader reader = new Mp3FileReader(ms); WaveOut wout = new WaveOut(); wout.Init(reader); wout.Play();
音訊錄製
-
錄製麥克風輸入
// 藉助 WaveIn 類, 我們可以輕易的捕獲麥克風輸入, 在每一次錄製到資料時, 將資料寫入到檔案或其他流, 這就實現了儲存錄音 // 在儲存波形檔案時需要藉助 WaveFileWriter, 當然, 如果你想儲存為其他格式, 也可以使用其它的 Writer, 例如 CurWaveFileWriter 以及 // AiffFileWriter, 美中不足的是沒有直接寫入到 MP3 的 FileWriter // 需要注意的是, 如果你是用的桌面程式, 那麼你可以直接使用 WaveIn, 其回撥基於 Windows 訊息, 所以無法在控制檯應用中使用 WaveIn // 如果要在控制檯應用中實現錄音, 只需要使用 WaveInEvent, 它的回撥基於事件而不是 Windows 訊息, 所以可以通用 WaveIn cap = new WaveIn(); // cap, capture WaveFileWriter writer = new WaveFileWriter(); cap.DataAvailable += (s, args) => writer.Write(args.Buffer, 0, args.BytesRecorded); // 訂閱事件 cap.StartRecording(); // 開始錄製 // 結束錄製時: cap.StopRecording(); // 停止錄製 writer.Close(); // 關閉 FileWriter, 儲存資料 // 另外, 除了使用 WaveIn, 你還可以使用 WasapiCapture, 它與 WaveIn 的使用方式是一致的, 可以用來錄製麥克風 // Wasapi 全稱 Windows Audio Session Application Programming Interface (Windows音訊會話應用程式設計介面) // 具體 WaveIn, WaveInEvent, WasapiCapture 的效能, 筆者還沒有測試過, 但估計不會有太大差異. // 提示: WasapiCapture 和 WasapiLoopbackCapture 位於 NAudio.Wave 名稱空間下
-
錄製音效卡輸出
// 錄製音效卡輸出, 也就是錄製計算機正在播放的聲音, 藉助 WasapiLoopbackCapture 即可簡單實現, 使用方式與 WasapiCapture 無異 WasapiLoopbackCapture cap = new WasapiLoopbackCapture(); WaveFileWriter writer = new WaveFileWriter(); cap.DataAvailable += (s, args) => writer.Write(args.Buffer, 0, args.BytesRecorded); cap.StartRecording();
高階應用
-
獲取計算機實時播放音量大小
// 其實這個是基於剛剛的錄製音效卡輸出的, 錄製時的回撥中, Buffer, BytesRecorded 指定了此次錄製的資料 (緩衝區和資料長度) // 而這些資料, 其實是計算機對聲音的取樣(Sample), 具體的取樣格式可以檢視 WasapiLoopbackCapture 例項的 WaveForamt // 波形格式中的 Encoding 與 BitsPerSample 是我們所需要的. 一般預設的 Encoding 是 IeeeFloat, 也就是每一個取樣都是 // 一個浮點數, 而 BitsPerSample 也就是 32 了. 通過 BitConverter.ToSingle() 我們可以從緩衝區中取得浮點數 // 遍歷, 每 32 位一個浮點數, 最終取最大值, 這就是我們所需要的音量了 float volume; WasapiLoopbackCapture cap = new WasapiLoopbackCapture(); cap.DataAvailable += (s, args) => volume = Enumerable .Range(0, args.BytesRecorded / 4) // 每一個取樣的位置 .Select(i => BitConverter.ToSingle(args.Buffer, i * 4)) // 獲取每一個取樣 .Aggregate((v1, v2) => v1 > v2 ? v1 : v2); // 找到值最大的取樣
-
實現音樂視覺化
// 既然我們已經知道了, 那些資料都是一個個的取樣, 自然也可以通過它們來繪製頻譜, 只需要進行快速傅立葉變換即可 // 而且有意思的是, NAudio 也為我們準備好了快速傅立葉變換的方法, 位於 NAudio.Dsp 名稱空間下 // 提示: 進行傅立葉變換所需要的複數(Complex)類也位於 NAudio.Dsp 名稱空間, 它有兩個欄位, X(實部) 與 Y(虛部) // 下面給出在 IeeeFloat 格式下的音樂視覺化的簡單示例: WasapiLoopbackCapture cap = new WasapiLoopbackCapture(); cap.DataAvailable += (s, args) => { float[] samples = Enumerable .Range(0, args.BytesRecorded / 4) .Select(i => BitConverter.ToSingle(args.Buffer, i * 4)) .ToArray(); // 獲取取樣 int log = (int)Math.Ceiling(Math.Log(samples.Length, 2)); float[] filledSamples = new float[(int)Math.Pow(2, log)]; Array.Copy(samples, filledSamples, samples.Length); // 填充資料 int sampleRate = (s as WasapiLoopbackCapture).WaveFormat.SampleRate; // 獲取取樣率 Complex[] complexSrc = filledSamples.Select((v, i) => { double deg = i / (double)sampleRate * Math.PI * 2; // 獲取當前取樣率在圓上對應的角度 (弧度制) return new Complex() { X = (float)(Math.Cos(deg) * v), Y = (float)(Math.Sin(deg) * v) }; }).ToArray(); // 將取樣轉換為對應的複數 (纏繞到圓) FastFourierTransform.FFT(false, log, complexSrc); // 進行傅立葉變換 double[] result = complexSrc.Select(v => Math.Sqrt(v.X * v.X + v.Y * v.Y)).ToArray(); // 取得結果 };
-
音訊格式轉換
// 對於 Wave, CueWave, Aiff, 這些格式都有其對應的 FileWriter, 我們可以直接呼叫其 Writer 的 Create***File 來 // 從 IWaveProvider 建立對應格式的檔案. 對於 MP3 這類沒有 FileWriter 的格式, 可以呼叫 MediaFoundationEncoder // 例如一個檔案, "./Disconnected.mp3", 我們要將它轉換為 wav 格式, 只需要使用下面的程式碼, CurWave 與 Aiff 同理 using (Mp3FileReader reader = new Mp3FileReader("./Disconnected.mp3")) WaveFileWriter.CreateWaveFile("./Disconnected.wav", reader); // 從 IWaveProvider 建立 MP3 檔案, 假如一個 WaveFileReader 為 src MediaFoundationEncoder.EncodeToMp3(src, "./NewMp3.mp3");
提示
對於剛剛所說的音訊錄製, 取樣的格式有一點需要注意, 將資料轉換為一個 float 陣列後, 其中還需要區分音訊通道, 例如一般音樂是雙通道, WaveFormat 的 Channel 為 2, 那麼在 float 陣列中, 每兩個取樣為一組, 一組取樣中每一個取樣都是一個通道在當前時間內的取樣.
以雙通道距離, 下圖中, 取樣資料中每一個圓圈都表示一個 float 值, 那麼每兩個取樣時間點相同, 而各個通道的取樣就是每一組中每一個取樣
所以對於我們剛剛進行的音樂視覺化, 嚴格意義上來講, 還需要區分通道
示例
本文提到的部分內容在 github.com/SlimeNull/AudioTest 倉庫中有示例, 例如音訊視覺化, 音訊錄製, 以及其他零星的示例
如有錯誤, 還請指出