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

Windows Media Player 的交互式 3D 频谱分析器可视化

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2009年5月17日

CPOL

12分钟阅读

viewsIcon

171072

downloadIcon

6623

使用 DirectX 9 和一些轻量级 GPGPU 实现的 Windows Media Player 交互式 3D 频谱分析器。

City Lights Color Scheme

引言

当您收听喜欢的歌曲时,一点点视觉娱乐会让您更享受这首歌曲。作为一名技术爱好者和音频/音乐爱好者,我喜欢看到一切的技术细节,甚至包括我的音乐。这款交互式 3D 频谱分析器不仅提供了具有视觉吸引力的音频可视化效果,还显示了一些声音随时间变化的细节,帮助我们更深入地了解音频的工作原理。

本项目使用 DirectX 9.0c 进行 3D 渲染,并与 Windows Media Player 集成。仅在 Vista Home Premium 上进行了测试,但只要您安装了 Windows Media Player 11 和 DirectX 9.0c 可再发行组件,它应该也能在 XP 上运行。

入门

如果您只想安装二进制文件,则需要确保您的系统已安装 DirectX 9.0c 可再发行组件和 Windows Media Player 11。附带的安装程序应该会为您安装所有其他组件。

要构建项目,您需要 DirectX 9.0c SDK 和 Windows Platform SDK 版本 6.1。您的显卡应支持 DirectX 着色器模型 3 (vs_3_0 和 ps_3_0)。DirectX include 文件和库应位于其适当的搜索路径中。我已经将 Windows SDK 的路径配置在项目文件中,因此如果您将 SDK 安装在默认位置,则无需更改这些路径。在 Windows Vista 上构建时,Visual Studio 将会尝试注册 WM3DSpectrum.dll,但会失败。您需要从管理员权限的命令提示符运行“regsvr32 WM3DSpectrum.dll”才能在 Vista 上注册 WM3DSpectrum.dll

如果您想从头开始创建自己的可视化效果,可以使用 Windows Platform SDK 中的 WMP SDK。有关入门方法的详细概述,您可以点击此处。请仔细按照说明操作,因为细节决定成败。

为什么选择 DirectX 9.0c

虽然 DirectX 10 确实让一些事情变得更简单,但 DirectX 10 本质上是 DirectX 9 加上一些重组。DirectX 10 包含 Microsoft 对 Windows Vista 图形管线的重组。DirectX 10 还增加了 DXGI 框架。DXGI 基本上有助于在不将 DirectX 设备直接与窗口绑定的情况下使用图形处理器。DirectX 10 SDK 中包含的 GPUSpectogram 项目展示了一个无窗口 DirectX 10 设备的示例。GPUSpectogram 在不将渲染的位图与窗口句柄关联的情况下创建位图频谱图。Windows XP 使用一个更简单的图形管线,该管线不支持 DirectX 10 和 DXGI 的功能。难怪 DirectX 10 只能与 Windows Vista 配合使用!

使用 DirectX 10 的另一个显著好处是,当用户执行更改屏幕分辨率等操作时,您无需删除 GPU 中存在的对象。在 DirectX 9 中,当用户执行某些操作(如更改屏幕分辨率)时,您基本上会“丢失”您的 DirectX 设备对象,因为硬件配置已更改。在 DirectX 9 中,要恢复设备丢失后的状态,您首先需要调用设备对象上的 TestCooperativeLevel()。如果返回值是 D3DERR_DEVICENOTRESET,您需要释放默认对象池中的所有对象(存在于 GPU 中的对象),对字体和精灵等对象调用 OnLostDevice(),对设备调用 Reset(),在默认池中重新创建所有需要的对象,然后对字体和精灵等对象调用 OnResetDevice()。我知道这听起来很复杂,但实际上并不难。本项目提供了一个如何处理 DirectX 9 中此问题的示例。DirectX 10 在您“丢失”设备时不需要您执行任何特殊操作。

那么,为什么还要选择 DirectX 9 呢?好吧,在本文最初撰写之时(2009 年 5 月 7 日),hitslink.com 过去几个月的报告显示 Windows XP 仍占有约 62% 的操作系统市场份额。Vista 只有约 24%。这些统计数据来自此处。Vista 和 XP 目前在所有操作系统中合计占有 86% 的市场份额。(62% + 24% = 86%)您可以看出,XP 在 Vista 和 XP 的合并市场份额中仍占 74%。(62% / 86% * 100 = 74%)尽管 Vista 是一个出色的操作系统,但为 DirectX 10 构建应用程序将使应用程序只能供有限数量的用户(仅限 Vista 用户)使用。

有趣的是,在抛出所有这些统计数据后,我甚至没有一台 XP 机器来测试它。我已经切换到 Vista 了。:) 您关于此功能在 Windows Media Player 11 和 Windows XP 上如何工作的反馈将很有用。

一张图片胜过千言万语

我将不深入探讨采样理论和其他随机 DSP 主题,但我必须指出,DSP 编程中最困难的方面之一是,有时您无法轻松地看到声音是如何工作的。要开发新的无损音频压缩格式或酷炫的音频效果工具,您必须非常熟悉音频的工作原理,以至于您可以“看到”声音。

