[C#] NAudio 庫的各種常用使用方式: 播放 錄製 轉碼 音訊視覺化

SlimeNull發表於2021-05-06

概述

在 NAudio 中, 常用型別有 WaveIn, WaveOut, WaveStream, WaveFileWriter, WaveFileReader, AudioFileReader 以及介面: IWaveProvider, ISampleProvider, IWaveIn, IWavePlayer

  1. WaveIn 表示波形輸入, 繼承了 IWaveIn, 例如麥克風輸入, 或者計算機正在播放的音訊流.
  2. WaveOut 表示波形輸出, 繼承了 IWavePlayer, 用來播放波形音樂, 以 IWaveProvider 作為播放源播放音訊, 通過擴充方法也支援以 ISampleProvider 作為播放源播放音訊
  3. WaveStream 表示波形流, 它繼承了 IWaveProvider, 可以用來作為播放源.
  4. WaveFileReader 繼承了 WaveStream, 用來讀取波形檔案
  5. WaveFileWriter 繼承了Stream, 用來寫入檔案, 常用於儲存音訊錄製的資料.
  6. AudioFileReader 通用的音訊檔案讀取器, 可以讀取波形檔案, 也可以讀取其他型別的音訊檔案例如 Aiff, MP3
  7. IWaveProvider 波形提供者, 上面已經提到, 是音訊播放的提供者, 通過擴充方法可以轉換為 ISampleProvider
  8. ISampleProvider 取樣提供者, 上面已經提到, 通過擴充方法可以作為 WaveOut 的播放源

播放音訊

常用的播放音訊方式有兩種, 播放波形音樂, 以及播放 MP3 音樂

  1. 播放波形音樂:

    // NAudio 中, 通過 WaveFileReader 來讀取波形資料, 在例項化時, 你可以指定檔名或者是輸入流, 這意味著你可以讀取記憶體流中的音訊資料
    // 但是需要注意的是, 不可以讀取來自網路流的音訊, 因為網路流不可以進行 Seek 操作.
    
    // 此處, 假設 ms 為一個 MemoryStream, 記憶體有音訊資料.
    WaveFileReader reader = new WaveFileReader(ms);
    WaveOut wout = new WaveOut();
    wout.Init(reader);             // 通過 IWaveProvider 為音訊輸出初始化
    wout.Play();                   // 至此, wout 將從指定的 reader 中提供的資料進行播放
    
  2. 播放 MP3 音樂:

    // 播放 MP3 音樂其實與播放波形音樂沒有太大區別, 只不過將 WaveFileReader 換成了 Mp3FileReader 罷了
    // 另外, 也可以使用通用的 Reader, MediaFoundationReader, 它既可以讀取波形音樂, 也可以讀取 MP3
    
    // 此處, 假設 ms 為一個 MemoryStream, 記憶體有音訊資料.
    Mp3FileReader reader = new Mp3FileReader(ms);
    WaveOut wout = new WaveOut();
    wout.Init(reader);
    wout.Play();
    

音訊錄製

  1. 錄製麥克風輸入

    // 藉助 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 名稱空間下
    
  2. 錄製音效卡輸出

    // 錄製音效卡輸出, 也就是錄製計算機正在播放的聲音, 藉助 WasapiLoopbackCapture 即可簡單實現, 使用方式與 WasapiCapture 無異
    
    WasapiLoopbackCapture cap = new WasapiLoopbackCapture();
    WaveFileWriter writer = new WaveFileWriter();
    cap.DataAvailable += (s, args) => writer.Write(args.Buffer, 0, args.BytesRecorded);
    cap.StartRecording();
    

高階應用

  1. 獲取計算機實時播放音量大小

    // 其實這個是基於剛剛的錄製音效卡輸出的, 錄製時的回撥中, 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);                // 找到值最大的取樣
    
  2. 實現音樂視覺化

    // 既然我們已經知道了, 那些資料都是一個個的取樣, 自然也可以通過它們來繪製頻譜, 只需要進行快速傅立葉變換即可
    // 而且有意思的是, 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();    // 取得結果
    };
    
  3. 音訊格式轉換

    // 對於 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 值, 那麼每兩個取樣時間點相同, 而各個通道的取樣就是每一組中每一個取樣

image

所以對於我們剛剛進行的音樂視覺化, 嚴格意義上來講, 還需要區分通道

示例

本文提到的部分內容在 github.com/SlimeNull/AudioTest 倉庫中有示例, 例如音訊視覺化, 音訊錄製, 以及其他零星的示例


如有錯誤, 還請指出

相關文章