2024 年 7 月的时候,我患上了某种 “AI 疲惫症”,身为 AI 产品经理,却对 AI 的一切提不上兴趣。大概是经历了 Generative AI 极其兴奋的行业周期,PTSD 了,于是决定做一些含 AI 量 0% 的事情。
我想重新捡起一个一直以来想玩但没空玩的领域:音频。
本科期间受到很多录音工程的同学影响,看了一本奇书:设计声音。第一次看的时候就震惊了,痴迷于里面对于声音合成的系统建模,比如如何合成一只昆虫飞行的声音,会分为翅膀扰动空气空气的声音、翅膀与空气摩擦的声音、肌肉与翅膀连接处的声音、肌肉挥动的声音。每次打开都觉得,好想学,但看到 Pure Data 那像打印机菜单一样的界面(罗老师:这算非遗了),和边上正在爆炸的 AI 模型梯度,哎,放弃吧。
不仅是算法原理难,音频算法的实现也极其费劲,因为声音处理的算法普遍比图像复杂,没个 FFT 下不来,想做一个能合成、处理音频的 app,找遍社区发现只有 Juce 这样的商业软件能满足开发需求,而且它是 C++,开发体验一言难尽。
就在启动困难症发作的时候,我发现了一个很有趣的 Rust 库:FunDSP。
FunDSP 是一个 Rust 的音频 DSP 库,这个库使用了极其有趣的设计,将音频信号流的处理与 Rust(通过 trait 实现)的运算符重载结合在一起,音频信号可以被 + - * /
,而这一切都借助了 Rust 非常有趣的编程范式。
于是我根据 FunDSP 的样例,改出了一个简单的 Synthesizer,可以有多个 notes 被 ADSR 包络,驱动不同的波形如 sine wave,再进入滤波器、和声效果器和混响,输出最后的音频。
我用这样的发声机制程序化复现了 YouTube 上广为流传的 poly rhythm。
解决完音频开发难的问题后,剩下的大头就是渲染了。
一开始的时候,我想用 Godot 做渲染引擎。初次接触的时候我非常兴奋(可能是苦 Unity、Unreal 久矣):居然有一个引擎,如此轻量、自己的脚本语言 + IDE 开发体验如此好,出教程的人越来越多(比如我最爱的 Unity 博主 Brackeys),社区维护的 Rust 扩展也非常好用,没几下就把 Rust 的声音算法集成了,全平台可用。
但做完才发现问题一下子浮现了。
当时的 Godot 还不支持 macOS、iOS 直接编译 Metal,全部需要通过 Vulkan 中转一次,这不仅会有性能问题,而且特性支持不全。比如开发视觉效果最常用的 compute shader(GPGPU),在 Godot 上就是几乎不可用的状态,和自带 GPU 粒子系统并可以和工程代码互操作的 Unity(Visual Effect Graph)、Unreal(Niagara)完全不在一个 level。
还是 Unity 香,团结引擎我错了。
第二个问题就是,我在这里开始萌生 “视觉专辑” 的概念,想把这一个个音画交互的场景封装成一个个可交互的 “专辑”,那么在移动端,他应该是一个类似音乐播放器的 app,于是我尝试在 Godot 里实现一个 iOS app UI,心想做成那些酷炫的 Web3D 网站一样也挺好。但现实啪啪打脸,如果以 120Hz 运行实时渲染的软件主菜单,再加上每个场景在 Viewport 中的渲染,最新款的 iPhone 也顶不住。
最终,我抛弃了 Godot,选择了一套非常底层的实现方案,重新设计了架构,完全重构了项目:
渲染层使用 wgpu 开发,用 WGSL 写 shader,几乎支持了所有我想要的绘图特性,比如 compute shader、indirect draw,只有类似硬件光追的 API 不在其中。
iOS app 使用 SwiftUI 原生开发,不仅简单、性能好、体积小,还可以使用一系列 only apple can do 的功能,比如小组件、后台音乐播放、触觉反馈。
Windows / macOS 端使用 Electron + react.js 作为一个启动器,唤起 Rust 写的 player 程序进行播放,这样做还有一些隐藏的好处,文章后面会提到。
之所以在最下层选择 wgpu,有很多原因。
wgpu 是一个跨端的渲染库,使用 Rust 作为开发语言、WGSL 作为 shading 语言,最终可以编译运行与 iOS(Metal)、Android(Vulkan)、macOS(Metal)、Windows(Vulkan / DirectX)的绘图程序。它的跨端特性吸引了我,结合 Rust 优秀的工具链,wgpu 变得非常适合独立开发。
两年前,我曾用 wgpu 完成了我的本科毕设:大规模 3D 图可视化应用:GraphPU,手撸了 18 个 GPGPU kernel 加速图可视化中弹簧电子力分布模拟,为了在 macOS 也能进行 compute shader 内递归(实现并行版本的 barnus-hut 算法),我们甚至魔改了 wgpu 的 shader 编译器 naga。之前在 IEG 实习时也跟着我的 intern leader 学习了如何用 RenderDoc 来 debug GPU 程序,也让我更加有信心能驾驭这个 API 尚不稳定、但非常面向未来的渲染库(WebGPU 可能会在未来更流行,基于 wgpu 的游戏引擎 bevy 也在慢慢变好)。
为了测试 wgpu 能否满足我的需求(做到 Godot 做不到的事情),我在 wgpu 完全复刻了此前创作的新媒体作品:参数生命,一个由几万个相互交互的粒子组成的复杂系统,这样的粒子系统如果不经优化计算的复杂度将会是 O(N2)
,VFX Graph 和 Niagara 也很难做到。但我在 Claude 的帮助下几乎一天就完成了模拟、渲染代码和 shader 代码的迁移,且因为 WGSL 对结构体的支持更好,代码更加清晰可读了。
为了在将来能在 wgpu 中满足普通粒子系统的开发需求,我又参考 Unity(通过查看编译完的 Shader 和 Shader Graph、Visual Effect Graph 的库文件)和开源引擎复刻了它们的 GPU 粒子系统。
在有了 wgpu 的渲染程序后,ælbum 如何解决渲染画面在 iOS 原生 UI 中显示的问题?这就不得不提到 Jinlei Li 大佬完成的工作了,他提供了生动的文档与开源的范例程序讲解如何在 iOS app 中 embed wgpu,他做的 app 字习也在 App Store 可以下载体验。
又是多年以前,我和一位同事曾完成过 Unity 嵌入 iOS 的工程,相比之下,Unity 非常的重,启动时有强制的 splash screen,对屏幕 resize 非常不友好,如果只拿来当渲染引擎非常的没必要,wgpu 完美解决了这一切,一个基础功能的 wgpu 打包进 iOS 可能只会增加 10MB 的空间,且启动非常迅速,几乎能在 app 打开动画完成前就开始渲染。且对 iOS 的 CADisplayLink 的控制更加主动后,可以更好的调整渲染的启停、帧率、分辨率,对 HDR、离屏渲染的支持也非常好。
于是我制作了一个手感上类似 Apple Music 的播放器 app。一张张专辑在原生 UI 中呈现,下方的 player 在折叠时会以高斯模糊渲染每个专辑的视觉内容,展开时会是一个全屏的浏览和交互体验。
因为外层是 iOS native,所以可以调用任何苹果的 API,并通过消息通道和渲染程序传递,比如可以在参数生命的专辑中,参数的 UI 不再需要在渲染程序中渲染,而是可以用 SwiftUI 来绘制一个个旋钮,旋钮按下、弹起、数值变动时都会有一点非常 nuance 的 haptic feedback,体验相当有趣。除此之外,还可以制作和麦克风、陀螺仪、Apple Pencil 相关的交互。
至此,我们终于可以实现最初的目标:制作一个视觉专辑播放器,最后一步就是支持苹果的 background mode,并让专辑封面可以出现在控制中心,为此,我们还专门写了一封邮件告诉苹果我们费尽心血希望能在后台实时进行音频生成,给我们开一下权限吧,不要杀后台。
开发 iOS app 的同时,我们又用 Electron + Rsbuild + React.js 封装了一个桌面端 app。
桌面端 app 中的最大惊喜,是我们使用 Rust 里的 windows / cocoa API,成功 hack 了 Windows 和 macOS,让我们的渲染程序可以在双端都设为动态桌面壁纸。macOS 为桌面壁纸提供了一个窗口层级 key,而 Windows 需要发送一串神秘代码:0x052C
(实在是太莫名其妙了,完全找不到文档)。
设为桌面壁纸后,就可以一边敲代码,一边享受动态壁纸和音频带来的奇妙氛围(画个饼,还没做,哈哈)。
为了发布这个 app,我攻略了一套新的收款架构,即通过离岸经营的美国公司和美国银行对公账户收款,而不需要使用 Pingpong 这样的收款工具,附一份架构设置图供参考!
设置完毕后,ælbum 也顺利在 App Store 与 Steam 上架了,欢迎前来下载!
后续,我们会不定期更新 ælbum 这个项目,我想加入更加吸引眼球的渲染画面,在辅助睡眠、解压、保持专注、激发创造力这四个主题下设计更多有趣的视觉专辑,欢迎关注!
最后,非常感谢这一路和我一起写了每一个工程的搭档 CPunisher,没有和他的合作,我无法独自一人完成 Rust in iOS、Rust in Godot、Rust + Electron(这位小天才也是 Rust-based 前端编译器 SWC 的 core team member 以及前端工具链开源贡献者),也期待未来我们会一起做更多有趣的项目!
同样谢谢交流了许多音频创意的 @byh、@qyy、@jxy!
收工!