1.

音乐是软件。

这向来不只是修辞,因为 Strudel、Sonic Pi、SuperCollider 等音乐编程语言 / 环境并不是什么新鲜事物。

如果从结构上来对照:

  • 参数 (Parameter) -> 音高 (Pitch) / 力度 (Velocity) / 时值 (Duration)
  • 语句 (Statement) -> 音符事件 (Note Event)
  • 函数 (Function) -> 乐句 (Phrase) / 动机 (Motif)
  • 模块 (Module) -> 段落 (Section) / 音轨 (Track)
  • 源码 (Source Code) -> 乐谱 (Score)
  • 中间表示 (Intermediate Representation) -> MIDI
  • 库 (Library) -> 音色库 (Sound Bank) / 采样库 (Sample Library)
  • 运行时 (Runtime) -> 音序器 + 音源 (乐器 / 合成器 / 采样器)
  • 执行 (Execution) -> 演奏 / 播放 (Performance / Playback)
  • 输出 (Output) -> 声音 / 音频 (Sound / Audio)

2.

站在功能性的角度,现阶段的音乐生成事实上也已经足够强大了。

但目前的 AI 音乐仍然没有可被直接消费的价值。原因是大模型的训练目标,是不断逼近训练数据的概率分布,以达成「平均正确」。

而在音乐创作中,「正确」某种程度上就是原罪,等同于平庸。

如果站在创作者的角度,Text-to-Music 基本上也彻底消解了创作的乐趣,以及作为创作者的主体性。

那能不能作为采样的形式来参与创作?创作者只负责拼贴,人人都是 DJ Shadow、OPN… 这是一个可切入的角度,但前提是不再追求「根源性」在采样文化中的意义,以及有耐心在平庸的产物中一次次尝试抽卡。

Text-to-Music 缺少结构化、细粒度的二次编辑能力,我认为这是它现阶段无法比拟 Vibe Coding 的最大短板。

3.

所以我的想法是,Text-to-MIDI 可能才是现阶段通过 AI 来辅助音乐创作的有效手段。

当然这显然也不是什么洞见,事实上,许多提供 Python 运行环境和文件导出能力的 AI Chat App,都可以调用现成的 MIDI 库,根据描述生成一个 .mid 文件。

只是体验上很糟糕,只能口述参数,生成后也无法方便地预览,必须下载文件后再通过 DAW 来调用。

在今年年初的时候,我就一直在构思尝试优化这件事情。

做一个 AU / VST 显然是更直接的方案,但是 C++ 的技术栈相对比较陌生。再者作为 Ableton 的拥趸,一直也想尝试一下 Max4Live 的构建。

最后选择的技术方案是 Max + Node,大体方案是:

  • Max device 根据当前 Track 上所选的 Clip 上下文,触发生成请求发送给 Node 层
  • Node 接受请求后调用 AI provider 并组装 Prompt,生成并返回合法的标准化 Note 数据给 Max
  • Max 层拼装并提供 MIDI 的预览和摘要,用户确认后即可 Apply

4.

在跑通基础流程后,我最终还是放弃了这种方案。

虽然基于 Max 可以在轨道上指哪改哪,但是:

  • Max 作为插件,跟随的是轨道,这意味着需要挂载在所有 MIDI 轨
  • 需要维护独立的 Node 进程、依赖和通信边界,作为个人实验尚可接受,但作为产品太不优雅了
  • Max 的节点式编辑方式太繁琐,在 AI 时代彻底变成了负债,这可能是近期我参与程度最高的开发体验了(x)

5.

我一度搁置了这个产品,直到我突然意识到,MIDI 文件本来就可以很方便地从 DAW 外部拖拽进入,并可以放置在 MIDI 轨道上的任意位置…

所以根本就是想太多了,只需要创建一个走原生 AppKit 拖放的 App 就行了。

最后的大致方案是:

  • 统一 Adapter 接入 OpenAI、Anthropic、Gemini、Ollama 及多种兼容供应商
  • AI 输出可验证的结构化音乐 JSON,而非直接生成 MIDI 二进制
  • 本地校验模型输出,并对可恢复的问题发起有限次数的 AI 修复
  • 使用纯 Swift 将结构化音符、BPM 和拍号编译为标准 MIDI 文件
  • 将 MIDI 临时写成 .mid 文件,并通过 AppKit 原生拖放传递给 DAW

至此主流程已经能完全满足我的需求。

6.

在交互层面,我将 BPM、Time Signature、Key、Scale 和 Clip Length 包装为了独立的参数配置项,这样能降低输入成本和表达歧义,也让生成结果更稳定。

对于生成的片段,我还实现了内置试听功能:将 MIDI Data 交给 AVAudioSequencer 解析并驱动播放时序,再由 AVAudioUnitSampler 加载系统 Sound Bank 中的内置音色,最后通过 AVAudioEngine 完成实时音频渲染和输出。整个试听过程完全在本地完成,无需导出文件或打开 DAW,就能快速验证生成结果。

额外的一些单点功能:

  • 继续生成: 复用已有片段的结构化音乐数据、Prompt 和参数上下文,生成自然衔接的新内容
  • 并行生成: 使用 Swift Concurrency 独立运行多个生成任务,互不阻塞
  • Metadata 详情面板: 集中展示 Clip 的 BPM、拍号、调性、音阶、长度、供应商和生成来源
  • 即时 Clip 搜索: 通过名称、Prompt 和相关元数据实时筛选和定位片段
  • 快捷键: 全局快速唤起

7.

不过没想到这次踩得最大的坑是发布。

因为有 OpenCat、Chatbox 这种已上架 app 的先例,所以没有多怀疑就提了审。

等待了一周多后被拒,理由是违反了 Guideline 3.1.1:API Key 的存在 == 用户可绕开 IAP 来付费使用。

所以按照要求严格执行的话必须做 IAP 来订阅以供用户直接使用。

但现在我也不想再猜那些热门产品内的 API Key 填写方式是怎么过审的,我完全打消了上架的念头。相应的,可以借此机会走一遍自动更新框架的集成。

8.

Sparkle 作为最主流的更新框架没什么争议,但是发布渠道是个问题。

我不太想把源码库开源 (写太烂),所以最初的方案是,源码库做构建签名等事务,然后发布 Release 到另外的公开库。

但是 GitHub 免费账户的 Actions 用量实在太少了,Build 了 3 次甚至还没跑通就用完了当月额度…

于是本着节俭原则重新规划了方案:

  • Xcode Cloud:Build -> Developer ID Export -> Apple Notarization -> DMG Packaging -> Sparkle Signing -> GitHub Release
  • GitHub Actions:Validate Release -> Deploy appcast.xml -> Verify Production Feed

Xcode Cloud 负责必须在 macOS 环境中完成的构建、签名和公证,并将最终 DMG 与 appcast 发布到公开 GitHub 仓库;GitHub Actions 只运行成本更低的任务,把已经验证过的 Feed 部署到 Cloudflare。

这套方案比最初预想的复杂得多,中间还踩过证书导入、Keychain 隔离、公证凭据、DMG 打包、版本门禁和 CI 环境差异等问题。但跑通以后,后续发布只需要更新版本号和 Release Notes,再推送一个 Tag 即可自动完成全部链路。

某种程度上,发布链路的构建甚至比产品功能本身的开发还复杂一些。

不过也好在是 Mac App,换做 iOS 的话,所有心血都付诸东流了 (中心化之罪)。