JUCE 音频波形可视化:3 步把实时波形导出为 SVG 矢量图
【免费下载链接】JUCEJUCE is an open-source cross-platform C++ application framework for desktop and mobile applications, including VST, VST3, AU, AUv3, LV2 and AAX audio plug-ins.项目地址: https://gitcode.com/GitHub_Trending/ju/JUCE
本教程围绕 JUCE 音频波形可视化展开:用仓库自带的LiveScrollingAudioDisplay实时画出输入波形,再把这段波形导出成 SVG 矢量文件,补上 JUCE 没有现成导出接口这一环。
👀 效果先看
这是仓库示例自带的 4:3 演示图,DemoRunner的音频页面拿它当背景——把静态画面换成实时滚动波形,就是这套流程要做的事:左边在屏幕上看,右边存成矢量文件。
动手前准备
- 克隆代码:
git clone https://gitcode.com/GitHub_Trending/ju/JUCE - 装好支持 C++17 的编译器(GCC 9+ / Clang / MSVC)和 CMake。
- 在你的工程里启用juce_audio_devices与juce_audio_utils两个模块,波形组件依赖后者。
- 准备一台能出声、进声的设备,回调里必须有真实输入,画面才动得起来。
核心原理:数据从哪来,SVG 怎么写
链路是三段:音频设备回调送来一批采样点 → 多声道求和后交给pushSample,AudioVisualiserComponent(框架的滚动波形组件)把它存进环形缓冲并画出滚动画面 → 你的导出逻辑把同样的采样点画进Path(框架内置的矢量路径类),再遍历路径序列化成 SVG 字符串。
下面这段是examples/Assets/AudioLiveScrollingDisplay.h中回调的核心逻辑,它演示了"原始数据怎么变成波形"。看完你会明白:导出 SVG 时需要的只是同一份求和后的采样值。
for (int i = 0; i < numberOfSamples; ++i) { float sample = 0.0f; for (int chan = 0; chan < numInputChannels; ++chan) if (const float* ch = inputChannelData[chan]) sample += ch[i]; // 多声道叠加成一路 sample *= 10.0f; // 放大电平,波形才看得清 pushSample (&sample, 1); // 送入滚动缓冲 }分步实现:LiveScrollingAudioDisplay 到 SVG 导出
第 1 步:挂上组件,让画面先动起来
做什么:实例化LiveScrollingAudioDisplay,把设备拿到手并注册回调。为什么:没有这一步,后面没有任何数据可导。
这段代码把演示组件接入音频设备。跑起来后,你会看到窗口里持续滚动的输入波形。
LiveScrollingAudioDisplay display; if (auto device = AudioDeviceManager::getInstance().getAudioDevice()) device->addAudioCallback (&display);第 2 步:旁路一份采样,专供导出
做什么:再注册一个自己的AudioIODeviceCallback,把求和后的采样按序存进std::vector<float>。为什么:预览走组件内部缓冲,导出一份独立数据更干净,也方便你裁剪长度。
这段代码就是数据的"采集器"。设备启动后,你会得到一个可随时读取的波形数组。
struct WaveformCollector : public AudioIODeviceCallback { std::vector<float> samples; void audioDeviceAboutToStart (AudioIODevice*) override { samples.clear(); } void audioDeviceStopped() override { samples.clear(); } void audioDeviceIOCallbackWithContext (const float* const* in, int numIn, float* const*, int, int numSamples, const AudioIODeviceCallbackContext&) override { for (int i = 0; i < numSamples; ++i) { float sum = 0.0f; for (int c = 0; c < numIn; ++c) if (const float* ch = in[c]) sum += ch[i]; samples.push_back (sum * 10.0f); } } };第 3 步:采样点落成矢量路径
做什么:把数组映射进指定矩形,用startNewSubPath起笔、lineTo逐点连线。为什么:Path是 JUCE 里所有矢量图形的通用载体,画到屏幕和写成 SVG 都能复用它。
这段代码把数据变成图形对象。执行完,你会得到一个可直接渲染、也待序列化的路径。
Path buildWaveformPath (const std::vector<float>& data, const Rectangle<float>& area) { Path path; if (data.size() < 2) return path; const float xStep = area.getWidth() / static_cast<float> (data.size() - 1); const float midY = area.getCentreY(); const float amp = area.getHeight() * 0.5f; path.startNewSubPath (area.getX(), midY - data[0] * amp); for (size_t i = 1; i < data.size(); ++i) path.lineTo (area.getX() + static_cast<float> (i) * xStep, midY - data[i] * amp); return path; }第 4 步:序列化成 SVG 并写文件
做什么:用PathFlatteningIterator把路径逐段取出,拼成 SVG 的d属性,再整包写入文件。为什么:SVG 的 path 语法就是"M 移动 + L 直线"的坐标串,逐段输出即可,不需要额外库。
这两段代码一上一下:上面负责生成坐标串,下面负责套上 XML 外壳落盘。完成后,你的磁盘上会出现一个可无限缩放的波形文件。
String makePathData (const Path& path) { String d; PathFlatteningIterator it (path, {}, 0.25f); while (it.next()) { if (it.subPathIndex == 0) d << "M " << it.x1 << ' ' << it.y1 << ' '; d << "L " << it.x2 << ' ' << it.y2 << ' '; } return d; }void saveWaveformSVG (const Path& path, const File& target) { const auto b = path.getBounds(); const String svg = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + String (b.getWidth()) + "\" height=\"" + String (b.getHeight()) + "\">\n" " <path d=\"" + makePathData (path) + "\" fill=\"none\" stroke=\"black\" stroke-width=\"1\"/>\n" "</svg>"; target.replaceWithTextFile (svg); }可调参数
| 参数 | 作用 | 建议值 |
|---|---|---|
| setBufferSize | 可视化组件环形缓冲的深度,决定一屏能装多长的波形 | 1024 |
| setSamplesPerBlock | 每次推入缓冲的采样数,影响滚动颗粒度 | 256 |
| 回调增益(如 10.0f) | 放大输入电平,波形才可辨认 | 1 ~ 10 |
| 展平 tolerance | 曲线拆直线的允许偏差,越大点越少、文件越小 | 0.1 ~ 0.5 |
stroke-width | SVG 里波形线的粗细 | 1 |
⚠️ 踩坑速查
- 波形几乎是一条直线→ 麦克风电平太低,肉眼分不出起伏 → 在回调里乘增益,示例用的是 10.0f。
- SVG 文件异常大→ 路径被拆成过多直线段 → 调大 tolerance,或按固定间隔抽点后再建Path。
- 预览滚动卡顿→ block 与 buffer 的比例失调 → 让setSamplesPerBlock约为setBufferSize的 1/4。
收尾
这套流程可以直接放进音频编辑器、示波监控面板或教学工具,让波形既能实时看、也能无损存档。
- 官方 CMake 构建文档:docs/CMake API.md
- 波形组件源码:examples/Assets/AudioLiveScrollingDisplay.h
- Path类定义:modules/juce_graphics/geometry/juce_Path.h
- PathFlatteningIterator定义:modules/juce_graphics/geometry/juce_PathIterator.h
把第 2~4 步粘进你的工程,跑通第一个属于自己的波形 SVG。
【免费下载链接】JUCEJUCE is an open-source cross-platform C++ application framework for desktop and mobile applications, including VST, VST3, AU, AUv3, LV2 and AAX audio plug-ins.项目地址: https://gitcode.com/GitHub_Trending/ju/JUCE
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考