上一篇心得记录中提到了 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 的原始波形图,

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

记得耳机别戴反!

[UWP] 用 AudioGraph 来增强 UWP 的音频处理能力——AudioFrameInputNode的更多相关文章

  1. [UWP] 用 AudioGraph 来增强 UWP 的音频处理能力

    Audio Graph AudioGraph 是 Windows.Media.Audio 命名空间下提供的音频处理接口之一. 可以通过 AudioGraph 的静态方法 CreateAsync 来实例 ...

  2. [UWP小白日记-11]在UWP中使用Entity Framework Core(Entity Framework 7)操作SQLite数据库(一)

    前言 本文中,您将创建一个通用应用程序(UWP),使用Entity Framework Core(Entity Framework 7)框架在SQLite数据库上执行基本的数据访问. 准备: Enti ...

  3. [UWP小白日记-15]在UWP手机端实时限制Textbox的输入

    说实话重来没想到验证输入是如此的苦逼的一件事情.     网上好多验证都是在输入完成后再验证,我的想法是在输入的时候就限制输入,这样我就不用再写代码来验证了 应为是手机端,所以不用判断其他非法字符,直 ...

  4. UWP: 通过命令行启动 UWP 应用

    最近在开发应用的过程中,我遇到了如标题所述的需求,其实主要是为了能够快捷启动应用,正像我们可以在"运行"对话框中可以输入一些可执行程序的名称后,就能够直接启动它:这样做,可以增加 ...

  5. UWP Control Toolkit Collections 求UWP工作

    1. it is like wechat wait-sliderdeleteitem in iOS 看起来比较像微信删掉项 now support listview and gridview in C ...

  6. UWP学习目录整理

    UWP学习目录整理 0x00 可以忽略的废话 10月6号靠着半听半猜和文字直播的补充看完了微软的秋季新品发布会,信仰充值成功,对UWP的开发十分感兴趣,打算后面找时间学习一下.谁想到学习的欲望越来越强 ...

  7. 【广州.NET社区推荐】【译】Visual Studio 2019 中 WPF & UWP 的 XAML 开发工具新特性

    原文 | Dmitry 翻译 | 郑子铭 自Visual Studio 2019推出以来,我们为使用WPF或UWP桌面应用程序的XAML开发人员发布了许多新功能.在本周的 Visual Studio ...

  8. 【译】Visual Studio 2019 中 WPF & UWP 的 XAML 开发工具新特性

    原文 | Dmitry 翻译 | 郑子铭 自Visual Studio 2019推出以来,我们为使用WPF或UWP桌面应用程序的XAML开发人员发布了许多新功能.在本周的 Visual Studio ...

  9. xamarin android,UWP 网络类型和IP地址

    App开发经常要判断网络连通情况,并判断网络类型,获取网络IP.xamarin中可以使用Dependencies提供各平台下的方法,现把各平台代码记录如下: using System; using S ...

随机推荐

  1. spring 之 property-placeholder 分析2

    其实我们可以完全不使用  context:property-placeholder  ,而是使用 PropertySourcesPlaceholderConfigurer : <bean cla ...

  2. ---dd-wrt memo

    http://blog.csdn.net/fyh2003/article/details/44458657http://blog.csdn.net/u010189241/article/details ...

  3. 使用wireshark以及filddler配合抓去手机端的TCP以及HTTP请求

    在测试手机客户端时,有时候需要查看网络请求状况.使用在IDE中查看log的方式,能够解决问题,但是会比较复杂.wireshark不能够做代理,而fiddler主要是抓HTTP请求,没有wireshar ...

  4. mobilenet之Depthwise +Pointwise

    我们知道,mobilenet是适用于移动端的深度学习网络,主要优点是参数少.模型小.准确率相比一些传统卷积损失少等特点. mobileNet之所以这么ok,是因为引入了Depthwise +Point ...

  5. R语言-优化作图

    par()函数:用来设置画图参数的函数par()的作用直到画板被关闭为止 1.设置背景颜色 #设置背景颜色 > par(bg="gray") #设置画板背景色 > pl ...

  6. Linux jdk安装

    Linux上一般会安装Open JDK,关于OpenJDK和JDK的区别:http://www.cnblogs.com/sxdcgaq8080/p/7487369.html 下面开始安装步骤: --- ...

  7. 专题 查找与排序的Java代码实现(一)

    专题 查找与排序的Java代码实现(一) 查找(Searching) 线性查找(linear search) 属于无序查找算法,适合于存储结构为顺序存储或链接存储的线性表. 基本思想:从数据结构线形表 ...

  8. 深度学习项目——基于卷积神经网络(CNN)的人脸在线识别系统

    基于卷积神经网络(CNN)的人脸在线识别系统 本设计研究人脸识别技术,基于卷积神经网络构建了一套人脸在线检测识别系统,系统将由以下几个部分构成: 制作人脸数据集.CNN神经网络模型训练.人脸检测.人脸 ...

  9. HDU-1160.FatMouse'sSpeed.(LIS变形 + 路径打印)

    本题大意:给定一定数量的数对,每个数保存着一只老鼠的质量和速度,让你求出一个最长序列,这个序列按照质量严格递增,速度严格递减排列,让你输出这个序列的最长长度,并且输出组成这个最长长度的序列的对应的老鼠 ...

  10. lenet-5

    https://blog.csdn.net/happyorg/article/details/78274066 深度学习 CNN卷积神经网络 LeNet-5详解 2017年10月18日 16:04:3 ...