音频流由独立频率组成,当它们组合在一起时,会产生一个可能和谐的单一声音。如果您看过电影“鼓乐乐队”(Drum Line),您可能熟悉乐队的口号:“一支乐队,一个声音!”那么,各种声音是如何融合成一个的呢?通过可以由某些物理学原理建模的紧密联系。这个网站列出了音频中音符的频率。您会注意到音符重复 C、D、E、F、G、A、B、C、D、E、F、G、A、B,依此类推,其中 C 被视为起点,每个连续的 C 的频率是前一个 C 的两倍。例如,C4(中央 C)的频率是 C3 的两倍。C4 是 261.63 Hz,C3 是 130.81 Hz。您会在 3D 频谱分析器中看到,通常,许多人耳可辨别的独立声音都集中在较低的频率中。这是由于音频物理学的特性。

Natural Progression of the Frequencies of Notes

代码图片

此可视化的设计并不特别出色,但它是面向对象的。下图对设计提供了一些见解。

Architectural Overview

Windows Media Player 支持两种可视化模式:窗口模式和非窗口模式。我假设非窗口模式适用于 Media Player 作为 ActiveX 控件托管时。我还没有深入研究非窗口模式,因为它似乎与本项目无关。

为了透明地支持窗口与非窗口功能,有一个 IRenderer 接口,两种模式都可以使用。有一个 CWindowedRenderer 类和一个 CNonWindowedRenerer 类,它们分别使用 IRenderer 接口在窗口模式和非窗口模式下进行渲染。

大多数核心渲染信息存储在 RenderContext 结构中。RenderContext 结构的唯一实例存储在根 WM3DSpectrum COM 对象中。该 RenderContext 结构的一个指针贯穿渲染层次结构,传递给所有需要使用它的对象。

如果您想在场景中添加一个额外的 3D 对象,您可以在一个类中实现 IRenderable 接口,并将该类添加到可渲染对象向量之一中。您应该在 WM3DSpectrum 构造函数中的 renderables 向量中添加新类。您所有可渲染对象将按照它们在向量中存在的顺序进行渲染。

该项目目前支持 8 种不同的可视化效果。有两种配色方案,我称之为“玫瑰花园”和“城市灯光”。您可以选择纯色模式或点模式进行渲染。有两种插值选项 – 线性(Linear)和流畅(Smooth)。线性模式实际上是一个简单的平均值,它将 Windows Media Player 提供的 1024 个独立频率转换为 512 个独立频率。流畅模式基本上与线性模式相同,但它还会将每个频率级别周围的频率与其相邻的频率级别进行平均。这是线性插值器的代码

// Prepare the interpolation 
void CLinearInterpolator::PrepareInterpolation(TimedLevel* pLevel ) 
{ 
    int x, y; const
    int xmax = 512; for( x = 0, y = 0; x < xmax; x++, y+=2) 
    { 
        // Scale the 1024 separate frequencies down to 512
        // separate frequencies linearly (by average). 
        m_LevelCacheL[x] = (unsigned char)((int)(*pLevel).frequency[0][y] + 
                           (int)(*pLevel).frequency[0][y + 1]) / 2;
        m_LevelCacheR[x] = (unsigned char)((int)(*pLevel).frequency[1][y] + 
                           (int)(*pLevel).frequency[1][y + 1]) / 2; 
    } 
}

这是流畅插值器的代码

// Prepare the interpolation
void CSmoothInterpolator::PrepareInterpolation( TimedLevel* pLevel ) 
{ 
    int x, y;
    const int xmax = 512; 
    for( x = 0, y = 0; x < xmax; x++, y+=2) 
    { 
        // Scale the 1024 separate frequencies down to 512
        // separate frequencies linearly (by average).
        m_LevelCacheL[x] = (unsigned char)((int)(*pLevel).frequency[0][y] + 
                           (int)(*pLevel).frequency[0][y + 1]) / 2;
        m_LevelCacheR[x] = (unsigned char)((int)(*pLevel).frequency[1][y] + 
                           (int)(*pLevel).frequency[1][y + 1]) / 2; 
    } 

    // Smooth all of the frequency samples using
    // a linear average with a given radius. 
    const int radius = 10; 
    for( x = 0; x < xmax; x++) 
    { 
        float sumL = 0.0f, sumR = 0.0f; 
        int count = 0; 
        for(y = x - radius; y < x + radius; y++)
        { 
            sumL += m_LevelCacheL[ max(0,min(xmax - 1,y)) ];
            sumR += m_LevelCacheR[ max(0,min(xmax - 1,y)) ];
            count++; 
        } 
        
        m_LevelCacheL[x] = (unsigned char)(sumL / (float)count); 
        m_LevelCacheR[x] = (unsigned char)(sumR / (float)count); 
    } 
}

您可以从 Windows Media Player 中的右键单击菜单中选择上述可视化模式的任意组合。以下是该可视化的更多图像

City Lights Points

Rose Garden

