[UWP] 用 AudioGraph 來增強 UWP 的音頻處理能力——AudioFrameInputNode

来源:https://www.cnblogs.com/cjw1115/archive/2018/12/09/10092526.html
-Advertisement-
Play Games

上一篇心得記錄中提到了 AudioGraph, 描述了一下 什麼是 AudioGraph 以及其中涉及到的各種類型的 節點(Node)。 這一篇就其中比較有意思的 AudioFrameInputNode 來詳細展開一下。 借用 AudioFrameInputNode, 實現簡單的音頻左右聲道互換 什 ...


上一篇心得記錄中提到了 AudioGraph, 描述了一下 什麼是 AudioGraph 以及其中涉及到的各種類型的 節點(Node)。

這一篇就其中比較有意思的 AudioFrameInputNode 來詳細展開一下。

借用 AudioFrameInputNode, 實現簡單的音頻左右聲道互換

什麼是 AudioFrameInputNode?

在微軟的文檔中這麼介紹

An audio frame input node allows you to push audio data that you generate in your own code into the audio graph. This enables scenarios like creating a custom software synthesizer.

按照我個人的理解,AudioFrameInputNode 可以讓我們自由的訪問音頻數據,音頻數據是 PCM 格式,我們可以對音頻數據做一些魔改,具體怎麼魔改,就需要一些音頻處理的演算法知識了。

如何使用 AudioFrameInputNode?

1.創建 AudioFrameInputNode

AudioEncodingProperties nodeEncodingProperties = audioGraph.EncodingProperties;
nodeEncodingProperties.ChannelCount = 2;
nodeEncodingProperties.Subtype = "float";
nodeEncodingProperties.SampleRate = 44100;
nodeEncodingProperties.BitsPerSample = 32;

AudioFrameInputNode frameInputNode = audioGraph.CreateFrameInputNode(nodeEncodingProperties);
frameInputNode.QuantumStarted += FrameInputNode_QuantumStarted;

所有的音頻輸入節點,都必須通過 AudioGragh 的實例方法來創建,AudioFrameInputNode 也不例外,在創建時,需要傳入一個 AudioEncodingProperties,來描述 AudioFrameInputNode 需要處理的音頻的一些屬性。

在創建完成一個 AudioFrameInputNode 的對象實例後,需要訂閱其 QuantumStarted 事件,這個事件會在 AudioGraph 開始處理音頻數據時調用,在該事件方法內部,可以完成對音頻數據的添加和修改。

2.訪問 AudioFrame

AudioFrameInputNode 是基於 AudioFrame, 需要對其數據進行讀取和寫入。

所以在事件的訂閱方法 FrameInputNode_QuantumStarted 內部,需要對 AudioFrame 填充 PCM 音頻數據。

首先需要創建一個 AudioFrame 對象,在構造函數中,需要傳入緩衝區的大小。

在這個示例中,每一個 採樣點(Sample) 都是 Float 類型,採用立體聲,也就是雙通道,所以計算緩衝區大小的代碼如下:

var bufferSize = args.RequiredSamples * sizeof(float) * 2;
AudioFrame audioFrame = new AudioFrame((uint)bufferSize);

在 AudioFrame 內部是一個 AudioBuffer,它代表存儲 PCM 數據的緩衝區,所以接下來需要獲取對該緩衝區的訪問權,需要如下方法:

AudioBuffer audioBuffer = audioFrame.LockBuffer(AudioBufferAccessMode.Write);
IMemoryBufferReference bufferReference = audioBuffer.CreateReference();

通過 AudioBuffer 的實例方法 CreateReference,得到 IMemoryBufferReference 的對象,它實際上是一個 COM 介面,通過如下方法強制轉換,可以獲取 native 的緩衝區指針和緩衝區長度:

((IMemoryBufferByteAccess)bufferReference).GetBuffer(out byte* dataInBytes, out uint capacityInBytes);

其中 IMemoryBufferByteAccess 介面定義如下:

