上一篇心得記錄中提到了 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 對象,避免記憶體泄漏。
踩到的坑
大小端問題
借用百度百科內容:
大端模式,是指數據的高位元組保存在記憶體的低地址中,而數據的低位元組保存在記憶體的高地址中,這樣的存儲模式有點兒類似於把數據當作字元串順序處理:地址由小向大增加,而數據從高位往低位放;這和我們的閱讀習慣一致。
小端模式,是指數據的高位元組保存在記憶體的高地址中,而數據的低位元組保存在記憶體的低地址中,這種存儲模式將地址的高低和數據位權有效地結合起來,高地址部分權值高,低地址部分權值低。
二進位內容在記憶體裡面存儲,是存在大小端問題的,對於PCM格式,也存在大小端問題,所以如果對數據想進一步處理,大小端的問題一定要註意。
在C#中調用 native 內容時,我的機器上實測時小端模式。
也可以通過如下 unsafe 代碼來判斷:
int temp = 0x01; int* pTempInt = &temp; byte* pTempByte = (byte*)pTempInt; if(0x01== *pTempByte) { //小端 } else { //大端 }
float 在記憶體中如何排布?
對於 int 類型,將其轉換為二進位後,求補碼,即是它在記憶體中的實際值,但是對於浮點型,就有一套自己的計算方法了,可以參考如下博客(大學電腦課本里的內容,忘得差不多了)
附件
Github AudioFrameInputNode Demo
附上我測試用的 PCM 數據,44100,32位 浮點型,小端模式
聽說最近杭州下雪了,這歌現在很火!
下圖是該 PCM 的原始波形圖,
所以聽的時候聽到的順序應該是:先右聲道,再立體聲,最後左聲道,和波形圖裡相反。
記得耳機別戴反!