产品10/13/2026·约 4 分钟

WaveCraft 的时间线终于成为信号图

WaveCraft 曾经有看起来像轨道、却不是信号路径的行。我们让每一次编辑、效果、声像与导出共用一张图,并让工程跨重启存活。

假装自己是轨道的那一行

WaveCraft 在拥有真正的多轨引擎之前就有时间线。屏幕能画出行,片段能移动,波形也能采样到正确的矩形里。这足以让编辑器看起来像多轨编辑器,却不足以让这些行对音频引擎有意义。

用户一旦提出真正让一条轨道成为轨道的操作,缺口就暴露了:给它调声像、挂一个效果、移动它而不改变其他轨道、预听结果、导出用户听见的同一个东西。UI 有一行,图里有一组片段,导出器还有自己的遍历方式。每一处单独看都合理,它们却没有共享的不变式。

我们把不变式写成系统的形状:

每一条轨道都是一条从源到混音的信号路径。

这条路径有顺序:读取源片段,应用轨道效果链,应用轨道声像曲线,再到混音总线。预览和导出消费同一份计划。时间线是这条路径的投影,不是第二份音频模型。

让图拥有声音

第一版实现有一个很诱人的捷径:让编辑器决定哪些效果可见,再让音频图重新读模型,尽量构建它能构建的东西。这样图会比较宽容,编辑器的承诺却失去意义。一行可以显示一个效果,实时预览悄悄跳过它;导出又可以包含实时播放器不认识的节点。

现在图构建器接收一条轨道并生成一个 AudioGraphPlan。计划里有源、按顺序排列的效果节点、声像单元以及到混音总线的连接。只有 isTrackMountable 接受,轨道效果才会进入计划。编辑器展示效果面板时使用同一条准入规则。

这比维护一张「支持的效果」列表严格一些。问题不是「产品认不认识这个效果」,而是「在这条轨道实际拥有的输入和声道形状下,这个效果能不能挂上去」。立体声轨道、单声道录音、预览图和导出图之间都可能不同。目录里存在、但路径承载不了的能力,不算能力。

于是三个消费者不再各说各话:

  • 编辑器在创建不可能的命令之前拒绝效果;
  • 预览播放器渲染用户正在看的那张图;
  • 导出器序列化同一张图,而不是从 UI 状态重建另一张。

测试也沿着这条边界分开。轨道效果测试断言准入与顺序;音频图测试断言节点连接;导出测试断言渲染路径包含轨道效果。没有一张快照被要求从像素猜完整个系统。

声像是模型,不是旋钮

声像看起来像一个很小的 UI 功能:加一根滑杆,保存一个数字,再把数字传给播放器。这个形状藏着一个关于能量的决定。线性左右分配会让中点比两端更安静,用户拖过中间时轨道听起来会塌一下。WaveCraft 采用等功率声像曲线。

曲线位于时间线模型旁边的 PanLaw,而不是 View 或音频单元里。模型负责把归一化位置映射成左右增益;音频单元应用映射;编辑器显示同一个位置。Codable 往返测试保证保存的轨道回来之后位置相同,也就保证增益相同。

这个位置决定了行为能不能活下来。如果曲线是 View 里的常量,导出路径可以使用另一条曲线而照样通过编译。如果它藏在音频单元里,时间线就不知道应该怎样描述自己保存的值。一个模型、一个 owner、三个消费者。

下一次操作之后也要成立的编辑

轨道成为真实路径之后,编辑暴露出第二个问题。插入片段、删除一段或移动轨道会同时改变多个投影:时间线、图计划和序列化工程。旧命令按屏幕交互分组,于是一次操作在工程还没有持久表示之前就显得完成了。

WaveCraft 的编辑命令现在描述领域操作。波纹删除会移动后续内容填上空隙;轨道命令移动或复制轨道,同时保留它的效果和声像;轨道效果命令通过和图相同的准入规则挂载、替换或移除节点。每条命令都有逆操作,撤销栈拥有整个序列,而不是某一页。

这对移动端尤其重要。如果撤销属于编辑页的 View controller,离开页面就会销毁历史。工程持久化把状态放入带 writer、reader 与 installer 的包。包记录工程模型和重新打开它所需的编辑历史。重启不再是刚刚做完的操作的一次隐形 reset。

包边界也给失败一个落点。损坏的工程可以带着明确理由安装失败。丢失的素材不会变成一个空片段。writer 可以在 UI 说工程可以安全关闭之前先提交完整包。编辑器随后把包当成持久状态,而不再依赖某个 View 当前还留在内存里的东西。

输入与工具也进入同一份契约

后面的两个功能沿用了同一条边界。

分享扩展不在扩展进程里编辑音频。它接收外部音频项,保留来源文件名,把它放入 App Group 暂存区。ExternalAudioIngest 成为唯一 owner,把暂存项变成工程素材。扩展负责交接;App 通过与 App 内导入相同的模型和持久化路径完成导入。

AI 工具页也不再是一组直接改编辑器的按钮。每个工具创建一份 job,job 报告进度和结果,编辑器再通过编辑命令应用结果。人声分离或母带处理因此成为工程上一份可持久的操作,有地方重试,也有结果可以撤销。

这比把按钮接到回调上慢一些。它也是一次演示和一个能在分享交接、后台 job、重启或导出之后仍然说得清的功能之间的差别。

图改变了什么

看得见的结果是更可靠的编辑器:轨道可以移动、调声像、挂效果并导出,而屏幕和声音不会互相矛盾。更深一层的结果是,WaveCraft 对「轨道是什么」只有一个 owner。时间线、预览、导出、分享导入和 AI 工具都在这个 owner 处汇合。

当你打开 WaveCraft 拖动一个片段时,你操作的是一个能保存、重放和渲染的模型。波形是视图;信号图才是产品的承诺。

关键结论

给一个可见的编辑对象一个领域含义。不是信号路径的时间线行,迟早会让预览和导出给出不同答案。

所有消费者使用同一条准入规则。isTrackMountable 的价值在于编辑器、图构建器、预览播放器和导出器在接收效果之前问的是同一个问题。

把声音决策放进模型。等功率声像应该与被保存的值放在一起,并测试中点、端点与往返结果。

让持久化成为功能契约。分享扩展、后台 AI job 和撤销动作一旦通过同一个包、同一个命令 owner 进入,就都只是普通的工程编辑。

文中提到的产品

继续阅读