[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

註意,因為用到了指針,所以需要在工程配置文件中 允許unsafe code 選項打開, 並且在該方法簽名中指明 unsafe 關鍵字。

至此,就得到了音頻數據緩衝區的指針,但是此時整個緩衝區都是空的,需要填充 PCM 音頻數據。

此處便是 AudioFrame 的便利之處,因為我們可以任意填充我們想要的音頻數據,無論是處理過的還是沒有處理過的。而獲取 PCM 原始音頻數據的途徑很多,可以代碼生成,也可以從文件讀取,對於我這種對音頻處理技術幾乎白痴的人,我選擇從一個 PCM 文件導入。

此處可以借用 Adobe Audition 等工具轉換生成 PCM。

3.PCM 音頻數據填充

打開一個 PCM 格式的文件流 fileStream, 其中 PCM 採樣率是44100,32位浮點型,立體聲。這些格式很重要,需要和初始化 AudioFrameInputNode 對象實例時設定的一樣,才能保證數據填充過程正確。

在構造 AudioFrame 時傳入了代表緩衝區長度的值 bufferSize,所以此處需要從文件流 fileStream 讀取對應長度的數據到記憶體中,

var managedBuffer = new byte[capacityInBytes];

var lastLength = fileStream.Length - fileStream.Position;
int readLength = (int)(lastLength < capacityInBytes ? lastLength : capacityInBytes);
if (readLength <= 0)
{
    fileStream.Close();
    fileStream = null;
    return;
}
fileStream.Read(managedBuffer, 0, readLength);

為了稍微體現一下 AudioFrameInputNode 的價值,這兒對要填充的數據做一項最簡單的處理,即交換左右聲道的內容。

在 PCM 中,每一個 Sample 是四個位元組,具體排布是:

左聲道,右聲道,左聲道,右聲道,左聲道,右聲道,左聲道,右聲道........

所以交換聲道就很簡單了,代碼如下:

for (int i = 0; i < readLength; i+=8)
{
    dataInBytes[i+4] = managedBuffer[i+0];
    dataInBytes[i+5] = managedBuffer[i+1];
    dataInBytes[i+6] = managedBuffer[i+2];
    dataInBytes[i+7] = managedBuffer[i+3];

    dataInBytes[i+0] = managedBuffer[i+4];
    dataInBytes[i+1] = managedBuffer[i+5];
    dataInBytes[i+2] = managedBuffer[i+6];
    dataInBytes[i+3] = managedBuffer[i+7];
}

因為 dataInBytes 是緩衝區的指針,所以對緩衝區賦值就是填充緩衝區的過程。在填充完後,需要釋放 audioBuffer 和 bufferReference 對象,避免記憶體泄漏。

踩到的坑

  1. 大小端問題

    借用百度百科內容:

    大端模式,是指數據的高位元組保存在記憶體的低地址中,而數據的低位元組保存在記憶體的高地址中,這樣的存儲模式有點兒類似於把數據當作字元串順序處理:地址由小向大增加,而數據從高位往低位放;這和我們的閱讀習慣一致。

    小端模式,是指數據的高位元組保存在記憶體的高地址中,而數據的低位元組保存在記憶體的低地址中,這種存儲模式將地址的高低和數據位權有效地結合起來,高地址部分權值高,低地址部分權值低。

    二進位內容在記憶體裡面存儲,是存在大小端問題的,對於PCM格式,也存在大小端問題,所以如果對數據想進一步處理,大小端的問題一定要註意。

    在C#中調用 native 內容時,我的機器上實測時小端模式。

    也可以通過如下 unsafe 代碼來判斷:

    int temp = 0x01;
    int* pTempInt = &temp;
    byte* pTempByte = (byte*)pTempInt;
    if(0x01== *pTempByte)
    {
        //小端
    }
    else
    {
        //大端
    }
  2. float 在記憶體中如何排布?

    對於 int 類型,將其轉換為二進位後,求補碼,即是它在記憶體中的實際值,但是對於浮點型,就有一套自己的計算方法了,可以參考如下博客(大學電腦課本里的內容,忘得差不多了)

    float & double 記憶體佈局

附件

Github AudioFrameInputNode Demo

附上我測試用的 PCM 數據,44100,32位 浮點型,小端模式

聽說最近杭州下雪了,這歌現在很火!

許嵩-斷橋殘雪 片段 PCM

下圖是該 PCM 的原始波形圖,

波形圖

所以聽的時候聽到的順序應該是:先右聲道,再立體聲,最後左聲道,和波形圖裡相反。

記得耳機別戴反!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 今天晚上學習《零基礎學Java》,看到第50頁時,發現書上一行代碼自己想不通,這行代碼意思是將123按位取反,程式運行後輸出結果是-124,即~123=-124. 開始我個人理解是123轉換成二進位為:01111011,按位取反後為10000100,即132,與真實輸出結果-124不一致。後來我查閱 ...
  • python學習之數字 1.python數值類型 2. 數字類型轉換 3. 常用函數 3.1 數學函數 3.2 隨機函數 ...
  • 列表(list) 基本操作 比如說我要整理一個近期熱映的電影列表: 列表很像數組,但功能超越數組。列表都是從0開始的,python中列表無需事先聲明類型。 從列表後面加上一個新的元素,比如說加個“無名之輩”,是 方法。 刪除列表最後一個元素: 方法。 兩個列表相銜接,用的是 方法。 我想在某個條件下 ...
  • python學習之運算符; 算術運算符; 比較運算符;位運算符; 邏輯運算符; 成員運算符; 身份運算符; ++ 或 -- 自運算符; is 和 ==; 數組的兩種賦值方式區別 ...
  • 這兩天比較忙,周末也在加班,所以更新的就慢了一點,不過沒關係,今天我們就進行千呼萬喚的系統開發框架的設計。不知道上篇關於架構設計的文章大家有沒有閱讀,如果閱讀後相信一定對架構設計有了更近一部的理解,如果你沒有閱讀也希望大家能好好閱讀一下!其實說白了,架構是為了應對軟體系統複雜度而提出的一個解決方案, ...
  • .NET Core跨平臺部署 1. Windows IIS 大家對於在IIS上部署.NET站點已經駕輕就熟了,部署.NET Core也沒有什麼本質區別,但是這其中仍然有一些細節是不同的,下麵記錄了一些我在部署時遇到的問題 1.1 安裝.NET Core Windows Server Hosting ...
  • 上一章我們搭建了k8s集群,這一章我們開始在k8s集群上運行.netcore程式 1.kubectl run 在我的Docker系列教程里,我曾往docker hub中推送過一個鏡像“webdokcer_s_provider”,今天我們就是使用這個鏡像來配合K8S來演示。 我們可以看到,創建了資源d ...
  • 參考文檔:https://www.cnblogs.com/xbzhu/p/7064642.html 這是參考文檔的博主寫的Demo:https://github.com/zhu-xb/AES-Cryptography 再次感謝博主 朱小波 https://www.cnblogs.com/xbzhu/ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...