预览:

捕捉声卡输出:

实现音频可视化, 第一步就是获得音频采样, 这里我们选择使用计算机正在播放的音频作为采样源进行处理:

NAudio 中, 可以借助 WasapiLoopbackCapture 来进行捕捉:

WasapiLoopbackCapture cap = new WasapiLoopbackCapture();
cap.DataAvailable += (sender, e) => // 录制数据可用时触发此事件, 参数中包含音频数据
{
float[] allSamples = Enumerable // 提取数据中的采样
.Range(0, e.BytesRecorded / 4) // 除以四是因为, 缓冲区内每 4 个字节构成一个浮点数, 一个浮点数是一个采样
.Select(i => BitConverter.ToSingle(e.Buffer, i * 4)) // 转换为 float
.ToArray(); // 转换为数组
// 获取采样后, 在这里进行详细处理
}
cap.StartRecording(); // 开始录制

分离左右通道:

获取完采样后, 我们还需要对采样进行一点小处理, 因为捕获的数据是分通道的, 一般是左右声道:

// 设定我们已经将刚刚的采样保存到了变量 AllSamples 中, 类型为 float[]
int channelCount = cap.WaveFormat.Channels; // WasapiLoopbackCapture 的 WaveFormat 指定了当前声音的波形格式, 其中包含就通道数
float[][] channelSamples = Enumerable
.Range(0, channelCount)
.Select(channel => Enumerable
.Range(0, AllSamples.Length / channelCount)
.Select(i => AllSamples[channel + i * channelCount])
.ToArray())
.ToArray();

取通道平均值

将采样分为一个个通道的采样后, 我们可以将其合并, 取平均值, 以便于绘制:

// 设定我们已经将分开了的采样保存到了变量 ChannelSamples 中, 类型为 float[][]
// 例如通道数为2, 那么左声道的采样为 ChannelSamples[0], 右声道为 ChannelSamples[1]
float[] averageSamples = Enumerable
.Range(0, AllSamples.Length / channelCount)
.Select(index => Enumerable
.Range(0, channelCount)
.Select(channel => ChannelSamples[channel][index])
.Average())
.ToArray();

绘制时域图象:

处理刚刚的采样后, 你可以直接将其作为数据绘制到窗口中, 这即是时域图象, 这里使用最简单的折线绘制.

// 设定 g 为窗口的 Graphics 对象, windowHeight 为窗口的显示区域高度
// 设定通道采样平均值为 AverageSamples, 类型为 float[]
Point[] points = AverageSamples
.Select((v, i) => new Point(i, windowHeight - v))
.ToArray(); // 将数据转换为一个个的坐标点
g.DrawLines(Pens.Black, points); // 连接这些点, 画线

傅里叶变换:

NAudio 中还提供了快速傅里叶变换的方法, 通过傅里叶变换, 可以将时域数据转换为频域数据, 也就是我们所说的频谱

// 我们将对 AverageSamples 进行傅里叶变换, 得到一个复数数组

// 因为对于快速傅里叶变换算法, 需要数据长度为 2 的 n 次方, 这里进行
float log = Math.Ceiling(Math.Log(AverageSamples.Length, 2)); // 取对数并向上取整
int newLen = (int)Math.Pow(2, log); // 计算新长度
float[] filledSamples = new float[newLen];
Array.Copy(AverageSamples, filledSamples, AverageSamples.Length); // 拷贝到新数组
Complex[] complexSrc = filledSamples
.Select(v => new Complex(){ X = v }) // 将采样转换为复数
.ToArray();
FastFourierTransform(false, log, complexSrc); // 进行傅里叶变换 // 变换之后, complexSrc 则已经被处理过, 其中存储了频域信息

分析频域信息:

对于傅里叶变换的频域信息, 需要稍加处理才可以方便的使用, 首先是提取有用的信息:

// NAudio 的傅里叶变换结果中, 似乎不存在直流分量(这使我们的处理更加方便了), 但它也是有共轭什么的(也就是数据左右对称, 只有一半是有用的)
// 仍然使用刚刚的 complexSrc 作为变换结果, 它的类型是 Complex[] Complex[] halfData = complexSrc
.Take(complexSrc.Length / 2)
.ToArray(); // 一半的数据
float[] dftData = halfData
.Select(v => Math.Sqrt(v.X * v.X + v.Y * v.Y)) // 取复数的模
.ToArray(); // 将复数结果转换为我们所需要的频率幅度 // 其实, 到这里你完全可以把这些数据绘制到窗口上, 这已经算是频域图象了, 但是对于音乐可视化来讲, 某些频率的数据我们完全不需要
// 例如 10000Hz 的频率, 我们完全没必要去绘制它, 取 最小频率 ~ 2500Hz 足矣
// 对于变换结果, 每两个数据之间所差的频率计算公式为 采样率/采样数, 那么我们要取的个数也可以由 2500 / (采样率 / 采样数) 来得出
int count = 2500 / (cap.WaveFormat.SampleRate / filledSamples.Length);
float[] finalData = dftData.Take(count).ToArray();

绘制频域图象:

得到上面分析后的 finalData 后, 我们就可以直接绘制出来了, 这次使用柔和的曲线绘制

// 设定 g 为窗口的 Graphics 对象, height 为窗口高度
PointF[] points = finalData
.Select((v, i) => new PointF(i, height - v))
.ToArray();
g.DrawCurve(Pens.Purple, points); // Graphics 可以直接绘制曲线

更优的绘制:

上面的时域和频域图象, 我们都是一股脑的将数据的索引作为 X 坐标, 窗口高度减去数据值作为 Y 坐标, 有两个突出的问题:

  1. 数据可能无法填满窗口的宽度或者超出窗口的宽度范围
  2. 数据太大时, 也会导致绘制的线条超出窗口高度

第一个问题好解决, 直接使索引所占数据长度的百分比恰好等于 X 坐标相对于窗口宽度的百分比即可:

\[x = index \div dataLength * windowWidth
\]

对于第二个问题, 有两个解决方案, 一是直接为数据加权重, 例如统一乘 0.5, 使数据减小一节, 二就是套一个函数, 例如 log 函数, 毕竟 log 函数在较高自变量的情况下, 因变量的变化趋势越来越小, 我们只需要对这个 log 函数进行稍加处理, 就可以直接应用到数据变换数据上, 使其不超出窗口绘图区域

另外, 我们也可以平滑频谱显示(指动画变换), 它的原理大概是这样:

  1. 例如这次进行傅里叶变换的结果是: {0, 100, 50},

  2. 下一次傅里叶变换的结果是: {100, 0, 0},

  3. 可以得出, 增量为: {100, -100, -50},

    在更新变换结果时, 我们不再直接将新的结果替换旧的结果, 而是在旧的结果的基础上, 加上增量×权重

  4. 例如权重是 0.5 时, 那么实际增量是: {50, -50, -25},

  5. 那么实际新的值是: {50, 50, 25},

  6. 如果下一次变换的结果还是 {100, 0, 0}, 那我们再次从 {50, 50, 25} 向新值逼近, 权重仍然是 0.5, 那么实际增量是: {25, -25, -12.5},

注意到了吗? 这次的增量是上次增量的一半, 这正好是一个减速运动, 而且新值与旧值的差越大, 变化的就越快, 而它们会不断重合, 因而速度不断变慢, 形成减速运动的频谱图.

更多内容:

