.NET 音訊採集

唐宋元明清2188發表於2024-08-24

本文介紹Windows下聲音資料的採集,用於本地錄音、視訊會議、投屏等場景

聲音錄製有麥克風、揚聲器以及混合錄製三類方式,麥克風和揚聲器單獨錄製的場景更多點,混合錄製更多的是用於本地錄音

我們基於NAudio實現,開源元件NAudio已經很穩定的實現了各類播放、錄製、轉碼等功能,WaveIn,WaveInEvent,WasapiCapture,WasapiLoopbackCapture, WaveOut, WaveStream, WaveFileWriter, WaveFileReader, AudioFileReader都是比較常見的類,下面詳細介紹下錄製模組的實現

麥克風錄製

1.WaveInEvent

透過WaveInEvent類,我們可以捕獲麥克風輸入:

 1     private WaveInEvent _waveIn;
 2     private WaveFileWriter _writer;
 3     private void MainWindow_Loaded(object sender, RoutedEventArgs e)
 4     {
 5         _waveIn = new WaveInEvent();
 6         //441取樣率,單通道
 7         _waveIn.WaveFormat = new WaveFormat(44100, 1);
 8         _writer = new WaveFileWriter("recordedAudio.wav", _waveIn.WaveFormat);
 9         _waveIn.DataAvailable += (s, a) =>
10         {
11             _writer.Write(a.Buffer, 0, a.BytesRecorded);
12         };
13         // 列出所有可用的錄音裝置
14         for (int i = 0; i < WaveIn.DeviceCount; i++)
15         {
16             var deviceInfo = WaveIn.GetCapabilities(i);
17             OutputTextBlock.Text += $"Device {i}: {deviceInfo.ProductName}\r\n";
18         }
19     }
20     private void StartRecordButton_OnClick(object sender, RoutedEventArgs e)
21     {
22         _waveIn.StartRecording();
23     }
24     private void StopRecordButton_OnClick(object sender, RoutedEventArgs e)
25     {
26         _waveIn.StopRecording();
27         _waveIn.Dispose();
28         _writer.Close();
29     }

在每次錄製到資料時,將資料寫入檔案。上面是實現儲存錄音的DEMO

2.WaveIn

還有WaveIn,和WaveInEvent是一樣介面IWaveIn。如果是Windows視窗應用,可以直接使用WaveIn,但需要傳入視窗控制代碼。控制檯應用是無法支援WaveIn的

WaveIn構造引數需要傳入視窗控制代碼,預設不傳的話NAudio會建立一個視窗:

 1     internal void Connect(WaveInterop.WaveCallback callback)
 2     {
 3       if (this.Strategy == WaveCallbackStrategy.NewWindow)
 4       {
 5         this.waveOutWindow = new WaveWindow(callback);
 6         this.waveOutWindow.CreateControl();
 7         this.Handle = this.waveOutWindow.Handle;
 8       }
 9       else
10       {
11          .........
12       }
13     }

另外這裡的WaveWindow是winform視窗,internal class WaveWindow : Form

WaveIn 使用回撥函式(Callback)來處理音訊資料,這種回撥函式會在 Windows 收到音訊資料時透過訊息機制排程。這通常意味著你需要管理並處理這些回撥函式,以確保音訊資料的正確捕捉和處理。然而這也意味著需要更多的底層工作和執行緒安全控制。

而在控制檯這類非GUI應用,就建議使用WaveInEvent了,它未使用視窗訊息,而是透過while迴圈監聽buffers資料,透過判斷buffer.Done是否完成來觸發輸出buffer資料事件DataAvailable。