Rose Garden Points

使用 DirectX 简化

在任何给定时刻,根据您使用的是点模式还是纯色模式,此可视化效果中显示的点数将超过一百万个顶点点或超过三百万个三角形。所有顶点的都需要在每一帧中进行移位并添加新的频率。由于顶点数量庞大,在 CPU 上不断移动所有这些内存可能会导致性能问题。

一种加快速度的简单方法是使用 GPU 和 Ping-Pong 纹理。您可以通过创建两个(或多个)纹理来使用 Ping-Pong 纹理,然后将纹理相互渲染。当您将纹理相互渲染时,您可以选择使用着色器来执行一些 GPGPU 处理。这个项目实际上不需要做任何非常花哨的事情。我们只需要使用 GPU 来移动内存。

由于顶点唯一变化的方面是高度——即y位置,因此我们可以使用 Ping-Pong 纹理来保存高度图。我们基本上是在几个 Ping-Pong 纹理中构建一个频谱图,并使用另一个纹理进行内存移动。下图说明了这一点。

Ping-Pong Textures

由于 DirectX 不允许开发者访问 GPU 上的纹理,因此您必须将 GPU 纹理(默认池中的纹理)复制到主内存纹理(系统池中的纹理)中,以便您可以读取或写入 GPU 纹理。您可以使用 GetRenderTargetData() 读取纹理数据,使用 UpdateSurface() 写入纹理数据,但只能读取或写入系统池中的纹理。因此,您需要一个交换纹理 – 一个位于系统池中的纹理,您可以用来与 GPU 之间复制纹理数据。下图提供了此场景的视觉效果。

Ping-Pong Textures with a Memory Swap Texture in DirectX 9.0c

例如,在将数据从纹理 A 复制到纹理 B 时,我们需要将所有数据向上移动,因此我们只需使用一个精灵对象将纹理 A 的底部 1023 行像素复制到纹理 B 的顶部 1023 行像素。将纹理 B 的内容绑定到交换纹理后,我们使用 GetRenderTargetData()UpdateSurface() 将新数据添加到纹理 B 的底部。现在我们拥有了二维并行内存复制功能,这使得可视化效果能够以显著减少的 CPU 周期运行。

显示数据

着色器有时看起来令人生畏,因为它们通常在数学上非常密集,而且一开始看起来有点陌生。实际上并没有初看起来那么难。开始可能是最难的部分。这是一个快速的 10 秒着色器教程:在 DirectX 9 中,基本上有两种类型的着色器,像素着色器和顶点着色器。顶点着色器通常用于修改或生成顶点,而像素着色器通常用于修改或生成颜色。像素着色器和顶点着色器都可以访问全局着色器变量。顶点着色器首先执行,并将信息传递给像素着色器。DirectX 允许您定义一个顶点声明对象,该对象定义最初传递到顶点着色器的数据的所有顶点用法类型信息。您可以使用 SetVertexDeclaration() 方法将顶点声明绑定到设备,并使用 SetStreamSource() 方法将初始顶点数据绑定到设备。可以输入和输出到顶点着色器,并可能传递给像素着色器的这些数据,通常也在您的着色器代码中定义,如结构所示

struct OutputVS
{
    float4 posH :POSITION0; 
    float4 color :COLOR0; 
};

例如,posHOutputVS 结构中的一个变量名,就像普通的 C/C++ 代码一样,但您会注意到结构末尾添加了额外的名称。额外的名称(POSITION0COLOR0)是用法标识符。用法标识符源自您的顶点声明,它们告诉 DirectX 如何处理 GPU 中的变量。这些用法标识符也用于定义着色器的函数参数。例如,在函数定义中

OutputVS ColorVSRoseGarden(float3 posL : POSITION0)
{ 
    ... 
}

posL 是对应于 OutputVS 结构中 posH 的值的函数局部副本。POSITION0 用法标识符在结构中的值和传递到着色器的值之间创建了关系。用法类型末尾的数字可以上升到多位数。

这个频谱分析器中的着色器非常简单;它主要将 Ping-Pong 纹理中的数据关联到顶点高度,并实现配色方案。纹理数据通常从像素着色器读取,但我们需要从顶点着色器中读取纹理数据。我们必须使用至少顶点着色器版本 3 (vs_3_0),并且我们的纹理格式需要是 D3DFMT_A32B32G32R32F,因为这是 tex2Dlod 函数能够很好处理的格式。

结论

大功告成——一个不那么耗费处理器的 3D 频谱分析器,可以与 Windows Media Player 一起工作。如果能进行连续的过完备 FFT 来提供更高分辨率的音频视图就更好了,但这我留给另一篇文章。

许可

本项目根据我撰写的一项许可进行许可,该许可仅允许项目用于教育目的。您可以在项目所有源文件的顶部找到许可副本。

参考文献

  • [1] Luna, F.D. (2003) "Introduction to 3D Game Programming with DirectX 9.0" pp. 326-333 Wordware Publishing Inc. Plano, Texas.

历史

还没有。我猜代码是完美的!:)

© . All rights reserved.