[UWP] 用 AudioGraph 来增强 UWP 的音频处理能力——AudioFrameInputNode

简介: 原文:[UWP] 用 AudioGraph 来增强 UWP 的音频处理能力——AudioFrameInputNode上一篇心得记录中提到了 AudioGraph, 描述了一下 什么是 AudioGraph 以及其中涉及到的各种类型的 节点(Node)。
原文: [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 的原始波形图,

波形图

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

记得耳机别戴反!

目录
相关文章
|
7月前
|
Web App开发 前端开发 JavaScript
SAP UI5 应用的屏幕尺寸检测工作原理深入剖析试读版
SAP UI5 应用的屏幕尺寸检测工作原理深入剖析试读版
34 0
|
XML Java Android开发
移动应用程序设计基础——安卓动画与视音频播放器的实现
《移动应用程序设计基础》实验6 安卓动画与视音频播放器的实现 通过本实验,使得学生掌握导航的制作基本方法,掌握安卓动画和多媒体播放器的制作。 【实验内容】 1、 实现底部导航功能,包括Tween动画、Frame动画、音频播放、视频播放四个按键。 2、 实现动画功能,其中Tween动画可在界面选择四种类型的动画效果。 3、 实现音频播放。 4、 实现视频播放。 ...
194 0
移动应用程序设计基础——安卓动画与视音频播放器的实现
|
网络协议 Ubuntu Linux
基于C++(QT框架)设计的网络摄像头项目(支持跨平台运行)
基于C++(QT框架)设计的网络摄像头项目(支持跨平台运行)
867 0
基于C++(QT框架)设计的网络摄像头项目(支持跨平台运行)
|
搜索推荐 Linux C#
Unity基础
Unity是什么,Unity是一个游戏开发引擎,他功能强大,学习简单,炉石传说,王者荣耀等游戏就是利用Unity引擎开发出来的
259 0
Unity基础
|
图形学 UED
Unity2018新功能探索|图形渲染、下一代Runtime、众多美工工具!
关于Unity2018Unity2018增强了Unity的核心技术, 让创作者能够充分发挥自身才智,进行更有效协作。探索Unity广告如何获取新的用户利用Unity Ads全球广告网络推广游戏,坐拥广大新玩家访问高级游戏娱乐内容10亿以上的独立设备玩家触手可及在正确场合适时获取目标玩家,即玩家参与度最高时创作者的工作空间Unity Editor是艺术家、设计师、开发者和其他职业人员的创意中心,有2D 、3D场景设计工具, 故事和电影, 灯光, 音响系统,精灵管理工具,还有粒子特效和功能强大的dopesheet 动画系统。
1893 0
|
C# Windows
支持 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 应用开发
原文:支持 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 应用开发 版权声明:本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
1340 0
|
编解码 UED
UWP开发入门(六)——对多设备不同分辨率显示效果的讨论
原文:UWP开发入门(六)——对多设备不同分辨率显示效果的讨论   本篇不涉及具体代码,而是把实际开发UWP APP的过程中,遇到的不同设备,不同分辨率显示效果差异的问题进行讨论。希望能够抛砖引玉,和各位擦出一些火花。
1225 0
|
监控 图形学 容器
使用Unity做类的增强
我们已经实现了用户注册功能,现在想增加日志记录功能。具体来讲就是在用户注册前后,分别输出一条日志。
1475 0