[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天前
|
监控 Shell API
鸿蒙next版开发:使用HiChecker检测问题(ArkTS)
在HarmonyOS 5.0中,HiChecker是一个强大的工具,帮助开发者检测应用中的潜在问题,如耗时调用和资源泄露。本文详细介绍了如何在ArkTS中使用HiChecker,包括添加检测规则、触发检测和日志输出等步骤,并提供了示例代码。通过合理使用HiChecker,开发者可以提高应用的稳定性和性能。
25 6
|
2月前
|
IDE 开发工具 Android开发
探索iOS与安卓应用开发的核心差异
在移动开发的广阔天地中,iOS和安卓这两大平台各领风骚。本文将深入浅出地剖析这两个系统在应用开发上的主要区别,从编程语言到开发环境,再到用户界面设计,我们将一探究竟。无论你是开发者还是对技术充满好奇的读者,这篇文章都将为你打开新世界的大门,带你领略不同平台下的开发魅力。
33 0
|
3月前
|
开发者 C# UED
WPF与多媒体:解锁音频视频播放新姿势——从界面设计到代码实践,全方位教你如何在WPF应用中集成流畅的多媒体功能
【8月更文挑战第31天】本文以随笔形式介绍了如何在WPF应用中集成音频和视频播放功能。通过使用MediaElement控件,开发者能轻松创建多媒体应用程序。文章详细展示了从创建WPF项目到设计UI及实现媒体控制逻辑的过程,并提供了完整的示例代码。此外,还介绍了如何添加进度条等额外功能以增强用户体验。希望本文能为WPF开发者提供实用的技术指导与灵感。
141 0
|
3月前
|
容器 iOS开发 Linux
震惊!Uno Platform 响应式 UI 构建秘籍大公开!从布局容器到自适应设计,带你轻松打造跨平台完美界面
【8月更文挑战第31天】Uno Platform 是一款强大的跨平台应用开发框架,支持 Web、桌面(Windows、macOS、Linux)及移动(iOS、Android)等平台,仅需单一代码库。本文分享了四个构建响应式用户界面的最佳实践:利用布局容器(如 Grid)适配不同屏幕尺寸;采用自适应布局调整 UI;使用媒体查询定制样式;遵循响应式设计原则确保 UI 元素自适应调整。通过这些方法,开发者可以为用户提供一致且优秀的多设备体验。
140 0
|
XML Java Android开发
移动应用程序设计基础——安卓动画与视音频播放器的实现
《移动应用程序设计基础》实验6 安卓动画与视音频播放器的实现 通过本实验,使得学生掌握导航的制作基本方法,掌握安卓动画和多媒体播放器的制作。 【实验内容】 1、 实现底部导航功能,包括Tween动画、Frame动画、音频播放、视频播放四个按键。 2、 实现动画功能,其中Tween动画可在界面选择四种类型的动画效果。 3、 实现音频播放。 4、 实现视频播放。 ...
248 0
移动应用程序设计基础——安卓动画与视音频播放器的实现
|
存储 缓存 数据可视化
iOS 开发全能工具箱:技术篇
iOS 开发工具箱是一系列的非常好用的 iOS 开发工具的集合,里面包括了网站,在桌面/移动设备上的应用,还有些后端(Back-end)的服务。我会尽力把这些工具分好类,如果有新添加近来的工具,我会放在 NEW 类别下。
320 0
|
图形学 UED
Unity2018新功能探索|图形渲染、下一代Runtime、众多美工工具!
关于Unity2018Unity2018增强了Unity的核心技术, 让创作者能够充分发挥自身才智,进行更有效协作。探索Unity广告如何获取新的用户利用Unity Ads全球广告网络推广游戏,坐拥广大新玩家访问高级游戏娱乐内容10亿以上的独立设备玩家触手可及在正确场合适时获取目标玩家,即玩家参与度最高时创作者的工作空间Unity Editor是艺术家、设计师、开发者和其他职业人员的创意中心,有2D 、3D场景设计工具, 故事和电影, 灯光, 音响系统,精灵管理工具,还有粒子特效和功能强大的dopesheet 动画系统。
1935 0
|
C# Windows
支持 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 应用开发
原文:支持 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 应用开发 版权声明:本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
1438 0
|
C# 数据格式 XML
c#开发移动APP-Xamarin入门扩展
原文:c#开发移动APP-Xamarin入门扩展   这节主要演示了如何通过添加第二个屏幕来跟踪应用程序的call历史来扩展Phoneword应用程序。最终如下:       按如下步骤扩展Phoneword   在Phoneword项目右键新建Content Page,命名为CallHistoryPage    修改后CallHistoryPage.
1114 0
|
C# UED
c#开发移动APP-Xamarin入门扩展剖析
原文:c#开发移动APP-Xamarin入门扩展剖析   上节将Phoneword应用程序扩展到包含第二个屏幕,该屏幕可以跟踪应用程序的拨打历史 Navigation   Xamarin.Form提供了一个内置的导航模型,用于管理一堆页面的导航和用户体验,这个模型实现了Page对象的后进先出(LIFO)堆栈,要从一个页面移动到另一个页面,应用程序将把一个新页面推到这个堆栈上,要返回到前一个页面,应用程序将从堆栈中弹出当前页面。
1019 0