65.9K
CodeProject 正在变化。 阅读更多。
Home

极致实时音乐可视化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (19投票s)

2015年3月23日

CPOL

6分钟阅读

viewsIcon

41620

downloadIcon

4076

多种技术集成在一个应用程序中,为您带来极致的放松时刻

引言

本文档面向对数字信号处理(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 的效果演示
© . All rights reserved.