更多关于 NAudio 的使用, 可以看这篇文章: [C#] NAudio 的各种常见使用方式 播放 录制 转码 音频可视化

项目已开源:

关于本文章涉及的大部分内容, 均在 github.com/SlimeNull/AudioTest 仓库中的 Null.AudioVisualizer 项目中有写. (注释妥当了)


其实音频可视化我老早就想做了, 但是本人数学不是非常的好, 不过最后总算是坚持下来了, 弄出来了啊, 心情老激动了

求个赞, 求个评论~

[C#] 使用 NAudio 实现音频可视化的更多相关文章

  1. [C#] NAudio 库的各种常用使用方式: 播放 录制 转码 音频可视化

    概述 在 NAudio 中, 常用类型有 WaveIn, WaveOut, WaveStream, WaveFileWriter, WaveFileReader 以及接口: IWaveProvider ...

  2. HTML5音频可视化频谱跳动代码

    今天学习到用canvas来写  HTML5音频可视化频谱跳动代码  将代码在此做一总结: <!DOCTYPE html> <html lang="en"> ...

  3. 用webAudio和canvas实现音频可视化

    前两天遇到了要显示音频波形图的需求,因为时间紧,就直接用了wavesufer.js,这两天有空,就研究了一下怎么用webAudio实现音频的可视化. 大致流程是对音源进行解析,解析得到的数据是个频谱数 ...

  4. H5录音音频可视化-实时波形频谱绘制、频率直方图

    这段时间给GitHub Recorder开源库添加了两个新的音频可视化功能,比以前单一的动态波形显示丰富了好多(下图后两行是不是比第一行看起来丰满些):趁热打铁写了一个音频可视化相关扩展测试代码,下面 ...

  5. vue解决音频可视化播放,使用wavesurfer.js

    vue解决音频可视化播放,使用wavesurfer.js 上效果:   1.安装wavesurfer  npm install wavesurfer.js 2.在页面导入 import WaveSur ...

  6. Web Audio API 实现音频可视化

    声明:本文为原创文章,如需转载,请注明来源WAxes,谢谢! 一转眼就已经有三个月没写博客了,毕业季事情确实多,现在也终于完全毕业了,博客还是不能落下.偶尔还是要写一下. 玩HTML5的Audio A ...

  7. Web应用实例:音频可视化

    准备 语言:TypeScript 工具:Visual Studio Code 演示:Audio Visualiazer 小明告诉我,他希望打开一个网页,立即听到他喜欢的音乐,如果有视觉特效就更棒了. ...

  8. python 音频可视化

    代码整理好放在 github 上了: https://github.com/darkchii/visualize bilibili 演示视频:https://www.bilibili.com/vide ...

  9. win api 音频可视化

    暂时记录,改天有时间再完善...其实写好好久了,但以前的代码丢了,重新写一遍.. 原理和 python 的一样,获取输入设备,然后把数据读取到 buffer 中,在绘制出来. 这里要注意两点: 1. ...

随机推荐

  1. Android实现三角形气泡效果方式汇总

    在开发过程中,我们可能会经常遇到这样的需求样式: 这张图是截取京东消息通知的弹出框,我们可以看到右上方有个三角形的气泡效果,这只是其中一种,三角形的方向还可以是上.下.左.右. 通过截图可以发现,气泡 ...

  2. MediaCodec编码结合FFmpeg封装流

    在Android平台上合成视频一般使用MediaCodec进行硬编码,使用MediaMuxer进行封装,但是因为MediaMuxer在某些机型上合成的视频在其他手机上播放会出现问题,而且只支持一个音频 ...

  3. windbg安装pykd记录

    https://githomelab.ru/pykd/pykd 1.安装python   (坑,分x86和x64,对应windbg版本) 2.安装pykd:'pip install pykd' 3.安 ...

  4. ASPOSE.Cells & ASPOSE.Words 操纵Excel和Word文档的 .NET Core 实例

    Aspose.Total是Aspose公司旗下的最全的一套office文档管理方案,它提供的原生API可以对Word.Excel.PDF.Powerpoint.Outlook.CAD.图片.3D.ZI ...

  5. 关于在forEach中使用await的问题

    先说需求,根据数组中的ID值,对每个ID发送请求,获取数据进行操作. 首先肯定考虑用forEach 或者 map对数组进行遍历,然后根据值进行操作,但是请求是个异步操作,forEach又是一个同步操作 ...

  6. C#修改AD账号及密码

    在使用AD域环境搭建的账号系统修改密码的时候比较麻烦一般需要管理员在域环境去进行对用户的密码进行修改. 以下就是用来查询和修改AD域密码的方法. 1 /// <summary> 2 /// ...

  7. 软工案例分析之OJ

    项目 内容 这个作业属于哪个课程 2021春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 案例分析作业要求 我在这个课程的目标是 和我的团队开发一个真正的软件,一起提升开发与合作的能力 这 ...

  8. Leedcode算法专题训练(哈希表)

    Java 中的 HashSet 用于存储一个集合,可以查找元素是否在集合中.如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在.例如对于只有小写字符的元素,就可以用一个长度为 2 ...

  9. 现代 CLI 和 GUI 方案指南

    原文链接:http://axuebin.com/articles/fe-solution/cli/desc.html,转载请联系 写在前面 提到初始化项目,那就必须提到脚手架这个名词,那为什么这篇文章 ...

  10. 2020.1 IDEA 激活

    1 下载安装 平台windows,官网这里. 选位置之后: 选项分别是创建32/64位的快捷方式,把运行目录添加到PATH环境变量,添加右键菜单"打开文件夹作为工程",添加java ...