JUCE 音频波形 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 导出流程:从可视化器的块缓冲区取波形快照,用getChannelAsPath转成Path,再自行序列化保存为 SVG 文件。读完你可以给自己的音频应用加一个"把当前波形存成矢量图"的功能,不依赖任何截屏。
先看效果,再讲为什么可行
假设你正在做一个带电平监视的 demo 应用(参考examples/DemoRunner),屏幕上滚动的波形看着不错,但产品同事要一张可以无限放大的波形矢量图放进官网或插件文档。这里有个边界要认清:LiveScrollingAudioDisplay(源码在 examples/Assets/AudioLiveScrollingDisplay.h)只负责往屏幕画——它继承自AudioVisualiserComponent,最终走Graphics::fillPath填充像素,画完就没了。而当前版本的Path没有公开任何"转 SVG 路径数据"的方法(在modules/juce_graphics里搜不到对应接口),方向是反的:Drawable::parseSVGPath能读 SVG,却不能写。所以导出环节没有现成 API,只能自己把Path拉平、拼出d属性。好消息是中间环节全有:AudioVisualiserComponent::getChannelAsPath是个 public static 方法(见 modules/juce_audio_utils/gui/juce_AudioVisualiserComponent.h L124),直接返回归一化坐标下的波形Path。
三步跑通
本地没有仓库的话先拉一份:
git clone https://gitcode.com/GitHub_Trending/ju/JUCE给波形数据打快照
levels(每块的峰值Range<float>数组)和nextSample是组件的私有状态,但paintChannel这个 public virtual 方法把它们当参数传了进来。在LiveScrollingAudioDisplay里重写它,顺手缓存一份路径即可:
// 子类里声明: Path waveformPath; void paintChannel (Graphics& g, Rectangle<float> area, const Range<float>* levels, int numLevels, int nextSample) override { Path snapshot; getChannelAsPath (snapshot, levels, numLevels, nextSample); // 归一化坐标 waveformPath = snapshot; // 缓存最新一帧 AudioVisualiserComponent::paintChannel (g, area, levels, numLevels, nextSample); }注意两点:组件按setRepaintRate (60)的频率重绘,所以waveformPath始终是最近一帧的快照;getChannelAsPath取的是每块的峰值包络(Range的getEnd()/getStart()),导出的形状是包络线而非逐采样波形,这是设计使然。
把 Path 拉到像素尺寸
getChannelAsPath产出的是归一化空间:X 轴 0 到numLevels,Y 轴 -1 到 +1。序列化前做一次仿射变换,映射到你想要的像素宽高(比如 800×200):
waveformPath.applyTransform (AffineTransform::scale (800.0f, 100.0f).translated (0.0f, 100.0f));
变换后 y=-1 落在顶部 0,y=+1 落在底部 200,和屏幕上的渲染逻辑一致(可对照paintChannel里fromTargetPoints的三点映射)。
把 Path 拼成 SVG 文件
JUCE 提供PathFlatteningIterator(modules/juce_graphics/geometry/juce_PathIterator.h),能把曲线按容差拉平成线段序列,用它逐段拼出d属性:
String buildPathData (const Path& path) { String d; PathFlatteningIterator it (path, 0.5f); // 0.5px 拉平容差 Point<float> a, b; while (it.getNextLineSegment (a, b)) d += (d.isEmpty() ? "M" : "L") + String (b.x, 3) + " " + String (b.y, 3); return d; }外面再包一层 SVG 信封、落盘:
void exportWaveformSVG (const Path& path, const File& outFile) { const auto b = path.getBounds(); String svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + String (b.getWidth()) + "\" height=\"" + String (b.getHeight()) + "\">" + "<path d=\"" + buildPathData (path) + "\" fill=\"none\" stroke=\"black\" stroke-width=\"1\"/></svg>"; outFile.createTextFile (svg); }UI 里放个按钮,点击时取waveformPath、变换、调用导出即可。想验证往返正确性,可以用 examples/Assets/DemoUtilities.h 里getJUCELogoPath的同款手法Drawable::parseSVGPath把你的输出读回来,和原图叠在一起比对。
踩坑与调优
- 窗口时长和颗粒度:构造函数里默认
setSamplesPerBlock (256)+setBufferSize (1024)(源码 L37-38)。48kHz 下一个点约 5.3ms,1024 个点覆盖约 5.4 秒。想让 SVG 更锐利:block 降到 64、buffer 提到 4096;想让文件更小则反向调。 - 文件体积:拉平路径每段输出一个
L指令,1024 点宽、800px 宽的图会吐出上千个点。把拉平容差从0.5f提到1.0f,或先对levels隔点采样再画图,体积直接减半。 - 削顶是故意的:demo 里输入被
*= 10.0f放大(源码 L66),getChannelAsPath再按 ±1 截断,所以峰值处的包络会压平成横线。如果导出的图要保留真实动态范围,把这个增益调小,或干脆在快照逻辑里自己算不截断的包络。
还能往哪走
两条现成的延伸:一是把信号源从实时输入换成AudioTransportSource播放的离线文件,让导出的 SVG 对应某段固定音频,适合做文档插图;二是把导出的Path反向塞回Drawable,在 UI 里叠一层矢量波形做对比展示。相关入口:
- 组件源码:examples/Assets/AudioLiveScrollingDisplay.h
- 可视化器:modules/juce_audio_utils/gui/juce_AudioVisualiserComponent.h
- 图形工具:examples/Assets/DemoUtilities.h
【免费下载链接】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),仅供参考