## Play the music around you:Pulsefield 想创造什么体验? 想象如下场景: *音乐已经在房间里播放*。 你和朋友打开 Pulsefield,不需要先寻找对应谱面,也不必进入一个预先安排好的游戏曲库。我们希望每个人都能沿着同一个音乐时间轴,以适合自己的动作方式参与其中——手机、键盘,或者一套自制控制器。 Pulsefield 想解决的问题,是怎样让正在发生的音乐,成为一种可以*共同进入的可玩媒介*。 ### 从 4k 下落式音游开始! 音乐游戏已经成为了一个成熟的游戏品类, 而其中恐怕最具代表性的基本形式就是 下落式音游. 而其中建模起来最方便又不失通用性的, 恐怕是 4k 定轨 下落式音游. 社区音乐游戏如malody, osu! 都将其列为基础游玩模式之一. 社区谱面库提供了大量可供机器学习的语料库. > [!note]- 游玩体验简介 > > ![[beatmap-lens-5s-4star.webp]] > > 下落的音符分为两种, TAP 和 LN. 玩家在适当的时机 (掉落在判定线上) 单击或者进入长按或者释放长按进行游玩. 音符的放置往往和旋律, 以及谱师个人表达有关. 一张结构上合法的谱面, 并不意味着其可玩性必然高, ==如何确保模型生成可玩性高的谱面==将会在之后的模块详细说明. > 对绝大多数玩家而言, 四个轨道分别对应左手和右手的食指和中指. 这篇 log 记录的是通向这一体验的第一段研究路线。 [[#最早的地基:先让游戏、设备和音乐说同一种“时间” |同步和音乐时间分析]] 已有独立实现;最近的主要进展,是在给定时间结构与短谱面前缀的条件下,生成具有持续组织的 4K [[#milestone R1 用3M参数, 在给定时间骨架下生成长程稳定, 跨难度泛化的高质量谱面|choreography]]。音频到时间计划、无前缀启动和显式用户控制,仍是未来要连接的部分。 ## 最早的地基:先让游戏、设备和音乐说同一种“时间” Pulsefield通过外部API, 音频指纹匹配去获取**周围正在播放的歌曲是什么**, 之后要区分 “对齐音乐” “找节拍” “生成起音”等多个阶段. | 问题 | 它回答什么 | 角色 | | -------------------- | ----------------------------------- | ------------- | | Ambient sync | 周围正在播放的声音,对应 reference audio 的哪个位置? | 让设备进入现实音乐的时间轴 | | Beat/timing analysis | 音乐的拍点、节奏周期和速度变化怎样组织? | 提供音乐时间结构 | | Chart timing | 哪些时刻应该安排起音或提供释放机会? | 决定这次编排怎样表达音乐 | ### Ambient Sync 详见: [音乐对齐engine](https://github.com/Pulsefield/sonalign) ### Beat / timing analysis 详见: [timing module](https://github.com/Pulsefield/Pulsefield-model/tree/main/src/pulsefield_model/timing) ### 我们逐渐能够回答“音乐现在在哪里”和“它的时间结构是什么”。但即使这些答案都给对,仍然不能直接得到一张好玩的谱面。 ## 从“合法”到“好玩”, Pulsefield v3 的问题分解 在开发 Pulsefield 的过程中,我参考了 osuT5 路线及其后续项目 Mapperatorinator,以及 MuG Diffusion。前者主要将谱面表示为事件序列,通过 audio-conditioned encoder-decoder 逐步生成;后者采用音频条件下的 Diffusion 路线生成谱面。这两类方法提供了不同的技术起点,但我更关心的问题是:**生成结果不仅要能被游戏读取,还要能持续组织出有意义的游玩体验。** 在我此前接触的部分 osuT5 生成结果中,局部模式会退化为长时间持续的 Jack,迟迟无法退出,LN 的组织变化也比较有限。这里的问题不是“出现 Jack”或者“重复不够少”:重复本身就是谱面表达的重要组成部分。真正的问题是,模型似乎进入了某种自我延续的局部模式,缺少有意义的发展、转换与结束。(这些是我在具体生成结果中的初步观察,而不是总体评测) 从架构上看,这种失败有值得追查的信息路径。Mapperatorinator 通过重叠窗口和先前生成的事件 token 维持跨段连续性,因此模型自己生成的历史会继续影响后续生成。这个机制既能维持一致性,也可能让已经出现的退化模式被反复延续。 对于 MuG Diffusion,我尚未完成系统实测。公开展示中部分结果的模式变化未达我的预期,但不足以据此判断其整体能力。其表示包含逐轨道的 note start、holding 状态与起止时间偏移,能够承载 LN 和细粒度时间关系;真正需要检验的是,模型能否将这些元素组织成有上下文意义的动作关系。 因此,问题不只是选择哪种生成架构,而是明确模型需要利用哪些依赖:同轨道再击打的间隔、同手与换手的关系、LN 占用对其他动作的影响,以及当前动作留下的释放与再击打机会。无论采用事件 token 还是密集时间表示,**能够生成合法排列,不等于已经有效建模了这些动作对后续游玩的影响。** 运行方式同样重要:我希望 Pulsefield 能在有限设备资源下,以可控延迟持续生成可提交的谱面片段,而不只是离线输出完整谱面。 这些观察与目标促成了 Pulsefield v3 的问题分解:一部分研究音乐与谱面的时间组织,另一部分研究如何在给定时间条件和已提交谱面历史下生成四轨道 choreography。**这一分解不假定两者独立,而是为了明确各自的条件、职责与失败模式,让时间选择与动作组织能够被分别研究和验证。** Pulsefield v3 formulation的起点是定义该问题的数学形式化表达. 我们希望控制框架的结构使其特化于4k实时高质量谱面生成问题, 加入有用的人类inductive bias, 同时不施加具体的implementation detail约束. - **从四条轨道到双手的动作组织。** 我们在 canonical 4K 手位约定下,将四条轨道表示为左右手各自的 outer/inner 角色。模型在两个对称的手视角上共享参数,同时显式评分双手联合动作,而不是将左右手视为互不相关的生成过程。后续加入的 seed conditioning、历史 memory、head/release routing 和动作后果模块,也延续了这一镜像等变设计。 - **将时间组织与具体 choreography 分开研究。** 我们先固定时间条件,研究模型能否将这些位置组织成连贯的四轨道 press/hold/release 动作。R1 使用源谱面提供的 typed timing 与 complete-object seed,生成后续的 lane、TAP/LN 与 release 决策;它验证的是这一条件下的 choreography 子问题,而不是完整的 audio-to-chart 系统。时间骨架生成和自主 seed 生成是后续需要接入的部分。 - **从下一步预测,走向动作后果与 gameplay response。** 我们在 formulation 中把 gameplay demand frontier 定义为:同一段历史面对不同合法续写时产生的响应,而不是一个预先指定的难度标量。当前 R1 已经显式利用占用、动作间隔和释放机会等候选动作后果,并引入有限 horizon 的 response proxy 来修正生成偏好。这是向该目标迈出的具体一步;具有独立语义验证的 demand readout 与 style/demand control,仍是后续研究内容。 详见: [Pulsefield Formulation](https://github.com/Pulsefield/Pulsefield-model/blob/main/docs/formulation/README.md) ## beatmap-lens 为什么需要[beatmap-lens](https://github.com/Pulsefield/beatmap-lens)? - 因为现有的谱面渲染库不够ergonomic, 我们提供了一套更加易用, 稳定和高效的底层渲染API - 因为 ”好玩的谱面“ 不是一个良定义, 我们需要实际的数据集, 去记录人类对于一段谱面的理解和判断 - 因为现有谱面metadata数据集局限于整曲标记, osu community tag也存在数据质量的问题, 有style意义的标注相当稀疏, 冷门谱面一个标注也不会有 -->> 我们需要建立一套自己的style taxonomy和标注规范, 并且是section-level! ### 底层查询, 渲染库的包设计 antfu是我的偶像之一, 该包的设计参考了他的思想, 也参考了reamber-py(老资历谱面渲染库)在实际使用体验下的不足之处. 我们从几个具体的使用场景出发, 重新考虑了谱面渲染API应该替用户处理哪些事情. 用户可能是从osu过来的, 已经有自己习惯的scroll speed. 因此我们提供了`osuLazerManiaPixelsPerSecond` adapter, 根据osu!lazer的scroll speed和游玩区域尺寸, 换算出渲染所需的`pixelsPerSecond`, 让note的纵向间距对应到玩家熟悉的基础阅读尺度. 用户可以沿用自己的游戏设置, 不必从一个陌生的像素参数开始反复调试. 用户做inspection时, 经常需要左右切换图片、对照相邻片段. 因此批量渲染采用固定页面尺寸, 内容按照时间顺序从左到右分栏, 放不下再分页;即使最后一页没有填满, 也保留相同的画布大小. 这些SVG页面按统一尺寸转成PNG后, 翻页时就能保持稳定的显示区域. 我们还将页面宽度和每个panel的note row数量分开控制:前者决定一页能放几栏, 后者决定一次阅读多少排列. 阅读目的也会影响布局. 检查节奏时, 需要保留note之间的真实时间比例;检查密集排列时, 则希望展开挤在一起的rows、减少大段空白. 因此我们分别提供`linear`和`row-aware`模式:前者使用固定时间尺度, 后者调整局部间距, 展开密集rows并压缩没有active LN的空白, 同时保留真实时间标尺和压缩位置标记. 用户可以根据当前要观察的问题选择view. 这些需求对应到两层API. 常用入口围绕`Beatmap`组织:导入谱面后, 给出要看的时间范围, 就可以渲染单个片段或导出分页文档. 底层则分别暴露解析、normalization、scene构造、document排版和serialization, 方便开发者在需要的位置接入自己的处理逻辑. ![[mermaid-diagram.png]] 开发者可能还需要在图上叠加playhead、框选section或高亮evidence, 所以`RenderScene`会保留可检查的几何信息和时间投影, 并提供`projectTime/unprojectTime`完成时间与画面坐标的双向转换. 渲染、点击和overlay共用同一套映射, 调整滚速或时间方向时可以一起更新;自动排版的实际结果则可以通过`document.resolved`读取, 供上层交互继续使用. 查询与渲染也通过evidence衔接:查询负责找出匹配的排列, 渲染负责呈现匹配结果及其上下文. 这样, “找到某种结构”和“查看它在谱面中如何出现”可以组成连续的inspection流程, 也为后面的section标注、evidence选择和人工审阅提供了底层基础. ### agent-human 谱面标注工作流的迭代 & agent harness 设计 这部分我们做了特别多的迭代. 基本的思路: 人类专家产出golden label, agent基于人类固定的foundation (section-style分类的权威定义) & 不断迭代的skill以及harness, 产出silver label 增广覆盖. 这部分涉及到了很多问题, 产出了不少见解: 1. skill应当做thin. 做一个行为上的guiding router. 具体的谱面判例应当放在retrieval bank里面, 让agent按需检索 2. 让agent去做最有信息增量的标注工作. 考虑label的平衡性等 3. 在beatmap-lens 渲染图 和 .osu原始文本文件之外, 需要给agent提供更多的view, 让它去方便query某种特定pattern. 这方面的设计高度依赖人类判例经验 4. 尽可能利用agent的智慧/通用能力, 在查看一个section的时候, 不要仅输出一个label可以输出全部label+confidence, 还能自己选择evidence note + context 范围. 5. 要做严provenance, 人类给出的信息要明确+consistent. 一开始标注schema里面没有confidence, 对于边界条件的判断模糊, 后来确立了典型明确例子, 典型模糊例子, 并在comment里面用自然语言描述了consistent的判例依据, 人类看法 (可以包含主观的手感看法, 总之必须是人类专家最直接的判断依据), 并为skill的迭代建立了清晰的门控 (哪些例子必须通过率达到多少, 多次独立判断+审阅), agent产出的silver label才真正达标. ## milestone [R1](https://huggingface.co/sed-i/pulsefield-r1-restored): 用3M参数,从重建任务走到完整谱面续写 R1 的 architecture 是一路试出来的。最初做 style prediction,后来转向 reconstruction,再走到完整谱面续写,每次调整都和实际遇到的问题有关:模型学到了哪些关系,当前任务能提供多少监督,输入的信息又有没有被预测用上。对我来说,从 style readout 走到 Plain R1 的这段过程最值得记录,后面针对 long rollout 的几个 patch 也建立在这些尝试上。最初的 Plain R1 有约 2.28M 参数,加上后续的 seed conditioning、long memory 和动作修正,目前保留的版本为 **3,084,432 个参数**。先从最早的 style prediction 讲起。[^r1-origin][^r1-results] ### 从 style prediction 转向 reconstruction 有了 section 数据集之后,我最初想做的是 style prediction:给定谱面和 section,让模型判断 Jack、Stream、Trill、Tech、LN coordination 的表达强度,并尝试利用 evidence notes 帮助它学会这些判断。在实际实现中,assessment 与 evidence selector 共享 encoder,但 assessment 不直接读取标注答案或 evidence mask;这样才能检验 evidence supervision 是否真的改善了谱面表示。[^style-model] 我们试过更丰富的时间输入、multiscale composition 和不同 readout,部分 NLL 确实下降了,几个困难判例却一直没有改善。Trill 就是一个例子:在那轮诊断里,模型连 human training positives 都没有很好地区分出来,调整判断阈值也解决不了这个问题。[^style-failure] 这让我开始怀疑,style prediction 本身能学到多少东西。两段谱面可以有完全相同的 tags,动作组织却很不一样;evidence notes 也只选出了支持某个判断的局部证据,整段谱面还有很多关系没有被描述。对于那些不影响标签、却会影响后续编排的区别,标签预测任务缺少保留它们的动力。[^representation] 所以我决定先直接研究 reconstruction:给定一部分谱面,让模型恢复被隐藏的完整动作 rows。Style readout 留作观察表示的工具,Beatmap Lens 和数据集则继续用来检查具体片段。这样除了平均 loss,我们还能看到模型在哪些排列上出了问题。[^representation] ### BiGRU 阶段:检查信息怎样参与预测 这时仍然使用 BiGRU。为了研究完整动作的重建,我们把完整 row 的表示、局部组合、关系检索和全局 context 分开,保留中间状态供不同 query 读取。每加一个模块,都要考虑它形成的关系能通过什么路径影响预测。[^source-action] ActionReader 的一次诊断很有意思。它会读取多个层次的 memory,同时又从 contextual state 获得自己的 query 和 residual。在两个 seeds、两种 composition order 的 frozen-checkpoint 诊断中,只从 memory 中移除这个 state,NLL 几乎不变;移除对应 residual 后,NLL 却增加了约 1.04–1.65 nats/row。原来模型一直在通过 residual 使用这份信息。以后再做模块消融,就得把这些出口分别检查,否则很容易误判一个模块有没有用。[^diagnostics] Purshow 的 *Beyond Residual Connection* 给了我不少启发,尤其是从读写的角度看网络:每层读取什么、结果写到哪里、旧信息怎样继续传递。后面的 direct state access 和 residual correction 设计都受到了这种思路的影响,具体架构则是根据 R1 的实验重新设计的。[^purshow] 输出层也遇到过问题。展开早期双手 interaction 的公式后,我们发现它无法随 context 自由切换对 `aa / bb` 和 `ab / ba` 两组合法组合的相对偏好。输出形式已经限制了它能表达的关系,继续加强 encoder 也无济于事。后来改成 context-dependent signed bilinear coupling,双手联合动作的偏好才可以随当前历史改变。[^objective] 时间输入这边,同样相隔几个 rows,可能是一次密集换手,也可能已经过去几秒,单看 event index 很难分辨。我们先把真实 source events 与 synthetic boundary markers 分开,再引入 physical time、relative pace 和多尺度时间坐标,尝试让局部 kernel 同时依赖 elapsed time 与两端 actions。早期 time-action conv 就是这样来的,希望时间能直接参与局部动作的组合。[^time-local] Sampling 和 loss 也需要放在一起考虑。怎样抽 section、窗口多长、每个窗口等权还是每个 row 等权,都会改变实际优化的分布。尤其在 teacher forcing 下,越靠后的预测能读到越多真实 prefix,长窗口的 mean-row NLL 更低,也可能是这些额外信息带来的。因此我们同时记录 sequence NLL、mean-row NLL 和不同预测位置的结果,并保留采样分布与归一化方式,方便判断改善究竟来自哪里。[^objective] 这一阶段形成了一个习惯:检查某种关系时,分别看输入有没有提供相关信息、输出形式能否表达、预测通过哪条路径使用它,以及训练任务有没有给出足够的监督。几个问题要分开验证,单个 loss 很难说明全部情况。 ### 从 reconstruction 到 continuation 接下来要让模型续写。此前的 masked reconstruction 能看到目标片段前后的 context,生成时则只有已经提交的历史,未来 actions 需要自己产生。旧模型的 autoregressive decoder 也需要调整。ActionReader 的 context 在解码前就已经确定,新产生的动作主要进入小 decoder 自己的 recurrent state,整套 representation pipeline 没有随着输出更新历史。主干对已生成排列的理解就停留在了解码之前。[^source-action] 我们移除未来 action context,暂时去掉 style readout,把生成改成一个完整的循环:**根据已提交历史预测当前 row,提交整行,更新状态,再预测下一行。** Occupancy、动作时钟、局部摘要和历史表示,都随实际提交的动作更新。[^causal-plan] Attention 也要进入这个循环,参与主干对历史的读取与组合。这里预测当前动作的 query 和提交之后写入的 content 需要分开;训练时还要检查梯度的截断位置,过去的表示可以截断,当前读取它的 projection 仍然需要训练。这些都要逐条检查信息与梯度路径,外层的 causal mask 只能解决其中的可见性问题。[^causal-attention] 中间尝试的完整 prefix replay 训练成本很高,我们也开始怀疑,看过这么长的历史究竟换来了多少有效监督。最后收缩成一个更小、依赖范围明确、能做 dense training 的 baseline,用相同 backbone 比较不同的 timing 与动作表示,这就是 Plain R1。[^r1-origin] 前面试过的模块只保留了一部分,诊断信息路径的方法则继续沿用。 ### R1 的输入条件 R1 当前不读取音频。它接收两部分条件:一段真实谱面开头,以及后续的 typed timing skeleton。 Seed 从谱面开头取到累计至少 30 个 note heads 的完整 row,一个 chord 中的多个 heads 分别计数,实际行数随排列变化。跨越 seed 边界的 LN 会连同原有 endpoint 一起提供,保留为完整对象;R1 新生成的 LN 则需要自己决定何时释放。[^r1-task] Timing 分为候选时刻集合 `R`,和其中要求至少产生一个 head 的子集 `H`。在 `H` 上,模型决定哪些 lanes 按下、是 TAP 还是 LN,以及其他 lanes 是否同时释放;在非 `H` 的候选点,可以释放,也可以不产生新 event。后缀的真实 lane、chord size、TAP/LN 类型和新 LN 配对都不提供给模型。[^r1-task] 区分 `R` 和 `H` 是因为源谱面的 event times 同时包含 heads 和 releases。如果模型把原来的一条 LN 改成 TAP,稍后的源 release 时刻就可能没有东西可释放。旧任务要求那个位置非空,模型就会被迫再产生 head。把必须起音的位置和可供释放的位置分开之后,模型就可以改变动作选择,同时保留给定的起音结构。[^task-review][^r1-task] 这一轮先用给定的 timing information 集中检验 choreography。后续再接入 timing module,由它从音乐中提供兼容的时间条件。 ### Final R1 architecture 下面是这一轮保留的 `response6.75M` 版本。这里的 `6.75M` 指训练累计的 source-onset exposures,模型本身仍然只有约 3.08M 参数。图中的实线是当前一步的计算,虚线表示 commit 之后对下一步历史的更新。[^r1-results] ![[mermaid-diagram (1).png]] 所有分支共同给完整 row distribution 评分,最后一次抽样同时选出四轨的动作。Seed residual 接在局部历史与 exact query 的融合结果上,landmark attention 随后读取更早的组织;得到的双手表示同时送入基础评分和各个修正分支。[^final-model] #### 精确状态与 legality mask 哪些 lanes 正在被 LN 占用、它们何时开始、距离最近一次 attack 或 release 过去了多久,都能从已提交历史精确计算出来。我们将它们单独 replay,让有限宽度的 hidden state 腾出容量学习排列关系。[^r1-state] 这些状态会同时送给 mask 和预测分支。比如一个 lane 刚刚释放,和已经空闲很久,虽然都允许 TAP,再次按下时的手感却不同。Exact-state MLP 因此与历史 encoder 并行,在 readout 时融合,让精确状态直接参与动作评分。[^final-model][^r1-state] Mask 负责硬约束:已经占用的 lane 上不能重新起音,尚未开启的 LN 不能释放,给定的 seed endpoint 必须遵守,还要保证下一次 required onset 有动作可选。谱面终点需要完成 closure,训练窗口结束时则保留当时的持有状态。[^r1-task] #### 多尺度时间输入 R1 保留真实时间差,并使用覆盖 8–4,096 ms 的多尺度 smooth basis,加上 bounded seconds、`asinh` 和 availability。这样既能给短间隔保留不同的敏感尺度,也不会让长曲目的绝对年龄直接压过其他输入。未知时钟有独立的缺失标记,不会被误写成“刚刚发生,间隔为零”。[^features][^time-basis] 当前 query 还能读取接下来 16 个候选时刻的 gaps 与 roles、多个时间范围内的候选与起音数量,以及后续长空隙的描述。这些信息全部来自给定的 timing,未来 actions 仍由模型生成。有了这些输入,模型在开启或维持 LN 之前,就能考虑后面是持续密集起音,还是即将进入一段长空白。[^features] 早期的 time-action kernel 帮助我们研究了这些依赖。Final R1 改用 causal convolution 处理动作与多尺度时间输入,精确时钟和候选动作后果则直接参与评分。[^time-local][^r1-state][^consequence] #### 局部组合、双手对称与联合输出 Plain R1 的历史主干是 8 层 dilated causal convolution,宽度 128,dilation 从 1 增长到 128。每层通过 gated convolution 和 feed-forward residual 更新表示,形成明确的 511-row content dependency。训练时可以并行计算,生成时可以逐行维护 cache;我们检查两条路径在相同历史下的一致性。[^tcn][^r1-state] 511 行限定了局部主干能读取的 event span,实际覆盖多久取决于谱面密度,也未必对应完整乐句。这部分主要保留近期排列的细节,更早的历史交给后面的 memory 分支。[^r1-state] 左右手使用共享参数的 canonical 视角,各自保留 own / other hand 以及 outer / inner 角色,每个视角都能看到完整四轨 row。最终 joint head 为每手的 16 种二轨动作组合评分,再用 context-dependent bilinear coupling 连接双手,得到 256 个完整四轨候选的联合 logits。镜像会交换手的视角,两边共享学习到的关系,具体谱面中仍然可以有不同的用力分配。[^features][^final-head] 同手组合、双手配合、某一侧正在 hold 而另一侧继续 TAP,都由这个 joint head 一起预测。 ### Persistent seed 与 long memory 随着续写进行,局部历史会逐渐被模型自己的输出替换,原始 seed 也会离开 receptive field。为了让模型一直能直接读到开头,我们增加 persistent seed conditioning:用共享 TCN 编码原始 seed,对有效输出做 masked mean,在每一步通过 residual 提供这两个 hand vectors。[^memory] Seed 保留这次续写的起点,long memory 则记录之后的发展。两者需要同时可读:一段安静的 intro 后面可能接着热闹的 chorus,模型需要结合开头和后续历史继续编排。[^memory] LandmarkMemory 用另一条 shared-hand GRU 顺序读取全部已提交的 physical rows,每经过 64 个 head rows 保存一个 landmark。当前表示通过 attention 读取此前的 landmarks,再以 residual 加回主路径。这样既有局部 TCN 的细节,也能重新访问更早的组织。Landmark 的记录位置由固定行数决定,这里没有做乐句边界识别。[^memory-code] 训练时,这条分支用当前参数重算完整历史,让梯度进入更早的 history encoding;生成时使用逐行更新的 cache。相应的代价是,landmark bank、恢复所需的原始历史和读取成本都会随曲长增长。Final R1 只有局部分支的范围固定,整个模型的内存和计算成本仍然受曲长影响。[^memory] 那轮 memory candidate 减少了部分短间隔诊断事件,但还是生成过持续约 18 秒、大量重复四键 TAP 的段落。有了更早的历史可读,模型仍然会卡在这样的排列里,接下来需要让它在自己的失败历史上学习怎样退出。[^memory] ### Head、release 与 action consequence 的后续修正 Plain R1 已经能生成一些值得继续推进的排列,long rollout 中的局部 collapse 也逐渐暴露出来。Jack、Trill 和重复 chord 都是常见表达,直接限制重复会连带伤害这些排列。我们需要查看具体失败片段,找到被卡住的动作分配,再决定开放哪些修正。 最初试过用生成失败样例做全参数 recovery,连续重复确实大幅减少了,检查过的 LN passages 却也丢失了原本的重叠持有、独立释放和 TAP 配合。那些需要精细协调的 LN 关系一起减少了。[^recovery] 因此后续改成更有限的 residual corrections,尽量保留已经学会的组织,只开放与当前失败有关的自由度。 HeadRouting 为 16 种 binary head masks 增加评分,调整哪些 lanes 按下。同一 head mask 下,TAP 和 LN_START 获得相同修正,因此在冻结原模型、固定当前历史时,该 head-mask family 内部的条件分布保持不变。这样可以调整 lane group 与 chord size,同时保留其中已经学到的 TAP/LN 偏好。[^recovery] 后来检查一首长曲目时,我们又看到三条 lanes 长时间被 LN 占据,剩下一条 lane 承担连续起音。几个候选的释放选择不同,却共享同一个 head mask,HeadRouting 给它们加的分数完全一样,无法调整释放哪条 lane 的相对概率。[^recovery] 于是增加 ReleaseRouting,按 close mask 修正完整动作评分,让 release 决策也能被单独训练。它保留同一 close-mask family 内部的条件分布,通过学习调整退出持有状态的概率,LN 本身没有额外设置最大长度。两种 routing 对条件分布的保留都以同一个当前状态为前提;抽样动作一旦变化,后面的生成轨迹也会跟着变化。[^recovery][^r1-results] RowConsequence 进一步计算候选动作会留下的状态。同一个 occupancy 下,几个合法候选可能给后续留下很不同的活动空间。`frontier2` 为每个候选计算 post-action occupancy、当前 head 距离上一 head / release 的间隔、release 时的 LN age,以及这些时钟推进到下一次 required onset 时的状态。它还描述被占用 lane 最早可用的释放机会,并提供后面两次 required onsets 的时间信息。[^consequence] 这里的释放机会只表示 timing 允许何时释放,实际选择留给后续生成。这个分支根据当前状态和已知 timing 计算直接后果,不读取未来谱面,也不对每个候选做完整的未来 rollout。哪些 lanes 的时钟改变了、哪些持有会继续、接下来有多少周转时间,都作为候选相关的输入直接参与评分,减少网络需要自行推导的部分。[^consequence] 最终可以把评分写成: $ s(a)=s_{\mathrm{base}}(a) +r_{\mathrm{head}}(m_h(a)) +r_{\mathrm{release}}(m_r(a)) +r_{\mathrm{consequence}}(a). $ 其中 `m_h` 与 `m_r` 分别是完整 row 的 head mask 与 close mask;所有分数都依赖当前可读的 context,最后一起经过同一个 support mask 和联合归一化。新增 residual 的最后一层从零开始,最初不会改变 parent model 的分布,再通过训练逐步修正。[^final-model][^memory][^consequence] 实现上还有一处节省开销的处理。四轨共有 256 个完整 rows,单轨只有四种 actions,我们先投影 16 个 lane/action descriptors,再按候选组合,就可以保留候选相关的计算,同时省去为每个完整 row 重复构造原始 feature tensor 的开销。[^consequence] ### 在生成历史上训练 recovery R1 的基础训练仍然是 source reconstruction。给定 timing 和真实 prefix,在当前 row 提交之前计算 likelihood,再提交真实动作。生成时则把提交对象换成抽样结果。训练窗口里包含的 release factors 不能丢掉;最终的 bounded R1 将这些 likelihood terms 一起求和,再用选中的 source-onset 数归一化。这个分母与旧的 equal-block mean-row loss 不同,代表了新的训练目标。[^r1-training] 模型自己生成的历史会偏离 source prefixes,所以最后的 response recovery 使用固定的 TRAIN generated trajectories,从中构造当前 row 与后面两次 required onsets 的有限 response proxy。未来部分取乐观的最小负担估计,这个 proxy 与人类难度评分的语义还有距离。选择替代动作时优先保留 head 数和 LN_START 数,减少模型通过少放音符降低 cost 的机会。[^recovery][^r1-results] 这一轮冻结已有模型,只训练新的 row residual,同时保留 source CE 和对 parent distribution 的 KL anchor。我们希望模型学会退出失败排列,并用这两个约束保留正常 source states 上已有的分布。92 条机器 preference 中,有 81 条保留了原动作的 head 与 LN_START 数量。[^r1-results] 更早的一轮只增加 action-consequence features,没有达到预先声明的 native-generation 改善标准。这次同时调整了信息路径、生成状态上的监督、冻结范围和 source anchor,才得到有效的候选;各部分单独贡献多少,还需要进一步拆开验证。[^memory][^r1-results] ### 配对生成结果与开发审阅 最后一轮配对开发评估包含 24 个 validation musical groups,每个使用两个 generation seeds,共 48 份完整输出。下面比较 **Release6.5M → Response6.75M** 这一步,两组都使用原生 temperature 1 和相同的可行性约束。从 Plain R1 到最终版本的完整 ablation 还没有包含在这组比较里。[^r1-results] | 诊断项 | Release6.5M | Response6.75M | | --- | ---: | ---: | | 距同 lane 上一次 head 或 release 不足 20 ms 的 heads | 45 | 13 | | 不足 30 ms 的 heads | 160 | 70 | | 不足 40 ms 的 heads | 442 | 178 | | 同样三条 holds 不变时,最长连续 required-onset rows | 4 | 2 | | 始终包含某一固定 lane 的最长连续起音段,按 rows 计 | 14 | 18 | | 生成的 heads 总数 | 166,272 | 177,531 | 在这轮配对检查中,短间隔负担下降,heads 总数增加,说明这些改善发生在生成密度更高的情况下。最长 fixed-lane run 则从 14 增加到 18,对应片段是持续约 2.36 秒、具有变化的 chord-Jack。这个例子也说明,重复长度需要和实际排列一起看,单看数字很难判断质量。[^r1-results] 所有 48 份输出都通过了独立的 mechanics 检查与 `.osu` export / reparse,合计覆盖 120,416 个 required suffix onsets。这轮完整生成在 M5 Air 的 CPU 单线程上耗时 461 秒,采样到的 peak RSS 为 613 MB。 开发审阅还检查了长达约 17.5 分钟的 continuation,在早、中、晚选取的 scopes 中仍能看到组织变化;独立 LN/TAP passages 也保留了错开的 starts、releases、不同持有长度,以及其他 lanes 的 TAP 参与。审阅时用 Beatmap Lens 和已有 human examples 校准判断,采用未盲化的抽段检查,逐秒人工检查和玩家盲测尚未进行。[^r1-results] 目前同一个模型已经在不同 source difficulty 和长曲目条件下接受检查,结果让我愿意继续推进。不过难度控制还需要单独做,最后一轮仍有明显的 difficulty drift:约 3.14★ 的 source 条件生成了约 4.24★ / 4.38★ 的结果。接下来除了继续检查长程组织,还需要验证怎样才能按用户要求稳定生成某个难度。[^r1-results] ### 接下来 回头看,Mapperatorinator 将丰富的谱面事件放入 audio-conditioned encoder-decoder,用已有 event tokens 衔接生成窗口;MuG Diffusion 在带有起音、holding 与时间偏移的表示上做音频条件生成。R1 则先固定时间条件,集中研究四轨 choreography,把完整双手动作、精确状态、真实时间、不同范围的历史和候选动作后果分别接入预测。[^mapper-architecture][^mapper-generation][^mug][^final-model] 这一路做下来,我觉得有用的经验是把可以直接计算的东西交给明确的模块:occupancy 和动作时钟可以精确维护,legality 可以用规则保证,模型就能把更多容量用在排列关系上。需要从数据中学习的组织仍然留给模型,memory 等模块则要配合相应训练,才能知道它究竟有没有被用起来。 现在这套约 3M 参数的 choreography 系统已经可以逐项检查、修改和复现。和 Mapperatorinator、MuG Diffusion 的质量比较还需要在匹配条件下评测,单凭这轮结果也无法判断不同模型规模的优劣。 接下来要把 timing module 接进来,研究起音和释放候选的分布、自主 seed,以及玩家实际能用的控制方式。R1 已经给了我们一个可以拿来玩、检查和继续改进的模型,之后就沿着这些具体问题,继续实现最初想做的 **Play the music around you**。 [^r1-origin]: [R0 / R1 / O1 的共同 baseline 与初始参数量](https://github.com/Pulsefield/Pulsefield-model/blob/c4edd10bb486c0cbfb88564f6d4920e14edbbb75/artifacts/agent-notes/proposed/2026-09-18-bounded-typed-time-three-arm-comparison.md)。 [^r1-results]: [Response6.75M:配对生成、资源与质量审阅记录](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/docs/research/bounded_typed_continuation.md#native-response-recovery-result-675m)。 [^style-model]: [Scoped style model:assessment 与 evidence selector](https://github.com/Pulsefield/Pulsefield-model/blob/7bf4ff2cfc10131144830d01ee0136d5b6cbab34/docs/research/scoped_style_training.md)。 [^style-failure]: [Style probes:结果与 postmortem](https://github.com/Pulsefield/Pulsefield-model/blob/7bf4ff2cfc10131144830d01ee0136d5b6cbab34/docs/research/scoped_style_probe_postmortem.md)。 [^representation]: [从 style classification 转向 source-action representation](https://github.com/Pulsefield/Pulsefield-model/blob/7bf4ff2cfc10131144830d01ee0136d5b6cbab34/docs/research/source_action_representation_directions.md)。 [^source-action]: [Source-action:composition 与 representation access](https://github.com/Pulsefield/Pulsefield-model/blob/7bf4ff2cfc10131144830d01ee0136d5b6cbab34/docs/research/source_action_stage2.md)。 [^diagnostics]: [H residual、memory 与其他路径的干预诊断](https://github.com/Pulsefield/Pulsefield-model/blob/c4edd10bb486c0cbfb88564f6d4920e14edbbb75/artifacts/agent-notes/proposed/2026-09-15-source-action-network-diagnostics.md)。 [^purshow]: [Purshow:Beyond Residual Connection](https://github.com/Purshow/Purshow_Notes/blob/60e0ca933877686cc13fe704928f0f81834227be/Beyond_Residual/CN/Beyond_Residual_CN.md)。 [^objective]: [联合输出表达能力、sequence / row NLL 与采样风险](https://github.com/Pulsefield/Pulsefield-model/blob/7bf4ff2cfc10131144830d01ee0136d5b6cbab34/docs/research/source_action_objective.md)。 [^time-local]: [Source-action:time coordinates 与 local composition](https://github.com/Pulsefield/Pulsefield-model/blob/7bf4ff2cfc10131144830d01ee0136d5b6cbab34/docs/research/source_action_time_local.md)。 [^causal-plan]: [Oracle-time causal continuation 计划](https://github.com/Pulsefield/Pulsefield-model/blob/7bf4ff2cfc10131144830d01ee0136d5b6cbab34/docs/research/Pulsefield_oracle_time_causal_continuation_plan.md)。 [^causal-attention]: [Query / content、temporal memory 与梯度路径](https://github.com/Pulsefield/Pulsefield-model/blob/7bf4ff2cfc10131144830d01ee0136d5b6cbab34/docs/research/Pulsefield_oracle_time_causal_continuation_plan.md)。 [^r1-task]: [R1 的条件与预测任务](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/docs/research/bounded_typed_continuation.md#conditions-and-prediction-tasks)。 [^task-review]: [无类型非空事件行的支持集问题](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/docs/research/oracle_time_expert_question.md)。 [^final-model]: [Final R1:readout 与联合评分的实际实现](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/src/pulsefield_model/research/bounded_typed_continuation/model.py#L243-L322)。 [^r1-state]: [Exact state 与 bounded learned context](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/docs/research/bounded_typed_continuation.md#exact-state-and-bounded-learned-context)。 [^features]: [R1:permitted facts、timing 与 content features](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/src/pulsefield_model/research/bounded_typed_continuation/features.py)。 [^time-basis]: [Physical-time basis 与 availability](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/src/pulsefield_model/research/oracle_time_continuation/features.py#L22-L47)。 [^consequence]: [RowConsequence / frontier2 实现](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/src/pulsefield_model/research/bounded_typed_continuation/consequence.py)。 [^tcn]: [FiniteTemporal 与 causal residual blocks](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/src/pulsefield_model/research/bounded_typed_continuation/temporal.py)。 [^final-head]: [JointHead:hand unary 与 signed bilinear coupling](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/src/pulsefield_model/research/bounded_typed_continuation/model.py#L76-L97)。 [^memory]: [Persistent seed、landmark memory 与候选后果实验](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/docs/research/bounded_typed_continuation.md#r1-persistent-original-seed-conditioning)。 [^memory-code]: [LandmarkMemory:写入、attention read 与因果可见性](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/src/pulsefield_model/research/bounded_typed_continuation/long_memory.py)。 [^recovery]: [Native-prefix recovery、head routing 与 release routing](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/docs/research/bounded_typed_continuation.md#native-prefix-recovery-training)。 [^r1-training]: [Source projection、训练与原生生成](https://github.com/Pulsefield/Pulsefield-model/blob/db0a9b8b1a24d481e18bbb40e1a1266bf885756b/docs/research/bounded_typed_continuation.md#source-projection-and-leakage-checks)。 [^mapper-architecture]: [Mapperatorinator:tokenization 与 architecture](https://github.com/OliBomby/Mapperatorinator/blob/main/README.md#model-architecture)。 [^mapper-generation]: [Mapperatorinator:seamless long generation](https://github.com/OliBomby/Mapperatorinator/blob/main/README.md#seamless-long-generation)。 [^mug]: [MuG Diffusion:osu!mania 表示](https://github.com/Keytoyze/Mug-Diffusion/blob/master/mug/data/convertor.py#L209-L310)。