所以效能來說WaveIn從執行緒處理上會佔優很多,未做過對比測試(待補充

3.WasapiCapture

另外,除了WaveIn API,還可以使用WasapiCapture, 它與WaveIn的使用方式是一致的, 可以用來錄製麥克風WaveInAPI雖然沒有獨佔、共享功能,但也需要處理併發問題,即多個錄音例項訪問同一個麥克風裝置的話會存在併發訪問問題。

WasapiCapture是WASAPI About WASAPI - Win32 apps | Microsoft Learn,全稱Windows Audio Session Application Programming Interface (Windows音訊會話應用程式設計介面) ,它在Windows Vista引入 、提供了一些關鍵的改進

比如,提供更低的音訊延遲和高效能音訊處理,可以提供共享模式和獨佔模式

在共享模式下,可以與多個應用程式共享一個音訊裝置;WasapiCapture.ShareMode = AudioClientShareMode.Shared;

在獨佔模式下,應用程式可以完全控制音訊裝置,降低延遲 AudioClientShareMode.Exclusive

看看WasapiCapture DEMO,都是基於IWaveIn介面實現,所以程式碼無差別:

 1     private WaveFileWriter _writer;
 2     private WasapiCapture _capture;
 3     private void MainWindow_Loaded(object sender, RoutedEventArgs e)
 4     {
 5         _capture = new WasapiCapture();
 6         _writer = new WaveFileWriter("recordedAudio.wav", _capture.WaveFormat);
 7         _capture.DataAvailable += (s, a) =>
 8         {
 9             _writer.Write(a.Buffer, 0, a.BytesRecorded);
10         };
11         // 列出所有可用的錄音裝置
12         for (int i = 0; i < WaveIn.DeviceCount; i++)
13         {
14             var deviceInfo = WaveIn.GetCapabilities(i);
15             OutputTextBlock.Text += $"Device {i}: {deviceInfo.ProductName}\r\n";
16         }
17     }

錄製麥克風音訊,WasapiCapture 是最佳選擇,專為低延遲、高效能設計

另外,如果音訊採集時需要重取樣,可以使用BufferedWaveProvider快取DataAvailable事件過來的原始音訊資料,

 1     //建立BufferedWaveProvider,快取原始音訊資料
 2     var bufferedProvider = new BufferedWaveProvider(provider.NAudioWaveFormat)
 3     {
 4         DiscardOnBufferOverflow = true,
 5         ReadFully = false
 6     };
 7     provider.WaveIn.DataAvailable += (s, e) =>
 8     {
 9         //將音訊資料寫入 BufferedWaveProvider
10         bufferedProvider.AddSamples(e.Buffer, 0, e.BytesRecorded);
11     };
12     //獲取取樣介面
13     var sampleProvider = bufferedProvider.ToSampleProvider();
14     sampleProvider = new WdlResamplingSampleProvider(sampleProvider, TargetFormat.SampleRate);
15     //重取樣後的音訊資料
16     _waveProvider = sampleProvider.ToWaveProvider16();

BufferedWaveProvider、SampleToWaveProvider16均是實現IWaveProvider通用介面,可提供音訊格式以及獲取資料介面

 1   public interface IWaveProvider
 2   {
 3     /// <summary>Gets the WaveFormat of this WaveProvider.</summary>
 4     /// <value>The wave format.</value>
 5     WaveFormat WaveFormat { get; }
 6 
 7     /// <summary>Fill the specified buffer with wave data.</summary>
 8     /// <param name="buffer">The buffer to fill of wave data.</param>
 9     /// <param name="offset">Offset into buffer</param>
10     /// <param name="count">The number of bytes to read</param>
11     /// <returns>the number of bytes written to the buffer.</returns>
12     int Read(byte[] buffer, int offset, int count);
13   }

將重取樣的資料寫入本地檔案儲存:

 1     /// <summary>
 2     /// 目標音訊格式
 3     /// </summary>
 4     public WaveFormat TargetFormat { get; }
 5     public void Save()
 6     {
 7         using var writer = new WaveFileWriter("recordedAudio.wav", TargetFormat);
 8         // 將重取樣後的資料寫入檔案
 9         byte[] buffer = new byte[TargetFormat.AverageBytesPerSecond];
10         int bytesRead;
11         while ((bytesRead = _waveProvider.Read(buffer, 0, buffer.Length)) > 0)
12         {
13             writer.Write(buffer, 0, bytesRead);
14         }
15     }

這樣,我們使用 WasapiCapture 捕獲音訊資料,並將這些資料實時重取樣到指定取樣率如44.1kHz(常見的取樣率有441和480),單聲道格式。錄音結束後,重取樣後的音訊資料再被儲存到一個WAV檔案中。

另外如果是單通道聲音,可以轉換成多通道即立體聲:

1     // Mono to Stereo
2     if (simpleFormat.Channels == 1)
3     {
4         sampleProvider = sampleProvider.ToStereo();
5     }

ToStereo返回的MonoToStereoSampleProvider,會將單通道聲音資料,轉換為雙通道的音訊格式。但實際上,取樣器MonoToStereoSampleProvider內部只有一份source資料,在Read時外部引數Samples直接除以2即變成了1,左右聲道均輸出此音訊資料。

揚聲器錄製

錄製揚聲器聲音即音效卡輸出,藉助WasapiLoopbackCapture可簡單實現,使用方式與WasapiCapture沒區別。部分程式碼:

 1     var capture = new WasapiLoopbackCapture();
 2     var writer = new WaveFileWriter("recordedAudio.wav", capture.WaveFormat);
 3     capture.DataAvailable += (s, a) =>
 4     {
 5         writer.Write(a.Buffer, 0, a.BytesRecorded);
 6     };
 7     capture.StartRecording();
 8     // 列出所有可用的揚聲器裝置
 9     for (int i = 0; i < WaveOut.DeviceCount; i++)
10     {
11         var deviceInfo = WaveOut.GetCapabilities(i);
12         OutputTextBlock.Text += $"Device {i}: {deviceInfo.ProductName}\r\n";
13     }

1. 音訊視覺化

值得另外說的,揚聲器錄製有一類廠測場景,上位機工廠測試軟體測試揚聲器,需要顯示聲道的音訊曲線

音訊波形圖或者頻譜圖,可以透過DataAvailable拿到的位元組陣列,根據視覺化圖X座標需要顯示的點列數量,在陣列中獲取資料然後對映到視覺化圖表座標Y值上。詳細的可參考這篇 [C#] 使用 NAudio 實現音訊視覺化_c#聲音訊譜-CSDN部落格,它實現的是曲線,也可以另外換成柱狀圖。

錄製揚聲器,有些場景需要關閉本地揚聲器外放。投屏軟體有這個場景,會將當前裝置A的音效卡音訊資料傳輸到其它裝置B上播放,但裝置A不想重複播放聲音。因為裝置A播放聲音的話,會議室會有混音,並且投屏裝置A一般是筆記本、裝置B是會議大屏,揚聲器質量和功率是不如專業的互動大屏的,大屏揚聲器價格會貴點。

1     var volume = playbackDevice.AudioEndpointVolume;
2     // 記錄原音量,用於結束錄製時恢復音量
3     float originalVolume = volume.MasterVolumeLevelScalar;
4     // 靜音播放裝置
5     volume.MasterVolumeLevelScalar = 0;

2.保持揚聲器活躍

同時,錄製揚聲器是一個持續活動,為避免因無音訊訊號導致裝置自動關閉或進入低功耗狀態,在不想關閉音訊裝置而又沒有實際音訊播放任務時,會用沉默音訊保持裝置活躍。可以按如下操作

1).建立一個WasapiOut例項,指定使用共享模式:var wasapiOut = new WasapiOut(device, AudioClientShareMode.Shared, true, 50);

2).獲取音訊裝置MMDevice的AudioClient物件

