HandPiano 是一个原生 iOS 音乐应用:整块屏幕就是琴盘,横向控制音高,纵向控制音量。它没有传统钢琴键,也没有复杂按钮,目标是让用户直接用手指在屏幕上“摸出”旋律。

这篇复盘不是介绍某个单点功能,而是记录一个更完整的过程:如何把一个模糊的创意,通过和 Codex 的连续协作,推进到一个基本可玩的工程状态。

HandPiano gameplay

1. 先做出能玩的核心

项目最初的核心很简单:

  • 从左到右,音调由低到高。
  • 从下到上,音量由低到高。
  • 用户触摸屏幕时发声,离开时停止。

这类应用最怕一开始就陷入复杂架构。我们选择先把“触摸到声音”的闭环跑通:PianoTouchSurface 负责触摸、坐标和视觉反馈,PianoAudioEngine 负责实时合成。这个分层很朴素,但非常关键,因为之后几乎所有功能都围绕这两层扩展。

第一阶段 Codex 的价值主要体现在两个方面:

  • 快速在现有 Swift 项目中定位入口文件、触摸逻辑和音频逻辑。
  • 每次修改后都用 xcodebuild 验证,避免“看起来合理但不能编译”的漂移。

比如早期出现过“触摸离开后声音没有停止”的问题,修复时不是单纯加一行 stop,而是围绕触摸生命周期检查 touchesEndedtouchesCancelled、active touch 状态和 voice stop 行为。这类问题很适合让 Codex 先读上下文,再做小范围修改。

2. 音色不是一次调好的

声音体验是整个项目里反复最多的部分。

一开始的声音偏电子合成,后来逐步调整为更接近钢琴,再往“木质、更柔”的方向调。之后真机测试又暴露出高音、中音、低音的大音量区域会有毛刺、丝丝声、嗡嗡声。这里的经验是:音频代码不能只在模拟器或主观想象里完成,真机反馈非常重要。

最终音频引擎做了几类关键处理:

  • voice pool 限制,避免疯狂多指敲击后声音堆积失控。
  • 释放阶段淡出,模拟真实乐器离键后的尾音。
  • 输出滤波和 soft limit,控制刺耳高频和大动态失真。
  • 多音色 preset:钢琴、古典吉他、电吉他、古筝等。

这部分协作的节奏很典型:用户用真机听,描述主观感受;Codex 根据描述调整合成参数和处理链;然后再编译、再试。这不是“一次生成完美代码”,而是 AI 辅助下的快速试音迭代。

3. 从工具到作品:录制、曲库和内置演奏

当触摸和声音稳定后,项目开始从“玩具”变成“应用”。

我们加入了:

  • 用户录制自己的演奏轨迹。
  • 保存和回放演奏。
  • 内置演奏列表。
  • 播放时显示轨迹、按压点和涟漪反馈。
  • Library 分区、折叠、左滑删除。

这里最重要的设计选择是:录制的不只是音频,而是触摸事件轨迹。

每个事件包含:

  • 时间点。
  • 触摸 ID。
  • began / moved / ended 阶段。
  • x/y 比例坐标。

这样回放时既能重新发声,也能重新绘制轨迹和按压位置。它让 HandPiano 的回放更像“重新演奏一遍”,而不是播放一段固定音频。

这个阶段 Codex 很适合承担重复但容易出错的工作:生成多个内置演奏、补多指事件、调整曲库 UI、让保存和删除逻辑保持一致。人工则负责判断“这首好不好听”“这个列表高度是否舒服”“这个交互是不是符合预期”。

4. 节奏感:从声音节拍器到视觉节拍器

后来我们意识到:演奏最重要的是节奏。

一开始节拍器实现成了声音 click,但这很快被推翻了。对于一个音乐演奏应用,节拍器声音会和用户正在演奏的音色抢空间。于是方向变成了视觉节拍器:

  • toolbar 本体按节拍轻微膨胀。
  • 从 toolbar 向外扩散水波纹。
  • 重音拍使用更亮的金色。
  • 支持 BPM、拍号、重音开关。
  • 支持 haptics,并检测设备是否支持震动。

这个迭代体现了 Codex 协作里很重要的一点:先做出来,再根据体验判断方向是否对。错误实现并不是浪费,只要能快速改掉,它就是探索成本的一部分。

之后我们又给内置演奏补上 metronome metadata。能关联固定节拍的演奏都关联,节奏不规则的直接移除。这样播放内置演奏时,视觉节拍器会同步工作,用户能同时看到“手指轨迹”和“节奏脉冲”。

5. 方向引导:不要让系统状态破坏体验

HandPiano 的核心体验依赖横屏。但当项目改为支持 iPhone + iPad 后,竖屏状态必须被认真处理。

