极致实时音乐可视化






4.74/5 (19投票s)
多种技术集成在一个应用程序中,为您带来极致的放松时刻
- 下载 Visualizer_EXE.zip - 336.9 KB,请按照“运行前步骤”部分中的说明操作
- 下载源代码 - 347.3 KB
引言
本文档面向对数字信号处理(DSP - FFT)和图形可视化相结合感兴趣的读者。生活中每个人都听过音乐,但如果他们想想象音乐会是什么样子?或者想分析某一时刻有多少频率存在?它有多详细?
现在,这是一个可以回答这些问题的工具/代码!
视频演示: https://www.youtube.com/watch?v=dUoeREaTroU。
注意
该工具最强大的地方在于频率分辨率,它相当精细:每个频段高达 11Hz。
背景
- 面向对象编程(绝对)- 构建类等。
- Winform 基本组件,基本事件处理(绘制)
- .NET GDI
- 声音规格(采样率、通道、原始数据格式)
- 使用外部库
- 数学知识(复数)
- 链表,一个通过对象构建和链接的列表(可以看作是相互链接的指针)
- DFT - 离散傅里叶变换,将波形信号转换为频域信号。更多细节请参阅维基百科。如果您想在代码中清楚地了解它,源代码中也包含了(但已注释掉)。
运行前步骤
推荐测试歌曲: https://www.youtube.com/watch?v=GXHouoD4KVM
确保您的声卡支持立体声混音并已正确配置
您可以称之为“硬件敏感”,如果您使用的是扬声器,我建议将其设置为 50(或其他值,只要频谱高度合适即可)。
关注点
总体兴趣:可视化是基于声卡信号捕获运行的。这意味着它不依赖于任何媒体文件。不像 Window Media 或 Winamp,您需要打开一个文件。想象一下,您正在以传统方式听 YouTube 上的歌曲,为了看到视觉效果,您会下载它并用软件播放?哦,不!那是愚蠢的方式,现在请忘记那种方式。只需运行该工具并享受!
1. 捕获声音信号
这是我们需要做的第一件事,通过使用外部库(NAudio),我们可以轻松捕获声音信号。录制格式为:44100 样本/秒,单声道,每次捕获事件 10ms(441 样本)。使用以下代码
using NAudio.Wave;
using NAudio;
WaveIn waveInStream;
private void Form1_Load(object sender, EventArgs e)
{
//
//
// Some init stuff, will talk later
//
//Force high priority if needed
//System.Diagnostics.Process.GetCurrentProcess().PriorityClass =
//System.Diagnostics.ProcessPriorityClass.High;
// Main
InitSoundCapture();
}
在加载窗体事件中,暂时忽略一些 init
设置,只关注声音捕获 init
。我将声音捕获调用封装到了一个名为 InitSoundCapture
的函数中,如下所示
private void InitSoundCapture()
{
waveInStream = new WaveIn();
waveInStream.NumberOfBuffers = 2;
waveInStream.BufferMilliseconds = 10;
waveInStream.WaveFormat = new WaveFormat(44100, 1);
waveInStream.DataAvailable += new EventHandler<WaveInEventArgs>(waveInStream_DataAvailable);
waveInStream.StartRecording();
}
private void waveInStream_DataAvailable(object sender, WaveInEventArgs e)
{
if (sourceData == null)
sourceData = new double[e.BytesRecorded / 2];
for (int i = 0; i < e.BytesRecorded; i += 2)
{
short sampleL = (short)((e.Buffer[i + 1] << 8) | e.Buffer[i + 0]);
// short sampleR = (short)((e.Buffer[i + 1+2] << 8) | e.Buffer[i + 2]);
double sample32 = (sampleL) / 32722d;
sourceData[i / 2] = sample32;// (double)(e.Buffer[i]) / 255;
}
AppendData(sourceData);
}
给它一些参数,录制的信号将被传递到
waveInStream_DataAvailable
从那里,我们将继续解析到波形数据。解析前的数据是原始 PCM 数据。您应该参考 WAV 数据结构来理解为什么代码是这样编写的。
2. 使用链表存储录制的数据
为什么必须采用这种技术?
解释如下:在录制完数据后,我必须将其转换为频域。如果我只转换从捕获函数接收到的信号,数据长度太短 => 信息不足以代表。如果我增加捕获间隔到更长?数据长度可以,但这种情况下,信号太离散了,您不希望可视化效果以 10 帧/秒的速度渲染,对吧?
=> 出现了 2 个问题,所以现在我有一个解决方案来解决它们:使用链表(我自己定义的),应用程序将不断将录制的数据收集到一个链接/动态列表中。当信号长度足够进行进一步处理时,它就会进行处理。与此同时,任何进入 LIST
的信号,另一个信号片段(最旧的信号)将被弹出。请看下图
阶段 1:假设信号是:ABCDEF
阶段 2:新信号是“GH
”,让它加入主流:ABCDEFGH
,现在它比阈值(预先定义)长,最旧的信号AB
将被切掉。现在流是:CDEFGH
=> stream
始终保持足够的长度,并且数据以短间隔录制。
class Node
{
public Node(ComplexNumber value)
{
Value = value;
}
/// <summary>
/// Linked to the node after
/// </summary>
public Node NextNode
{
get;
set;
}
/// <summary>
/// Linked to the node before
/// </summary>
public Node PrevNode
{
get;
set;
}
/// <summary>
/// To mark end point
/// </summary>
public bool isEndPoint
{
get;
set;
}
/// <summary>
/// To mark start point
/// </summary>
public bool isStartPoint
{
get;
set;
}
/// <summary>
/// The Node's value
/// </summary>
public ComplexNumber Value
{
get;
set;
}
}
简要介绍复数。为什么必须是复数?因为 FFT 变换算法是针对复数设计的。公式是复数中一个非常冗长和复杂的事物。如果我们尝试用实数表示,可能需要很多页。FFT 实现代码也用于复数,我必须在应用程序中使用复数作为主要值类型。
3. 将录制的数据转换为频域数据
这是一个非常复杂的过程,我只是按照指南编写了代码,我仍然不完全理解它 100%,但如果您愿意,这里有一个阅读链接:http://www.ti.com/ww/cn/uprogram/share/ppt/c6000/Chapter19.ppt。
4. 压缩转换后的数据
在转换了长输入数据后,我们也得到了长输出。通常在屏幕上显示转换后的数据不是一个好的风格。此应用程序中的长度为 2048。如果我们为每个元素使用 1 像素,即使是全高清屏幕也不够,渲染的可视化也难以观察。您知道人耳听到的频谱不是线性的,我们在低频范围(40Hz -> 1000Hz)听得很清楚,但在高频范围(> 10Khz)听得不太清楚。所以我使用了简单的 SUM 计算,在不同范围上有所不同
int[] chunk_freq = { 200, 1000, 2000, 4000, 8000, 16000,22000 };
int[] chunk_freq_jump = { 1, 2, 4, 8, 12,20,100 };
代码含义:在 < 200Hz 时,它不做任何事情。从 200 到 < 1000:将 2 个信号列合并成 1 个信号列,在 1000 到 2000 时,合并 4 列,依此类推。
5. .NET GDI 实现惊艳效果
5.1 ColorMatrix 和 Blitter 反馈
它用于变换绘制屏幕的颜色和重绘屏幕,以实现屏幕叠加效果。
让我们来弄清楚
首先,字母“A
”绘制到屏幕上,然后整个屏幕备份到一个临时缓冲区。下次将备份的缓冲区重新绘制到屏幕上时,另一个字母“B
”绘制到屏幕上。所有屏幕再次备份,依此类推,新屏幕连续叠加在旧屏幕上。
ColorMatrix colormatrix = new ColorMatrix(new float[][]
{
new float[]{1, 0, 0, 0, 0},
new float[]{0, 1, 0, 0, 0},
new float[]{0, 0, 1, 0, 0},
new float[]{0, 0, 0, 1, 0},
// new float[]{0, 0, 0, -0.001f, 1},
new float[]{-0.01f, -0.01f, -0.01f, 0, 0}
// new float[]{0, 0, 0, 0, 1}
});
private void InitBufferAndGraphic()
{
mainBuffer = new Bitmap(panel1.Width, panel1.Height, PixelFormat.Format32bppArgb);
gMainBuffer = Graphics.FromImage(mainBuffer);
gMainBuffer.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
gMainBuffer.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Low;
tempBuffer = new Bitmap(mainBuffer.Width, mainBuffer.Height);
gTempBuffer = Graphics.FromImage(tempBuffer);
imgAttribute = new ImageAttributes();
imgAttribute.SetColorMatrix(colormatrix, ColorMatrixFlag.Default, ColorAdjustType.Default);
gTempBuffer.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
gTempBuffer.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
}
gMainBuffer.DrawImage(tempBuffer, new Rectangle(-2, -1, mainBuffer.Width + 4,
mainBuffer.Height + 3), 0, 0, tempBuffer.Width, tempBuffer.Height, GraphicsUnit.Pixel, imgAttribute);
限制
第一个限制始终是性能。我在以下机器上进行了测试,结果如下
- Core 2 duo P8400, 集成显卡: 延迟 20ms <-> 50 fps
- 笔记本 Core 2 T6400, 集成显卡: 60ms <-> 2x fps => 影响很大
如果我尝试最大化应用程序窗口,性能会更差!即使在 i5 2500K OC 4.2Ghz,使用 GTX 970 也是如此。
结束语
我需要将这个项目重写成 3D 模式(Managed DirectX/OpenGL)来摆脱性能问题。任何对此文章感兴趣的人,请通过下面的评论区提供帮助。
历史
- 0.2: 添加设置屏幕
- 0.1: 使用内置 GDI 的效果演示