using var audioClient = device.AudioClient;
wasapiOut.Init(new SilenceProvider(audioClient.MixFormat));

3).在啟動WasapiLoopbackCapture錄製時,將此靜音波形播放物件啟動,持續生成靜音訊號

混音錄製

也有必要介紹下混音錄製,雖然場景較少。

初始化多個麥克風、揚聲器錄製器,然後同上面重取樣操作,建立一個 BufferedWaveProvider (bufferedProvider),用於儲存輸入的音訊資料。

訂閱訂閱 IWaveIn 的 DataAvailable 事件,將資料都塞進快取音訊快取器

最後返回16位浮點波形資料儲存器,IWaveProvider資料獲取方式同上面重取樣操作。

 1     public MixAudioCapture(params IWaveIn[] audioWaveCaptures)
 2     {
 3         _audioWaveCaptures = audioWaveCaptures;
 4         var sampleProviders = new List<ISampleProvider>();
 5         foreach (var waveIn in audioWaveCaptures)
 6         {
 7             var bufferedProvider = new BufferedWaveProvider(waveIn.WaveFormat)
 8             {
 9                 DiscardOnBufferOverflow = true,
10                 ReadFully = false
11             };
12             waveIn.DataAvailable += (s, e) =>
13             {
14                 bufferedProvider.AddSamples(e.Buffer, 0, e.BytesRecorded);
15             };
16             var sampleProvider = bufferedProvider.ToSampleProvider();
17             sampleProviders.Add(sampleProvider);
18         }
19         var waveProviders = sampleProviders.Select(m => m.ToWaveProvider());
20         // 混音後的音訊資料
21         _waveProvider = new MixingWaveProvider32(waveProviders).ToSampleProvider().ToWaveProvider16();
22     }

一般混音的同時,也會重取樣。看具體場景操作吧

參考:

簡要介紹WASAPI播放音訊的方法 - PeacoorZomboss - 部落格園 (cnblogs.com)

[C#] 使用 NAudio 實現音訊視覺化_c#聲音訊譜-CSDN部落格

naudio/NAudio: Audio and MIDI library for .NET (github.com)

關鍵字:音訊採集、麥克風/揚聲器/混音採集

相關文章