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

概述

在 NAudio 中, 常用類型有 WaveIn, WaveOut, WaveStream, WaveFileWriter, WaveFileReader 以及介面: IWaveProvider

  1. WaveIn 表示波形輸入, 例如麥克風輸入, 或者電腦正在播放的音頻流.
  2. WaveOut 表示波形輸出, 用來播放波形音樂, 以繼承了 IWaveProvider 的類型作為播放源播放音樂
  3. WaveStream 表示波形流, 它繼承了 IWaveProvider, 可以用來作為播放源.
  4. WaveFileReader 繼承了 WaveStream, 用來讀取波形文件
  5. WaveFileWriter 繼承了 Stream, 用來寫入文件, 常用於保存音頻錄製的數據
  6. IWaveProvider 上面已經提到, 是音頻播放的提供者

播放音頻

常用的播放音頻方式有兩種, 播放波形音樂, 以及播放 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 倉庫中有示例, 例如音頻可視化, 音頻錄製, 以及其他零星的示例


如有錯誤, 還請指出