[UWP] 用 AudioGraph 来增强 UWP 的音频处理能力——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 的原始波形图,

波形图

所以听的时候听到的顺序应该是:先右声道,再立体声,最后左声道,和波形图里相反。

记得耳机别戴反!

posted @ 2018-12-09 19:17  DemoApp  阅读(1034)  评论(2编辑  收藏  举报