我们做了一个方向门:

  • 竖屏或窗口不是横向时,不显示琴盘。
  • 显示一个整屏提示,引导用户关闭旋转锁并横过来。
  • 横屏过程中先保留提示页,避免露出系统白底。
  • 横屏稳定后,提示淡出,琴盘淡入。

这里踩过一个很现实的坑:如果 Info.plist 只声明横屏,设备竖着时系统仍然给 app 一个横屏 window。结果提示页写得再“全屏”,也只能填满那个横屏窗口,看起来就像竖屏中间一块。解决方式是让 app 声明支持竖屏,再用自己的界面逻辑控制竖屏显示提示、横屏显示琴盘。

这个问题很适合复盘,因为它不是业务逻辑错误,而是平台行为和界面架构之间的错位。Codex 能快速帮忙定位 Info.plist、SwiftUI 根视图和 UIKit window 背景之间的关系。

6. Codex 在这个项目里的工作方式

整个过程里,Codex 不是一次性“生成一个 app”,而更像一个持续在场的工程搭档。

比较有效的协作模式有几种。

小步快跑

每次只提出一个相对明确的问题:

  • 修复触摸离开不停止。
  • 调整音量范围。
  • 加一个 icon。
  • 改音色。
  • 加曲库删除。
  • 加节拍器。
  • 改方向提示。

每次修改后都构建验证,再继续下一个问题。这样可以避免多个风险叠在一起。

用户负责体验判断

很多需求不是“对或错”,而是“感觉对不对”:

  • 声音是否更木质。
  • 毛刺是否还能听见。
  • 水波纹是否明显。
  • 竖屏提示是否生硬。
  • 曲子是否动听。

这类判断必须由人来做。Codex 负责快速改代码,人负责判断方向。

Codex 负责工程记忆

随着功能增加,项目上下文会变复杂。Codex 可以持续记住:

  • 哪些文件负责音频。
  • 哪些文件负责曲库。
  • 哪些功能已经提交。
  • 哪些行为是后来特意保留的,比如隔空识别代码暂时关闭。

这让连续迭代不至于每次重新解释项目结构。

每个阶段都提交

项目中形成了清晰的提交节点,例如:

  • Add visual metronome with haptics
  • Sync built-in performances with metronome
  • Add orientation prompt transition
  • Support iPhone devices
  • Update README with gameplay overview

这些提交不仅是版本保存,也是复盘线索。和 Codex 协作时尤其建议频繁提交,因为 AI 修改速度很快,版本边界能给探索留安全绳。

7. 代码落地中的几个经验

让 AI 先读代码

不要直接让 Codex 猜项目结构。每次进入新模块前,先让它用 rgsedgit diff 读当前实现。这个习惯避免了很多“凭空发明架构”的问题。

让实现贴着现有风格走

这个项目虽然是 SwiftUI 入口,但主交互是一个 UIKit-backed touch surface。后续很多 UI 能继续在 PianoTouchSurface 里完成,就没有强行重构成纯 SwiftUI。项目落地时,统一和稳定比“范式洁癖”更重要。

真机反馈优先

音频、震动、横竖屏、触摸密度,这些都必须靠真机。Codex 可以构建验证,但不能替代手感验证。

能删就删

内置演奏中不能稳定关联节拍器的曲子,最后选择删除,而不是硬塞 metadata。这是一个小但重要的取舍:示例内容宁可少一点,也要和产品主张一致。

8. 当前状态与下一步

现在的 HandPiano 已经具备基本可玩状态:

  • 触摸即演奏。
  • 支持多音色。
  • 支持录制和回放。
  • 有内置演奏。
  • 有视觉节拍器。
  • 支持 iPhone 和 iPad。
  • 有竖屏引导和横屏过渡。

下一步可以继续往两个方向走。

一个是演奏体验:

  • 更细腻的音色模型。
  • 更自然的力度曲线。
  • 更好的和声示例曲。
  • 更明确的节奏训练模式。

另一个是隔空演奏:

  • 恢复前置摄像头手势识别入口。
  • 优化手部轮廓绘制。
  • 将悬空手指位置映射到琴盘触发。
  • 处理延迟、误触和稳定性。

结语

这次经历最有价值的地方,不是 Codex 一次性写了多少代码,而是它让“想法 - 试做 - 真机反馈 - 修改 - 提交”的循环变得很短。

对于独立应用、小型原型或早期产品探索,这种工作方式非常有效:人保留审美、判断和方向,Codex 负责把想法快速落到代码里。HandPiano 能从一个手势钢琴想法推进到当前的可玩版本,靠的正是这种持续协作。

References