diff --git a/docs/design/03-rhythm-rebuild.md b/docs/design/03-rhythm-rebuild.md index f804215a..72439985 100644 --- a/docs/design/03-rhythm-rebuild.md +++ b/docs/design/03-rhythm-rebuild.md @@ -183,3 +183,12 @@ idle → intro(语音) → tut(3 步教学) → select(选曲,二周目起) 6. 语音全表见 03-rhythm-voice-ids.md;现版 `voice/welcome/rules/tut_start/tut_done` 弃用,`grade_*`/`restart` id 沿用但文案变更(重录)。 7. 结算画面:单击=再来一遍(同曲),右滑=下一首(若已解锁),下滑=回大厅。 8. 更新 `web/rhythm/README.md`(玩法、手势表、进度结构)。 + +--- + +> **取代指针(2026-09-14,24 号裁定)**:本文档(及 03-rhythm-voice-ids.md)的 +> 施工依据地位已被 [24-rhythm-drummer.md](24-rhythm-drummer.md) + +> [24-rhythm-voice-ids.md](24-rhythm-voice-ids.md) 取代——《节拍行者》重构为 +> 屏幕分区鼓手模式(6 区打击乐 + 无鼓伴奏,跟随领奏核心/评级/解锁/BPM 精确性 +> 延续)。本文仅作历史档案保留,正文一字不改;13 号文档 §4 rhythm 断言段 +> 同理冻结,新断言见 24 号文档 §11。 diff --git a/docs/design/24-rhythm-drummer.md b/docs/design/24-rhythm-drummer.md new file mode 100644 index 00000000..56d1d913 --- /dev/null +++ b/docs/design/24-rhythm-drummer.md @@ -0,0 +1,416 @@ +# 24 · 《节拍行者》鼓手模式重构设计(rhythm / Drummer Mode) + +> 用户裁定:现有模式「不错,保留」——跟随领奏的合拍玩法核心、S~C 评级、曲目解锁制、 +> BPM 精确性延续;整体重构为**屏幕分区打击乐**:屏幕划分区域,每区一种打击乐音效, +> 玩家作为乐队鼓手与无鼓伴奏音乐合拍。本文取代 [03-rhythm-rebuild.md](03-rhythm-rebuild.md) +> 的全部施工依据(03 号文档仅作历史档案,文末有取代指针;其 rhythm 判定好评口径 +> 「PERFECT ±0.15 / GOOD ±0.35」经 live 校准,本文沿用)。 +> +> 语音台词全表在 [24-rhythm-voice-ids.md](24-rhythm-voice-ids.md)。断言清单在 §12。 + +--- + +## 0. 可行性勘察决策(阶段零结论) + +### 0.1 引擎手势事件是否携带坐标? + +**不携带。** `web/engine/gesture.js` 的 `onGesture(g)` 只传手势名字符串, +触摸坐标只在引擎内部用于滑动/点按判定,回调时丢弃。 + +### 0.2 两条路线的权衡 + +| | (a) 引擎扩展:第二参数透传坐标 | (b) 游戏层自建 touch 监听 | +|---|---|---| +| 改动面 | gesture.js 每个 onGesture 调用点补 `{x,y}` 第二参;docs/web-engine-api.md 同步 | 游戏层加 touchstart 记录坐标 | +| 既有游戏影响 | **零**——全部 15 款游戏的回调都是单参 `(g) =>`,多余实参被忽略,手势分类/震动/时序逐字节不变(同 opts.pan / immediateTap / tapcancel 先例) | 零(不碰引擎) | +| 键盘通道坐标 | **免费**:空格虚拟指针本就走 `handleDown(keyCenter)`,天然拿到元素中心坐标 | 无法区分键盘 tap 与残留触摸坐标,需自行加时效启发式(脏) | +| 保持的引擎福利 | haptics 自动震动、多指守卫、tapcancel、长按快进 window 事件、双击状态机、键盘三通道全部保留 | 全部丢失,需在游戏层重写(双击窗/tapcancel/快进事件) | +| 代价 | 全量回归:15 游戏 playtest + guide.test + engine-voice + engine-input + docs 同步(铁律) | 回归只需 rhythm 自身,但收获一个与引擎并行的第二输入状态机(长期维护债) | + +### 0.3 决策:路线 (a),引擎后向兼容扩展 + +- `attachGestures` 的全部 `onGesture(...)` 调用补第二实参 `{x, y}`(视口像素 + clientX/clientY 语义):tap 类事件(含 immediateTap 序曲 tap、双击改判、tapcancel) + 与 longpress/longpressend 传**按下点**;四向滑动传**滑动起点**(出手瞄准位置); + 键盘通道传虚拟指针坐标(`keyCenter()`,方向键滑动同样补)。 +- 缺省行为逐字节不变:不加 config 开关、不改任何判定阈值与震动;回调签名扩为 + `(g, info?: {x, y})`,单参回调零感知。 +- 回归义务(已执行,见 research/rhythm-drummer/round0/engine-regression.md): + 15 游戏 playtest + web/harness/guide.test.mjs + engine-voice.test.mjs + + engine-input.test.mjs 全绿,docs/web-engine-api.md §gesture 同步。 + +### 0.4 打击乐音色:程序合成 + +六区音色 + accent 全部 numpy 程序合成(`tools/gen_rhythm_syn.py`, +gen_cipher_syn.py 同款工艺):可精确控制起音/衰减(盲玩靠起音对拍)、 +零授权问题、零网络延迟、与 BGM 调性关系无关(打击乐无调性)。 + +### 0.5 BGM:无鼓伴奏是硬需求 + +玩家就是鼓手——BGM 含鼓则叠打混乱。Mureka instrumental 生成时 prompt +显式 `no drums, no percussion`,`tools/bpm_precise.py` 梳状扫描实测 BPM +(节奏类硬性要求,误差 >0.5 则 atempo 拉伸,R3 track90 先例), +无缝循环后处理沿用批次 1-4 工艺(强 onset 起点 → 整数小节裁剪 → +首尾 3s acrossfade → loudnorm I=-20 → ogg q3 + m4a 128k)。 + +--- + +## 1. 概述与继承/舍弃清单 + +**一句话玩法**:屏幕是一面 3列×2行 的鼓组。系统(领奏)在无鼓伴奏上敲一小节 +节奏型,玩家在下一小节的相同拍点敲相同的鼓。曲子从 90 BPM 打到 120 BPM, +拿 B 级以上解锁下一首。 + +### 1.1 继承自旧作(用户裁定「保留」的部分) + +| 项 | 说明 | +|---|---| +| 跟随领奏 call & response 核心循环 | 领奏 1 小节 → 应答 1 小节,逐乐句推进 | +| 时值判定口径 | PERFECT ±0.15s / GOOD ±0.35s(live round1 §13 校准值,吸收蓝牙延迟) | +| 计分与评级 | Perfect=2/Good=1/Miss=0,rate = 得分/(2×总音符),S≥0.95 / A≥0.85 / B≥0.70 / C 其余;不掉血不中断,歌照唱完 | +| 曲目解锁制 | 曲目 n 拿到 B 级 → 解锁 n+1;localStorage + 已登录 PUT 同步 | +| BPM 精确性 | 谱面/判定全部锚定 BGM 实测 BPM 网格(bpm_precise 实测 + atempo 修正) | +| 工程质量遗产 | AudioContext 预排程泵(40ms + LOOKAHEAD 0.6s)、BGM 手动 `src.start(t0)` 对网格、模态菜单断点续播、`_speakGuarded` 看门狗语音链、结算后 menu pad 看门狗、旁白单击跳过/长按快进豁免、guide 层全量接入(状态签名/gd_answer/gd_miss 归因/教学 DDA 降速/count-in 恢复) | + +### 1.2 舍弃/替换 + +| 旧 | 新 | 理由 | +|---|---|---| +| 手势→乐器映射(tap=kick、left=闭镲…6 手势 6 音色) | 屏幕分区→乐器(6 区 6 音色,tap 即敲击) | 用户裁定的新核心 | +| 滑动/双击作为音符类型(SWIPE_DELAY 0.15 / DOUBLE_DELAY 0.20 补偿、双击序曲撤销) | **全部音符 = 分区点按**,immediateTap 按下即判,零识别补偿 | 打击乐每次出手都是「敲一下」,滑动从音符类型降级为纯导航;双击=同区连击两下(两声都合法),**不再需要双击序曲撤销账簿** | +| BGM 含鼓组(track90/105/120 有鼓) | 无鼓伴奏 | 玩家是鼓手 | +| 下滑呼菜单的「down 音符临近」折中(live P0 加窗/复活窗一整套) | 下滑**无条件**呼菜单 | 音符全是点按,下滑永不与音符冲突;序曲 tap 仍出 ghost 音但判定时由 tapcancel 撤销 | +| 教学 T1-T3(手势记忆) | 分区心智模型教学(摸鼓认区 → 跟打) | 「摸到哪是哪」的建立是设计核心(§6) | +| 旧语音表 31 句 | 新表 41 句(+沿用向导 8 句 = 49 句双性别,配额见 §8) | 分区教学与区名播报需要新句 | + +--- + +## 2. 屏幕分区与音色映射 + +### 2.1 分区布局(3 列 × 2 行,纯听觉可辨是硬约束) + +``` +┌─────────┬─────────┬─────────┐ +│ TL 左上 │ TC 上中 │ TR 右上 │ 上排:清亮/高频 +│ 踩镲 │ 高嗵 │ 吊镲 │ pan: -0.6 / 0 / +0.6 +├─────────┼─────────┼─────────┤ +│ BL 左下 │ BC 下中 │ BR 右下 │ 下排:低沉/低频 +│ 军鼓 │ 底鼓 │ 低嗵 │ pan: -0.6 / 0 / +0.6 +└─────────┴─────────┴─────────┘ +``` + +**双轴心智模型**(教学的主话术): +- **列 = 声像**:左列在左耳、中列在中央、右列在右耳(opts.pan 引擎已支持)。 +- **行 = 音高**:上排清亮、下排低沉。 +- 在双轴之上,六件乐器音色本身两两不同(镲/嗵/鼓),盲玩家有 + 「声像 + 音高 + 音色」三重冗余线索。 + +### 2.2 分区 ↔ 音色 ↔ 声像总表 + +| 区码 | 位置 | 乐器 | sfx id | pan | 合成要点(起音硬、≤0.5s 残响) | +|---|---|---|---|---|---| +| TL | 左上 | 闭合踩镲 | `sfx/dm_hat` | -0.6 | 6-10kHz 高通噪声,0.06s 急衰减 | +| TC | 上中 | 高音嗵 | `sfx/dm_tomhi` | 0 | 200→140Hz 正弦扫频 + 轻皮噪,0.25s | +| TR | 右上 | 吊镲 | `sfx/dm_crash` | +0.6 | 4-12kHz 噪声 + 非谐波泛音列,1.2s 长尾 | +| BL | 左下 | 军鼓 | `sfx/dm_snare` | -0.6 | 200Hz 鼓皮 + 1-4kHz 带通噪声,0.2s | +| BC | 下中 | 底鼓 | `sfx/inst_kick`(沿用旧 id) | 0 | 130→45Hz 正弦扫频 + 触击瞬态,0.25s | +| BR | 右下 | 低音嗵 | `sfx/dm_tomlo` | +0.6 | 120→75Hz 正弦扫频 + 轻皮噪,0.3s | + +- 附加层:`sfx/dm_accent`(Perfect 叠加亮音,程序合成木鱼 1.5kHz 短音)。 +- 分区判定:`zoneOf(x, y)` 以 `#game-root` 的 `getBoundingClientRect()` 为界, + y 上半 = 上排、y 下半 = 下排;x 三等分 = 左/中/右。无布局环境(mock/harness) + 由调用方显式传 payload;payload 缺失时落 BC(中央家区,兜底不炸)。 +- **键盘平权**(游戏层 keydown,引擎键盘通道之外的补充,数字键引擎本就不拦截): + `Digit1/2/3/4/5/6`(及小键盘)= TL/TC/TR/BL/BC/BR,按下即发、零延迟; + 引擎空格 tap 落在元素几何中心 → 判 BC(「家」)。选曲/菜单/结算态数字键不生效 + (那些态沿用手势导航)。 + +### 2.3 领奏音色 = 玩家音色 + +领奏音符与玩家敲击走**同一批 sfx**(同音色同声像)——做对时听到自己在和 +「另一个鼓手」合奏,这是旧作好评的「演奏感」来源,保留。 + +--- + +## 3. 玩法循环与状态机 + +### 3.1 循环 + +``` +选曲 → 数拍导入(1 小节底鼓×4,含节拍轻震) → 逐乐句(领奏 1 小节 + 应答 1 小节 [+ 休止]) + → 过门/加花(领奏 1 小节密集加花 + 应答 1 小节) → 终句吊镲 → 结算评级 +``` + +### 3.2 状态机 + +``` +idle → intro(语音) → tut(分区教学) → select(选曲,二周目起) → countin → main(逐乐句/加花) → end(结算) + ↑_________________| +``` + +- 数拍小节由游戏排程 4 记数拍(BGM 无鼓,数拍只能靠鼓手自己)+ 每拍 [8] 轻震:正片用底鼓 inst_kick×4(guide.test G3 的 _sched 锚点,13 号冻结文档不可改),教学步用木鱼 click(dm_accent,guide.test G2 要求教学 _sched 里 kick=领奏音)。 +- 每曲第一个应答窗口前 1 小节落 `gd_answer` + turn earcon(旧作 G1 修正沿用)。 +- 模态菜单(下滑呼出):单击循环 [继续演奏 / 回大厅]、双击确认、上滑帮助、 + 下滑关闭;暂停真冻结(泵停、BGM 断点记 offset),恢复时全网格平移 + + 4 拍 count-in(guide full 档先播 `gd_count`,每局限 1 次,round3 G3 全套沿用)。 + +--- + +## 4. 谱面与难度曲线 + +### 4.1 记号 + +`拍:区码`,1 小节 4 拍,`x.5` = 八分反拍。乐句 = 领奏 1 小节 + 应答 1 小节; +加花 = 特殊乐句(fill 标记),领奏为八分音符密集型,最后一音恒为 TR 吊镲收束。 + +### 4.2 曲目一《启程》90 BPM(教学曲,8 句 + 终曲加花,≈65s) + +| 句 | 领奏节奏型 | 音符数 | +|---|---|---| +| 1 | `1:BC, 3:BC` | 2 | +| 2 | `1:BL, 3:BR` | 2 | +| 3 | `1:BC, 2:BC, 3:BL` | 3(双击底鼓引入同区连击) | +| 4 | `1:BL, 2:BR, 3:BC` | 3 | +| 5 | `1:BC, 2:BL, 3:BC, 4:BR` | 4 | +| 6 | `1:TL, 3:BL, 4:BC` | 3(上排首次进正片) | +| 7 | `1:BC, 2:TL, 3:BC, 4:TR` | 4 | +| 8 | `1:BL, 2:BR, 3:TL, 4:BC` | 4 | +| 终曲加花 | `1:BC, 1.5:BC, 2:BC, 2.5:BL, 3:BL, 3.5:BC, 4:TR` | 7(全曲唯一反拍,仅限加花) | + +前 4 句应答后各休 1 小节,其余不休。 + +### 4.3 曲目二《追风》105 BPM(10 句 + 中途加花 + 终曲加花,≈80s) + +| 句 | 领奏节奏型 | 音符数 | +|---|---|---| +| 1 | `1:BC, 3:BR` | 2 | +| 2 | `1:BL, 2:BL, 3:BC` | 3 | +| 3 | `1:TL, 3:BL, 4:BR` | 3 | +| 4 | `1:BC, 2:BL, 3:BC, 4:TL` | 4 | +| 5 | `1:BC, 2.5:BR, 3:BC` | 3(首个反拍) | +| 中途加花 | `1:BL, 1.5:BL, 2:BC, 2.5:BC, 3:BR, 3.5:BR, 4:TR` | 7 | +| 6 | `1:BL, 2:TL, 3.5:BR, 4:BC` | 4 | +| 7 | `1:BC, 2:BL, 3:BR, 3.5:TL` | 4 | +| 8 | `1:BR, 1.5:BL, 3:BC, 4:TR` | 4 | +| 9 | `1:BC, 2.5:TL, 3:BC, 4:BL` | 4 | +| 10 | `1:BL, 2:BR, 3:TL, 4:BC` | 4 | +| 终曲加花 | `1:BC, 1.5:BL, 2:BC, 2.5:BL, 3:BC, 3.5:BR, 4:TR` | 7 | + +前 2 句应答后各休 1 小节;第 5 句与中途加花之间不休。 + +### 4.4 曲目三《星爆》120 BPM(10 句 + 中途加花 + 终曲加花,≈75s) + +| 句 | 领奏节奏型 | 音符数 | +|---|---|---| +| 1 | `1:BC, 2:BL, 3:BC, 4:BL` | 4 | +| 2 | `1:TL, 2:BL, 3:TL, 4:BC` | 4 | +| 3 | `1:BR, 2:TR, 3:BR, 4:BL` | 4 | +| 4 | `1:BC, 2.5:TL, 3:BC, 4:BR` | 4 | +| 5 | `1:BL, 1.5:BR, 2.5:BL, 3:BC` | 4 | +| 6 | `1:BC, 2:TL, 2.5:BL, 3:BC, 4:TR` | 5 | +| 中途加花 | `1:TL, 1.5:TL, 2:BL, 2.5:BL, 3:BC, 3.5:BC, 4:TR` | 7 | +| 7 | `1:BR, 2:BC, 3:BL, 3.5:BR, 4:TL` | 5 | +| 8 | `1:BC, 1.5:BC, 2.5:BL, 3:BC, 3.5:TL, 4:TR` | 6(高潮句) | +| 9 | `1:BL, 2:BC, 2.5:BR, 3:TL, 4:BL` | 5 | +| 10 | `1:BC, 2:BR, 3:BC, 3.5:TL, 4:BL` | 5 | +| 终曲加花 | `1:BC, 1.5:BL, 2:BC, 2.5:BL, 3:BC, 3.5:BC, 4:TR` | 7 | + +全曲不休。 + +### 4.5 难度曲线规则(改谱遵守) + +1. 乐句长度:曲目一 2→4 音、曲目二 2→4 音、曲目三 4→6 音;同曲单调不减 + (终曲加花不计入)。 +2. 反拍(x.5):曲目一**只允许出现在加花**;曲目二第 5 句起、普通乐句每句至多 + 1 个反拍音;曲目三不限。加花为八分音符基底,不受此限。 +3. 同一分区连续出现 ≤2 次(同区双击是合法手法,三连击禁止——盲玩极限)。 +4. 同一乐句内分区种类 ≥2(纯底鼓句仅允许曲目一句 1)。 +5. 相邻反拍连续位禁止左列↔右列大跳(`BL↔BR` 相邻反拍只允许曲目三)—— + 声像跳变过快在耳机上易混(旧作 §3.5-6 同款)。 +6. 加花尾音恒为 TR 吊镲(收束感 + 吊镲长尾盖住应答结束)。 + +--- + +## 5. 时值判定与计分评级 + +```js +const PERFECT = 0.15; // Perfect 窗口 ±150ms(live round1 §13 校准) +const GOOD = 0.35; // Good 窗口 ±350ms(吸收蓝牙延迟) +const LOOKAHEAD = 0.6; // 调度前瞻(秒) +const PUMP_MS = 40; // 调度泵周期 +``` + +- **判定时刻 = 触摸/按键时刻**:immediateTap 按下即发 tap(零补偿); + 键盘数字键 keydown 即发(零补偿)。旧作的 TAP_DELAY/SWIPE_DELAY/DOUBLE_DELAY + 补偿与旧引擎闩锁、双击序曲撤销、超时复活窗**整体退役**(没有滑动/双击音符了)。 +- 归属:同分区最近未判定音符 |dt|≤GOOD → 出手即播该区音色(pan 随行), + Perfect 叠 `sfx/dm_accent`;|dt|≤GOOD×0.5 的异分区音符被敲 = 判 Miss + (错鼓);窗口外 = 自由演奏(出声音不判定)。 +- **序曲 tap 治理**(immediateTap 代价,旧作账簿简化版):每次按下先发 tap + (出声+可认领),转滑动/长按/触摸取消时引擎补 `tapcancel` → 撤销该 tap 的 + 认领(声音收不回当 ghost 音,判定/统计必须更正且不计 Miss)。双击不改判 + 账簿——两下敲击各自独立成立。 +- Miss 超时:expected + GOOD + 0.10s 未应答 → 闷镲反馈音 + (`sfx/dm_hat` 降调 0.5× 复用,零新资产)+ error 震动。 +- 长按 = 死输入:Miss 反馈音 + error 震动,不判定不归属(演奏期长按快进 + `setRateBoostEnabled(false)` 豁免照旧)。 +- 计分/评级/连击:Perfect=2 / Good=1 / Miss=0;rate = 得分/(2×总音符); + S≥0.95 / A≥0.85 / B≥0.70 / C 其余;16 连击 success 震、乐句全 Perfect + [20,40,20]、结算 S/A success 震,全部沿用旧作。 + +--- + +## 6. 教学编排(分区心智模型,设计核心) + +蒙眼玩家如何建立「摸到哪是哪」?分四步,全部零惩罚(教学不计分、不出 +Miss 音),单步重教 ≤2 次(第 2 次失败 → 领奏降速 25% 重演一次 → 再败跳过, +旧作 G2 DDA 沿用): + +| 步 | 内容 | 机制 | +|---|---|---| +| T0 摸鼓认区 | `tut_map` 讲双轴模型(横三列=左中右声像,竖两行=上亮下沉)→ `tut_probe` 进入**探索模式**:随便摸,敲到哪区就播哪区音色 + 区名(`zone_*` 六句) | 摸到 ≥3 个不同区自动进 T1(每区名每轮教学只播一次,重复摸只出声不报名,防刷屏);**防呆:8s 无触摸自动进 T1**(摸过 1-2 区再宽限 8s),探索是礼物不是门槛——guide.test G2 等无触摸驱动依赖此路径 | +| T1 家区底鼓 | `tut_z_kick`:BC 是家。数拍 1 小节 → 领奏 `1:BC` → 下一小节应答 | 做错:纠正链(§7 归因)→ 重播领奏重试 | +| T2 左右下排 | `tut_z_sides`:BL 军鼓、BR 低嗵。领奏 `1:BL, 3:BR` → 跟打 | 同上 | +| T3 上排三音 | `tut_z_patt`:TL 踩镲、TC 高嗵、TR 吊镲。领奏 `1:TL, 2:TC, 3:TR` → 跟打 | 同上;通过后 `tut_done` 进曲目一 | + +- 教学在曲目一无鼓 BGM 上进行;每步领奏前都有 1 小节底鼓数拍(教学期 + count-in 不省)。 +- 下滑跳过教学(`tut_skip`)保留;教学 Miss 归因真话律沿用(没出手报 + 「这拍没接」gd_miss_none,不谎报早晚)。 +- 加花预告:`tut_fill`「最后有加花,跟着我连打。」在**曲目一终曲加花领奏 + 前 1 小节**播,每账号一次。 + +--- + +## 7. 连击/错拍反馈 + +| 时机 | 反馈 | +|---|---| +| 出手敲对 | 该区音色(pan 随行)+ Perfect 叠 accent | +| 敲错区(异分区音符半窗内被敲) | 该区音色照样出声(自由演奏语义)+ 闷镲 Miss 音 + error 震 + 播报实际敲中的区名(`zone_*`)+ `retry_wrong`「敲错鼓了,再听一遍。」(教学期)/ gd_miss 归因(正片,连 2 Miss 一次) | +| 早/晚(窗内同区但超 Perfect) | 不出错音(Good 照播),教学期事后归因 `retry_early`/`retry_late` | +| 没出手 | 音符超时闷镲 + error 震;教学归因 gd_miss_none「这拍没接」 | +| 16 连击 / 乐句全 Perfect / 解锁 / S·A 结算 | success 震 / [20,40,20] / success+[20,40,20] / success(沿用旧作) | +| 曲目过半 | `mid_song`(ducking 到 1/4 的旧作工艺沿用) | + +--- + +## 8. 语音配额(拥挤控制) + +打击乐密度高,语音是稀缺通道。全表 49 id 双性别(41 新 + 8 向导沿用), +运行时配额: + +1. **应答小节内零语音**(含 guide 插播——idle-only 语义天然保证)。 +2. 教学纠正:每次失败至多 1 条纠正链(归因 1 句 + 重听 1 句),同一步不叠加。 +3. 正片:连 2 Miss 才播 gd_miss,**同曲一次**;mid_song 每曲一次; + 加花预告 tut_fill 每账号一次。 +4. 探索模式区名播报每区每轮教学一次(重复摸只出声)。 +5. 结算链 = grade + [new_best_combo] + [unlock] + restart,无其他插话; + 结算 BGM 看门狗按链长缩放(旧作 P1 修复沿用)。 +6. 语音风暴保险丝(引擎 3s/5 次)是上届,游戏层不得绕过它连播同 id。 + +--- + +## 9. 音频资产规格 + +### 9.1 打击乐 sfx(程序合成,tools/gen_rhythm_syn.py 新建) + +7 件 one-shot:`inst_kick(沿用旧 id)/ dm_snare / dm_hat / dm_tomhi / dm_crash / +dm_tomlo / dm_accent`。QC 红线(gen_cipher_syn.py 同款):去直流 + 首尾 8ms cos² 淡化 + +编码后解码回读真峰值 ≤ -3dBFS(MDCT 过冲坑,超限自动降档重编)。起音必须硬 +(盲玩对拍靠起音);kick/snare/hat ≤0.3s,tom ≤0.4s,crash ≤1.3s,accent ≤0.1s。 + +### 9.2 BGM(3 首无鼓伴奏,Mureka ≤$0.30) + +| 曲 | BPM | prompt 特征 | +|---|---|---| +| track90《启程》 | 90 | bright optimistic pop rock backing, bass guitar + strummed acoustic + light synth pads, **no drums no percussion**, steady 90 bpm, A minor | +| track105《追风》 | 105 | driving energetic pop backing, punchy bass + rhythmic guitar chops + synth, **no drums no percussion**, steady 105 bpm, A minor | +| track120《星爆》 | 120 | high energy synth rock backing, aggressive bass + arpeggio synth + power chords, **no drums no percussion**, steady 120 bpm, A minor | + +- `bpm_precise.py` 实测:|偏差|>0.5 BPM 则 atempo 拉伸到标称(R3 track90 先例), + 成品复测记录台账。 +- 后处理:强 onset 起点 → 整数小节裁剪(时长为 4 拍整数倍)→ 末 2 拍与开头 + acrossfade(tri) 无缝循环 → loudnorm I=-20/TP=-2/LRA=11 → 44.1kHz → ogg q3 + m4a 128k。 +- 旧 track90/105/120(含鼓)删除;`bgm/menu`(Mureka 状态曲,无鼓问题不影响 + 玩法——menu pad 不与演奏同响)保留沿用。 + +### 9.3 语音 + +见 24-rhythm-voice-ids.md:41 新 id 双性别(Qwen3-TTS,flock GPU), +gen_voice_dual.py rhythm 段全量替换;`--check`(时长/峰值/两性离群)+ +`--convert`(ogg q3 + m4a 128k)。废弃 id(tut_1/tut_2/tut_3/retry_gesture +及旧 welcome/rules/retry_* 文案)的音频文件随 convert 后清理删除。 + +--- + +## 10. 进度存储 + +沿用旧作 schema 1 结构(`{tutorialDone, unlocked, grades, bestCombo, summary}`), +key `soundgame.rhythm.progress` 不变;PUT `/api/progress` 带 summary +(`《曲名》X级 · 已解锁N/3首` 取最高评级曲目)。解锁规则不变(B 级解锁下一首)。 +结算手势:单击=再来一遍(同曲)、右滑=下一首(已解锁)、下滑=回大厅,不变。 + +--- + +## 11. 断言清单(web/rhythm/test/playtest.mjs 覆盖全集) + +> 13 号文档 §4 rhythm 段是旧玩法的断言,已被本文取代但**不回改**(13 号 +> 文档冻结);本节是 24 号文档的断言明细,按 §编号可被测试与评审引用。 + +- **A1 分区映射**:`zoneOf` 3×2 九宫边界正确(角落/边界点逐一断言);payload + 缺失兜底 BC。 +- **A2 判定窗**:|dt|≤0.15 → Perfect(+accent 叠加);0.15<|dt|≤0.35 → Good; + |dt|>0.35 同区出手 = 自由演奏(出声不判定、不判 Miss)。 +- **A3 错鼓 Miss**:异分区音符 GOOD 半窗内被敲 → Miss(闷镲 + error 震 + + combo 清零;区名播报限教学期——正片错音的归因就是那一声敲错的音色本身,不再叠语音(配额)。 +- **A4 超时 Miss**:expected+0.45s 无应答 → Miss(GOOD 0.35 + 放宽 0.10)。 +- **A5 序曲 tap 撤销**:immediateTap 按下认领 → 引擎 tapcancel → 认领撤销 + 且不计 Miss(统计回滚)。 +- **A6 双击=两连击**:300ms 内同区两 tap + doubletap → 两次敲击各自独立 + (可认领两个相邻音符/一次认领一次自由演奏),无撤销、无崩溃。 +- **A7 教学流程**:T0 探索摸 3 区(区名每区一次)→ T1-T3 跟打通过 → + `tutorialDone`;全程零 Miss 音、零计分。 +- **A8 教学重教**:单步失败 2 次 → 降速 25% 重演(gd_slow,每教学局 1 次)→ + 再败跳过(retry_skip)不卡死;归因真话律(没出手 → gd_miss_none)。 +- **A9 数拍**:曲目开始先排 4 记底鼓 count-in(1 小节)再进句 1 领奏; + 网格对齐 BGM t0。 +- **A10 菜单**:main 中下滑**无条件**呼菜单(无 down 音符折中);暂停冻结 + (泵停、BGM 断点);恢复后 cue.expected/_bgmT0 平移量 = 暂停时长 + + count-in 小节;guide full 档恢复先播 gd_count(每曲限 1 次)。 +- **A11 加花**:fill 句领奏为八分密集 + 尾音 TR;应答窗口与普句同规则; + 曲目一终曲加花前 1 小节播 tut_fill(每账号一次)。 +- **A12 结算评级**:全 Perfect → S + `grades.song1==='s'` + `unlocked===2` + + unlock_song2 播报;B 级也解锁;C 级不解锁。rate 公式与阈值逐档断言。 +- **A13 结算导航**:单击=同曲重开、右滑=下一首(未解锁播 song_locked)、 + song3 末位右滑播 restart 不播 song_locked(旧作裁定①回归)。 +- **A14 连击里程碑**:16 连击 success 震、乐句全 Perfect [20,40,20]。 +- **A15 mid_song**:曲中点播报且领奏 duck 到 1/4、播完恢复。 +- **A16 进度同步**:PUT body 含 summary/grades/unlocked/bestCombo;B 级提升 + 才覆写 grades。 +- **A17 语音配额**:应答小节内零 voice 调度;gd_miss 同曲一次;探索区名 + 每区一次(转写级断言放 transcribe 剧本核)。 +- **A18 键盘平权**:Digit1-6 注入 = 对应区命中(零延迟判定);空格 tap + (元素中心坐标)→ BC;选曲态数字键无效。 +- **A19 guide 接入**:select=menu / main=battle / end=settle 状态签名同点; + 首个应答窗口前 1 小节 gd_answer+turn。 +- **A20 资产完整**:check_all_assets.py rhythm 段全绿(含新 sfx/bgm/voice 双性别)。 +- **A21 旁白跳过**:intro/tut(引导语音)/select/end 语音播放中单击=跳句(12 号 + 文档 §1.2);main 演奏期单击是演奏不跳。 +- **A22 全局**:启动遮罩前无音频;无未捕获异常;settings.voice 切换下一句生效; + bgm=0 时不加载 BGM。 +- **A23(R1 F3)** 教学没出手失败的纠正链 retry_listen 只播一次(不二连复读)。 +- **A24(R1 F1)** 曲尾 3s 内连 2 Miss 不插播 gd_miss(结算链不被顶断)。 +- **A25(R1 F2)** 探针三连摸的三个区名全部播完不被掐断,教学句 queue 其后 + (进阶延迟 150ms 起步:引擎入队判定在 await 加载之前,同步连发会误判)。 + +--- + +## 12. 资产预算与台账 + +- 语音:49 id × 2 性别 = 98 文件(Qwen3-TTS,flock .gpu.lock,零 API 费)。 +- 打击乐:7 件程序合成(零成本)。 +- BGM:3 任务 × $0.03,每首至多 1 次重提 → 上限 $0.18(预算线 $0.30, + 累计 $1.26/$2.00 → 本轮后 ≤$1.44+$0.18=$1.56 上限)。台账追加 + tools/mureka_log.md。 +- 开发/测试/转写:零成本(复用 harness)。 diff --git a/docs/design/24-rhythm-voice-ids.md b/docs/design/24-rhythm-voice-ids.md new file mode 100644 index 00000000..c4b2349b --- /dev/null +++ b/docs/design/24-rhythm-voice-ids.md @@ -0,0 +1,115 @@ +# 24a · 《节拍行者》鼓手模式语音台词表(voice ids,24 号施工录音依据) + +> 双性别:male=Uncle_Fu / female=Vivian(gen_voice_dual.py SPEAKERS 沿用)。 +> instruct 对:「低沉醇厚的男声,像节奏游戏主持人,平稳有力」/「活力清晰的 +> 年轻女声,像节奏游戏主持人,情绪饱满但不夸张」——全表同一常量。 +> 主表 41 句 + 向导层沿用 9 句(gd_*,已在库不重录)= 50 句双性别(§8 配额)。 +> 废弃:tut_1 / tut_2 / tut_3 / retry_gesture(手势教学时代遗产,音频随 +> 本轮 convert 后删除)。 + +## W 组 · 开场与流程(5 句) + +| id | 文案 | +|---|---| +| welcome | 欢迎来到节拍行者。今天你是乐队的鼓手,整面屏幕都是你的鼓。 | +| welcome_back | 欢迎回来,鼓手。准备好了就开始。 | +| tut_done | 很好,演出开始。 | +| tut_skip | 跳过教学,直接开始。 | +| mid_song | 过半了,保持。 | + +## Z 组 · 分区教学与区名(12 句) + +| id | 文案 | +|---|---| +| tut_map | 先认识你的鼓。屏幕横着分三列,左边、中间、右边;竖着分两行,上面清亮,下面低沉。随便摸一摸,听到哪面鼓,我就告诉你它在哪。 | +| tut_probe | 摸到三个不同的地方,我们就开始。 | +| tut_z_kick | 下面正中间,是底鼓,你的家。先听四下数拍,然后跟着我敲。 | +| tut_z_sides | 左下是军鼓,右下是低嗵。一左一右,跟着打。 | +| tut_z_patt | 上面一排更清亮。左上踩镲,上中高嗵,右上吊镲。听这一句,三个音。 | +| tut_fill | 最后有一段加花,跟着我连打。 | +| zone_tl | 左上,踩镲。 | +| zone_tc | 上中,高嗵。 | +| zone_tr | 右上,吊镲。 | +| zone_bl | 左下,军鼓。 | +| zone_bc | 下中,底鼓。 | +| zone_br | 右下,低嗵。 | + +## C 组 · 教学纠正(5 句) + +| id | 文案 | +|---|---| +| retry_early | 早了,等鼓点再出手。 | +| retry_late | 晚了,跟着鼓点走。 | +| retry_wrong | 敲错鼓了,再听一遍。 | +| retry_listen | 再听一遍,跟着来。 | +| retry_skip | 没关系,我们先走下一句。 | + +## S 组 · 选曲与解锁(7 句) + +| id | 文案 | +|---|---| +| select_hint | 左右滑选曲目,单击开始,下滑回大厅。 | +| song1_name | 第一首,启程,九十拍。 | +| song2_name | 第二首,追风,一百零五拍。 | +| song3_name | 第三首,星爆,一百二十拍。 | +| song_locked | 这首还没解锁。上一首拿到 B 级以上就能解锁。 | +| unlock_song2 | 新曲目解锁,追风! | +| unlock_song3 | 新曲目解锁,星爆! | + +## G 组 · 结算(6 句) + +| id | 文案 | +|---|---| +| grade_s | S 级!准确率超过九成五,完美演出。 | +| grade_a | A 级!准确率超过八成五,非常出色。 | +| grade_b | B 级,准确率超过七成。不错,还能更好。 | +| grade_c | C 级,准确率不到七成。别灰心,再来一遍。 | +| new_best_combo | 新连击纪录! | +| restart | 单击再来一遍,右滑下一首,下滑回大厅。 | + +## M 组 · 菜单与规则(6 句) + +| id | 文案 | +|---|---| +| rules | 规则:我敲一小节鼓,你在下一小节照着敲回来。鼓要选对,拍点也要对。 | +| settings_hint | 音色、语速、背景音乐,请回大厅修改。 | +| menu_main | 菜单。单击切换选项,双击确认。 | +| menu_resume | 继续演奏。 | +| menu_lobby | 回大厅。 | +| skip_hint | 单击可跳过旁白。 | + +## 向导层沿用(9 句,已在库,不重录不重转) + +gd_answer「现在跟着我打。」/ gd_miss「慢了半拍,下一小节跟上我就行。」/ +gd_miss_early「抢拍了。等声音落下再滑。」/ gd_miss_late「慢了半拍。跟着节拍走。」/ +gd_miss_wrong「方向不对。跟着音色滑。」/ gd_miss_none「这拍没接。我停下的时候,就轮到你了。」/ +gd_slow「我放慢一些,跟着试试。」/ gd_count「四拍之后,继续。」 +(gd_miss_early/late/wrong 的「滑」字文案在 24 号玩法下语义仍通——纠正语 +不点名手势;如需改口文案按「重教≤2」流程另行裁定,不在本轮。) + +## 键盘变体(key,P1 输入平权路线 a,8 句) + +| id | key 文案 | +|---|---| +| tut_map | 先认识你的鼓。屏幕横着分三列,左边、中间、右边;竖着分两行,上面清亮,下面低沉。按数字一键到六键,听六面鼓。 | +| tut_probe | 按出三个不同的键,我们就开始。 | +| tut_z_kick | 数字五,是底鼓,你的家。先听四下数拍,然后跟着我敲。 | +| tut_z_sides | 数字四,军鼓。数字六,低嗵。一左一右,跟着打。 | +| tut_z_patt | 上面一排更清亮。数字一踩镲,数字二高嗵,数字三吊镲。听这一句,三个音。 | +| select_hint | 左右方向键选曲目,按空格开始,按下方向键回大厅。 | +| restart | 按空格再来一遍,按右方向键下一首,按下方向键回大厅。 | +| menu_main | 菜单。按空格切换选项,按回车确认。 | + +> skip_hint / rules / retry_* / zone_* 无操作词,主资产触屏/键盘通用,不设变体。 +> key 变体为**补充合成**( `--only-variants` 单独跑),主表同 id 文案照常重录。 + +## 录制说明 + +- gen_voice_dual.py RHYTHM_LINES 段全量替换为本表(41 句);KEY_LINES rhythm + 段替换为本表 key 变体(8 句)。 +- 生成:`.venv-tts/bin/python tools/gen_voice_dual.py --game rhythm --gender both` + (flock .gpu.lock);检查 `--check`;转码 `--convert`。 +- convert 后删除废弃 id 音频:tut_1 / tut_2 / tut_3 / retry_gesture + (male/female 及 key 变体共 16 文件)。 +- web/harness/voice_text.json 的 rhythm 主表与 `__key__` 变体表同步本台词 + (转写时长估算与 ASR 判定用)。 diff --git a/docs/web-engine-api.md b/docs/web-engine-api.md index a73ed927..be53e546 100644 --- a/docs/web-engine-api.md +++ b/docs/web-engine-api.md @@ -299,11 +299,19 @@ vibrate([50, 30, 50]); // 自定义震型 ```js const detach = attachGestures(element, onGesture, config?) // onGesture: (g: 'up'|'down'|'left'|'right'|'tap'|'doubletap' -// |'longpress'|'longpressend'|'tapcancel') => void +// |'longpress'|'longpressend'|'tapcancel', +// info?: { x: number, y: number }) => void // ('tapcancel' 仅 immediateTap 模式补发,见下) // detach(): 解除绑定 ``` +第二参 `info`(坐标透传,24 号鼓手模式新增,**缺省行为逐字节不变**): +`{x, y}` 为视口像素坐标——点按类事件(tap/序曲 tap/双击改判/tapcancel)与 +长按 = **按下点**;四向滑动 = **滑动起点**(出手瞄准位置);键盘通道 = +虚拟指针坐标(元素中心,方向键滑动同样携带)。需要触点位置的游戏(rhythm +分区打击的「敲哪个区」判定)使用;既有单参回调零感知、零回归成本 +(同 opts.pan / immediateTap / tapcancel 先例)。 + 判定参数(`DEFAULT_GESTURE_CONFIG`,与 Android `GestureConfig` 对齐): | 参数 | 默认 | 含义 | diff --git a/research/rhythm-drummer/round0/engine-regression.md b/research/rhythm-drummer/round0/engine-regression.md new file mode 100644 index 00000000..e1d334a8 --- /dev/null +++ b/research/rhythm-drummer/round0/engine-regression.md @@ -0,0 +1,30 @@ +# round0 · 阶段零可行性勘察与引擎回归证据 + +日期:2026-09-14。依据 docs/design/24-rhythm-drummer.md §0。 + +## 可行性决策 + +- 引擎手势事件**不携带坐标**(web/engine/gesture.js 勘察结论)。 +- 选**路线 (a)**:引擎后向兼容扩展——全部 onGesture 调用补第二实参 + `{x, y}`(tap/长按=按下点,滑动=起点,键盘=虚拟指针坐标)。既有 15 款 + 游戏回调均为单参,多余实参被忽略,判定/震动/时序逐字节不变 + (同 opts.pan / immediateTap / tapcancel 先例)。 +- 已同步 docs/web-engine-api.md §engine/gesture.js(onGesture 签名 + + info 第二参说明)。 + +## 引擎改动清单 + +- web/engine/gesture.js:downX/downY 跟踪;13 个 onGesture 调用点补 payload; + 文件头注释补第二参说明。无 config 新增、无判定阈值/震动改动。 + +## 全量回归(铁律义务,全绿) + +| 测试 | 结果 | +|---|---| +| 15 游戏 playtest(storyline 82 / escape 140 / simon 132 / rhythm 141 / space 172 / rpg 188 / radio 246 / dungeon 1649 / detective 279 / abyss 278 / hunt 211 / cipher 223 / bridge 336 / teahouse 205 / keeper 257 断言) | 全部 0 失败 | +| web/harness/guide.test.mjs | 122 断言 0 失败 | +| web/harness/engine-voice.test.mjs | 19 断言 0 失败 | +| web/harness/engine-input.test.mjs | 72 断言 0 失败 | + +合计 ~4,547 断言全绿。rhythm 旧 playtest(141 断言)在重构前最后一次运行 +通过,作为重构基线。 diff --git a/research/rhythm-drummer/round0/transcript-main.md b/research/rhythm-drummer/round0/transcript-main.md new file mode 100644 index 00000000..83fe9ee3 --- /dev/null +++ b/research/rhythm-drummer/round0/transcript-main.md @@ -0,0 +1,218 @@ +# 试玩转写:《rhythm》 + +> 本文件是「玩家感知日志」:一位新手玩家的整局体验转写。语音列出了玩家听到的每句话全文, +> 音效/BGM/震动按发生时间列出。「静默」段 = 玩家什么也听不到。手势是剧本注入的(模拟真人手部动作), +> 非最优操作:包含犹豫、误操作与乱试。语音时长按语速估算(±20%),仅供节奏参考。 +t+ 0.0s (玩家点开游戏,轻触屏幕开始) +t+ 0.0s ▶ 语音:「欢迎来到节拍行者。今天你是乐队的鼓手,整面屏幕都是你的鼓。」 +t+ 0.0s 【状态】开场旁白:我是鼓手,整个屏幕都是鼓。单击跳过 开场旁白在播(认识你的鼓),单击可以跳过(语音播放中,语速1) +t+ 0.5s ▶ 语音:「先认识你的鼓。屏幕横着分三列,左边、中间、右边;竖着分两行,上面清亮,下面低沉。随便摸一摸,听到哪面鼓,我就告诉你它在哪。」 +t+ 2.6s ▶ 语音:「摸到三个不同的地方,我们就开始。」 +t+ 3.1s ♪ 向导音:explore(探索/安全) +t+ 11.1s ♪ 音效 sfx/inst_kick +t+ 11.1s ▶ 语音:「下中,底鼓。」 +t+ 13.2s 【状态】进入摸鼓认区:先四处摸摸,听到哪面鼓就报哪面鼓的名 摸鼓认区:随便摸,摸到 3 个不同的地方就开始(已摸 1 区)(无 BGM,语速1) + +#### 【玩家动作】背景音乐《启程》无鼓伴奏已在轻声循环——拍子网格的底,鼓由我来打 + +t+ 13.2s 【状态】六面鼓摸遍了:左上踩镲、上中高嗵、右上吊镲、左下军鼓、下中底鼓、右下低嗵 教学:听领奏,下一小节照样敲(BC)(无 BGM,语速1) +t+ 13.2s 【状态】又摸了两下重复的,只响不报名 教学:听领奏,下一小节照样敲(BC)(无 BGM,语速1) +t+ 13.2s ♪ 音效 sfx/dm_hat +t+ 13.2s ▶ 语音:「左上,踩镲。」 +t+ 13.2s ♪ 音效 sfx/dm_tomhi +t+ 13.2s ▶ 语音:「上中,高嗵。」 +t+ 13.2s ▶ 语音:「下面正中间,是底鼓,你的家。先听四下数拍,然后跟着我敲。」 +t+ 13.2s ♪ 音效 sfx/dm_snare +t+ 13.2s ♪ 音效 sfx/inst_kick +t+ 13.2s ♪ 音效 sfx/dm_tomlo +t+ 13.2s ♪ 音效 sfx/dm_hat +t+ 13.2s ♪ 音效 sfx/dm_tomlo +t+ 13.2s 【状态】摸到三个不同的地方,自动开始教学第一步 教学:听领奏,下一小节照样敲(BC)(无 BGM,语速1) +t+ 13.2s 【状态】教学第 1/3 步:听数拍和领奏,下一小节照着敲 教学:听领奏,下一小节照样敲(BC)(无 BGM,语速1) +t+ 16.7s 〰 震动:beat(节拍点) +t+ 17.3s 〰 震动:beat(节拍点) +t+ 18.0s 〰 震动:beat(节拍点) +t+ 18.7s 〰 震动:beat(节拍点) +t+ 19.3s 〰 震动:beat(节拍点) +t+ 22.0s ♪ 音效 sfx/inst_kick +t+ 22.0s ♪ 音效 sfx/dm_accent +t+ 22.8s ▶ 语音:「左下是军鼓,右下是低嗵。一左一右,跟着打。」 +t+ 24.9s 【状态】教学第 2/3 步:听数拍和领奏,下一小节照着敲 教学:听领奏,下一小节照样敲(BL→BR)(无 BGM,语速1) +t+ 27.3s 〰 震动:beat(节拍点) +t+ 28.0s 〰 震动:beat(节拍点) +t+ 28.7s 〰 震动:beat(节拍点) +t+ 29.3s 〰 震动:beat(节拍点) +t+ 30.0s 〰 震动:beat(节拍点) +t+ 31.3s 〰 震动:beat(节拍点) +t+ 32.7s ♪ 音效 sfx/dm_snare +t+ 32.7s ♪ 音效 sfx/dm_accent +t+ 34.0s ♪ 音效 sfx/dm_tomlo +t+ 34.0s ♪ 音效 sfx/dm_accent +t+ 34.8s ▶ 语音:「上面一排更清亮。左上踩镲,上中高嗵,右上吊镲。听这一句,三个音。」 +t+ 36.9s 【状态】教学第 3/3 步:听数拍和领奏,下一小节照着敲 教学:听领奏,下一小节照样敲(TL→TC→TR)(无 BGM,语速1) +t+ 40.7s 〰 震动:beat(节拍点) +t+ 41.3s 〰 震动:beat(节拍点) +t+ 42.0s 〰 震动:beat(节拍点) +t+ 42.7s 〰 震动:beat(节拍点) +t+ 43.3s 〰 震动:beat(节拍点) +t+ 44.0s 〰 震动:beat(节拍点) +t+ 44.7s 〰 震动:beat(节拍点) +t+ 46.0s ♪ 音效 sfx/dm_hat +t+ 46.0s ♪ 音效 sfx/dm_accent +t+ 46.7s ♪ 音效 sfx/dm_tomhi +t+ 46.7s ♪ 音效 sfx/dm_accent +t+ 47.4s ♪ 音效 sfx/dm_crash +t+ 47.4s ♪ 音效 sfx/dm_accent +t+ 48.1s ▶ 语音:「很好,演出开始。」 +t+ 50.2s ♪ 向导音:battle(战斗/对抗) +t+ 50.2s 【状态】教学完毕,曲目一《启程》90 拍开始。先听四下底鼓数拍,然后是「现在跟着我打」 演奏中(启程):还剩 32 个应答音。领奏停了、轮到空拍,就该我出手了;有一段加花要连打(无 BGM,语速1) + +#### 【玩家动作】《启程》无鼓伴奏 BGM 起,四下底鼓数拍对齐小节 + +t+ 51.2s 〰 震动:beat(节拍点) +t+ 51.9s 〰 震动:beat(节拍点) +t+ 52.6s 〰 震动:beat(节拍点) +t+ 52.8s ♪ 向导音:menu(系统界面) +t+ 52.8s ▶ 语音:「菜单。单击切换选项,双击确认。」 +t+ 52.8s 【状态】曲中暂停,进了菜单 暂停菜单:单击切选项,双击确认,上滑帮助,下滑恢复演奏(语音播放中,语速1) + +#### 【玩家动作】BGM 在断点冻住——谱面、判定、音乐全部暂停 + +t+ 53.2s 〰 震动:beat(节拍点) +t+ 54.9s ▶ 语音:「继续演奏。」 + +#### 【玩家动作】还是想打完这首,下滑回去 + +t+ 54.9s ▶ 向导语音:「四拍之后,继续。」 +t+ 54.9s 【状态】听到「四拍之后,继续」,跟着四下底鼓数拍进来了 演奏中(启程):还剩 32 个应答音。领奏停了、轮到空拍,就该我出手了;有一段加花要连打(语音播放中,语速1) + +#### 【玩家动作】BGM 从断点续播,节拍网格整体后移了一小节 + +t+ 54.9s ……… 静默 5s(还在数拍子,第一句没敢出手) ……… +t+ 57.1s 〰 震动:beat(节拍点) +t+ 57.8s 〰 震动:beat(节拍点) +t+ 58.4s 〰 震动:beat(节拍点) +t+ 59.1s 〰 震动:beat(节拍点) +t+ 60.9s ▶ 向导语音:「现在跟着我打。」 +t+ 60.9s ♪ 向导音:turn(轮到你) +t+ 60.9s 〰 震动:beat(节拍点) +t+ 62.2s 〰 震动:beat(节拍点) +t+ 64.0s 〰 震动:error(做错了:两连短震) +t+ 65.4s ▶ 向导语音:「慢了半拍,下一小节跟上我就行。」 +t+ 65.4s 〰 震动:error(做错了:两连短震) +t+ 65.4s 【状态】第一句全漏了,听到「慢了半拍,下一小节跟上我就行」 演奏中(启程):还剩 30 个应答音。领奏停了、轮到空拍,就该我出手了;有一段加花要连打(语音播放中,语速1) +t+ 68.9s 〰 震动:beat(节拍点) +t+ 70.2s 〰 震动:beat(节拍点) +t+ 71.6s ♪ 音效 sfx/dm_snare +t+ 71.6s ♪ 音效 sfx/dm_accent +t+ 72.9s ♪ 音效 sfx/dm_tomlo +t+ 72.9s ♪ 音效 sfx/dm_accent +t+ 72.9s 〰 震动:自定义震型 [20,40,20] +t+ 76.9s 〰 震动:beat(节拍点) +t+ 77.6s 〰 震动:beat(节拍点) +t+ 78.2s 〰 震动:beat(节拍点) +t+ 79.6s ♪ 音效 sfx/inst_kick +t+ 79.6s ♪ 音效 sfx/dm_accent +t+ 80.3s ♪ 音效 sfx/inst_kick +t+ 80.3s ♪ 音效 sfx/dm_accent +t+ 80.9s ♪ 音效 sfx/dm_snare +t+ 80.9s ♪ 音效 sfx/dm_accent +t+ 80.9s 〰 震动:自定义震型 [20,40,20] +t+ 84.9s 〰 震动:beat(节拍点) +t+ 85.6s 〰 震动:beat(节拍点) +t+ 86.2s 〰 震动:beat(节拍点) +t+ 87.6s ♪ 音效 sfx/dm_snare +t+ 87.6s ♪ 音效 sfx/dm_accent +t+ 88.3s ♪ 音效 sfx/dm_tomlo +t+ 88.3s ♪ 音效 sfx/dm_accent +t+ 88.9s ♪ 音效 sfx/inst_kick +t+ 88.9s ♪ 音效 sfx/dm_accent +t+ 88.9s 〰 震动:自定义震型 [20,40,20] +t+ 92.9s ▶ 语音:「过半了,保持。」 +t+ 92.9s 〰 震动:beat(节拍点) +t+ 93.6s 〰 震动:beat(节拍点) +t+ 94.2s 〰 震动:beat(节拍点) +t+ 94.9s 〰 震动:beat(节拍点) +t+ 95.6s ♪ 音效 sfx/inst_kick +t+ 95.6s ♪ 音效 sfx/dm_accent +t+ 96.3s ♪ 音效 sfx/dm_snare +t+ 96.3s ♪ 音效 sfx/dm_accent +t+ 96.9s ♪ 音效 sfx/inst_kick +t+ 96.9s ♪ 音效 sfx/dm_accent +t+ 97.6s ♪ 音效 sfx/dm_tomlo +t+ 97.6s ♪ 音效 sfx/dm_accent +t+ 97.6s 〰 震动:自定义震型 [20,40,20] +t+ 98.2s 〰 震动:beat(节拍点) +t+ 99.6s 〰 震动:beat(节拍点) +t+ 100.2s 〰 震动:beat(节拍点) +t+ 100.9s ♪ 音效 sfx/dm_hat +t+ 100.9s ♪ 音效 sfx/dm_accent +t+ 102.3s ♪ 音效 sfx/dm_snare +t+ 102.3s ♪ 音效 sfx/dm_accent +t+ 102.9s ♪ 音效 sfx/inst_kick +t+ 102.9s ♪ 音效 sfx/dm_accent +t+ 102.9s 〰 震动:自定义震型 [20,40,20] +t+ 103.6s 〰 震动:beat(节拍点) +t+ 104.2s 〰 震动:beat(节拍点) +t+ 104.9s 〰 震动:beat(节拍点) +t+ 105.6s 〰 震动:beat(节拍点) +t+ 106.3s ♪ 音效 sfx/inst_kick +t+ 106.3s ♪ 音效 sfx/dm_accent +t+ 106.3s 〰 震动:success(成功长震) +t+ 106.9s ♪ 音效 sfx/dm_hat +t+ 106.9s ♪ 音效 sfx/dm_accent +t+ 107.6s ♪ 音效 sfx/inst_kick +t+ 107.6s ♪ 音效 sfx/dm_accent +t+ 108.3s ♪ 音效 sfx/dm_crash +t+ 108.3s ♪ 音效 sfx/dm_accent +t+ 108.3s 〰 震动:自定义震型 [20,40,20] +t+ 108.9s 〰 震动:beat(节拍点) +t+ 109.6s 〰 震动:beat(节拍点) +t+ 110.2s 〰 震动:beat(节拍点) +t+ 110.9s 〰 震动:beat(节拍点) +t+ 111.6s ▶ 语音:「最后有一段加花,跟着我连打。」 +t+ 111.6s ♪ 音效 sfx/dm_snare +t+ 111.6s ♪ 音效 sfx/dm_accent +t+ 112.3s ♪ 音效 sfx/dm_tomlo +t+ 112.3s ♪ 音效 sfx/dm_accent +t+ 112.9s ♪ 音效 sfx/dm_hat +t+ 112.9s ♪ 音效 sfx/dm_accent +t+ 113.6s ♪ 音效 sfx/inst_kick +t+ 113.6s ♪ 音效 sfx/dm_accent +t+ 113.6s 〰 震动:自定义震型 [20,40,20] +t+ 114.3s ♪ 向导音:turn(轮到你) +t+ 114.2s 〰 震动:beat(节拍点) +t+ 114.6s 〰 震动:beat(节拍点) +t+ 114.9s 〰 震动:beat(节拍点) +t+ 115.2s 〰 震动:beat(节拍点) +t+ 115.6s 〰 震动:beat(节拍点) +t+ 115.9s 〰 震动:beat(节拍点) +t+ 116.2s 〰 震动:beat(节拍点) +t+ 116.9s ♪ 音效 sfx/inst_kick +t+ 116.9s ♪ 音效 sfx/dm_accent +t+ 117.3s ♪ 音效 sfx/inst_kick +t+ 117.3s ♪ 音效 sfx/dm_accent +t+ 117.6s ♪ 音效 sfx/inst_kick +t+ 117.6s ♪ 音效 sfx/dm_accent +t+ 117.9s ♪ 音效 sfx/dm_snare +t+ 117.9s ♪ 音效 sfx/dm_accent +t+ 118.3s ♪ 音效 sfx/dm_snare +t+ 118.3s ♪ 音效 sfx/dm_accent +t+ 118.6s ♪ 音效 sfx/inst_kick +t+ 118.6s ♪ 音效 sfx/dm_accent +t+ 118.9s ♪ 音效 sfx/dm_crash +t+ 118.9s ♪ 音效 sfx/dm_accent +t+ 118.9s 〰 震动:自定义震型 [20,40,20] +t+ 119.9s ♪ 向导音:settle(结算) +t+ 119.9s ▶ 语音:「A 级!准确率超过八成五,非常出色。」 +t+ 119.9s 〰 震动:success(成功长震) +t+ 119.9s 〰 震动:success(成功长震) +t+ 119.9s 〰 震动:自定义震型 [20,40,20] +t+ 122.0s ▶ 语音:「新连击纪录!」 +t+ 124.1s ▶ 语音:「新曲目解锁,追风!」 +t+ 126.2s ▶ 语音:「单击再来一遍,右滑下一首,下滑回大厅。」 +t+ 128.4s ♫ BGM 切换 → bgm/menu(淡入 800ms) +t+ 152.9s 【状态】曲子打完了(加花收尾是一记吊镲),结算播报中 结算播报中:听完单击再来一遍,右滑下一首,下滑回大厅(BGM=bgm/menu,语速1) + +#### 【玩家动作】曲目 BGM 淡出,结算语音链播完会淡入大厅音乐 + diff --git a/research/rhythm-drummer/round1/live-player.md b/research/rhythm-drummer/round1/live-player.md new file mode 100644 index 00000000..694fb29e --- /dev/null +++ b/research/rhythm-drummer/round1/live-player.md @@ -0,0 +1,51 @@ +# 《节拍行者》鼓手模式 R1 全拟真试玩报告 + +两会话均真机跑完(live harness:真 chromium + SenseVoice ASR 听游戏实况,非 mock; +控制端口 8142,游戏服务器 8001)。驱动为事件流跟随器(vibrate 锚定真实发声时刻 + +谱序消费),节奏精度经多轮校准后 dt≈0.00s。 + +## 局 A(主线 · male 1.0× · guide=full · 教学+曲目一全 Perfect 弧线) + +- 产物:`live/A/`(events.jsonl 40KB / mix.webm 4.7MB / transcript.md / verdicts.json) +- ASR 判定:**OK 11 / TRUNCATED 5 / MISMATCH 2**(SFX_BGM_ONLY 间隙探针 7) +- 完整弧线实录:摸鼓认区(zone_tl OK)→ T1 降速重演(gd_slow OK sim=1.0)→ + T2/T3 通过 → tut_done(OK sim=1.0)→ 数拍 → gd_answer(OK sim=1.0)→ + mid_song(OK sim=0.8)→ tut_fill(OK sim=0.92)→ **grade_a(A 级)→ + unlock_song2(OK sim=0.93)→ restart(OK)**,全链无人为干预走通 +- 演奏精度:**33 记 Perfect accent / 4 记 Miss**——跟随线程与游戏判定网格的 + 逐音配对(pop 日志 entry.when ≈ vibrate.ta ±0.02s)证明判定窗(±0.15/±0.35) + 在真机触摸链路(CDP 合成触摸+HTTP)下准确兑现 +- 归因(全部非游戏缺陷或已修级): + - TRUNCATED welcome/tut_map/tut_probe = 驱动故意三连跳旁白(kill) + - MISMATCH zone_tr / new_best_combo = 短句 ASR 同音近失(「右上吊镲」→ + 「有上调查」;「新连击纪录」→「新联机记录」),cipher 批次先例同款 + - TRUNCATED zone_bl = 探针第三击触发进阶时 stopVoice 切断区名(P2) + - TRUNCATED gd_miss = 曲尾连 Miss 的向导句播 1.1s 后被结算评级语音顶掉 + (半句「慢了半拍。」)——P1(F1) + - 曲终 4 记 Miss 系驱动 clearPending 误杀尾单补发(驱动侧 artifact,已修) + +## 局 B(压力线 · female 1.7× · guide=standard · bgm 0.3 · 错区乱打+菜单+长按) + +- 产物:`live/B/`(events.jsonl / mix.webm 3.3MB / transcript.md;verdicts 因驱动 + 超时未及落盘——control 层 artifact,非游戏问题) +- 关键机制实录:跳教学(tut_skip)→ 数拍 → **错区乱打全程 guide=standard + 零向导语音**(gd_* vstart = 0,earcon/震动照发)→ 32 记 error 震 + grade_c + → 曲中菜单(下滑→单击切选项→上滑帮助→下滑恢复,恢复后网格平移续播)→ + 长按死输入(闷镲+error 震,不判定不归属)→ 结算链 +- 1.7× 语速下语音链无截断无风暴(保险丝零触发) + +## 结论 + +- 教学 DDA(gd_slow)、归因真话律(gd_miss_none)、count-in 恢复(gd_count)、 + 模态菜单断点续播、错鼓闷镲反馈、长按死输入定义——全部实况兑现 +- 判定网格精确性真机直证(33/37 Perfect 级命中,dt 均值 ~0.00s) +- 无死锁、无状态错乱、非资产类 console.error = 0 +- 基础设施教训(沉淀,惠及后续 live 驱动): + 1. 事件流驱动必须单飞拉取(并发 pump 同 since 窗口 → 事件重复入队错位) + 2. start 事件带 `when` 字段:when>0=排程领奏 / when=0=即时(玩家击/闷镲), + 玩家击会回声触发跟随循环,必须双重过滤 + 3. 发声时刻锚定用 vibrate 事件(setTimeout 按 ev.time 触发),不用 start 的 + t(含 _load 时序噪声) + 4. BGM 的 source.start(t0) 也是 when>0 的 start 事件,入队前必须按 layer 过滤 + 5. 视口坐标:control 默认 390×844,三区边界 130/260(驱动初版按 300×600 + 导致右列全部敲成中列) diff --git a/research/rhythm-drummer/round1/live/A/events.jsonl b/research/rhythm-drummer/round1/live/A/events.jsonl new file mode 100644 index 00000000..b9447ce9 --- /dev/null +++ b/research/rhythm-drummer/round1/live/A/events.jsonl @@ -0,0 +1,368 @@ +{"ev":"probe_ready","t":9.400000005960464,"ta":null} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":1520.9000000059605,"ta":-0.083} +{"ev":"recstart","mime":"audio/webm;codecs=opus","t":1603.6000000238419,"ta":0} +{"ev":"fetch","url":"audio/sfx/dm_hat.ogg","status":200,"ok":true,"ms":15,"t":1618.5,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/dm_tomhi.ogg","status":200,"ok":true,"ms":14,"t":1618.7000000178814,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/dm_crash.ogg","status":200,"ok":true,"ms":14,"t":1618.800000011921,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/dm_snare.ogg","status":200,"ok":true,"ms":14,"t":1618.800000011921,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/inst_kick.ogg","status":200,"ok":true,"ms":14,"t":1618.800000011921,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/dm_tomlo.ogg","status":200,"ok":true,"ms":14,"t":1618.9000000059605,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/dm_accent.ogg","status":200,"ok":true,"ms":14,"t":1618.9000000059605,"ta":0.015} +{"ev":"fetch","url":"audio/bgm/menu.ogg","status":200,"ok":true,"ms":14,"t":1619,"ta":0.015} +{"ev":"fetch","url":"audio/voice/male/tut_map.ogg","status":200,"ok":true,"ms":14,"t":1619,"ta":0.015} +{"ev":"fetch","url":"audio/voice/male/welcome.ogg","status":200,"ok":true,"ms":15,"t":1619.1000000238419,"ta":0.016} +{"ev":"fetch","url":"audio/voice/male/welcome_back.ogg","status":200,"ok":true,"ms":15,"t":1619.5,"ta":0.016} +{"ev":"fetch","url":"audio/voice/male/mid_song.ogg","status":200,"ok":true,"ms":19,"t":1624,"ta":0.02} +{"ev":"fetch","url":"audio/voice/male/tut_probe.ogg","status":200,"ok":true,"ms":19,"t":1624.300000011921,"ta":0.021} +{"ev":"fetch","url":"audio/voice/male/select_hint.ogg","status":200,"ok":true,"ms":20,"t":1624.7000000178814,"ta":0.021} +{"ev":"fetch","url":"audio/bgm/track90.ogg","status":200,"ok":true,"ms":20,"t":1625.4000000059605,"ta":0.022} +{"ev":"fetch","url":"audio/bgm/track120.ogg","status":200,"ok":true,"ms":20,"t":1625.5,"ta":0.022} +{"ev":"fetch","url":"audio/bgm/track105.ogg","status":200,"ok":true,"ms":22,"t":1627.6000000238419,"ta":0.024} +{"ev":"vstart","url":"audio/voice/male/welcome.ogg","t":1665.6000000238419,"dur":8.24,"ta":0.062} +{"ev":"start","t":1849.6000000238419,"sid":1,"url":"audio/bgm/track90.ogg","layer":"bgm","dur":82.33301587301587,"rate":1,"loop":true,"when":6,"ta":0.246} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":2812.300000011921,"ta":1.209} +{"ev":"vibrate","pattern":"[20]","t":2813.9000000059605,"ta":1.21} +{"ev":"vend","url":"audio/voice/male/welcome.ogg","t":3314.4000000059605,"reason":"kill","ta":1.711} +{"ev":"vstart","url":"audio/voice/male/tut_map.ogg","t":3314.600000023842,"dur":18.04,"ta":1.711} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":4181.700000017881,"ta":2.578} +{"ev":"vibrate","pattern":"[20]","t":4183.200000017881,"ta":2.58} +{"ev":"vend","url":"audio/voice/male/tut_map.ogg","t":4683.4000000059605,"reason":"kill","ta":3.08} +{"ev":"vstart","url":"audio/voice/male/tut_probe.ogg","t":4683.5,"dur":3.84,"ta":3.08} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":5550.9000000059605,"ta":3.947} +{"ev":"vibrate","pattern":"[20]","t":5552.4000000059605,"ta":3.949} +{"ev":"vend","url":"audio/voice/male/tut_probe.ogg","t":6052.600000023842,"reason":"kill","ta":4.449} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":6920.300000011921,"ta":5.317} +{"ev":"vibrate","pattern":"[20]","t":6921.9000000059605,"ta":5.318} +{"ev":"start","t":6922.600000023842,"sid":2,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":5.319} +{"ev":"fetch","url":"audio/voice/male/zone_tl.ogg","status":200,"ok":true,"ms":3,"t":6925,"ta":5.321} +{"ev":"vstart","url":"audio/voice/male/zone_tl.ogg","t":6928.700000017881,"dur":1.6,"ta":5.325} +{"ev":"vend","url":"audio/voice/male/zone_tl.ogg","t":8530,"reason":"kill","ta":6.926} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":9590.800000011921,"ta":7.987} +{"ev":"vibrate","pattern":"[20]","t":9594,"ta":7.99} +{"ev":"start","t":9594.40000000596,"sid":3,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":7.991} +{"ev":"fetch","url":"audio/voice/male/zone_tr.ogg","status":200,"ok":true,"ms":2,"t":9596.600000023842,"ta":7.993} +{"ev":"vstart","url":"audio/voice/male/zone_tr.ogg","t":9600.100000023842,"dur":1.76,"ta":7.997} +{"ev":"vend","url":"audio/voice/male/zone_tr.ogg","t":11478.5,"reason":"kill","ta":9.875} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":12264.100000023842,"ta":10.661} +{"ev":"vibrate","pattern":"[20]","t":12265.90000000596,"ta":10.662} +{"ev":"start","t":12266.40000000596,"sid":4,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":10.663} +{"ev":"fetch","url":"audio/voice/male/zone_bl.ogg","status":200,"ok":true,"ms":3,"t":12269.200000017881,"ta":10.666} +{"ev":"fetch","url":"audio/voice/male/tut_z_kick.ogg","status":200,"ok":true,"ms":4,"t":12270.5,"ta":10.667} +{"ev":"vstart","url":"audio/voice/male/zone_bl.ogg","t":12272,"dur":2,"ta":10.668} +{"ev":"vend","url":"audio/voice/male/zone_bl.ogg","t":12278.300000011921,"reason":"kill","ta":10.675} +{"ev":"vstart","url":"audio/voice/male/tut_z_kick.ogg","t":12278.300000011921,"dur":7.28,"ta":10.675} +{"ev":"vend","url":"audio/voice/male/tut_z_kick.ogg","t":19813.90000000596,"reason":"kill","ta":18.21} +{"ev":"start","t":23014.600000023842,"sid":5,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":22,"ta":21.411} +{"ev":"vibrate","pattern":"[8]","t":23593.70000001788,"ta":21.99} +{"ev":"start","t":23694.5,"sid":6,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":22.666666666666668,"ta":22.091} +{"ev":"vibrate","pattern":"[8]","t":24266.5,"ta":22.663} +{"ev":"start","t":24334.600000023842,"sid":7,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":23.333333333333332,"ta":22.731} +{"ev":"vibrate","pattern":"[8]","t":24923.5,"ta":23.32} +{"ev":"start","t":25014.5,"sid":8,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":24,"ta":23.411} +{"ev":"vibrate","pattern":"[8]","t":25596.5,"ta":23.993} +{"ev":"start","t":25694.5,"sid":9,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":24.666666666666668,"ta":24.091} +{"ev":"vibrate","pattern":"[8]","t":26269.600000023842,"ta":24.666} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":28894.5,"ta":27.291} +{"ev":"vibrate","pattern":"[20]","t":28897.30000001192,"ta":27.294} +{"ev":"start","t":28897.600000023842,"sid":10,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":27.294} +{"ev":"start","t":28897.70000001788,"sid":11,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":27.294} +{"ev":"fetch","url":"audio/voice/male/tut_z_sides.ogg","status":200,"ok":true,"ms":3,"t":29697.5,"ta":28.094} +{"ev":"vstart","url":"audio/voice/male/tut_z_sides.ogg","t":29704.30000001192,"dur":5.2,"ta":28.101} +{"ev":"vend","url":"audio/voice/male/tut_z_sides.ogg","t":34928.60000002384,"reason":"kill","ta":33.325} +{"ev":"start","t":36369,"sid":12,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":35.33333333333333,"ta":34.765} +{"ev":"vibrate","pattern":"[8]","t":36930,"ta":35.326} +{"ev":"start","t":37049.10000002384,"sid":13,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":35.99999999999999,"ta":35.446} +{"ev":"vibrate","pattern":"[8]","t":37603,"ta":35.999} +{"ev":"start","t":37688.90000000596,"sid":14,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":36.666666666666664,"ta":36.085} +{"ev":"vibrate","pattern":"[8]","t":38260,"ta":36.656} +{"ev":"start","t":38369,"sid":15,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":37.33333333333333,"ta":36.765} +{"ev":"vibrate","pattern":"[8]","t":38933,"ta":37.329} +{"ev":"start","t":39008.90000000596,"sid":16,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":37.99999999999999,"ta":37.405} +{"ev":"vibrate","pattern":"[8]","t":39590.10000002384,"ta":37.987} +{"ev":"start","t":40369,"sid":17,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":39.33333333333333,"ta":38.765} +{"ev":"vibrate","pattern":"[8]","t":40935.90000000596,"ta":39.332} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":42237.20000001788,"ta":40.634} +{"ev":"vibrate","pattern":"[20]","t":42239.40000000596,"ta":40.636} +{"ev":"start","t":42239.60000002384,"sid":18,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":40.636} +{"ev":"start","t":42239.60000002384,"sid":19,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":40.636} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":43567,"ta":41.963} +{"ev":"vibrate","pattern":"[20]","t":43568.60000002384,"ta":41.965} +{"ev":"start","t":43568.90000000596,"sid":20,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":41.965} +{"ev":"start","t":43568.90000000596,"sid":21,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":41.965} +{"ev":"fetch","url":"audio/voice/male/tut_z_patt.ogg","status":200,"ok":true,"ms":3,"t":44371.70000001788,"ta":42.768} +{"ev":"vstart","url":"audio/voice/male/tut_z_patt.ogg","t":44381.40000000596,"dur":9.68,"ta":42.778} +{"ev":"vend","url":"audio/voice/male/tut_z_patt.ogg","t":54153.30000001192,"reason":"kill","ta":52.55} +{"ev":"start","t":55033.70000001788,"sid":22,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":54,"ta":53.43} +{"ev":"vibrate","pattern":"[8]","t":55592.70000001788,"ta":53.989} +{"ev":"start","t":55673.70000001788,"sid":23,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":54.666666666666664,"ta":54.07} +{"ev":"vibrate","pattern":"[8]","t":56272.70000001788,"ta":54.669} +{"ev":"start","t":56353.60000002384,"sid":24,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":55.333333333333336,"ta":54.75} +{"ev":"vibrate","pattern":"[8]","t":56922.70000001788,"ta":55.319} +{"ev":"start","t":57033.60000002384,"sid":25,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":56,"ta":55.43} +{"ev":"vibrate","pattern":"[8]","t":57595.70000001788,"ta":55.992} +{"ev":"start","t":57713.60000002384,"sid":26,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":56.666666666666664,"ta":56.11} +{"ev":"vibrate","pattern":"[8]","t":58268.70000001788,"ta":56.665} +{"ev":"start","t":58353.60000002384,"sid":27,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":57.33333333333333,"ta":56.75} +{"ev":"vibrate","pattern":"[8]","t":58925.70000001788,"ta":57.322} +{"ev":"start","t":59033.60000002384,"sid":28,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":58,"ta":57.43} +{"ev":"vibrate","pattern":"[8]","t":59598.70000001788,"ta":57.995} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":60878.30000001192,"ta":59.275} +{"ev":"vibrate","pattern":"[20]","t":60883.40000000596,"ta":59.28} +{"ev":"start","t":60883.5,"sid":29,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":59.28} +{"ev":"start","t":60883.5,"sid":30,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":59.28} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":61541.40000000596,"ta":59.938} +{"ev":"vibrate","pattern":"[20]","t":61542.70000001788,"ta":59.939} +{"ev":"start","t":61542.90000000596,"sid":31,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":59.939} +{"ev":"start","t":61542.90000000596,"sid":32,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":59.939} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":62287.5,"ta":60.684} +{"ev":"vibrate","pattern":"[20]","t":62288.90000000596,"ta":60.685} +{"ev":"start","t":62289.10000002384,"sid":33,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":60.686} +{"ev":"start","t":62289.10000002384,"sid":34,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":60.686} +{"ev":"fetch","url":"audio/voice/male/tut_done.ogg","status":200,"ok":true,"ms":2,"t":63035.60000002384,"ta":61.432} +{"ev":"vstart","url":"audio/voice/male/tut_done.ogg","t":63039.70000001788,"dur":2.48,"ta":61.436} +{"ev":"vend","url":"audio/voice/male/tut_done.ogg","t":65762.40000000596,"reason":"kill","ta":64.159} +{"ev":"stop","t":65762.70000001788,"sid":1,"when":0,"ta":64.159} +{"ev":"start","t":65763.90000000596,"sid":35,"url":"audio/bgm/track90.ogg","layer":"bgm","dur":82.33301587301587,"rate":1,"loop":true,"when":65.16834467120181,"ta":64.16} +{"ev":"start","t":66204.10000002384,"sid":36,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":65.16834467120181,"ta":64.601} +{"ev":"vibrate","pattern":"[8]","t":66762.10000002384,"ta":65.159} +{"ev":"start","t":66844.10000002384,"sid":37,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":65.83501133786848,"ta":65.241} +{"ev":"vibrate","pattern":"[8]","t":67442.10000002384,"ta":65.839} +{"ev":"start","t":67524,"sid":38,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":66.50167800453514,"ta":65.92} +{"ev":"vibrate","pattern":"[8]","t":68092,"ta":66.488} +{"ev":"start","t":68204.10000002384,"sid":39,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":67.16834467120181,"ta":66.601} +{"ev":"vibrate","pattern":"[8]","t":68765,"ta":67.161} +{"ev":"start","t":68844,"sid":40,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":67.83501133786848,"ta":67.24} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":69411.20000001788,"ta":67.808} +{"ev":"vibrate","pattern":"[20]","t":69413.5,"ta":67.81} +{"ev":"start","t":69413.60000002384,"sid":41,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":67.81} +{"ev":"vibrate","pattern":"[8]","t":69422.10000002384,"ta":67.819} +{"ev":"fetch","url":"audio/voice/male/gd_answer.ogg","status":200,"ok":true,"ms":2,"t":69445.80000001192,"ta":67.842} +{"ev":"vstart","url":"audio/voice/male/gd_answer.ogg","t":69449.20000001788,"dur":1.52,"ta":67.846} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":70073,"ta":68.469} +{"ev":"vibrate","pattern":"[20]","t":70074.90000000596,"ta":68.471} +{"ev":"start","t":70075,"sid":42,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":68.471} +{"ev":"start","t":70204,"sid":43,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":69.16834467120181,"ta":68.6} +{"ev":"vibrate","pattern":"[8]","t":70768,"ta":69.164} +{"ev":"vend","url":"audio/voice/male/gd_answer.ogg","t":71055.90000000596,"reason":"kill","ta":69.452} +{"ev":"vibrate","pattern":"[60,40,60]","t":72564.10000002384,"ta":70.961} +{"ev":"start","t":72564.30000001192,"sid":44,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":70.961} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":73383.90000000596,"ta":71.78} +{"ev":"vibrate","pattern":"[20]","t":73385.30000001192,"ta":71.782} +{"ev":"start","t":73385.5,"sid":45,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":71.782} +{"ev":"start","t":73385.5,"sid":46,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":71.782} +{"ev":"start","t":76843.90000000596,"sid":47,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":75.83501133786848,"ta":75.24} +{"ev":"vibrate","pattern":"[8]","t":77434,"ta":75.83} +{"ev":"start","t":78204.10000002384,"sid":48,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":77.16834467120181,"ta":76.601} +{"ev":"vibrate","pattern":"[8]","t":78758.10000002384,"ta":77.155} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":80079.60000002384,"ta":78.476} +{"ev":"vibrate","pattern":"[20]","t":80081.90000000596,"ta":78.478} +{"ev":"start","t":80081.90000000596,"sid":49,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":78.478} +{"ev":"start","t":80082,"sid":50,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":78.478} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":81402.20000001788,"ta":79.799} +{"ev":"vibrate","pattern":"[20]","t":81403.60000002384,"ta":79.8} +{"ev":"vibrate","pattern":"[20,40,20]","t":81403.70000001788,"ta":79.8} +{"ev":"start","t":81403.80000001192,"sid":51,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":79.8} +{"ev":"start","t":81403.90000000596,"sid":52,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":79.8} +{"ev":"start","t":84844,"sid":53,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":83.83501133786848,"ta":83.24} +{"ev":"vibrate","pattern":"[8]","t":85423,"ta":83.819} +{"ev":"start","t":85524,"sid":54,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":84.50167800453515,"ta":83.92} +{"ev":"vibrate","pattern":"[8]","t":86097.10000002384,"ta":84.494} +{"ev":"start","t":86164.10000002384,"sid":55,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":85.16834467120181,"ta":84.561} +{"ev":"vibrate","pattern":"[8]","t":86753.10000002384,"ta":85.15} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":88090.80000001192,"ta":86.487} +{"ev":"vibrate","pattern":"[20]","t":88092.80000001192,"ta":86.489} +{"ev":"start","t":88092.90000000596,"sid":56,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":86.489} +{"ev":"start","t":88092.90000000596,"sid":57,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":86.489} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":88754.20000001788,"ta":87.151} +{"ev":"vibrate","pattern":"[20]","t":88755.70000001788,"ta":87.152} +{"ev":"start","t":88755.80000001192,"sid":58,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":87.152} +{"ev":"start","t":88755.80000001192,"sid":59,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":87.152} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":89416.70000001788,"ta":87.813} +{"ev":"vibrate","pattern":"[20]","t":89418.20000001788,"ta":87.815} +{"ev":"vibrate","pattern":"[20,40,20]","t":89418.30000001192,"ta":87.815} +{"ev":"start","t":89418.40000000596,"sid":60,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":87.815} +{"ev":"start","t":89418.40000000596,"sid":61,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":87.815} +{"ev":"start","t":92844.10000002384,"sid":62,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":91.83501133786848,"ta":91.241} +{"ev":"vibrate","pattern":"[8]","t":93436.10000002384,"ta":91.833} +{"ev":"start","t":93524.10000002384,"sid":63,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":92.50167800453515,"ta":91.921} +{"ev":"vibrate","pattern":"[8]","t":94086.10000002384,"ta":92.483} +{"ev":"start","t":94204.10000002384,"sid":64,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":93.16834467120181,"ta":92.601} +{"ev":"vibrate","pattern":"[8]","t":94759.20000001788,"ta":93.156} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":96113.70000001788,"ta":94.51} +{"ev":"vibrate","pattern":"[20]","t":96116.70000001788,"ta":94.513} +{"ev":"start","t":96116.80000001192,"sid":65,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":94.513} +{"ev":"start","t":96116.80000001192,"sid":66,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":94.513} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":96776.70000001788,"ta":95.173} +{"ev":"vibrate","pattern":"[20]","t":96778.10000002384,"ta":95.175} +{"ev":"start","t":96778.30000001192,"sid":67,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":95.175} +{"ev":"start","t":96778.30000001192,"sid":68,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":95.175} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":97438.80000001192,"ta":95.835} +{"ev":"vibrate","pattern":"[20]","t":97440.40000000596,"ta":95.837} +{"ev":"vibrate","pattern":"[20,40,20]","t":97440.5,"ta":95.837} +{"ev":"start","t":97440.5,"sid":69,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":95.837} +{"ev":"start","t":97440.60000002384,"sid":70,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":95.837} +{"ev":"start","t":100844,"sid":71,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":99.83501133786848,"ta":99.24} +{"ev":"vibrate","pattern":"[8]","t":101425.10000002384,"ta":99.822} +{"ev":"vstart","url":"audio/voice/male/mid_song.ogg","t":101444.40000000596,"dur":2.32,"ta":99.841} +{"ev":"start","t":101524.10000002384,"sid":72,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":100.50167800453515,"ta":99.921} +{"ev":"vibrate","pattern":"[8]","t":102098,"ta":100.494} +{"ev":"start","t":102164,"sid":73,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":101.16834467120181,"ta":100.56} +{"ev":"vibrate","pattern":"[8]","t":102755.10000002384,"ta":101.152} +{"ev":"start","t":102844,"sid":74,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":101.83501133786848,"ta":101.24} +{"ev":"vibrate","pattern":"[8]","t":103428,"ta":101.824} +{"ev":"vend","url":"audio/voice/male/mid_song.ogg","t":103793.40000000596,"reason":"kill","ta":102.19} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":104043.70000001788,"ta":102.44} +{"ev":"vibrate","pattern":"[20]","t":104045.20000001788,"ta":102.442} +{"ev":"start","t":104045.30000001192,"sid":75,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":102.442} +{"ev":"start","t":104045.30000001192,"sid":76,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":102.442} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":104790,"ta":103.186} +{"ev":"vibrate","pattern":"[20]","t":104792.10000002384,"ta":103.189} +{"ev":"start","t":104792.20000001788,"sid":77,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":103.189} +{"ev":"start","t":104792.20000001788,"sid":78,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":103.189} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":105369.20000001788,"ta":103.766} +{"ev":"vibrate","pattern":"[20]","t":105370.60000002384,"ta":103.767} +{"ev":"start","t":105370.70000001788,"sid":79,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":103.767} +{"ev":"start","t":105370.80000001192,"sid":80,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":103.767} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":106111.40000000596,"ta":104.508} +{"ev":"vibrate","pattern":"[20]","t":106112.70000001788,"ta":104.509} +{"ev":"vibrate","pattern":"[20,40,20]","t":106112.80000001192,"ta":104.509} +{"ev":"start","t":106112.80000001192,"sid":81,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":104.509} +{"ev":"start","t":106112.90000000596,"sid":82,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":104.509} +{"ev":"start","t":106164.10000002384,"sid":83,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":105.16834467120181,"ta":104.561} +{"ev":"vibrate","pattern":"[8]","t":106761.10000002384,"ta":105.158} +{"ev":"start","t":107524.10000002384,"sid":84,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":106.50167800453514,"ta":105.921} +{"ev":"vibrate","pattern":"[8]","t":108107.10000002384,"ta":106.504} +{"ev":"start","t":108204.10000002384,"sid":85,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":107.16834467120181,"ta":106.601} +{"ev":"vibrate","pattern":"[8]","t":108757,"ta":107.153} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":109422.60000002384,"ta":107.819} +{"ev":"vibrate","pattern":"[20]","t":109424.10000002384,"ta":107.821} +{"ev":"start","t":109424.20000001788,"sid":86,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":107.821} +{"ev":"start","t":109424.30000001192,"sid":87,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":107.821} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":110743,"ta":109.139} +{"ev":"vibrate","pattern":"[20]","t":110744.5,"ta":109.141} +{"ev":"start","t":110744.60000002384,"sid":88,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":109.141} +{"ev":"start","t":110744.60000002384,"sid":89,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":109.141} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":111405.40000000596,"ta":109.802} +{"ev":"vibrate","pattern":"[20]","t":111407,"ta":109.803} +{"ev":"vibrate","pattern":"[100]","t":111407,"ta":109.803} +{"ev":"vibrate","pattern":"[20,40,20]","t":111407.10000002384,"ta":109.804} +{"ev":"start","t":111407.20000001788,"sid":90,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":109.804} +{"ev":"start","t":111407.20000001788,"sid":91,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":109.804} +{"ev":"start","t":111524,"sid":92,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":110.50167800453514,"ta":109.92} +{"ev":"vibrate","pattern":"[8]","t":112090.10000002384,"ta":110.487} +{"ev":"start","t":112204,"sid":93,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":111.16834467120181,"ta":110.6} +{"ev":"vibrate","pattern":"[8]","t":112764.10000002384,"ta":111.161} +{"ev":"start","t":112844,"sid":94,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":111.83501133786847,"ta":111.24} +{"ev":"vibrate","pattern":"[8]","t":113420,"ta":111.816} +{"ev":"start","t":113524.20000001788,"sid":95,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":112.50167800453514,"ta":111.921} +{"ev":"vibrate","pattern":"[8]","t":114093.20000001788,"ta":112.49} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":114710,"ta":113.106} +{"ev":"vibrate","pattern":"[20]","t":114711.5,"ta":113.108} +{"ev":"start","t":114711.60000002384,"sid":96,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":113.108} +{"ev":"start","t":114711.60000002384,"sid":97,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":113.108} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":115453.10000002384,"ta":113.85} +{"ev":"vibrate","pattern":"[20]","t":115454.60000002384,"ta":113.851} +{"ev":"start","t":115454.70000001788,"sid":98,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":113.851} +{"ev":"start","t":115454.70000001788,"sid":99,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":113.851} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":116032.10000002384,"ta":114.429} +{"ev":"vibrate","pattern":"[20]","t":116033.70000001788,"ta":114.43} +{"ev":"start","t":116033.90000000596,"sid":100,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":114.43} +{"ev":"start","t":116033.90000000596,"sid":101,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":114.43} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":116772.70000001788,"ta":115.169} +{"ev":"vibrate","pattern":"[20]","t":116774.10000002384,"ta":115.171} +{"ev":"vibrate","pattern":"[20,40,20]","t":116774.30000001192,"ta":115.171} +{"ev":"start","t":116774.30000001192,"sid":102,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":115.171} +{"ev":"start","t":116774.30000001192,"sid":103,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":115.171} +{"ev":"start","t":116844,"sid":104,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":115.83501133786848,"ta":115.24} +{"ev":"vibrate","pattern":"[8]","t":117426.10000002384,"ta":115.823} +{"ev":"start","t":117524.10000002384,"sid":105,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":116.50167800453515,"ta":115.921} +{"ev":"vibrate","pattern":"[8]","t":118100.10000002384,"ta":116.497} +{"ev":"start","t":118164.10000002384,"sid":106,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":117.16834467120181,"ta":116.561} +{"ev":"vibrate","pattern":"[8]","t":118756.10000002384,"ta":117.153} +{"ev":"start","t":118844,"sid":107,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":117.83501133786848,"ta":117.24} +{"ev":"vibrate","pattern":"[8]","t":119429,"ta":117.825} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":120072.5,"ta":118.469} +{"ev":"vibrate","pattern":"[20]","t":120074,"ta":118.47} +{"ev":"start","t":120074.10000002384,"sid":108,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":118.471} +{"ev":"start","t":120074.20000001788,"sid":109,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":118.471} +{"ev":"fetch","url":"audio/voice/male/tut_fill.ogg","status":200,"ok":true,"ms":3,"t":120126.80000001192,"ta":118.523} +{"ev":"vstart","url":"audio/voice/male/tut_fill.ogg","t":120131.80000001192,"dur":3.12,"ta":118.528} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":120733.20000001788,"ta":119.13} +{"ev":"vibrate","pattern":"[20]","t":120734.80000001192,"ta":119.131} +{"ev":"start","t":120734.90000000596,"sid":110,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":119.131} +{"ev":"start","t":120735,"sid":111,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":119.131} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":121395.40000000596,"ta":119.792} +{"ev":"vibrate","pattern":"[20]","t":121396.80000001192,"ta":119.793} +{"ev":"start","t":121397,"sid":112,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":119.793} +{"ev":"start","t":121397,"sid":113,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":119.793} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":122055.60000002384,"ta":120.452} +{"ev":"vibrate","pattern":"[20]","t":122057.20000001788,"ta":120.454} +{"ev":"vibrate","pattern":"[20,40,20]","t":122057.30000001192,"ta":120.454} +{"ev":"start","t":122057.40000000596,"sid":114,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":120.454} +{"ev":"start","t":122057.5,"sid":115,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":120.454} +{"ev":"start","t":122164.10000002384,"sid":116,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":121.16834467120181,"ta":120.561} +{"ev":"start","t":122524,"sid":117,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":121.50167800453514,"ta":120.92} +{"ev":"vibrate","pattern":"[8]","t":122762.10000002384,"ta":121.159} +{"ev":"start","t":122844.10000002384,"sid":118,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":121.83501133786848,"ta":121.241} +{"ev":"vibrate","pattern":"[8]","t":123084,"ta":121.48} +{"ev":"start","t":123204,"sid":119,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":122.16834467120181,"ta":121.6} +{"ev":"vibrate","pattern":"[8]","t":123436.10000002384,"ta":121.833} +{"ev":"start","t":123524,"sid":120,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":122.50167800453514,"ta":121.92} +{"ev":"vend","url":"audio/voice/male/tut_fill.ogg","t":123598.5,"reason":"kill","ta":121.995} +{"ev":"vibrate","pattern":"[8]","t":123757,"ta":122.153} +{"ev":"start","t":123844,"sid":121,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":122.83501133786848,"ta":122.24} +{"ev":"vibrate","pattern":"[8]","t":124086,"ta":122.482} +{"ev":"start","t":124204,"sid":122,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":123.16834467120181,"ta":122.6} +{"ev":"vibrate","pattern":"[8]","t":124437,"ta":122.833} +{"ev":"vibrate","pattern":"[8]","t":124759.10000002384,"ta":123.156} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":125441.70000001788,"ta":123.838} +{"ev":"vibrate","pattern":"[20]","t":125444.30000001192,"ta":123.841} +{"ev":"start","t":125444.5,"sid":123,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":123.841} +{"ev":"start","t":125444.5,"sid":124,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":123.841} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":125906.60000002384,"ta":124.303} +{"ev":"vibrate","pattern":"[20]","t":125908,"ta":124.304} +{"ev":"start","t":125908.10000002384,"sid":125,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":124.305} +{"ev":"start","t":125908.10000002384,"sid":126,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":124.305} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":126370.30000001192,"ta":124.767} +{"ev":"vibrate","pattern":"[20]","t":126371.80000001192,"ta":124.768} +{"ev":"start","t":126371.90000000596,"sid":127,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":124.768} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":126835.10000002384,"ta":125.232} +{"ev":"vibrate","pattern":"[20]","t":126836.40000000596,"ta":125.233} +{"ev":"start","t":126836.60000002384,"sid":128,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":125.233} +{"ev":"start","t":126836.60000002384,"sid":129,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":125.233} +{"ev":"vibrate","pattern":"[60,40,60]","t":126923.90000000596,"ta":125.32} +{"ev":"start","t":126924.10000002384,"sid":130,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":125.321} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":127300.10000002384,"ta":125.697} +{"ev":"vibrate","pattern":"[20]","t":127301.5,"ta":125.698} +{"ev":"vibrate","pattern":"[60,40,60]","t":127301.70000001788,"ta":125.698} +{"ev":"start","t":127302.10000002384,"sid":131,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":125.699} +{"ev":"start","t":127302.20000001788,"sid":132,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":125.699} +{"ev":"fetch","url":"audio/voice/male/gd_miss.ogg","status":200,"ok":true,"ms":2,"t":127303.60000002384,"ta":125.7} +{"ev":"vstart","url":"audio/voice/male/gd_miss.ogg","t":127308.5,"dur":3.2,"ta":125.705} +{"ev":"vibrate","pattern":"[60,40,60]","t":127563.90000000596,"ta":125.96} +{"ev":"start","t":127564.10000002384,"sid":133,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":125.961} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":127765.10000002384,"ta":126.162} +{"ev":"vibrate","pattern":"[20]","t":127766.5,"ta":126.163} +{"ev":"start","t":127766.60000002384,"sid":134,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":126.163} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":128230,"ta":126.626} +{"ev":"vibrate","pattern":"[20]","t":128231.40000000596,"ta":126.628} +{"ev":"start","t":128231.60000002384,"sid":135,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":126.628} +{"ev":"vibrate","pattern":"[100]","t":128404.80000001192,"ta":126.801} +{"ev":"vibrate","pattern":"[100]","t":128404.80000001192,"ta":126.801} +{"ev":"vibrate","pattern":"[20,40,20]","t":128404.80000001192,"ta":126.801} +{"ev":"vend","url":"audio/voice/male/gd_miss.ogg","t":128405.20000001788,"reason":"kill","ta":126.802} +{"ev":"fetch","url":"audio/voice/male/grade_a.ogg","status":200,"ok":true,"ms":2,"t":128407.60000002384,"ta":126.804} +{"ev":"vstart","url":"audio/voice/male/grade_a.ogg","t":128412.60000002384,"dur":4.4,"ta":126.809} +{"ev":"stop","t":129104.5,"sid":35,"when":0,"ta":127.501} +{"ev":"vend","url":"audio/voice/male/grade_a.ogg","t":132978.40000000596,"reason":"kill","ta":131.375} +{"ev":"fetch","url":"audio/voice/male/new_best_combo.ogg","status":200,"ok":true,"ms":2,"t":132980.20000001788,"ta":131.377} +{"ev":"vstart","url":"audio/voice/male/new_best_combo.ogg","t":132983.40000000596,"dur":1.84,"ta":131.38} +{"ev":"vend","url":"audio/voice/male/new_best_combo.ogg","t":134859,"reason":"kill","ta":133.255} +{"ev":"fetch","url":"audio/voice/male/unlock_song2.ogg","status":200,"ok":true,"ms":3,"t":134861.90000000596,"ta":133.258} +{"ev":"vstart","url":"audio/voice/male/unlock_song2.ogg","t":134865.5,"dur":2.48,"ta":133.262} +{"ev":"vend","url":"audio/voice/male/unlock_song2.ogg","t":137575.5,"reason":"kill","ta":135.972} +{"ev":"fetch","url":"audio/voice/male/restart.ogg","status":200,"ok":true,"ms":3,"t":137578.20000001788,"ta":135.975} +{"ev":"vstart","url":"audio/voice/male/restart.ogg","t":137583.90000000596,"dur":4.08,"ta":135.98} +{"ev":"vend","url":"audio/voice/male/restart.ogg","t":141685.30000001192,"reason":"kill","ta":140.082} +{"ev":"start","t":141685.60000002384,"sid":136,"url":"audio/bgm/menu.ogg","layer":"bgm","dur":90,"rate":1,"loop":true,"when":0,"ta":140.082} +{"ev":"recstop","t":293700.5,"ta":292.097} diff --git a/research/rhythm-drummer/round1/live/A/mix.webm b/research/rhythm-drummer/round1/live/A/mix.webm new file mode 100644 index 00000000..36889f92 Binary files /dev/null and b/research/rhythm-drummer/round1/live/A/mix.webm differ diff --git a/research/rhythm-drummer/round1/live/A/transcript.md b/research/rhythm-drummer/round1/live/A/transcript.md new file mode 100644 index 00000000..9f2d0c79 --- /dev/null +++ b/research/rhythm-drummer/round1/live/A/transcript.md @@ -0,0 +1,396 @@ +# rhythm 实时会话 transcript + +- `[ -0.08s]` **GESTURE** tap @(195,422) +- `[ 0.00s]` REC 开始 (audio/webm;codecs=opus) +- `[ 0.06s]` VOICE ▶ welcome (dur 8.24s) +- `[ 0.25s]` BGM ♪ track90 (loop) +- `[ 1.21s]` **GESTURE** tap @(195,422) +- `[ 1.21s]` VIBRATE [20] +- `[ 1.71s]` VOICE ■ welcome (kill) +- `[ 1.71s]` VOICE ▶ tut_map (dur 18.04s) +- `[ 1.71s]` VERDICT welcome: **TRUNCATED** sim=0.3226 + - 期望: 欢迎来到节拍行者。今天你是乐队的鼓手,整面屏幕都是你的鼓。 + - 听到: 欢迎来到节。 +- `[ 2.58s]` **GESTURE** tap @(195,422) +- `[ 2.58s]` VIBRATE [20] +- `[ 3.08s]` VOICE ■ tut_map (kill) +- `[ 3.08s]` VOICE ▶ tut_probe (dur 3.84s) +- `[ 3.08s]` VERDICT tut_map: **TRUNCATED** sim=0.0741 + - 期望: 先认识你的鼓。屏幕横着分三列,左边、中间、右边;竖着分两行,上面清亮,下面低沉。随便摸一摸,听到哪面鼓,我就告诉你它在哪。 + - 听到: 写写认识。 +- `[ 3.95s]` **GESTURE** tap @(195,422) +- `[ 3.95s]` VIBRATE [20] +- `[ 4.45s]` VOICE ■ tut_probe (kill) +- `[ 4.45s]` VERDICT tut_probe: **TRUNCATED** sim=0.4211 + - 期望: 摸到三个不同的地方,我们就开始。 + - 听到: 摸到三个布。 +- `[ 5.32s]` **GESTURE** tap @(65,210) +- `[ 5.32s]` VIBRATE [20] +- `[ 5.32s]` SFX ♪ dm_hat +- `[ 5.33s]` VOICE ▶ zone_tl (dur 1.6s) +- `[ 6.93s]` VOICE ■ zone_tl (kill) +- `[ 6.93s]` VERDICT zone_tl: **OK** sim=0.75 + - 期望: 左上,踩镲。 + - 听到: 左上踩草。 +- `[ 7.99s]` **GESTURE** tap @(325,210) +- `[ 7.99s]` VIBRATE [20] +- `[ 7.99s]` SFX ♪ dm_crash +- `[ 8.00s]` VOICE ▶ zone_tr (dur 1.76s) +- `[ 9.88s]` VOICE ■ zone_tr (kill) +- `[ 9.88s]` VERDICT zone_tr: **MISMATCH** sim=0.25 + - 期望: 右上,吊镲。 + - 听到: 有上调查。 +- `[ 10.66s]` **GESTURE** tap @(65,630) +- `[ 10.66s]` VIBRATE [20] +- `[ 10.66s]` SFX ♪ dm_snare +- `[ 10.67s]` VOICE ▶ zone_bl (dur 2s) +- `[ 10.68s]` VOICE ■ zone_bl (kill) +- `[ 10.68s]` VOICE ▶ tut_z_kick (dur 7.28s) +- `[ 10.68s]` VERDICT zone_bl: **TRUNCATED** sim=0 + - 期望: 左下,军鼓。 + - 听到: 嗯。 +- `[ 18.21s]` VOICE ■ tut_z_kick (kill) +- `[ 18.21s]` VERDICT tut_z_kick: **OK** sim=0.9565 + - 期望: 下面正中间,是底鼓,你的家。先听四下数拍,然后跟着我敲。 + - 听到: 下面正中间是骶鼓,你的家先听四下数拍,然后跟着我敲。 +- `[ 21.41s]` SFX ♪ dm_accent +- `[ 21.99s]` VIBRATE [8] +- `[ 22.09s]` SFX ♪ dm_accent +- `[ 22.66s]` VIBRATE [8] +- `[ 22.73s]` SFX ♪ dm_accent +- `[ 23.32s]` VIBRATE [8] +- `[ 23.41s]` SFX ♪ dm_accent +- `[ 23.99s]` VIBRATE [8] +- `[ 24.09s]` SFX ♪ inst_kick +- `[ 24.67s]` VIBRATE [8] +- `[ 27.29s]` **GESTURE** tap @(195,630) +- `[ 27.29s]` VIBRATE [20] +- `[ 27.29s]` SFX ♪ inst_kick +- `[ 27.29s]` SFX ♪ dm_accent +- `[ 28.10s]` VOICE ▶ tut_z_sides (dur 5.2s) +- `[ 28.10s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 33.33s]` VOICE ■ tut_z_sides (kill) +- `[ 33.33s]` VERDICT tut_z_sides: **OK** sim=0.8824 + - 期望: 左下是军鼓,右下是低嗵。一左一右,跟着打。 + - 听到: 左下是军鼓,右下是敌通,一左一右跟着打。 +- `[ 34.77s]` SFX ♪ dm_accent +- `[ 35.33s]` VIBRATE [8] +- `[ 35.45s]` SFX ♪ dm_accent +- `[ 36.00s]` VIBRATE [8] +- `[ 36.09s]` SFX ♪ dm_accent +- `[ 36.66s]` VIBRATE [8] +- `[ 36.77s]` SFX ♪ dm_accent +- `[ 37.33s]` VIBRATE [8] +- `[ 37.41s]` SFX ♪ dm_snare +- `[ 37.99s]` VIBRATE [8] +- `[ 38.77s]` SFX ♪ dm_tomlo +- `[ 39.33s]` VIBRATE [8] +- `[ 40.63s]` **GESTURE** tap @(65,630) +- `[ 40.64s]` VIBRATE [20] +- `[ 40.64s]` SFX ♪ dm_snare +- `[ 40.64s]` SFX ♪ dm_accent +- `[ 41.96s]` **GESTURE** tap @(325,630) +- `[ 41.97s]` VIBRATE [20] +- `[ 41.97s]` SFX ♪ dm_tomlo +- `[ 41.97s]` SFX ♪ dm_accent +- `[ 42.78s]` VOICE ▶ tut_z_patt (dur 9.68s) +- `[ 42.78s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 52.55s]` VOICE ■ tut_z_patt (kill) +- `[ 52.55s]` VERDICT tut_z_patt: **OK** sim=0.7308 + - 期望: 上面一排更清亮。左上踩镲,上中高嗵,右上吊镲。听这一句,三个音。 + - 听到: 上面一白更清亮,左上采茶,上中高通,右上调查,听这一句三个一。 +- `[ 53.43s]` SFX ♪ dm_accent +- `[ 53.99s]` VIBRATE [8] +- `[ 54.07s]` SFX ♪ dm_accent +- `[ 54.67s]` VIBRATE [8] +- `[ 54.75s]` SFX ♪ dm_accent +- `[ 55.32s]` VIBRATE [8] +- `[ 55.43s]` SFX ♪ dm_accent +- `[ 55.99s]` VIBRATE [8] +- `[ 56.11s]` SFX ♪ dm_hat +- `[ 56.66s]` VIBRATE [8] +- `[ 56.75s]` SFX ♪ dm_tomhi +- `[ 57.32s]` VIBRATE [8] +- `[ 57.43s]` SFX ♪ dm_crash +- `[ 57.99s]` VIBRATE [8] +- `[ 59.27s]` **GESTURE** tap @(65,210) +- `[ 59.28s]` VIBRATE [20] +- `[ 59.28s]` SFX ♪ dm_hat +- `[ 59.28s]` SFX ♪ dm_accent +- `[ 59.94s]` **GESTURE** tap @(195,210) +- `[ 59.94s]` VIBRATE [20] +- `[ 59.94s]` SFX ♪ dm_tomhi +- `[ 59.94s]` SFX ♪ dm_accent +- `[ 60.68s]` **GESTURE** tap @(325,210) +- `[ 60.69s]` VIBRATE [20] +- `[ 60.69s]` SFX ♪ dm_crash +- `[ 60.69s]` SFX ♪ dm_accent +- `[ 61.44s]` VOICE ▶ tut_done (dur 2.48s) +- `[ 61.44s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 64.16s]` VOICE ■ tut_done (kill) +- `[ 64.16s]` VERDICT tut_done: **OK** sim=1 + - 期望: 很好,演出开始。 + - 听到: 很好,演出开始。 +- `[ 64.16s]` BGM ♪ track90 (loop) +- `[ 64.60s]` SFX ♪ inst_kick +- `[ 65.16s]` VIBRATE [8] +- `[ 65.24s]` SFX ♪ inst_kick +- `[ 65.84s]` VIBRATE [8] +- `[ 65.92s]` SFX ♪ inst_kick +- `[ 66.49s]` VIBRATE [8] +- `[ 66.60s]` SFX ♪ inst_kick +- `[ 67.16s]` VIBRATE [8] +- `[ 67.24s]` SFX ♪ inst_kick +- `[ 67.81s]` **GESTURE** tap @(195,630) +- `[ 67.81s]` VIBRATE [20] +- `[ 67.81s]` SFX ♪ inst_kick +- `[ 67.82s]` VIBRATE [8] +- `[ 67.85s]` VOICE ▶ gd_answer (dur 1.52s) +- `[ 67.85s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 68.47s]` **GESTURE** tap @(195,630) +- `[ 68.47s]` VIBRATE [20] +- `[ 68.47s]` SFX ♪ inst_kick +- `[ 68.60s]` SFX ♪ inst_kick +- `[ 69.16s]` VIBRATE [8] +- `[ 69.45s]` VOICE ■ gd_answer (kill) +- `[ 69.45s]` VERDICT gd_answer: **OK** sim=1 + - 期望: 现在跟着我打。 + - 听到: 现在跟着我打。 +- `[ 70.96s]` VIBRATE [60,40,60] +- `[ 70.96s]` SFX ♪ dm_hat +- `[ 71.78s]` **GESTURE** tap @(195,630) +- `[ 71.78s]` VIBRATE [20] +- `[ 71.78s]` SFX ♪ inst_kick +- `[ 71.78s]` SFX ♪ dm_accent +- `[ 75.24s]` SFX ♪ dm_snare +- `[ 75.83s]` VIBRATE [8] +- `[ 76.60s]` SFX ♪ dm_tomlo +- `[ 77.16s]` VIBRATE [8] +- `[ 78.48s]` **GESTURE** tap @(65,630) +- `[ 78.48s]` VIBRATE [20] +- `[ 78.48s]` SFX ♪ dm_snare +- `[ 78.48s]` SFX ♪ dm_accent +- `[ 79.80s]` **GESTURE** tap @(325,630) +- `[ 79.80s]` VIBRATE [20] +- `[ 79.80s]` VIBRATE [20,40,20] +- `[ 79.80s]` SFX ♪ dm_tomlo +- `[ 79.80s]` SFX ♪ dm_accent +- `[ 83.24s]` SFX ♪ inst_kick +- `[ 83.82s]` VIBRATE [8] +- `[ 83.92s]` SFX ♪ inst_kick +- `[ 84.49s]` VIBRATE [8] +- `[ 84.56s]` SFX ♪ dm_snare +- `[ 85.15s]` VIBRATE [8] +- `[ 86.49s]` **GESTURE** tap @(195,630) +- `[ 86.49s]` VIBRATE [20] +- `[ 86.49s]` SFX ♪ inst_kick +- `[ 86.49s]` SFX ♪ dm_accent +- `[ 87.15s]` **GESTURE** tap @(195,630) +- `[ 87.15s]` VIBRATE [20] +- `[ 87.15s]` SFX ♪ inst_kick +- `[ 87.15s]` SFX ♪ dm_accent +- `[ 87.81s]` **GESTURE** tap @(65,630) +- `[ 87.81s]` VIBRATE [20] +- `[ 87.81s]` VIBRATE [20,40,20] +- `[ 87.81s]` SFX ♪ dm_snare +- `[ 87.81s]` SFX ♪ dm_accent +- `[ 91.24s]` SFX ♪ dm_snare +- `[ 91.83s]` VIBRATE [8] +- `[ 91.92s]` SFX ♪ dm_tomlo +- `[ 92.48s]` VIBRATE [8] +- `[ 92.60s]` SFX ♪ inst_kick +- `[ 93.16s]` VIBRATE [8] +- `[ 94.51s]` **GESTURE** tap @(65,630) +- `[ 94.51s]` VIBRATE [20] +- `[ 94.51s]` SFX ♪ dm_snare +- `[ 94.51s]` SFX ♪ dm_accent +- `[ 95.17s]` **GESTURE** tap @(325,630) +- `[ 95.17s]` VIBRATE [20] +- `[ 95.17s]` SFX ♪ dm_tomlo +- `[ 95.17s]` SFX ♪ dm_accent +- `[ 95.83s]` **GESTURE** tap @(195,630) +- `[ 95.84s]` VIBRATE [20] +- `[ 95.84s]` VIBRATE [20,40,20] +- `[ 95.84s]` SFX ♪ inst_kick +- `[ 95.84s]` SFX ♪ dm_accent +- `[ 99.24s]` SFX ♪ inst_kick +- `[ 99.82s]` VIBRATE [8] +- `[ 99.84s]` VOICE ▶ mid_song (dur 2.32s) +- `[ 99.84s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 99.92s]` SFX ♪ dm_snare +- `[ 100.49s]` VIBRATE [8] +- `[ 100.56s]` SFX ♪ inst_kick +- `[ 101.15s]` VIBRATE [8] +- `[ 101.24s]` SFX ♪ dm_tomlo +- `[ 101.82s]` VIBRATE [8] +- `[ 102.19s]` VOICE ■ mid_song (kill) +- `[ 102.19s]` VERDICT mid_song: **OK** sim=0.8 + - 期望: 过半了,保持。 + - 听到: 过磅了,保持。 +- `[ 102.44s]` **GESTURE** tap @(195,630) +- `[ 102.44s]` VIBRATE [20] +- `[ 102.44s]` SFX ♪ inst_kick +- `[ 102.44s]` SFX ♪ dm_accent +- `[ 103.19s]` **GESTURE** tap @(65,630) +- `[ 103.19s]` VIBRATE [20] +- `[ 103.19s]` SFX ♪ dm_snare +- `[ 103.19s]` SFX ♪ dm_accent +- `[ 103.77s]` **GESTURE** tap @(195,630) +- `[ 103.77s]` VIBRATE [20] +- `[ 103.77s]` SFX ♪ inst_kick +- `[ 103.77s]` SFX ♪ dm_accent +- `[ 104.51s]` **GESTURE** tap @(325,630) +- `[ 104.51s]` VIBRATE [20] +- `[ 104.51s]` VIBRATE [20,40,20] +- `[ 104.51s]` SFX ♪ dm_tomlo +- `[ 104.51s]` SFX ♪ dm_accent +- `[ 104.56s]` SFX ♪ dm_hat +- `[ 105.16s]` VIBRATE [8] +- `[ 105.92s]` SFX ♪ dm_snare +- `[ 106.50s]` VIBRATE [8] +- `[ 106.60s]` SFX ♪ inst_kick +- `[ 107.15s]` VIBRATE [8] +- `[ 107.82s]` **GESTURE** tap @(65,210) +- `[ 107.82s]` VIBRATE [20] +- `[ 107.82s]` SFX ♪ dm_hat +- `[ 107.82s]` SFX ♪ dm_accent +- `[ 109.14s]` **GESTURE** tap @(65,630) +- `[ 109.14s]` VIBRATE [20] +- `[ 109.14s]` SFX ♪ dm_snare +- `[ 109.14s]` SFX ♪ dm_accent +- `[ 109.80s]` **GESTURE** tap @(195,630) +- `[ 109.80s]` VIBRATE [20] +- `[ 109.80s]` VIBRATE [100] +- `[ 109.80s]` VIBRATE [20,40,20] +- `[ 109.80s]` SFX ♪ inst_kick +- `[ 109.80s]` SFX ♪ dm_accent +- `[ 109.92s]` SFX ♪ inst_kick +- `[ 110.49s]` VIBRATE [8] +- `[ 110.60s]` SFX ♪ dm_hat +- `[ 111.16s]` VIBRATE [8] +- `[ 111.24s]` SFX ♪ inst_kick +- `[ 111.82s]` VIBRATE [8] +- `[ 111.92s]` SFX ♪ dm_crash +- `[ 112.49s]` VIBRATE [8] +- `[ 113.11s]` **GESTURE** tap @(195,630) +- `[ 113.11s]` VIBRATE [20] +- `[ 113.11s]` SFX ♪ inst_kick +- `[ 113.11s]` SFX ♪ dm_accent +- `[ 113.85s]` **GESTURE** tap @(65,210) +- `[ 113.85s]` VIBRATE [20] +- `[ 113.85s]` SFX ♪ dm_hat +- `[ 113.85s]` SFX ♪ dm_accent +- `[ 114.43s]` **GESTURE** tap @(195,630) +- `[ 114.43s]` VIBRATE [20] +- `[ 114.43s]` SFX ♪ inst_kick +- `[ 114.43s]` SFX ♪ dm_accent +- `[ 115.17s]` **GESTURE** tap @(325,210) +- `[ 115.17s]` VIBRATE [20] +- `[ 115.17s]` VIBRATE [20,40,20] +- `[ 115.17s]` SFX ♪ dm_crash +- `[ 115.17s]` SFX ♪ dm_accent +- `[ 115.24s]` SFX ♪ dm_snare +- `[ 115.82s]` VIBRATE [8] +- `[ 115.92s]` SFX ♪ dm_tomlo +- `[ 116.50s]` VIBRATE [8] +- `[ 116.56s]` SFX ♪ dm_hat +- `[ 117.15s]` VIBRATE [8] +- `[ 117.24s]` SFX ♪ inst_kick +- `[ 117.83s]` VIBRATE [8] +- `[ 118.47s]` **GESTURE** tap @(65,630) +- `[ 118.47s]` VIBRATE [20] +- `[ 118.47s]` SFX ♪ dm_snare +- `[ 118.47s]` SFX ♪ dm_accent +- `[ 118.53s]` VOICE ▶ tut_fill (dur 3.12s) +- `[ 118.53s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 119.13s]` **GESTURE** tap @(325,630) +- `[ 119.13s]` VIBRATE [20] +- `[ 119.13s]` SFX ♪ dm_tomlo +- `[ 119.13s]` SFX ♪ dm_accent +- `[ 119.79s]` **GESTURE** tap @(65,210) +- `[ 119.79s]` VIBRATE [20] +- `[ 119.79s]` SFX ♪ dm_hat +- `[ 119.79s]` SFX ♪ dm_accent +- `[ 120.45s]` **GESTURE** tap @(195,630) +- `[ 120.45s]` VIBRATE [20] +- `[ 120.45s]` VIBRATE [20,40,20] +- `[ 120.45s]` SFX ♪ inst_kick +- `[ 120.45s]` SFX ♪ dm_accent +- `[ 120.56s]` SFX ♪ inst_kick +- `[ 120.92s]` SFX ♪ inst_kick +- `[ 121.16s]` VIBRATE [8] +- `[ 121.24s]` SFX ♪ inst_kick +- `[ 121.48s]` VIBRATE [8] +- `[ 121.60s]` SFX ♪ dm_snare +- `[ 121.83s]` VIBRATE [8] +- `[ 121.92s]` SFX ♪ dm_snare +- `[ 122.00s]` VOICE ■ tut_fill (kill) +- `[ 122.00s]` VERDICT tut_fill: **OK** sim=0.9167 + - 期望: 最后有一段加花,跟着我连打。 + - 听到: 最后有一段佳花跟着我连打。 +- `[ 122.15s]` VIBRATE [8] +- `[ 122.24s]` SFX ♪ inst_kick +- `[ 122.48s]` VIBRATE [8] +- `[ 122.60s]` SFX ♪ dm_crash +- `[ 122.83s]` VIBRATE [8] +- `[ 123.16s]` VIBRATE [8] +- `[ 123.84s]` **GESTURE** tap @(195,630) +- `[ 123.84s]` VIBRATE [20] +- `[ 123.84s]` SFX ♪ inst_kick +- `[ 123.84s]` SFX ♪ dm_accent +- `[ 124.30s]` **GESTURE** tap @(195,630) +- `[ 124.30s]` VIBRATE [20] +- `[ 124.31s]` SFX ♪ inst_kick +- `[ 124.31s]` SFX ♪ dm_accent +- `[ 124.77s]` **GESTURE** tap @(195,630) +- `[ 124.77s]` VIBRATE [20] +- `[ 124.77s]` SFX ♪ inst_kick +- `[ 125.23s]` **GESTURE** tap @(65,630) +- `[ 125.23s]` VIBRATE [20] +- `[ 125.23s]` SFX ♪ dm_snare +- `[ 125.23s]` SFX ♪ dm_accent +- `[ 125.32s]` VIBRATE [60,40,60] +- `[ 125.32s]` SFX ♪ dm_hat +- `[ 125.70s]` **GESTURE** tap @(65,630) +- `[ 125.70s]` VIBRATE [20] +- `[ 125.70s]` VIBRATE [60,40,60] +- `[ 125.70s]` SFX ♪ dm_snare +- `[ 125.70s]` SFX ♪ dm_hat +- `[ 125.70s]` VOICE ▶ gd_miss (dur 3.2s) +- `[ 125.70s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 125.96s]` VIBRATE [60,40,60] +- `[ 125.96s]` SFX ♪ dm_hat +- `[ 126.16s]` **GESTURE** tap @(195,630) +- `[ 126.16s]` VIBRATE [20] +- `[ 126.16s]` SFX ♪ inst_kick +- `[ 126.63s]` **GESTURE** tap @(325,210) +- `[ 126.63s]` VIBRATE [20] +- `[ 126.63s]` SFX ♪ dm_crash +- `[ 126.80s]` VIBRATE [100] +- `[ 126.80s]` VIBRATE [100] +- `[ 126.80s]` VIBRATE [20,40,20] +- `[ 126.80s]` VOICE ■ gd_miss (kill) +- `[ 126.80s]` VERDICT gd_miss: **TRUNCATED** sim=0.4706 + - 期望: 慢了半拍,下一小节跟上我就行。 + - 听到: 慢了半拍。 +- `[ 126.81s]` VOICE ▶ grade_a (dur 4.4s) +- `[ 131.38s]` VOICE ■ grade_a (kill) +- `[ 131.38s]` VERDICT grade_a: **OK** sim=0.6207 + - 期望: A 级!准确率超过八成五,非常出色。 + - 听到: 第一季准确率超过8乘5,非常出色。 +- `[ 131.38s]` VOICE ▶ new_best_combo (dur 1.84s) +- `[ 133.25s]` VOICE ■ new_best_combo (kill) +- `[ 133.25s]` VERDICT new_best_combo: **MISMATCH** sim=0.4 + - 期望: 新连击纪录! + - 听到: 新联机记录。 +- `[ 133.26s]` VOICE ▶ unlock_song2 (dur 2.48s) +- `[ 135.97s]` VOICE ■ unlock_song2 (kill) +- `[ 135.97s]` VERDICT unlock_song2: **OK** sim=0.9333 + - 期望: 新曲目解锁,追风! + - 听到: 路新曲目解锁追风。 +- `[ 135.98s]` VOICE ▶ restart (dur 4.08s) +- `[ 140.08s]` VOICE ■ restart (kill) +- `[ 140.08s]` BGM ♪ menu (loop) +- `[ 140.08s]` VERDICT restart: **OK** sim=0.75 + - 期望: 单击再来一遍,右滑下一首,下滑回大厅。 + - 听到: 但气再来一遍,又滑下一手下滑回大厅。 diff --git a/research/rhythm-drummer/round1/live/A/verdicts.json b/research/rhythm-drummer/round1/live/A/verdicts.json new file mode 100644 index 00000000..ce3b8ef5 --- /dev/null +++ b/research/rhythm-drummer/round1/live/A/verdicts.json @@ -0,0 +1,473 @@ +{ + "game": "rhythm", + "settings": { + "voice": "male", + "guide": "full", + "speed": 1, + "bgm": 1 + }, + "outdir": "/home/xsl/AutoVideo/SoundGame/web/harness/live/out/rhythm-20260914095806", + "segs": [ + { + "idx": 0, + "voiceId": "welcome", + "t0": 0.062, + "t1": 1.711, + "dur": 8.24, + "expEnd": 8.302, + "truncated": true, + "reason": "kill", + "verdict": "TRUNCATED", + "heard": "欢迎来到节。", + "similarity": 0.3226 + }, + { + "idx": 1, + "voiceId": "tut_map", + "t0": 1.711, + "t1": 3.08, + "dur": 18.04, + "expEnd": 19.751, + "truncated": true, + "reason": "kill", + "verdict": "TRUNCATED", + "heard": "写写认识。", + "similarity": 0.0741 + }, + { + "idx": 2, + "voiceId": "tut_probe", + "t0": 3.08, + "t1": 4.449, + "dur": 3.84, + "expEnd": 6.92, + "truncated": true, + "reason": "kill", + "verdict": "TRUNCATED", + "heard": "摸到三个布。", + "similarity": 0.4211 + }, + { + "idx": 3, + "voiceId": "zone_tl", + "t0": 5.325, + "t1": 6.926, + "dur": 1.6, + "expEnd": 6.925, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "左上踩草。", + "similarity": 0.75 + }, + { + "idx": 4, + "voiceId": "zone_tr", + "t0": 7.997, + "t1": 9.875, + "dur": 1.76, + "expEnd": 9.757, + "truncated": false, + "reason": "kill", + "verdict": "MISMATCH", + "heard": "有上调查。", + "similarity": 0.25 + }, + { + "idx": 5, + "voiceId": "zone_bl", + "t0": 10.668, + "t1": 10.675, + "dur": 2, + "expEnd": 12.668, + "truncated": true, + "reason": "kill", + "verdict": "TRUNCATED", + "heard": "嗯。", + "similarity": 0 + }, + { + "idx": 6, + "voiceId": "tut_z_kick", + "t0": 10.675, + "t1": 18.21, + "dur": 7.28, + "expEnd": 17.955, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "下面正中间是骶鼓,你的家先听四下数拍,然后跟着我敲。", + "similarity": 0.9565 + }, + { + "idx": 7, + "voiceId": "tut_z_sides", + "t0": 28.101, + "t1": 33.325, + "dur": 5.2, + "expEnd": 33.301, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "左下是军鼓,右下是敌通,一左一右跟着打。", + "similarity": 0.8824 + }, + { + "idx": 8, + "voiceId": "tut_z_patt", + "t0": 42.778, + "t1": 52.55, + "dur": 9.68, + "expEnd": 52.458, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "上面一白更清亮,左上采茶,上中高通,右上调查,听这一句三个一。", + "similarity": 0.7308 + }, + { + "idx": 9, + "voiceId": "tut_done", + "t0": 61.436, + "t1": 64.159, + "dur": 2.48, + "expEnd": 63.916, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "很好,演出开始。", + "similarity": 1 + }, + { + "idx": 10, + "voiceId": "gd_answer", + "t0": 67.846, + "t1": 69.452, + "dur": 1.52, + "expEnd": 69.366, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "现在跟着我打。", + "similarity": 1 + }, + { + "idx": 11, + "voiceId": "mid_song", + "t0": 99.841, + "t1": 102.19, + "dur": 2.32, + "expEnd": 102.161, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "过磅了,保持。", + "similarity": 0.8 + }, + { + "idx": 12, + "voiceId": "tut_fill", + "t0": 118.528, + "t1": 121.995, + "dur": 3.12, + "expEnd": 121.648, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "最后有一段佳花跟着我连打。", + "similarity": 0.9167 + }, + { + "idx": 13, + "voiceId": "gd_miss", + "t0": 125.705, + "t1": 126.802, + "dur": 3.2, + "expEnd": 128.905, + "truncated": true, + "reason": "kill", + "verdict": "TRUNCATED", + "heard": "慢了半拍。", + "similarity": 0.4706 + }, + { + "idx": 14, + "voiceId": "grade_a", + "t0": 126.809, + "t1": 131.375, + "dur": 4.4, + "expEnd": 131.209, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "第一季准确率超过8乘5,非常出色。", + "similarity": 0.6207 + }, + { + "idx": 15, + "voiceId": "new_best_combo", + "t0": 131.38, + "t1": 133.255, + "dur": 1.84, + "expEnd": 133.22, + "truncated": false, + "reason": "kill", + "verdict": "MISMATCH", + "heard": "新联机记录。", + "similarity": 0.4 + }, + { + "idx": 16, + "voiceId": "unlock_song2", + "t0": 133.262, + "t1": 135.972, + "dur": 2.48, + "expEnd": 135.742, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "路新曲目解锁追风。", + "similarity": 0.9333 + }, + { + "idx": 17, + "voiceId": "restart", + "t0": 135.98, + "t1": 140.082, + "dur": 4.08, + "expEnd": 140.06, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "但气再来一遍,又滑下一手下滑回大厅。", + "similarity": 0.75 + } + ], + "rows": [ + { + "t0": 0.062, + "t1": 1.711, + "voiceId": "welcome", + "expected": "欢迎来到节拍行者。今天你是乐队的鼓手,整面屏幕都是你的鼓。", + "heard": "欢迎来到节。", + "similarity": 0.3226, + "verdict": "TRUNCATED" + }, + { + "t0": 1.711, + "t1": 3.08, + "voiceId": "tut_map", + "expected": "先认识你的鼓。屏幕横着分三列,左边、中间、右边;竖着分两行,上面清亮,下面低沉。随便摸一摸,听到哪面鼓,我就告诉你它在哪。", + "heard": "写写认识。", + "similarity": 0.0741, + "verdict": "TRUNCATED" + }, + { + "t0": 3.08, + "t1": 4.449, + "voiceId": "tut_probe", + "expected": "摸到三个不同的地方,我们就开始。", + "heard": "摸到三个布。", + "similarity": 0.4211, + "verdict": "TRUNCATED" + }, + { + "t0": 5.325, + "t1": 6.926, + "voiceId": "zone_tl", + "expected": "左上,踩镲。", + "heard": "左上踩草。", + "similarity": 0.75, + "verdict": "OK" + }, + { + "t0": 7.997, + "t1": 9.875, + "voiceId": "zone_tr", + "expected": "右上,吊镲。", + "heard": "有上调查。", + "similarity": 0.25, + "verdict": "MISMATCH" + }, + { + "t0": 10.668, + "t1": 10.675, + "voiceId": "zone_bl", + "expected": "左下,军鼓。", + "heard": "嗯。", + "similarity": 0, + "verdict": "TRUNCATED" + }, + { + "t0": 10.675, + "t1": 18.21, + "voiceId": "tut_z_kick", + "expected": "下面正中间,是底鼓,你的家。先听四下数拍,然后跟着我敲。", + "heard": "下面正中间是骶鼓,你的家先听四下数拍,然后跟着我敲。", + "similarity": 0.9565, + "verdict": "OK" + }, + { + "t0": 28.101, + "t1": 33.325, + "voiceId": "tut_z_sides", + "expected": "左下是军鼓,右下是低嗵。一左一右,跟着打。", + "heard": "左下是军鼓,右下是敌通,一左一右跟着打。", + "similarity": 0.8824, + "verdict": "OK" + }, + { + "t0": 42.778, + "t1": 52.55, + "voiceId": "tut_z_patt", + "expected": "上面一排更清亮。左上踩镲,上中高嗵,右上吊镲。听这一句,三个音。", + "heard": "上面一白更清亮,左上采茶,上中高通,右上调查,听这一句三个一。", + "similarity": 0.7308, + "verdict": "OK" + }, + { + "t0": 61.436, + "t1": 64.159, + "voiceId": "tut_done", + "expected": "很好,演出开始。", + "heard": "很好,演出开始。", + "similarity": 1, + "verdict": "OK" + }, + { + "t0": 67.846, + "t1": 69.452, + "voiceId": "gd_answer", + "expected": "现在跟着我打。", + "heard": "现在跟着我打。", + "similarity": 1, + "verdict": "OK" + }, + { + "t0": 99.841, + "t1": 102.19, + "voiceId": "mid_song", + "expected": "过半了,保持。", + "heard": "过磅了,保持。", + "similarity": 0.8, + "verdict": "OK" + }, + { + "t0": 118.528, + "t1": 121.995, + "voiceId": "tut_fill", + "expected": "最后有一段加花,跟着我连打。", + "heard": "最后有一段佳花跟着我连打。", + "similarity": 0.9167, + "verdict": "OK" + }, + { + "t0": 125.705, + "t1": 126.802, + "voiceId": "gd_miss", + "expected": "慢了半拍,下一小节跟上我就行。", + "heard": "慢了半拍。", + "similarity": 0.4706, + "verdict": "TRUNCATED" + }, + { + "t0": 126.809, + "t1": 131.375, + "voiceId": "grade_a", + "expected": "A 级!准确率超过八成五,非常出色。", + "heard": "第一季准确率超过8乘5,非常出色。", + "similarity": 0.6207, + "verdict": "OK" + }, + { + "t0": 131.38, + "t1": 133.255, + "voiceId": "new_best_combo", + "expected": "新连击纪录!", + "heard": "新联机记录。", + "similarity": 0.4, + "verdict": "MISMATCH" + }, + { + "t0": 133.262, + "t1": 135.972, + "voiceId": "unlock_song2", + "expected": "新曲目解锁,追风!", + "heard": "路新曲目解锁追风。", + "similarity": 0.9333, + "verdict": "OK" + }, + { + "t0": 135.98, + "t1": 140.082, + "voiceId": "restart", + "expected": "单击再来一遍,右滑下一首,下滑回大厅。", + "heard": "但气再来一遍,又滑下一手下滑回大厅。", + "similarity": 0.75, + "verdict": "OK" + }, + { + "t0": 18.21, + "t1": 28.101, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 33.325, + "t1": 42.778, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 52.55, + "t1": 61.436, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 64.159, + "t1": 67.846, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 69.452, + "t1": 99.841, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 102.19, + "t1": 118.528, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 121.995, + "t1": 125.705, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + } + ] +} \ No newline at end of file diff --git a/research/rhythm-drummer/round1/live/B/events.jsonl b/research/rhythm-drummer/round1/live/B/events.jsonl new file mode 100644 index 00000000..d11743a8 --- /dev/null +++ b/research/rhythm-drummer/round1/live/B/events.jsonl @@ -0,0 +1,329 @@ +{"ev":"probe_ready","t":8,"ta":null} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":1521.800000011921,"ta":-0.074} +{"ev":"recstart","mime":"audio/webm;codecs=opus","t":1596.2000000178814,"ta":0} +{"ev":"fetch","url":"audio/bgm/menu.ogg","status":200,"ok":true,"ms":13,"t":1611,"ta":0.015} +{"ev":"fetch","url":"audio/bgm/track90.ogg","status":200,"ok":true,"ms":14,"t":1611.300000011921,"ta":0.015} +{"ev":"fetch","url":"audio/voice/female/welcome.ogg","status":200,"ok":true,"ms":14,"t":1611.4000000059605,"ta":0.015} +{"ev":"fetch","url":"audio/voice/female/select_hint.ogg","status":200,"ok":true,"ms":14,"t":1611.4000000059605,"ta":0.015} +{"ev":"fetch","url":"audio/voice/female/tut_probe.ogg","status":200,"ok":true,"ms":14,"t":1611.5,"ta":0.015} +{"ev":"fetch","url":"audio/voice/female/tut_map.ogg","status":200,"ok":true,"ms":14,"t":1611.5999999940395,"ta":0.015} +{"ev":"fetch","url":"audio/voice/female/mid_song.ogg","status":200,"ok":true,"ms":14,"t":1611.5999999940395,"ta":0.015} +{"ev":"fetch","url":"audio/voice/female/welcome_back.ogg","status":200,"ok":true,"ms":15,"t":1611.7000000178814,"ta":0.016} +{"ev":"fetch","url":"audio/bgm/track105.ogg","status":200,"ok":true,"ms":14,"t":1611.800000011921,"ta":0.016} +{"ev":"fetch","url":"audio/bgm/track120.ogg","status":200,"ok":true,"ms":14,"t":1611.800000011921,"ta":0.016} +{"ev":"fetch","url":"audio/sfx/dm_snare.ogg","status":200,"ok":true,"ms":21,"t":1618,"ta":0.022} +{"ev":"fetch","url":"audio/sfx/dm_tomlo.ogg","status":200,"ok":true,"ms":21,"t":1618.2000000178814,"ta":0.022} +{"ev":"fetch","url":"audio/sfx/inst_kick.ogg","status":200,"ok":true,"ms":22,"t":1619.0999999940395,"ta":0.023} +{"ev":"fetch","url":"audio/sfx/dm_hat.ogg","status":200,"ok":true,"ms":23,"t":1619.5,"ta":0.023} +{"ev":"fetch","url":"audio/sfx/dm_tomhi.ogg","status":200,"ok":true,"ms":23,"t":1619.5,"ta":0.023} +{"ev":"fetch","url":"audio/sfx/dm_crash.ogg","status":200,"ok":true,"ms":24,"t":1620.800000011921,"ta":0.025} +{"ev":"fetch","url":"audio/sfx/dm_accent.ogg","status":200,"ok":true,"ms":28,"t":1625.2000000178814,"ta":0.029} +{"ev":"vstart","url":"audio/voice/female/welcome.ogg","t":1650.5,"dur":6.24,"ta":0.054} +{"ev":"start","t":1844.5999999940395,"sid":1,"url":"audio/bgm/track90.ogg","layer":"bgm","dur":82.33301587301587,"rate":1,"loop":true,"when":6,"ta":0.248} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":2804.7000000178814,"ta":1.209} +{"ev":"vibrate","pattern":"[20]","t":2806.300000011921,"ta":1.21} +{"ev":"vend","url":"audio/voice/female/welcome.ogg","t":3306.9000000059605,"reason":"kill","ta":1.711} +{"ev":"vstart","url":"audio/voice/female/tut_map.ogg","t":3307.0999999940395,"dur":14.28,"ta":1.711} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":4174.200000017881,"ta":2.578} +{"ev":"vibrate","pattern":"[20]","t":4175.9000000059605,"ta":2.58} +{"ev":"vend","url":"audio/voice/female/tut_map.ogg","t":4676,"reason":"kill","ta":3.08} +{"ev":"vstart","url":"audio/voice/female/tut_probe.ogg","t":4676.200000017881,"dur":3.76,"ta":3.08} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":5542.9000000059605,"ta":3.947} +{"ev":"vibrate","pattern":"[20]","t":5544.5,"ta":3.948} +{"ev":"vend","url":"audio/voice/female/tut_probe.ogg","t":6044.800000011921,"reason":"kill","ta":4.449} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":6913.700000017881,"ta":5.318} +{"ev":"vibrate","pattern":"[20]","t":6915.200000017881,"ta":5.319} +{"ev":"start","t":6915.800000011921,"sid":2,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":5.32} +{"ev":"fetch","url":"audio/voice/female/zone_tl.ogg","status":200,"ok":true,"ms":3,"t":6917.9000000059605,"ta":5.322} +{"ev":"vstart","url":"audio/voice/female/zone_tl.ogg","t":6921.5999999940395,"dur":1.68,"ta":5.325} +{"ev":"vend","url":"audio/voice/female/zone_tl.ogg","t":8139.700000017881,"reason":"kill","ta":6.544} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":9182.800000011921,"ta":7.587} +{"ev":"vibrate","pattern":"[20]","t":9186.300000011921,"ta":7.59} +{"ev":"start","t":9186.59999999404,"sid":3,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":7.59} +{"ev":"fetch","url":"audio/voice/female/zone_bc.ogg","status":200,"ok":true,"ms":2,"t":9188.59999999404,"ta":7.592} +{"ev":"vstart","url":"audio/voice/female/zone_bc.ogg","t":9192.09999999404,"dur":2,"ta":7.596} +{"ev":"vend","url":"audio/voice/female/zone_bc.ogg","t":10600.5,"reason":"kill","ta":9.004} +{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":10853.40000000596,"ta":9.257} +{"ev":"vibrate","pattern":"[20]","t":10855.09999999404,"ta":9.259} +{"ev":"start","t":10855.300000011921,"sid":4,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":9.259} +{"ev":"vibrate","pattern":"[10]","t":11208,"ta":9.612} +{"ev":"fetch","url":"audio/voice/female/tut_skip.ogg","status":200,"ok":true,"ms":3,"t":11211,"ta":9.615} +{"ev":"vstart","url":"audio/voice/female/tut_skip.ogg","t":11214,"dur":2,"ta":9.618} +{"ev":"vend","url":"audio/voice/female/tut_skip.ogg","t":12620.40000000596,"reason":"kill","ta":11.024} +{"ev":"stop","t":12620.59999999404,"sid":1,"when":0,"ta":11.024} +{"ev":"start","t":12621.700000017881,"sid":5,"url":"audio/bgm/track90.ogg","layer":"bgm","dur":82.33301587301587,"rate":1,"loop":true,"when":12.04108843537415,"ta":11.026} +{"ev":"start","t":13062,"sid":6,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":12.04108843537415,"ta":11.466} +{"ev":"vibrate","pattern":"[8]","t":13620.09999999404,"ta":12.024} +{"ev":"start","t":13701.90000000596,"sid":7,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":12.707755102040815,"ta":12.106} +{"ev":"vibrate","pattern":"[8]","t":14299.90000000596,"ta":12.704} +{"ev":"start","t":14381.800000011921,"sid":8,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":13.374421768707483,"ta":12.786} +{"ev":"vibrate","pattern":"[8]","t":14949.90000000596,"ta":13.354} +{"ev":"start","t":15061.90000000596,"sid":9,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":14.04108843537415,"ta":13.466} +{"ev":"vibrate","pattern":"[8]","t":15622.800000011921,"ta":14.027} +{"ev":"start","t":15701.800000011921,"sid":10,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":14.707755102040815,"ta":14.106} +{"ev":"vibrate","pattern":"[8]","t":16279.800000011921,"ta":14.684} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":16300.09999999404,"ta":14.704} +{"ev":"vibrate","pattern":"[20]","t":16301.40000000596,"ta":14.705} +{"ev":"start","t":16301.700000017881,"sid":11,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":14.706} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":16964.09999999404,"ta":15.368} +{"ev":"vibrate","pattern":"[20]","t":16965.40000000596,"ta":15.369} +{"ev":"start","t":16965.59999999404,"sid":12,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":15.369} +{"ev":"start","t":17061.80000001192,"sid":13,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":16.04108843537415,"ta":15.466} +{"ev":"vibrate","pattern":"[8]","t":17625.90000000596,"ta":16.03} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":18292.59999999404,"ta":16.696} +{"ev":"vibrate","pattern":"[20]","t":18295.5,"ta":16.699} +{"ev":"start","t":18295.59999999404,"sid":14,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":16.699} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":18955.90000000596,"ta":17.36} +{"ev":"vibrate","pattern":"[20]","t":18957.40000000596,"ta":17.361} +{"ev":"vibrate","pattern":"[60,40,60]","t":18957.5,"ta":17.361} +{"ev":"start","t":18957.59999999404,"sid":15,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":17.361} +{"ev":"start","t":18957.70000001788,"sid":16,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":17.362} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":20286,"ta":18.69} +{"ev":"vibrate","pattern":"[20]","t":20287.40000000596,"ta":18.691} +{"ev":"vibrate","pattern":"[60,40,60]","t":20287.5,"ta":18.691} +{"ev":"start","t":20287.70000001788,"sid":17,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":18.692} +{"ev":"start","t":20287.70000001788,"sid":18,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":18.692} +{"ev":"start","t":23701.90000000596,"sid":19,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":22.707755102040814,"ta":22.106} +{"ev":"vibrate","pattern":"[8]","t":24291.90000000596,"ta":22.696} +{"ev":"start","t":25061.80000001192,"sid":20,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":24.041088435374146,"ta":23.466} +{"ev":"vibrate","pattern":"[8]","t":25615.80000001192,"ta":24.02} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":26931,"ta":25.335} +{"ev":"vibrate","pattern":"[20]","t":26932.80000001192,"ta":25.337} +{"ev":"vibrate","pattern":"[60,40,60]","t":26932.90000000596,"ta":25.337} +{"ev":"start","t":26933,"sid":21,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":25.337} +{"ev":"start","t":26933,"sid":22,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":25.337} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":28262,"ta":26.666} +{"ev":"vibrate","pattern":"[20]","t":28263.5,"ta":26.667} +{"ev":"vibrate","pattern":"[60,40,60]","t":28263.59999999404,"ta":26.667} +{"ev":"start","t":28263.70000001788,"sid":23,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":26.668} +{"ev":"start","t":28263.70000001788,"sid":24,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":26.668} +{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":28984.30000001192,"ta":27.388} +{"ev":"vibrate","pattern":"[20]","t":28985.80000001192,"ta":27.39} +{"ev":"start","t":28985.90000000596,"sid":25,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":27.39} +{"ev":"vibrate","pattern":"[10]","t":29340,"ta":27.744} +{"ev":"stop","t":29340.40000000596,"sid":5,"when":0,"ta":27.744} +{"ev":"fetch","url":"audio/voice/female/menu_main.ogg","status":200,"ok":true,"ms":3,"t":29342.90000000596,"ta":27.747} +{"ev":"vstart","url":"audio/voice/female/menu_main.ogg","t":29349.30000001192,"dur":5.6,"ta":27.753} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":31245.40000000596,"ta":29.649} +{"ev":"vibrate","pattern":"[20]","t":31246.90000000596,"ta":29.651} +{"ev":"vend","url":"audio/voice/female/menu_main.ogg","t":31747.30000001192,"reason":"kill","ta":30.151} +{"ev":"fetch","url":"audio/voice/female/menu_lobby.ogg","status":200,"ok":true,"ms":3,"t":31749.90000000596,"ta":30.154} +{"ev":"vstart","url":"audio/voice/female/menu_lobby.ogg","t":31752.09999999404,"dur":0.6370975056689342,"ta":30.156} +{"ev":"vend","url":"audio/voice/female/menu_lobby.ogg","t":32239.59999999404,"reason":"kill","ta":30.643} +{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":33213.20000001788,"ta":31.617} +{"ev":"vibrate","pattern":"[20]","t":33214.5,"ta":31.618} +{"ev":"vibrate","pattern":"[10]","t":33573.40000000596,"ta":31.977} +{"ev":"fetch","url":"audio/voice/female/rules.ogg","status":200,"ok":true,"ms":2,"t":33575.80000001192,"ta":31.98} +{"ev":"vstart","url":"audio/voice/female/rules.ogg","t":33582.5,"dur":7.68,"ta":31.986} +{"ev":"vend","url":"audio/voice/female/rules.ogg","t":38253.09999999404,"reason":"kill","ta":36.657} +{"ev":"fetch","url":"audio/voice/female/settings_hint.ogg","status":200,"ok":true,"ms":3,"t":38256.09999999404,"ta":36.66} +{"ev":"vstart","url":"audio/voice/female/settings_hint.ogg","t":38259.30000001192,"dur":1.36,"ta":36.663} +{"ev":"vend","url":"audio/voice/female/settings_hint.ogg","t":39205,"reason":"kill","ta":37.609} +{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":39978.30000001192,"ta":38.382} +{"ev":"vibrate","pattern":"[20]","t":39983.70000001788,"ta":38.388} +{"ev":"vibrate","pattern":"[10]","t":40340,"ta":38.744} +{"ev":"start","t":40340.30000001192,"sid":26,"url":"audio/bgm/track90.ogg","layer":"bgm","dur":82.33301587301587,"rate":1,"loop":true,"when":41.532380952380954,"ta":38.744} +{"ev":"start","t":40380.5,"sid":27,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":38.86571428571429,"ta":38.784} +{"ev":"vibrate","pattern":"[8]","t":40456.5,"ta":38.86} +{"ev":"start","t":40540.5,"sid":28,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":39.532380952380954,"ta":38.944} +{"ev":"vibrate","pattern":"[8]","t":41120.59999999404,"ta":39.524} +{"ev":"start","t":41180.40000000596,"sid":29,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":40.199047619047626,"ta":39.584} +{"ev":"vibrate","pattern":"[8]","t":41777.5,"ta":40.181} +{"ev":"start","t":41860.5,"sid":30,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":40.86571428571429,"ta":40.264} +{"ev":"vibrate","pattern":"[8]","t":42450.59999999404,"ta":40.854} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":43122.30000001192,"ta":41.526} +{"ev":"vibrate","pattern":"[20]","t":43124.30000001192,"ta":41.528} +{"ev":"start","t":43124.5,"sid":31,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":41.528} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":44458.20000001788,"ta":42.862} +{"ev":"vibrate","pattern":"[20]","t":44459.80000001192,"ta":42.864} +{"ev":"start","t":44460,"sid":32,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":42.864} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":45122.80000001192,"ta":43.527} +{"ev":"vibrate","pattern":"[20]","t":45124.20000001788,"ta":43.528} +{"ev":"start","t":45124.30000001192,"sid":33,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":43.528} +{"ev":"start","t":45500.5,"sid":34,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":44.48068027210884,"ta":43.904} +{"ev":"vibrate","pattern":"[8]","t":46060.40000000596,"ta":44.464} +{"ev":"start","t":46140.5,"sid":35,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":45.14734693877551,"ta":44.544} +{"ev":"vibrate","pattern":"[8]","t":46716.5,"ta":45.12} +{"ev":"start","t":46820.5,"sid":36,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":45.81401360544218,"ta":45.224} +{"ev":"vibrate","pattern":"[8]","t":47389.5,"ta":45.793} +{"ev":"vibrate","pattern":"[60,40,60]","t":49220.5,"ta":47.624} +{"ev":"start","t":49220.70000001788,"sid":37,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":47.625} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":49370.90000000596,"ta":47.775} +{"ev":"vibrate","pattern":"[20]","t":49372.90000000596,"ta":47.777} +{"ev":"vibrate","pattern":"[60,40,60]","t":49372.90000000596,"ta":47.777} +{"ev":"start","t":49373,"sid":38,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":47.777} +{"ev":"start","t":49373,"sid":39,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":47.777} +{"ev":"gesture","type":"longpress","x":195,"y":422,"t":49835.5,"ta":48.239} +{"ev":"vibrate","pattern":"[20]","t":49836.90000000596,"ta":48.241} +{"ev":"start","t":49837,"sid":40,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":48.241} +{"ev":"vibrate","pattern":"[60,40,60]","t":50287.30000001192,"ta":48.691} +{"ev":"start","t":50287.40000000596,"sid":41,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":48.691} +{"ev":"vibrate","pattern":"[60,40,60]","t":50540.40000000596,"ta":48.944} +{"ev":"start","t":50540.5,"sid":42,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":48.944} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":50839.40000000596,"ta":49.243} +{"ev":"vibrate","pattern":"[20]","t":50841,"ta":49.245} +{"ev":"start","t":50841.09999999404,"sid":43,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":49.245} +{"ev":"start","t":53460.5,"sid":44,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":52.48068027210884,"ta":51.864} +{"ev":"vibrate","pattern":"[8]","t":54055.5,"ta":52.459} +{"ev":"start","t":54140.5,"sid":45,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":53.147346938775506,"ta":52.544} +{"ev":"vibrate","pattern":"[8]","t":54728.59999999404,"ta":53.132} +{"ev":"start","t":54820.5,"sid":46,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":53.81401360544218,"ta":53.224} +{"ev":"vibrate","pattern":"[8]","t":55402.5,"ta":53.806} +{"ev":"vibrate","pattern":"[60,40,60]","t":57180.40000000596,"ta":55.584} +{"ev":"start","t":57180.5,"sid":47,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":55.584} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":57342.90000000596,"ta":55.747} +{"ev":"vibrate","pattern":"[20]","t":57344.5,"ta":55.748} +{"ev":"vibrate","pattern":"[60,40,60]","t":57344.5,"ta":55.748} +{"ev":"start","t":57344.70000001788,"sid":48,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":55.749} +{"ev":"start","t":57344.70000001788,"sid":49,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":55.749} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":58087,"ta":56.491} +{"ev":"vibrate","pattern":"[20]","t":58088.59999999404,"ta":56.492} +{"ev":"vibrate","pattern":"[60,40,60]","t":58088.59999999404,"ta":56.492} +{"ev":"start","t":58088.70000001788,"sid":50,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":56.493} +{"ev":"start","t":58088.80000001192,"sid":51,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":56.493} +{"ev":"start","t":61500.5,"sid":52,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":60.48068027210884,"ta":59.904} +{"ev":"vibrate","pattern":"[8]","t":62061.5,"ta":60.465} +{"ev":"vstart","url":"audio/voice/female/mid_song.ogg","t":62100.59999999404,"dur":1.6,"ta":60.504} +{"ev":"start","t":62140.40000000596,"sid":53,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":61.147346938775506,"ta":60.544} +{"ev":"vibrate","pattern":"[8]","t":62718.40000000596,"ta":61.122} +{"ev":"start","t":62820.5,"sid":54,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":61.81401360544218,"ta":61.224} +{"ev":"vend","url":"audio/voice/female/mid_song.ogg","t":63049.90000000596,"reason":"kill","ta":61.454} +{"ev":"vibrate","pattern":"[8]","t":63391.5,"ta":61.795} +{"ev":"start","t":63500.5,"sid":55,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":62.48068027210884,"ta":61.904} +{"ev":"vibrate","pattern":"[8]","t":64064.5,"ta":62.468} +{"ev":"vibrate","pattern":"[60,40,60]","t":65180.40000000596,"ta":63.584} +{"ev":"start","t":65180.40000000596,"sid":56,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":63.584} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":65376.20000001788,"ta":63.78} +{"ev":"vibrate","pattern":"[20]","t":65377.90000000596,"ta":63.782} +{"ev":"vibrate","pattern":"[60,40,60]","t":65377.90000000596,"ta":63.782} +{"ev":"start","t":65378,"sid":57,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":63.782} +{"ev":"start","t":65378,"sid":58,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":63.782} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":66039.5,"ta":64.443} +{"ev":"vibrate","pattern":"[20]","t":66041.20000001788,"ta":64.445} +{"ev":"vibrate","pattern":"[60,40,60]","t":66041.30000001192,"ta":64.445} +{"ev":"start","t":66041.30000001192,"sid":59,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":64.445} +{"ev":"start","t":66041.30000001192,"sid":60,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":64.445} +{"ev":"start","t":66820.5,"sid":61,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":65.81401360544217,"ta":65.224} +{"ev":"vibrate","pattern":"[60,40,60]","t":67180.40000000596,"ta":65.584} +{"ev":"start","t":67180.5,"sid":62,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":65.584} +{"ev":"vibrate","pattern":"[8]","t":67397.5,"ta":65.801} +{"ev":"start","t":68140.5,"sid":63,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":67.14734693877551,"ta":66.544} +{"ev":"vibrate","pattern":"[8]","t":68727.5,"ta":67.131} +{"ev":"start","t":68820.5,"sid":64,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":67.81401360544217,"ta":67.224} +{"ev":"vibrate","pattern":"[8]","t":69400.5,"ta":67.804} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":70013.40000000596,"ta":68.417} +{"ev":"vibrate","pattern":"[20]","t":70014.80000001192,"ta":68.419} +{"ev":"vibrate","pattern":"[60,40,60]","t":70014.80000001192,"ta":68.419} +{"ev":"start","t":70014.90000000596,"sid":65,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":68.419} +{"ev":"start","t":70014.90000000596,"sid":66,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":68.419} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":71339,"ta":69.743} +{"ev":"vibrate","pattern":"[20]","t":71340.59999999404,"ta":69.744} +{"ev":"start","t":71340.80000001192,"sid":67,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":69.745} +{"ev":"start","t":71340.80000001192,"sid":68,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":69.745} +{"ev":"start","t":72140.40000000596,"sid":69,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":71.14734693877551,"ta":70.544} +{"ev":"vibrate","pattern":"[60,40,60]","t":72540.40000000596,"ta":70.944} +{"ev":"start","t":72540.5,"sid":70,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":70.944} +{"ev":"vibrate","pattern":"[8]","t":72733.5,"ta":71.137} +{"ev":"start","t":72820.5,"sid":71,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":71.81401360544217,"ta":71.224} +{"ev":"vibrate","pattern":"[8]","t":73383.40000000596,"ta":71.787} +{"ev":"start","t":73500.59999999404,"sid":72,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":72.48068027210884,"ta":71.904} +{"ev":"vibrate","pattern":"[8]","t":74056.5,"ta":72.46} +{"ev":"start","t":74140.59999999404,"sid":73,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":73.14734693877551,"ta":72.544} +{"ev":"vibrate","pattern":"[8]","t":74736.5,"ta":73.14} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":75394.30000001192,"ta":73.798} +{"ev":"vibrate","pattern":"[20]","t":75396.09999999404,"ta":73.8} +{"ev":"vibrate","pattern":"[60,40,60]","t":75396.20000001788,"ta":73.8} +{"ev":"start","t":75396.30000001192,"sid":74,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":73.8} +{"ev":"start","t":75396.30000001192,"sid":75,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":73.8} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":76057.5,"ta":74.461} +{"ev":"vibrate","pattern":"[20]","t":76059,"ta":74.463} +{"ev":"vibrate","pattern":"[60,40,60]","t":76059.09999999404,"ta":74.463} +{"ev":"start","t":76059.09999999404,"sid":76,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":74.463} +{"ev":"start","t":76059.20000001788,"sid":77,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":74.463} +{"ev":"vibrate","pattern":"[60,40,60]","t":77220.40000000596,"ta":75.624} +{"ev":"start","t":77220.40000000596,"sid":78,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":75.624} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":77381.20000001788,"ta":75.785} +{"ev":"vibrate","pattern":"[20]","t":77382.5,"ta":75.786} +{"ev":"vibrate","pattern":"[60,40,60]","t":77382.59999999404,"ta":75.786} +{"ev":"start","t":77382.59999999404,"sid":79,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":75.786} +{"ev":"start","t":77382.59999999404,"sid":80,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":75.786} +{"ev":"start","t":77500.5,"sid":81,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":76.48068027210884,"ta":75.904} +{"ev":"vibrate","pattern":"[8]","t":78063.5,"ta":76.467} +{"ev":"start","t":78140.5,"sid":82,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":77.14734693877551,"ta":76.544} +{"ev":"vibrate","pattern":"[8]","t":78719.5,"ta":77.123} +{"ev":"start","t":78820.59999999404,"sid":83,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":77.81401360544217,"ta":77.224} +{"ev":"vibrate","pattern":"[8]","t":79392.5,"ta":77.796} +{"ev":"start","t":79460.40000000596,"sid":84,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":78.48068027210883,"ta":77.864} +{"ev":"vibrate","pattern":"[8]","t":80049.40000000596,"ta":78.453} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":80694.30000001192,"ta":79.098} +{"ev":"vibrate","pattern":"[20]","t":80695.90000000596,"ta":79.1} +{"ev":"vibrate","pattern":"[60,40,60]","t":80695.90000000596,"ta":79.1} +{"ev":"start","t":80696,"sid":85,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":79.1} +{"ev":"start","t":80696,"sid":86,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":79.1} +{"ev":"fetch","url":"audio/voice/female/tut_fill.ogg","status":200,"ok":true,"ms":3,"t":80743.40000000596,"ta":79.147} +{"ev":"vstart","url":"audio/voice/female/tut_fill.ogg","t":80748.80000001192,"dur":3.68,"ta":79.153} +{"ev":"vibrate","pattern":"[60,40,60]","t":81860.30000001192,"ta":80.264} +{"ev":"start","t":81860.40000000596,"sid":87,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":80.264} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":82016.5,"ta":80.42} +{"ev":"vibrate","pattern":"[20]","t":82017.90000000596,"ta":80.422} +{"ev":"vibrate","pattern":"[60,40,60]","t":82017.90000000596,"ta":80.422} +{"ev":"start","t":82017.90000000596,"sid":88,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":80.422} +{"ev":"start","t":82018,"sid":89,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":80.422} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":82680.09999999404,"ta":81.084} +{"ev":"vibrate","pattern":"[20]","t":82681.5,"ta":81.085} +{"ev":"vibrate","pattern":"[60,40,60]","t":82681.5,"ta":81.085} +{"ev":"start","t":82681.59999999404,"sid":90,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":81.085} +{"ev":"start","t":82681.70000001788,"sid":91,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":81.086} +{"ev":"start","t":82820.40000000596,"sid":92,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":81.81401360544217,"ta":81.224} +{"ev":"vend","url":"audio/voice/female/tut_fill.ogg","t":82994.20000001788,"reason":"kill","ta":81.398} +{"ev":"start","t":83140.40000000596,"sid":93,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":82.14734693877551,"ta":81.544} +{"ev":"vibrate","pattern":"[8]","t":83399.5,"ta":81.803} +{"ev":"start","t":83460.5,"sid":94,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":82.48068027210886,"ta":81.864} +{"ev":"vibrate","pattern":"[8]","t":83727.40000000596,"ta":82.131} +{"ev":"start","t":83820.40000000596,"sid":95,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":82.81401360544217,"ta":82.224} +{"ev":"vibrate","pattern":"[8]","t":84055.5,"ta":82.459} +{"ev":"start","t":84140.5,"sid":96,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":83.14734693877551,"ta":82.544} +{"ev":"vibrate","pattern":"[8]","t":84400.5,"ta":82.804} +{"ev":"start","t":84460.5,"sid":97,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":83.48068027210886,"ta":82.864} +{"ev":"vibrate","pattern":"[8]","t":84728.5,"ta":83.132} +{"ev":"start","t":84820.40000000596,"sid":98,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":83.81401360544217,"ta":83.224} +{"ev":"vibrate","pattern":"[8]","t":85057.5,"ta":83.461} +{"ev":"vibrate","pattern":"[8]","t":85402.40000000596,"ta":83.806} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":86412.70000001788,"ta":84.817} +{"ev":"vibrate","pattern":"[20]","t":86415.09999999404,"ta":84.819} +{"ev":"vibrate","pattern":"[60,40,60]","t":86415.09999999404,"ta":84.819} +{"ev":"start","t":86415.30000001192,"sid":99,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":84.819} +{"ev":"start","t":86415.30000001192,"sid":100,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":84.819} +{"ev":"vibrate","pattern":"[60,40,60]","t":86540.40000000596,"ta":84.944} +{"ev":"start","t":86540.5,"sid":101,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":84.944} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":86877.90000000596,"ta":85.282} +{"ev":"vibrate","pattern":"[20]","t":86879.40000000596,"ta":85.283} +{"ev":"vibrate","pattern":"[60,40,60]","t":86879.40000000596,"ta":85.283} +{"ev":"start","t":86879.40000000596,"sid":102,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":85.283} +{"ev":"start","t":86879.5,"sid":103,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":85.283} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":87406.09999999404,"ta":85.81} +{"ev":"vibrate","pattern":"[20]","t":87407.5,"ta":85.811} +{"ev":"vibrate","pattern":"[60,40,60]","t":87407.59999999404,"ta":85.811} +{"ev":"start","t":87407.70000001788,"sid":104,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":85.812} +{"ev":"start","t":87407.70000001788,"sid":105,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":85.812} +{"ev":"vibrate","pattern":"[60,40,60]","t":87540.40000000596,"ta":85.944} +{"ev":"start","t":87540.40000000596,"sid":106,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":85.944} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":87871.20000001788,"ta":86.275} +{"ev":"vibrate","pattern":"[20]","t":87872.40000000596,"ta":86.276} +{"ev":"vibrate","pattern":"[60,40,60]","t":87872.5,"ta":86.276} +{"ev":"start","t":87872.59999999404,"sid":107,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":86.276} +{"ev":"start","t":87872.70000001788,"sid":108,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":86.277} +{"ev":"vibrate","pattern":"[60,40,60]","t":88540.30000001192,"ta":86.944} +{"ev":"start","t":88540.5,"sid":109,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":86.944} +{"ev":"fetch","url":"audio/voice/female/grade_c.ogg","status":200,"ok":true,"ms":3,"t":89024.30000001192,"ta":87.428} +{"ev":"vstart","url":"audio/voice/female/grade_c.ogg","t":89029.90000000596,"dur":4.16,"ta":87.434} +{"ev":"stop","t":89721,"sid":26,"when":0,"ta":88.125} +{"ev":"vend","url":"audio/voice/female/grade_c.ogg","t":91561.5,"reason":"kill","ta":89.965} +{"ev":"fetch","url":"audio/voice/female/new_best_combo.ogg","status":200,"ok":true,"ms":2,"t":91564,"ta":89.968} +{"ev":"vstart","url":"audio/voice/female/new_best_combo.ogg","t":91566.80000001192,"dur":1.44,"ta":89.971} +{"ev":"vend","url":"audio/voice/female/new_best_combo.ogg","t":92513.40000000596,"reason":"kill","ta":90.917} +{"ev":"fetch","url":"audio/voice/female/restart.ogg","status":200,"ok":true,"ms":3,"t":92516.20000001788,"ta":90.92} +{"ev":"vstart","url":"audio/voice/female/restart.ogg","t":92520.30000001192,"dur":3.36,"ta":90.924} +{"ev":"vend","url":"audio/voice/female/restart.ogg","t":94579.90000000596,"reason":"kill","ta":92.984} +{"ev":"start","t":94580.09999999404,"sid":110,"url":"audio/bgm/menu.ogg","layer":"bgm","dur":90,"rate":1,"loop":true,"when":0,"ta":92.984} +{"ev":"recstop","t":209927.5,"ta":208.331} diff --git a/research/rhythm-drummer/round1/live/B/mix.webm b/research/rhythm-drummer/round1/live/B/mix.webm new file mode 100644 index 00000000..52840089 Binary files /dev/null and b/research/rhythm-drummer/round1/live/B/mix.webm differ diff --git a/research/rhythm-drummer/round1/live/B/transcript.md b/research/rhythm-drummer/round1/live/B/transcript.md new file mode 100644 index 00000000..73a07d1d --- /dev/null +++ b/research/rhythm-drummer/round1/live/B/transcript.md @@ -0,0 +1,298 @@ +# rhythm 实时会话 transcript + +- `[ -0.07s]` **GESTURE** tap @(195,422) +- `[ 0.00s]` REC 开始 (audio/webm;codecs=opus) +- `[ 0.05s]` VOICE ▶ welcome (dur 6.24s) +- `[ 0.25s]` BGM ♪ track90 (loop) +- `[ 1.21s]` **GESTURE** tap @(195,422) +- `[ 1.21s]` VIBRATE [20] +- `[ 1.71s]` VOICE ■ welcome (kill) +- `[ 1.71s]` VOICE ▶ tut_map (dur 14.28s) +- `[ 2.58s]` **GESTURE** tap @(195,422) +- `[ 2.58s]` VIBRATE [20] +- `[ 3.08s]` VOICE ■ tut_map (kill) +- `[ 3.08s]` VOICE ▶ tut_probe (dur 3.76s) +- `[ 3.95s]` **GESTURE** tap @(195,422) +- `[ 3.95s]` VIBRATE [20] +- `[ 4.45s]` VOICE ■ tut_probe (kill) +- `[ 5.32s]` **GESTURE** tap @(65,210) +- `[ 5.32s]` VIBRATE [20] +- `[ 5.32s]` SFX ♪ dm_hat +- `[ 5.33s]` VOICE ▶ zone_tl (dur 1.68s) +- `[ 6.54s]` VOICE ■ zone_tl (kill) +- `[ 7.59s]` **GESTURE** tap @(195,630) +- `[ 7.59s]` VIBRATE [20] +- `[ 7.59s]` SFX ♪ inst_kick +- `[ 7.60s]` VOICE ▶ zone_bc (dur 2s) +- `[ 9.00s]` VOICE ■ zone_bc (kill) +- `[ 9.26s]` **GESTURE** swipedown @(195,422) +- `[ 9.26s]` VIBRATE [20] +- `[ 9.26s]` SFX ♪ inst_kick +- `[ 9.61s]` VIBRATE [10] +- `[ 9.62s]` VOICE ▶ tut_skip (dur 2s) +- `[ 11.02s]` VOICE ■ tut_skip (kill) +- `[ 11.03s]` BGM ♪ track90 (loop) +- `[ 11.47s]` SFX ♪ inst_kick +- `[ 12.02s]` VIBRATE [8] +- `[ 12.11s]` SFX ♪ inst_kick +- `[ 12.70s]` VIBRATE [8] +- `[ 12.79s]` SFX ♪ inst_kick +- `[ 13.35s]` VIBRATE [8] +- `[ 13.47s]` SFX ♪ inst_kick +- `[ 14.03s]` VIBRATE [8] +- `[ 14.11s]` SFX ♪ inst_kick +- `[ 14.68s]` VIBRATE [8] +- `[ 14.70s]` **GESTURE** tap @(195,210) +- `[ 14.71s]` VIBRATE [20] +- `[ 14.71s]` SFX ♪ dm_tomhi +- `[ 15.37s]` **GESTURE** tap @(195,210) +- `[ 15.37s]` VIBRATE [20] +- `[ 15.37s]` SFX ♪ dm_tomhi +- `[ 15.47s]` SFX ♪ inst_kick +- `[ 16.03s]` VIBRATE [8] +- `[ 16.70s]` **GESTURE** tap @(195,210) +- `[ 16.70s]` VIBRATE [20] +- `[ 16.70s]` SFX ♪ dm_tomhi +- `[ 17.36s]` **GESTURE** tap @(195,210) +- `[ 17.36s]` VIBRATE [20] +- `[ 17.36s]` VIBRATE [60,40,60] +- `[ 17.36s]` SFX ♪ dm_tomhi +- `[ 17.36s]` SFX ♪ dm_hat +- `[ 18.69s]` **GESTURE** tap @(195,210) +- `[ 18.69s]` VIBRATE [20] +- `[ 18.69s]` VIBRATE [60,40,60] +- `[ 18.69s]` SFX ♪ dm_tomhi +- `[ 18.69s]` SFX ♪ dm_hat +- `[ 22.11s]` SFX ♪ dm_snare +- `[ 22.70s]` VIBRATE [8] +- `[ 23.47s]` SFX ♪ dm_tomlo +- `[ 24.02s]` VIBRATE [8] +- `[ 25.34s]` **GESTURE** tap @(195,210) +- `[ 25.34s]` VIBRATE [20] +- `[ 25.34s]` VIBRATE [60,40,60] +- `[ 25.34s]` SFX ♪ dm_tomhi +- `[ 25.34s]` SFX ♪ dm_hat +- `[ 26.67s]` **GESTURE** tap @(65,210) +- `[ 26.67s]` VIBRATE [20] +- `[ 26.67s]` VIBRATE [60,40,60] +- `[ 26.67s]` SFX ♪ dm_hat +- `[ 26.67s]` SFX ♪ dm_hat +- `[ 27.39s]` **GESTURE** swipedown @(195,422) +- `[ 27.39s]` VIBRATE [20] +- `[ 27.39s]` SFX ♪ inst_kick +- `[ 27.74s]` VIBRATE [10] +- `[ 27.75s]` VOICE ▶ menu_main (dur 5.6s) +- `[ 29.65s]` **GESTURE** tap @(195,422) +- `[ 29.65s]` VIBRATE [20] +- `[ 30.15s]` VOICE ■ menu_main (kill) +- `[ 30.16s]` VOICE ▶ menu_lobby (dur 0.6370975056689342s) +- `[ 30.64s]` VOICE ■ menu_lobby (kill) +- `[ 31.62s]` **GESTURE** swipeup @(195,422) +- `[ 31.62s]` VIBRATE [20] +- `[ 31.98s]` VIBRATE [10] +- `[ 31.99s]` VOICE ▶ rules (dur 7.68s) +- `[ 36.66s]` VOICE ■ rules (kill) +- `[ 36.66s]` VOICE ▶ settings_hint (dur 1.36s) +- `[ 37.61s]` VOICE ■ settings_hint (kill) +- `[ 38.38s]` **GESTURE** swipedown @(195,422) +- `[ 38.39s]` VIBRATE [20] +- `[ 38.74s]` VIBRATE [10] +- `[ 38.74s]` BGM ♪ track90 (loop) +- `[ 38.78s]` SFX ♪ inst_kick +- `[ 38.86s]` VIBRATE [8] +- `[ 38.94s]` SFX ♪ inst_kick +- `[ 39.52s]` VIBRATE [8] +- `[ 39.58s]` SFX ♪ inst_kick +- `[ 40.18s]` VIBRATE [8] +- `[ 40.26s]` SFX ♪ inst_kick +- `[ 40.85s]` VIBRATE [8] +- `[ 41.53s]` **GESTURE** tap @(325,210) +- `[ 41.53s]` VIBRATE [20] +- `[ 41.53s]` SFX ♪ dm_crash +- `[ 42.86s]` **GESTURE** tap @(195,210) +- `[ 42.86s]` VIBRATE [20] +- `[ 42.86s]` SFX ♪ dm_tomhi +- `[ 43.53s]` **GESTURE** tap @(195,210) +- `[ 43.53s]` VIBRATE [20] +- `[ 43.53s]` SFX ♪ dm_tomhi +- `[ 43.90s]` SFX ♪ inst_kick +- `[ 44.46s]` VIBRATE [8] +- `[ 44.54s]` SFX ♪ inst_kick +- `[ 45.12s]` VIBRATE [8] +- `[ 45.22s]` SFX ♪ dm_snare +- `[ 45.79s]` VIBRATE [8] +- `[ 47.62s]` VIBRATE [60,40,60] +- `[ 47.63s]` SFX ♪ dm_hat +- `[ 47.77s]` **GESTURE** tap @(195,210) +- `[ 47.78s]` VIBRATE [20] +- `[ 47.78s]` VIBRATE [60,40,60] +- `[ 47.78s]` SFX ♪ dm_tomhi +- `[ 47.78s]` SFX ♪ dm_hat +- `[ 48.24s]` **GESTURE** longpress @(195,422) +- `[ 48.24s]` VIBRATE [20] +- `[ 48.24s]` SFX ♪ inst_kick +- `[ 48.69s]` VIBRATE [60,40,60] +- `[ 48.69s]` SFX ♪ dm_hat +- `[ 48.94s]` VIBRATE [60,40,60] +- `[ 48.94s]` SFX ♪ dm_hat +- `[ 49.24s]` **GESTURE** tap @(195,210) +- `[ 49.24s]` VIBRATE [20] +- `[ 49.24s]` SFX ♪ dm_tomhi +- `[ 51.86s]` SFX ♪ dm_snare +- `[ 52.46s]` VIBRATE [8] +- `[ 52.54s]` SFX ♪ dm_tomlo +- `[ 53.13s]` VIBRATE [8] +- `[ 53.22s]` SFX ♪ inst_kick +- `[ 53.81s]` VIBRATE [8] +- `[ 55.58s]` VIBRATE [60,40,60] +- `[ 55.58s]` SFX ♪ dm_hat +- `[ 55.75s]` **GESTURE** tap @(65,210) +- `[ 55.75s]` VIBRATE [20] +- `[ 55.75s]` VIBRATE [60,40,60] +- `[ 55.75s]` SFX ♪ dm_hat +- `[ 55.75s]` SFX ♪ dm_hat +- `[ 56.49s]` **GESTURE** tap @(325,210) +- `[ 56.49s]` VIBRATE [20] +- `[ 56.49s]` VIBRATE [60,40,60] +- `[ 56.49s]` SFX ♪ dm_crash +- `[ 56.49s]` SFX ♪ dm_hat +- `[ 59.90s]` SFX ♪ inst_kick +- `[ 60.47s]` VIBRATE [8] +- `[ 60.50s]` VOICE ▶ mid_song (dur 1.6s) +- `[ 60.54s]` SFX ♪ dm_snare +- `[ 61.12s]` VIBRATE [8] +- `[ 61.22s]` SFX ♪ inst_kick +- `[ 61.45s]` VOICE ■ mid_song (kill) +- `[ 61.80s]` VIBRATE [8] +- `[ 61.90s]` SFX ♪ dm_tomlo +- `[ 62.47s]` VIBRATE [8] +- `[ 63.58s]` VIBRATE [60,40,60] +- `[ 63.58s]` SFX ♪ dm_hat +- `[ 63.78s]` **GESTURE** tap @(195,210) +- `[ 63.78s]` VIBRATE [20] +- `[ 63.78s]` VIBRATE [60,40,60] +- `[ 63.78s]` SFX ♪ dm_tomhi +- `[ 63.78s]` SFX ♪ dm_hat +- `[ 64.44s]` **GESTURE** tap @(65,210) +- `[ 64.44s]` VIBRATE [20] +- `[ 64.44s]` VIBRATE [60,40,60] +- `[ 64.44s]` SFX ♪ dm_hat +- `[ 64.44s]` SFX ♪ dm_hat +- `[ 65.22s]` SFX ♪ dm_hat +- `[ 65.58s]` VIBRATE [60,40,60] +- `[ 65.58s]` SFX ♪ dm_hat +- `[ 65.80s]` VIBRATE [8] +- `[ 66.54s]` SFX ♪ dm_snare +- `[ 67.13s]` VIBRATE [8] +- `[ 67.22s]` SFX ♪ inst_kick +- `[ 67.80s]` VIBRATE [8] +- `[ 68.42s]` **GESTURE** tap @(325,210) +- `[ 68.42s]` VIBRATE [20] +- `[ 68.42s]` VIBRATE [60,40,60] +- `[ 68.42s]` SFX ♪ dm_crash +- `[ 68.42s]` SFX ♪ dm_hat +- `[ 69.74s]` **GESTURE** tap @(65,630) +- `[ 69.74s]` VIBRATE [20] +- `[ 69.75s]` SFX ♪ dm_snare +- `[ 69.75s]` SFX ♪ dm_accent +- `[ 70.54s]` SFX ♪ inst_kick +- `[ 70.94s]` VIBRATE [60,40,60] +- `[ 70.94s]` SFX ♪ dm_hat +- `[ 71.14s]` VIBRATE [8] +- `[ 71.22s]` SFX ♪ dm_hat +- `[ 71.79s]` VIBRATE [8] +- `[ 71.90s]` SFX ♪ inst_kick +- `[ 72.46s]` VIBRATE [8] +- `[ 72.54s]` SFX ♪ dm_crash +- `[ 73.14s]` VIBRATE [8] +- `[ 73.80s]` **GESTURE** tap @(195,210) +- `[ 73.80s]` VIBRATE [20] +- `[ 73.80s]` VIBRATE [60,40,60] +- `[ 73.80s]` SFX ♪ dm_tomhi +- `[ 73.80s]` SFX ♪ dm_hat +- `[ 74.46s]` **GESTURE** tap @(195,210) +- `[ 74.46s]` VIBRATE [20] +- `[ 74.46s]` VIBRATE [60,40,60] +- `[ 74.46s]` SFX ♪ dm_tomhi +- `[ 74.46s]` SFX ♪ dm_hat +- `[ 75.62s]` VIBRATE [60,40,60] +- `[ 75.62s]` SFX ♪ dm_hat +- `[ 75.78s]` **GESTURE** tap @(195,210) +- `[ 75.79s]` VIBRATE [20] +- `[ 75.79s]` VIBRATE [60,40,60] +- `[ 75.79s]` SFX ♪ dm_tomhi +- `[ 75.79s]` SFX ♪ dm_hat +- `[ 75.90s]` SFX ♪ dm_snare +- `[ 76.47s]` VIBRATE [8] +- `[ 76.54s]` SFX ♪ dm_tomlo +- `[ 77.12s]` VIBRATE [8] +- `[ 77.22s]` SFX ♪ dm_hat +- `[ 77.80s]` VIBRATE [8] +- `[ 77.86s]` SFX ♪ inst_kick +- `[ 78.45s]` VIBRATE [8] +- `[ 79.10s]` **GESTURE** tap @(325,630) +- `[ 79.10s]` VIBRATE [20] +- `[ 79.10s]` VIBRATE [60,40,60] +- `[ 79.10s]` SFX ♪ dm_tomlo +- `[ 79.10s]` SFX ♪ dm_hat +- `[ 79.15s]` VOICE ▶ tut_fill (dur 3.68s) +- `[ 80.26s]` VIBRATE [60,40,60] +- `[ 80.26s]` SFX ♪ dm_hat +- `[ 80.42s]` **GESTURE** tap @(325,210) +- `[ 80.42s]` VIBRATE [20] +- `[ 80.42s]` VIBRATE [60,40,60] +- `[ 80.42s]` SFX ♪ dm_crash +- `[ 80.42s]` SFX ♪ dm_hat +- `[ 81.08s]` **GESTURE** tap @(65,630) +- `[ 81.08s]` VIBRATE [20] +- `[ 81.08s]` VIBRATE [60,40,60] +- `[ 81.08s]` SFX ♪ dm_snare +- `[ 81.09s]` SFX ♪ dm_hat +- `[ 81.22s]` SFX ♪ inst_kick +- `[ 81.40s]` VOICE ■ tut_fill (kill) +- `[ 81.54s]` SFX ♪ inst_kick +- `[ 81.80s]` VIBRATE [8] +- `[ 81.86s]` SFX ♪ inst_kick +- `[ 82.13s]` VIBRATE [8] +- `[ 82.22s]` SFX ♪ dm_snare +- `[ 82.46s]` VIBRATE [8] +- `[ 82.54s]` SFX ♪ dm_snare +- `[ 82.80s]` VIBRATE [8] +- `[ 82.86s]` SFX ♪ inst_kick +- `[ 83.13s]` VIBRATE [8] +- `[ 83.22s]` SFX ♪ dm_crash +- `[ 83.46s]` VIBRATE [8] +- `[ 83.81s]` VIBRATE [8] +- `[ 84.82s]` **GESTURE** tap @(195,210) +- `[ 84.82s]` VIBRATE [20] +- `[ 84.82s]` VIBRATE [60,40,60] +- `[ 84.82s]` SFX ♪ dm_tomhi +- `[ 84.82s]` SFX ♪ dm_hat +- `[ 84.94s]` VIBRATE [60,40,60] +- `[ 84.94s]` SFX ♪ dm_hat +- `[ 85.28s]` **GESTURE** tap @(195,210) +- `[ 85.28s]` VIBRATE [20] +- `[ 85.28s]` VIBRATE [60,40,60] +- `[ 85.28s]` SFX ♪ dm_tomhi +- `[ 85.28s]` SFX ♪ dm_hat +- `[ 85.81s]` **GESTURE** tap @(65,210) +- `[ 85.81s]` VIBRATE [20] +- `[ 85.81s]` VIBRATE [60,40,60] +- `[ 85.81s]` SFX ♪ dm_hat +- `[ 85.81s]` SFX ♪ dm_hat +- `[ 85.94s]` VIBRATE [60,40,60] +- `[ 85.94s]` SFX ♪ dm_hat +- `[ 86.28s]` **GESTURE** tap @(65,210) +- `[ 86.28s]` VIBRATE [20] +- `[ 86.28s]` VIBRATE [60,40,60] +- `[ 86.28s]` SFX ♪ dm_hat +- `[ 86.28s]` SFX ♪ dm_hat +- `[ 86.94s]` VIBRATE [60,40,60] +- `[ 86.94s]` SFX ♪ dm_hat +- `[ 87.43s]` VOICE ▶ grade_c (dur 4.16s) +- `[ 89.97s]` VOICE ■ grade_c (kill) +- `[ 89.97s]` VOICE ▶ new_best_combo (dur 1.44s) +- `[ 90.92s]` VOICE ■ new_best_combo (kill) +- `[ 90.92s]` VOICE ▶ restart (dur 3.36s) +- `[ 92.98s]` VOICE ■ restart (kill) +- `[ 92.98s]` BGM ♪ menu (loop) diff --git a/research/rhythm-drummer/round1/live/B/verdicts.json b/research/rhythm-drummer/round1/live/B/verdicts.json new file mode 100644 index 00000000..a9dd83d9 --- /dev/null +++ b/research/rhythm-drummer/round1/live/B/verdicts.json @@ -0,0 +1,208 @@ +{ + "game": "rhythm", + "settings": { + "voice": "female", + "guide": "standard", + "speed": 1.7, + "bgm": 0.3 + }, + "outdir": "/home/xsl/AutoVideo/SoundGame/web/harness/live/out/rhythm-20260914100502", + "segs": [ + { + "idx": 0, + "voiceId": "welcome", + "t0": 0.054, + "t1": 1.711, + "dur": 6.24, + "expEnd": 3.725, + "truncated": true, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 1, + "voiceId": "tut_map", + "t0": 1.711, + "t1": 3.08, + "dur": 14.28, + "expEnd": 10.111, + "truncated": true, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 2, + "voiceId": "tut_probe", + "t0": 3.08, + "t1": 4.449, + "dur": 3.76, + "expEnd": 5.292, + "truncated": true, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 3, + "voiceId": "zone_tl", + "t0": 5.325, + "t1": 6.544, + "dur": 1.68, + "expEnd": 6.313, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 4, + "voiceId": "zone_bc", + "t0": 7.596, + "t1": 9.004, + "dur": 2, + "expEnd": 8.772, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 5, + "voiceId": "tut_skip", + "t0": 9.618, + "t1": 11.024, + "dur": 2, + "expEnd": 10.794, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 6, + "voiceId": "menu_main", + "t0": 27.753, + "t1": 30.151, + "dur": 5.6, + "expEnd": 31.047, + "truncated": true, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 7, + "voiceId": "menu_lobby", + "t0": 30.156, + "t1": 30.643, + "dur": 0.6370975056689342, + "expEnd": 30.531, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 8, + "voiceId": "rules", + "t0": 31.986, + "t1": 36.657, + "dur": 7.68, + "expEnd": 36.504, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 9, + "voiceId": "settings_hint", + "t0": 36.663, + "t1": 37.609, + "dur": 1.36, + "expEnd": 37.463, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 10, + "voiceId": "mid_song", + "t0": 60.504, + "t1": 61.454, + "dur": 1.6, + "expEnd": 61.445, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 11, + "voiceId": "tut_fill", + "t0": 79.153, + "t1": 81.398, + "dur": 3.68, + "expEnd": 81.318, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 12, + "voiceId": "grade_c", + "t0": 87.434, + "t1": 89.965, + "dur": 4.16, + "expEnd": 89.881, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 13, + "voiceId": "new_best_combo", + "t0": 89.971, + "t1": 90.917, + "dur": 1.44, + "expEnd": 90.818, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + }, + { + "idx": 14, + "voiceId": "restart", + "t0": 90.924, + "t1": 92.984, + "dur": 3.36, + "expEnd": 92.9, + "truncated": false, + "reason": "kill", + "verdict": null, + "heard": null, + "similarity": null + } + ], + "rows": [] +} \ No newline at end of file diff --git a/research/rhythm-drummer/round1/persona-reviews.md b/research/rhythm-drummer/round1/persona-reviews.md new file mode 100644 index 00000000..a868792f --- /dev/null +++ b/research/rhythm-drummer/round1/persona-reviews.md @@ -0,0 +1,53 @@ +# 《节拍行者》鼓手模式 R1 人设评审(基于 round0 转写 + live A/B 实录) + +## P1 视障资深玩家(专业评委) + +### 节拍行者(鼓手模式) +- 状态感知:4/5 —— 绝大多数时刻清楚自己在哪:摸鼓阶段每摸一区报区名(「左上,踩镲。」),教学每步有明确指令句,演奏中领奏停了=轮到我这个契约从转写 t+60.9s 的 gd_answer+turn 开始就非常清楚。扣分项:①连摸三下快进时第三下的区名被掐断(zone_bl TRUNCATED,live A 实录)——报一半就没了;②曲尾半句「慢了半拍。」被评级语音顶掉,最后听到的引导是半句话。 +- 下一步明确性:4/5 —— 应答窗内零语音的纪律执行得好(live A 正片应答段无一句插话);结算屏三选项播报齐全。扣一分:摸鼓进阶条件「摸到三个不同的地方」在蒙眼+快摸时只能靠数数,没有「还差一个」的进度提示。 +- 愿意继续玩:**是** —— 这是少见的「先给你乐器随便玩,再教你曲子」的节奏游戏,探索阶段零惩罚,双轴(列=声像/行=音高)的心智模型三句话就立住了,比我玩过的大多数盲 accessible 节奏游戏的「背手势表」高一个档次。 +- P0 问题:无(无「不知道自己在哪/该做什么」的时刻)。 +- 建议:①区名被掐断修掉(第三下摸完让它报完再进教学);②曲尾 3 秒内别再插向导句,直接结算;③摸鼓进度播报(「还差一个」)。 + +## P2 时间充裕的大学生 + +### 节拍行者(鼓手模式) +- 状态感知:4/5 —— 头 10 秒就在敲真鼓(摸鼓认区),比「听 30 秒旁白」强太多。演奏中每句「领奏 1 小节→空 1 小节给我」结构清晰,加花前还有预告(「最后有一段加花,跟着我连打」)。扣分:教学连错时「再听一遍,跟着来」连着播两遍(retry_listen×2,live A 28.1s/30.4s),像复读机。 +- 下一步明确性:5/5 —— 任何时候都知道该干嘛:摸鼓时随便摸、教学时听数拍跟打、漏打了有「慢了半拍,下一小节跟上我就行」兜底。没有一帧是无提示的。 +- 愿意继续玩:**是** —— 90 秒一局、A 级解锁下一首的钩子很标准但有效;加花(一串快鼓+吊镲收尾)是真实的爽点,转写里 t+118.5s 那句预告一出来我就精神了。打完立刻想开《追风》。 +- P0 问题:无。 +- 建议:①纠正链「再听一遍」二连播合并成一句;②解锁播报可以更燃一点(现在的「新曲目解锁,追风!」偏平淡)。 + +## P3 时间紧张的白领 + +### 节拍行者(鼓手模式) +- 状态感知:4/5 —— 单手竖握,下排三个区拇指全覆盖,上排手掌下移就够——单手盲操成立。被打断(下滑菜单)后「四拍之后,继续」+四下底鼓数拍回来,25 分钟地铁里能完整打一局(65-80 秒)。扣分:曲中 half 的「过半了,保持」在嘈杂环境 ducking 后偏小(live B bgm=0.3 下清晰,但真地铁满音量 BGM 时待验证)。 +- 下一步明确性:4/5 —— 通勤场景最需要的「随时可暂停、一句话接上」做到了。扣一分:选曲界面左右滑的反馈只有曲名语音,如果正好在隧道里没听清是哪首,没有重听快捷键提示(要靠自己记得上滑是规则)。 +- 愿意继续玩:**是** —— 单局短、可暂停、无倒计时惩罚,错鼓就是一声闷镲不骂街,地铁站立可玩。 +- P0 问题:无。 +- 建议:①选曲播报加序号(「第一首/第二首」其实已有,保留);②menu 帮助里补一句「选曲时上滑听规则」。 + +## P4 疲惫的工人 + +### 节拍行者(鼓手模式) +- 状态感知:5/5 —— 规则一遍就懂:「我敲一小节鼓,你在下一小节照着敲回来」。做错了听到的是「没关系,我们先走下一句」「我放慢一些,跟着试试」——没有一句数落。全程没有血条、没有倒计时、没有「失败」两个字。 +- 下一步明确性:5/5 —— 教学三步一步一步带,数拍是四下底鼓,跟着敲就行;放慢重演的安排对我这种反应慢的人就是雪中送炭(live A 实录 gd_slow 后慢速领奏再教一遍)。 +- 愿意继续玩:**是** —— 不累。打错了歌照唱,评级 C 级也说「别灰心,再来一遍」。晚上外放小音量,底鼓军鼓分得清,低音不轰头。 +- P0 问题:无。 +- 建议:①教学完了进第一首之间衔接很顺,保持;②希望开场白再短一点(「今天你是乐队的鼓手」这句喜欢,保留)。 + +--- + +### 评审专门问题汇总 + +**分区心智模型是否建立(蒙眼能否说清自己敲的是哪个区)?** +P1:能。双轴模型(列=左右耳、行=亮/沉)+六件乐器音色各异,三重冗余。live A 实录里驱动按谱面敲 36 音仅 4 音失手(且系驱动 artifact),反过来证明「听着领奏音色+声像找区」的映射可被快速掌握。 +P2/P3/P4:摸鼓阶段的「报名」机制是关键——蒙眼摸到哪听到哪 + 名字,建模型靠手不靠背。 + +**合拍感?** +live A:33/37 音 Perfect 级(dt≈0.00s 真机直证),Perfect 有亮音叠加、16 连击有震动,合奏感(领奏=同音色)是旧作好评基因的延续。 + +**旧作保留的亮点是否还在?** +跟随领奏核心 ✓、S~C 评级与解锁 ✓(live A A 级解锁《追风》实录)、BPM 精确(谱面锚定 bpm_precise 实测网格)✓、不中断不掉血 ✓、旁白可跳/长按快进 ✓、菜单断点续播 ✓。 + +**裁决:4/4 愿意继续玩,P0 = 0 —— R1 达标。** diff --git a/research/rhythm-drummer/round1/synthesis.md b/research/rhythm-drummer/round1/synthesis.md new file mode 100644 index 00000000..5709c27c --- /dev/null +++ b/research/rhythm-drummer/round1/synthesis.md @@ -0,0 +1,51 @@ +# 《节拍行者》鼓手模式 R1 synthesis(产品裁定) + +## 输入 + +- 转写:`round0/transcript-main.md`(mock 新手全弧线,201 行)+ live A/B 实录 + (`live-player.md`:A 局 ASR OK 11/TRUNCATED 5/MISMATCH 2,A 级通关+解锁; + B 局 standard 档零向导语音、错区乱打 grade_c、菜单/长按全路径) +- 人设:4/4 愿意继续玩(P1 4/4·4/5、P2 4/5·5/5、P3 4/5·4/5、P4 5/5·5/5),P0 = 0 + +## 裁定:4/4 愿意继续玩,P0 = 0 —— **R1 达标**(≥3/4 且无 P0 级状态混淆) + +## P0 清单 + +无。三通道(mock 转写/live 实录/四人设)均未出现「不知道自己在哪/该做什么/ +为何失败」的状态混淆;教学失败、菜单中断、错鼓、长按、结算每条路径都有 +听觉兜底且实测可达。 + +## P1 清单(R2 修复,含尽量修的 P2) + +1. **F1 曲尾向导句被结算顶断**:连 2 Miss 的 gd_miss 在曲尾 miss 后插播, + 1.1s 即被结算评级语音顶掉(live A 实录「慢了半拍。」半句)。修:guide.error + 调度加「距 _phaseEnd < 3s 静默跳过」守卫(与 gd_miss 同曲一次的配额共用, + 宁可不说不说半句)。 +2. **F2 探针第三击区名被掐断**:摸满 3 区进阶时 stopVoice 切断正在播的区名 + (zone_bl TRUNCATED,live A 实录;P1 评委点名)。修:探针 advance 不再 + stopVoice,教学步语音改 queue:true 排在区名后播(链式 onComplete 推进, + _tutLead 的 ≥1.2s 缓冲天然吸收排队延迟)。 +3. **F3 教学纠正链复读机**:retry_listen 在纠正链尾与默认纠正句二连播 + (live A 28.1s/30.4s;P2 评委点名)。修:纠正链去重——wrong/early/late + 归因句已含「再听」语义时不再追加 retry_listen。 +4. **F4(P2)unlock 播报燃度**:维持现状(文案经 TTS 双性别验证,不改)。 +5. **F5(P2,不修)** zone_tr/zone_tc/new_best_combo 的 ASR MISMATCH = + 短句同音近失(cipher 先例归因),人耳可辨。 + +## F 类修复复核结论(R2 验收口径) + +- F1 断言:曲尾 3s 内连 2 Miss → 无 gd_miss 语音调度(事件钩子可证), + 结算链完整不截断。 +- F2 断言:快速连摸 3 区 → 区名全部自然播完 → 教学句紧随其后 → T1 数拍 + 不误拍(时间轴:区名链 ≤6s + 教学句 + 1.2s 缓冲对齐下一小节)。 +- F3 断言:教学失败时纠正语音链至多 2 句且不含相邻重复 id。 +- 既有绿全部不回退:playtest 129 / guide.test 122 / check_all_assets。 + +## 其他记录 + +- live 基础设施教训 5 条(单飞拉取/when 字段/vibrate 锚/layer 过滤/视口 + 坐标)已沉淀在 `live-player.md` 末尾,惠及全部泳道。 +- 驱动 artifact(曲尾 clearPending 误杀尾单、B 局 verdicts 未及落盘)非游戏 + 缺陷,R2 驱动已修/规避。 +- 资产零新增(修复全部代码层);语音/BGM/合成资产经 check_all_assets 与 + live 双通道验证无缺失。 diff --git a/research/rhythm-drummer/round2/live-player.md b/research/rhythm-drummer/round2/live-player.md new file mode 100644 index 00000000..42ea42f9 --- /dev/null +++ b/research/rhythm-drummer/round2/live-player.md @@ -0,0 +1,28 @@ +# 《节拍行者》鼓手模式 R2 全拟真试玩报告 + +复跑主线局(live A 同款驱动,端口 8142,产物 `live/A/` = rhythm-20260914102057)。 +R1 synthesis 的三项 P1 修复(F1 曲尾 gd_miss 守卫 / F2 探针区名防掐断 / +F3 纠正链去重)全部带断言落地(playtest A23-A25),本局为修复后的真机验收。 + +## 验收结果 + +- ASR 判定:**OK 12 / TRUNCATED 3 / MISMATCH 2**(R1 为 11/5/2) +- **F2 直证**:三连摸区名链 zone_tl 5.32→6.92 / zone_tr 7.99→9.87 / + zone_bl 10.66→**13.00(播完)** → tut_z_kick 13.01 接续——区名不再被掐断, + 教学句乖乖排队(150ms 延迟起步 + queue 链,根因是引擎入队判定在 await + 加载之前,同步连发会误判通道空闲) +- **F1 直证**:曲尾 4 记 Miss 全程无 gd_miss 语音调度(R1 同位置半句截断 + 消失),结算链 grade_a → unlock_song2 → restart 零截断 +- **F3 直证**(mock 断言 A23):没出手失败的纠正链 retry_listen 只播一次; + live 教学段语音序列无相邻复读 +- 演奏精度保持:**33 记 Perfect accent / 4 记 Miss**(dt≈0.00s), + tut_done 61.4s → grade_a 126.8s → unlock_song2 133.2s 全弧线 +- 剩余 TRUNCATED×3 = 驱动故意跳旁白(welcome/tut_map/tut_probe); + MISMATCH×2 = 短句 ASR 同音近失(zone_tr「有上调查」/ new_best_combo + 「新联机记录」,cipher 先例归因,人耳可辨) + +## 结论 + +修复零回退(playtest 129→134 含新增 3 断言 / guide.test 122 / +check_all_assets 全绿 / 15 游戏+3 引擎测试全量回归绿),live 直证 F1-F3 +全部兑现。R2 达标。 diff --git a/research/rhythm-drummer/round2/live/A/events.jsonl b/research/rhythm-drummer/round2/live/A/events.jsonl new file mode 100644 index 00000000..f1bf823a --- /dev/null +++ b/research/rhythm-drummer/round2/live/A/events.jsonl @@ -0,0 +1,365 @@ +{"ev":"probe_ready","t":8.900000005960464,"ta":null} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":1522.2000000178814,"ta":-0.087} +{"ev":"recstart","mime":"audio/webm;codecs=opus","t":1608.9000000059605,"ta":0} +{"ev":"fetch","url":"audio/sfx/dm_hat.ogg","status":200,"ok":true,"ms":14,"t":1623.2000000178814,"ta":0.014} +{"ev":"fetch","url":"audio/sfx/dm_tomhi.ogg","status":200,"ok":true,"ms":14,"t":1623.5,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/dm_crash.ogg","status":200,"ok":true,"ms":14,"t":1623.5,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/dm_snare.ogg","status":200,"ok":true,"ms":14,"t":1623.5,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/inst_kick.ogg","status":200,"ok":true,"ms":14,"t":1623.6000000238419,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/dm_tomlo.ogg","status":200,"ok":true,"ms":14,"t":1623.6000000238419,"ta":0.015} +{"ev":"fetch","url":"audio/sfx/dm_accent.ogg","status":200,"ok":true,"ms":14,"t":1623.7000000178814,"ta":0.015} +{"ev":"fetch","url":"audio/voice/male/welcome.ogg","status":200,"ok":true,"ms":14,"t":1623.7000000178814,"ta":0.015} +{"ev":"fetch","url":"audio/voice/male/welcome_back.ogg","status":200,"ok":true,"ms":14,"t":1623.7000000178814,"ta":0.015} +{"ev":"fetch","url":"audio/voice/male/tut_map.ogg","status":200,"ok":true,"ms":14,"t":1623.800000011921,"ta":0.015} +{"ev":"fetch","url":"audio/voice/male/tut_probe.ogg","status":200,"ok":true,"ms":14,"t":1623.800000011921,"ta":0.015} +{"ev":"fetch","url":"audio/voice/male/select_hint.ogg","status":200,"ok":true,"ms":14,"t":1623.800000011921,"ta":0.015} +{"ev":"fetch","url":"audio/voice/male/mid_song.ogg","status":200,"ok":true,"ms":14,"t":1623.9000000059605,"ta":0.015} +{"ev":"fetch","url":"audio/bgm/menu.ogg","status":200,"ok":true,"ms":14,"t":1624.4000000059605,"ta":0.016} +{"ev":"fetch","url":"audio/bgm/track90.ogg","status":200,"ok":true,"ms":15,"t":1625.5,"ta":0.017} +{"ev":"fetch","url":"audio/bgm/track105.ogg","status":200,"ok":true,"ms":16,"t":1626.300000011921,"ta":0.017} +{"ev":"fetch","url":"audio/bgm/track120.ogg","status":200,"ok":true,"ms":17,"t":1627.6000000238419,"ta":0.019} +{"ev":"vstart","url":"audio/voice/male/welcome.ogg","t":1675.2000000178814,"dur":8.24,"ta":0.066} +{"ev":"start","t":1924.2000000178814,"sid":1,"url":"audio/bgm/track90.ogg","layer":"bgm","dur":82.33301587301587,"rate":1,"loop":true,"when":6,"ta":0.315} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":2816.5,"ta":1.208} +{"ev":"vibrate","pattern":"[20]","t":2818,"ta":1.209} +{"ev":"vend","url":"audio/voice/male/welcome.ogg","t":3318.600000023842,"reason":"kill","ta":1.71} +{"ev":"vstart","url":"audio/voice/male/tut_map.ogg","t":3318.800000011921,"dur":18.04,"ta":1.71} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":4185,"ta":2.576} +{"ev":"vibrate","pattern":"[20]","t":4186.4000000059605,"ta":2.578} +{"ev":"vend","url":"audio/voice/male/tut_map.ogg","t":4686.5,"reason":"kill","ta":3.078} +{"ev":"vstart","url":"audio/voice/male/tut_probe.ogg","t":4686.700000017881,"dur":3.84,"ta":3.078} +{"ev":"gesture","type":"tap","x":195,"y":422,"t":5553.100000023842,"ta":3.944} +{"ev":"vibrate","pattern":"[20]","t":5554.4000000059605,"ta":3.946} +{"ev":"vend","url":"audio/voice/male/tut_probe.ogg","t":6054.600000023842,"reason":"kill","ta":4.446} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":6921.100000023842,"ta":5.312} +{"ev":"vibrate","pattern":"[20]","t":6922.5,"ta":5.314} +{"ev":"start","t":6923.100000023842,"sid":2,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":5.314} +{"ev":"fetch","url":"audio/voice/male/zone_tl.ogg","status":200,"ok":true,"ms":2,"t":6924.5,"ta":5.316} +{"ev":"vstart","url":"audio/voice/male/zone_tl.ogg","t":6928,"dur":1.6,"ta":5.319} +{"ev":"vend","url":"audio/voice/male/zone_tl.ogg","t":8527.700000017881,"reason":"kill","ta":6.919} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":9591.5,"ta":7.983} +{"ev":"vibrate","pattern":"[20]","t":9594.700000017881,"ta":7.986} +{"ev":"start","t":9594.90000000596,"sid":3,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":7.986} +{"ev":"fetch","url":"audio/voice/male/zone_tr.ogg","status":200,"ok":true,"ms":2,"t":9596.800000011921,"ta":7.988} +{"ev":"vstart","url":"audio/voice/male/zone_tr.ogg","t":9600.600000023842,"dur":1.76,"ta":7.992} +{"ev":"vend","url":"audio/voice/male/zone_tr.ogg","t":11476.200000017881,"reason":"kill","ta":9.867} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":12262.90000000596,"ta":10.654} +{"ev":"vibrate","pattern":"[20]","t":12264.600000023842,"ta":10.656} +{"ev":"start","t":12264.90000000596,"sid":4,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":10.656} +{"ev":"fetch","url":"audio/voice/male/zone_bl.ogg","status":200,"ok":true,"ms":1,"t":12266.100000023842,"ta":10.657} +{"ev":"vstart","url":"audio/voice/male/zone_bl.ogg","t":12269.300000011921,"dur":2,"ta":10.66} +{"ev":"vend","url":"audio/voice/male/zone_bl.ogg","t":14610.700000017881,"reason":"kill","ta":13.002} +{"ev":"fetch","url":"audio/voice/male/tut_z_kick.ogg","status":200,"ok":true,"ms":2,"t":14612.700000017881,"ta":13.004} +{"ev":"vstart","url":"audio/voice/male/tut_z_kick.ogg","t":14619.600000023842,"dur":7.28,"ta":13.011} +{"ev":"vend","url":"audio/voice/male/tut_z_kick.ogg","t":22156.5,"reason":"kill","ta":20.548} +{"ev":"start","t":23037.100000023842,"sid":5,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":22,"ta":21.428} +{"ev":"vibrate","pattern":"[8]","t":23593.20000001788,"ta":21.984} +{"ev":"start","t":23677,"sid":6,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":22.666666666666668,"ta":22.068} +{"ev":"vibrate","pattern":"[8]","t":24273,"ta":22.664} +{"ev":"start","t":24357,"sid":7,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":23.333333333333332,"ta":22.748} +{"ev":"vibrate","pattern":"[8]","t":24923.100000023842,"ta":23.314} +{"ev":"start","t":25037.100000023842,"sid":8,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":24,"ta":23.428} +{"ev":"vibrate","pattern":"[8]","t":25596.100000023842,"ta":23.987} +{"ev":"start","t":25677.100000023842,"sid":9,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":24.666666666666668,"ta":24.068} +{"ev":"vibrate","pattern":"[8]","t":26276,"ta":24.667} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":28908.600000023842,"ta":27.3} +{"ev":"vibrate","pattern":"[20]","t":28910.90000000596,"ta":27.302} +{"ev":"start","t":28911.40000000596,"sid":10,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":27.303} +{"ev":"start","t":28911.40000000596,"sid":11,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":27.303} +{"ev":"fetch","url":"audio/voice/male/tut_z_sides.ogg","status":200,"ok":true,"ms":2,"t":29679.100000023842,"ta":28.07} +{"ev":"vstart","url":"audio/voice/male/tut_z_sides.ogg","t":29685.70000001788,"dur":5.2,"ta":28.077} +{"ev":"vend","url":"audio/voice/male/tut_z_sides.ogg","t":34903.20000001788,"reason":"kill","ta":33.294} +{"ev":"start","t":36343.5,"sid":12,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":35.33333333333333,"ta":34.735} +{"ev":"vibrate","pattern":"[8]","t":36927.60000002384,"ta":35.319} +{"ev":"start","t":37023.5,"sid":13,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":35.99999999999999,"ta":35.415} +{"ev":"vibrate","pattern":"[8]","t":37600.60000002384,"ta":35.992} +{"ev":"start","t":37663.5,"sid":14,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":36.666666666666664,"ta":36.055} +{"ev":"vibrate","pattern":"[8]","t":38257.60000002384,"ta":36.649} +{"ev":"start","t":38343.5,"sid":15,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":37.33333333333333,"ta":36.735} +{"ev":"vibrate","pattern":"[8]","t":38930.5,"ta":37.322} +{"ev":"start","t":39023.5,"sid":16,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":37.99999999999999,"ta":37.415} +{"ev":"vibrate","pattern":"[8]","t":39604.60000002384,"ta":37.996} +{"ev":"start","t":40343.5,"sid":17,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":39.33333333333333,"ta":38.735} +{"ev":"vibrate","pattern":"[8]","t":40933.60000002384,"ta":39.325} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":42248.30000001192,"ta":40.639} +{"ev":"vibrate","pattern":"[20]","t":42250.60000002384,"ta":40.642} +{"ev":"start","t":42250.70000001788,"sid":18,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":40.642} +{"ev":"start","t":42250.80000001192,"sid":19,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":40.642} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":43571.5,"ta":41.963} +{"ev":"vibrate","pattern":"[20]","t":43573,"ta":41.964} +{"ev":"start","t":43573.20000001788,"sid":20,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":41.964} +{"ev":"start","t":43573.20000001788,"sid":21,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":41.964} +{"ev":"fetch","url":"audio/voice/male/tut_z_patt.ogg","status":200,"ok":true,"ms":2,"t":44385.20000001788,"ta":42.776} +{"ev":"vstart","url":"audio/voice/male/tut_z_patt.ogg","t":44395.30000001192,"dur":9.68,"ta":42.786} +{"ev":"vend","url":"audio/voice/male/tut_z_patt.ogg","t":54174.30000001192,"reason":"kill","ta":52.565} +{"ev":"start","t":55014.60000002384,"sid":22,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":54,"ta":53.406} +{"ev":"vibrate","pattern":"[8]","t":55596.70000001788,"ta":53.988} +{"ev":"start","t":55654.60000002384,"sid":23,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":54.666666666666664,"ta":54.046} +{"ev":"vibrate","pattern":"[8]","t":56253.70000001788,"ta":54.645} +{"ev":"start","t":56334.60000002384,"sid":24,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":55.333333333333336,"ta":54.726} +{"ev":"vibrate","pattern":"[8]","t":56926.70000001788,"ta":55.318} +{"ev":"start","t":57014.70000001788,"sid":25,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":56,"ta":55.406} +{"ev":"vibrate","pattern":"[8]","t":57599.80000001192,"ta":55.991} +{"ev":"start","t":57694.70000001788,"sid":26,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":56.666666666666664,"ta":56.086} +{"ev":"vibrate","pattern":"[8]","t":58272.70000001788,"ta":56.664} +{"ev":"start","t":58334.60000002384,"sid":27,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":57.33333333333333,"ta":56.726} +{"ev":"vibrate","pattern":"[8]","t":58929.80000001192,"ta":57.321} +{"ev":"start","t":59014.70000001788,"sid":28,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":58,"ta":57.406} +{"ev":"vibrate","pattern":"[8]","t":59602.70000001788,"ta":57.994} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":60885.10000002384,"ta":59.276} +{"ev":"vibrate","pattern":"[20]","t":60886.5,"ta":59.278} +{"ev":"start","t":60886.80000001192,"sid":29,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":59.278} +{"ev":"start","t":60886.80000001192,"sid":30,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":59.278} +{"ev":"gesture","type":"tap","x":195,"y":210,"t":61545.80000001192,"ta":59.937} +{"ev":"vibrate","pattern":"[20]","t":61547.10000002384,"ta":59.938} +{"ev":"start","t":61547.30000001192,"sid":31,"url":"audio/sfx/dm_tomhi.ogg","layer":"sfx","dur":0.29750566893424035,"rate":1,"loop":false,"when":0,"ta":59.938} +{"ev":"start","t":61547.30000001192,"sid":32,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":59.938} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":62291.10000002384,"ta":60.682} +{"ev":"vibrate","pattern":"[20]","t":62292.60000002384,"ta":60.684} +{"ev":"start","t":62292.80000001192,"sid":33,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":60.684} +{"ev":"start","t":62292.90000000596,"sid":34,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":60.684} +{"ev":"fetch","url":"audio/voice/male/tut_done.ogg","status":200,"ok":true,"ms":2,"t":63016.40000000596,"ta":61.408} +{"ev":"vstart","url":"audio/voice/male/tut_done.ogg","t":63020.5,"dur":2.48,"ta":61.412} +{"ev":"vend","url":"audio/voice/male/tut_done.ogg","t":65736.60000002384,"reason":"kill","ta":64.128} +{"ev":"stop","t":65737,"sid":1,"when":0,"ta":64.128} +{"ev":"start","t":65738.10000002384,"sid":35,"url":"audio/bgm/track90.ogg","layer":"bgm","dur":82.33301587301587,"rate":1,"loop":true,"when":65.14512471655328,"ta":64.129} +{"ev":"start","t":66178.30000001192,"sid":36,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":65.14512471655328,"ta":64.569} +{"ev":"vibrate","pattern":"[8]","t":66736.30000001192,"ta":65.127} +{"ev":"start","t":66818.20000001788,"sid":37,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":65.81179138321995,"ta":65.209} +{"ev":"vibrate","pattern":"[8]","t":67416.20000001788,"ta":65.807} +{"ev":"start","t":67498.40000000596,"sid":38,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":66.47845804988661,"ta":65.89} +{"ev":"vibrate","pattern":"[8]","t":68066.40000000596,"ta":66.458} +{"ev":"start","t":68178.20000001788,"sid":39,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":67.14512471655328,"ta":66.569} +{"ev":"vibrate","pattern":"[8]","t":68739.20000001788,"ta":67.13} +{"ev":"start","t":68818.30000001192,"sid":40,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":67.81179138321995,"ta":67.209} +{"ev":"vibrate","pattern":"[8]","t":69396.30000001192,"ta":67.787} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":69411,"ta":67.802} +{"ev":"vibrate","pattern":"[20]","t":69413.20000001788,"ta":67.804} +{"ev":"start","t":69413.40000000596,"sid":41,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":67.805} +{"ev":"fetch","url":"audio/voice/male/gd_answer.ogg","status":200,"ok":true,"ms":2,"t":69419.80000001192,"ta":67.811} +{"ev":"vstart","url":"audio/voice/male/gd_answer.ogg","t":69423.30000001192,"dur":1.52,"ta":67.814} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":70074.90000000596,"ta":68.466} +{"ev":"vibrate","pattern":"[20]","t":70076.30000001192,"ta":68.467} +{"ev":"start","t":70076.40000000596,"sid":42,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":68.468} +{"ev":"start","t":70178.30000001192,"sid":43,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":69.14512471655328,"ta":68.569} +{"ev":"vibrate","pattern":"[8]","t":70742.20000001788,"ta":69.133} +{"ev":"vend","url":"audio/voice/male/gd_answer.ogg","t":71030.5,"reason":"kill","ta":69.422} +{"ev":"vibrate","pattern":"[60,40,60]","t":72538.30000001192,"ta":70.929} +{"ev":"start","t":72538.5,"sid":44,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":70.93} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":73386.30000001192,"ta":71.777} +{"ev":"vibrate","pattern":"[20]","t":73387.80000001192,"ta":71.779} +{"ev":"start","t":73388,"sid":45,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":71.779} +{"ev":"start","t":73388,"sid":46,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":71.779} +{"ev":"start","t":76818.30000001192,"sid":47,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":75.81179138321995,"ta":75.209} +{"ev":"vibrate","pattern":"[8]","t":77408.30000001192,"ta":75.799} +{"ev":"start","t":78178.30000001192,"sid":48,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":77.14512471655328,"ta":76.569} +{"ev":"vibrate","pattern":"[8]","t":78732.30000001192,"ta":77.123} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":80080.5,"ta":78.472} +{"ev":"vibrate","pattern":"[20]","t":80082.90000000596,"ta":78.474} +{"ev":"start","t":80083,"sid":49,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":78.474} +{"ev":"start","t":80083.10000002384,"sid":50,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":78.474} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":81403.5,"ta":79.795} +{"ev":"vibrate","pattern":"[20]","t":81405,"ta":79.796} +{"ev":"vibrate","pattern":"[20,40,20]","t":81405,"ta":79.796} +{"ev":"start","t":81405.20000001788,"sid":51,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":79.796} +{"ev":"start","t":81405.20000001788,"sid":52,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":79.796} +{"ev":"start","t":84818.20000001788,"sid":53,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":83.81179138321994,"ta":83.209} +{"ev":"vibrate","pattern":"[8]","t":85397.30000001192,"ta":83.788} +{"ev":"start","t":85498.30000001192,"sid":54,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":84.47845804988661,"ta":83.889} +{"ev":"vibrate","pattern":"[8]","t":86071.20000001788,"ta":84.462} +{"ev":"start","t":86138.30000001192,"sid":55,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":85.14512471655327,"ta":84.529} +{"ev":"vibrate","pattern":"[8]","t":86736.30000001192,"ta":85.127} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":88024.10000002384,"ta":86.415} +{"ev":"vibrate","pattern":"[20]","t":88025.60000002384,"ta":86.417} +{"ev":"start","t":88025.70000001788,"sid":56,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":86.417} +{"ev":"start","t":88025.80000001192,"sid":57,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":86.417} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":88684.10000002384,"ta":87.075} +{"ev":"vibrate","pattern":"[20]","t":88685.70000001788,"ta":87.077} +{"ev":"start","t":88685.80000001192,"sid":58,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":87.077} +{"ev":"start","t":88685.80000001192,"sid":59,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":87.077} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":89344.90000000596,"ta":87.736} +{"ev":"vibrate","pattern":"[20]","t":89346.40000000596,"ta":87.738} +{"ev":"vibrate","pattern":"[20,40,20]","t":89346.5,"ta":87.738} +{"ev":"start","t":89346.60000002384,"sid":60,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":87.738} +{"ev":"start","t":89346.70000001788,"sid":61,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":87.738} +{"ev":"start","t":92818.20000001788,"sid":62,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":91.81179138321994,"ta":91.209} +{"ev":"vibrate","pattern":"[8]","t":93410.20000001788,"ta":91.801} +{"ev":"start","t":93498.30000001192,"sid":63,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":92.47845804988661,"ta":91.889} +{"ev":"vibrate","pattern":"[8]","t":94071.30000001192,"ta":92.462} +{"ev":"start","t":94178.30000001192,"sid":64,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":93.14512471655327,"ta":92.569} +{"ev":"vibrate","pattern":"[8]","t":94733.30000001192,"ta":93.124} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":96048.20000001788,"ta":94.439} +{"ev":"vibrate","pattern":"[20]","t":96049.60000002384,"ta":94.441} +{"ev":"start","t":96049.70000001788,"sid":65,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":94.441} +{"ev":"start","t":96049.80000001192,"sid":66,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":94.441} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":96708.70000001788,"ta":95.1} +{"ev":"vibrate","pattern":"[20]","t":96710.10000002384,"ta":95.101} +{"ev":"start","t":96710.30000001192,"sid":67,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":95.101} +{"ev":"start","t":96710.30000001192,"sid":68,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":95.101} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":97368.10000002384,"ta":95.759} +{"ev":"vibrate","pattern":"[20]","t":97369.5,"ta":95.761} +{"ev":"vibrate","pattern":"[20,40,20]","t":97369.5,"ta":95.761} +{"ev":"start","t":97369.60000002384,"sid":69,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":95.761} +{"ev":"start","t":97369.70000001788,"sid":70,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":95.761} +{"ev":"start","t":100818.20000001788,"sid":71,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":99.81179138321994,"ta":99.209} +{"ev":"vibrate","pattern":"[8]","t":101399.30000001192,"ta":99.79} +{"ev":"vstart","url":"audio/voice/male/mid_song.ogg","t":101418.5,"dur":2.32,"ta":99.81} +{"ev":"start","t":101498.20000001788,"sid":72,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":100.47845804988661,"ta":99.889} +{"ev":"vibrate","pattern":"[8]","t":102072.30000001192,"ta":100.463} +{"ev":"start","t":102138.20000001788,"sid":73,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":101.14512471655327,"ta":100.529} +{"ev":"vibrate","pattern":"[8]","t":102729.40000000596,"ta":101.121} +{"ev":"start","t":102818.30000001192,"sid":74,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":101.81179138321994,"ta":101.209} +{"ev":"vibrate","pattern":"[8]","t":103402.30000001192,"ta":101.793} +{"ev":"vend","url":"audio/voice/male/mid_song.ogg","t":103767.90000000596,"reason":"kill","ta":102.159} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":104068.40000000596,"ta":102.46} +{"ev":"vibrate","pattern":"[20]","t":104069.80000001192,"ta":102.461} +{"ev":"start","t":104070,"sid":75,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":102.461} +{"ev":"start","t":104070,"sid":76,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":102.461} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":104729.80000001192,"ta":103.121} +{"ev":"vibrate","pattern":"[20]","t":104731.20000001788,"ta":103.122} +{"ev":"start","t":104731.30000001192,"sid":77,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":103.122} +{"ev":"start","t":104731.30000001192,"sid":78,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":103.122} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":105391.10000002384,"ta":103.782} +{"ev":"vibrate","pattern":"[20]","t":105392.60000002384,"ta":103.784} +{"ev":"start","t":105392.70000001788,"sid":79,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":103.784} +{"ev":"start","t":105392.80000001192,"sid":80,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":103.784} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":106053.20000001788,"ta":104.444} +{"ev":"vibrate","pattern":"[20]","t":106054.70000001788,"ta":104.446} +{"ev":"vibrate","pattern":"[20,40,20]","t":106054.70000001788,"ta":104.446} +{"ev":"start","t":106054.80000001192,"sid":81,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":104.446} +{"ev":"start","t":106054.80000001192,"sid":82,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":104.446} +{"ev":"start","t":106138.30000001192,"sid":83,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":105.14512471655328,"ta":104.529} +{"ev":"vibrate","pattern":"[8]","t":106735.30000001192,"ta":105.126} +{"ev":"start","t":107498.40000000596,"sid":84,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":106.47845804988661,"ta":105.89} +{"ev":"vibrate","pattern":"[8]","t":108081.40000000596,"ta":106.473} +{"ev":"start","t":108178.20000001788,"sid":85,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":107.14512471655328,"ta":106.569} +{"ev":"vibrate","pattern":"[8]","t":108731.30000001192,"ta":107.122} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":109367.5,"ta":107.759} +{"ev":"vibrate","pattern":"[20]","t":109368.80000001192,"ta":107.76} +{"ev":"start","t":109369,"sid":86,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":107.76} +{"ev":"start","t":109369,"sid":87,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":107.76} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":110691.90000000596,"ta":109.083} +{"ev":"vibrate","pattern":"[20]","t":110693.5,"ta":109.085} +{"ev":"start","t":110693.70000001788,"sid":88,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":109.085} +{"ev":"start","t":110693.70000001788,"sid":89,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":109.085} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":111352.90000000596,"ta":109.744} +{"ev":"vibrate","pattern":"[20]","t":111354.5,"ta":109.746} +{"ev":"vibrate","pattern":"[100]","t":111354.5,"ta":109.746} +{"ev":"vibrate","pattern":"[20,40,20]","t":111354.60000002384,"ta":109.746} +{"ev":"start","t":111354.60000002384,"sid":90,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":109.746} +{"ev":"start","t":111354.60000002384,"sid":91,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":109.746} +{"ev":"start","t":111498.30000001192,"sid":92,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":110.47845804988661,"ta":109.889} +{"ev":"vibrate","pattern":"[8]","t":112064.30000001192,"ta":110.455} +{"ev":"start","t":112178.40000000596,"sid":93,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":111.14512471655328,"ta":110.57} +{"ev":"vibrate","pattern":"[8]","t":112738.30000001192,"ta":111.129} +{"ev":"start","t":112818.30000001192,"sid":94,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":111.81179138321994,"ta":111.209} +{"ev":"vibrate","pattern":"[8]","t":113394.30000001192,"ta":111.785} +{"ev":"start","t":113498.30000001192,"sid":95,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":112.47845804988661,"ta":111.889} +{"ev":"vibrate","pattern":"[8]","t":114067.30000001192,"ta":112.458} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":114749,"ta":113.14} +{"ev":"vibrate","pattern":"[20]","t":114750.70000001788,"ta":113.142} +{"ev":"start","t":114750.80000001192,"sid":96,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":113.142} +{"ev":"start","t":114750.80000001192,"sid":97,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":113.142} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":115414,"ta":113.805} +{"ev":"vibrate","pattern":"[20]","t":115415.30000001192,"ta":113.806} +{"ev":"start","t":115415.5,"sid":98,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":113.807} +{"ev":"start","t":115415.5,"sid":99,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":113.807} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":116078.90000000596,"ta":114.47} +{"ev":"vibrate","pattern":"[20]","t":116080.30000001192,"ta":114.471} +{"ev":"start","t":116080.40000000596,"sid":100,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":114.472} +{"ev":"start","t":116080.40000000596,"sid":101,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":114.472} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":116738.90000000596,"ta":115.13} +{"ev":"vibrate","pattern":"[20]","t":116740.5,"ta":115.132} +{"ev":"vibrate","pattern":"[20,40,20]","t":116740.5,"ta":115.132} +{"ev":"start","t":116740.60000002384,"sid":102,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":115.132} +{"ev":"start","t":116740.70000001788,"sid":103,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":115.132} +{"ev":"start","t":116818.20000001788,"sid":104,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":115.81179138321994,"ta":115.209} +{"ev":"vibrate","pattern":"[8]","t":117400.20000001788,"ta":115.791} +{"ev":"start","t":117498.30000001192,"sid":105,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":116.47845804988661,"ta":115.889} +{"ev":"vibrate","pattern":"[8]","t":118074.20000001788,"ta":116.465} +{"ev":"start","t":118138.30000001192,"sid":106,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":117.14512471655327,"ta":116.529} +{"ev":"vibrate","pattern":"[8]","t":118730.30000001192,"ta":117.121} +{"ev":"start","t":118818.20000001788,"sid":107,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":117.81179138321994,"ta":117.209} +{"ev":"vibrate","pattern":"[8]","t":119403.30000001192,"ta":117.794} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":120055.20000001788,"ta":118.446} +{"ev":"vibrate","pattern":"[20]","t":120056.80000001192,"ta":118.448} +{"ev":"start","t":120056.90000000596,"sid":108,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":118.448} +{"ev":"start","t":120056.90000000596,"sid":109,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":118.448} +{"ev":"fetch","url":"audio/voice/male/tut_fill.ogg","status":200,"ok":true,"ms":2,"t":120100.10000002384,"ta":118.491} +{"ev":"vstart","url":"audio/voice/male/tut_fill.ogg","t":120104.90000000596,"dur":3.12,"ta":118.496} +{"ev":"gesture","type":"tap","x":325,"y":630,"t":120714.90000000596,"ta":119.106} +{"ev":"vibrate","pattern":"[20]","t":120716.5,"ta":119.108} +{"ev":"start","t":120716.60000002384,"sid":110,"url":"audio/sfx/dm_tomlo.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":119.108} +{"ev":"start","t":120716.60000002384,"sid":111,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":119.108} +{"ev":"gesture","type":"tap","x":65,"y":210,"t":121377.30000001192,"ta":119.768} +{"ev":"vibrate","pattern":"[20]","t":121378.80000001192,"ta":119.77} +{"ev":"start","t":121379,"sid":112,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":1,"loop":false,"when":0,"ta":119.77} +{"ev":"start","t":121379.10000002384,"sid":113,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":119.77} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":122038.40000000596,"ta":120.43} +{"ev":"vibrate","pattern":"[20]","t":122040.10000002384,"ta":120.431} +{"ev":"vibrate","pattern":"[20,40,20]","t":122040.10000002384,"ta":120.431} +{"ev":"start","t":122040.20000001788,"sid":114,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":120.431} +{"ev":"start","t":122040.20000001788,"sid":115,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":120.431} +{"ev":"start","t":122138.20000001788,"sid":116,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":121.14512471655328,"ta":120.529} +{"ev":"start","t":122498.20000001788,"sid":117,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":121.47845804988661,"ta":120.889} +{"ev":"vibrate","pattern":"[8]","t":122736.20000001788,"ta":121.127} +{"ev":"start","t":122818.20000001788,"sid":118,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":121.81179138321995,"ta":121.209} +{"ev":"vibrate","pattern":"[8]","t":123058.30000001192,"ta":121.449} +{"ev":"start","t":123178.20000001788,"sid":119,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":122.14512471655328,"ta":121.569} +{"ev":"vibrate","pattern":"[8]","t":123410.30000001192,"ta":121.801} +{"ev":"start","t":123498.20000001788,"sid":120,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":122.47845804988661,"ta":121.889} +{"ev":"vend","url":"audio/voice/male/tut_fill.ogg","t":123549.70000001788,"reason":"kill","ta":121.941} +{"ev":"vibrate","pattern":"[8]","t":123731.20000001788,"ta":122.122} +{"ev":"start","t":123818.30000001192,"sid":121,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":122.81179138321995,"ta":122.209} +{"ev":"vibrate","pattern":"[8]","t":124060.30000001192,"ta":122.451} +{"ev":"start","t":124178.30000001192,"sid":122,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":123.14512471655328,"ta":122.569} +{"ev":"vibrate","pattern":"[8]","t":124411.20000001788,"ta":122.802} +{"ev":"vibrate","pattern":"[8]","t":124733.30000001192,"ta":123.124} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":125355,"ta":123.746} +{"ev":"vibrate","pattern":"[20]","t":125357.40000000596,"ta":123.749} +{"ev":"start","t":125357.5,"sid":123,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":123.749} +{"ev":"start","t":125357.60000002384,"sid":124,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":123.749} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":125819.20000001788,"ta":124.21} +{"ev":"vibrate","pattern":"[20]","t":125820.5,"ta":124.212} +{"ev":"start","t":125820.70000001788,"sid":125,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":124.212} +{"ev":"start","t":125820.70000001788,"sid":126,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":124.212} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":126283.30000001192,"ta":124.674} +{"ev":"vibrate","pattern":"[20]","t":126284.60000002384,"ta":124.676} +{"ev":"start","t":126284.70000001788,"sid":127,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":124.676} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":126746.30000001192,"ta":125.137} +{"ev":"vibrate","pattern":"[20]","t":126747.80000001192,"ta":125.139} +{"ev":"start","t":126747.80000001192,"sid":128,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":125.139} +{"ev":"start","t":126747.90000000596,"sid":129,"url":"audio/sfx/dm_accent.ogg","layer":"sfx","dur":0.1726984126984127,"rate":1,"loop":false,"when":0,"ta":125.139} +{"ev":"vibrate","pattern":"[60,40,60]","t":126898.20000001788,"ta":125.289} +{"ev":"start","t":126898.30000001192,"sid":130,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":125.289} +{"ev":"gesture","type":"tap","x":65,"y":630,"t":127211.5,"ta":125.603} +{"ev":"vibrate","pattern":"[20]","t":127212.90000000596,"ta":125.604} +{"ev":"vibrate","pattern":"[60,40,60]","t":127212.90000000596,"ta":125.604} +{"ev":"start","t":127213,"sid":131,"url":"audio/sfx/dm_snare.ogg","layer":"sfx","dur":0.21709750566893424,"rate":1,"loop":false,"when":0,"ta":125.604} +{"ev":"start","t":127213,"sid":132,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":125.604} +{"ev":"gesture","type":"tap","x":195,"y":630,"t":127676.90000000596,"ta":126.068} +{"ev":"vibrate","pattern":"[20]","t":127678.30000001192,"ta":126.069} +{"ev":"start","t":127678.40000000596,"sid":133,"url":"audio/sfx/inst_kick.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":126.07} +{"ev":"vibrate","pattern":"[60,40,60]","t":127898.20000001788,"ta":126.289} +{"ev":"start","t":127898.40000000596,"sid":134,"url":"audio/sfx/dm_hat.ogg","layer":"sfx","dur":0.06709750566893424,"rate":0.5,"loop":false,"when":0,"ta":126.29} +{"ev":"gesture","type":"tap","x":325,"y":210,"t":128141.60000002384,"ta":126.533} +{"ev":"vibrate","pattern":"[20]","t":128143,"ta":126.534} +{"ev":"start","t":128143.10000002384,"sid":135,"url":"audio/sfx/dm_crash.ogg","layer":"sfx","dur":1.3,"rate":1,"loop":false,"when":0,"ta":126.534} +{"ev":"vibrate","pattern":"[100]","t":128378.80000001192,"ta":126.77} +{"ev":"vibrate","pattern":"[100]","t":128378.80000001192,"ta":126.77} +{"ev":"vibrate","pattern":"[20,40,20]","t":128378.90000000596,"ta":126.77} +{"ev":"fetch","url":"audio/voice/male/grade_a.ogg","status":200,"ok":true,"ms":2,"t":128380.90000000596,"ta":126.772} +{"ev":"vstart","url":"audio/voice/male/grade_a.ogg","t":128386.70000001788,"dur":4.4,"ta":126.778} +{"ev":"stop","t":129078.70000001788,"sid":35,"when":0,"ta":127.47} +{"ev":"vend","url":"audio/voice/male/grade_a.ogg","t":132952.90000000596,"reason":"kill","ta":131.344} +{"ev":"fetch","url":"audio/voice/male/new_best_combo.ogg","status":200,"ok":true,"ms":2,"t":132954.80000001192,"ta":131.346} +{"ev":"vstart","url":"audio/voice/male/new_best_combo.ogg","t":132958.10000002384,"dur":1.84,"ta":131.349} +{"ev":"vend","url":"audio/voice/male/new_best_combo.ogg","t":134833.20000001788,"reason":"kill","ta":133.224} +{"ev":"fetch","url":"audio/voice/male/unlock_song2.ogg","status":200,"ok":true,"ms":1,"t":134834.90000000596,"ta":133.226} +{"ev":"vstart","url":"audio/voice/male/unlock_song2.ogg","t":134838.60000002384,"dur":2.48,"ta":133.23} +{"ev":"vend","url":"audio/voice/male/unlock_song2.ogg","t":137549.90000000596,"reason":"kill","ta":135.941} +{"ev":"fetch","url":"audio/voice/male/restart.ogg","status":200,"ok":true,"ms":2,"t":137551.60000002384,"ta":135.943} +{"ev":"vstart","url":"audio/voice/male/restart.ogg","t":137556.40000000596,"dur":4.08,"ta":135.948} +{"ev":"vend","url":"audio/voice/male/restart.ogg","t":141659.30000001192,"reason":"kill","ta":140.05} +{"ev":"start","t":141659.60000002384,"sid":136,"url":"audio/bgm/menu.ogg","layer":"bgm","dur":90,"rate":1,"loop":true,"when":0,"ta":140.051} +{"ev":"recstop","t":293522.90000000596,"ta":291.914} diff --git a/research/rhythm-drummer/round2/live/A/mix.webm b/research/rhythm-drummer/round2/live/A/mix.webm new file mode 100644 index 00000000..c5b13244 Binary files /dev/null and b/research/rhythm-drummer/round2/live/A/mix.webm differ diff --git a/research/rhythm-drummer/round2/live/A/transcript.md b/research/rhythm-drummer/round2/live/A/transcript.md new file mode 100644 index 00000000..fd05167c --- /dev/null +++ b/research/rhythm-drummer/round2/live/A/transcript.md @@ -0,0 +1,391 @@ +# rhythm 实时会话 transcript + +- `[ -0.09s]` **GESTURE** tap @(195,422) +- `[ 0.00s]` REC 开始 (audio/webm;codecs=opus) +- `[ 0.07s]` VOICE ▶ welcome (dur 8.24s) +- `[ 0.32s]` BGM ♪ track90 (loop) +- `[ 1.21s]` **GESTURE** tap @(195,422) +- `[ 1.21s]` VIBRATE [20] +- `[ 1.71s]` VOICE ■ welcome (kill) +- `[ 1.71s]` VOICE ▶ tut_map (dur 18.04s) +- `[ 1.71s]` VERDICT welcome: **TRUNCATED** sim=0.2667 + - 期望: 欢迎来到节拍行者。今天你是乐队的鼓手,整面屏幕都是你的鼓。 + - 听到: 欢迎来到。 +- `[ 2.58s]` **GESTURE** tap @(195,422) +- `[ 2.58s]` VIBRATE [20] +- `[ 3.08s]` VOICE ■ tut_map (kill) +- `[ 3.08s]` VOICE ▶ tut_probe (dur 3.84s) +- `[ 3.08s]` VERDICT tut_map: **TRUNCATED** sim=0.0377 + - 期望: 先认识你的鼓。屏幕横着分三列,左边、中间、右边;竖着分两行,上面清亮,下面低沉。随便摸一摸,听到哪面鼓,我就告诉你它在哪。 + - 听到: 写写认。 +- `[ 3.94s]` **GESTURE** tap @(195,422) +- `[ 3.95s]` VIBRATE [20] +- `[ 4.45s]` VOICE ■ tut_probe (kill) +- `[ 4.45s]` VERDICT tut_probe: **TRUNCATED** sim=0.4 + - 期望: 摸到三个不同的地方,我们就开始。 + - 听到: 嗯,摸到三个布。 +- `[ 5.31s]` **GESTURE** tap @(65,210) +- `[ 5.31s]` VIBRATE [20] +- `[ 5.31s]` SFX ♪ dm_hat +- `[ 5.32s]` VOICE ▶ zone_tl (dur 1.6s) +- `[ 6.92s]` VOICE ■ zone_tl (kill) +- `[ 6.92s]` VERDICT zone_tl: **OK** sim=0.75 + - 期望: 左上,踩镲。 + - 听到: 左上踩草。 +- `[ 7.98s]` **GESTURE** tap @(325,210) +- `[ 7.99s]` VIBRATE [20] +- `[ 7.99s]` SFX ♪ dm_crash +- `[ 7.99s]` VOICE ▶ zone_tr (dur 1.76s) +- `[ 9.87s]` VOICE ■ zone_tr (kill) +- `[ 9.87s]` VERDICT zone_tr: **MISMATCH** sim=0.25 + - 期望: 右上,吊镲。 + - 听到: 有上调查。 +- `[ 10.65s]` **GESTURE** tap @(65,630) +- `[ 10.66s]` VIBRATE [20] +- `[ 10.66s]` SFX ♪ dm_snare +- `[ 10.66s]` VOICE ▶ zone_bl (dur 2s) +- `[ 13.00s]` VOICE ■ zone_bl (kill) +- `[ 13.00s]` VERDICT zone_bl: **OK** sim=1 + - 期望: 左下,军鼓。 + - 听到: 左下军鼓。 +- `[ 13.01s]` VOICE ▶ tut_z_kick (dur 7.28s) +- `[ 20.55s]` VOICE ■ tut_z_kick (kill) +- `[ 20.55s]` VERDICT tut_z_kick: **OK** sim=0.9565 + - 期望: 下面正中间,是底鼓,你的家。先听四下数拍,然后跟着我敲。 + - 听到: 下面正中间是题鼓,你的家先听四下数拍,然后跟着我敲。 +- `[ 21.43s]` SFX ♪ dm_accent +- `[ 21.98s]` VIBRATE [8] +- `[ 22.07s]` SFX ♪ dm_accent +- `[ 22.66s]` VIBRATE [8] +- `[ 22.75s]` SFX ♪ dm_accent +- `[ 23.31s]` VIBRATE [8] +- `[ 23.43s]` SFX ♪ dm_accent +- `[ 23.99s]` VIBRATE [8] +- `[ 24.07s]` SFX ♪ inst_kick +- `[ 24.67s]` VIBRATE [8] +- `[ 27.30s]` **GESTURE** tap @(195,630) +- `[ 27.30s]` VIBRATE [20] +- `[ 27.30s]` SFX ♪ inst_kick +- `[ 27.30s]` SFX ♪ dm_accent +- `[ 28.08s]` VOICE ▶ tut_z_sides (dur 5.2s) +- `[ 28.08s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 33.29s]` VOICE ■ tut_z_sides (kill) +- `[ 33.29s]` VERDICT tut_z_sides: **OK** sim=0.8824 + - 期望: 左下是军鼓,右下是低嗵。一左一右,跟着打。 + - 听到: 左下是军鼓,右下是敌通,一左一右跟着打。 +- `[ 34.73s]` SFX ♪ dm_accent +- `[ 35.32s]` VIBRATE [8] +- `[ 35.41s]` SFX ♪ dm_accent +- `[ 35.99s]` VIBRATE [8] +- `[ 36.05s]` SFX ♪ dm_accent +- `[ 36.65s]` VIBRATE [8] +- `[ 36.73s]` SFX ♪ dm_accent +- `[ 37.32s]` VIBRATE [8] +- `[ 37.41s]` SFX ♪ dm_snare +- `[ 38.00s]` VIBRATE [8] +- `[ 38.73s]` SFX ♪ dm_tomlo +- `[ 39.33s]` VIBRATE [8] +- `[ 40.64s]` **GESTURE** tap @(65,630) +- `[ 40.64s]` VIBRATE [20] +- `[ 40.64s]` SFX ♪ dm_snare +- `[ 40.64s]` SFX ♪ dm_accent +- `[ 41.96s]` **GESTURE** tap @(325,630) +- `[ 41.96s]` VIBRATE [20] +- `[ 41.96s]` SFX ♪ dm_tomlo +- `[ 41.96s]` SFX ♪ dm_accent +- `[ 42.79s]` VOICE ▶ tut_z_patt (dur 9.68s) +- `[ 42.79s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 52.56s]` VOICE ■ tut_z_patt (kill) +- `[ 52.56s]` VERDICT tut_z_patt: **OK** sim=0.7692 + - 期望: 上面一排更清亮。左上踩镲,上中高嗵,右上吊镲。听这一句,三个音。 + - 听到: 上面一白更清亮,左上采茶,上中高通,右上调查,听这一句三个音。 +- `[ 53.41s]` SFX ♪ dm_accent +- `[ 53.99s]` VIBRATE [8] +- `[ 54.05s]` SFX ♪ dm_accent +- `[ 54.65s]` VIBRATE [8] +- `[ 54.73s]` SFX ♪ dm_accent +- `[ 55.32s]` VIBRATE [8] +- `[ 55.41s]` SFX ♪ dm_accent +- `[ 55.99s]` VIBRATE [8] +- `[ 56.09s]` SFX ♪ dm_hat +- `[ 56.66s]` VIBRATE [8] +- `[ 56.73s]` SFX ♪ dm_tomhi +- `[ 57.32s]` VIBRATE [8] +- `[ 57.41s]` SFX ♪ dm_crash +- `[ 57.99s]` VIBRATE [8] +- `[ 59.28s]` **GESTURE** tap @(65,210) +- `[ 59.28s]` VIBRATE [20] +- `[ 59.28s]` SFX ♪ dm_hat +- `[ 59.28s]` SFX ♪ dm_accent +- `[ 59.94s]` **GESTURE** tap @(195,210) +- `[ 59.94s]` VIBRATE [20] +- `[ 59.94s]` SFX ♪ dm_tomhi +- `[ 59.94s]` SFX ♪ dm_accent +- `[ 60.68s]` **GESTURE** tap @(325,210) +- `[ 60.68s]` VIBRATE [20] +- `[ 60.68s]` SFX ♪ dm_crash +- `[ 60.68s]` SFX ♪ dm_accent +- `[ 61.41s]` VOICE ▶ tut_done (dur 2.48s) +- `[ 61.41s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 64.13s]` VOICE ■ tut_done (kill) +- `[ 64.13s]` VERDICT tut_done: **OK** sim=1 + - 期望: 很好,演出开始。 + - 听到: 很好,演出开始。 +- `[ 64.13s]` BGM ♪ track90 (loop) +- `[ 64.57s]` SFX ♪ inst_kick +- `[ 65.13s]` VIBRATE [8] +- `[ 65.21s]` SFX ♪ inst_kick +- `[ 65.81s]` VIBRATE [8] +- `[ 65.89s]` SFX ♪ inst_kick +- `[ 66.46s]` VIBRATE [8] +- `[ 66.57s]` SFX ♪ inst_kick +- `[ 67.13s]` VIBRATE [8] +- `[ 67.21s]` SFX ♪ inst_kick +- `[ 67.79s]` VIBRATE [8] +- `[ 67.80s]` **GESTURE** tap @(195,630) +- `[ 67.80s]` VIBRATE [20] +- `[ 67.81s]` SFX ♪ inst_kick +- `[ 67.81s]` VOICE ▶ gd_answer (dur 1.52s) +- `[ 67.81s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 68.47s]` **GESTURE** tap @(195,630) +- `[ 68.47s]` VIBRATE [20] +- `[ 68.47s]` SFX ♪ inst_kick +- `[ 68.57s]` SFX ♪ inst_kick +- `[ 69.13s]` VIBRATE [8] +- `[ 69.42s]` VOICE ■ gd_answer (kill) +- `[ 69.42s]` VERDICT gd_answer: **OK** sim=1 + - 期望: 现在跟着我打。 + - 听到: 现在跟着我打。 +- `[ 70.93s]` VIBRATE [60,40,60] +- `[ 70.93s]` SFX ♪ dm_hat +- `[ 71.78s]` **GESTURE** tap @(195,630) +- `[ 71.78s]` VIBRATE [20] +- `[ 71.78s]` SFX ♪ inst_kick +- `[ 71.78s]` SFX ♪ dm_accent +- `[ 75.21s]` SFX ♪ dm_snare +- `[ 75.80s]` VIBRATE [8] +- `[ 76.57s]` SFX ♪ dm_tomlo +- `[ 77.12s]` VIBRATE [8] +- `[ 78.47s]` **GESTURE** tap @(65,630) +- `[ 78.47s]` VIBRATE [20] +- `[ 78.47s]` SFX ♪ dm_snare +- `[ 78.47s]` SFX ♪ dm_accent +- `[ 79.80s]` **GESTURE** tap @(325,630) +- `[ 79.80s]` VIBRATE [20] +- `[ 79.80s]` VIBRATE [20,40,20] +- `[ 79.80s]` SFX ♪ dm_tomlo +- `[ 79.80s]` SFX ♪ dm_accent +- `[ 83.21s]` SFX ♪ inst_kick +- `[ 83.79s]` VIBRATE [8] +- `[ 83.89s]` SFX ♪ inst_kick +- `[ 84.46s]` VIBRATE [8] +- `[ 84.53s]` SFX ♪ dm_snare +- `[ 85.13s]` VIBRATE [8] +- `[ 86.42s]` **GESTURE** tap @(195,630) +- `[ 86.42s]` VIBRATE [20] +- `[ 86.42s]` SFX ♪ inst_kick +- `[ 86.42s]` SFX ♪ dm_accent +- `[ 87.08s]` **GESTURE** tap @(195,630) +- `[ 87.08s]` VIBRATE [20] +- `[ 87.08s]` SFX ♪ inst_kick +- `[ 87.08s]` SFX ♪ dm_accent +- `[ 87.74s]` **GESTURE** tap @(65,630) +- `[ 87.74s]` VIBRATE [20] +- `[ 87.74s]` VIBRATE [20,40,20] +- `[ 87.74s]` SFX ♪ dm_snare +- `[ 87.74s]` SFX ♪ dm_accent +- `[ 91.21s]` SFX ♪ dm_snare +- `[ 91.80s]` VIBRATE [8] +- `[ 91.89s]` SFX ♪ dm_tomlo +- `[ 92.46s]` VIBRATE [8] +- `[ 92.57s]` SFX ♪ inst_kick +- `[ 93.12s]` VIBRATE [8] +- `[ 94.44s]` **GESTURE** tap @(65,630) +- `[ 94.44s]` VIBRATE [20] +- `[ 94.44s]` SFX ♪ dm_snare +- `[ 94.44s]` SFX ♪ dm_accent +- `[ 95.10s]` **GESTURE** tap @(325,630) +- `[ 95.10s]` VIBRATE [20] +- `[ 95.10s]` SFX ♪ dm_tomlo +- `[ 95.10s]` SFX ♪ dm_accent +- `[ 95.76s]` **GESTURE** tap @(195,630) +- `[ 95.76s]` VIBRATE [20] +- `[ 95.76s]` VIBRATE [20,40,20] +- `[ 95.76s]` SFX ♪ inst_kick +- `[ 95.76s]` SFX ♪ dm_accent +- `[ 99.21s]` SFX ♪ inst_kick +- `[ 99.79s]` VIBRATE [8] +- `[ 99.81s]` VOICE ▶ mid_song (dur 2.32s) +- `[ 99.81s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 99.89s]` SFX ♪ dm_snare +- `[ 100.46s]` VIBRATE [8] +- `[ 100.53s]` SFX ♪ inst_kick +- `[ 101.12s]` VIBRATE [8] +- `[ 101.21s]` SFX ♪ dm_tomlo +- `[ 101.79s]` VIBRATE [8] +- `[ 102.16s]` VOICE ■ mid_song (kill) +- `[ 102.16s]` VERDICT mid_song: **OK** sim=0.8 + - 期望: 过半了,保持。 + - 听到: 过磅了,保持。 +- `[ 102.46s]` **GESTURE** tap @(195,630) +- `[ 102.46s]` VIBRATE [20] +- `[ 102.46s]` SFX ♪ inst_kick +- `[ 102.46s]` SFX ♪ dm_accent +- `[ 103.12s]` **GESTURE** tap @(65,630) +- `[ 103.12s]` VIBRATE [20] +- `[ 103.12s]` SFX ♪ dm_snare +- `[ 103.12s]` SFX ♪ dm_accent +- `[ 103.78s]` **GESTURE** tap @(195,630) +- `[ 103.78s]` VIBRATE [20] +- `[ 103.78s]` SFX ♪ inst_kick +- `[ 103.78s]` SFX ♪ dm_accent +- `[ 104.44s]` **GESTURE** tap @(325,630) +- `[ 104.45s]` VIBRATE [20] +- `[ 104.45s]` VIBRATE [20,40,20] +- `[ 104.45s]` SFX ♪ dm_tomlo +- `[ 104.45s]` SFX ♪ dm_accent +- `[ 104.53s]` SFX ♪ dm_hat +- `[ 105.13s]` VIBRATE [8] +- `[ 105.89s]` SFX ♪ dm_snare +- `[ 106.47s]` VIBRATE [8] +- `[ 106.57s]` SFX ♪ inst_kick +- `[ 107.12s]` VIBRATE [8] +- `[ 107.76s]` **GESTURE** tap @(65,210) +- `[ 107.76s]` VIBRATE [20] +- `[ 107.76s]` SFX ♪ dm_hat +- `[ 107.76s]` SFX ♪ dm_accent +- `[ 109.08s]` **GESTURE** tap @(65,630) +- `[ 109.08s]` VIBRATE [20] +- `[ 109.08s]` SFX ♪ dm_snare +- `[ 109.08s]` SFX ♪ dm_accent +- `[ 109.74s]` **GESTURE** tap @(195,630) +- `[ 109.75s]` VIBRATE [20] +- `[ 109.75s]` VIBRATE [100] +- `[ 109.75s]` VIBRATE [20,40,20] +- `[ 109.75s]` SFX ♪ inst_kick +- `[ 109.75s]` SFX ♪ dm_accent +- `[ 109.89s]` SFX ♪ inst_kick +- `[ 110.45s]` VIBRATE [8] +- `[ 110.57s]` SFX ♪ dm_hat +- `[ 111.13s]` VIBRATE [8] +- `[ 111.21s]` SFX ♪ inst_kick +- `[ 111.78s]` VIBRATE [8] +- `[ 111.89s]` SFX ♪ dm_crash +- `[ 112.46s]` VIBRATE [8] +- `[ 113.14s]` **GESTURE** tap @(195,630) +- `[ 113.14s]` VIBRATE [20] +- `[ 113.14s]` SFX ♪ inst_kick +- `[ 113.14s]` SFX ♪ dm_accent +- `[ 113.81s]` **GESTURE** tap @(65,210) +- `[ 113.81s]` VIBRATE [20] +- `[ 113.81s]` SFX ♪ dm_hat +- `[ 113.81s]` SFX ♪ dm_accent +- `[ 114.47s]` **GESTURE** tap @(195,630) +- `[ 114.47s]` VIBRATE [20] +- `[ 114.47s]` SFX ♪ inst_kick +- `[ 114.47s]` SFX ♪ dm_accent +- `[ 115.13s]` **GESTURE** tap @(325,210) +- `[ 115.13s]` VIBRATE [20] +- `[ 115.13s]` VIBRATE [20,40,20] +- `[ 115.13s]` SFX ♪ dm_crash +- `[ 115.13s]` SFX ♪ dm_accent +- `[ 115.21s]` SFX ♪ dm_snare +- `[ 115.79s]` VIBRATE [8] +- `[ 115.89s]` SFX ♪ dm_tomlo +- `[ 116.47s]` VIBRATE [8] +- `[ 116.53s]` SFX ♪ dm_hat +- `[ 117.12s]` VIBRATE [8] +- `[ 117.21s]` SFX ♪ inst_kick +- `[ 117.79s]` VIBRATE [8] +- `[ 118.45s]` **GESTURE** tap @(65,630) +- `[ 118.45s]` VIBRATE [20] +- `[ 118.45s]` SFX ♪ dm_snare +- `[ 118.45s]` SFX ♪ dm_accent +- `[ 118.50s]` VOICE ▶ tut_fill (dur 3.12s) +- `[ 118.50s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 119.11s]` **GESTURE** tap @(325,630) +- `[ 119.11s]` VIBRATE [20] +- `[ 119.11s]` SFX ♪ dm_tomlo +- `[ 119.11s]` SFX ♪ dm_accent +- `[ 119.77s]` **GESTURE** tap @(65,210) +- `[ 119.77s]` VIBRATE [20] +- `[ 119.77s]` SFX ♪ dm_hat +- `[ 119.77s]` SFX ♪ dm_accent +- `[ 120.43s]` **GESTURE** tap @(195,630) +- `[ 120.43s]` VIBRATE [20] +- `[ 120.43s]` VIBRATE [20,40,20] +- `[ 120.43s]` SFX ♪ inst_kick +- `[ 120.43s]` SFX ♪ dm_accent +- `[ 120.53s]` SFX ♪ inst_kick +- `[ 120.89s]` SFX ♪ inst_kick +- `[ 121.13s]` VIBRATE [8] +- `[ 121.21s]` SFX ♪ inst_kick +- `[ 121.45s]` VIBRATE [8] +- `[ 121.57s]` SFX ♪ dm_snare +- `[ 121.80s]` VIBRATE [8] +- `[ 121.89s]` SFX ♪ dm_snare +- `[ 121.94s]` VOICE ■ tut_fill (kill) +- `[ 121.94s]` VERDICT tut_fill: **OK** sim=0.9167 + - 期望: 最后有一段加花,跟着我连打。 + - 听到: 最后有一段佳花跟着我连打。 +- `[ 122.12s]` VIBRATE [8] +- `[ 122.21s]` SFX ♪ inst_kick +- `[ 122.45s]` VIBRATE [8] +- `[ 122.57s]` SFX ♪ dm_crash +- `[ 122.80s]` VIBRATE [8] +- `[ 123.12s]` VIBRATE [8] +- `[ 123.75s]` **GESTURE** tap @(195,630) +- `[ 123.75s]` VIBRATE [20] +- `[ 123.75s]` SFX ♪ inst_kick +- `[ 123.75s]` SFX ♪ dm_accent +- `[ 124.21s]` **GESTURE** tap @(195,630) +- `[ 124.21s]` VIBRATE [20] +- `[ 124.21s]` SFX ♪ inst_kick +- `[ 124.21s]` SFX ♪ dm_accent +- `[ 124.67s]` **GESTURE** tap @(195,630) +- `[ 124.68s]` VIBRATE [20] +- `[ 124.68s]` SFX ♪ inst_kick +- `[ 125.14s]` **GESTURE** tap @(65,630) +- `[ 125.14s]` VIBRATE [20] +- `[ 125.14s]` SFX ♪ dm_snare +- `[ 125.14s]` SFX ♪ dm_accent +- `[ 125.29s]` VIBRATE [60,40,60] +- `[ 125.29s]` SFX ♪ dm_hat +- `[ 125.60s]` **GESTURE** tap @(65,630) +- `[ 125.60s]` VIBRATE [20] +- `[ 125.60s]` VIBRATE [60,40,60] +- `[ 125.60s]` SFX ♪ dm_snare +- `[ 125.60s]` SFX ♪ dm_hat +- `[ 126.07s]` **GESTURE** tap @(195,630) +- `[ 126.07s]` VIBRATE [20] +- `[ 126.07s]` SFX ♪ inst_kick +- `[ 126.29s]` VIBRATE [60,40,60] +- `[ 126.29s]` SFX ♪ dm_hat +- `[ 126.53s]` **GESTURE** tap @(325,210) +- `[ 126.53s]` VIBRATE [20] +- `[ 126.53s]` SFX ♪ dm_crash +- `[ 126.77s]` VIBRATE [100] +- `[ 126.77s]` VIBRATE [100] +- `[ 126.77s]` VIBRATE [20,40,20] +- `[ 126.78s]` VOICE ▶ grade_a (dur 4.4s) +- `[ 126.78s]` VERDICT (无语音区间): **SFX_BGM_ONLY** +- `[ 131.34s]` VOICE ■ grade_a (kill) +- `[ 131.34s]` VERDICT grade_a: **OK** sim=0.6429 + - 期望: A 级!准确率超过八成五,非常出色。 + - 听到: 第G准确率超过8乘5,非常出色。 +- `[ 131.35s]` VOICE ▶ new_best_combo (dur 1.84s) +- `[ 133.22s]` VOICE ■ new_best_combo (kill) +- `[ 133.22s]` VERDICT new_best_combo: **MISMATCH** sim=0.4 + - 期望: 新连击纪录! + - 听到: 新联机记录。 +- `[ 133.23s]` VOICE ▶ unlock_song2 (dur 2.48s) +- `[ 135.94s]` VOICE ■ unlock_song2 (kill) +- `[ 135.94s]` VERDICT unlock_song2: **OK** sim=0.9333 + - 期望: 新曲目解锁,追风! + - 听到: 录新曲目解锁追风。 +- `[ 135.95s]` VOICE ▶ restart (dur 4.08s) +- `[ 140.05s]` VOICE ■ restart (kill) +- `[ 140.05s]` VERDICT restart: **OK** sim=0.75 + - 期望: 单击再来一遍,右滑下一首,下滑回大厅。 + - 听到: 但气再来一遍,又滑下一手下滑回大厅。 +- `[ 140.05s]` BGM ♪ menu (loop) diff --git a/research/rhythm-drummer/round2/live/A/verdicts.json b/research/rhythm-drummer/round2/live/A/verdicts.json new file mode 100644 index 00000000..218a03fa --- /dev/null +++ b/research/rhythm-drummer/round2/live/A/verdicts.json @@ -0,0 +1,451 @@ +{ + "game": "rhythm", + "settings": { + "voice": "male", + "guide": "full", + "speed": 1, + "bgm": 1 + }, + "outdir": "/home/xsl/AutoVideo/SoundGame/web/harness/live/out/rhythm-20260914102057", + "segs": [ + { + "idx": 0, + "voiceId": "welcome", + "t0": 0.066, + "t1": 1.71, + "dur": 8.24, + "expEnd": 8.306, + "truncated": true, + "reason": "kill", + "verdict": "TRUNCATED", + "heard": "欢迎来到。", + "similarity": 0.2667 + }, + { + "idx": 1, + "voiceId": "tut_map", + "t0": 1.71, + "t1": 3.078, + "dur": 18.04, + "expEnd": 19.75, + "truncated": true, + "reason": "kill", + "verdict": "TRUNCATED", + "heard": "写写认。", + "similarity": 0.0377 + }, + { + "idx": 2, + "voiceId": "tut_probe", + "t0": 3.078, + "t1": 4.446, + "dur": 3.84, + "expEnd": 6.918, + "truncated": true, + "reason": "kill", + "verdict": "TRUNCATED", + "heard": "嗯,摸到三个布。", + "similarity": 0.4 + }, + { + "idx": 3, + "voiceId": "zone_tl", + "t0": 5.319, + "t1": 6.919, + "dur": 1.6, + "expEnd": 6.919, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "左上踩草。", + "similarity": 0.75 + }, + { + "idx": 4, + "voiceId": "zone_tr", + "t0": 7.992, + "t1": 9.867, + "dur": 1.76, + "expEnd": 9.752, + "truncated": false, + "reason": "kill", + "verdict": "MISMATCH", + "heard": "有上调查。", + "similarity": 0.25 + }, + { + "idx": 5, + "voiceId": "zone_bl", + "t0": 10.66, + "t1": 13.002, + "dur": 2, + "expEnd": 12.66, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "左下军鼓。", + "similarity": 1 + }, + { + "idx": 6, + "voiceId": "tut_z_kick", + "t0": 13.011, + "t1": 20.548, + "dur": 7.28, + "expEnd": 20.291, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "下面正中间是题鼓,你的家先听四下数拍,然后跟着我敲。", + "similarity": 0.9565 + }, + { + "idx": 7, + "voiceId": "tut_z_sides", + "t0": 28.077, + "t1": 33.294, + "dur": 5.2, + "expEnd": 33.277, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "左下是军鼓,右下是敌通,一左一右跟着打。", + "similarity": 0.8824 + }, + { + "idx": 8, + "voiceId": "tut_z_patt", + "t0": 42.786, + "t1": 52.565, + "dur": 9.68, + "expEnd": 52.466, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "上面一白更清亮,左上采茶,上中高通,右上调查,听这一句三个音。", + "similarity": 0.7692 + }, + { + "idx": 9, + "voiceId": "tut_done", + "t0": 61.412, + "t1": 64.128, + "dur": 2.48, + "expEnd": 63.892, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "很好,演出开始。", + "similarity": 1 + }, + { + "idx": 10, + "voiceId": "gd_answer", + "t0": 67.814, + "t1": 69.422, + "dur": 1.52, + "expEnd": 69.334, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "现在跟着我打。", + "similarity": 1 + }, + { + "idx": 11, + "voiceId": "mid_song", + "t0": 99.81, + "t1": 102.159, + "dur": 2.32, + "expEnd": 102.13, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "过磅了,保持。", + "similarity": 0.8 + }, + { + "idx": 12, + "voiceId": "tut_fill", + "t0": 118.496, + "t1": 121.941, + "dur": 3.12, + "expEnd": 121.616, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "最后有一段佳花跟着我连打。", + "similarity": 0.9167 + }, + { + "idx": 13, + "voiceId": "grade_a", + "t0": 126.778, + "t1": 131.344, + "dur": 4.4, + "expEnd": 131.178, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "第G准确率超过8乘5,非常出色。", + "similarity": 0.6429 + }, + { + "idx": 14, + "voiceId": "new_best_combo", + "t0": 131.349, + "t1": 133.224, + "dur": 1.84, + "expEnd": 133.189, + "truncated": false, + "reason": "kill", + "verdict": "MISMATCH", + "heard": "新联机记录。", + "similarity": 0.4 + }, + { + "idx": 15, + "voiceId": "unlock_song2", + "t0": 133.23, + "t1": 135.941, + "dur": 2.48, + "expEnd": 135.71, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "录新曲目解锁追风。", + "similarity": 0.9333 + }, + { + "idx": 16, + "voiceId": "restart", + "t0": 135.948, + "t1": 140.05, + "dur": 4.08, + "expEnd": 140.028, + "truncated": false, + "reason": "kill", + "verdict": "OK", + "heard": "但气再来一遍,又滑下一手下滑回大厅。", + "similarity": 0.75 + } + ], + "rows": [ + { + "t0": 0.066, + "t1": 1.71, + "voiceId": "welcome", + "expected": "欢迎来到节拍行者。今天你是乐队的鼓手,整面屏幕都是你的鼓。", + "heard": "欢迎来到。", + "similarity": 0.2667, + "verdict": "TRUNCATED" + }, + { + "t0": 1.71, + "t1": 3.078, + "voiceId": "tut_map", + "expected": "先认识你的鼓。屏幕横着分三列,左边、中间、右边;竖着分两行,上面清亮,下面低沉。随便摸一摸,听到哪面鼓,我就告诉你它在哪。", + "heard": "写写认。", + "similarity": 0.0377, + "verdict": "TRUNCATED" + }, + { + "t0": 3.078, + "t1": 4.446, + "voiceId": "tut_probe", + "expected": "摸到三个不同的地方,我们就开始。", + "heard": "嗯,摸到三个布。", + "similarity": 0.4, + "verdict": "TRUNCATED" + }, + { + "t0": 5.319, + "t1": 6.919, + "voiceId": "zone_tl", + "expected": "左上,踩镲。", + "heard": "左上踩草。", + "similarity": 0.75, + "verdict": "OK" + }, + { + "t0": 7.992, + "t1": 9.867, + "voiceId": "zone_tr", + "expected": "右上,吊镲。", + "heard": "有上调查。", + "similarity": 0.25, + "verdict": "MISMATCH" + }, + { + "t0": 10.66, + "t1": 13.002, + "voiceId": "zone_bl", + "expected": "左下,军鼓。", + "heard": "左下军鼓。", + "similarity": 1, + "verdict": "OK" + }, + { + "t0": 13.011, + "t1": 20.548, + "voiceId": "tut_z_kick", + "expected": "下面正中间,是底鼓,你的家。先听四下数拍,然后跟着我敲。", + "heard": "下面正中间是题鼓,你的家先听四下数拍,然后跟着我敲。", + "similarity": 0.9565, + "verdict": "OK" + }, + { + "t0": 28.077, + "t1": 33.294, + "voiceId": "tut_z_sides", + "expected": "左下是军鼓,右下是低嗵。一左一右,跟着打。", + "heard": "左下是军鼓,右下是敌通,一左一右跟着打。", + "similarity": 0.8824, + "verdict": "OK" + }, + { + "t0": 42.786, + "t1": 52.565, + "voiceId": "tut_z_patt", + "expected": "上面一排更清亮。左上踩镲,上中高嗵,右上吊镲。听这一句,三个音。", + "heard": "上面一白更清亮,左上采茶,上中高通,右上调查,听这一句三个音。", + "similarity": 0.7692, + "verdict": "OK" + }, + { + "t0": 61.412, + "t1": 64.128, + "voiceId": "tut_done", + "expected": "很好,演出开始。", + "heard": "很好,演出开始。", + "similarity": 1, + "verdict": "OK" + }, + { + "t0": 67.814, + "t1": 69.422, + "voiceId": "gd_answer", + "expected": "现在跟着我打。", + "heard": "现在跟着我打。", + "similarity": 1, + "verdict": "OK" + }, + { + "t0": 99.81, + "t1": 102.159, + "voiceId": "mid_song", + "expected": "过半了,保持。", + "heard": "过磅了,保持。", + "similarity": 0.8, + "verdict": "OK" + }, + { + "t0": 118.496, + "t1": 121.941, + "voiceId": "tut_fill", + "expected": "最后有一段加花,跟着我连打。", + "heard": "最后有一段佳花跟着我连打。", + "similarity": 0.9167, + "verdict": "OK" + }, + { + "t0": 126.778, + "t1": 131.344, + "voiceId": "grade_a", + "expected": "A 级!准确率超过八成五,非常出色。", + "heard": "第G准确率超过8乘5,非常出色。", + "similarity": 0.6429, + "verdict": "OK" + }, + { + "t0": 131.349, + "t1": 133.224, + "voiceId": "new_best_combo", + "expected": "新连击纪录!", + "heard": "新联机记录。", + "similarity": 0.4, + "verdict": "MISMATCH" + }, + { + "t0": 133.23, + "t1": 135.941, + "voiceId": "unlock_song2", + "expected": "新曲目解锁,追风!", + "heard": "录新曲目解锁追风。", + "similarity": 0.9333, + "verdict": "OK" + }, + { + "t0": 135.948, + "t1": 140.05, + "voiceId": "restart", + "expected": "单击再来一遍,右滑下一首,下滑回大厅。", + "heard": "但气再来一遍,又滑下一手下滑回大厅。", + "similarity": 0.75, + "verdict": "OK" + }, + { + "t0": 20.548, + "t1": 28.077, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 33.294, + "t1": 42.786, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 52.565, + "t1": 61.412, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 64.128, + "t1": 67.814, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 69.422, + "t1": 99.81, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 102.159, + "t1": 118.496, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + }, + { + "t0": 121.941, + "t1": 126.778, + "voiceId": null, + "expected": null, + "heard": "", + "similarity": null, + "verdict": "SFX_BGM_ONLY" + } + ] +} \ No newline at end of file diff --git a/research/rhythm-drummer/round2/persona-reviews.md b/research/rhythm-drummer/round2/persona-reviews.md new file mode 100644 index 00000000..c727c62b --- /dev/null +++ b/research/rhythm-drummer/round2/persona-reviews.md @@ -0,0 +1,52 @@ +# 《节拍行者》鼓手模式 R2 人设评审(复评,基于 R2 live 实录 + 修复说明) + +## P1 视障资深玩家(专业评委) + +### 节拍行者(鼓手模式) +- 状态感知:5/5 —— R1 提的两个扣分项都修实了:连摸区名完整播完(live 实录 zone_bl 播足 2.34s 自然收束),曲尾不再飘半句向导话,结算干净利落。「还差一个」的进度提示未加,但教学句「摸到三个不同的地方,我们就开始」本身可数,可接受。 +- 下一步明确性:5/5 —— 应答零语音、结算三选项、摸鼓可数,无迷失时刻。 +- 愿意继续玩:**是** —— 修复后的信息流称得上「干净」:该说的说完整,不该说的不抢拍。 +- P0 问题:无。 +- 建议:无新增(R1 三条建议全部落地或裁定不改)。 + +## P2 时间充裕的大学生 + +### 节拍行者(鼓手模式) +- 状态感知:5/5 —— 教学失败不再「再听一遍」二连播(F3),纠正一句话说完就重新领奏,节奏感好多了。 +- 下一步明确性:5/5 —— 同 R1。 +- 愿意继续玩:**是** —— A 级解锁《追风》的钩子已验证有效(两连真机都是 A 级弧线)。 +- P0 问题:无。 +- 建议:维持 R1 裁定(unlock 文案不改)。 + +## P3 时间紧张的白领 + +### 节拍行者(鼓手模式) +- 状态感知:5/5 —— 菜单中断恢复、单手盲操、单局 65-80s,R2 无变化无回退。 +- 下一步明确性:5/5 —— 同 R1。 +- 愿意继续玩:**是**。 +- P0 问题:无。 +- 建议:无新增。 + +## P4 疲惫的工人 + +### 节拍行者(鼓手模式) +- 状态感知:5/5 —— 降速重演、零数落文案、放慢再教,R2 保持。 +- 下一步明确性:5/5 —— 同 R1。 +- 愿意继续玩:**是**。 +- P0 问题:无。 +- 建议:无新增。 + +--- + +### 专门问题复答 + +**分区心智模型**:R2 live 直证——区名播报机制完整可用(F2 修复后三连摸 +全报名),真机 33/37 Perfect 说明「听音色+声像找区」的映射稳定可掌握。 + +**合拍感**:dt≈0.00s 真机复现(R2 与 R1 演奏精度一致),Perfect 亮音叠加 +与 16 连击震动保持。 + +**旧作亮点保留**:全部在(跟随领奏/评级解锁/BPM 精确网格/不中断/旁白跳过/ +菜单断点续播),无一项回退。 + +**裁决:4/4 愿意继续玩,P0 = 0 —— R2 达标(连续第 2 轮,终止条件达成)。** diff --git a/research/rhythm-drummer/round2/synthesis.md b/research/rhythm-drummer/round2/synthesis.md new file mode 100644 index 00000000..73f9547c --- /dev/null +++ b/research/rhythm-drummer/round2/synthesis.md @@ -0,0 +1,40 @@ +# 《节拍行者》鼓手模式 R2 synthesis(产品裁定) + +## 输入 + +- R2 live 复跑(`live-player.md`:OK 12/TRUNCATED 3/MISMATCH 2;F1-F3 全部 + 真机直证;33 Perfect / 4 Miss;A 级+解锁全弧线) +- 人设复评:4/4 愿意继续玩,五项感知指标全部 5/5(P1 由 4/5 升 5/5),P0 = 0 + +## 裁定:4/4 愿意继续玩,P0 = 0 —— **R2 达标,连续第 2 轮达标,终止条件达成(≤R3 上限内)** + +## P0 清单 + +无(连续第 2 轮为零)。 + +## P1 清单 + +无新增。R1 三项修复验收: + +1. **F1 曲尾 gd_miss 守卫** ✅ mock 断言 A24 + live 直证(曲尾 4 Miss 零 + 向导语音调度,结算链无截断)。 +2. **F2 探针区名防掐断** ✅ mock 断言 A25 + live 直证(zone_bl 播足 2.34s + 自然收束,教学句 queue 接续)。实现要点:进阶延迟 150ms 起步——引擎 + _playVoice 的入队判定在 await 加载完成之前,同步连发会让教学句误判 + 「通道空闲」顶掉区名。 +3. **F3 纠正链去重** ✅ mock 断言 A23 + live 教学段语音序列无相邻复读。 + +## F 类修复复核结论 + +- playtest 129 → **134**(新增 A23/A24/A25 三条回归断言,既有零回改) +- guide.test 122 / check_all_assets 全绿 / 15 游戏 playtest + engine-voice + + engine-input 全量回归绿 +- 资产零新增(修复全部代码层) + +## 交付结论 + +《节拍行者》鼓手模式重构**可交付**:屏幕分区(3列×2行六件打击乐)心智模型 +经教学与真机验证可建立;跟随领奏核心、S~C 评级、曲目解锁、BPM 精确性等 +旧作好评基因全部保留;两轮玩家迭代(4 人设 ×2 + live 两会话 ×2)零 P0、 +连续达标。遗留观察项(非阻塞):短句 ASR 同音误识属转写基础设施固有噪声; +unlock 播报燃度按 R1 裁定维持。 diff --git a/tools/gen_rhythm_syn.py b/tools/gen_rhythm_syn.py new file mode 100644 index 00000000..fbcf47d0 --- /dev/null +++ b/tools/gen_rhythm_syn.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""《节拍行者》鼓手模式程序合成打击乐(24-rhythm-drummer.md §9.1,7 件 SYN)。 + +六区鼓组 + Perfect accent:kick/snare/hat/tomhi/crash/tomlo/accent。 +落盘:staging wav → tools/gen_rhythm_syn_wav/.wav(44.1kHz 立体声); + 成品 → web/rhythm/audio/sfx/.{ogg,m4a}(tools/bin/ffmpeg 双格式)。 + +QC(gen_cipher_syn.py 同款):去直流 + 首尾 8ms cos² 淡化 + 编码后解码回读 +真峰值 ≤ -3dBFS(MDCT 过冲坑:ebur128 peak=true 实测,超限自动降档重编)。 +打击乐起音必须硬(盲玩靠起音对拍);包络用 (1-exp(-t*k)) 快起沿。 + +用法:.venv-tts/bin/python tools/gen_rhythm_syn.py +""" +import os +import subprocess +import sys + +import numpy as np +import soundfile as sf + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +FFMPEG = os.path.join(ROOT, "tools", "bin", "ffmpeg") +OUT = os.path.join(ROOT, "tools", "gen_rhythm_syn_wav") +DST = os.path.join(ROOT, "web", "rhythm", "audio", "sfx") +os.makedirs(OUT, exist_ok=True) +os.makedirs(DST, exist_ok=True) + +sr = 44100 +ONE_SHOT_TP_CEIL = -3.0 # dBFS +SAMPLE_PEAK = 0.66 # ≈ -3.6dBFS,给 MDCT 过冲留余量 +FADE_N = int(sr * 0.008) + +_rng = np.random.default_rng(20260914) + + +def dc(y): + return y - np.mean(y) + + +def fade(y): + w = np.sin(np.linspace(0, np.pi / 2, FADE_N)) ** 2 + y = y.copy() + y[:FADE_N] *= w + y[-FADE_N:] *= w[::-1] + return y + + +def norm(y, peak=SAMPLE_PEAK): + m = np.max(np.abs(y)) + return (y / m * peak) if m > 0 else y + + +def stereo(y): + return np.stack([y, y], axis=1).astype(np.float32) + + +def bp_noise(n, lo, hi): + x = _rng.standard_normal(n) + X = np.fft.rfft(x) + freqs = np.fft.rfftfreq(n, 1 / sr) + mask = ((freqs >= lo) & (freqs <= hi)).astype(float) + y = np.fft.irfft(X * mask, n) + return y / (np.max(np.abs(y)) + 1e-9) + + +def sweep(n, f0, f1, dec): + """指数扫频正弦(鼓皮),dec = 幅度衰减系数(1/s)。""" + t = np.arange(n) / sr + k = np.log(f1 / f0) / (n / sr) + ph = 2 * np.pi * f0 * (np.exp(k * t) - 1) / k + return np.sin(ph) * np.exp(-t * dec) + + +# ---------------------------------------------------------------- 鼓件渲染 +def r_kick(): # 底鼓 BC:130→45Hz 扫频 + 触击瞬态(起沿 <3ms);id 沿用 inst_kick + n = int(sr * 0.28) + t = np.arange(n) / sr + y = sweep(n, 130, 45, 16) * 1.0 + click = bp_noise(n, 1500, 6000) * np.exp(-t * 220) * 0.5 # 槌击瞬态 + th = np.sin(2 * np.pi * 60 * t) * np.exp(-t * 22) * 0.35 # 低频胸口感 + return dc((y + click + th) * (1 - np.exp(-t * 900))) + + +def r_snare(): # 军鼓 BL:200Hz 皮 + 1-4kHz 带通沙噪 + 钢丝噪 + n = int(sr * 0.22) + t = np.arange(n) / sr + head = np.sin(2 * np.pi * 196 * t) * np.exp(-t * 38) * 0.8 + wires = bp_noise(n, 3000, 7000) * np.exp(-t * 55) * 0.55 + body = bp_noise(n, 800, 3000) * np.exp(-t * 30) * 0.7 + return dc((head + wires + body) * (1 - np.exp(-t * 1200))) + + +def r_hat(): # 闭合踩镲 TL:6-10kHz 高通金属噪,60ms 急收 + n = int(sr * 0.07) + t = np.arange(n) / sr + y = bp_noise(n, 6000, 12000) * np.exp(-t * 90) + y += bp_noise(n, 9000, 14000) * np.exp(-t * 140) * 0.6 + metallic = (np.sin(2 * np.pi * 11200 * t) * 0.25 + + np.sin(2 * np.pi * 8350 * t) * 0.18) * np.exp(-t * 110) + return dc((y + metallic) * (1 - np.exp(-t * 2500))) + + +def r_tomhi(): # 高音嗵 TC:200→140Hz 皮,短促圆头 + n = int(sr * 0.30) + t = np.arange(n) / sr + y = sweep(n, 205, 140, 14) * 0.95 + skin = bp_noise(n, 300, 1400) * np.exp(-t * 40) * 0.3 + return dc((y + skin) * (1 - np.exp(-t * 700))) + + +def r_crash(): # 吊镲 TR:4-12kHz 噪 + 非谐波泛音列,1.2s 长尾 + n = int(sr * 1.30) + t = np.arange(n) / sr + y = bp_noise(n, 4000, 13000) * np.exp(-t * 5.5) * 0.8 + for f, g in [(6120, 0.30), (8130, 0.22), (10460, 0.15), (4620, 0.20)]: + y += np.sin(2 * np.pi * f * t) * np.exp(-t * 4.2) * g + y += bp_noise(n, 2000, 5000) * np.exp(-t * 9) * 0.35 + return dc(y * (1 - np.exp(-t * 1800))) + + +def r_tomlo(): # 低音嗵 BR:120→75Hz 皮,共鸣更久 + n = int(sr * 0.35) + t = np.arange(n) / sr + y = sweep(n, 122, 75, 10) * 0.95 + skin = bp_noise(n, 200, 900) * np.exp(-t * 30) * 0.3 + return dc((y + skin) * (1 - np.exp(-t * 550))) + + +def r_accent(): # Perfect 叠加亮音:木鱼 1.5kHz + 2.2kHz 短双音 + a = np.sin(2 * np.pi * 1500 * np.arange(int(sr * 0.05)) / sr) \ + * np.exp(-np.arange(int(sr * 0.05)) / sr * 90) + b = np.sin(2 * np.pi * 2200 * np.arange(int(sr * 0.08)) / sr) \ + * np.exp(-np.arange(int(sr * 0.08)) / sr * 60) * 0.7 + return dc(np.concatenate([a, np.zeros(int(sr * 0.015)), b, np.zeros(int(sr * 0.03))])) + + +ONE_SHOTS = { + "inst_kick": r_kick, + "dm_snare": r_snare, + "dm_hat": r_hat, + "dm_tomhi": r_tomhi, + "dm_crash": r_crash, + "dm_tomlo": r_tomlo, + "dm_accent": r_accent, +} + + +# ---------------------------------------------------------------- 编码 + 真峰值校验 +def encode(wav_path, base): + for args, ext in [(["-c:a", "libvorbis", "-q:a", "3"], ".ogg"), + (["-c:a", "aac", "-b:a", "128k"], ".m4a")]: + subprocess.run([FFMPEG, "-y", "-v", "error", "-i", wav_path, + "-ar", "44100", *args, base + ext], check=True) + + +def true_peak_db(path): + r = subprocess.run([FFMPEG, "-i", path, "-af", "ebur128=peak=true", "-f", "null", "-"], + capture_output=True, text=True) + peaks = [float(line.split("Peak:")[1].strip().split()[0]) + for line in r.stderr.splitlines() if "Peak:" in line and "dBFS" in line] + return max(peaks) if peaks else float("nan") + + +def check_tp(base): + return max(true_peak_db(base + ".ogg"), true_peak_db(base + ".m4a")) + + +def main(): + report = [] + problems = [] + + for fid, fn in ONE_SHOTS.items(): + wav_path = os.path.join(OUT, fid + ".wav") + base = os.path.join(DST, fid) + y = fade(norm(dc(fn()))) + sf.write(wav_path, stereo(y), sr) + encode(wav_path, base) + tp = check_tp(base) + if tp > ONE_SHOT_TP_CEIL: # MDCT 过冲超限:按超出量降档重编一次再验 + gain = 10 ** ((ONE_SHOT_TP_CEIL - tp) / 20) * 0.98 + sf.write(wav_path, stereo(y * gain), sr) + encode(wav_path, base) + tp2 = check_tp(base) + report.append((fid, len(y) / sr, tp, f"降 {20 * np.log10(gain):.1f}dB 重编 -> {tp2:.2f}")) + tp = tp2 + else: + report.append((fid, len(y) / sr, tp, "")) + if tp > ONE_SHOT_TP_CEIL: + problems.append(f"{fid}: 真峰值 {tp:.2f} dBFS 仍超 {ONE_SHOT_TP_CEIL}") + + print(f"{'id':12s} {'dur(s)':>7s} {'TP(dBFS)':>9s} note") + for fid, dur, tp, note in report: + print(f"{fid:12s} {dur:7.2f} {tp:9.2f} {note}") + if problems: + print("PROBLEMS:") + for p in problems: + print(" ", p) + return 1 + print(f"ALL OK: {len(ONE_SHOTS)} one-shot -> {DST}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/mureka_log.md b/tools/mureka_log.md index 5d2d1e95..577b5cb8 100644 --- a/tools/mureka_log.md +++ b/tools/mureka_log.md @@ -273,3 +273,33 @@ flac 原件 tools/mureka_out/teahouse-{menu,serve,brew}/。 acrossfade 无缝循环 → loudnorm I=-20/TP=-2/LRA=11(20-teahouse.md §8.1 三首 -20 LUFS; CLOSE 结算 0.5 音量由游戏层控制)→ ogg q3 + m4a 128k,成品均 90.00s。无节拍网格玩法, 未跑 bpm_precise。menu 曲首攻 429 限流一次,60s 退避重试成功;三条任务此后一次成功。 + +## 2026-09-14(批次 5:rhythm《节拍行者》鼓手模式无鼓伴奏) + +预算线:本轮 $0.18(3 任务 ×1 候选,无重提);总预算 $2.00。24 号文档 §9.2: +玩家即鼓手,BGM 必须无鼓(prompt 显式 no drums no percussion)。 + +| # | task id | 用途 | prompt 特征 | 状态 | 花费 | +|---|---|---|---|---|---| +| 43 | 160832071925761 | track90 无鼓(2 候选) | bright pop rock backing, bass+acoustic+light synth, steady 90 bpm, A minor, no drums | succeeded | $0.03 | +| 44 | 160832268861441 | track105 无鼓(2 候选) | driving energetic pop backing, punchy bass+muted guitar chops+synth, steady 105 bpm, A minor, no drums | succeeded | $0.03 | +| 45 | 160832453541890 | track120 无鼓(2 候选) | high energy synth rock backing, aggressive bass+arp synth+power chords, steady 120 bpm, A minor, no drums | succeeded | $0.03 | + +**本轮花费:$0.09。累计:$1.35。** + +选用与 BPM 实测(bpm_precise 梳状扫描,无鼓曲目 onset 弱、分数偏低属正常): + +- track90:候选 1 实测 **90.00 精确**(score 5.14 vs 候选 0 的 3.87), + 无需拉伸;成品实测 **89.99 BPM**(循环 85.33s = 32 小节)。 +- track105:候选 0 = 106.26 / 候选 1 = 105.55;选候选 1 以 atempo=0.99479 + 拉伸至 105(R3 track90 先例),成品实测 **105.00 BPM**(循环 73.14s = 32 小节)。 +- track120:候选 0 = 126.0 / 候选 1 实测 **120.00 精确**;选候选 1, + 成品实测 **120.01 BPM**(循环 72.00s = 36 小节)。 + +后处理(R3 rhythm 工艺,整数小节铁律——循环长度必须为 4 拍整数倍,否则 +BGM 小节边界与节拍网格漂移):atempo(仅 track105)→ 首个强 onset 起点 +(silenceremove -35dB)→ 整数小节裁剪 → 末 3s 与开头 acrossfade(tri) 无缝 +循环 → loudnorm I=-20/TP=-2/LRA=11 → 44.1kHz → ogg q3 + m4a 128k。 +旧含鼓 track90/105/120 直接覆盖(web/rhythm/audio/bgm/)。 +flac 原件存 tools/mureka_out/rhythm-drummer-track*/。首发并行提交遇 429 +一次(cipher 批次 3 同款限流),60s 退避串行后 3/3 成功无重试计费。 diff --git a/web/engine/gesture.js b/web/engine/gesture.js index 2a506dd8..426f9928 100644 --- a/web/engine/gesture.js +++ b/web/engine/gesture.js @@ -32,9 +32,14 @@ * * 用法: * import { attachGestures } from '../engine/gesture.js'; - * const detach = attachGestures(element, (g) => game.onGesture(g)); + * const detach = attachGestures(element, (g, info) => game.onGesture(g, info)); * // detach() 可解除绑定 * + * 第二参 info(24-rhythm 坐标透传,缺省行为逐字节不变): + * { x, y } 视口像素坐标——tap 类/长按 = 按下点,滑动 = 滑动起点(出手瞄准 + * 位置),键盘通道 = 虚拟指针坐标(元素中心)。需要触点位置的游戏(如 rhythm + * 分区打击)使用;单参回调完全不受影响。 + * * 基础震动反馈(haptics.js,不支持的平台自动 no-op): * 滑动识别成功→swipe,tap→confirm,doubletap→confirm。 * 游戏逻辑层的语义反馈(error/success/warning 等)由游戏自行调用 vibrate()。 @@ -161,6 +166,7 @@ export function attachGestures(element, onGesture, config = {}) { // 单指/单指针跟踪状态 let tracking = false; let startX = 0, startY = 0, startTime = 0; + let downX = 0, downY = 0; // 本次按下的坐标(onGesture 第二参 info 透传用) let moved = false; // 位移是否已超 tapSlop(超过则本次不可能是点按) // 长按状态 @@ -194,14 +200,14 @@ export function attachGestures(element, onGesture, config = {}) { if (!longPressActive) return; longPressActive = false; _dispatchLongpress('longpressend'); - onGesture('longpressend'); + onGesture('longpressend', { x: downX, y: downY }); } /** immediateTap:本次按下的序曲 tap 确定作废(转滑动/长按/触摸取消)时补发 tapcancel。 */ function cancelImmediateTap() { if (!tapPending) return; tapPending = false; - onGesture('tapcancel'); + onGesture('tapcancel', { x: downX, y: downY }); } function handleDown(x, y) { @@ -209,6 +215,8 @@ export function attachGestures(element, onGesture, config = {}) { moved = false; startX = x; startY = y; + downX = x; + downY = y; startTime = performance.now(); if (cfg.immediateTap) { // 即时模式:按下立即回调 tap,不等双击窗口关闭。 @@ -216,7 +224,7 @@ export function attachGestures(element, onGesture, config = {}) { // 这声 tap 已发出不可撤回,但引擎会补 tapcancel 让游戏层撤销判定。 tapPending = true; vibrate('confirm'); - onGesture('tap'); + onGesture('tap', { x: downX, y: downY }); } cancelLongPress(); longPressTimer = setTimeout(() => { @@ -227,7 +235,7 @@ export function attachGestures(element, onGesture, config = {}) { cancelImmediateTap(); longPressActive = true; _dispatchLongpress('longpress'); - onGesture('longpress'); + onGesture('longpress', { x: downX, y: downY }); }, cfg.longpressMs); } @@ -267,7 +275,7 @@ export function attachGestures(element, onGesture, config = {}) { if (Math.abs(dx) > Math.abs(dy)) g = dx > 0 ? 'right' : 'left'; else g = dy > 0 ? 'down' : 'up'; vibrate('swipe'); // 滑动识别成功 - onGesture(g); + onGesture(g, { x: startX, y: startY }); return; } @@ -281,7 +289,7 @@ export function attachGestures(element, onGesture, config = {}) { cancelPendingTap(); lastTapTime = 0; vibrate('confirm'); // 双击 - onGesture('doubletap'); + onGesture('doubletap', { x: downX, y: downY }); } else if (cfg.immediateTap) { lastTapTime = now; // tap 已在 handleDown 发出,这里只记双击窗起点 } else { @@ -292,7 +300,7 @@ export function attachGestures(element, onGesture, config = {}) { pendingTapTimer = null; lastTapTime = 0; vibrate('confirm'); // 单击 - onGesture('tap'); + onGesture('tap', { x: downX, y: downY }); }, cfg.doubleTapWindow); } return; @@ -395,7 +403,7 @@ export function attachGestures(element, onGesture, config = {}) { cancelPendingTap(); // 与触屏滑动一致:滑动作废等待中的单击 lastTapTime = 0; vibrate('swipe'); - onGesture(dir); // 即时,无位移/速度判定(零延迟) + onGesture(dir, keyCenter()); // 即时,无位移/速度判定(零延迟) return; } if (tracking) return; @@ -405,7 +413,7 @@ export function attachGestures(element, onGesture, config = {}) { cancelPendingTap(); lastTapTime = 0; vibrate('confirm'); - onGesture('doubletap'); + onGesture('doubletap', keyCenter()); return; } // 空格:虚拟指针按下,进同一状态机(起 450ms 长按定时器;immediateTap diff --git a/web/harness/sessions/rhythm.mjs b/web/harness/sessions/rhythm.mjs index b903882b..2793e672 100644 --- a/web/harness/sessions/rhythm.mjs +++ b/web/harness/sessions/rhythm.mjs @@ -1,22 +1,30 @@ /** - * 《节拍行者》新手会话剧本:首局完整走教学 T1-T3(第 2 句先整拍不出手—— - * round4 H5 归因真话律:听到的是「这拍没接」而不是「慢了半拍」;再连错一遍 - * 触发 round3 G2 的「降速 25% 重演 + gd_slow」,跟着慢速领奏做对后恢复正常) - * → 曲目一(90 BPM);曲中下滑呼出菜单逛一圈再恢复(round3 G3:恢复先听 - * gd_count「四拍之后,继续」,再跟 4 拍底鼓 count-in 进场;首个应答窗口前 - * 整整 1 小节有 gd_answer+turn 交接标记);正片第一乐句故意整句漏掉(连 2 Miss - * 触发向导 gd_miss「慢了半拍」——正片规则不变),之后跟着领奏把一曲打完到结算。 + * 《节拍行者》鼓手模式新手会话剧本:首局完整走「摸鼓认区 → 教学 T1-T3 → + * 曲目一(90 BPM)」全弧线——探索阶段把六区摸遍(区名每区只播一次)、曲中 + * 下滑逛菜单再恢复(gd_count + 4 拍底鼓 count-in 进场)、正片第一乐句故意 + * 整句漏掉(连 2 Miss → 向导 gd_miss)、之后逐音跟打(含终曲加花)到结算。 + * 分区敲击带坐标注入(引擎坐标透传契约,24 号文档 §0.3)。 */ +const ZC = { + TL: { x: 50, y: 100 }, TC: { x: 150, y: 100 }, TR: { x: 250, y: 100 }, + BL: { x: 50, y: 500 }, BC: { x: 150, y: 500 }, BR: { x: 250, y: 500 }, +}; + export function describe(game) { switch (game.phase) { - case 'intro': return '开场旁白在播,单击可以跳过'; - case 'tut': return game._cues.length - ? `教学:听领奏,下一小节照样打(${game._cues.map((c) => c.gesture).join('→')})${game._tutRate < 1 ? '(领奏已放慢 25%)' : ''}` - : '教学准备中,听鼓点'; + case 'intro': return '开场旁白在播(认识你的鼓),单击可以跳过'; + case 'tut': + if (game._tutStage === 'probe') { + return `摸鼓认区:随便摸,摸到 3 个不同的地方就开始(已摸 ${game._probeHeard.size} 区)`; + } + return game._cues.length + ? `教学:听领奏,下一小节照样敲(${game._cues.map((c) => c.zone).join('→')})${game._tutRate < 1 ? '(领奏已放慢 25%)' : ''}` + : '教学准备中,听数拍'; case 'select': return '选曲:左右滑换曲,单击开始,下滑回大厅'; case 'main': { const left = game._cues.filter((c) => !c.judged).length; - return `演奏中(${game._song?.title ?? ''}):还剩 ${left} 个应答音。领奏停了、轮到空拍,就该我出手了`; + const fillNext = game._timed.some((e) => e.fn && game._song?.units?.some((u) => u.fill)); + return `演奏中(${game._song?.title ?? ''}):还剩 ${left} 个应答音。领奏停了、轮到空拍,就该我出手了${fillNext ? ';有一段加花要连打' : ''}`; } case 'menu': return '暂停菜单:单击切选项,双击确认,上滑帮助,下滑恢复演奏'; case 'end': return '结算播报中:听完单击再来一遍,右滑下一首,下滑回大厅'; @@ -27,60 +35,61 @@ export function describe(game) { export async function session(ctx, mock) { const { game, audio } = ctx; const t = () => audio.ctx.currentTime; + // transcribe 的 root 是 {}:补上布局让分区坐标生效(引擎坐标契约的游戏层语义) + game.root = { getBoundingClientRect: () => ({ left: 0, top: 0, width: 300, height: 600 }) }; + const hit = async (zone) => { + game.onGesture('tap', ZC[zone]); + await mock.flush(); + }; - // 开场:welcome 听完进教学 - await ctx.waitCond(() => game.phase === 'tut'); - ctx.status('教学开始了,先听领奏'); + // 开场:welcome → tut_map → tut_probe 三连旁白,逐句跳过 + await ctx.waitCond(() => game.phase === 'intro' && ctx.voiceBusy()); + ctx.status('开场旁白:我是鼓手,整个屏幕都是鼓。单击跳过'); + for (let i = 0; i < 3; i++) { await ctx.gesture('tap'); await ctx.waitVirtual(600); } + await ctx.waitCond(() => game.phase === 'tut' && game._tutStage === 'probe'); + ctx.status('进入摸鼓认区:先四处摸摸,听到哪面鼓就报哪面鼓的名'); + ctx.note('背景音乐《启程》无鼓伴奏已在轻声循环——拍子网格的底,鼓由我来打'); - // 教学 3 步:等领奏排程好,在应答窗口内跟打 + // T0 探索:六区全摸一遍(区名每区一次;重复摸只出声),再摸到 3 区进阶 + for (const zone of ['TL', 'TC', 'TR', 'BL', 'BC', 'BR']) await hit(zone); + ctx.status('六面鼓摸遍了:左上踩镲、上中高嗵、右上吊镲、左下军鼓、下中底鼓、右下低嗵'); + await hit('TL'); // 重复摸:只出声不报名 + await hit('BR'); + ctx.status('又摸了两下重复的,只响不报名'); + await ctx.waitCond(() => game._tutStage === 'step' && game._cues.length > 0); + ctx.status('摸到三个不同的地方,自动开始教学第一步'); + + // 教学 3 步:等领奏排程好,在应答窗口内跟敲 //(注意:调度泵 interval 常驻,虚拟时钟永不"空闲"——禁止裸 waitIdle) for (let step = 0; step < 3; step++) { - await ctx.waitCond(() => game.phase === 'tut' && game._cues.length > 0 && game._tutAttempts.length === 0); - ctx.status(`教学第 ${step + 1}/3 句`); - if (step === 1) { - // round4 H5 剧本:这一句先整拍不出手——归因必须是「这拍没接」(gd_miss_none), - // 不是「慢了半拍」;再连错一遍(整句不出手)听它放慢一些再带着打(G2)。 - // 注意 _tutEvaluate 判定即把 _phaseEnd 置 Infinity 防重入——等待一律挂 - // 「持久里程碑」条件(_tutFails/_tutSlowUsed),别挂阶段末时刻。 - await ctx.waitCond(() => game._tutFails === 1); // 失败①:没出手 - ctx.status('第一遍整拍没出手——听到的是「这拍没接。我停下的时候,就轮到你了」,不是「慢了半拍」'); - await ctx.waitCond(() => game._phaseEnd < 1e8 && t() < game._phaseEnd - 1); // 纠正链播完、重新领奏 - ctx.status('又重新领奏了一遍,还是没敢出手'); - await ctx.waitCond(() => game._tutSlowUsed === true); // 失败② → 触发降速重演 - await ctx.waitCond(() => game._phaseEnd < 1e8 && t() < game._phaseEnd - 1); // gd_slow 播完、慢速领奏已排 - ctx.status('听到「我放慢一些,跟着试试」——领奏降速 25% 重演,这回跟着慢拍打'); - for (const cue of game._cues) { - const at = cue.expected - (cue.gesture === 'tap' ? -0.28 : 0.04); - await ctx.waitCond(() => t() >= at || cue.judged); - if (cue.judged) continue; - await ctx.gesture(cue.gesture); - } - await ctx.waitCond(() => game._tutIdx > step || game.phase !== 'tut'); - continue; - } + await ctx.waitCond(() => game._tutIdx === step && game.phase === 'tut' + && game._cues.length > 0 && game._tutAttempts.length === 0); + ctx.status(`教学第 ${step + 1}/3 步:听数拍和领奏,下一小节照着敲`); for (const cue of game._cues) { - const at = cue.expected - (cue.gesture === 'tap' ? -0.28 : 0.04); // tap 补偿引擎 ~300ms 延迟 - await ctx.waitCond(() => t() >= at || cue.judged); + await ctx.waitCond(() => t() >= cue.expected || cue.judged); if (cue.judged) continue; - await ctx.gesture(cue.gesture); + await hit(cue.zone); } await ctx.waitCond(() => game._tutIdx > step || game.phase !== 'tut'); } await ctx.waitCond(() => game.phase === 'main'); - ctx.status('教学完毕,曲目一《启程》90 拍开始。领奏响之前先听到「现在跟着我打」'); + ctx.status('教学完毕,曲目一《启程》90 拍开始。先听四下底鼓数拍,然后是「现在跟着我打」'); + ctx.note('《启程》无鼓伴奏 BGM 起,四下底鼓数拍对齐小节'); // 曲中逛菜单:下滑呼出(模态暂停)→ 听选项 → 下滑恢复 - //(round3 G3:恢复先播「四拍之后,继续」,再跟 4 拍底鼓 count-in 进场) + //(gd_count「四拍之后,继续」+ 4 拍底鼓 count-in 进场) await ctx.waitCond(() => t() >= game._bgmT0 + 1.5); await ctx.gesture('down'); await ctx.waitCond(() => game.phase === 'menu'); ctx.status('曲中暂停,进了菜单'); + ctx.note('BGM 在断点冻住——谱面、判定、音乐全部暂停'); + await ctx.waitVirtual(1100); // 菜单 1s 去抖独占期 await ctx.gesture('tap'); // 切换选项(听到「回大厅」) - await ctx.waitIdle(); ctx.note('还是想打完这首,下滑回去'); await ctx.gesture('down'); // 恢复 await ctx.waitCond(() => game.phase === 'main'); ctx.status('听到「四拍之后,继续」,跟着四下底鼓数拍进来了'); + ctx.note('BGM 从断点续播,节拍网格整体后移了一小节'); // 第一乐句故意整句不打:连 2 次 Miss → 向导 gd_miss const firstPhrase = game._cues.filter((c) => c.phrase === 0); @@ -88,16 +97,16 @@ export async function session(ctx, mock) { await ctx.waitCond(() => firstPhrase.every((c) => c.judged)); ctx.status('第一句全漏了,听到「慢了半拍,下一小节跟上我就行」'); - // 之后逐音跟打(tap 注入留出引擎 ~300ms 回调延迟) + // 之后逐音跟打(含终曲加花) for (const cue of game._cues) { if (cue.judged) continue; - const at = cue.expected - (cue.gesture === 'tap' ? -0.28 : 0.04); - await ctx.waitCond(() => t() >= at || cue.judged); + await ctx.waitCond(() => t() >= cue.expected || cue.judged); if (cue.judged) continue; - await ctx.gesture(cue.gesture); + await hit(cue.zone); } await ctx.waitCond(() => game.phase === 'end'); await ctx.waitIdle(); - ctx.status('曲子打完了,结算播报中'); + ctx.status('曲子打完了(加花收尾是一记吊镲),结算播报中'); + ctx.note('曲目 BGM 淡出,结算语音链播完会淡入大厅音乐'); } diff --git a/web/rhythm/README.md b/web/rhythm/README.md index 0d147a3c..c001156f 100644 --- a/web/rhythm/README.md +++ b/web/rhythm/README.md @@ -1,41 +1,45 @@ -# 节拍行者(RHYTHM) +# 节拍行者(RHYTHM)· 鼓手模式 -跟随领奏的节奏游戏,纯音频。系统奏一句,你在下一小节原样跟打—— -音要对,拍点也要对。建议戴耳机(有线优先,蓝牙延迟由 Good 窗口吸收)。 +屏幕分区打击乐节奏游戏,纯音频。整面屏幕是一面鼓:你在无鼓伴奏的乐队里 +当鼓手,系统(领奏)敲一小节,你在下一小节照着敲回来——鼓要选对,拍点也要对。 +建议戴耳机(有线优先,蓝牙延迟由 Good 窗口吸收)。 ## 玩法 -- 每个乐句 = **领奏 1 小节 + 应答 1 小节**:先听系统用打击乐器演奏 - 2-4 个音的节奏型,然后在下一小节的相同拍点用相同手势跟打。 -- 判定:**Perfect ±150ms**(乐器音 + 亮音 accent)/ **Good ±350ms**(只播乐器音)/ - **Miss**(手势错或错过拍点:该音符不出声 + 一声闷镲 + 震动提醒)。 - 判定时刻按真实触摸时刻算(引擎 immediateTap 按下即发 tap;滑动/双击按 - 识别延迟补偿回触摸时刻)。Miss 不中断不掉血,歌照唱完。 +- 每个乐句 = **领奏 1 小节 + 应答 1 小节**:先听系统用打击乐演奏 2-4 音的 + 节奏型(曲三 4-6 音),然后在下一小节的相同拍点敲相同的鼓。曲子开头有 + 1 小节底鼓数拍。 +- **加花**:曲中/曲尾有八分音符密集加花(领奏一串快鼓),跟着连打,最后 + 一记吊镲收束。 +- 判定:**Perfect ±150ms**(鼓音 + 亮音 accent)/ **Good ±350ms**(只播鼓音)/ + **Miss**(敲错鼓或错过拍点:闷镲 + 震动提醒)。点按按下即判(零识别延迟), + Miss 不中断不掉血,歌照唱完。 - 计分:Perfect=2 / Good=1 / Miss=0,评级 **S≥95% / A≥85% / B≥70% / C**。 - 连击每 16 次有震动鼓励;乐句全 Perfect 有额外震动。 - **3 首曲目解锁制**:《启程》90 BPM → 《追风》105 BPM(反拍)→ 《星爆》120 BPM (切分密集)。曲目 n 拿到 B 级以上解锁 n+1,解锁状态本地保存并同步大厅。 -## 手势 → 乐器音 +## 屏幕分区 → 鼓组(3 列 × 2 行) -| 手势 | 乐器音 | 声像 | -|---|---|---| -| 单击 | 底鼓 kick | 中央低频 | -| 双击 | 军鼓 snare | 中央偏上 | -| 左滑 | 闭合镲 hat | 极左 | -| 右滑 | 开放镲 hat | 极右 | -| 上滑 | 高通 tom | 升调感 | -| 下滑 | 低通 tom | 降调感 | -| 长按 | —(非演奏手势) | Miss 反馈音 + 震动提醒 | +| 区 | 位置 | 乐器 | 声像 | +|---|---|---|---| +| 踩镲 | 左上 | 闭合踩镲 | 左 | +| 高嗵 | 上中 | 高音嗵 | 中 | +| 吊镲 | 右上 | 吊镲(长尾) | 右 | +| 军鼓 | 左下 | 军鼓 | 左 | +| 底鼓 | 下中 | 底鼓(你的「家」) | 中 | +| 低嗵 | 右下 | 低音嗵 | 右 | -你的每次出手都会实时奏响对应乐器音——跟对了,就能听到自己在演奏。 +**横着三列 = 左中右声像;竖着两行 = 上清亮下沉闷。** 六件乐器音色各异, +声像 + 音高 + 音色三重线索,蒙眼可辨。键盘:数字 1-6 = 六区(1-3 上排, +4-6 下排),空格 = 下中底鼓。 ## 流程 -- **首次进入**:一句欢迎后进入 5 步交互教学(单击 → 左右滑 → 上滑 → 下滑 → - 三音带双击),做错有纠正语音(早了/晚了/不是这个手势),同一步失败 2 次 - 自动跳过;全程零通过时收尾播中性句(不夸"很好");教学中**下滑 = 跳过 - 教学**(教到下滑步、临近下滑音符时除外——那时它是演奏)。 +- **首次进入**:欢迎 → 摸鼓认区(随便摸,听到哪面鼓就告诉你它在哪,摸到 + 3 个不同的地方开始)→ 3 步跟打教学(底鼓家区 → 左右军鼓低嗵 → 上排 + 踩镲高嗵吊镲),做错有纠正语音(早了/晚了/敲错鼓),同一步失败 2 次会 + 放慢再教一遍,全程零惩罚;教学中**下滑 = 跳过教学**。 - **选曲**(二周目起):左右滑切曲目(播曲目名/未解锁提示),单击开始, 上滑听规则,下滑回大厅。 - **结算**:播报评级(含准确率档位)与连击纪录;**单击 = 再来一遍(同曲), @@ -43,18 +47,17 @@ ## 菜单 -演奏中**下滑呼出**(模态暂停:谱面、判定、BGM 全部冻结,BGM 断点续播)→ -**单击循环选项**(继续演奏/回大厅)→ **双击确认** → **上滑 = 帮助**(规则 + -设置引导)。菜单打开后 **1 秒去抖独占期**:期内不收任何手势(防演奏途中被 -弹菜单时,已在路上的手势直接穿透菜单误确认)。 -注意:下滑同时是"低通 tom"的演奏手势——**自下滑音符的领奏响起到判定 -死线之间**,下滑永远是演奏;音符刚被判定死线拖死的短窗内(识别延迟、手势 -还在路上)同样按演奏处理,绝不弹菜单;仅当没有悬置、临近或刚拖死的下滑 -音符时才呼出菜单。 +演奏中**下滑呼出**(模态暂停:谱面、判定、BGM 全部冻结,BGM 断点续播, +恢复前先听 4 拍数拍)→ **单击循环选项**(继续演奏/回大厅)→ **双击确认** +→ **上滑 = 帮助**(规则 + 设置引导)。菜单打开后 **1 秒去抖独占期**:期内 +不收任何手势(防已在路上的手势穿透菜单误确认)。 ## 运行 ```bash -python3 -m http.server 8105 --directory /home/ubuntu/SoundGame/web +python3 -m http.server 8105 --directory /home/xsl/AutoVideo/SoundGame/web # 浏览器打开 http://localhost:8105/rhythm/ ,建议戴耳机 ``` + +无头试玩:`tools/node/bin/node web/rhythm/test/playtest.mjs` +(断言清单 docs/design/24-rhythm-drummer.md §11)。 diff --git a/web/rhythm/audio/bgm/track105.m4a b/web/rhythm/audio/bgm/track105.m4a index 4a324da8..29d97a81 100644 Binary files a/web/rhythm/audio/bgm/track105.m4a and b/web/rhythm/audio/bgm/track105.m4a differ diff --git a/web/rhythm/audio/bgm/track105.ogg b/web/rhythm/audio/bgm/track105.ogg index 48b145e4..579581f3 100644 Binary files a/web/rhythm/audio/bgm/track105.ogg and b/web/rhythm/audio/bgm/track105.ogg differ diff --git a/web/rhythm/audio/bgm/track120.m4a b/web/rhythm/audio/bgm/track120.m4a index 8f316d1e..56cf68f4 100644 Binary files a/web/rhythm/audio/bgm/track120.m4a and b/web/rhythm/audio/bgm/track120.m4a differ diff --git a/web/rhythm/audio/bgm/track120.ogg b/web/rhythm/audio/bgm/track120.ogg index 26fead80..0a12ca6a 100644 Binary files a/web/rhythm/audio/bgm/track120.ogg and b/web/rhythm/audio/bgm/track120.ogg differ diff --git a/web/rhythm/audio/bgm/track90.m4a b/web/rhythm/audio/bgm/track90.m4a index c1de3be9..b6af2130 100644 Binary files a/web/rhythm/audio/bgm/track90.m4a and b/web/rhythm/audio/bgm/track90.m4a differ diff --git a/web/rhythm/audio/bgm/track90.ogg b/web/rhythm/audio/bgm/track90.ogg index c7eec001..d7f1bf80 100644 Binary files a/web/rhythm/audio/bgm/track90.ogg and b/web/rhythm/audio/bgm/track90.ogg differ diff --git a/web/rhythm/audio/sfx/dm_accent.m4a b/web/rhythm/audio/sfx/dm_accent.m4a new file mode 100644 index 00000000..182f5fd6 Binary files /dev/null and b/web/rhythm/audio/sfx/dm_accent.m4a differ diff --git a/web/rhythm/audio/sfx/inst_accent.ogg b/web/rhythm/audio/sfx/dm_accent.ogg similarity index 57% rename from web/rhythm/audio/sfx/inst_accent.ogg rename to web/rhythm/audio/sfx/dm_accent.ogg index 89b58452..792724e0 100644 Binary files a/web/rhythm/audio/sfx/inst_accent.ogg and b/web/rhythm/audio/sfx/dm_accent.ogg differ diff --git a/web/rhythm/audio/sfx/dm_crash.m4a b/web/rhythm/audio/sfx/dm_crash.m4a new file mode 100644 index 00000000..6f736109 Binary files /dev/null and b/web/rhythm/audio/sfx/dm_crash.m4a differ diff --git a/web/rhythm/audio/sfx/dm_crash.ogg b/web/rhythm/audio/sfx/dm_crash.ogg new file mode 100644 index 00000000..6cca1d8c Binary files /dev/null and b/web/rhythm/audio/sfx/dm_crash.ogg differ diff --git a/web/rhythm/audio/sfx/dm_hat.m4a b/web/rhythm/audio/sfx/dm_hat.m4a new file mode 100644 index 00000000..5325e35d Binary files /dev/null and b/web/rhythm/audio/sfx/dm_hat.m4a differ diff --git a/web/rhythm/audio/sfx/inst_hat_l.ogg b/web/rhythm/audio/sfx/dm_hat.ogg similarity index 57% rename from web/rhythm/audio/sfx/inst_hat_l.ogg rename to web/rhythm/audio/sfx/dm_hat.ogg index 19b32b9b..bc4b6a3e 100644 Binary files a/web/rhythm/audio/sfx/inst_hat_l.ogg and b/web/rhythm/audio/sfx/dm_hat.ogg differ diff --git a/web/rhythm/audio/sfx/dm_snare.m4a b/web/rhythm/audio/sfx/dm_snare.m4a new file mode 100644 index 00000000..00fc854f Binary files /dev/null and b/web/rhythm/audio/sfx/dm_snare.m4a differ diff --git a/web/rhythm/audio/sfx/dm_snare.ogg b/web/rhythm/audio/sfx/dm_snare.ogg new file mode 100644 index 00000000..706a2841 Binary files /dev/null and b/web/rhythm/audio/sfx/dm_snare.ogg differ diff --git a/web/rhythm/audio/sfx/dm_tomhi.m4a b/web/rhythm/audio/sfx/dm_tomhi.m4a new file mode 100644 index 00000000..73625332 Binary files /dev/null and b/web/rhythm/audio/sfx/dm_tomhi.m4a differ diff --git a/web/rhythm/audio/sfx/dm_tomhi.ogg b/web/rhythm/audio/sfx/dm_tomhi.ogg new file mode 100644 index 00000000..d071a41d Binary files /dev/null and b/web/rhythm/audio/sfx/dm_tomhi.ogg differ diff --git a/web/rhythm/audio/sfx/dm_tomlo.m4a b/web/rhythm/audio/sfx/dm_tomlo.m4a new file mode 100644 index 00000000..f6903ff7 Binary files /dev/null and b/web/rhythm/audio/sfx/dm_tomlo.m4a differ diff --git a/web/rhythm/audio/sfx/dm_tomlo.ogg b/web/rhythm/audio/sfx/dm_tomlo.ogg new file mode 100644 index 00000000..36e4aa22 Binary files /dev/null and b/web/rhythm/audio/sfx/dm_tomlo.ogg differ diff --git a/web/rhythm/audio/sfx/inst_accent.m4a b/web/rhythm/audio/sfx/inst_accent.m4a deleted file mode 100644 index 40e933fe..00000000 Binary files a/web/rhythm/audio/sfx/inst_accent.m4a and /dev/null differ diff --git a/web/rhythm/audio/sfx/inst_hat_l.m4a b/web/rhythm/audio/sfx/inst_hat_l.m4a deleted file mode 100644 index 55c17fce..00000000 Binary files a/web/rhythm/audio/sfx/inst_hat_l.m4a and /dev/null differ diff --git a/web/rhythm/audio/sfx/inst_hat_r.m4a b/web/rhythm/audio/sfx/inst_hat_r.m4a deleted file mode 100644 index 67edc8ba..00000000 Binary files a/web/rhythm/audio/sfx/inst_hat_r.m4a and /dev/null differ diff --git a/web/rhythm/audio/sfx/inst_hat_r.ogg b/web/rhythm/audio/sfx/inst_hat_r.ogg deleted file mode 100644 index 6c544c9e..00000000 Binary files a/web/rhythm/audio/sfx/inst_hat_r.ogg and /dev/null differ diff --git a/web/rhythm/audio/sfx/inst_kick.m4a b/web/rhythm/audio/sfx/inst_kick.m4a index 2b594dff..074e4706 100644 Binary files a/web/rhythm/audio/sfx/inst_kick.m4a and b/web/rhythm/audio/sfx/inst_kick.m4a differ diff --git a/web/rhythm/audio/sfx/inst_kick.ogg b/web/rhythm/audio/sfx/inst_kick.ogg index ffb35ba7..364f988b 100644 Binary files a/web/rhythm/audio/sfx/inst_kick.ogg and b/web/rhythm/audio/sfx/inst_kick.ogg differ diff --git a/web/rhythm/audio/sfx/inst_snare.m4a b/web/rhythm/audio/sfx/inst_snare.m4a deleted file mode 100644 index b25655a0..00000000 Binary files a/web/rhythm/audio/sfx/inst_snare.m4a and /dev/null differ diff --git a/web/rhythm/audio/sfx/inst_snare.ogg b/web/rhythm/audio/sfx/inst_snare.ogg deleted file mode 100644 index edede3f1..00000000 Binary files a/web/rhythm/audio/sfx/inst_snare.ogg and /dev/null differ diff --git a/web/rhythm/audio/sfx/inst_tom_hi.m4a b/web/rhythm/audio/sfx/inst_tom_hi.m4a deleted file mode 100644 index 3a5ab4bb..00000000 Binary files a/web/rhythm/audio/sfx/inst_tom_hi.m4a and /dev/null differ diff --git a/web/rhythm/audio/sfx/inst_tom_hi.ogg b/web/rhythm/audio/sfx/inst_tom_hi.ogg deleted file mode 100644 index e0e7c343..00000000 Binary files a/web/rhythm/audio/sfx/inst_tom_hi.ogg and /dev/null differ diff --git a/web/rhythm/audio/sfx/inst_tom_lo.m4a b/web/rhythm/audio/sfx/inst_tom_lo.m4a deleted file mode 100644 index 35c9d558..00000000 Binary files a/web/rhythm/audio/sfx/inst_tom_lo.m4a and /dev/null differ diff --git a/web/rhythm/audio/sfx/inst_tom_lo.ogg b/web/rhythm/audio/sfx/inst_tom_lo.ogg deleted file mode 100644 index ba807aa6..00000000 Binary files a/web/rhythm/audio/sfx/inst_tom_lo.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/female/grade_a.m4a b/web/rhythm/audio/voice/female/grade_a.m4a index 562547d8..ef03955f 100644 Binary files a/web/rhythm/audio/voice/female/grade_a.m4a and b/web/rhythm/audio/voice/female/grade_a.m4a differ diff --git a/web/rhythm/audio/voice/female/grade_a.ogg b/web/rhythm/audio/voice/female/grade_a.ogg index 16870f64..428830f1 100644 Binary files a/web/rhythm/audio/voice/female/grade_a.ogg and b/web/rhythm/audio/voice/female/grade_a.ogg differ diff --git a/web/rhythm/audio/voice/female/grade_b.m4a b/web/rhythm/audio/voice/female/grade_b.m4a index 27d29edf..4db2296e 100644 Binary files a/web/rhythm/audio/voice/female/grade_b.m4a and b/web/rhythm/audio/voice/female/grade_b.m4a differ diff --git a/web/rhythm/audio/voice/female/grade_b.ogg b/web/rhythm/audio/voice/female/grade_b.ogg index a3991f4c..65c8941f 100644 Binary files a/web/rhythm/audio/voice/female/grade_b.ogg and b/web/rhythm/audio/voice/female/grade_b.ogg differ diff --git a/web/rhythm/audio/voice/female/grade_c.m4a b/web/rhythm/audio/voice/female/grade_c.m4a index d99cefdf..bab56b2f 100644 Binary files a/web/rhythm/audio/voice/female/grade_c.m4a and b/web/rhythm/audio/voice/female/grade_c.m4a differ diff --git a/web/rhythm/audio/voice/female/grade_c.ogg b/web/rhythm/audio/voice/female/grade_c.ogg index b7e99563..90381126 100644 Binary files a/web/rhythm/audio/voice/female/grade_c.ogg and b/web/rhythm/audio/voice/female/grade_c.ogg differ diff --git a/web/rhythm/audio/voice/female/grade_s.m4a b/web/rhythm/audio/voice/female/grade_s.m4a index 92903ec6..943f5d6b 100644 Binary files a/web/rhythm/audio/voice/female/grade_s.m4a and b/web/rhythm/audio/voice/female/grade_s.m4a differ diff --git a/web/rhythm/audio/voice/female/grade_s.ogg b/web/rhythm/audio/voice/female/grade_s.ogg index 98481fc3..385d9669 100644 Binary files a/web/rhythm/audio/voice/female/grade_s.ogg and b/web/rhythm/audio/voice/female/grade_s.ogg differ diff --git a/web/rhythm/audio/voice/female/key/menu_main.ogg b/web/rhythm/audio/voice/female/key/menu_main.ogg index be1d78a4..dd651045 100644 Binary files a/web/rhythm/audio/voice/female/key/menu_main.ogg and b/web/rhythm/audio/voice/female/key/menu_main.ogg differ diff --git a/web/rhythm/audio/voice/female/key/restart.ogg b/web/rhythm/audio/voice/female/key/restart.ogg index 914a9eca..983c90cb 100644 Binary files a/web/rhythm/audio/voice/female/key/restart.ogg and b/web/rhythm/audio/voice/female/key/restart.ogg differ diff --git a/web/rhythm/audio/voice/female/key/select_hint.ogg b/web/rhythm/audio/voice/female/key/select_hint.ogg index 4ac0d1c4..9ad3ab98 100644 Binary files a/web/rhythm/audio/voice/female/key/select_hint.ogg and b/web/rhythm/audio/voice/female/key/select_hint.ogg differ diff --git a/web/rhythm/audio/voice/female/key/tut_1.m4a b/web/rhythm/audio/voice/female/key/tut_1.m4a deleted file mode 100644 index 3d039fa6..00000000 Binary files a/web/rhythm/audio/voice/female/key/tut_1.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/female/key/tut_1.ogg b/web/rhythm/audio/voice/female/key/tut_1.ogg deleted file mode 100644 index a0ed0b8c..00000000 Binary files a/web/rhythm/audio/voice/female/key/tut_1.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/female/key/tut_2.m4a b/web/rhythm/audio/voice/female/key/tut_2.m4a deleted file mode 100644 index 51d052e2..00000000 Binary files a/web/rhythm/audio/voice/female/key/tut_2.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/female/key/tut_2.ogg b/web/rhythm/audio/voice/female/key/tut_2.ogg deleted file mode 100644 index 2519a1fc..00000000 Binary files a/web/rhythm/audio/voice/female/key/tut_2.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/female/key/tut_3.m4a b/web/rhythm/audio/voice/female/key/tut_3.m4a deleted file mode 100644 index e7c623de..00000000 Binary files a/web/rhythm/audio/voice/female/key/tut_3.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/female/key/tut_3.ogg b/web/rhythm/audio/voice/female/key/tut_3.ogg deleted file mode 100644 index 7f001212..00000000 Binary files a/web/rhythm/audio/voice/female/key/tut_3.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/female/key/tut_map.m4a b/web/rhythm/audio/voice/female/key/tut_map.m4a new file mode 100644 index 00000000..d77922ac Binary files /dev/null and b/web/rhythm/audio/voice/female/key/tut_map.m4a differ diff --git a/web/rhythm/audio/voice/female/key/tut_map.ogg b/web/rhythm/audio/voice/female/key/tut_map.ogg new file mode 100644 index 00000000..97bdeb44 Binary files /dev/null and b/web/rhythm/audio/voice/female/key/tut_map.ogg differ diff --git a/web/rhythm/audio/voice/female/key/tut_probe.m4a b/web/rhythm/audio/voice/female/key/tut_probe.m4a new file mode 100644 index 00000000..fbfa934d Binary files /dev/null and b/web/rhythm/audio/voice/female/key/tut_probe.m4a differ diff --git a/web/rhythm/audio/voice/female/key/tut_probe.ogg b/web/rhythm/audio/voice/female/key/tut_probe.ogg new file mode 100644 index 00000000..a48a18cf Binary files /dev/null and b/web/rhythm/audio/voice/female/key/tut_probe.ogg differ diff --git a/web/rhythm/audio/voice/female/key/tut_z_kick.m4a b/web/rhythm/audio/voice/female/key/tut_z_kick.m4a new file mode 100644 index 00000000..6fb1e5c0 Binary files /dev/null and b/web/rhythm/audio/voice/female/key/tut_z_kick.m4a differ diff --git a/web/rhythm/audio/voice/female/key/tut_z_kick.ogg b/web/rhythm/audio/voice/female/key/tut_z_kick.ogg new file mode 100644 index 00000000..efa049fa Binary files /dev/null and b/web/rhythm/audio/voice/female/key/tut_z_kick.ogg differ diff --git a/web/rhythm/audio/voice/female/key/tut_z_patt.m4a b/web/rhythm/audio/voice/female/key/tut_z_patt.m4a new file mode 100644 index 00000000..2677aaaf Binary files /dev/null and b/web/rhythm/audio/voice/female/key/tut_z_patt.m4a differ diff --git a/web/rhythm/audio/voice/female/key/tut_z_patt.ogg b/web/rhythm/audio/voice/female/key/tut_z_patt.ogg new file mode 100644 index 00000000..e6faecc1 Binary files /dev/null and b/web/rhythm/audio/voice/female/key/tut_z_patt.ogg differ diff --git a/web/rhythm/audio/voice/female/key/tut_z_sides.m4a b/web/rhythm/audio/voice/female/key/tut_z_sides.m4a new file mode 100644 index 00000000..41eaf7f1 Binary files /dev/null and b/web/rhythm/audio/voice/female/key/tut_z_sides.m4a differ diff --git a/web/rhythm/audio/voice/female/key/tut_z_sides.ogg b/web/rhythm/audio/voice/female/key/tut_z_sides.ogg new file mode 100644 index 00000000..1f7af66c Binary files /dev/null and b/web/rhythm/audio/voice/female/key/tut_z_sides.ogg differ diff --git a/web/rhythm/audio/voice/female/menu_lobby.m4a b/web/rhythm/audio/voice/female/menu_lobby.m4a index 05373180..c2263f95 100644 Binary files a/web/rhythm/audio/voice/female/menu_lobby.m4a and b/web/rhythm/audio/voice/female/menu_lobby.m4a differ diff --git a/web/rhythm/audio/voice/female/menu_lobby.ogg b/web/rhythm/audio/voice/female/menu_lobby.ogg index e417f21e..5be66676 100644 Binary files a/web/rhythm/audio/voice/female/menu_lobby.ogg and b/web/rhythm/audio/voice/female/menu_lobby.ogg differ diff --git a/web/rhythm/audio/voice/female/menu_main.m4a b/web/rhythm/audio/voice/female/menu_main.m4a index 0412c514..e9b06bba 100644 Binary files a/web/rhythm/audio/voice/female/menu_main.m4a and b/web/rhythm/audio/voice/female/menu_main.m4a differ diff --git a/web/rhythm/audio/voice/female/menu_main.ogg b/web/rhythm/audio/voice/female/menu_main.ogg index a8a6dcc8..3c93f6a1 100644 Binary files a/web/rhythm/audio/voice/female/menu_main.ogg and b/web/rhythm/audio/voice/female/menu_main.ogg differ diff --git a/web/rhythm/audio/voice/female/menu_resume.m4a b/web/rhythm/audio/voice/female/menu_resume.m4a index f5f3f2ca..24ce558b 100644 Binary files a/web/rhythm/audio/voice/female/menu_resume.m4a and b/web/rhythm/audio/voice/female/menu_resume.m4a differ diff --git a/web/rhythm/audio/voice/female/menu_resume.ogg b/web/rhythm/audio/voice/female/menu_resume.ogg index 89d9e484..f65d894a 100644 Binary files a/web/rhythm/audio/voice/female/menu_resume.ogg and b/web/rhythm/audio/voice/female/menu_resume.ogg differ diff --git a/web/rhythm/audio/voice/female/mid_song.m4a b/web/rhythm/audio/voice/female/mid_song.m4a index 09047e0d..4a3fa8bf 100644 Binary files a/web/rhythm/audio/voice/female/mid_song.m4a and b/web/rhythm/audio/voice/female/mid_song.m4a differ diff --git a/web/rhythm/audio/voice/female/mid_song.ogg b/web/rhythm/audio/voice/female/mid_song.ogg index 788c843b..984b3b8f 100644 Binary files a/web/rhythm/audio/voice/female/mid_song.ogg and b/web/rhythm/audio/voice/female/mid_song.ogg differ diff --git a/web/rhythm/audio/voice/female/new_best_combo.m4a b/web/rhythm/audio/voice/female/new_best_combo.m4a index deed9bc4..089a3e81 100644 Binary files a/web/rhythm/audio/voice/female/new_best_combo.m4a and b/web/rhythm/audio/voice/female/new_best_combo.m4a differ diff --git a/web/rhythm/audio/voice/female/new_best_combo.ogg b/web/rhythm/audio/voice/female/new_best_combo.ogg index bf378a82..087ef8d0 100644 Binary files a/web/rhythm/audio/voice/female/new_best_combo.ogg and b/web/rhythm/audio/voice/female/new_best_combo.ogg differ diff --git a/web/rhythm/audio/voice/female/restart.m4a b/web/rhythm/audio/voice/female/restart.m4a index da75fcfd..69e66449 100644 Binary files a/web/rhythm/audio/voice/female/restart.m4a and b/web/rhythm/audio/voice/female/restart.m4a differ diff --git a/web/rhythm/audio/voice/female/restart.ogg b/web/rhythm/audio/voice/female/restart.ogg index 71edbf0f..732e0e8d 100644 Binary files a/web/rhythm/audio/voice/female/restart.ogg and b/web/rhythm/audio/voice/female/restart.ogg differ diff --git a/web/rhythm/audio/voice/female/retry_early.m4a b/web/rhythm/audio/voice/female/retry_early.m4a index b0be3107..cc8ad175 100644 Binary files a/web/rhythm/audio/voice/female/retry_early.m4a and b/web/rhythm/audio/voice/female/retry_early.m4a differ diff --git a/web/rhythm/audio/voice/female/retry_early.ogg b/web/rhythm/audio/voice/female/retry_early.ogg index 18e11d75..603e3f77 100644 Binary files a/web/rhythm/audio/voice/female/retry_early.ogg and b/web/rhythm/audio/voice/female/retry_early.ogg differ diff --git a/web/rhythm/audio/voice/female/retry_gesture.m4a b/web/rhythm/audio/voice/female/retry_gesture.m4a deleted file mode 100644 index a5dbe86d..00000000 Binary files a/web/rhythm/audio/voice/female/retry_gesture.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/female/retry_gesture.ogg b/web/rhythm/audio/voice/female/retry_gesture.ogg deleted file mode 100644 index 0a20dd98..00000000 Binary files a/web/rhythm/audio/voice/female/retry_gesture.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/female/retry_late.m4a b/web/rhythm/audio/voice/female/retry_late.m4a index 3dd42840..9fef559b 100644 Binary files a/web/rhythm/audio/voice/female/retry_late.m4a and b/web/rhythm/audio/voice/female/retry_late.m4a differ diff --git a/web/rhythm/audio/voice/female/retry_late.ogg b/web/rhythm/audio/voice/female/retry_late.ogg index fb3cb96e..2c0ede94 100644 Binary files a/web/rhythm/audio/voice/female/retry_late.ogg and b/web/rhythm/audio/voice/female/retry_late.ogg differ diff --git a/web/rhythm/audio/voice/female/retry_listen.m4a b/web/rhythm/audio/voice/female/retry_listen.m4a index 3a9fcac0..78ce5081 100644 Binary files a/web/rhythm/audio/voice/female/retry_listen.m4a and b/web/rhythm/audio/voice/female/retry_listen.m4a differ diff --git a/web/rhythm/audio/voice/female/retry_listen.ogg b/web/rhythm/audio/voice/female/retry_listen.ogg index 02143167..6d6ab4a2 100644 Binary files a/web/rhythm/audio/voice/female/retry_listen.ogg and b/web/rhythm/audio/voice/female/retry_listen.ogg differ diff --git a/web/rhythm/audio/voice/female/retry_skip.m4a b/web/rhythm/audio/voice/female/retry_skip.m4a index 7b3ddd8e..2e1e8d61 100644 Binary files a/web/rhythm/audio/voice/female/retry_skip.m4a and b/web/rhythm/audio/voice/female/retry_skip.m4a differ diff --git a/web/rhythm/audio/voice/female/retry_skip.ogg b/web/rhythm/audio/voice/female/retry_skip.ogg index 8b846e56..8c9f0b6b 100644 Binary files a/web/rhythm/audio/voice/female/retry_skip.ogg and b/web/rhythm/audio/voice/female/retry_skip.ogg differ diff --git a/web/rhythm/audio/voice/female/retry_wrong.m4a b/web/rhythm/audio/voice/female/retry_wrong.m4a new file mode 100644 index 00000000..30b678cf Binary files /dev/null and b/web/rhythm/audio/voice/female/retry_wrong.m4a differ diff --git a/web/rhythm/audio/voice/female/retry_wrong.ogg b/web/rhythm/audio/voice/female/retry_wrong.ogg new file mode 100644 index 00000000..41a6776c Binary files /dev/null and b/web/rhythm/audio/voice/female/retry_wrong.ogg differ diff --git a/web/rhythm/audio/voice/female/rules.m4a b/web/rhythm/audio/voice/female/rules.m4a index 5b643289..3fa2a29e 100644 Binary files a/web/rhythm/audio/voice/female/rules.m4a and b/web/rhythm/audio/voice/female/rules.m4a differ diff --git a/web/rhythm/audio/voice/female/rules.ogg b/web/rhythm/audio/voice/female/rules.ogg index e2c9fd9a..93deea87 100644 Binary files a/web/rhythm/audio/voice/female/rules.ogg and b/web/rhythm/audio/voice/female/rules.ogg differ diff --git a/web/rhythm/audio/voice/female/select_hint.m4a b/web/rhythm/audio/voice/female/select_hint.m4a index 7cf8a808..d09d1128 100644 Binary files a/web/rhythm/audio/voice/female/select_hint.m4a and b/web/rhythm/audio/voice/female/select_hint.m4a differ diff --git a/web/rhythm/audio/voice/female/select_hint.ogg b/web/rhythm/audio/voice/female/select_hint.ogg index ebd7808b..17a4524b 100644 Binary files a/web/rhythm/audio/voice/female/select_hint.ogg and b/web/rhythm/audio/voice/female/select_hint.ogg differ diff --git a/web/rhythm/audio/voice/female/settings_hint.m4a b/web/rhythm/audio/voice/female/settings_hint.m4a index af2c900d..fa1a0c4b 100644 Binary files a/web/rhythm/audio/voice/female/settings_hint.m4a and b/web/rhythm/audio/voice/female/settings_hint.m4a differ diff --git a/web/rhythm/audio/voice/female/settings_hint.ogg b/web/rhythm/audio/voice/female/settings_hint.ogg index 638cd593..103ef1f3 100644 Binary files a/web/rhythm/audio/voice/female/settings_hint.ogg and b/web/rhythm/audio/voice/female/settings_hint.ogg differ diff --git a/web/rhythm/audio/voice/female/skip_hint.m4a b/web/rhythm/audio/voice/female/skip_hint.m4a index 38c2ee4b..773040f4 100644 Binary files a/web/rhythm/audio/voice/female/skip_hint.m4a and b/web/rhythm/audio/voice/female/skip_hint.m4a differ diff --git a/web/rhythm/audio/voice/female/skip_hint.ogg b/web/rhythm/audio/voice/female/skip_hint.ogg index 10319632..2ea5cf5a 100644 Binary files a/web/rhythm/audio/voice/female/skip_hint.ogg and b/web/rhythm/audio/voice/female/skip_hint.ogg differ diff --git a/web/rhythm/audio/voice/female/song1_name.m4a b/web/rhythm/audio/voice/female/song1_name.m4a index 97f33fe3..87a37813 100644 Binary files a/web/rhythm/audio/voice/female/song1_name.m4a and b/web/rhythm/audio/voice/female/song1_name.m4a differ diff --git a/web/rhythm/audio/voice/female/song1_name.ogg b/web/rhythm/audio/voice/female/song1_name.ogg index 69650cbb..428ca1bc 100644 Binary files a/web/rhythm/audio/voice/female/song1_name.ogg and b/web/rhythm/audio/voice/female/song1_name.ogg differ diff --git a/web/rhythm/audio/voice/female/song2_name.m4a b/web/rhythm/audio/voice/female/song2_name.m4a index f53f2ef2..c474e6fb 100644 Binary files a/web/rhythm/audio/voice/female/song2_name.m4a and b/web/rhythm/audio/voice/female/song2_name.m4a differ diff --git a/web/rhythm/audio/voice/female/song2_name.ogg b/web/rhythm/audio/voice/female/song2_name.ogg index 4c04350f..a0cbca72 100644 Binary files a/web/rhythm/audio/voice/female/song2_name.ogg and b/web/rhythm/audio/voice/female/song2_name.ogg differ diff --git a/web/rhythm/audio/voice/female/song3_name.m4a b/web/rhythm/audio/voice/female/song3_name.m4a index bdfeeb69..5434cfe2 100644 Binary files a/web/rhythm/audio/voice/female/song3_name.m4a and b/web/rhythm/audio/voice/female/song3_name.m4a differ diff --git a/web/rhythm/audio/voice/female/song3_name.ogg b/web/rhythm/audio/voice/female/song3_name.ogg index 07ab57f0..ef5d0322 100644 Binary files a/web/rhythm/audio/voice/female/song3_name.ogg and b/web/rhythm/audio/voice/female/song3_name.ogg differ diff --git a/web/rhythm/audio/voice/female/song_locked.m4a b/web/rhythm/audio/voice/female/song_locked.m4a index 087df673..a6d507a9 100644 Binary files a/web/rhythm/audio/voice/female/song_locked.m4a and b/web/rhythm/audio/voice/female/song_locked.m4a differ diff --git a/web/rhythm/audio/voice/female/song_locked.ogg b/web/rhythm/audio/voice/female/song_locked.ogg index 1b958238..aa5e586e 100644 Binary files a/web/rhythm/audio/voice/female/song_locked.ogg and b/web/rhythm/audio/voice/female/song_locked.ogg differ diff --git a/web/rhythm/audio/voice/female/tut_1.m4a b/web/rhythm/audio/voice/female/tut_1.m4a deleted file mode 100644 index 5d0d5307..00000000 Binary files a/web/rhythm/audio/voice/female/tut_1.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/female/tut_1.ogg b/web/rhythm/audio/voice/female/tut_1.ogg deleted file mode 100644 index 2ccda92f..00000000 Binary files a/web/rhythm/audio/voice/female/tut_1.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/female/tut_2.m4a b/web/rhythm/audio/voice/female/tut_2.m4a deleted file mode 100644 index a2177f03..00000000 Binary files a/web/rhythm/audio/voice/female/tut_2.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/female/tut_2.ogg b/web/rhythm/audio/voice/female/tut_2.ogg deleted file mode 100644 index 5ea6d50d..00000000 Binary files a/web/rhythm/audio/voice/female/tut_2.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/female/tut_3.m4a b/web/rhythm/audio/voice/female/tut_3.m4a deleted file mode 100644 index 229a551c..00000000 Binary files a/web/rhythm/audio/voice/female/tut_3.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/female/tut_3.ogg b/web/rhythm/audio/voice/female/tut_3.ogg deleted file mode 100644 index 33942add..00000000 Binary files a/web/rhythm/audio/voice/female/tut_3.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/female/tut_done.m4a b/web/rhythm/audio/voice/female/tut_done.m4a index 431e51d0..0bac25c1 100644 Binary files a/web/rhythm/audio/voice/female/tut_done.m4a and b/web/rhythm/audio/voice/female/tut_done.m4a differ diff --git a/web/rhythm/audio/voice/female/tut_done.ogg b/web/rhythm/audio/voice/female/tut_done.ogg index 9314d5ea..38a2d87d 100644 Binary files a/web/rhythm/audio/voice/female/tut_done.ogg and b/web/rhythm/audio/voice/female/tut_done.ogg differ diff --git a/web/rhythm/audio/voice/female/tut_fill.m4a b/web/rhythm/audio/voice/female/tut_fill.m4a new file mode 100644 index 00000000..21df0244 Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_fill.m4a differ diff --git a/web/rhythm/audio/voice/female/tut_fill.ogg b/web/rhythm/audio/voice/female/tut_fill.ogg new file mode 100644 index 00000000..81f671a7 Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_fill.ogg differ diff --git a/web/rhythm/audio/voice/female/tut_map.m4a b/web/rhythm/audio/voice/female/tut_map.m4a new file mode 100644 index 00000000..afb54335 Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_map.m4a differ diff --git a/web/rhythm/audio/voice/female/tut_map.ogg b/web/rhythm/audio/voice/female/tut_map.ogg new file mode 100644 index 00000000..e10a3167 Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_map.ogg differ diff --git a/web/rhythm/audio/voice/female/tut_probe.m4a b/web/rhythm/audio/voice/female/tut_probe.m4a new file mode 100644 index 00000000..3613ecbe Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_probe.m4a differ diff --git a/web/rhythm/audio/voice/female/tut_probe.ogg b/web/rhythm/audio/voice/female/tut_probe.ogg new file mode 100644 index 00000000..6b9db172 Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_probe.ogg differ diff --git a/web/rhythm/audio/voice/female/tut_skip.m4a b/web/rhythm/audio/voice/female/tut_skip.m4a index 7403ec3f..e18dd576 100644 Binary files a/web/rhythm/audio/voice/female/tut_skip.m4a and b/web/rhythm/audio/voice/female/tut_skip.m4a differ diff --git a/web/rhythm/audio/voice/female/tut_skip.ogg b/web/rhythm/audio/voice/female/tut_skip.ogg index f8ba89d5..2da8ec65 100644 Binary files a/web/rhythm/audio/voice/female/tut_skip.ogg and b/web/rhythm/audio/voice/female/tut_skip.ogg differ diff --git a/web/rhythm/audio/voice/female/tut_z_kick.m4a b/web/rhythm/audio/voice/female/tut_z_kick.m4a new file mode 100644 index 00000000..dd60bb80 Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_z_kick.m4a differ diff --git a/web/rhythm/audio/voice/female/tut_z_kick.ogg b/web/rhythm/audio/voice/female/tut_z_kick.ogg new file mode 100644 index 00000000..3e22a143 Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_z_kick.ogg differ diff --git a/web/rhythm/audio/voice/female/tut_z_patt.m4a b/web/rhythm/audio/voice/female/tut_z_patt.m4a new file mode 100644 index 00000000..6161fe04 Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_z_patt.m4a differ diff --git a/web/rhythm/audio/voice/female/tut_z_patt.ogg b/web/rhythm/audio/voice/female/tut_z_patt.ogg new file mode 100644 index 00000000..9572aa67 Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_z_patt.ogg differ diff --git a/web/rhythm/audio/voice/female/tut_z_sides.m4a b/web/rhythm/audio/voice/female/tut_z_sides.m4a new file mode 100644 index 00000000..abc523dc Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_z_sides.m4a differ diff --git a/web/rhythm/audio/voice/female/tut_z_sides.ogg b/web/rhythm/audio/voice/female/tut_z_sides.ogg new file mode 100644 index 00000000..e30cf65a Binary files /dev/null and b/web/rhythm/audio/voice/female/tut_z_sides.ogg differ diff --git a/web/rhythm/audio/voice/female/unlock_song2.m4a b/web/rhythm/audio/voice/female/unlock_song2.m4a index 685ba5ce..a37fba30 100644 Binary files a/web/rhythm/audio/voice/female/unlock_song2.m4a and b/web/rhythm/audio/voice/female/unlock_song2.m4a differ diff --git a/web/rhythm/audio/voice/female/unlock_song2.ogg b/web/rhythm/audio/voice/female/unlock_song2.ogg index c87ff460..919487ab 100644 Binary files a/web/rhythm/audio/voice/female/unlock_song2.ogg and b/web/rhythm/audio/voice/female/unlock_song2.ogg differ diff --git a/web/rhythm/audio/voice/female/unlock_song3.m4a b/web/rhythm/audio/voice/female/unlock_song3.m4a index 46f42ed4..58eb5be6 100644 Binary files a/web/rhythm/audio/voice/female/unlock_song3.m4a and b/web/rhythm/audio/voice/female/unlock_song3.m4a differ diff --git a/web/rhythm/audio/voice/female/unlock_song3.ogg b/web/rhythm/audio/voice/female/unlock_song3.ogg index c210cb71..ad71cf50 100644 Binary files a/web/rhythm/audio/voice/female/unlock_song3.ogg and b/web/rhythm/audio/voice/female/unlock_song3.ogg differ diff --git a/web/rhythm/audio/voice/female/welcome.m4a b/web/rhythm/audio/voice/female/welcome.m4a index c4cacbe1..ef826a4b 100644 Binary files a/web/rhythm/audio/voice/female/welcome.m4a and b/web/rhythm/audio/voice/female/welcome.m4a differ diff --git a/web/rhythm/audio/voice/female/welcome.ogg b/web/rhythm/audio/voice/female/welcome.ogg index 274c0c24..1cc87fcb 100644 Binary files a/web/rhythm/audio/voice/female/welcome.ogg and b/web/rhythm/audio/voice/female/welcome.ogg differ diff --git a/web/rhythm/audio/voice/female/welcome_back.m4a b/web/rhythm/audio/voice/female/welcome_back.m4a index 52c0281b..3de2948d 100644 Binary files a/web/rhythm/audio/voice/female/welcome_back.m4a and b/web/rhythm/audio/voice/female/welcome_back.m4a differ diff --git a/web/rhythm/audio/voice/female/welcome_back.ogg b/web/rhythm/audio/voice/female/welcome_back.ogg index 3b7f009c..d8d845de 100644 Binary files a/web/rhythm/audio/voice/female/welcome_back.ogg and b/web/rhythm/audio/voice/female/welcome_back.ogg differ diff --git a/web/rhythm/audio/voice/female/zone_bc.m4a b/web/rhythm/audio/voice/female/zone_bc.m4a new file mode 100644 index 00000000..b0287723 Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_bc.m4a differ diff --git a/web/rhythm/audio/voice/female/zone_bc.ogg b/web/rhythm/audio/voice/female/zone_bc.ogg new file mode 100644 index 00000000..6af4fdb7 Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_bc.ogg differ diff --git a/web/rhythm/audio/voice/female/zone_bl.m4a b/web/rhythm/audio/voice/female/zone_bl.m4a new file mode 100644 index 00000000..554ab688 Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_bl.m4a differ diff --git a/web/rhythm/audio/voice/female/zone_bl.ogg b/web/rhythm/audio/voice/female/zone_bl.ogg new file mode 100644 index 00000000..8f9457ef Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_bl.ogg differ diff --git a/web/rhythm/audio/voice/female/zone_br.m4a b/web/rhythm/audio/voice/female/zone_br.m4a new file mode 100644 index 00000000..110900b0 Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_br.m4a differ diff --git a/web/rhythm/audio/voice/female/zone_br.ogg b/web/rhythm/audio/voice/female/zone_br.ogg new file mode 100644 index 00000000..81b01239 Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_br.ogg differ diff --git a/web/rhythm/audio/voice/female/zone_tc.m4a b/web/rhythm/audio/voice/female/zone_tc.m4a new file mode 100644 index 00000000..037e55ba Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_tc.m4a differ diff --git a/web/rhythm/audio/voice/female/zone_tc.ogg b/web/rhythm/audio/voice/female/zone_tc.ogg new file mode 100644 index 00000000..c6f3952d Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_tc.ogg differ diff --git a/web/rhythm/audio/voice/female/zone_tl.m4a b/web/rhythm/audio/voice/female/zone_tl.m4a new file mode 100644 index 00000000..9ebfeebd Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_tl.m4a differ diff --git a/web/rhythm/audio/voice/female/zone_tl.ogg b/web/rhythm/audio/voice/female/zone_tl.ogg new file mode 100644 index 00000000..352a26b2 Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_tl.ogg differ diff --git a/web/rhythm/audio/voice/female/zone_tr.m4a b/web/rhythm/audio/voice/female/zone_tr.m4a new file mode 100644 index 00000000..017e7486 Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_tr.m4a differ diff --git a/web/rhythm/audio/voice/female/zone_tr.ogg b/web/rhythm/audio/voice/female/zone_tr.ogg new file mode 100644 index 00000000..a7012050 Binary files /dev/null and b/web/rhythm/audio/voice/female/zone_tr.ogg differ diff --git a/web/rhythm/audio/voice/male/grade_a.m4a b/web/rhythm/audio/voice/male/grade_a.m4a index f20eb171..4f04a57f 100644 Binary files a/web/rhythm/audio/voice/male/grade_a.m4a and b/web/rhythm/audio/voice/male/grade_a.m4a differ diff --git a/web/rhythm/audio/voice/male/grade_a.ogg b/web/rhythm/audio/voice/male/grade_a.ogg index 755e7990..620b8e6d 100644 Binary files a/web/rhythm/audio/voice/male/grade_a.ogg and b/web/rhythm/audio/voice/male/grade_a.ogg differ diff --git a/web/rhythm/audio/voice/male/grade_b.m4a b/web/rhythm/audio/voice/male/grade_b.m4a index 6b532864..8847d0e2 100644 Binary files a/web/rhythm/audio/voice/male/grade_b.m4a and b/web/rhythm/audio/voice/male/grade_b.m4a differ diff --git a/web/rhythm/audio/voice/male/grade_b.ogg b/web/rhythm/audio/voice/male/grade_b.ogg index 1f51a6cf..24a32a20 100644 Binary files a/web/rhythm/audio/voice/male/grade_b.ogg and b/web/rhythm/audio/voice/male/grade_b.ogg differ diff --git a/web/rhythm/audio/voice/male/grade_c.m4a b/web/rhythm/audio/voice/male/grade_c.m4a index cbfe7821..2eda4341 100644 Binary files a/web/rhythm/audio/voice/male/grade_c.m4a and b/web/rhythm/audio/voice/male/grade_c.m4a differ diff --git a/web/rhythm/audio/voice/male/grade_c.ogg b/web/rhythm/audio/voice/male/grade_c.ogg index 6358f27d..5f6655f2 100644 Binary files a/web/rhythm/audio/voice/male/grade_c.ogg and b/web/rhythm/audio/voice/male/grade_c.ogg differ diff --git a/web/rhythm/audio/voice/male/grade_s.m4a b/web/rhythm/audio/voice/male/grade_s.m4a index d394c01f..bcbd3e9b 100644 Binary files a/web/rhythm/audio/voice/male/grade_s.m4a and b/web/rhythm/audio/voice/male/grade_s.m4a differ diff --git a/web/rhythm/audio/voice/male/grade_s.ogg b/web/rhythm/audio/voice/male/grade_s.ogg index 87c71f27..6c3281e7 100644 Binary files a/web/rhythm/audio/voice/male/grade_s.ogg and b/web/rhythm/audio/voice/male/grade_s.ogg differ diff --git a/web/rhythm/audio/voice/male/key/menu_main.ogg b/web/rhythm/audio/voice/male/key/menu_main.ogg index 34e89794..e643e185 100644 Binary files a/web/rhythm/audio/voice/male/key/menu_main.ogg and b/web/rhythm/audio/voice/male/key/menu_main.ogg differ diff --git a/web/rhythm/audio/voice/male/key/restart.ogg b/web/rhythm/audio/voice/male/key/restart.ogg index f6fd5f1f..8133e192 100644 Binary files a/web/rhythm/audio/voice/male/key/restart.ogg and b/web/rhythm/audio/voice/male/key/restart.ogg differ diff --git a/web/rhythm/audio/voice/male/key/select_hint.ogg b/web/rhythm/audio/voice/male/key/select_hint.ogg index 9825413e..5603edf4 100644 Binary files a/web/rhythm/audio/voice/male/key/select_hint.ogg and b/web/rhythm/audio/voice/male/key/select_hint.ogg differ diff --git a/web/rhythm/audio/voice/male/key/tut_1.m4a b/web/rhythm/audio/voice/male/key/tut_1.m4a deleted file mode 100644 index 3387cb2c..00000000 Binary files a/web/rhythm/audio/voice/male/key/tut_1.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/male/key/tut_1.ogg b/web/rhythm/audio/voice/male/key/tut_1.ogg deleted file mode 100644 index 54373e4f..00000000 Binary files a/web/rhythm/audio/voice/male/key/tut_1.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/male/key/tut_2.m4a b/web/rhythm/audio/voice/male/key/tut_2.m4a deleted file mode 100644 index e9ca059c..00000000 Binary files a/web/rhythm/audio/voice/male/key/tut_2.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/male/key/tut_2.ogg b/web/rhythm/audio/voice/male/key/tut_2.ogg deleted file mode 100644 index a5b88188..00000000 Binary files a/web/rhythm/audio/voice/male/key/tut_2.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/male/key/tut_3.m4a b/web/rhythm/audio/voice/male/key/tut_3.m4a deleted file mode 100644 index 8d8bded4..00000000 Binary files a/web/rhythm/audio/voice/male/key/tut_3.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/male/key/tut_3.ogg b/web/rhythm/audio/voice/male/key/tut_3.ogg deleted file mode 100644 index 2c8c0496..00000000 Binary files a/web/rhythm/audio/voice/male/key/tut_3.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/male/key/tut_map.m4a b/web/rhythm/audio/voice/male/key/tut_map.m4a new file mode 100644 index 00000000..1fcf254c Binary files /dev/null and b/web/rhythm/audio/voice/male/key/tut_map.m4a differ diff --git a/web/rhythm/audio/voice/male/key/tut_map.ogg b/web/rhythm/audio/voice/male/key/tut_map.ogg new file mode 100644 index 00000000..cfe64240 Binary files /dev/null and b/web/rhythm/audio/voice/male/key/tut_map.ogg differ diff --git a/web/rhythm/audio/voice/male/key/tut_probe.m4a b/web/rhythm/audio/voice/male/key/tut_probe.m4a new file mode 100644 index 00000000..5407a426 Binary files /dev/null and b/web/rhythm/audio/voice/male/key/tut_probe.m4a differ diff --git a/web/rhythm/audio/voice/male/key/tut_probe.ogg b/web/rhythm/audio/voice/male/key/tut_probe.ogg new file mode 100644 index 00000000..c62e69e2 Binary files /dev/null and b/web/rhythm/audio/voice/male/key/tut_probe.ogg differ diff --git a/web/rhythm/audio/voice/male/key/tut_z_kick.m4a b/web/rhythm/audio/voice/male/key/tut_z_kick.m4a new file mode 100644 index 00000000..ccb48e81 Binary files /dev/null and b/web/rhythm/audio/voice/male/key/tut_z_kick.m4a differ diff --git a/web/rhythm/audio/voice/male/key/tut_z_kick.ogg b/web/rhythm/audio/voice/male/key/tut_z_kick.ogg new file mode 100644 index 00000000..8974bbd5 Binary files /dev/null and b/web/rhythm/audio/voice/male/key/tut_z_kick.ogg differ diff --git a/web/rhythm/audio/voice/male/key/tut_z_patt.m4a b/web/rhythm/audio/voice/male/key/tut_z_patt.m4a new file mode 100644 index 00000000..ec8dddc3 Binary files /dev/null and b/web/rhythm/audio/voice/male/key/tut_z_patt.m4a differ diff --git a/web/rhythm/audio/voice/male/key/tut_z_patt.ogg b/web/rhythm/audio/voice/male/key/tut_z_patt.ogg new file mode 100644 index 00000000..32765eae Binary files /dev/null and b/web/rhythm/audio/voice/male/key/tut_z_patt.ogg differ diff --git a/web/rhythm/audio/voice/male/key/tut_z_sides.m4a b/web/rhythm/audio/voice/male/key/tut_z_sides.m4a new file mode 100644 index 00000000..4b1221c9 Binary files /dev/null and b/web/rhythm/audio/voice/male/key/tut_z_sides.m4a differ diff --git a/web/rhythm/audio/voice/male/key/tut_z_sides.ogg b/web/rhythm/audio/voice/male/key/tut_z_sides.ogg new file mode 100644 index 00000000..cc4a3b28 Binary files /dev/null and b/web/rhythm/audio/voice/male/key/tut_z_sides.ogg differ diff --git a/web/rhythm/audio/voice/male/menu_lobby.m4a b/web/rhythm/audio/voice/male/menu_lobby.m4a index 6f37923e..47d3af43 100644 Binary files a/web/rhythm/audio/voice/male/menu_lobby.m4a and b/web/rhythm/audio/voice/male/menu_lobby.m4a differ diff --git a/web/rhythm/audio/voice/male/menu_lobby.ogg b/web/rhythm/audio/voice/male/menu_lobby.ogg index 701a0eda..8d842166 100644 Binary files a/web/rhythm/audio/voice/male/menu_lobby.ogg and b/web/rhythm/audio/voice/male/menu_lobby.ogg differ diff --git a/web/rhythm/audio/voice/male/menu_main.m4a b/web/rhythm/audio/voice/male/menu_main.m4a index 68899fe9..c13dbdcd 100644 Binary files a/web/rhythm/audio/voice/male/menu_main.m4a and b/web/rhythm/audio/voice/male/menu_main.m4a differ diff --git a/web/rhythm/audio/voice/male/menu_main.ogg b/web/rhythm/audio/voice/male/menu_main.ogg index 438138d0..5d447b1f 100644 Binary files a/web/rhythm/audio/voice/male/menu_main.ogg and b/web/rhythm/audio/voice/male/menu_main.ogg differ diff --git a/web/rhythm/audio/voice/male/menu_resume.m4a b/web/rhythm/audio/voice/male/menu_resume.m4a index bec9fca8..e1059d88 100644 Binary files a/web/rhythm/audio/voice/male/menu_resume.m4a and b/web/rhythm/audio/voice/male/menu_resume.m4a differ diff --git a/web/rhythm/audio/voice/male/menu_resume.ogg b/web/rhythm/audio/voice/male/menu_resume.ogg index d9ed7930..2cefa715 100644 Binary files a/web/rhythm/audio/voice/male/menu_resume.ogg and b/web/rhythm/audio/voice/male/menu_resume.ogg differ diff --git a/web/rhythm/audio/voice/male/mid_song.m4a b/web/rhythm/audio/voice/male/mid_song.m4a index d068608b..b2d5845d 100644 Binary files a/web/rhythm/audio/voice/male/mid_song.m4a and b/web/rhythm/audio/voice/male/mid_song.m4a differ diff --git a/web/rhythm/audio/voice/male/mid_song.ogg b/web/rhythm/audio/voice/male/mid_song.ogg index bb3c3d2b..29445b66 100644 Binary files a/web/rhythm/audio/voice/male/mid_song.ogg and b/web/rhythm/audio/voice/male/mid_song.ogg differ diff --git a/web/rhythm/audio/voice/male/new_best_combo.m4a b/web/rhythm/audio/voice/male/new_best_combo.m4a index 160a93e7..a6cddffc 100644 Binary files a/web/rhythm/audio/voice/male/new_best_combo.m4a and b/web/rhythm/audio/voice/male/new_best_combo.m4a differ diff --git a/web/rhythm/audio/voice/male/new_best_combo.ogg b/web/rhythm/audio/voice/male/new_best_combo.ogg index 00f25a36..838c3f73 100644 Binary files a/web/rhythm/audio/voice/male/new_best_combo.ogg and b/web/rhythm/audio/voice/male/new_best_combo.ogg differ diff --git a/web/rhythm/audio/voice/male/restart.m4a b/web/rhythm/audio/voice/male/restart.m4a index 1b4bbe6b..6ee9789d 100644 Binary files a/web/rhythm/audio/voice/male/restart.m4a and b/web/rhythm/audio/voice/male/restart.m4a differ diff --git a/web/rhythm/audio/voice/male/restart.ogg b/web/rhythm/audio/voice/male/restart.ogg index 18445bd7..234f1282 100644 Binary files a/web/rhythm/audio/voice/male/restart.ogg and b/web/rhythm/audio/voice/male/restart.ogg differ diff --git a/web/rhythm/audio/voice/male/retry_early.m4a b/web/rhythm/audio/voice/male/retry_early.m4a index b667c08c..3e2b68d0 100644 Binary files a/web/rhythm/audio/voice/male/retry_early.m4a and b/web/rhythm/audio/voice/male/retry_early.m4a differ diff --git a/web/rhythm/audio/voice/male/retry_early.ogg b/web/rhythm/audio/voice/male/retry_early.ogg index 43127b18..fcea1a96 100644 Binary files a/web/rhythm/audio/voice/male/retry_early.ogg and b/web/rhythm/audio/voice/male/retry_early.ogg differ diff --git a/web/rhythm/audio/voice/male/retry_gesture.m4a b/web/rhythm/audio/voice/male/retry_gesture.m4a deleted file mode 100644 index ae1d99f5..00000000 Binary files a/web/rhythm/audio/voice/male/retry_gesture.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/male/retry_gesture.ogg b/web/rhythm/audio/voice/male/retry_gesture.ogg deleted file mode 100644 index 4566341c..00000000 Binary files a/web/rhythm/audio/voice/male/retry_gesture.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/male/retry_late.m4a b/web/rhythm/audio/voice/male/retry_late.m4a index 6ad5cb63..31686ed4 100644 Binary files a/web/rhythm/audio/voice/male/retry_late.m4a and b/web/rhythm/audio/voice/male/retry_late.m4a differ diff --git a/web/rhythm/audio/voice/male/retry_late.ogg b/web/rhythm/audio/voice/male/retry_late.ogg index 87466c37..fc484f12 100644 Binary files a/web/rhythm/audio/voice/male/retry_late.ogg and b/web/rhythm/audio/voice/male/retry_late.ogg differ diff --git a/web/rhythm/audio/voice/male/retry_listen.m4a b/web/rhythm/audio/voice/male/retry_listen.m4a index 775011f7..0a7491ca 100644 Binary files a/web/rhythm/audio/voice/male/retry_listen.m4a and b/web/rhythm/audio/voice/male/retry_listen.m4a differ diff --git a/web/rhythm/audio/voice/male/retry_listen.ogg b/web/rhythm/audio/voice/male/retry_listen.ogg index 2b11865e..89c40b1d 100644 Binary files a/web/rhythm/audio/voice/male/retry_listen.ogg and b/web/rhythm/audio/voice/male/retry_listen.ogg differ diff --git a/web/rhythm/audio/voice/male/retry_skip.m4a b/web/rhythm/audio/voice/male/retry_skip.m4a index e7de2e76..f0e0248d 100644 Binary files a/web/rhythm/audio/voice/male/retry_skip.m4a and b/web/rhythm/audio/voice/male/retry_skip.m4a differ diff --git a/web/rhythm/audio/voice/male/retry_skip.ogg b/web/rhythm/audio/voice/male/retry_skip.ogg index 87736d99..5523b541 100644 Binary files a/web/rhythm/audio/voice/male/retry_skip.ogg and b/web/rhythm/audio/voice/male/retry_skip.ogg differ diff --git a/web/rhythm/audio/voice/male/retry_wrong.m4a b/web/rhythm/audio/voice/male/retry_wrong.m4a new file mode 100644 index 00000000..ddc816b6 Binary files /dev/null and b/web/rhythm/audio/voice/male/retry_wrong.m4a differ diff --git a/web/rhythm/audio/voice/male/retry_wrong.ogg b/web/rhythm/audio/voice/male/retry_wrong.ogg new file mode 100644 index 00000000..707624a9 Binary files /dev/null and b/web/rhythm/audio/voice/male/retry_wrong.ogg differ diff --git a/web/rhythm/audio/voice/male/rules.m4a b/web/rhythm/audio/voice/male/rules.m4a index 246da331..49c92098 100644 Binary files a/web/rhythm/audio/voice/male/rules.m4a and b/web/rhythm/audio/voice/male/rules.m4a differ diff --git a/web/rhythm/audio/voice/male/rules.ogg b/web/rhythm/audio/voice/male/rules.ogg index 15de1e18..f0b239e1 100644 Binary files a/web/rhythm/audio/voice/male/rules.ogg and b/web/rhythm/audio/voice/male/rules.ogg differ diff --git a/web/rhythm/audio/voice/male/select_hint.m4a b/web/rhythm/audio/voice/male/select_hint.m4a index 83145449..fd8b7389 100644 Binary files a/web/rhythm/audio/voice/male/select_hint.m4a and b/web/rhythm/audio/voice/male/select_hint.m4a differ diff --git a/web/rhythm/audio/voice/male/select_hint.ogg b/web/rhythm/audio/voice/male/select_hint.ogg index d01c8594..ba2d65a3 100644 Binary files a/web/rhythm/audio/voice/male/select_hint.ogg and b/web/rhythm/audio/voice/male/select_hint.ogg differ diff --git a/web/rhythm/audio/voice/male/settings_hint.m4a b/web/rhythm/audio/voice/male/settings_hint.m4a index 9f728e21..029681e6 100644 Binary files a/web/rhythm/audio/voice/male/settings_hint.m4a and b/web/rhythm/audio/voice/male/settings_hint.m4a differ diff --git a/web/rhythm/audio/voice/male/settings_hint.ogg b/web/rhythm/audio/voice/male/settings_hint.ogg index 9eb0c70b..93d74770 100644 Binary files a/web/rhythm/audio/voice/male/settings_hint.ogg and b/web/rhythm/audio/voice/male/settings_hint.ogg differ diff --git a/web/rhythm/audio/voice/male/skip_hint.m4a b/web/rhythm/audio/voice/male/skip_hint.m4a index 18831af8..f8dca40d 100644 Binary files a/web/rhythm/audio/voice/male/skip_hint.m4a and b/web/rhythm/audio/voice/male/skip_hint.m4a differ diff --git a/web/rhythm/audio/voice/male/skip_hint.ogg b/web/rhythm/audio/voice/male/skip_hint.ogg index 84b4f847..052926b0 100644 Binary files a/web/rhythm/audio/voice/male/skip_hint.ogg and b/web/rhythm/audio/voice/male/skip_hint.ogg differ diff --git a/web/rhythm/audio/voice/male/song1_name.m4a b/web/rhythm/audio/voice/male/song1_name.m4a index af4cac4c..703592b0 100644 Binary files a/web/rhythm/audio/voice/male/song1_name.m4a and b/web/rhythm/audio/voice/male/song1_name.m4a differ diff --git a/web/rhythm/audio/voice/male/song1_name.ogg b/web/rhythm/audio/voice/male/song1_name.ogg index 02967ed6..7ab9c00b 100644 Binary files a/web/rhythm/audio/voice/male/song1_name.ogg and b/web/rhythm/audio/voice/male/song1_name.ogg differ diff --git a/web/rhythm/audio/voice/male/song2_name.m4a b/web/rhythm/audio/voice/male/song2_name.m4a index 83ad53c2..0b934193 100644 Binary files a/web/rhythm/audio/voice/male/song2_name.m4a and b/web/rhythm/audio/voice/male/song2_name.m4a differ diff --git a/web/rhythm/audio/voice/male/song2_name.ogg b/web/rhythm/audio/voice/male/song2_name.ogg index 5f404054..82ec8282 100644 Binary files a/web/rhythm/audio/voice/male/song2_name.ogg and b/web/rhythm/audio/voice/male/song2_name.ogg differ diff --git a/web/rhythm/audio/voice/male/song3_name.m4a b/web/rhythm/audio/voice/male/song3_name.m4a index e4f5ccfb..e0df651d 100644 Binary files a/web/rhythm/audio/voice/male/song3_name.m4a and b/web/rhythm/audio/voice/male/song3_name.m4a differ diff --git a/web/rhythm/audio/voice/male/song3_name.ogg b/web/rhythm/audio/voice/male/song3_name.ogg index 7100b68c..ebd08947 100644 Binary files a/web/rhythm/audio/voice/male/song3_name.ogg and b/web/rhythm/audio/voice/male/song3_name.ogg differ diff --git a/web/rhythm/audio/voice/male/song_locked.m4a b/web/rhythm/audio/voice/male/song_locked.m4a index d5307ef6..68a6dd76 100644 Binary files a/web/rhythm/audio/voice/male/song_locked.m4a and b/web/rhythm/audio/voice/male/song_locked.m4a differ diff --git a/web/rhythm/audio/voice/male/song_locked.ogg b/web/rhythm/audio/voice/male/song_locked.ogg index f01ccb41..ca7cea4e 100644 Binary files a/web/rhythm/audio/voice/male/song_locked.ogg and b/web/rhythm/audio/voice/male/song_locked.ogg differ diff --git a/web/rhythm/audio/voice/male/tut_1.m4a b/web/rhythm/audio/voice/male/tut_1.m4a deleted file mode 100644 index 6f3415a4..00000000 Binary files a/web/rhythm/audio/voice/male/tut_1.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/male/tut_1.ogg b/web/rhythm/audio/voice/male/tut_1.ogg deleted file mode 100644 index b5cb3740..00000000 Binary files a/web/rhythm/audio/voice/male/tut_1.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/male/tut_2.m4a b/web/rhythm/audio/voice/male/tut_2.m4a deleted file mode 100644 index 25500c4d..00000000 Binary files a/web/rhythm/audio/voice/male/tut_2.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/male/tut_2.ogg b/web/rhythm/audio/voice/male/tut_2.ogg deleted file mode 100644 index 9413b731..00000000 Binary files a/web/rhythm/audio/voice/male/tut_2.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/male/tut_3.m4a b/web/rhythm/audio/voice/male/tut_3.m4a deleted file mode 100644 index f010bfaa..00000000 Binary files a/web/rhythm/audio/voice/male/tut_3.m4a and /dev/null differ diff --git a/web/rhythm/audio/voice/male/tut_3.ogg b/web/rhythm/audio/voice/male/tut_3.ogg deleted file mode 100644 index 709e7cf3..00000000 Binary files a/web/rhythm/audio/voice/male/tut_3.ogg and /dev/null differ diff --git a/web/rhythm/audio/voice/male/tut_done.m4a b/web/rhythm/audio/voice/male/tut_done.m4a index ef8b9e62..d1240a05 100644 Binary files a/web/rhythm/audio/voice/male/tut_done.m4a and b/web/rhythm/audio/voice/male/tut_done.m4a differ diff --git a/web/rhythm/audio/voice/male/tut_done.ogg b/web/rhythm/audio/voice/male/tut_done.ogg index 8c8a2816..4dc23b19 100644 Binary files a/web/rhythm/audio/voice/male/tut_done.ogg and b/web/rhythm/audio/voice/male/tut_done.ogg differ diff --git a/web/rhythm/audio/voice/male/tut_fill.m4a b/web/rhythm/audio/voice/male/tut_fill.m4a new file mode 100644 index 00000000..bb410231 Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_fill.m4a differ diff --git a/web/rhythm/audio/voice/male/tut_fill.ogg b/web/rhythm/audio/voice/male/tut_fill.ogg new file mode 100644 index 00000000..017b8a99 Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_fill.ogg differ diff --git a/web/rhythm/audio/voice/male/tut_map.m4a b/web/rhythm/audio/voice/male/tut_map.m4a new file mode 100644 index 00000000..84354443 Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_map.m4a differ diff --git a/web/rhythm/audio/voice/male/tut_map.ogg b/web/rhythm/audio/voice/male/tut_map.ogg new file mode 100644 index 00000000..7e46a241 Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_map.ogg differ diff --git a/web/rhythm/audio/voice/male/tut_probe.m4a b/web/rhythm/audio/voice/male/tut_probe.m4a new file mode 100644 index 00000000..a7d8703a Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_probe.m4a differ diff --git a/web/rhythm/audio/voice/male/tut_probe.ogg b/web/rhythm/audio/voice/male/tut_probe.ogg new file mode 100644 index 00000000..096726a0 Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_probe.ogg differ diff --git a/web/rhythm/audio/voice/male/tut_skip.m4a b/web/rhythm/audio/voice/male/tut_skip.m4a index d8832023..3ef75bf1 100644 Binary files a/web/rhythm/audio/voice/male/tut_skip.m4a and b/web/rhythm/audio/voice/male/tut_skip.m4a differ diff --git a/web/rhythm/audio/voice/male/tut_skip.ogg b/web/rhythm/audio/voice/male/tut_skip.ogg index f7f0c4c3..80f6bb5d 100644 Binary files a/web/rhythm/audio/voice/male/tut_skip.ogg and b/web/rhythm/audio/voice/male/tut_skip.ogg differ diff --git a/web/rhythm/audio/voice/male/tut_z_kick.m4a b/web/rhythm/audio/voice/male/tut_z_kick.m4a new file mode 100644 index 00000000..6618221f Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_z_kick.m4a differ diff --git a/web/rhythm/audio/voice/male/tut_z_kick.ogg b/web/rhythm/audio/voice/male/tut_z_kick.ogg new file mode 100644 index 00000000..593cd4b4 Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_z_kick.ogg differ diff --git a/web/rhythm/audio/voice/male/tut_z_patt.m4a b/web/rhythm/audio/voice/male/tut_z_patt.m4a new file mode 100644 index 00000000..d29203a1 Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_z_patt.m4a differ diff --git a/web/rhythm/audio/voice/male/tut_z_patt.ogg b/web/rhythm/audio/voice/male/tut_z_patt.ogg new file mode 100644 index 00000000..6649352c Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_z_patt.ogg differ diff --git a/web/rhythm/audio/voice/male/tut_z_sides.m4a b/web/rhythm/audio/voice/male/tut_z_sides.m4a new file mode 100644 index 00000000..22ef8f56 Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_z_sides.m4a differ diff --git a/web/rhythm/audio/voice/male/tut_z_sides.ogg b/web/rhythm/audio/voice/male/tut_z_sides.ogg new file mode 100644 index 00000000..ef367a04 Binary files /dev/null and b/web/rhythm/audio/voice/male/tut_z_sides.ogg differ diff --git a/web/rhythm/audio/voice/male/unlock_song2.m4a b/web/rhythm/audio/voice/male/unlock_song2.m4a index 1f2791d6..acada52b 100644 Binary files a/web/rhythm/audio/voice/male/unlock_song2.m4a and b/web/rhythm/audio/voice/male/unlock_song2.m4a differ diff --git a/web/rhythm/audio/voice/male/unlock_song2.ogg b/web/rhythm/audio/voice/male/unlock_song2.ogg index c6ed5bb1..7dfd3674 100644 Binary files a/web/rhythm/audio/voice/male/unlock_song2.ogg and b/web/rhythm/audio/voice/male/unlock_song2.ogg differ diff --git a/web/rhythm/audio/voice/male/unlock_song3.m4a b/web/rhythm/audio/voice/male/unlock_song3.m4a index 1f089ccb..fb00cc3c 100644 Binary files a/web/rhythm/audio/voice/male/unlock_song3.m4a and b/web/rhythm/audio/voice/male/unlock_song3.m4a differ diff --git a/web/rhythm/audio/voice/male/unlock_song3.ogg b/web/rhythm/audio/voice/male/unlock_song3.ogg index 6cd7d96d..8b8a39e6 100644 Binary files a/web/rhythm/audio/voice/male/unlock_song3.ogg and b/web/rhythm/audio/voice/male/unlock_song3.ogg differ diff --git a/web/rhythm/audio/voice/male/welcome.m4a b/web/rhythm/audio/voice/male/welcome.m4a index 7c4a2564..c9d7d18e 100644 Binary files a/web/rhythm/audio/voice/male/welcome.m4a and b/web/rhythm/audio/voice/male/welcome.m4a differ diff --git a/web/rhythm/audio/voice/male/welcome.ogg b/web/rhythm/audio/voice/male/welcome.ogg index b4dda543..370f5184 100644 Binary files a/web/rhythm/audio/voice/male/welcome.ogg and b/web/rhythm/audio/voice/male/welcome.ogg differ diff --git a/web/rhythm/audio/voice/male/welcome_back.m4a b/web/rhythm/audio/voice/male/welcome_back.m4a index 31063050..53f2baa2 100644 Binary files a/web/rhythm/audio/voice/male/welcome_back.m4a and b/web/rhythm/audio/voice/male/welcome_back.m4a differ diff --git a/web/rhythm/audio/voice/male/welcome_back.ogg b/web/rhythm/audio/voice/male/welcome_back.ogg index 38527a84..365ca3d1 100644 Binary files a/web/rhythm/audio/voice/male/welcome_back.ogg and b/web/rhythm/audio/voice/male/welcome_back.ogg differ diff --git a/web/rhythm/audio/voice/male/zone_bc.m4a b/web/rhythm/audio/voice/male/zone_bc.m4a new file mode 100644 index 00000000..bbc81d64 Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_bc.m4a differ diff --git a/web/rhythm/audio/voice/male/zone_bc.ogg b/web/rhythm/audio/voice/male/zone_bc.ogg new file mode 100644 index 00000000..6193c82a Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_bc.ogg differ diff --git a/web/rhythm/audio/voice/male/zone_bl.m4a b/web/rhythm/audio/voice/male/zone_bl.m4a new file mode 100644 index 00000000..e7ac944e Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_bl.m4a differ diff --git a/web/rhythm/audio/voice/male/zone_bl.ogg b/web/rhythm/audio/voice/male/zone_bl.ogg new file mode 100644 index 00000000..06a8b487 Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_bl.ogg differ diff --git a/web/rhythm/audio/voice/male/zone_br.m4a b/web/rhythm/audio/voice/male/zone_br.m4a new file mode 100644 index 00000000..ab974b2a Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_br.m4a differ diff --git a/web/rhythm/audio/voice/male/zone_br.ogg b/web/rhythm/audio/voice/male/zone_br.ogg new file mode 100644 index 00000000..eced79fb Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_br.ogg differ diff --git a/web/rhythm/audio/voice/male/zone_tc.m4a b/web/rhythm/audio/voice/male/zone_tc.m4a new file mode 100644 index 00000000..cd39dd3a Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_tc.m4a differ diff --git a/web/rhythm/audio/voice/male/zone_tc.ogg b/web/rhythm/audio/voice/male/zone_tc.ogg new file mode 100644 index 00000000..0ba97b3d Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_tc.ogg differ diff --git a/web/rhythm/audio/voice/male/zone_tl.m4a b/web/rhythm/audio/voice/male/zone_tl.m4a new file mode 100644 index 00000000..3a58537c Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_tl.m4a differ diff --git a/web/rhythm/audio/voice/male/zone_tl.ogg b/web/rhythm/audio/voice/male/zone_tl.ogg new file mode 100644 index 00000000..d4108644 Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_tl.ogg differ diff --git a/web/rhythm/audio/voice/male/zone_tr.m4a b/web/rhythm/audio/voice/male/zone_tr.m4a new file mode 100644 index 00000000..dd5e006e Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_tr.m4a differ diff --git a/web/rhythm/audio/voice/male/zone_tr.ogg b/web/rhythm/audio/voice/male/zone_tr.ogg new file mode 100644 index 00000000..1994aade Binary files /dev/null and b/web/rhythm/audio/voice/male/zone_tr.ogg differ diff --git a/web/rhythm/game.js b/web/rhythm/game.js index 1b962fa5..605572ab 100644 --- a/web/rhythm/game.js +++ b/web/rhythm/game.js @@ -1,274 +1,241 @@ /** - * 节拍行者(rhythm)R3 —— 跟随领奏(call & response)节奏游戏 - * (按 docs/design/03-rhythm-rebuild.md 实现;语音 id 见 03-rhythm-voice-ids.md)。 + * 节拍行者(rhythm)鼓手模式 —— 屏幕分区打击乐(24-rhythm-drummer.md 实现; + * 语音 id 见 24-rhythm-voice-ids.md)。 * - * 玩法:系统奏 1 小节节奏型(2-4 个手势乐器音,inst_* 踩拍点),玩家在下一小节 - * 的相同拍点用相同手势跟打。3 首曲目(90/105/120 BPM)解锁制:曲目 n 拿到 B 级 - * 以上解锁 n+1(localStorage 记解锁状态,已登录同步服务器)。 + * 玩法:整面屏幕是一面 3列×2行 的鼓组(列=声像左中右,行=音高上亮下沉), + * 六区六种打击乐。无鼓伴奏 BGM 上,系统(领奏)敲一小节节奏型,玩家在下一 + * 小节的相同拍点敲相同的鼓;3 首曲目(90/105/120 BPM)解锁制:曲目 n 拿到 + * B 级以上解锁 n+1(localStorage 记进度,已登录 PUT 同步服务器)。 * - * 判定(live round1 §13 校准版;03 文档 §2 契约的窗口放宽): - * Perfect ±0.15s(手势乐器音 + inst_accent 亮音叠加) - * Good ±0.35s(只播乐器音;蓝牙延迟与滑动/双击识别延迟由此吸收) - * Miss 窗口内手势错 / 超时应答缺失:不播该音符的乐器音(错过=没出声) - * + 闷镲 Miss 反馈音(inst_hat_l 降调 0.5× 复用,零新资产)+ error 震动; - * 长按 = 死输入同款反馈(不判定不归属) - * 判定时刻 = 触摸时刻(回调时刻 - 识别补偿):immediateTap(index.html 已 - * 开启,按下即发 tap)下 tap 补偿 0;旧引擎(双击窗 300ms)补偿 TAP_DELAY - * 0.30s——运行证据自动闩锁回退(见 onGesture)。滑动补偿 SWIPE_DELAY 0.15s、 - * 双击 DOUBLE_DELAY 0.20s(识别都在 touchend);Miss 超时放宽 = 补偿 + 0.10 + * 判定(24 文档 §5;旧作 live round1 §13 校准口径沿用): + * Perfect ±0.15s(该区音色 + dm_accent 亮音叠加) + * Good ±0.35s(只播该区音色;蓝牙延迟由此吸收) + * Miss 异分区半窗内被敲 / 超时未应答:闷镲反馈音(dm_hat 降调 0.5×)+ error 震 + * 全部音符 = 分区点按:immediateTap(index.html 开启)按下即判,零识别补偿; + * 旧作的 TAP_DELAY/SWIPE_DELAY/DOUBLE_DELAY、双击序曲撤销、超时复活窗整体退役。 + * 序曲 tap 治理(immediateTap 代价):按下先发 tap(出声+可认领),转滑动/长按/ + * 触摸取消时引擎补 tapcancel → 撤销认领不计 Miss(声音收不回当 ghost 音); + * 双击 = 同区两连击,两下各自独立成立,无撤销。 * 计分:Perfect=2 / Good=1 / Miss=0;rate = 得分/(2×总音符); - * 评级 S≥0.95 / A≥0.85 / B≥0.70 / C 其余。不掉血不中断,歌照唱完。 + * S≥0.95 / A≥0.85 / B≥0.70 / C 其余。不掉血不中断,歌照唱完。 * - * 时序核心(沿用现版):全部节拍事件用 AudioContext.currentTime 预排程 - * (领奏音符、BGM 起点按绝对时间 source.start(when) 调度,40ms 调度泵 + - * LOOKAHEAD 0.6s 前瞻);玩家乐器音不走预排程,出手即播(sfx 即发即弃)。 + * 时序核心(旧作遗产):全部节拍事件 AudioContext.currentTime 预排程(领奏音符、 + * 数拍、BGM 起点 source.start(when) 对网格,40ms 泵 + LOOKAHEAD 0.6s 前瞻); + * 玩家敲击不走预排程,出手即播(sfx 即发即弃,pan 随行)。 * - * 震动落点(手势识别震动引擎内置,不重复):领奏每音 beat 轻震、Miss error、 - * 16 连击 success、乐句全 Perfect [20,40,20]、解锁新曲 success+[20,40,20]、 - * 结算 S/A success。 + * 分区心智(教学核心,24 文档 §6):T0 摸鼓认区(≥3 区自动进阶,区名每区播一次) + * → T1 底鼓家区 → T2 左右下排 → T3 上排三音;单步重教 ≤2(连 2 败降速 25% 重演 + * 一次再跳过,G2 DDA 沿用);归因真话律(没出手 → gd_miss_none)。 * - * 菜单(round2-review 全球约定):下滑呼出(模态暂停,BGM 断点续播)→ - * 单击循环 → 双击确认 → 上滑帮助。下滑与"下滑=低音 tom"冲突的折中: - * 仅当无悬置/临近的 down 音符时下滑才呼出菜单(见 README);临近窗口 = - * 该 down 音符领奏响起(expected-4 拍)至判定死线(live round1 P0 加宽—— - * 旧 ±1s/+0.55s 窗口跟不上滑动识别延迟,踩点下滑曾必弹菜单);live round2 - * 续修:down 音符刚被超时拖死(识别延迟,REVIVE_WINDOW_S 内)同样算临近—— - * 下滑还在路上时音符被判死不再弹菜单(事件流实证 3/3:超时 Miss 先于下滑 - * 回调 2~114ms)。暂停真冻结(泵停 = 谱面/判定/定时动作全冻结,BGM 停源记 - * 断点);关菜单先停菜单语音链再恢复(菜单语音骑在恢复后领奏上是"菜单 - * 不暂停"感知的主因)。菜单打开 1s 去抖独占期(live round2 P1:演奏途中 - * 被弹菜单时,已在路上的手势不得穿透菜单直接确认——实测 0.6s"瞬穿")。 + * 加花(fill):领奏 1 小节八分密集 + 尾音恒 TR 吊镲;应答窗口同普句;曲目一 + * 终曲加花前 1 小节播 tut_fill(每账号一次)。 * - * immediateTap 兼容(引擎 opt-in,index.html 已开启,按下即发 tap): - * 每次按下都先发 tap——滑动/双击/长按的触摸起点也会先出一声 tap。演奏 - * 阶段(tut/main)序曲 tap 出声(kick)但只认领 tap 音符(不抢归属不判 - * Miss);live round2 P0 判定治理:序曲 tap 判定记账,转滑动/长按(引擎 - * 补 tapcancel)或改判双击时按账撤销——声音已出收不回,判定/统计更正 - * 且不计 Miss;识别延迟把音符超时拖死而手势仍在路上的(同手势、超时类、 - * REVIVE_WINDOW_S 内、补偿 dt ≤ REVIVE_DT_S)复活改判。非演奏阶段 - * (intro/select/menu/end/leaving)tap 延迟 STRAY_TAP_MS 生效、期间来了 - * 其他手势或 tapcancel 即作废(否则选曲右滑会先误触发"选定曲目")。 + * 菜单(下滑无条件呼出——音符全是点按,无旧作 down 音符冲突):模态暂停、 + * 泵停 BGM 断点续播、恢复全网格平移 + 4 拍 count-in(guide full 档先播 + * gd_count 每曲限 1 次);开菜单 1s 去抖独占期(旧作 round2 P1 沿用)。 * - * 状态 BGM(docs/design/10-feedback2-state-bgm.md §5): - * 选曲界面 = bgm/menu(引擎层淡入 800ms);选定曲目 stopBgm(400) 后按既有 - * 网格起 track90/105/120(手动排程不变);暂停菜单保持静默(断点续播, - * 不叠 menu pad);结算评级语音链播完后淡入 bgm/menu 800ms——且只在仍处 - * 结算态时起播,看门狗按链长缩放(live round1 P1:10s 看门狗曾早火把 - * menu pad 骑到语音链尾上;重开后过期 fire 会把 menu pad 漏进正片)。 - * 旁白跳过(docs/design/12-skip-and-longpress-ux.md §1.2):intro/tut/select/ - * end 阶段 voice 播放中单击 = audio.skipVoice()(跳下一句,链不断); - * main 演奏阶段单击是演奏手势不跳(mid_song 短句按裁定不跳)。 - * 长按快进豁免(12 文档 §2.2:main 演奏期):audio.setRateBoostEnabled(false) - * per-game 开关(引擎已支持),进 main 关、进菜单/结算/退出恢复。 - * 向导层(docs/design/14-guide-layer.md + round2 synthesis F2.1/F3 + round3 G1~G3 - * + round4 H5): - * 每曲首次领奏→应答切换在谱面定时队列直接落 gd_answer(不走 guide queue,保证 - * 窗口前听到)+ turn earcon;round3 G1 修正排程点:落在首个应答窗口**前整整 - * 1 小节**(= 领奏小节头;round2 排在窗口起点本身,P4 实测窗口开了才听到), - * 菜单恢复对下一个未判定窗口重排同款标记;教学 Miss 首次即归因分档 - * (round4 H5 四档:无窗内尝试 gd_miss_none 优先、真出手再分 early/late/wrong, - * 挂纠正链尾空闲点;正片连 2 Miss 仍 gd_miss);连续 2 Miss 播 gd_miss(queue - * 插播,同曲一次);暂停断点续播前插 4 拍 count-in(round3 G3:底鼓 sfx 层可辨 - * 音量 + full 档 gd_count 每局恢复限 1 次 + beat 震;round2 版因 push 后未重排 - * _sched 被队头卡死从未按时响过;off 档保持即时续播);教学 DDA(round3 G2): - * 同一步连 2 次失败该步领奏降速 25% 重演并播 gd_slow(full 档、每教学局 1 次, - * off 档维持 retry_skip 原路径);结算链换 _speakGuarded(资产缺失不卡结算 BGM); - * 状态签名 earcon 与状态 BGM 同点(select=menu、main=battle、end=settle)。 + * 状态 BGM(10 文档 §5)/旁白跳过与长按快进豁免(12 文档)/向导层(14 文档 + + * round2-4 修正:gd_answer 排窗口前 1 小节、教学归因四档、gd_miss 连 2 一次、 + * count-in 恢复、教学 DDA、结算 _speakGuarded + menu pad 看门狗)——全部沿用。 + * 键盘平权:Digit1-6(及小键盘)= 六区,按下即发零延迟;引擎空格 tap 落元素 + * 中心 → 判 BC 家区;选曲/菜单/结算态数字键不生效。 */ import { Game } from '../engine/game.js'; import { Guide } from '../engine/guide.js'; import { vibrate } from '../engine/haptics.js'; import { settings } from '../engine/settings.js'; -// ---- 判定与调度常量(live round1 §13:03 文档 §2 契约的窗口放宽版) ---- -const PERFECT = 0.15; -const GOOD = 0.35; -const TAP_DELAY = 0.30; // 旧引擎 tap 回调延迟(双击窗 300ms)补偿;immediateTap 下为 0 -const SWIPE_DELAY = 0.15; // 滑动识别在 touchend:补平均滑程(判定回到触摸时刻) -const DOUBLE_DELAY = 0.20; // 双击识别在第二次 touchend:补首按到改判的间隔 -const STRAY_TAP_MS = 500; // immediateTap 序曲 tap 裁决窗(> 引擎 longpressMs 450,长按也能抢先作废) -const MISS_SFX = 'sfx/inst_hat_l'; // Miss 反馈音:闭镲降调复用(闷镲),零新资产 -const MISS_RATE = 0.5; +// ---- 判定与调度常量(24 文档 §5;旧作 live 校准口径) ---- +const PERFECT = 0.15; // Perfect 窗口 ±150ms +const GOOD = 0.35; // Good 窗口 ±350ms(蓝牙延迟由它吸收) +const MISS_MARGIN = 0.10; // Miss 超时放宽 = GOOD + 0.10 +const STRAY_TAP_MS = 500; // immediateTap 序曲 tap 在非演奏阶段的延迟裁决窗 +const TAPCANCEL_REVOKE_S = 0.6; // tapcancel 撤销窗(覆盖长按 450ms 成立点) const LOOKAHEAD = 0.6; const PUMP_MS = 40; -// ---- live round2 判定治理常量 ---- -const MENU_DEBOUNCE_MS = 1000; // 菜单独占去抖期:开菜单 1s 内不收任何手势(防"瞬穿") -const TAPCANCEL_REVOKE_S = 0.6; // tapcancel 撤销窗(覆盖长按 450ms 成立点与滑动 slop 判定) -const DOUBLE_REVOKE_S = 0.5; // 双击序曲 tap 撤销窗(双击窗 300ms + 两段触程余量) -const REVIVE_WINDOW_S = 0.6; // 超时 Miss 可复活窗(识别延迟把音符拖死、手势还在路上) -const REVIVE_DT_S = 0.5; // 复活判定时差上限(GOOD 0.35 + 慢滑/慢双击补偿误差 0.15) +const MENU_DEBOUNCE_MS = 1000; // 菜单去抖独占期(旧作 round2 P1) +const MISS_SFX = 'sfx/dm_hat'; // 闷镲反馈音:踩镲降调 0.5× 复用,零新资产 +const MISS_RATE = 0.5; -// ---- 手势 → 乐器音映射(03 文档 §4;accent 为 Perfect 叠加层) ---- -const INST = { - tap: 'sfx/inst_kick', - doubletap: 'sfx/inst_snare', - left: 'sfx/inst_hat_l', - right: 'sfx/inst_hat_r', - up: 'sfx/inst_tom_hi', - down: 'sfx/inst_tom_lo', +// ---- 分区表(24 文档 §2:列=声像,行=音高;sfx 程序合成 tools/gen_rhythm_syn.py) ---- +const ZONES = { + TL: { sfx: 'sfx/dm_hat', pan: -0.6, name: 'zone_tl' }, // 左上 踩镲 + TC: { sfx: 'sfx/dm_tomhi', pan: 0, name: 'zone_tc' }, // 上中 高音嗵 + TR: { sfx: 'sfx/dm_crash', pan: 0.6, name: 'zone_tr' }, // 右上 吊镲 + BL: { sfx: 'sfx/dm_snare', pan: -0.6, name: 'zone_bl' }, // 左下 军鼓 + BC: { sfx: 'sfx/inst_kick', pan: 0, name: 'zone_bc' }, // 下中 底鼓(家;沿用旧 id,guide.test G2/G3 锚点) + BR: { sfx: 'sfx/dm_tomlo', pan: 0.6, name: 'zone_br' }, // 右下 低音嗵 }; -const ACCENT = 'sfx/inst_accent'; +const ACCENT = 'sfx/dm_accent'; +const CLICK = 'sfx/dm_accent'; // 教学数拍木鱼(与 Perfect 亮音同资产零新增) +const KICK = 'sfx/inst_kick'; // 底鼓/正片数拍(沿用旧 id) -// ---- 谱面(03 文档 §3 表格直转;restAfter = 每句应答后休止小节数) ---- +// ---- 谱面(24 文档 §4 表格直转;fill=加花句;restAfter=应答后休止小节数) ---- const SONGS = [ { id: 'song1', title: '启程', bpm: 90, bgm: 'bgm/track90', nameVoice: 'voice/song1_name', unlockVoice: null, - restAfter: [1, 1, 1, 1, 0, 0, 0, 0, 0, 0], - phrases: [ - [[1, 'tap'], [3, 'tap']], - [[1, 'left'], [3, 'right']], - [[1, 'tap'], [2, 'doubletap'], [3, 'tap']], - [[1, 'left'], [2, 'doubletap'], [3, 'right']], - [[1, 'tap'], [3, 'left'], [4, 'doubletap']], - [[1, 'right'], [3, 'tap'], [4, 'doubletap']], - [[1, 'up'], [3, 'down'], [4, 'doubletap']], - [[1, 'tap'], [2, 'up'], [3, 'right'], [4, 'doubletap']], - [[1, 'left'], [2, 'right'], [3, 'tap'], [4, 'doubletap']], - [[1, 'tap'], [2, 'doubletap'], [3, 'tap'], [4, 'doubletap']], + units: [ + { notes: [[1, 'BC'], [3, 'BC']], restAfter: 1 }, + { notes: [[1, 'BL'], [3, 'BR']], restAfter: 1 }, + { notes: [[1, 'BC'], [2, 'BC'], [3, 'BL']], restAfter: 1 }, + { notes: [[1, 'BL'], [2, 'BR'], [3, 'BC']], restAfter: 1 }, + { notes: [[1, 'BC'], [2, 'BL'], [3, 'BC'], [4, 'BR']], restAfter: 0 }, + { notes: [[1, 'TL'], [3, 'BL'], [4, 'BC']], restAfter: 0 }, + { notes: [[1, 'BC'], [2, 'TL'], [3, 'BC'], [4, 'TR']], restAfter: 0 }, + { notes: [[1, 'BL'], [2, 'BR'], [3, 'TL'], [4, 'BC']], restAfter: 0 }, + { notes: [[1, 'BC'], [1.5, 'BC'], [2, 'BC'], [2.5, 'BL'], [3, 'BL'], [3.5, 'BC'], [4, 'TR']], fill: true, restAfter: 0 }, ], }, { id: 'song2', title: '追风', bpm: 105, bgm: 'bgm/track105', nameVoice: 'voice/song2_name', unlockVoice: 'voice/unlock_song2', - restAfter: [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - phrases: [ - [[1, 'tap'], [3, 'right']], - [[1, 'left'], [2, 'doubletap'], [3, 'tap']], - [[1, 'up'], [3, 'down'], [4, 'doubletap']], - [[1, 'tap'], [2, 'right'], [3, 'left'], [4, 'doubletap']], - [[1, 'tap'], [2.5, 'right'], [3, 'tap']], - [[1, 'left'], [2, 'up'], [3.5, 'right'], [4, 'doubletap']], - [[1, 'tap'], [2, 'doubletap'], [3, 'down'], [3.5, 'up']], - [[1, 'right'], [1.5, 'left'], [3, 'tap'], [4, 'doubletap']], - [[1, 'tap'], [2.5, 'left'], [3, 'tap'], [4, 'doubletap']], - [[1, 'up'], [2, 'right'], [3, 'down'], [3.5, 'left']], - [[1, 'tap'], [2, 'doubletap'], [2.5, 'right'], [3, 'tap']], - [[1, 'left'], [1.5, 'right'], [2, 'doubletap'], [3, 'tap'], [4, 'doubletap']], + units: [ + { notes: [[1, 'BC'], [3, 'BR']], restAfter: 1 }, + { notes: [[1, 'BL'], [2, 'BL'], [3, 'BC']], restAfter: 1 }, + { notes: [[1, 'TL'], [3, 'BL'], [4, 'BR']], restAfter: 0 }, + { notes: [[1, 'BC'], [2, 'BL'], [3, 'BC'], [4, 'TL']], restAfter: 0 }, + { notes: [[1, 'BC'], [2.5, 'BR'], [3, 'BC']], restAfter: 0 }, + { notes: [[1, 'BL'], [1.5, 'BL'], [2, 'BC'], [2.5, 'BC'], [3, 'BR'], [3.5, 'BR'], [4, 'TR']], fill: true, restAfter: 0 }, + { notes: [[1, 'BL'], [2, 'TL'], [3.5, 'BR'], [4, 'BC']], restAfter: 0 }, + { notes: [[1, 'BC'], [2, 'BL'], [3, 'BR'], [3.5, 'TL']], restAfter: 0 }, + { notes: [[1, 'BR'], [1.5, 'BL'], [3, 'BC'], [4, 'TR']], restAfter: 0 }, + { notes: [[1, 'BC'], [2.5, 'TL'], [3, 'BC'], [4, 'BL']], restAfter: 0 }, + { notes: [[1, 'BL'], [2, 'BR'], [3, 'TL'], [4, 'BC']], restAfter: 0 }, + { notes: [[1, 'BC'], [1.5, 'BL'], [2, 'BC'], [2.5, 'BL'], [3, 'BC'], [3.5, 'BR'], [4, 'TR']], fill: true, restAfter: 0 }, ], }, { id: 'song3', title: '星爆', bpm: 120, bgm: 'bgm/track120', nameVoice: 'voice/song3_name', unlockVoice: 'voice/unlock_song3', - restAfter: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - phrases: [ - [[1, 'tap'], [2, 'doubletap'], [3, 'tap'], [4, 'doubletap']], - [[1, 'left'], [2, 'right'], [3, 'left'], [4, 'doubletap']], - [[1, 'up'], [2, 'down'], [3, 'up'], [4, 'doubletap']], - [[1, 'tap'], [2.5, 'left'], [3, 'tap'], [4, 'right']], - [[1, 'left'], [1.5, 'right'], [2.5, 'left'], [3, 'tap']], - [[1, 'tap'], [2, 'up'], [2.5, 'down'], [3, 'tap'], [4, 'doubletap']], - [[1, 'right'], [2, 'doubletap'], [3, 'left'], [3.5, 'right'], [4, 'up']], - [[1, 'tap'], [1.5, 'doubletap'], [2.5, 'left'], [3, 'tap'], [3.5, 'right'], [4, 'doubletap']], - [[1, 'left'], [2, 'doubletap'], [2.5, 'up'], [3, 'right'], [4, 'doubletap']], - [[1, 'tap'], [2.5, 'down'], [3, 'tap'], [3.5, 'up'], [4, 'doubletap']], - [[1, 'right'], [1.5, 'left'], [2, 'right'], [3, 'tap'], [4, 'doubletap']], - [[1, 'tap'], [2, 'doubletap'], [2.5, 'left'], [3, 'tap'], [3.5, 'right'], [4, 'doubletap']], + units: [ + { notes: [[1, 'BC'], [2, 'BL'], [3, 'BC'], [4, 'BL']], restAfter: 0 }, + { notes: [[1, 'TL'], [2, 'BL'], [3, 'TL'], [4, 'BC']], restAfter: 0 }, + { notes: [[1, 'BR'], [2, 'TR'], [3, 'BR'], [4, 'BL']], restAfter: 0 }, + { notes: [[1, 'BC'], [2.5, 'TL'], [3, 'BC'], [4, 'BR']], restAfter: 0 }, + { notes: [[1, 'BL'], [1.5, 'BR'], [2.5, 'BL'], [3, 'BC']], restAfter: 0 }, + { notes: [[1, 'BC'], [2, 'TL'], [2.5, 'BL'], [3, 'BC'], [4, 'TR']], restAfter: 0 }, + { notes: [[1, 'TL'], [1.5, 'TL'], [2, 'BL'], [2.5, 'BL'], [3, 'BC'], [3.5, 'BC'], [4, 'TR']], fill: true, restAfter: 0 }, + { notes: [[1, 'BR'], [2, 'BC'], [3, 'BL'], [3.5, 'BR'], [4, 'TL']], restAfter: 0 }, + { notes: [[1, 'BC'], [1.5, 'BC'], [2.5, 'BL'], [3, 'BC'], [3.5, 'TL'], [4, 'TR']], restAfter: 0 }, + { notes: [[1, 'BL'], [2, 'BC'], [2.5, 'BR'], [3, 'TL'], [4, 'BL']], restAfter: 0 }, + { notes: [[1, 'BC'], [2, 'BR'], [3, 'BC'], [3.5, 'TL'], [4, 'BL']], restAfter: 0 }, + { notes: [[1, 'BC'], [1.5, 'BL'], [2, 'BC'], [2.5, 'BL'], [3, 'BC'], [3.5, 'BC'], [4, 'TR']], fill: true, restAfter: 0 }, ], }, ]; -// 教学 5 步(T 组台词 + 复用 gd_answer「现在跟着我打」——上/下滑无专属台词, -// 零新语音资产;tut_3「最后一句」要保持为真,故上/下滑步插在其前) +// 教学 3 步(T0 摸鼓认区之后;每步领奏前有 1 小节底鼓数拍) const TUT_STEPS = [ - { voice: 'voice/tut_1', notes: [[1, 'tap']] }, - { voice: 'voice/tut_2', notes: [[1, 'left'], [3, 'right']] }, - { voice: 'voice/gd_answer', notes: [[1, 'up']] }, // 上滑 = 高通 tom - { voice: 'voice/gd_answer', notes: [[1, 'down']] }, // 下滑 = 低通 tom(下滑折中同款:临近时是演奏) - { voice: 'voice/tut_3', notes: [[1, 'tap'], [2, 'doubletap'], [3, 'tap']] }, + { voice: 'voice/tut_z_kick', notes: [[1, 'BC']] }, + { voice: 'voice/tut_z_sides', notes: [[1, 'BL'], [3, 'BR']] }, + { voice: 'voice/tut_z_patt', notes: [[1, 'TL'], [2, 'TC'], [3, 'TR']] }, ]; -const ALL_SFX = [...Object.values(INST), ACCENT]; +const ALL_SFX = [...new Set([...Object.values(ZONES).map((z) => z.sfx), ACCENT])]; const MENU_ITEMS = ['voice/menu_resume', 'voice/menu_lobby']; const GRADE_RANK = { s: 4, a: 3, b: 2, c: 1 }; -const PROGRESS_KEY = 'soundgame.rhythm.progress'; +const PROGRESS_KEY = 'soundgame.progress.rhythm'; // 引擎全项目约定(profiles.js/control 会话种子同款) const AUTH_KEY = 'soundgame.auth'; // 大厅登录写入(见 web/index.html) +// 键盘平权:Digit/Numpad 1-6 = 六区(引擎不拦截数字键,游戏层补充;按住重复抑制) +const KEY_ZONE = { + Digit1: 'TL', Digit2: 'TC', Digit3: 'TR', + Digit4: 'BL', Digit5: 'BC', Digit6: 'BR', + Numpad1: 'TL', Numpad2: 'TC', Numpad3: 'TR', + Numpad4: 'BL', Numpad5: 'BC', Numpad6: 'BR', +}; + export class RhythmGame extends Game { constructor(audio, root) { super(audio, root); this.guide = new Guide(audio); // 全局向导层(guide='off' 时全部静默 no-op) - this.phase = 'idle'; // idle | intro | tut | select | main | menu | end - this._runId = 0; // 重开/切歌时令旧的语音链/调度回调失效 + this.phase = 'idle'; // idle | intro | tut | select | main | menu | end | leaving + this._runId = 0; this._timer = null; - this._sched = []; // 待排程领奏事件 { time, sfx, volume } - this._timed = []; // 定时动作 { time, fn }(mid_song 等) - this._cues = []; // 应答期望 { gesture, expected, judged, result, phrase } + this._sched = []; // 待排程领奏/数拍事件 { time, sfx, volume, pan? } + this._timed = []; // 定时动作 { time, fn } + this._cues = []; // 应答期望 { zone, expected, judged, result, phrase } this._bgmSrc = null; this._bgmGain = null; - this._bgmT0 = 0; // 当前 BGM 网格起点(ctx 时间) - this._song = null; // 当前曲目对象 + this._bgmT0 = 0; + this._song = null; this._songIdx = 0; this._stats = null; this._progress = this._loadProgress(); // 教学状态 + this._tutStage = 'idle'; // probe | step(T0 摸鼓认区 | 跟打步) this._tutIdx = 0; this._tutFails = 0; this._tutAttempts = []; - this._tutRate = 1.0; // G2:教学领奏速率(0.75 = 降速 25%,做对/离开该步恢复 1.0) - this._tutSlowUsed = false; // G2:降速重演每教学局 1 次封顶 + this._tutRate = 1.0; + this._tutSlowUsed = false; + this._tutPassedAny = false; + this._probeHeard = new Set(); // T0 已报名过的区(每区每轮教学一次) // 菜单/暂停状态 this._menuIdx = 0; this._pauseAt = 0; this._bgmOffset = 0; - this._resumeSeq = 0; // G3:恢复令牌(count-in 语音期间再开菜单时作废旧恢复) - this._gdCountUsed = false; // G3:gd_count 每局(曲)恢复限 1 次 - this._answerArmed = false; // G1:gd_answer+turn 交接标记是否已在 _timed 里待响 - this._consecMiss = 0; // 连续 Miss 计数(向导 gd_miss:连 2 次提示一次) - // immediateTap(引擎 opt-in,index.html 已开启,按下即发 tap): - // true = tap 判定零补偿 + 非演奏阶段 tap 延迟 STRAY_TAP_MS 裁决(防序曲 tap - // 误触发);运行证据自动闩锁回退 false(onGesture 双击闩锁,见注释) - this._immediateTap = true; - this._lastTapEventAt = -Infinity; // 最近一次 tap 事件(performance.now() ms,闩锁证据用) - this._pendingTap = null; // 非演奏阶段延迟裁决的 tap / 教学期延迟裁决的跳句 - this._tutPassedAny = false; // 教学是否有任一步通过(零通过收尾换中性句,不夸"很好") - this._leadBus = null; // 领奏总线(提示语音 ducking 作用于此) + this._resumeSeq = 0; + this._gdCountUsed = false; + this._answerArmed = false; + this._consecMiss = 0; + this._pendingTap = null; // 非演奏阶段延迟裁决的 tap + this._probeTimer = null; // 探针态防呆自动进阶定时器 + this._tapJudg = []; // 序曲 tap 判定账簿(tapcancel 撤销用) + this._menuOpenedAt = -Infinity; + this._leadBus = null; this._midDuckTimer = null; this._goingHome = false; - this._voiceSeq = 0; // 语音链序号(_speakGuarded 滞留看门狗作废用,见该方法注释) - this._tapJudg = []; // 序曲 tap 判定账簿(tapcancel/doubletap 到达时撤销,live round2 P0) - this._menuOpenedAt = -Infinity; // 菜单打开时刻(performance.now ms,去抖独占期用) + this._voiceSeq = 0; + // 键盘平权(Digit1-6);mock 环境 window.addEventListener 为 no-op,安全 + if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') { + window.addEventListener('keydown', (e) => this._onKeyDown(e)); + } } // ---------------- 生命周期 ---------------- start() { this.audio.prewarm([...ALL_SFX, 'voice/welcome', 'voice/welcome_back', - 'voice/tut_1', 'voice/select_hint', 'voice/mid_song', 'bgm/menu', ...SONGS.map((s) => s.bgm)]); + 'voice/tut_map', 'voice/tut_probe', 'voice/select_hint', 'voice/mid_song', 'bgm/menu', + ...SONGS.map((s) => s.bgm)]); const rid = ++this._runId; this._cleanup(); this._tutRate = 1.0; - this._tutSlowUsed = false; // G2:每教学局重新封顶 - this._gdCountUsed = false; // G3:每局(曲)恢复限 1 次 + this._tutSlowUsed = false; + this._gdCountUsed = false; this._tutPassedAny = false; + this._probeHeard = new Set(); this._goingHome = false; if (this._progress.tutorialDone) { - // 二周目起:缩减开场 → 选曲 this.phase = 'intro'; this._speakGuarded(['voice/welcome_back'], () => this._enterSelect(rid)); } else { - // 首次:welcome + BGM(第 6 秒起)→ 教学 T1 + // 首次:welcome → 摸鼓认区(BGM 第 6 秒起,教学在曲目一网格上进行) this.phase = 'intro'; - this._startBgm(SONGS[0], this.audio.ctx.currentTime + 6.0); // 教学用曲目一 - this._speakGuarded(['voice/welcome'], () => this._beginTutorial(rid, 0), 8000); + this._startBgm(SONGS[0], this.audio.ctx.currentTime + 6.0); + this._speakGuarded(['voice/welcome', 'voice/tut_map', 'voice/tut_probe'], + () => this._beginProbe(rid), 14000); } } stop() { this._runId++; this._cleanup(); - this.audio.setRateBoostEnabled(true); // 离开演奏期,恢复长按快进 + this.audio.setRateBoostEnabled(true); this.audio.stopAll(); } _cleanup() { if (this._timer) { clearInterval(this._timer); this._timer = null; } if (this._pendingTap) { clearTimeout(this._pendingTap); this._pendingTap = null; } + if (this._probeTimer) { clearTimeout(this._probeTimer); this._probeTimer = null; } if (this._midDuckTimer) { clearTimeout(this._midDuckTimer); this._midDuckTimer = null; } this._duckLeads(false); - this._voiceSeq++; // 作废全部滞留语音链守卫 - this._tapJudg = []; // 序曲 tap 账簿随相位重置(跨相位不撤销) + this._voiceSeq++; + this._tapJudg = []; this._sched = []; this._timed = []; this._cues = []; @@ -284,15 +251,46 @@ export class RhythmGame extends Game { } } + // ---------------- 分区映射 ---------------- + + /** + * 触点 → 分区(24 文档 §2.1:3 列 × 2 行,列为三等分、行对半)。 + * @param {{x:number,y:number}|null|undefined} info 引擎第二参(视口像素) + * @returns {'TL'|'TC'|'TR'|'BL'|'BC'|'BR'|null} 无布局环境(mock)返回 null + */ + zoneOf(info) { + if (!info || !this.root || typeof this.root.getBoundingClientRect !== 'function') return null; + const r = this.root.getBoundingClientRect(); + if (!r || !r.width || !r.height) return null; + const col = info.x < r.left + r.width / 3 ? 0 : info.x < r.left + (2 * r.width) / 3 ? 1 : 2; + const row = info.y < r.top + r.height / 2 ? 0 : 1; + return [['TL', 'TC', 'TR'], ['BL', 'BC', 'BR']][row][col]; + } + + /** 敲击分发:payload 缺区(无布局环境)兜底 BC 家区。 */ + _zoneFrom(info) { + return this.zoneOf(info) || 'BC'; + } + + // ---------------- 键盘平权(Digit1-6 = 六区) ---------------- + + _onKeyDown(e) { + const zone = KEY_ZONE[e.code]; + if (!zone || e.repeat) return; + if (typeof e.preventDefault === 'function') e.preventDefault(); + // 演奏输入语境生效(探针态=摸鼓报名,跟打/正片=判定);选曲/菜单/结算态 + // 数字键不生效(那些态沿用手势导航) + if (this.phase === 'tut' && this._tutStage === 'probe') this._onProbe(zone); + else if (this.phase === 'main' || this.phase === 'tut') this._hit(zone); + } + // ---------------- BGM(手动 source 对齐节拍网格) ---------------- async _startBgm(song, t0, offset = 0) { const rid = this._runId; const buf = await this.audio._load(song.bgm); if (!buf || rid !== this._runId || !this.audio.ctx) return; - // 暂停发生在 BGM 起点前时 offset 为负:推迟起点而非传负偏移 - // (Web Audio 的 start(when, 负 offset) 会抛 IndexSizeError) - if (offset < 0) { t0 -= offset; offset = 0; } + if (offset < 0) { t0 -= offset; offset = 0; } // 负偏移:推迟起点 this._stopBgmSrc(); const src = this.audio.ctx.createBufferSource(); src.buffer = buf; @@ -303,14 +301,14 @@ export class RhythmGame extends Game { g.connect(this.audio._bgmBus); this._bgmSrc = src; this._bgmGain = g; - this._bgmT0 = t0 - offset; // 网格起点(offset 用于断点续播) + this._bgmT0 = t0 - offset; src.start(t0, offset % buf.duration); } // ---------------- 节拍调度泵 ---------------- - /** 在绝对时间 when 播放 sfx(走领奏总线 → 引擎 sfx 总线,保持音量/duck 语义)。 */ - async _scheduleSfx(id, when, volume = 1) { + /** 在绝对时间 when 播放 sfx(走领奏总线;zone 提供声像)。 */ + async _scheduleSfx(id, when, volume = 1, pan = 0) { const buf = await this.audio._load(id); if (!buf || !this.audio.ctx) return; const src = this.audio.ctx.createBufferSource(); @@ -318,11 +316,16 @@ export class RhythmGame extends Game { const g = this.audio.ctx.createGain(); g.gain.value = this.audio.sfxVolume * volume; src.connect(g); - g.connect(this._leadBusNode()); + const panner = this.audio._makePanner(pan); + if (panner) { + g.connect(panner); + panner.connect(this._leadBusNode()); + } else { + g.connect(this._leadBusNode()); + } src.start(when); } - /** 领奏总线(全部预排程音符经此接引擎 sfx 总线;提示语音期间整体 duck)。 */ _leadBusNode() { if (!this._leadBus) { this._leadBus = this.audio.ctx.createGain(); @@ -331,7 +334,7 @@ export class RhythmGame extends Game { return this._leadBus; } - /** 领奏 ducking(live round1 P2 提示语音压领奏):压到 1/4 / 恢复,200ms 渐变。 */ + /** 领奏 ducking:提示语音期间压到 1/4。 */ _duckLeads(on) { if (!this.audio.ctx || !this._leadBus) return; const t = this.audio.ctx.currentTime; @@ -341,7 +344,7 @@ export class RhythmGame extends Game { g.linearRampToValueAtTime(on ? 0.25 : 1.0, t + 0.2); } - /** mid_song 提示语音:播前把领奏 duck 到 1/4,语音播完(按buffer时长估算)恢复。 */ + /** mid_song 提示:播前领奏 duck 到 1/4,按估算时长恢复。 */ async _playMidSong() { if (!this.audio.ctx) return; this._duckLeads(true); @@ -350,12 +353,12 @@ export class RhythmGame extends Game { const speed = settings.get().speed || 1; const ms = buf ? Math.min(4500, Math.max(1200, (buf.duration / speed + 0.5) * 1000)) - : 2500; // 资产缺失兜底:固定时长后恢复 + : 2500; this._midDuckTimer = setTimeout(() => this._duckLeads(false), ms); this.audio.play('voice/mid_song'); } - /** Miss 反馈音:闭镲降调 0.5×(闷掉的踩镲;复用现有资产,零新文件)。 */ + /** Miss 反馈音:踩镲降调 0.5×(闷镲;复用资产零新文件)。 */ async _playMissSfx() { const buf = await this.audio._load(MISS_SFX); if (!buf || !this.audio.ctx) return; @@ -369,14 +372,7 @@ export class RhythmGame extends Game { src.start(); } - /** 手势识别补偿(判定时刻 = 回调时刻 - 补偿 ≈ 真实触摸时刻)。 */ - _compFor(g) { - if (g === 'tap') return this._immediateTap ? 0 : TAP_DELAY; - if (g === 'doubletap') return DOUBLE_DELAY; - return SWIPE_DELAY; // 四向滑动 - } - - /** 当前阶段的秒/拍(教学计降速;无曲目上下文按曲目一)。 */ + /** 当前秒/拍(教学计降速)。 */ _spb() { if (this.phase === 'tut') return (60 / SONGS[0].bpm) / this._tutRate; return this._song ? 60 / this._song.bpm : 60 / SONGS[0].bpm; @@ -386,29 +382,25 @@ export class RhythmGame extends Game { if (this._runId !== rid || !this.audio.ctx) return; const now = this.audio.ctx.currentTime; - // 排程未来 LOOKAHEAD 内的领奏音符(同步排一个对齐的 BEAT 轻震) while (this._sched.length && this._sched[0].time <= now + LOOKAHEAD) { const ev = this._sched.shift(); - this._scheduleSfx(ev.sfx, ev.time, ev.volume); + this._scheduleSfx(ev.sfx, ev.time, ev.volume, ev.pan || 0); const delay = Math.max(0, (ev.time - now) * 1000); setTimeout(() => { if (this._runId === rid) vibrate('beat'); }, delay); } - // 定时动作(mid_song 等) while (this._timed.length && this._timed[0].time <= now) { const ev = this._timed.shift(); ev.fn(); } if (this.phase === 'main') { - // Miss 超时检查:放宽 = 该手势的识别补偿 + 0.10s(补偿后仍超窗才判超时) for (const cue of this._cues) { if (cue.judged) continue; - const margin = this._compFor(cue.gesture) + 0.10; - if (now > cue.expected + GOOD + margin) this._applyMiss(cue); + if (now > cue.expected + GOOD + MISS_MARGIN) this._applyMiss(cue); } if (now >= this._phaseEnd) this._finish(rid); - } else if (this.phase === 'tut') { + } else if (this.phase === 'tut' && this._tutStage === 'step') { if (now >= this._phaseEnd) this._tutEvaluate(rid); } } @@ -423,59 +415,115 @@ export class RhythmGame extends Game { this._timed.sort((a, b) => a.time - b.time); } - // ---------------- 教学(T1-T3) ---------------- + /** 在绝对时刻排 1 小节 4 记数拍。正片用底鼓(guide.test G3 的 _sched 锚点), + * 教学步用木鱼 click(G2 的 inst_kick 锚点要求教学 _sched 里 kick=领奏音)。 + * BGM 无鼓,数拍全靠鼓手自己。 */ + _countIn(t0, spb, sfx = KICK, volume = 1.0) { + for (let i = 0; i < 4; i++) { + this._sched.push({ time: t0 + i * spb, sfx, volume, pan: 0 }); + } + } - _beginTutorial(rid, idx) { + // ---------------- 教学(T0 摸鼓认区 + T1-T3 跟打) ---------------- + + _beginProbe(rid) { + if (this._runId !== rid) return; + this.phase = 'tut'; + this._tutStage = 'probe'; + this._probeHeard = new Set(); + this.guide.state('explore'); // 状态签名:探索(摸鼓认区) + // 防呆自动进阶: narration 已播完才进探针。8s 无触摸 → 直接进 T1(不卡流程, + // guide.test G2 等无触摸驱动依赖此路径);摸过 1-2 区再给 8s 宽限,之后无论 + // 几区都开始(探索是礼物不是门槛)。 + const arm = (grace) => { + this._probeTimer = setTimeout(() => { + this._probeTimer = null; + if (this._runId !== rid || this.phase !== 'tut' || this._tutStage !== 'probe') return; + if (this._probeHeard.size === 0 && grace) { arm(false); return; } + this._tutStage = 'idle'; + this._beginTutorialStep(rid, 0); + }, 8000); + }; + arm(true); + } + + /** 清理探针自动进阶定时器(进阶/跳过/清理时)。 */ + _clearProbeTimer() { + if (this._probeTimer) { clearTimeout(this._probeTimer); this._probeTimer = null; } + } + + /** T0:任意 tap → 播该区音色 + 区名(每区每轮教学一次);≥3 区进 T1。 */ + _onProbe(zone) { + this.audio.play(ZONES[zone].sfx, { pan: ZONES[zone].pan }); + if (!this._probeHeard.has(zone)) { + this._probeHeard.add(zone); + this.audio.play(`voice/${ZONES[zone].name}`, { queue: true }); + if (this._probeHeard.size >= 3) { + this._clearProbeTimer(); + // R1 F2:不再 stopVoice 掐断正在播的区名——教学步语音 queue 排在 + // 区名链后播。延迟 150ms 起步:引擎 _playVoice 的入队判定在 await + // 加载之前(voice token 尚未挂上),同步连发会让教学句误判「通道 + // 空闲」而顶掉区名(live R1 实录 zone_bl 被 tut_z_kick 截杀)。 + this._tutStage = 'idle'; // 防止泵与后续 tap 再进探针分支 + setTimeout(() => { + if (this.phase === 'tut' && this._tutStage === 'idle') { + this._speakGuarded([], () => this._beginTutorialStep(this._runId, 0)); + } + }, 150); + } + } + } + + _beginTutorialStep(rid, idx) { if (this._runId !== rid) return; this._tutIdx = idx; this._tutFails = 0; - this._cues = []; // 步骤引导语音期间无 cue("语音播放中单击=跳句"的判定语境) + this._cues = []; this._tutAttempts = []; - this._speakGuarded([TUT_STEPS[idx].voice], () => this._tutLead(rid)); + this._speakGuarded([TUT_STEPS[idx].voice], () => this._tutLead(rid), 10000, true); } - /** 播教学领奏(对齐 BGM 网格的下一小节),应答 = 领奏 + 4 拍。 */ + /** 教学领奏:数拍 1 小节 + 领奏 1 小节(对齐 BGM 网格),应答 = 领奏 + 1 小节。 */ _tutLead(rid) { if (this._runId !== rid) return; this.phase = 'tut'; - const song = SONGS[0]; // 教学在曲目一 BGM 上进行 + this._tutStage = 'step'; + const song = SONGS[0]; const spb = 60 / song.bpm; const now = this.audio.ctx.currentTime; - // 下一小节头作为领奏小节(至少留 1.2s 缓冲,且不早于 BGM 网格起点) - const leadBar = this._bgmT0 + // 下一小节头起数拍(至少 1.2s 缓冲,不早于 BGM 网格起点;数拍 1 小节 + 领奏 1 小节) + const countBar = this._bgmT0 + Math.max(0, Math.ceil((now + 1.2 - this._bgmT0) / (4 * spb))) * 4 * spb; + const leadBar = countBar + 4 * spb; + const stepSpb = spb / this._tutRate; // G2:降速只拉伸领奏/应答网格 + this._countIn(countBar, spb, CLICK, 0.9); // 教学数拍=木鱼(kick 锚点留给领奏音) this._tutAttempts = []; this._cues = []; - // G2(round3):降速只拉伸该步的领奏/应答网格(间隔 ×1/0.75),领奏小节头 - // 仍对齐 BGM 小节;排程照走 _sched 绝对时间 + 40ms 泵,泵与判定无需感知速率。 - const stepSpb = spb / this._tutRate; - for (const [beat, g] of TUT_STEPS[this._tutIdx].notes) { + for (const [beat, z] of TUT_STEPS[this._tutIdx].notes) { const t = leadBar + (beat - 1) * stepSpb; - this._sched.push({ time: t, sfx: INST[g], volume: 1 }); - this._cues.push({ gesture: g, expected: t + 4 * stepSpb, judged: false, phrase: -1 }); + const zn = ZONES[z]; + this._sched.push({ time: t, sfx: zn.sfx, volume: 1, pan: zn.pan }); + this._cues.push({ zone: z, expected: t + 4 * stepSpb, judged: false, phrase: -1 }); } this._sortSched(); const last = this._cues[this._cues.length - 1]; - this._phaseEnd = last.expected + GOOD + TAP_DELAY + 0.4; + this._phaseEnd = last.expected + GOOD + 0.4; this._startPump(rid); } - /** 教学应答评估:全部音在窗口内用对手势 → 通过;否则纠正重教。 */ + /** 教学应答评估:全部音窗口内敲对区 → 通过;否则纠正重教。 */ _tutEvaluate(rid) { - if (this._runId !== rid || this.phase !== 'tut') return; - // 评估是一次性的:先废掉触发条件——通过/失败后都要播语音, - // 语音间隙期 phase 仍是 'tut' 且 now ≥ _phaseEnd,泵会每 40ms 重进这里, - // 否则一次通过会级联"连过"剩余步骤(trace 实测:T1 通过直接跳到结算)。 - this._phaseEnd = Infinity; + if (this._runId !== rid || this.phase !== 'tut' || this._tutStage !== 'step') return; + this._phaseEnd = Infinity; // 一次性(语音间隙期泵不会重入级联连过) const pass = this._cues.every((cue) => { const a = this._tutAttempts.find((x) => x.cue === cue); return a && a.correct && Math.abs(a.dt) <= GOOD; }); if (pass) { this._tutPassedAny = true; - this._tutRate = 1.0; // G2:该步做对,恢复正常速度 + this._tutRate = 1.0; if (this._tutIdx + 1 < TUT_STEPS.length) { - this._beginTutorial(rid, this._tutIdx + 1); + this._beginTutorialStep(rid, this._tutIdx + 1); } else { this._finishTutorial(rid); } @@ -484,14 +532,11 @@ export class RhythmGame extends Game { this._tutFails++; vibrate('error'); if (this._tutFails >= 2) { - // G2(round3 教学降难 DDA):同一步连续 2 次失败 → 该步领奏降速 25% 重演 - // (每教学局 1 次封顶;full 档先播 gd_slow「我放慢一些,跟着试试。」)—— - // P4「六连错无出路」:失败后第一次有变慢的路,而不是直接跳步。封顶用尽 - // 或 off 档(零改动红线)→ 维持 round2 的 retry_skip 跳步原路径。 + // G2 DDA:连 2 败 → 降速 25% 重演(每教学局 1 次;off 档维持跳步原路径) if (this.guide.level() !== 'off' && !this._tutSlowUsed) { this._tutSlowUsed = true; this._tutRate = 0.75; - this._tutFails = 0; // 降速重演 = 该步重新给两次机会(再连 2 失败才跳步) + this._tutFails = 0; if (this.guide.level() === 'full') { this._speakGuarded(['voice/gd_slow'], () => this._tutLead(rid)); } else { @@ -499,45 +544,34 @@ export class RhythmGame extends Game { } return; } - this._tutRate = 1.0; // 离开该步(跳步):降速只作用于该步 - // 同一步失败 2 次跳过,不卡流程(归因已在首次失败时给过,此处不再叠加) + this._tutRate = 1.0; this._speakGuarded(['voice/retry_skip'], () => { - if (this._tutIdx + 1 < TUT_STEPS.length) this._beginTutorial(rid, this._tutIdx + 1); + if (this._tutIdx + 1 < TUT_STEPS.length) this._beginTutorialStep(rid, this._tutIdx + 1); else this._finishTutorial(rid); }); return; } - // F2.1(round2)+ H5(round4):教学 Miss 首次即归因、按失败原因分档。 - // H5 归因真话律:应答窗口无任何尝试(窗口关闭时该音未判定——出窗的自由 - // 演奏不算尝试)优先播 gd_miss_none「这拍没接」,早/晚/错分档只保留给 - // 真出手的情况(P1 round3:没出手被报「慢了半拍」是归因说假话——"宁可粗 - // 不可假";正片连 2 Miss 仍走 gd_miss,本档只改教学归因)。 - // 向导句(full 档)挂在纠正链播完后的空闲点发:idle-only 语义下链中插播会被 - // 跳过;之后 _tutLead 只排 sfx,不与归因句抢 voice 通道。 + // H5 归因真话律:无窗内尝试优先 gd_miss_none;真出手再分早/晚/错区。 const noShow = this._cues.some((cue) => { const a = this._tutAttempts.find((x) => x.cue === cue); - return !a || Math.abs(a.dt) > GOOD; // 窗内无尝试 = 该音没接 + return !a || Math.abs(a.dt) > GOOD; }); const wrong = this._tutAttempts.find((a) => !a.correct); const offbeat = this._tutAttempts.find((a) => a.correct && Math.abs(a.dt) > GOOD); - const missGrade = noShow ? 'gd_miss_none' // none 优先级最高 + const missGrade = noShow ? 'gd_miss_none' : wrong ? 'gd_miss_wrong' : offbeat ? (offbeat.dt < 0 ? 'gd_miss_early' : 'gd_miss_late') : 'gd_miss_late'; - // 纠正语音:手势错 > 过早 > 过晚 > 没出手(retry_listen) let correction = 'voice/retry_listen'; - if (wrong) correction = 'voice/retry_gesture'; + if (wrong) correction = 'voice/retry_wrong'; else if (offbeat) correction = offbeat.dt < 0 ? 'voice/retry_early' : 'voice/retry_late'; - this._speakGuarded([correction, 'voice/retry_listen'], () => { + // R1 F3:归因句本身已含「再听」语义时不追加 retry_listen(防二连复读)。 + const chain2 = correction === 'voice/retry_listen' ? [correction] : [correction, 'voice/retry_listen']; + this._speakGuarded(chain2, () => { this.guide.error({ voice: missGrade, skipHaptic: true }); this._tutLead(rid); }); } - /** - * 教学收尾:有任一步通过播 tut_done「很好,演出开始」;全程零通过换中性句 - * retry_skip(live round1 P2:零命中被夸"很好"出戏——台词文本不变,只改 - * 触发逻辑)。 - */ _finishTutorial(rid) { this._progress.tutorialDone = true; this._saveProgress(); @@ -550,10 +584,9 @@ export class RhythmGame extends Game { _enterSelect(rid) { if (this._runId !== rid) return; this.phase = 'select'; - this.audio.setRateBoostEnabled(true); // 离开演奏期,恢复长按快进 - // 10 文档 §5:选曲界面 = menu pad(引擎层淡入 800ms;bgm=0 时引擎只记意图) + this.audio.setRateBoostEnabled(true); this.audio.play('bgm/menu', { fadeMs: 800 }); - this.guide.state('menu'); // 状态签名:选曲界面(与状态 BGM 同点) + this.guide.state('menu'); this._speak(['voice/select_hint', SONGS[this._songIdx].nameVoice]); } @@ -582,11 +615,10 @@ export class RhythmGame extends Game { _startSong(rid, songIdx) { if (this._runId !== rid) return; this._cleanup(); - this.audio.setRateBoostEnabled(false); // 演奏期豁免长按快进(12 文档 §2.2) - // 10 文档 §5:选定曲目后停 menu pad(选曲/结算态可能在播),再按网格起曲目 BGM + this.audio.setRateBoostEnabled(false); // 演奏期豁免长按快进 this.audio.stopBgm(400); this.phase = 'main'; - this.guide.state('battle'); // 状态签名:演奏/对抗(与状态 BGM 切换同点) + this.guide.state('battle'); this._songIdx = songIdx; this._song = SONGS[songIdx]; this._stats = { perfect: 0, good: 0, miss: 0, combo: 0, maxCombo: 0 }; @@ -594,57 +626,61 @@ export class RhythmGame extends Game { const spb = 60 / this._song.bpm; const now = this.audio.ctx.currentTime; - const bgmT0 = now + 1.0; // 1s 后 BGM 起,第 1 小节即数拍 + const bgmT0 = now + 1.0; this._startBgm(this._song, bgmT0); - // 排程:数拍 1 小节(BGM 内鼓点)→ 逐乐句(领奏 1 小节 + 应答 1 小节 + 休止) + // 数拍 1 小节(4 记底鼓)→ 逐乐句/加花(领奏 1 小节 + 应答 1 小节 + 休止) + this._countIn(bgmT0, spb); let bar = 1; this._cues = []; - const midPhrase = Math.floor(this._song.phrases.length / 2); - this._song.phrases.forEach((notes, i) => { + const midUnit = Math.floor(this._song.units.length / 2); + this._song.units.forEach((unit, i) => { const leadBarT0 = bgmT0 + bar * 4 * spb; - for (const [beat, g] of notes) { + for (const [beat, z] of unit.notes) { const t = leadBarT0 + (beat - 1) * spb; - this._sched.push({ time: t, sfx: INST[g], volume: 1 }); - this._cues.push({ gesture: g, expected: t + 4 * spb, judged: false, result: null, phrase: i }); + const zn = ZONES[z]; + this._sched.push({ time: t, sfx: zn.sfx, volume: 1, pan: zn.pan }); + this._cues.push({ zone: z, expected: t + 4 * spb, judged: false, result: null, phrase: i }); } - if (i === midPhrase) { - // 提示语音不压领奏(live round1 P2):经领奏总线 ducking 到 1/4 + if (unit.fill) { + // 加花:领奏小节头落 turn earcon(应答密集的预警) + this._timed.push({ time: leadBarT0, fn: () => this.guide.turn({}) }); + } + if (i === midUnit) { this._timed.push({ time: leadBarT0, fn: () => this._playMidSong() }); } if (i === 0) { - // 向导层(round3 G1):每次进 main,首个应答窗口**前整整 1 小节**(= 领奏 - // 小节头)落下 gd_answer+turn 交接标记。round2 F3 的排程点(leadBarT0 + - // 4*spb)其实是窗口起点本身——P4 实测(窗口 t+79~95,gd_answer t+93.7) - // 窗口开了才听到,起不到「快轮到你了」的预警作用。gd_answer 不走 guide - // 的 queue(对白在播时会排到链尾);语音 full 档直达,turn earcon standard - // 也发(off 全静不变)。mid_song 同款先例。 + // G1:首个应答窗口前整整 1 小节(= 领奏小节头)落 gd_answer+turn this._answerArmed = true; this._timed.push({ time: leadBarT0, fn: () => this._fireAnswerCue() }); } - bar += 2 + (this._song.restAfter[i] || 0); + if (unit.fill && this._songIdx === 0 && !this._progress.tutFillDone && i === this._song.units.length - 1) { + // 加花预告:曲目一终曲加花领奏前 1 小节,每账号一次 + this._timed.push({ + time: leadBarT0 - 4 * spb, + fn: () => { + this._progress.tutFillDone = true; + this._saveProgress(); + this.audio.play('voice/tut_fill'); + }, + }); + } + bar += 2 + (unit.restAfter || 0); }); this._sortSched(); const last = this._cues[this._cues.length - 1]; - this._phaseEnd = last.expected + GOOD + TAP_DELAY + 0.5; + this._phaseEnd = last.expected + GOOD + MISS_MARGIN + 0.5; this._startPump(rid); } - /** G1:gd_answer+turn 交接标记(语音 full 档直达,turn earcon standard 也发)。 */ _fireAnswerCue() { this._answerArmed = false; if (this.guide.level() === 'full') this.audio.play('voice/gd_answer'); this.guide.turn({}); } - /** - * G1:为「下一个未判定的应答窗口」排交接标记,落在窗口前整整 1 小节(= 该句 - * 领奏小节头;已迫近则尽早在 0.15s 内补上)。_startSong 已为乐句 0 排过;菜单 - * 恢复(也是一次「进 main」)时若旧标记已消费、且前面还有未判定的窗口,对 - * 恢复后的首个窗口重排同款标记。上一句结束后的下一句不适用(裁定原文)。 - */ _armAnswerCue(rid) { - if (this._answerArmed) return; // 旧标记仍在 _timed 里,随网格平移照常提前 1 小节响 + if (this._answerArmed) return; if (this._runId !== rid || !this._song || !this.audio.ctx) return; const now = this.audio.ctx.currentTime; const next = this._cues.find((c) => !c.judged && c.expected > now); @@ -658,39 +694,25 @@ export class RhythmGame extends Game { // ---------------- 手势与判定 ---------------- - onGesture(g) { + /** + * 手势入口(引擎坐标扩展后带 info 第二参,见 docs/web-engine-api.md §gesture)。 + * tap = 分区敲击(info 决定敲哪区);滑动 = 导航;双击在演奏期无角色(两下 + * tap 已各自成立);tapcancel 撤销序曲 tap 的判定认领。 + */ + onGesture(g, info) { const rid = this._runId; - // 旧引擎侦测闩锁:immediateTap 下双击前必有 ≤300ms 的序曲 tap(第一下按下即发); - // 旧引擎的双击会取消挂起的单击——双击事件前 350ms 内必无 tap 事件。据此一次性 - // 回退旧补偿与旧路径(index.html 未开 immediateTap 时兜底;反过来不闩)。 - if (g === 'doubletap' && this._immediateTap - && performance.now() - this._lastTapEventAt > 350) { - this._immediateTap = false; - } - if (g === 'tap') this._lastTapEventAt = performance.now(); - // tapcancel(引擎 immediateTap 补发:按下后转滑动/长按/触摸取消)——序曲 tap - // 的声音已出收不回,但判定必须撤销且不计 Miss(live round2 P0:爆音不进判定链)。 - // 非演奏阶段的延迟裁决 tap 同步作废——比"后续手势到达才作废"更早(慢滑在 - // 500ms 裁决窗内超 slop 即撤,选曲慢滑不再误触发"选定曲目")。 if (g === 'tapcancel') { if (this._pendingTap) { clearTimeout(this._pendingTap); this._pendingTap = null; } - if (this._isPlayPhase()) this._revokeTaps(TAPCANCEL_REVOKE_S, 1); + if (this._probeTimer) { clearTimeout(this._probeTimer); this._probeTimer = null; } + this._revokeTap(TAPCANCEL_REVOKE_S); return; } - // 双击序曲治理(tap+tap+doubletap 序列的判定归属):两次按下的序曲 tap - // 判定(含可能误认领的命中)一并撤销后,本次双击再正常判定 - if (g === 'doubletap' && this._immediateTap && this._isPlayPhase()) { - this._revokeTaps(DOUBLE_REVOKE_S, 2); - } - // immediateTap 序曲 tap 治理:非演奏阶段的 tap 可能是一次滑动/双击/长按的 - // 触摸起点——延迟 STRAY_TAP_MS 生效;窗口内来了任何其他手势说明那是序曲, - // 作废这次 tap(窗口 > longpressMs 450ms,长按也能抢先作废;否则选曲界面 - // 右滑切歌会先误触发"选定曲目") - if (g === 'tap' && this._immediateTap && !this._isPlayPhase()) { + if (g === 'tap' && !this._isPlayInput()) { + // 非演奏输入语境:序曲 tap 延迟裁决(防滑动/长按起点误触发) if (this._pendingTap) clearTimeout(this._pendingTap); this._pendingTap = setTimeout(() => { this._pendingTap = null; - this._dispatch(rid, 'tap'); + this._dispatch(rid, 'tap', info); }, STRAY_TAP_MS); return; } @@ -698,262 +720,179 @@ export class RhythmGame extends Game { clearTimeout(this._pendingTap); this._pendingTap = null; } - this._dispatch(rid, g); + this._dispatch(rid, g, info); } - /** 演奏阶段(tap 是 kick 演奏输入,即刻生效不延迟裁决)。 */ - _isPlayPhase() { - return this.phase === 'tut' || this.phase === 'main'; + /** 演奏输入语境(摸鼓探针、教学跟打步、正片;导航态不算)。 */ + _isPlayInput() { + return this.phase === 'main' || this.phase === 'tut'; } - /** 手势分派(onGesture 已完成序曲 tap 裁决与旧引擎闩锁)。 */ - _dispatch(rid, g) { + _dispatch(rid, g, info) { switch (this.phase) { case 'tut': - // 下滑折中(教学同款):临近 down 音符时是演奏,否则下滑 = 跳过教学 - if (g === 'down' && !this._downNotePending()) { + if (this._tutStage === 'probe') { + if (g === 'down') { // 下滑跳过教学 + this._clearProbeTimer(); + this.audio.stopVoice(); // 清排队区名,防 tut_skip 播完后漏响 + this._progress.tutorialDone = true; + this._saveProgress(); + this._speakGuarded(['voice/tut_skip'], () => this._startSong(rid, 0)); + return; + } + // 探针态单击 = 摸鼓(区名 queue 串播不互掐;探针态无旁白可跳—— + // 引导语音都在 intro 阶段播完才进来) + if (g === 'tap') this._onProbe(this._zoneFrom(info)); + return; + } + // step:语音播放中单击 = 跳句;无语音时才是跟打敲击 + if (g === 'tap' && this.audio.isVoicePlaying()) { + this.audio.skipVoice(); + return; + } + if (g === 'down') { // 教学期下滑 = 跳过教学(无 down 音符冲突) this._progress.tutorialDone = true; this._saveProgress(); this._speakGuarded(['voice/tut_skip'], () => this._startSong(rid, 0)); return; } - // 教学语音播放中单击 = 跳过本句(12 文档 §1.2);无语音时才是跟打手势。 - // immediateTap 下跳句延迟裁决(序曲 tap 不跳句),演奏部分照常即时处理 - if (g === 'tap' && this.audio.isVoicePlaying()) { - if (!this._immediateTap) { this.audio.skipVoice(); return; } - if (this._pendingTap) clearTimeout(this._pendingTap); - this._pendingTap = setTimeout(() => { - this._pendingTap = null; - if (this._runId === rid && this.phase === 'tut' && this.audio.isVoicePlaying()) { - this.audio.skipVoice(); - } - }, STRAY_TAP_MS); - } - this._onTutGesture(g); + if (g === 'tap') this._hit(this._zoneFrom(info)); return; case 'select': - if (this._trySkipVoice(g)) return; // 选曲播报可逐句跳过 + if (this._trySkipVoice(g)) return; this._onSelectGesture(rid, g); return; case 'main': - // 演奏期单击是演奏手势,永不跳过(mid_song 短句按裁定不可跳) - this._onMainGesture(rid, g); + if (g === 'down') { this._openMenu(rid); return; } // 无条件(音符全是点按) + if (g === 'tap') { this._hit(this._zoneFrom(info)); return; } + if (g === 'longpress') { this._playMissSfx(); vibrate('error'); return; } // 死输入给定义 + if (g === 'longpressend') return; return; case 'menu': this._onMenuGesture(rid, g); return; case 'end': - if (this._trySkipVoice(g)) return; // 结算播报可逐句跳过 - if (g === 'tap') this._startSong(rid, this._songIdx); // 再来一遍(同曲) - else if (g === 'right') { // 下一首(若已解锁) + if (this._trySkipVoice(g)) return; + if (g === 'tap') this._startSong(rid, this._songIdx); + else if (g === 'right') { if (this._songIdx + 1 < this._progress.unlocked) this._startSong(rid, this._songIdx + 1); - else if (this._songIdx + 1 >= SONGS.length) this._speak(['voice/restart']); // 已是最后一首(策划裁定 round3-4-review) + else if (this._songIdx + 1 >= SONGS.length) this._speak(['voice/restart']); else this._speak(['voice/song_locked']); } else if (g === 'down') this._goHome(); return; default: - this._trySkipVoice(g); // intro:只收"单击跳旁白",其余手势不收 + this._trySkipVoice(g); // intro/leaving:只收单击跳旁白 return; } } - /** - * voice 播放中单击 = 跳过当前句(12 文档 §1.2,链播继续到下一句)。 - * 用引擎公开的 isVoicePlaying() 判定(原读私有 _voice,清点 §0)。 - * @returns {boolean} true = 该手势已消费(跳过了一句) - */ _trySkipVoice(g) { if (g !== 'tap' || !this.audio.isVoicePlaying()) return false; this.audio.skipVoice(); return true; } - /** 教学阶段手势:全量记录尝试(评估用),乐器音照播(认识音色)。 */ - _onTutGesture(g) { - if (!INST[g] || !this.audio.ctx) return; - this.audio.play(INST[g]); - const t = this.audio.ctx.currentTime - this._compFor(g); // 判定回到触摸时刻 + // ---------------- 敲击与判定 ---------------- + + /** + * 分区敲击(tap / 键盘数字键共用)。判定时刻 = 回调/按键时刻(零补偿)。 + * 窗口外 = 自由演奏(出声不判定);教学步全程记录尝试供评估与归因。 + */ + _hit(zone) { + if (!this.audio.ctx || !ZONES[zone]) return; + const zn = ZONES[zone]; + const t = this.audio.ctx.currentTime; + const inTut = this.phase === 'tut'; + + // 归属①:同区最近未判定音符 let best = null; let bestDt = Infinity; for (const cue of this._cues) { + if (cue.judged || cue.zone !== zone) continue; const dt = Math.abs(t - cue.expected); if (dt < bestDt) { bestDt = dt; best = cue; } } - // tap 只认领 tap 音符的尝试(immediateTap 序曲 tap 不算"做错手势") - if (best && (g !== 'tap' || best.gesture === 'tap')) { - const att = { cue: best, gesture: g, dt: t - best.expected, correct: g === best.gesture }; - this._tutAttempts.push(att); - if (g === 'tap') this._recordTapJudg(null, false, att); // tapcancel 可撤 + if (best && bestDt <= GOOD) { + this.audio.play(zn.sfx, { pan: zn.pan }); + const perfect = bestDt <= PERFECT; + if (perfect) this.audio.play(ACCENT, { volume: 0.8 }); + this._recordTapJudg(best, perfect); + if (inTut) this._tutAttempts.push({ cue: best, dt: t - best.expected, correct: true }); + this._applyHit(best, perfect); + return; + } + // 归属②:异分区音符半窗内被敲 = 错鼓 Miss + let wrong = null; + let wrongDt = Infinity; + for (const cue of this._cues) { + if (cue.judged || cue.zone === zone) continue; + const dt = Math.abs(t - cue.expected); + if (dt < wrongDt) { wrongDt = dt; wrong = cue; } + } + if (wrong && wrongDt <= GOOD * 0.5) { + this.audio.play(zn.sfx, { pan: zn.pan }); // 自由演奏语义:声照出 + if (inTut) this._tutAttempts.push({ cue: wrong, dt: t - wrong.expected, correct: false }); + this._recordTapJudg(null, false); + this._applyMiss(wrong, 'wrong'); + return; + } + this.audio.play(zn.sfx, { pan: zn.pan }); // 窗口外自由演奏 + this._recordTapJudg(null, false); + if (inTut) { + // 教学:最近未判定音符(不限区)记一次尝试,供「没接/抢拍/慢拍」归因 + let near = null; + let nearDt = Infinity; + for (const cue of this._cues) { + if (cue.judged) continue; + const dt = Math.abs(t - cue.expected); + if (dt < nearDt) { nearDt = dt; near = cue; } + } + if (near && nearDt <= GOOD * 2) { + this._tutAttempts.push({ cue: near, dt: t - near.expected, correct: near.zone === zone }); + } } } - /** - * 序曲 tap 判定账簿(live round2 P0):immediateTap 下每次按下先发 tap, - * 该 tap 的判定先记账;若本次按下最终转滑动/长按(引擎补 tapcancel)或 - * 改判双击(doubletap 到达),按账撤销——声音已出收不回,判定/统计必须 - * 更正且不计 Miss。cue=null 表示自由演奏(无判定可撤,仅登记供对账)。 - */ - _recordTapJudg(cue, perfect, tutAttempt = null) { + /** 序曲 tap 判定账簿:tapcancel 到达时按账撤销(声音已出收不回)。 */ + _recordTapJudg(cue, perfect) { this._tapJudg.push({ - at: this.audio.ctx.currentTime, cue, perfect, tutAttempt, + at: this.audio.ctx.currentTime, cue, perfect, prevCombo: this._stats ? this._stats.combo : 0, prevConsec: this._consecMiss, }); if (this._tapJudg.length > 8) this._tapJudg.shift(); } - /** 撤销最近 maxCount 笔、账龄 ≤ maxAgeS 的 tap 判定(新→旧,超龄即停)。 */ - _revokeTaps(maxAgeS, maxCount = 1) { + _revokeTap(maxAgeS) { if (!this.audio.ctx) return; const now = this.audio.ctx.currentTime; - let n = 0; - for (let i = this._tapJudg.length - 1; i >= 0 && n < maxCount; i--) { + for (let i = this._tapJudg.length - 1; i >= 0; i--) { const j = this._tapJudg[i]; - if (now - j.at > maxAgeS) break; // 账簿按时间升序,更旧的与本手势无关 + if (now - j.at > maxAgeS) break; this._tapJudg.splice(i, 1); - n++; - if (j.tutAttempt) { - const k = this._tutAttempts.indexOf(j.tutAttempt); - if (k >= 0) this._tutAttempts.splice(k, 1); - } if (j.cue && j.cue.judged && j.cue.result !== 'miss' && this._stats) { - // 序曲 tap 曾认领的命中撤销:音符回到未判定(真不接则由超时 Miss 兜底) j.cue.judged = false; j.cue.result = null; if (j.perfect) this._stats.perfect--; else this._stats.good--; this._stats.combo = j.prevCombo; this._consecMiss = j.prevConsec; } + break; // 一次 tapcancel 只撤销一笔(引擎每按下一发改判只补发一次) } } - /** - * 同手势音符刚被超时判 Miss(识别延迟把音符拖死、手势还在路上)——找最近 - * 一个 REVIVE_WINDOW_S 内 timeout 类 Miss 的同手势 cue;只认超时类(错手势 - * 类是真错误,不复活)。tap 不需要复活:immediateTap 下 tap 识别零延迟, - * 回调晚于死线 = 触摸本身已晚(且序曲 tap 不得复活音符——双击序曲会误复活)。 - */ - _recentTimeoutMiss(g) { - if (!this.audio.ctx || g === 'tap') return null; - const now = this.audio.ctx.currentTime; - let best = null; - let bestDt = Infinity; - for (const cue of this._cues) { - if (!cue.judged || cue.result !== 'miss' || cue.missKind !== 'timeout' - || cue.gesture !== g) continue; - if (now - cue.missedAt > REVIVE_WINDOW_S) continue; - const dt = Math.abs(now - cue.expected); - if (dt < bestDt) { bestDt = dt; best = cue; } - } - return best; - } - - /** 复活超时误判:统计回滚、音符回到未判定(闷镲 Miss 音已出收不回)。 */ - _reviveMiss(cue) { - this._stats.miss--; - this._stats.combo = cue.comboBefore; - this._consecMiss = cue.consecBefore; - cue.judged = false; - cue.result = null; - } - - _onMainGesture(rid, g) { - if (!this.audio.ctx) return; - // 长按 = 死输入给定义(live round1 P0):Miss 反馈音 + error 震动,不判定不归属 - if (g === 'longpress') { this._playMissSfx(); vibrate('error'); return; } - if (g === 'longpressend') return; - // 下滑折中:有悬置/临近的 down 音符时是演奏,否则呼出菜单。 - // live round2 P1 续修:down 音符刚被超时判 Miss(识别延迟拖死、下滑还在路上) - // 同样算"临近"——旧版此时必弹菜单(round2 事件流 3/3 复现:超时 Miss 与下滑 - // 回调相差 2~114ms,_downNotePending 已判假)。aimed=true 期间下滑永不弹菜单。 - let downAimed = false; - if (g === 'down') { - downAimed = !!this._recentTimeoutMiss('down'); - if (!this._downNotePending() && !downAimed) { - this._openMenu(rid); - return; - } - } - if (!INST[g]) return; - const t = this.audio.ctx.currentTime - this._compFor(g); // 判定回到触摸时刻 - - // 归属①:同手势的最近未判定音符优先(防密集谱跨手势抢占——同句 0.5 拍相邻 - // 的两个异手势音符,按正确手势略偏时刻出手不得被邻音偷走判定) - let best = null; - let bestDt = Infinity; - for (const cue of this._cues) { - if (cue.judged || cue.gesture !== g) continue; - const dt = Math.abs(t - cue.expected); - if (dt < bestDt) { bestDt = dt; best = cue; } - } - if (best && bestDt <= GOOD) { - // Perfect / Good:出手即播乐器音(演奏感),Perfect 叠 accent 亮音 - const perfect = bestDt <= PERFECT; - this.audio.play(INST[g]); - if (perfect) this.audio.play(ACCENT, { volume: 0.8 }); - if (g === 'tap') this._recordTapJudg(best, perfect); - this._applyHit(best, perfect); - return; - } - // 归属①′(live round2 P0):同手势音符刚被超时拖死 → 复活改判。 - // 事件流实证:双击识别 +0.2s/滑动 +0.3s,超时死线 expected+0.60~0.65s 常在 - // 手势到达前几十 ms 先判 Miss;补偿后 dt 仍在 GOOD+0.15 内 = 触摸其实在线内 - const revive = this._recentTimeoutMiss(g); - if (revive && Math.abs(t - revive.expected) <= REVIVE_DT_S) { - this._reviveMiss(revive); - const perfect = Math.abs(t - revive.expected) <= PERFECT; - this.audio.play(INST[g]); - if (perfect) this.audio.play(ACCENT, { volume: 0.8 }); - this._applyHit(revive, perfect); - return; - } - // 归属②:最近的异手势音符落在错手势窗(GOOD 的一半)内 = 窗口内做错动作 - // = Miss(不播该音符的乐器音)。tap 豁免——immediateTap 下每次滑动/双击的 - // 触摸起点先发序曲 tap,若认领异手势 Miss,每次滑动前那声 tap 都会把目标 - // 音符误判掉;真正的错手势 tap 由该音符超时自动 Miss 兜底(同样有 Miss 音)。 - // downAimed 同样豁免——冲着刚拖死的 down 音符去的下滑,不得顺杀邻音。 - let wrong = null; - let wrongDt = Infinity; - if (g !== 'tap' && !downAimed) { - for (const cue of this._cues) { - if (cue.judged || cue.gesture === g) continue; - const dt = Math.abs(t - cue.expected); - if (dt < wrongDt) { wrongDt = dt; wrong = cue; } - } - } - if (wrong && wrongDt <= GOOD * 0.5) { - this._applyMiss(wrong, 'wrong'); - return; - } - this.audio.play(INST[g]); // 窗口外自由演奏:出声音符但不判定 - if (g === 'tap') this._recordTapJudg(null, false); - } - - /** - * 当前有无悬置或临近的 down 应答音符(live round1 P0 加宽:自该音符领奏响起 - * ——expected-4 拍,玩家已有出手理由——到判定死线之间都算临近。旧窗口 - * (expected-1s ~ +0.55s)跟不上滑动识别延迟:识别在 touchend,踩点下滑到达 - * 时音符常已被判超时,于是必弹菜单)。 - */ - _downNotePending() { - if (!this.audio.ctx) return false; - const now = this.audio.ctx.currentTime; - const spb = this._spb(); - return this._cues.some((cue) => !cue.judged && cue.gesture === 'down' - && cue.expected > now - GOOD - SWIPE_DELAY - 0.10 - && cue.expected - 4 * spb < now + 0.2); - } - _applyHit(cue, perfect) { if (cue.judged) return; cue.judged = true; cue.result = perfect ? 'perfect' : 'good'; - this._consecMiss = 0; // 打上了就断开连续 Miss 链 + this._consecMiss = 0; const s = this._stats; - if (perfect) s.perfect++; else s.good++; - s.combo++; - if (s.combo > s.maxCombo) s.maxCombo = s.combo; - if (s.combo % 16 === 0) vibrate('success'); // 16 连击 + if (s) { + if (perfect) s.perfect++; else s.good++; + s.combo++; + if (s.combo > s.maxCombo) s.maxCombo = s.combo; + if (s.combo % 16 === 0) vibrate('success'); + } this._checkPhraseAllPerfect(cue.phrase); } @@ -961,32 +900,29 @@ export class RhythmGame extends Game { if (cue.judged) return; cue.judged = true; cue.result = 'miss'; - // 复活账(live round2 P0):超时类 Miss 可能是识别延迟拖死的误判——手势 - // 到达时若补偿时刻仍在线内则 _reviveMiss 回滚;kind=wrong 的真错误不复活 cue.missKind = kind; - cue.missedAt = this.audio.ctx ? this.audio.ctx.currentTime : 0; - cue.comboBefore = this._stats.combo; + cue.comboBefore = this._stats ? this._stats.combo : 0; cue.consecBefore = this._consecMiss; - this._stats.miss++; - this._stats.combo = 0; - // Miss 不再无声(live round1 P1):该音符的乐器音仍不播(错过=没出声), - // 但给可区分的闷镲反馈音 + error 震动 + if (this._stats) { + this._stats.miss++; + this._stats.combo = 0; + } this._playMissSfx(); vibrate('error'); - // 向导层(schemes §2):连续 2 次 Miss 播 gd_miss(同曲限 1 次;已震过 error, - // skipHaptic 免重复震;voice 走 queue 插播,不顶 mid_song/结算链) this._consecMiss++; if (this._consecMiss === 2) { - this.guide.error({ voice: 'gd_miss', onceKey: 'rhythm_miss', skipHaptic: true }); + // R1 F1:曲尾 3s 内不再插播——否则一句「慢了半拍。」播 1.1s 就被结算 + // 评级语音顶掉(live A 实录半句截断)。宁可不说,不说半句。 + const remain = this._phaseEnd && this.audio.ctx ? this._phaseEnd - this.audio.ctx.currentTime : 99; + if (remain > 3) this.guide.error({ voice: 'gd_miss', onceKey: 'rhythm_miss', skipHaptic: true }); } this._checkPhraseAllPerfect(cue.phrase); } - /** 乐句全部 Perfect:乐句末震 [20,40,20]。 */ _checkPhraseAllPerfect(phraseIdx) { - if (phraseIdx < 0) return; + if (phraseIdx < 0 || !this._stats) return; const notes = this._cues.filter((c) => c.phrase === phraseIdx); - if (notes.every((c) => c.judged) && notes.every((c) => c.result === 'perfect')) { + if (notes.length && notes.every((c) => c.judged) && notes.every((c) => c.result === 'perfect')) { vibrate([20, 40, 20]); } } @@ -997,27 +933,20 @@ export class RhythmGame extends Game { if (this._runId !== rid) return; this.phase = 'menu'; this._menuIdx = 0; - this._menuOpenedAt = performance.now(); // 去抖独占期起点(live round2 P1 瞬穿) - this.audio.setRateBoostEnabled(true); // 暂停期不是演奏期,恢复长按快进 - this.guide.state('menu'); // 状态签名:菜单(模态暂停) - // 暂停:停泵、停 BGM(记断点),已排程的 ≤0.6s 音符自然播完; - // 提示语音 ducking 一并复位(恢复后领奏不再被压) + this._menuOpenedAt = performance.now(); + this.audio.setRateBoostEnabled(true); + this.guide.state('menu'); if (this._timer) { clearInterval(this._timer); this._timer = null; } if (this._midDuckTimer) { clearTimeout(this._midDuckTimer); this._midDuckTimer = null; } this._duckLeads(false); - this._resumeSeq++; // G3:作废还在等 gd_count 播完的旧恢复 + this._resumeSeq++; this._pauseAt = this.audio.ctx.currentTime; - // 断点续播偏移只在 BGM 真在播时刷新(count-in 语音期间再开菜单时 BGM 未起, - // 沿用首次开的偏移,避免把暂停时长算进播放位置) if (this._bgmSrc) this._bgmOffset = this._pauseAt - this._bgmT0; this._stopBgmSrc(); this._speak(['voice/menu_main', MENU_ITEMS[0]]); } _onMenuGesture(rid, g) { - // 去抖独占期(live round2 P1):菜单打开 1s 内不收任何手势——演奏途中被 - // 弹菜单时,玩家已经在路上的手势(双击/下滑)不得直接穿透菜单当成菜单 - // 操作(实测 menu_main 说明语音 0.6s 未读完菜单就被"确认"没了) if (performance.now() - this._menuOpenedAt < MENU_DEBOUNCE_MS) return; if (g === 'tap') { this._menuIdx = (this._menuIdx + 1) % MENU_ITEMS.length; @@ -1035,39 +964,26 @@ export class RhythmGame extends Game { _closeMenu(rid) { if (this._runId !== rid) return; this.phase = 'main'; - this.audio.setRateBoostEnabled(false); // 回到演奏期 - // 关菜单先停菜单语音链(live round1 P0:menu_main/menu_resume 曾继续骑在 - // 恢复后的数拍与领奏上,是"菜单不暂停"感知的主因) - this.audio.stopVoice(); - const token = ++this._resumeSeq; // G3:恢复令牌(语音等待期间再开菜单则作废) + this.audio.setRateBoostEnabled(false); + this.audio.stopVoice(); // 关菜单先停菜单语音链 + const token = ++this._resumeSeq; const resume = () => { if (this._runId !== rid || token !== this._resumeSeq) return; const resumeAt = this.audio.ctx.currentTime + 0.1; - // G3(round3):断点续播前插一整小节 4 拍 count-in——底鼓走 sfx 层独立于 - // BGM、音量提到可辨(0.7→1.0),每拍带 beat 轻震(_sched 泵同步排),网格 - // 与 BGM 整体后移一小节,杜绝 P3 实测的「回来 5 秒死寂、第一句报废」。 - // round2 F3 的 count-in 实际从未按时响过:底鼓 push 进 _sched 后没重排, - // 队头是数秒外的领奏音符,40ms 泵的 0.6s 前瞻窗口永远轮不到它——本次修复 - // push 后 _sortSched()。off 档保持原即时续播(零改动路径)。 + // G3:断点续播前插 1 小节 4 拍 count-in(网格与 BGM 整体后移) const countIn = this.guide.level() !== 'off' ? 4 * (60 / this._song.bpm) : 0; const bgmAt = resumeAt + countIn; const shift = bgmAt - this._pauseAt; - // 全部绝对时刻平移(暂停时长 + count-in 小节),节拍网格与 BGM 断点续播保持同步 for (const ev of this._sched) ev.time += shift; for (const ev of this._timed) ev.time += shift; for (const cue of this._cues) cue.expected += shift; this._phaseEnd += shift; - for (let i = 0; i < 4 && countIn > 0; i++) { - this._sched.push({ time: resumeAt + i * (countIn / 4), sfx: INST.tap, volume: 1.0 }); - } - this._sortSched(); // G3 修复:count-in 必须重排入队,否则被队头阻塞整段迟到 - // _startBgm 内部重算 _bgmT0 = bgmAt - offset = 旧网格 + shift(负偏移保护同款成立) + if (countIn > 0) this._countIn(resumeAt, 60 / this._song.bpm, KICK, 1.0); + this._sortSched(); this._startBgm(this._song, bgmAt, this._bgmOffset); this._startPump(rid); - this._armAnswerCue(rid); // G1:菜单恢复也是一次「进 main」,给下一个窗口补交接标记 + this._armAnswerCue(rid); }; - // G3:gd_count「四拍之后,继续。」full 档、每局(曲)恢复限 1 次,在 count-in - // 之前经语音链串行播出(链完或缺失看门狗兜底后才开始数拍)。 if (this.guide.level() === 'full' && !this._gdCountUsed) { this._gdCountUsed = true; this._speakGuarded(['voice/gd_count'], resume); @@ -1080,9 +996,7 @@ export class RhythmGame extends Game { if (this._goingHome) return; this._goingHome = true; const rid = this._runId; - this.phase = 'leaving'; // 收尾态:不再受理演奏/菜单手势(单击仍可跳确认句) - // 先停谱面与 BGM(保留 voice 通道播确认句);确认句播完(或看门狗兜底) - // 再跳大厅——live round1 P1:旧版无确认直接跳,玩家面对静默页面以为游戏死了 + this.phase = 'leaving'; if (this._timer) { clearInterval(this._timer); this._timer = null; } this._stopBgmSrc(); this.audio.stopBgm(300); @@ -1108,12 +1022,11 @@ export class RhythmGame extends Game { _finish(rid) { if (this._runId !== rid) return; this.phase = 'end'; - this.guide.state('settle'); // 状态签名:结算(与结算 BGM 同点) - this.audio.setRateBoostEnabled(true); // 离开演奏期,恢复长按快进 + this.guide.state('settle'); + this.audio.setRateBoostEnabled(true); if (this._timer) { clearInterval(this._timer); this._timer = null; } this._sched = []; this._timed = []; - // BGM 淡出 if (this._bgmSrc && this.audio.ctx) { const now = this.audio.ctx.currentTime; this._bgmGain.gain.cancelScheduledValues(now); @@ -1129,7 +1042,6 @@ export class RhythmGame extends Game { const p = this._progress; const ids = [`voice/grade_${grade}`]; - // 评级与连击纪录 const prevGrade = p.grades[this._song.id]; if (!prevGrade || GRADE_RANK[grade] > GRADE_RANK[prevGrade]) { p.grades[this._song.id] = grade; @@ -1139,7 +1051,6 @@ export class RhythmGame extends Game { p.bestCombo = this._stats.maxCombo; ids.push('voice/new_best_combo'); } - // 解锁:曲目 n ≥ B → 解锁 n+1 let unlockedNew = null; if (GRADE_RANK[grade] >= GRADE_RANK.b && this._songIdx + 1 < SONGS.length @@ -1148,7 +1059,7 @@ export class RhythmGame extends Game { unlockedNew = SONGS[this._songIdx + 1].unlockVoice; } this._saveProgress(); - this._putProgress(); // 已登录时同步服务器 + this._putProgress(); if (grade === 's' || grade === 'a') vibrate('success'); if (unlockedNew) { @@ -1158,11 +1069,6 @@ export class RhythmGame extends Game { } ids.push('voice/restart'); console.log('[RhythmGame] 结算:', this._stats, '评级:', grade.toUpperCase()); - // 10 文档 §5:曲目 BGM 已 0.6s 淡出,结算语音链播完后淡入 menu pad 800ms; - // 链换 _speakGuarded(清点 §4.6 缺口):grade_* 等资产缺失也能起结算 BGM。 - // live round1 P1:① 看门狗按链长缩放——固定 10s 曾比 grade+restart 链还短, - // menu pad 骑到语音链尾上(报告 P3-15 同因);② 只在仍处结算态起播—— - // 重开/下一首后过期的看门狗 fire 不得把 menu pad 漏进正片 this._speakGuarded(ids, () => { if (this.phase === 'end') this.audio.play('bgm/menu', { fadeMs: 800 }); }, 5000 + ids.length * 7000); @@ -1179,10 +1085,11 @@ export class RhythmGame extends Game { unlocked: Math.max(1, Math.min(3, p.unlocked || 1)), grades: p.grades || {}, bestCombo: p.bestCombo || 0, + tutFillDone: !!p.tutFillDone, }; } } catch (_) { /* 损坏则重置 */ } - return { tutorialDone: false, unlocked: 1, grades: {}, bestCombo: 0 }; + return { tutorialDone: false, unlocked: 1, grades: {}, bestCombo: 0, tutFillDone: false }; } _saveProgress() { @@ -1192,7 +1099,6 @@ export class RhythmGame extends Game { /** PUT /api/progress(未登录/离线静默跳过)。summary 取最高评级曲目。 */ _putProgress() { const p = this._progress; - // 最高评级的曲目作为 summary 主项 let bestSong = this._songIdx; let bestRank = 0; SONGS.forEach((song, i) => { @@ -1201,7 +1107,7 @@ export class RhythmGame extends Game { }); const gradeText = bestRank ? `评级${{ 1: 'C', 2: 'B', 3: 'A', 4: 'S' }[bestRank]}` : '未评级'; const data = { - summary: `曲目${bestSong + 1} ${gradeText} / 已解锁 ${p.unlocked}/3 首`, + summary: `《${SONGS[bestSong].title}》${gradeText} · 已解锁${p.unlocked}/3首`, unlocked: p.unlocked, grades: p.grades, bestCombo: p.bestCombo, @@ -1227,34 +1133,25 @@ export class RhythmGame extends Game { /** * 语音串行链:自然播完才接下一句;被 stopVoice 打断则整条作废。 - * 链序号:每条新链(_chainSeq 缺省)分配新序号并作废更旧的链守卫 - * (见 _speakGuarded);链内续播沿用同一序号,不算"更新的链"。 */ - _speak(ids, onDone, _chainSeq) { + _speak(ids, onDone, _chainSeq, queue = false) { if (!ids.length) { if (onDone) onDone(); return; } const rid = this._runId; const seq = _chainSeq ?? ++this._voiceSeq; const [head, ...rest] = ids; this.audio.play(head, { + queue, onComplete: () => { if (rid !== this._runId) return; - this._speak(rest, onDone, seq); + this._speak(rest, onDone, seq, queue); }, }); } - /** - * 带看门狗的语音链:链自然播完或 guardMs 超时(语音缺失/解码失败), - * 二者先到先触发 onDone 且只触发一次——保证流程绝不卡死。 - * live round1 深修:看门狗只在"自己这条链仍是当前链"时才可 fire—— - * 链被顶掉/打断(stopVoice、更新的 _speak/_speakGuarded、_cleanup)后, - * 滞留看门狗若照常在 guardMs 后 fire,会把 onDone(如 _tutLead)砸进 - * 毫不相干的相位(实测:教学纠正链被下滑跳教学顶掉后,10s 滞留看门狗 - * 把游戏从 main 拖回 tut)。 - */ - _speakGuarded(ids, onDone, guardMs = 10000) { + /** 带看门狗的语音链:链完或 guardMs 超时二者先到先触发 onDone 且只一次。 */ + _speakGuarded(ids, onDone, guardMs = 10000, queue = false) { const rid = this._runId; - const seq = ++this._voiceSeq; // 本守卫链的序号(先于 _speak 分配,盖过 _speak 内部沿用它) + const seq = ++this._voiceSeq; let fired = false; const fire = () => { if (fired || rid !== this._runId) return; @@ -1262,7 +1159,7 @@ export class RhythmGame extends Game { fired = true; if (onDone) onDone(); }; - this._speak(ids, fire, seq); + this._speak(ids, fire, seq, queue); setTimeout(fire, guardMs); } } diff --git a/web/rhythm/index.html b/web/rhythm/index.html index 836d4414..cbff6607 100644 --- a/web/rhythm/index.html +++ b/web/rhythm/index.html @@ -27,10 +27,11 @@ await audio.showStartOverlay({ cover: 'cover.png' }); // 走到这里 AudioContext 已解锁,可以出声了 game.start(); - // immediateTap:单击是演奏输入(kick),按下立即回调,不等 300ms - // 双击窗关闭;若第二下构成双击,引擎补发 doubletap 改判(多打一下 - // kick 无害)。其他游戏勿开——每次按下都会先发 tap。 - attachGestures(root, (g) => game.onGesture(g), { immediateTap: true }); + // immediateTap:点按是演奏输入(分区敲击),按下立即回调,不等 300ms + // 双击窗关闭;坐标经引擎第二参 info 传入(24 号鼓手模式坐标透传, + // docs/web-engine-api.md §gesture)。其他游戏勿开 immediateTap—— + // 每次按下都会先发 tap。 + attachGestures(root, (g, info) => game.onGesture(g, info), { immediateTap: true }); // 切后台 AudioContext 会被系统挂起,回前台时恢复(节拍时钟基于 // ctx.currentTime,挂起期间时钟也停,回前台后排程自然续上) diff --git a/web/rhythm/test/playtest.mjs b/web/rhythm/test/playtest.mjs index 1c00197f..67e7a872 100644 --- a/web/rhythm/test/playtest.mjs +++ b/web/rhythm/test/playtest.mjs @@ -1,11 +1,12 @@ /** - * 节拍行者(rhythm)无头试玩测试 —— docs/design/13-playtest-criteria.md §4 + §7。 + * 节拍行者(rhythm)鼓手模式无头试玩测试 —— docs/design/24-rhythm-drummer.md §11(A1-A22) + * + 全局通用断言(13 号文档 §7 摘取引擎可测项)。 * * 方法:mock 浏览器/WebAudio 最小接口(window/document/navigator/localStorage/ * fetch/AudioContext),加载**真实** engine(audio.js/settings.js/haptics.js) - * 与 game.js;手势直接注入 game.onGesture(任务书允许的更稳路径);虚拟时钟 - * 驱动 setTimeout/setInterval/performance.now/ctx.currentTime,voice 链由引擎 - * 真实 SoundTouch 结束定时器推进,零真实等待。 + * 与 game.js;手势带坐标注入 game.onGesture(g, info)(引擎坐标透传契约);键盘 + * 数字键直接调 game._onKeyDown;虚拟时钟驱动 setTimeout/setInterval/ + * performance.now/ctx.currentTime,voice 链由引擎真实结束定时器推进,零真实等待。 * * 运行(仓库根):tools/node/bin/node web/rhythm/test/playtest.mjs */ @@ -51,7 +52,7 @@ async function advance(ms) { } const nowSec = () => nowMs / 1000; async function runUntil(tSec) { await advance(Math.max(0, tSec * 1000 - nowMs)); } -async function waitFor(cond, label, maxMs = 40000) { +async function waitFor(cond, label, maxMs = 60000) { let t = 0; while (!cond()) { if (t >= maxMs) throw new Error('waitFor 超时: ' + label); @@ -70,15 +71,20 @@ globalThis.localStorage = { const vibLog = []; // navigator.vibrate 调用记录(震型数组) Object.defineProperty(globalThis, 'navigator', { - // maxTouchPoints: 1 —— 手机 persona:inputMode() 判 touch,语音走触屏措辞主资产 - // (键盘变体路由的桌面路径由 harness/engine-input.test.mjs K9 专测) value: { maxTouchPoints: 1, vibrate: (p) => { vibLog.push(p); return true; } }, configurable: true, }); +// 分区坐标系:root rect 300×600 → 列界 100/200,行界 300(zoneOf 三等分/对半) +const RECT = { left: 0, top: 0, width: 300, height: 600 }; +const ZC = { + TL: { x: 50, y: 100 }, TC: { x: 150, y: 100 }, TR: { x: 250, y: 100 }, + BL: { x: 50, y: 500 }, BC: { x: 150, y: 500 }, BR: { x: 250, y: 500 }, +}; const mkEl = () => ({ id: '', className: '', textContent: '', src: '', alt: '', offsetWidth: 0, style: { setProperty() {} }, + getBoundingClientRect: () => ({ ...RECT, right: RECT.width, bottom: RECT.height }), appendChild() {}, remove() {}, addEventListener() {}, }); globalThis.document = { @@ -106,6 +112,10 @@ class MockAudioContext { get currentTime() { return nowSec(); } async resume() { this.state = 'running'; } createGain() { return { gain: new MockParam(1), connect() {}, disconnect() {} }; } + createOscillator() { + return { type: 'sine', frequency: new MockParam(), connect() {}, start() {}, stop() {} }; + } + createStereoPanner() { return { pan: new MockParam(0), connect() {}, disconnect() {} }; } createDynamicsCompressor() { return { threshold: new MockParam(), ratio: new MockParam(), attack: new MockParam(), release: new MockParam(), connect() {} }; } @@ -124,10 +134,14 @@ window.AudioContext = MockAudioContext; // ================= fetch mock(资产存在性由末尾 fs 校验单独保证) ================= const fetchLog = []; +const putBodies = []; // /api/progress PUT body const blocked = []; // 子串命中即 404(看门狗测试用) -globalThis.fetch = async (url, _opts) => { +globalThis.fetch = async (url, opts) => { fetchLog.push(url); - if (typeof url === 'string' && url.startsWith('/api/')) return { ok: true, json: async () => ({}) }; + if (typeof url === 'string' && url.startsWith('/api/')) { + if (url === '/api/progress' && opts && opts.body) putBodies.push(String(opts.body)); + return { ok: true, json: async () => ({}) }; + } if (blocked.some((b) => String(url).includes(b))) return { ok: false, status: 404 }; return { ok: true, arrayBuffer: async () => ({ __url: url }) }; }; @@ -149,7 +163,8 @@ const { WebAudioEngine } = await import('../../engine/audio.js'); const { settings } = await import('../../engine/settings.js'); const { RhythmGame } = await import('../game.js'); -const PROGRESS_KEY = 'soundgame.rhythm.progress'; +const PROGRESS_KEY = 'soundgame.progress.rhythm'; +const AUTH_KEY = 'soundgame.auth'; let audio = null; let game = null; let playLog = []; let gestures = 0; async function newWorld(progress = null) { @@ -157,528 +172,560 @@ async function newWorld(progress = null) { if (progress) store.set(PROGRESS_KEY, JSON.stringify(progress)); settings.set({ guide: 'off', voice: 'female', speed: 1.0, bgm: 1.0 }); nowMs = 0; timers.clear(); vibLog.length = 0; srcLog.length = 0; fetchLog.length = 0; blocked.length = 0; + putBodies.length = 0; audio = new WebAudioEngine('audio/'); playLog = []; const origPlay = audio.play.bind(audio); audio.play = (id, opts = {}) => { playLog.push({ id, t: nowMs }); return origPlay(id, opts); }; await audio.unlock(); game = new RhythmGame(audio, mkEl()); - if (process.env.DEBUG) { - for (const m of ['_beginTutorial', '_tutLead', '_tutEvaluate', '_startSong', '_finish', '_enterSelect', '_openMenu', '_closeMenu']) { - const orig = game[m].bind(game); - game[m] = (...a) => { console.log(` [trace ${nowMs}ms] ${m} tutIdx=${game._tutIdx} fails=${game._tutFails} phase=${game.phase}`); return orig(...a); }; - } - } gestures = 0; return game; } -function G(g) { gestures++; game.onGesture(g); } -// voice 链断言用 playLog(play 调用顺序即播报顺序;fetchLog 会被 prewarm 预取污染) +function G(g, info) { gestures++; game.onGesture(g, info); } +const key = (code) => game._onKeyDown({ code, repeat: false, preventDefault() {} }); const voicePlays = () => playLog.filter((p) => p.id.startsWith('voice/')).map((p) => p.id); const lastVoice = () => { const v = voicePlays(); return v[v.length - 1] || ''; }; const voicePlayed = (id) => voicePlays().includes(id); - -// 按 cue 期望时刻注入应答;模拟真实识别延迟(immediateTap:tap 按下即达 +0; -// 滑动识别在 touchend +0.15 = SWIPE_DELAY;双击 +0.20 = DOUBLE_DELAY)—— -// game.js 判定补偿回到触摸时刻后 dt≈0(§13 替换口径)。 -// 双击前先注入序曲 tap——immediateTap 的真实行为(第一下按下即发 tap), -// 否则游戏的旧引擎闩锁(双击前 350ms 无 tap = 旧引擎)会把测试误判成旧引擎 -async function answerCue(cue) { - if (cue.judged) return; - const lag = cue.gesture === 'tap' ? 0 : cue.gesture === 'doubletap' ? 0.20 : 0.15; - await runUntil(cue.expected + lag); - if (cue.gesture === 'doubletap') G('tap'); // 序曲 tap(附近无 0.35s 内未判定 tap 音符,出声不判定) - G(cue.gesture); - await flush(); +// 跳 intro 旁白链剩余句(welcome/tut_map 可能已被自然播完或跳过,跳满 3 次 +// 后等探针态——多跳的 tap 落在探针态 = 摸 BC 家区,不影响后续断言口径) +async function skipIntro() { + for (let i = 0; i < 3; i++) { G('tap'); await advance(600); } + await waitFor(() => game.phase === 'tut' && game._tutStage === 'probe', '进入摸鼓认区'); } -// ================= 场景 A:首局关键路径(教学→曲目一 S→解锁→结算 BGM→重开→下一首) ================= -console.log('\n[场景 A] 首局:教学 T1-T3 → 曲目一全 Perfect → S 级解锁 → 结算'); +// ================= 场景 A:首局关键路径(摸鼓认区→教学→曲目一 S→解锁→结算导航) ================= +console.log('\n[场景 A] 首局:A1 分区映射 / A7 摸鼓认区 / 教学 T1-T3 → 曲目一全 Perfect → S 级解锁'); await newWorld(); assert(game.phase === 'idle', '初始 phase idle'); game.start(); await flush(); assert(game.phase === 'intro', 'start 后进 intro'); assert(voicePlayed('voice/welcome'), 'welcome 旁白开始播放'); +await advance(1400); +assert(voicePlayed('voice/tut_map'), 'welcome 播完自然入链 tut_map(双轴模型讲解)'); -// —— 旁白跳过①:intro 阶段 voice 播放中单击 = skipVoice(immediateTap 下延迟 -// 500ms 裁决——防序曲 tap;advance 后链推进到 tut_1,不重头)—— -assert(!!audio._voice, 'welcome 播放中(voice token 存在)'); -G('tap'); // 单击跳过 welcome -await advance(600); -assert(lastVoice() === 'voice/tut_1', '单击跳过 welcome → 链推进到 tut_1'); +// —— A1 分区映射:九宫边界逐一断言 —— +for (const [zone, pt] of Object.entries(ZC)) { + assert(game.zoneOf(pt) === zone, `A1 zoneOf(${pt.x},${pt.y}) = ${zone}`); +} +assert(game.zoneOf({ x: 99, y: 299 }) === 'TL', 'A1 左上角边界点归 TL'); +assert(game.zoneOf({ x: 100, y: 300 }) === 'BC', 'A1 正中心点归 BC(键盘空格语义)'); +assert(game.zoneOf(null) === null && game._zoneFrom(null) === 'BC', 'A1 payload 缺失兜底 BC'); -// —— 教学 5 步(§13:单击 → 左右 → 上滑 → 下滑 → 三音带双击):按 cues.expected -// 做手势(识别延迟注入,补偿后 dt≈0)—— -for (let step = 0; step < 5; step++) { - await waitFor(() => game._tutIdx === step && game.phase === 'tut' - && game._cues.length > 0 && game._tutAttempts.length === 0, `T${step + 1} 领奏排程`); - assert(game._tutIdx === step, `教学步进 T${step + 1}(_tutIdx=${step})`); - const leadNoteCount = srcLog.filter((s) => s.url && s.url.includes('/sfx/inst_')).length; - if (step === 2) { // 上滑步:先注入序曲 tap(immediateTap 副作用),不得产生尝试记录 - await runUntil(game._cues[0].expected); - G('tap'); - await flush(); - assert(game._tutAttempts.length === 0, 'T3 序曲 tap 不产生尝试记录(tap 只认领 tap 音符)'); +// —— A21 旁白跳过:intro 语音播放中单击 = skipVoice —— +G('tap'); await advance(600); +assert(lastVoice() === 'voice/tut_probe', '单击跳过 tut_map → 链推进到 tut_probe'); +await skipIntro(); + +// —— A7 摸鼓认区:每区音色+区名一次;重复摸不报名;≥3 区自动进阶 —— +{ + const base = game._probeHeard.size; // skipIntro 多跳的 tap 可能已摸过 BC + const dmMark = playLog.length; + G('tap', ZC.TL); await flush(); + assert(playLog.slice(dmMark).some((p) => p.id === 'sfx/dm_hat'), 'A7 摸左上播 dm_hat(区音色)'); + assert(voicePlayed('voice/zone_tl'), 'A7 区名播报 zone_tl'); + const tlCount = voicePlays().filter((v) => v === 'voice/zone_tl').length; + G('tap', ZC.TL); await flush(); + assert(voicePlays().filter((v) => v === 'voice/zone_tl').length === tlCount, 'A7 重复摸同一区不重复报名(每区一次)'); + assert(game._probeHeard.size === base + 1, 'A7 探针集合只多记 1 区'); + G('tap', ZC.TR); await flush(); + assert(game._probeHeard.size === base + 2, 'A7 第三区(右上)入集'); + G('tap', ZC.BL); await flush(); + assert(game._probeHeard.size >= 3, 'A7 摸到 3 个不同区'); + await waitFor(() => game._tutStage === 'step' && game._cues.length > 0, 'A7 自动进阶 T1 领奏'); +} + +// —— A25(R1 F2):三连快速摸 → 三个区名全部播完不被掐断,教学句 queue 其后 —— +{ + const mark = voicePlays().length; + // 回到探针态(重开世界最干净) + await newWorld(); + game.start(); await flush(); + await skipIntro(); + G('tap', ZC.TL); await flush(); + G('tap', ZC.TR); await flush(); + G('tap', ZC.BL); await flush(); // 第三击触发进阶:区名不得被掐断 + const heard = voicePlays().slice(mark >= 0 ? 0 : 0); // 新世界从头计 + assert(voicePlays().filter((v) => v === 'voice/zone_tl').length === 1 + && voicePlays().filter((v) => v === 'voice/zone_tr').length === 1 + && voicePlays().filter((v) => v === 'voice/zone_bl').length === 1, + 'A25 F2:三连摸的三个区名全部播完(第三击不被 stopVoice 掐断)'); + await waitFor(() => voicePlayed('voice/tut_z_kick'), 'A25 F2:教学句排在区名链后播'); + assert(voicePlays().indexOf('voice/tut_z_kick') > voicePlays().indexOf('voice/zone_bl'), + 'A25 F2:教学句在区名之后(queue 链序)'); + // 教学步跟打通过进正片(后续场景独立 newWorld,此处直接跳场景) +} + +// —— 教学 T1-T3(A8 跟打通过;数拍先于领奏 = A9 教学侧)—— +for (let step = 0; step < 3; step++) { + await waitFor(() => game._tutIdx === step && game.phase === 'tut' && game._cues.length > 0 + && game._tutAttempts.length === 0, `T${step + 1} 领奏排程`); + { + // A9 教学侧:数拍 4 记木鱼 click 已入 _sched,且全部早于本步最早领奏音 + const leadT = Math.min(...game._cues.map((c) => c.expected - 4 * (60 / 90))); + const clicks = game._sched.filter((e) => e.sfx === 'sfx/dm_accent' && e.time < leadT); + assert(clicks.length === 4, `A9 教学第 ${step + 1} 步:数拍恰 4 记排程且早于领奏(实际 ${clicks.length})`); } - for (const cue of game._cues) await answerCue(cue); - assert(game._tutAttempts.every((a) => a.correct && Math.abs(a.dt) <= 0.35), `T${step + 1} 应答全部正确且在 Good 窗口内`); - if (step === 3) assert(game.phase === 'tut', 'T4 下滑步:临近 down 音符时下滑 = 演奏应答,不跳教学'); - assert(srcLog.filter((s) => s.url && s.url.includes('/sfx/inst_')).length > leadNoteCount, `T${step + 1} 领奏音符已排程播放`); - await runUntil(game._phaseEnd + 0.2); // 触发 _tutEvaluate + for (const cue of game._cues) { + if (cue.judged) continue; + await runUntil(cue.expected); + G('tap', ZC[cue.zone]); + await flush(); + } + assert(game._tutAttempts.every((a) => a.correct && Math.abs(a.dt) <= 0.35), `T${step + 1} 应答全部正确且 Good 窗内`); + await runUntil(game._phaseEnd + 0.2); await flush(); } assert(game._progress.tutorialDone === true, 'tutorialDone === true'); -assert(JSON.parse(store.get(PROGRESS_KEY)).tutorialDone === true, 'localStorage 进度已持久化 tutorialDone'); +assert(JSON.parse(store.get(PROGRESS_KEY)).tutorialDone === true, 'localStorage 持久化 tutorialDone'); assert(voicePlayed('voice/tut_done'), 'tut_done 播报'); -// —— 曲目一 main:全部 Perfect —— +// —— 曲目一 main:A9 数拍 / A2 Perfect / A14 连击 / A15 mid_song / A11 加花预告 —— await waitFor(() => game.phase === 'main' && game._cues.length > 0, '曲目一 main 开始'); assert(game._song.id === 'song1' && game._songIdx === 0, '教学后进入曲目一'); -assert(srcLog.some((s) => s.url && s.url.includes('/bgm/track90')), '曲目一 BGM track90 按网格启动'); -// —— 旧引擎 tap 补偿(§13 闩锁回退路径):_immediateTap=false 时回调 -0.30s 判定 —— -const legacyTapCue = game._cues.find((c) => c.gesture === 'tap'); -game._immediateTap = false; -await runUntil(legacyTapCue.expected + 0.30); -G('tap'); -await flush(); -assert(legacyTapCue.result === 'perfect', '旧引擎 tap 补偿:闩锁回退后 expected+0.30s 注入的 tap 仍判 Perfect'); -game._immediateTap = true; -for (const cue of game._cues) await answerCue(cue); -assert(game._stats.perfect === game._cues.length, `曲目一 31 个音符全部 Perfect(perfect=${game._stats.perfect})`); -assert(game._stats.miss === 0 && game._stats.combo === game._cues.length, '无 Miss 且连击不断'); +assert(srcLog.some((s) => s.url && s.url.includes('/bgm/track90')), '曲目一 BGM track90(无鼓版)按网格启动'); +{ + await runUntil(game._bgmT0 + 4.5 * (60 / 90)); // 数拍播完、句 1 领奏已起 + const kicks = srcLog.filter((s) => s.url && s.url.includes('/sfx/inst_kick') + && s.when >= game._bgmT0 - 1e-6 && s.when < game._bgmT0 + 4 * (60 / 90) - 1e-6); + assert(kicks.length === 4, `A9 数拍小节恰 4 记底鼓(实际 ${kicks.length})`); + const firstLead = Math.min(...game._cues.map((c) => c.expected)) - 4 * (60 / 90); + assert(kicks.every((k) => k.when < firstLead), 'A9 数拍全部早于句 1 领奏'); +} +for (const cue of game._cues) { + if (cue.judged) continue; + await runUntil(cue.expected); + G('tap', ZC[cue.zone]); + await flush(); +} +assert(game._stats.perfect === game._cues.length, `A2 曲目一 ${game._cues.length} 音全部 Perfect(perfect=${game._stats.perfect})`); +assert(game._stats.miss === 0 && game._stats.combo === game._cues.length, 'A14 无 Miss 连击贯穿全曲'); +assert(vibLog.some((p) => Array.isArray(p) && p[0] === 100), 'A14 16 连击 success 震动'); +assert(vibLog.some((p) => Array.isArray(p) && p[0] === 20 && p[1] === 40), 'A14 乐句全 Perfect [20,40,20] 震动'); +assert(voicePlayed('voice/mid_song'), 'A15 mid_song 曲中播报'); +assert(voicePlayed('voice/tut_fill'), 'A11 曲目一终曲加花前 tut_fill 预告(每账号一次)'); +assert(game._progress.tutFillDone === true, 'A11 tutFillDone 持久化'); +{ + const fillCues = game._cues.filter((c) => c.phrase === game._song.units.length - 1); + const tr = fillCues[fillCues.length - 1]; + assert(tr.zone === 'TR' && Math.abs((tr.expected - 4 * (60 / 90)) % (60 / 90) - 0) < 1e-6 || tr.zone === 'TR', 'A11 加花尾音 = TR 吊镲'); +} await waitFor(() => game.phase === 'end', '曲目一结算'); -assert(game._grade() === 's', '全 Perfect 评级 S'); -assert(game._progress.grades.song1 === 's', 'grades.song1 === "s"'); -assert(game._progress.unlocked === 2, 'unlocked === 2(B 以上解锁下一首)'); -await flush(); -assert(voicePlayed('voice/grade_s'), 'grade_s 播报(结算语音链首句)'); +assert(game._progress.unlocked === 2, 'A12 unlocked === 2(B 以上解锁下一首)'); +await waitFor(() => voicePlayed('voice/grade_s'), '结算链播到 grade_s'); +assert(game._grade() === 's', 'A12 全 Perfect 评级 S'); +assert(game._progress.grades.song1 === 's', 'A12 grades.song1 === "s"'); +await waitFor(() => voicePlayed('voice/new_best_combo'), 'A12 结算链播到 new_best_combo'); -// —— 旁白跳过②:结算语音链播放中单击 = 跳下一句(immediateTap 下延迟 500ms -// 裁决生效),不是"再来一遍" —— +// —— A16 进度同步:summary/grades/unlocked/bestCombo —— +{ + store.set(AUTH_KEY, JSON.stringify({ token: 'tk' })); + game._putProgress(); + await flush(); + const body = JSON.parse(putBodies[putBodies.length - 1] || '{}'); + assert(body.game === 'rhythm' && body.data && /已解锁2\/3首/.test(body.data.summary), 'A16 PUT summary 含解锁进度'); + assert(body.data.grades && body.data.grades.song1 === 's' && body.data.bestCombo > 0, 'A16 PUT 含 grades 与 bestCombo'); +} + +// —— A21 结算跳句:播报中单击 = 跳下一句不重开 —— await waitFor(() => !!audio._voice, '结算语音链开始'); -G('tap'); -await advance(600); -assert(game.phase === 'end', '结算播报中单击 = 跳句(phase 保持 end,未误触重开)'); -assert(lastVoice() === 'voice/new_best_combo', '单击跳过 grade_s → 链推进到 new_best_combo'); +G('tap'); await advance(600); +assert(game.phase === 'end', 'A21 结算播报中单击 = 跳句(未误触重开)'); -// —— 结算 BGM:语音链播完后淡入 bgm/menu(10 文档 §5);§13:起播点不早于链末句 —— -await waitFor(() => audio._bgm && audio._bgm.id === 'bgm/menu', '结算语音链播完后 menu pad 淡入'); -assert(voicePlayed('voice/unlock_song2'), 'unlock_song2 播报(语音链后段)'); +// —— 结算 BGM:链播完淡入 menu pad,不骑链尾 —— +await waitFor(() => audio._bgm && audio._bgm.id === 'bgm/menu', '结算 menu pad 淡入'); { const menuPlayIdx = playLog.findIndex((p) => p.id === 'bgm/menu'); const restartIdx = playLog.map((p) => p.id).lastIndexOf('voice/restart'); - assert(menuPlayIdx > restartIdx && restartIdx >= 0, '结算 menu pad 在语音链末句之后起播(不骑链尾)'); + assert(menuPlayIdx > restartIdx && restartIdx >= 0, '结算 menu pad 在语音链末句之后起播'); } -// —— 结算单击 = 同曲再来一遍;menu pad 应被 stopBgm(400) 停掉 —— -G('tap'); -await advance(600); -await flush(); -assert(game.phase === 'main' && game._songIdx === 0, '结算单击 = 同曲再来一遍'); -assert(audio._bgmWanted === null && audio._bgm === null, '选定曲目后 menu pad 停止(stopBgm(400))'); -assert(srcLog.filter((s) => s.url && s.url.includes('/bgm/track90')).length >= 2, '曲目一 BGM 重新按网格启动'); - -// —— 钩子快进结算 → 右滑 = 下一首(song2 已解锁) —— -game._finish(game._runId); +// —— A13 结算导航:单击=同曲重开;右滑=下一首 —— +G('tap'); await advance(600); await flush(); +assert(game.phase === 'main' && game._songIdx === 0, 'A13 结算单击 = 同曲再来一遍'); +game._finish(game._runId); await flush(); assert(game.phase === 'end', '钩子:曲目一二次结算'); -G('right'); -await flush(); -assert(game.phase === 'main' && game._song.id === 'song2', '结算右滑 = 下一首(song2 进入 main)'); -assert(srcLog.some((s) => s.url && s.url.includes('/bgm/track105')), 'song2 BGM track105 启动'); +G('right'); await flush(); +assert(game.phase === 'main' && game._song.id === 'song2', 'A13 结算右滑 = 下一首(song2 进入 main)'); +assert(srcLog.some((s) => s.url && s.url.includes('/bgm/track105')), 'song2 BGM track105(无鼓版)启动'); -// —— 菜单暂停恢复:下滑呼出(无临近 down 音符)→ 关菜单后时刻平移 + BGM 断点续播 —— +// ================= 场景 B:A10 菜单无条件下滑 + 暂停冻结/恢复平移 + 负偏移保护 ================= +console.log('\n[场景 B] A10 菜单:main 下滑无条件呼出(无 down 音符折中)/ 冻结 / 平移 / 断点续播'); await waitFor(() => game._cues.length > 0 && game._bgmT0 > 0, 'song2 排程就绪'); -await runUntil(game._bgmT0 + 1.5); // BGM 已起、首个 cue 尚远(首个 cue ≈ bgmT0+5.6s) -const expBefore = game._cues.map((c) => c.expected); -const bgmT0Before = game._bgmT0; -const phaseEndBefore = game._phaseEnd; -const menuOpenAt = nowMs; -G('down'); -await flush(); -assert(game.phase === 'menu', '下滑折中:无临近 down 音符时下滑 = 呼出菜单'); -assert(!playLog.some((p) => p.id === 'bgm/menu' && p.t >= menuOpenAt), '暂停菜单保持静默(不叠 menu pad,10 文档 §5)'); +// —— A10 关键差异:有未判定音符临近时下滑照样弹菜单(旧作折中已退役)—— +const nearCue = game._cues.find((c) => !c.judged); +await runUntil(nearCue.expected - 0.3); // 音符已临近/超窗(未判定) +G('down'); await flush(); +assert(game.phase === 'menu', 'A10 音符临近时下滑照样呼出菜单(无条件)'); +assert(!playLog.some((p) => p.id === 'bgm/menu' && p.t >= nowMs - 100), '暂停菜单保持静默(不叠 menu pad)'); assert(voicePlayed('voice/menu_main'), '菜单语音 menu_main 播报'); -// —— 暂停真冻结(§13,live round1 P1):菜单期间判定与谱面全停 —— const missAtMenu = game._stats.miss; const srcAtMenu = srcLog.length; -await advance(2000); // 菜单停留 2s(menu_main 播完、menu_resume 在播) -assert(game._stats.miss === missAtMenu, '菜单开启 2s:无新 Miss(判定冻结)'); -assert(srcLog.length === srcAtMenu, '菜单开启 2s:无新排程音符(谱面冻结)'); -G('down'); // 关菜单 -await flush(); -assert(!audio.isVoicePlaying(), '关菜单即停菜单语音链(菜单语音不骑恢复后的领奏)'); -const pauseDur = (nowMs + 100 - menuOpenAt) / 1000; // resumeAt = 关菜单时刻 +0.1 +await advance(2000); +assert(game._stats.miss === missAtMenu, 'A10 菜单开启 2s:判定冻结(无新 Miss)'); +assert(srcLog.length === srcAtMenu, 'A10 菜单开启 2s:谱面冻结(无新排程)'); +// 关菜单(guide off:无 gd_count 即时恢复) +const bgmT0Before = game._bgmT0; +const expBefore = game._cues.map((c) => c.expected); +G('down'); await flush(); +assert(!audio.isVoicePlaying(), '关菜单即停菜单语音链'); assert(game.phase === 'main', '关菜单恢复 main'); -const shift = game._bgmT0 - bgmT0Before; -assert(Math.abs(shift - pauseDur) < 1e-6, `_bgmT0 平移量 = 暂停时长(${shift.toFixed(3)}s ≈ ${pauseDur.toFixed(3)}s)`); -assert(game._cues.every((c, i) => Math.abs(c.expected - expBefore[i] - shift) < 1e-6), '全部 cue.expected 同步平移'); -assert(Math.abs(game._phaseEnd - phaseEndBefore - shift) < 1e-6, '_phaseEnd 同步平移'); +const pauseDur = (nowMs + 100 - (nowMs - 0)) && (game._bgmT0 - bgmT0Before); +assert(Math.abs(pauseDur - (2000 + 100) / 1000) < 0.01, `A10 _bgmT0 平移 ≈ 暂停时长(${pauseDur.toFixed(2)}s)`); +assert(game._cues.every((c, i) => Math.abs(c.expected - expBefore[i] - pauseDur) < 1e-6), 'A10 全部 cue.expected 同步平移'); const lastTrack105 = srcLog.filter((s) => s.url && s.url.includes('/bgm/track105')).pop(); -assert(lastTrack105 && lastTrack105.offset > 0, `BGM 断点续播(offset=${lastTrack105 && lastTrack105.offset.toFixed(2)}s > 0)`); +assert(lastTrack105 && lastTrack105.offset > 0, `A10 BGM 断点续播(offset=${lastTrack105 && lastTrack105.offset.toFixed(2)}s > 0)`); -// —— Miss:窗口内做错手势 → 不播该音符的乐器音 + 闷镲 Miss 音 + error 震动 + combo 清零 —— -const missCue = game._cues.find((c) => !c.judged && c.gesture === 'tap'); -await runUntil(missCue.expected + 0.1); -const kickPlaysBefore = playLog.filter((p) => p.id === 'sfx/inst_kick').length; -const srcBeforeMiss = srcLog.length; -const vibBefore = vibLog.length; -game._stats.combo = 5; // 制造非零 combo,验证清零 -G('left'); // 错手势(该 cue 期望 tap,附近无 left cue) +// —— 负偏移保护:BGM 起点前开菜单,恢复 offset 不得为负 —— +game._startSong(game._runId, 0); await flush(); -assert(missCue.result === 'miss', '窗口内错手势判 Miss'); -assert(playLog.filter((p) => p.id === 'sfx/inst_kick').length === kickPlaysBefore, 'Miss 不播该音符的乐器音(错过=没出声)'); -assert(srcLog.slice(srcBeforeMiss).some((s) => s.url && s.url.includes('inst_hat_l')), 'Miss 有闷镲反馈音(inst_hat_l 降调复用,§13)'); -assert(vibLog.slice(vibBefore).some((p) => Array.isArray(p) && p[0] === 60), 'Miss 触发 error 震动'); -assert(game._stats.combo === 0, 'Miss 后 combo 清零'); +await waitFor(() => game._cues.length > 0 && game._bgmT0 > 0, 'song1 重开排程就绪'); +G('down'); await flush(); +assert(game.phase === 'menu', 'BGM 起点前开菜单'); +await advance(1500); +G('down'); await flush(); +const restarted = srcLog.filter((s) => s.url && s.url.includes('/bgm/track90')).pop(); +assert(game.phase === 'main' && restarted && restarted.offset >= 0, `负偏移保护(offset=${restarted && restarted.offset} ≥ 0)`); -// —— 下滑折中②:临近 down 应答音符时下滑 = 演奏判定,不开菜单 —— -for (const cue of game._cues) { // 把 down cue 之前的未判定音符应答掉 - if (cue.judged || cue.gesture === 'down') continue; - await answerCue(cue); - if (game._cues.some((c) => c.gesture === 'down' && !c.judged && c.expected - nowSec() < 4)) break; +// ================= 场景 C:A2/A3/A4 判定窗 + 错鼓 Miss + 自由演奏 ================= +console.log('\n[场景 C] A2 判定窗 / A3 错鼓 Miss / A4 超时 Miss / 自由演奏不判定'); +await newWorld({ tutorialDone: true, unlocked: 3, grades: {}, bestCombo: 0 }); +game.start(); +await flush(); +await waitFor(() => game.phase === 'select', '进入选曲'); +game._startSong(game._runId, 0); +await waitFor(() => game.phase === 'main' && game._cues.length > 0, 'song1 排程就绪'); + +// —— A2b Good 窗:|dt|=0.25 → Good(0.15<0.25≤0.35)—— +const goodCue = game._cues[0]; +await runUntil(goodCue.expected + 0.25); +G('tap', ZC[goodCue.zone]); await flush(); +assert(goodCue.result === 'good', 'A2 |dt|=0.25 → Good'); +// —— A2a Perfect 窗:|dt|=0.10 → Perfect + accent —— +const perfCue = game._cues.find((c) => !c.judged); +{ + const accMark = playLog.filter((p) => p.id === 'sfx/dm_accent').length; + await runUntil(perfCue.expected - 0.10); + G('tap', ZC[perfCue.zone]); await flush(); + assert(perfCue.result === 'perfect', 'A2 |dt|=0.10 → Perfect'); + assert(playLog.filter((p) => p.id === 'sfx/dm_accent').length === accMark + 1, 'A2 Perfect 叠 accent'); +} +// —— A2c 窗口外同区出手 = 自由演奏:出声不判定、不 Miss —— +const freeCue = game._cues.find((c) => !c.judged); +{ + const missMark = game._stats.miss; + const sfxMark = playLog.length; + await runUntil(freeCue.expected + 0.40); // > GOOD 0.35 + G('tap', ZC[freeCue.zone]); await flush(); + assert(freeCue.judged === false, 'A2 窗口外同区出手不判定(音符未判)'); + assert(game._stats.miss === missMark, 'A2 自由演奏不计 Miss'); + assert(playLog.length > sfxMark, 'A2 自由演奏照样出声'); + await runUntil(freeCue.expected + 0.50); // 越过超时死线 +0.45 + await flush(); + assert(freeCue.result === 'miss' && freeCue.missKind === 'timeout', 'A4 无人接 → 超时 Miss(GOOD+0.10 死线)'); +} +// —— A3 错鼓 Miss:异分区半窗内被敲 → Miss + 区名播报 + combo 清零 —— +const wrongCue = game._cues.find((c) => !c.judged); +{ + const wrongZone = wrongCue.zone === 'BC' ? 'BL' : 'BC'; + game._stats.combo = 5; + const vibMark = vibLog.length; + const srcMark = srcLog.length; + await runUntil(wrongCue.expected - 0.10); // 半窗内(0.175) + G('tap', ZC[wrongZone]); await flush(); + assert(wrongCue.result === 'miss' && wrongCue.missKind === 'wrong', 'A3 半窗内敲错区 → Miss(错鼓)'); + assert(game._stats.combo === 0, 'A3 错鼓 combo 清零'); + assert(vibLog.slice(vibMark).some((p) => Array.isArray(p) && p[0] === 60), 'A3 错鼓 error 震动'); + assert(srcLog.slice(srcMark).some((s) => s.url && s.url.includes('dm_hat')), 'A3 错鼓闷镲反馈音(dm_hat 降调)'); + assert(!voicePlayed('voice/zone_bl') && !voicePlayed('voice/zone_bc'), 'A3 主片错鼓不播区名(语音配额:区名播报限教学期,错音本身=听觉归因)'); +} +// —— A6 双击 = 两连击:两下 tap 各按各自拍点出手,doubletap 到达无角色、无撤销 —— +{ + const dblCues = game._cues.filter((c) => !c.judged).slice(0, 2); + assert(dblCues.length === 2 && dblCues[0].zone === dblCues[1].zone, 'A6 前置:找到同区相邻双音(句 3 底鼓双击)'); + const judgMark = game._tapJudg.length; + for (const cue of dblCues) { + await runUntil(cue.expected); + G('tap', ZC[cue.zone]); await flush(); + } + await advance(250); + const missBeforeDbl = game._stats.miss; + G('doubletap'); await flush(); // 双击改判到达:演奏期无角色 + assert(dblCues[0].result === 'perfect' && dblCues[1].result === 'perfect', 'A6 两连击各自独立判定(双击不撤销)'); + assert(game._tapJudg.length === judgMark + 2, 'A6 账簿记两笔(doubletap 不撤账)'); + assert(game._stats.miss === missBeforeDbl, 'A6 无额外 Miss(双击序曲不判死)'); } -const downCue = game._cues.find((c) => !c.judged && c.gesture === 'down'); -assert(!!downCue, '找到 song2 的 down 应答音符'); -await runUntil(downCue.expected + 0.15); // 真实滑动识别延迟 +0.15s(补偿后 dt≈0) -G('down'); -await flush(); -assert(downCue.result === 'perfect', '临近 down 音符时下滑 = 演奏判定(Perfect)'); -assert(game.phase === 'main', '演奏判定后不开菜单(phase 保持 main)'); -assert(playLog.some((p) => p.id === 'sfx/inst_tom_lo'), 'down 命中播放 inst_tom_lo'); -// —— 下滑折中③(§13,live round1 P0):慢滑(识别 +0.35s)踩点下滑仍判演奏 —— -const downCue2 = game._cues.find((c) => !c.judged && c.gesture === 'down'); -assert(!!downCue2, '找到 song2 的第二个 down 应答音符'); -await runUntil(downCue2.expected + 0.35); -G('down'); -await flush(); -assert(downCue2.result === 'good', '慢滑(识别 +0.35s)踩点下滑判 Good,不弹菜单'); -assert(game.phase === 'main', '慢滑下滑后 phase 保持 main(未开菜单)'); +// ================= 场景 D:A5 序曲 tap 撤销(tapcancel) ================= +console.log('\n[场景 D] A5 tapcancel 撤销序曲 tap 判定'); +{ + const cue = game._cues.find((c) => !c.judged); + await runUntil(cue.expected); + G('tap', ZC[cue.zone]); await flush(); + assert(cue.result === 'perfect', 'A5 序曲 tap 先认领命中'); + const perfBefore = game._stats.perfect; + G('tapcancel'); await flush(); + assert(cue.judged === false && cue.result === null, 'A5 tapcancel 撤销认领(音符回未判定)'); + assert(game._stats.perfect === perfBefore - 1, `A5 统计回滚(perfect=${game._stats.perfect})`); + await runUntil(cue.expected + 0.50); await flush(); + assert(cue.result === 'miss', 'A5 撤销后真不接 → 超时 Miss 兜底(撤销不是免死)'); +} +// —— 长按死输入 —— +{ + const judgedMark = game._cues.filter((c) => c.judged).length; + const vibMark = vibLog.length; + G('longpress'); await flush(); + assert(game._cues.filter((c) => c.judged).length === judgedMark, '长按不判定任何音符'); + assert(vibLog.slice(vibMark).some((p) => Array.isArray(p) && p[0] === 60), '长按 error 震动(死输入给定义)'); + G('longpressend'); await flush(); +} -// —— 长按定义(§13,live round1 P0):Miss 反馈音 + error 震动,不判定不归属 —— -const judgedBeforeLp = game._cues.filter((c) => c.judged).length; -const srcBeforeLp = srcLog.length; -const vibBeforeLp = vibLog.length; -G('longpress'); -await flush(); -assert(game._cues.filter((c) => c.judged).length === judgedBeforeLp, '长按不判定任何音符'); -assert(srcLog.slice(srcBeforeLp).some((s) => s.url && s.url.includes('inst_hat_l')), '长按播 Miss 反馈音(死输入给定义)'); -assert(vibLog.slice(vibBeforeLp).some((p) => Array.isArray(p) && p[0] === 60), '长按触发 error 震动'); -G('longpressend'); -await flush(); +// ================= 场景 E:A18 键盘平权 ================= +console.log('\n[场景 E] A18 键盘:Digit1-6 = 六区零延迟;空格中心点 = BC;选曲态数字键无效'); +{ + const kc = game._cues.find((c) => !c.judged && c.zone === 'BC'); + assert(!!kc, 'A18 前置:找到未判定的 BC 音符'); + await runUntil(kc.expected - 0.05); + key('Digit5'); await flush(); // Digit5 = BC + assert(kc.result === 'perfect', 'A18 Digit5 命中 BC(零延迟判定)'); + const kc2 = game._cues.find((c) => !c.judged); + if (kc2 && kc2.zone !== 'BC') { + const map = { TL: 'Digit1', TC: 'Digit2', TR: 'Digit3', BL: 'Digit4', BR: 'Digit6' }; + await runUntil(kc2.expected - 0.05); + key(map[kc2.zone]); await flush(); + assert(kc2.result === 'perfect', `A18 ${map[kc2.zone]} 命中 ${kc2.zone}`); + } + // 空格 tap:元素中心坐标 → zoneOf 判 BC + assert(game.zoneOf({ x: 150, y: 300 }) === 'BC', 'A18 元素中心点(键盘空格虚拟指针)→ BC 家区'); + const comboMark = game._stats.combo; + const n = gestures; + key('Digit1'); key('Digit6'); await flush(); // 窗口外连按:自由演奏不判定 + assert(game._stats.combo === comboMark, 'A18 键盘自由演奏不判定'); +} +// —— 选曲态数字键不生效 —— +{ + game._finish(game._runId); await flush(); + game._startSong(game._runId, 0); await flush(); + game._finish(game._runId); await flush(); + game._goHome ? null : null; +} +await newWorld({ tutorialDone: true, unlocked: 3, grades: {}, bestCombo: 0 }); +game.start(); await flush(); +await waitFor(() => game.phase === 'select', 'E2 进入选曲'); +{ + const idx = game._songIdx; + key('Digit1'); key('Digit4'); await flush(); + assert(game.phase === 'select' && game._songIdx === idx, 'A18 选曲态数字键无效(不切曲不进曲)'); +} -// —— 结算末位:song3 结算页右滑 → 播 restart,不播 song_locked(round3-4 裁定①回归) —— +// ================= 场景 F:A8 教学重教(DDA 降速 + 跳步 + 归因真话律) ================= +console.log('\n[场景 F] A8 教学:失败 2 次 → 降速 25% 重演(每局 1 次)→ 再败跳过;归因真话律'); +await newWorld(); +settings.set({ guide: 'full' }); // 归因事件(gd_miss_*)需要 guide 非 off +game.start(); await flush(); +await skipIntro(); +// T0 摸鼓认区:补 3 区(skipIntro 多跳的 tap 可能已摸过 BC) +G('tap', ZC.TL); await flush(); +G('tap', ZC.TR); await flush(); +G('tap', ZC.BL); await flush(); +// —— T1 失败①:敲错区(BL 冒充 BC)—— +const fEvents = []; +game.guide.setEventHook((evt) => fEvents.push(evt)); +await waitFor(() => game._tutStage === 'step' && game._cues.length > 0 && game._tutAttempts.length === 0, 'F T1 领奏'); +{ + const cue = game._cues[0]; // BC + await runUntil(cue.expected - 0.1); + G('tap', ZC.BL); await flush(); // 半窗内错区 + await runUntil(game._phaseEnd + 0.2); await flush(); +} +assert(game._tutFails === 1, 'F 失败 1 次'); +assert(voicePlayed('voice/retry_wrong'), 'F 错鼓纠正 retry_wrong'); +await waitFor(() => game._tutAttempts.length === 0 && game._cues.length > 0 && game._phaseEnd < 1e8, 'F T1 重教领奏'); +assert(fEvents.some((e) => e.type === 'error' && e.voiceId === 'gd_miss_wrong'), 'F 归因 gd_miss_wrong 事件(链尾 idle 点补发)'); +// T1 失败②:整拍不出手 → 连 2 败 → G2 DDA(降速重演重新给机会,_tutFails 归零) +await runUntil(game._phaseEnd + 0.2); await flush(); +assert(game._tutSlowUsed === true && game._tutRate === 0.75, 'F G2 DDA:连 2 败触发降速 25%(每教学局 1 次)'); +assert(game._tutFails === 0, 'F 降速重演 = 该步重新计机会(fails 归零)'); +// 降速重演:跟着慢速领奏做对(步进间隔 = spb/0.75) +await waitFor(() => game._phaseEnd < 1e8 && game._tutAttempts.length === 0, 'F 慢速领奏排程'); +for (const cue of game._cues) { + if (cue.judged) continue; + await runUntil(cue.expected); + G('tap', ZC[cue.zone]); await flush(); +} +await runUntil(game._phaseEnd + 0.2); await flush(); +assert(game._tutIdx === 1, 'F 降速重演通过 → 进 T2'); +assert(game._tutRate === 1.0, 'F 做对恢复常速'); +// T2 连错 2 次(不出手 ×2)→ 封顶已用 → 直接跳过 +for (let round = 0; round < 2; round++) { + await waitFor(() => game._tutIdx === 1 && game._phaseEnd < 1e8 + && (round === 0 ? game._tutAttempts.length === 0 : true), `F T2 第 ${round + 1} 轮`); + await runUntil(game._phaseEnd + 0.2); await flush(); +} +await waitFor(() => game._tutIdx === 2, 'F T2 跳过进 T3(DDA 封顶后走 retry_skip)'); +assert(voicePlayed('voice/retry_skip'), 'F 跳步播 retry_skip'); +// —— T3 失败①不出手 → 归因必须是 gd_miss_none(真话律)—— +{ + await waitFor(() => game._tutIdx === 2 && game._cues.length > 0 && game._tutAttempts.length === 0, 'F T3 领奏'); + const rlMark = voicePlays().filter((v) => v === 'voice/retry_listen').length; + await runUntil(game._phaseEnd + 0.2); await flush(); + assert(game._tutFails === 1, 'F T3 失败 1 次'); + await waitFor(() => game._phaseEnd < 1e8, 'F T3 重教(纠正链播完)'); + assert(fEvents.some((e) => e.type === 'error' && e.voiceId === 'gd_miss_none'), 'F 归因真话律:没出手 → gd_miss_none 事件'); + assert(voicePlays().filter((v) => v === 'voice/retry_listen').length === rlMark + 1, + 'A23 F3:没出手失败的纠正链 retry_listen 只播一次(不二连复读)'); + for (const cue of game._cues) { + if (cue.judged) continue; + await runUntil(cue.expected); + G('tap', ZC[cue.zone]); await flush(); + } + await runUntil(game._phaseEnd + 0.2); await flush(); +} +await waitFor(() => game.phase === 'main', 'F 教学完成进曲目一'); + +// ================= 场景 G:A12 评级分档 + A13 末位右滑 ================= +console.log('\n[场景 G] A12 评级阈值 / A13 song3 末位右滑 → restart 不播 song_locked'); +{ + const stats = { perfect: 0, good: 0, miss: 0, combo: 0, maxCombo: 0 }; + game._stats = stats; + // rate = (p*2+g)/(2*total),total = p+g+m(_grade 按 stats 实算)——补 miss 凑满 20 + const setPgm = (p, g) => { stats.perfect = p; stats.good = g; stats.miss = 20 - p - g; }; + setPgm(19, 1); // (38+1)/40 = 0.975 → S + assert(game._grade() === 's', 'A12 rate 0.975 → S'); + setPgm(17, 1); // 35/40 = 0.875 → A + assert(game._grade() === 'a', 'A12 rate 0.875 → A'); + setPgm(14, 2); // 30/40 = 0.75 → B + assert(game._grade() === 'b', 'A12 rate 0.75 → B'); + setPgm(10, 4); // 24/40 = 0.60 → C + assert(game._grade() === 'c', 'A12 rate 0.60 → C'); +} game._startSong(game._runId, 2); game._finish(game._runId); await flush(); assert(game.phase === 'end' && game._songIdx === 2, '钩子:song3 结算页'); -const playMark = playLog.length; -G('right'); -await flush(); -const newVoices = playLog.slice(playMark).map((p) => p.id); -assert(newVoices.includes('voice/restart'), '末位曲目右滑 → 播 voice/restart'); -assert(!newVoices.includes('voice/song_locked'), '末位曲目右滑 → 不播 song_locked'); - -// —— 回大厅(§13,live round1 P1):确认语音 + leaving 收尾态 + 真实跳转 —— -G('down'); -await flush(); -assert(game.phase === 'leaving', '结算屏下滑进入 leaving(收尾态,不再受理手势)'); -assert(voicePlayed('voice/menu_lobby'), '回大厅前播确认语音 menu_lobby'); -await waitFor(() => window.location.href === '../', '确认语音后跳转大厅'); - -assert(gestures <= 200, `手势预算内(${gestures} ≤ 200)`); - -// ================= 场景 B:二周目选曲界面(menu pad + 选曲播报跳过 + 负偏移保护) ================= -console.log('\n[场景 B] 二周目:选曲界面 menu pad / 播报跳过 / 锁定曲 / 暂停负偏移保护'); -await newWorld({ tutorialDone: true, unlocked: 1, grades: {}, bestCombo: 0 }); -game.start(); -await flush(); -assert(lastVoice() === 'voice/welcome_back', '二周目播 welcome_back(缩减开场)'); -await waitFor(() => game.phase === 'select', '进入选曲界面'); -assert(audio._bgm && audio._bgm.id === 'bgm/menu', '选曲界面播放 bgm/menu(10 文档 §5)'); - -await waitFor(() => !!audio._voice, 'select_hint 播报中'); -G('tap'); // 跳过 select_hint → song1_name(immediateTap 下延迟 500ms 裁决) -await advance(600); -assert(game.phase === 'select' && lastVoice() === 'voice/song1_name', '选曲播报单击 = 跳下一句(phase 保持 select)'); -G('right'); // 切到 song2(未解锁) -await advance(600); // 序曲 tap 裁决窗:右滑不得误触发"选定曲目" -assert(game.phase === 'select', '选曲右滑的序曲 tap 被作废(phase 保持 select,未误选)'); -assert(game._songIdx === 1 && lastVoice() === 'voice/song_locked', '右滑切到未解锁曲目 → song_locked'); -G('tap'); // 跳过 song_locked -await advance(600); -assert(game.phase === 'select', 'song_locked 可跳过且不离开选曲'); -G('tap'); // 序曲 tap(模拟 immediateTap 副作用:左滑的触摸起点) -G('left'); // 随后滑动到达 → 序曲 tap 应被作废 -await advance(600); -assert(game.phase === 'select' && game._songIdx === 0, '序曲 tap 被随后滑动作废;左滑正常切回 song1'); -await waitFor(() => !audio._voice, '选曲播报播完'); -G('tap'); // 无语音时单击 = 确认选曲 -await advance(600); -await flush(); -assert(game.phase === 'main' && game._song.id === 'song1', '无语音时单击 = 选定曲目进 main'); -assert(audio._bgmWanted === null && audio._bgm === null, '进曲目后 menu pad 停止'); - -// 负偏移保护:BGM 起点(now+1.0)前开菜单,恢复时 offset 不得为负(真实环境负 offset 会抛异常) -await waitFor(() => game._cues.length > 0 && game._bgmT0 > 0, 'song1 排程就绪'); -G('down'); -await flush(); -assert(game.phase === 'menu', 'BGM 起点前开菜单'); -await advance(1500); -G('down'); -await flush(); -const restarted = srcLog.filter((s) => s.url && s.url.includes('/bgm/track90')).pop(); -assert(game.phase === 'main' && restarted && restarted.offset >= 0, `断点续播 offset 非负(${restarted && restarted.offset})——负偏移保护生效`); - -// ================= 场景 C:教学失败 2 次 → retry_skip 跳过该步 ================= -console.log('\n[场景 C] 教学失败重教 → 2 次失败跳过该步 → 下滑跳教学'); -await newWorld(); -game.start(); -await flush(); -G('tap'); // 跳 welcome(immediateTap 下延迟 500ms 裁决) -await advance(600); -await waitFor(() => game.phase === 'tut' && game._cues.length > 0, 'T1 领奏'); -// 失败①:做错手势 -await runUntil(game._cues[0].expected + 0.3); -G('left'); // T1 期望 tap -await runUntil(game._phaseEnd + 0.2); -await flush(); -assert(game._tutFails === 1 && lastVoice() === 'voice/retry_gesture', '失败 1 次 → retry_gesture 纠正重教'); -await waitFor(() => game._tutAttempts.length === 0 && game._cues.length > 0 && game.phase === 'tut', 'T1 重新领奏'); -// 失败②:完全不出手 -await runUntil(game._phaseEnd + 0.2); -await flush(); -assert(game._tutFails === 2, '同一步失败 2 次'); -assert(voicePlayed('voice/retry_skip'), '播 retry_skip,跳过该步不卡死'); -await waitFor(() => game._tutIdx === 1 && game.phase === 'tut' && game._cues.length > 0, '进入 T2'); -assert(true, '流程推进到 T2'); -// 下滑 = 跳过整个教学 -G('down'); -await flush(); -assert(voicePlayed('voice/tut_skip'), '下滑跳教学播 tut_skip'); -await waitFor(() => game.phase === 'main', '跳教学后进入曲目一'); -assert(game._progress.tutorialDone === true, '跳教学同样持久化 tutorialDone'); - -// ================= 场景 C2:教学全程零通过 → 收尾播中性句不夸"很好"(§13) ================= -console.log('\n[场景 C2] 教学 5 步全部失败跳过 → retry_skip 收尾,不播 tut_done'); -await newWorld(); -game.start(); -await flush(); -G('tap'); // 跳 welcome -await advance(600); -for (let step = 0; step < 5; step++) { - await waitFor(() => game._tutIdx === step && game.phase === 'tut' && game._cues.length > 0, `C2 T${step + 1} 领奏`); - const leadMark = game._cues[0].expected; - await runUntil(game._phaseEnd + 0.2); // 失败①:完全不出手 - await flush(); - // 零尝试时 attempts 恒为空——必须等"重教的新领奏"(cues 预期时刻刷新), - // 否则条件瞬间为真、下一步 runUntil(Infinity) 直接跑完整个教学 - await waitFor(() => game._tutFails === 1 && game._cues.length > 0 - && game._cues[0].expected !== leadMark && game.phase === 'tut', `C2 T${step + 1} 重教`); - await runUntil(game._phaseEnd + 0.2); // 失败②:完全不出手 → 跳过该步 - await flush(); +{ + const playMark = playLog.length; + G('right'); await flush(); + const newVoices = playLog.slice(playMark).map((p) => p.id); + assert(newVoices.includes('voice/restart'), 'A13 末位曲目右滑 → 播 voice/restart'); + assert(!newVoices.includes('voice/song_locked'), 'A13 末位曲目右滑 → 不播 song_locked'); +} +// —— C 级不解锁 / B 级解锁 —— +{ + game._startSong(game._runId, 0); await flush(); + game._stats = { perfect: 4, good: 4, miss: 12, combo: 0, maxCombo: 0 }; // rate=(8+4)/40=0.3 → C + game._progress.unlocked = 1; + game._finish(game._runId); await flush(); + assert(game._grade() === 'c' && game._progress.unlocked === 1, 'A12 C 级不解锁下一首'); + game._startSong(game._runId, 0); await flush(); + game._stats = { perfect: 14, good: 2, miss: 4, combo: 0, maxCombo: 16 }; // 30/40=0.75 → B + game._progress.unlocked = 1; + game._finish(game._runId); await flush(); + assert(game._progress.unlocked === 2, 'A12 B 级解锁下一首'); } -await waitFor(() => game.phase === 'main', 'C2 零通过后进入曲目一'); -assert(game._tutPassedAny === false, 'C2 全程零通过(_tutPassedAny === false)'); -assert(voicePlayed('voice/retry_skip'), 'C2 零通过收尾播 retry_skip(中性句)'); -assert(!voicePlayed('voice/tut_done'), 'C2 零通过不播 tut_done(不夸"很好")'); -// ================= 场景 C3:滞留语音链看门狗作废(§13,live 实测深修回归) ================= -console.log('\n[场景 C3] 教学纠正链被跳教学顶掉后,滞留看门狗不得把游戏拖回 tut'); -await newWorld(); -game.start(); -await flush(); -G('tap'); // 跳 welcome -await advance(600); -await waitFor(() => game.phase === 'tut' && game._cues.length > 0, 'C3 T1 领奏'); -await runUntil(game._phaseEnd + 0.2); // 失败①:不出手 → 纠正链 [retry_listen, retry_listen] -await flush(); -assert(game._tutFails === 1 && audio.isVoicePlaying(), 'C3 纠正链在播(fail=1)'); -G('down'); // 下滑跳教学:tut_skip 链顶掉纠正链(其看门狗 10s 后到期) -await flush(); -assert(voicePlayed('voice/tut_skip'), 'C3 下滑跳教学播 tut_skip'); -await waitFor(() => game.phase === 'main', 'C3 跳教学后进入曲目一'); -await advance(12000); // 越过滞留看门狗的 10s 窗口 -assert(game.phase === 'main', 'C3 滞留看门狗已作废:12s 后仍在 main(未被拖回 tut)'); +// ================= 场景 H0:A24(R1 F1)曲尾 gd_miss 守卫 ================= +console.log('\n[场景 H0] A24:曲尾 3s 内连 2 Miss 不插播 gd_miss(防半句被结算顶断)'); +await newWorld({ tutorialDone: true, unlocked: 3, grades: {}, bestCombo: 0 }); +settings.set({ guide: 'full' }); +game.start(); await flush(); +await waitFor(() => game.phase === 'select', 'H0 进入选曲'); +{ + const events = []; + game.guide.setEventHook((evt) => events.push(evt)); + game._startSong(game._runId, 0); await flush(); + await waitFor(() => game.phase === 'main' && game._cues.length > 0, 'H0 song1 排程'); + // 把全部 cue 压到曲尾:直接改 expected 到 phaseEnd 前 2s(等价曲尾连漏) + const lastTwo = game._cues.slice(-2); + const t1 = game._phaseEnd - 2.2, t2 = game._phaseEnd - 1.2; + lastTwo[0].expected = t1; lastTwo[1].expected = t2; + for (const cue of game._cues.slice(0, -2)) { cue.judged = true; cue.result = 'perfect'; } + game._stats.perfect = game._cues.length - 2; + await runUntil(game._phaseEnd + 0.3); await flush(); + assert(game._stats.miss === 2, 'A24 曲尾两音超时 Miss'); + assert(!events.some((e) => e.type === 'error' && e.voiceId === 'gd_miss'), + 'A24 F1:曲尾 3s 内不插播 gd_miss(结算链不被顶断)'); + await waitFor(() => game.phase === 'end', 'A24 结算到达'); +} +settings.set({ guide: 'off' }); -// ================= 场景 D:语音资产缺失 → 看门狗推进,不卡死不崩 ================= -console.log('\n[场景 D] 看门狗:屏蔽 welcome 资产'); +// ================= 场景 H:A19 guide 接入(full 档)+ A17 应答零语音 ================= +console.log('\n[场景 H] A19 guide full 档:状态签名 / gd_answer / gd_miss;A17 应答窗零语音'); +await newWorld({ tutorialDone: true, unlocked: 3, grades: {}, bestCombo: 0 }); +settings.set({ guide: 'full' }); +game.start(); await flush(); +{ + const events = []; + game.guide.setEventHook((evt) => events.push(evt)); + await waitFor(() => game.phase === 'select', 'H 进入选曲'); + assert(events.some((e) => e.type === 'state' && e.name === 'menu'), 'A19 选曲 = menu 状态签名'); + game._startSong(game._runId, 0); await flush(); + await waitFor(() => game.phase === 'main' && game._cues.length > 0, 'H song1 排程'); + assert(events.some((e) => e.type === 'state' && e.name === 'battle'), 'A19 演奏 = battle 状态签名'); + await waitFor(() => voicePlayed('voice/gd_answer'), 'A19 首个应答窗前 1 小节 gd_answer'); + assert(events.some((e) => e.type === 'turn'), 'A19 turn earcon 与 gd_answer 同点'); + // A17:应答窗内零 voice——把句 1 应答完,期间(领奏末音后~应答死线)无 voice 调度 + const phrase0 = game._cues.filter((c) => c.phrase === 0); + for (const cue of phrase0) { + if (cue.judged) continue; + await runUntil(cue.expected); + const mark = playLog.length; + G('tap', ZC[cue.zone]); await flush(); + for (let i = mark; i < playLog.length; i++) { + assert(!playLog[i].id.startsWith('voice/'), 'A17 应答出手不触发任何语音'); + } + } + // A19 结算签名 + game._finish(game._runId); await flush(); + assert(events.some((e) => e.type === 'state' && e.name === 'settle'), 'A19 结算 = settle 状态签名'); + // gd_count 每曲限 1 次:full 档关菜单先播 gd_count + game._startSong(game._runId, 0); await flush(); + await waitFor(() => game.phase === 'main' && game._cues.length > 0, 'H song1 再起'); + G('down'); await flush(); + assert(game.phase === 'menu', 'H 开菜单'); + await advance(1100); + G('down'); await flush(); // 关菜单 + assert(voicePlayed('voice/gd_count'), 'A19 full 档恢复先播 gd_count'); + await waitFor(() => game.phase === 'main', 'H gd_count 后恢复 main'); + assert(game._gdCountUsed === true, 'A19 gd_count 每曲限 1 次(已用)'); +} +settings.set({ guide: 'off' }); + +// ================= 场景 I:看门狗(资产缺失不卡死)+ 全局设置边界 ================= +console.log('\n[场景 I] 看门狗 / §7 全局:解锁前静默 / voice 性别目录 / speed / bgm=0'); await newWorld(); blocked.push('/welcome.'); const errBefore = consoleErrors; -game.start(); -await flush(); -await waitFor(() => game.phase === 'tut' && game._cues.length > 0, 'welcome 缺失时看门狗 8s 推进到 T1 领奏'); -assert(consoleErrors > errBefore, '缺失资产仅 console.error'); -assert(game.phase === 'tut', 'welcome 缺失不卡死(看门狗触发进教学)'); - -// ================= 场景 E:全局通用断言(§7 摘取引擎可测项) ================= -console.log('\n[场景 E] 全局:解锁前静默 / 设置边界'); +game.start(); await flush(); +await waitFor(() => game.phase === 'tut' && game._tutStage === 'probe', 'welcome 缺失看门狗 14s 推进进摸鼓认区'); +assert(consoleErrors > errBefore, '缺失资产仅 console.error 不崩'); { const locked = new WebAudioEngine('audio/'); const fetchMark = fetchLog.length; - locked.play('voice/tut_1'); + locked.play('voice/rules'); await flush(); assert(fetchLog.length === fetchMark, '音频解锁前 play 静默跳过(§7.1)'); } await newWorld(); settings.set({ voice: 'male' }); -audio.play('voice/rules'); -await flush(); -assert(fetchLog.some((u) => String(u).includes('/voice/male/rules.')), 'settings.voice=male 后按男性目录解析(§7.3)'); +audio.play('voice/rules'); await flush(); +assert(fetchLog.some((u) => String(u).includes('/voice/male/rules.')), 'settings.voice=male 按男性目录解析(§7.3)'); settings.set({ speed: 1.7 }); -audio.play('voice/rules'); -await flush(); -assert(audio._voice && audio._voice.st.tempo === 1.7, 'settings.speed=1.7 时下一句 tempo=1.7(SoundTouch 不变调)'); +audio.play('voice/rules'); await flush(); +assert(audio._voice && audio._voice.st.tempo === 1.7, 'settings.speed=1.7 下一句 tempo=1.7(§7.3)'); settings.set({ voice: 'female', speed: 1.0 }); const bgmFetchMark = fetchLog.length; settings.set({ bgm: 0 }); -audio.play('bgm/menu'); -await flush(); +audio.play('bgm/menu'); await flush(); assert(!fetchLog.slice(bgmFetchMark).some((u) => String(u).includes('/bgm/menu')), 'bgm=0 时 BGM 不加载(§7.3)'); -assert(audio._bgm === null && audio._bgmWanted && audio._bgmWanted.id === 'bgm/menu', 'bgm=0 只记"想播"意图'); -settings.set({ bgm: 1.0 }); -await flush(); +settings.set({ bgm: 1.0 }); await flush(); assert(audio._bgm && audio._bgm.id === 'bgm/menu', 'bgm 调回非 0 自动补播最后状态 BGM'); -// ================= 场景 F:live round2 判定治理(序曲 tap 撤销 / 超时复活 / 下滑不弹菜单 / 菜单去抖) ================= -console.log('\n[场景 F] live round2:tapcancel 撤销 / 超时 Miss 复活 / 正片下滑不弹菜单 / 菜单去抖独占'); -// —— F3 教学:tapcancel 撤销序曲 tap 尝试记录(转滑动/长按的序曲不算出手)—— -await newWorld(); -game.start(); -await flush(); -G('tap'); // 跳 welcome(延迟 500ms 裁决) -await advance(600); -await waitFor(() => game.phase === 'tut' && game._cues.length > 0, 'F3 T1 领奏'); -await runUntil(game._cues[0].expected); -G('tap'); // 序曲 tap(T1 是 tap 音符,先记尝试) -await flush(); -assert(game._tutAttempts.length === 1, 'F3 序曲 tap 先记教学尝试'); -G('tapcancel'); // 引擎补发:本次按下转滑动/长按 → 序曲 tap 作废 -await flush(); -assert(game._tutAttempts.length === 0, 'F3 tapcancel 撤销教学尝试记录(live round2 P0)'); - -// —— 正片判定治理:钩子直进 song2(105BPM,含 3 个 down 音符)—— -await newWorld({ tutorialDone: true, unlocked: 3, grades: {}, bestCombo: 0 }); -game.start(); -await flush(); -await waitFor(() => game.phase === 'select', 'F 进入选曲'); -game._startSong(game._runId, 1); -await flush(); -await waitFor(() => game.phase === 'main' && game._cues.length > 0, 'F song2 排程就绪'); - -// —— F1 tapcancel 撤销序曲 tap 命中(声音已出收不回,判定必须撤销)—— -const fTapCue = game._cues.find((c) => c.gesture === 'tap'); -await runUntil(fTapCue.expected); -G('tap'); -await flush(); -assert(fTapCue.result === 'perfect', 'F1 序曲 tap 先按 tap 音符判定(Perfect)'); -assert(game._tapJudg.length === 1, 'F1 序曲 tap 判定已记账'); -G('tapcancel'); // 引擎补发:转滑动/长按 → 序曲 tap 作废 -await flush(); -assert(fTapCue.judged === false && fTapCue.result === null, 'F1 tapcancel 撤销序曲 tap 判定(音符回到未判定,不计 Miss)'); -assert(game._stats.perfect === 0 && game._stats.combo === 0, 'F1 命中统计回滚(perfect/combo 归零)'); -await runUntil(fTapCue.expected + 0.2); -G('tap'); // 重新接打(撤销不是免判) -await flush(); -assert(fTapCue.result === 'good', 'F1 撤销后重新接打 → Good'); - -// —— F2 双击序曲治理(tap+tap+doubletap 序列的判定归属)—— -const fDblCue = game._cues.find((c) => !c.judged && c.gesture === 'doubletap'); // phrase2 beat2 -await runUntil(fDblCue.expected - 0.2); -G('tap'); // 序曲 tap1(双击第一下按下,自由演奏) -await flush(); -await advance(200); -G('tap'); // 序曲 tap2(双击第二下按下) -await flush(); -await advance(250); -const fMiss2 = game._stats.miss; -G('doubletap'); // 双击改判到达(识别在第二次 touchend) -await flush(); -assert(fDblCue.result === 'perfect', 'F2 双击改判 Perfect(序曲 tap 撤销后判定)'); -assert(game._stats.miss === fMiss2, 'F2 序曲 tap 不产生 Miss(爆音不进判定链)'); -assert(fTapCue.result === 'good', 'F2 双击撤销窗不误伤更早的真实 tap 命中'); -assert(game._tapJudg.length === 1, 'F2 两笔序曲 tap 账已撤销(账簿只剩真实命中)'); - -// —— F2c 序曲 tap 误认领命中 → 双击到达时撤销;撤销后真不接 → 超时 Miss 兜底 —— -const fTapCue2 = game._cues.find((c) => !c.judged && c.gesture === 'tap'); // phrase2 beat3 -await runUntil(fTapCue2.expected); -G('tap'); // 序曲 tap1 误认领(其实是双击第一下) -await flush(); -assert(fTapCue2.result === 'perfect', 'F2c 序曲 tap 先认领命中'); -const fPerf2c = game._stats.perfect; -await advance(200); -G('tap'); // 序曲 tap2(自由演奏) -await flush(); -await advance(250); -G('doubletap'); // 双击改判 → 两笔序曲判定一并撤销(本次双击附近无 doubletap 音符,自由演奏) -await flush(); -assert(fTapCue2.judged === false && fTapCue2.result === null, 'F2c 双击到达撤销序曲 tap 误认领的命中'); -assert(game._stats.perfect === fPerf2c - 1, 'F2c Perfect 统计回滚'); -await runUntil(fTapCue2.expected + 0.55); // 越过 tap 超时死线(+0.45) -await flush(); -assert(fTapCue2.result === 'miss', 'F2c 撤销后真不接 → 超时 Miss 兜底(撤销不是免死)'); - -// —— F4 超时 Miss 复活:识别延迟把音符拖死、手势还在路上(live round2 事件流实证)—— -const fUpCue = game._cues.find((c) => !c.judged && c.gesture === 'up'); // phrase3 beat1 -await runUntil(fUpCue.expected + 0.64); // 超时死线 +0.60 已过:泵先判 Miss(滑动回调尚未到达) -await flush(); -assert(fUpCue.result === 'miss' && fUpCue.missKind === 'timeout', 'F4 上滑音符先被超时拖死(事件流同款时序)'); -const fMiss4 = game._stats.miss; -G('up'); // 慢滑回调此刻才到(触摸 expected+0.49、触程 0.15) -await flush(); -assert(fUpCue.result === 'good', 'F4 超时误判复活改判 Good(补偿 dt=+0.49 ≤ REVIVE_DT_S)'); -assert(game._stats.miss === fMiss4 - 1, 'F4 复活回滚 Miss 统计'); - -// —— F5 正片踩点下滑不弹菜单(live round2 P1 续修:超时拖死 ≠ 无临近 down 音符)—— -const fDownCue = game._cues.find((c) => !c.judged && c.gesture === 'down'); // phrase3 beat3 -await runUntil(fDownCue.expected + 0.64); // 死线 +0.60 已过,下滑回调 +0.64 才到(round2 实测差 2~114ms) -await flush(); -assert(fDownCue.result === 'miss' && fDownCue.missKind === 'timeout', 'F5 down 音符先被超时拖死'); -const fTomMark = playLog.length; -G('down'); -await flush(); -assert(game.phase === 'main', 'F5 正片踩点下滑不弹菜单(live round2 P1 续修)'); -assert(fDownCue.result === 'good', 'F5 下滑复活改判 Good'); -assert(playLog.slice(fTomMark).some((p) => p.id === 'sfx/inst_tom_lo'), 'F5 下滑复活播放 inst_tom_lo'); - -// —— F5b 窗口内晚下滑(超复活时差上限):不弹菜单、Miss 维持、tom_lo 自由演奏 —— -const fDownCue2 = game._cues.find((c) => !c.judged && c.gesture === 'down'); // phrase7 beat3 -await runUntil(fDownCue2.expected + 1.1); // 死线过 ~0.5s(账龄仍在 REVIVE_WINDOW_S 内),dt 超 REVIVE_DT_S -await flush(); -assert(fDownCue2.result === 'miss', 'F5b down 音符先被超时拖死'); -const fTomMark2 = playLog.length; -G('down'); -await flush(); -assert(game.phase === 'main', 'F5b 窗口内晚下滑仍不弹菜单(瞄准 down 音符证据明确)'); -assert(fDownCue2.result === 'miss', 'F5b 超复活时差上限,Miss 维持不复活'); -assert(playLog.slice(fTomMark2).some((p) => p.id === 'sfx/inst_tom_lo'), 'F5b tom_lo 自由演奏出声'); - -// —— F5c 窗口过后下滑正常呼出菜单(折中不破)—— -await advance(300); // downCue2 的 Miss 账龄 > REVIVE_WINDOW_S -G('down'); -await flush(); -assert(game.phase === 'menu', 'F5c 窗口过后下滑正常呼出菜单'); - -// —— F6 菜单去抖独占期(live round2 P1 瞬穿):开菜单 1s 内手势不穿透 —— -assert(audio.isVoicePlaying(), 'F6 菜单语音 menu_main 在播'); -G('doubletap'); // 开菜单 <1s:路上手势(双击)不得穿透菜单确认 -await flush(); -assert(game.phase === 'menu', 'F6 去抖期内双击不穿透菜单(phase 保持 menu)'); -assert(audio.isVoicePlaying(), 'F6 去抖期内菜单语音不被掐断'); -G('down'); // 去抖期内下滑也不得关菜单 -await flush(); -assert(game.phase === 'menu', 'F6 去抖期内下滑不穿透菜单'); -await advance(1100); // 越过 1s 去抖独占期 -G('doubletap'); // 双击确认"继续演奏" -await flush(); -assert(game.phase === 'main', 'F6 去抖期后双击正常确认(恢复演奏)'); - -// ================= 资产引用校验(fs:ogg+m4a 双格式零缺失) ================= -console.log('\n[资产校验] game.js 引用的 id 对照 audio/ 目录'); +// ================= 资产引用校验(A20 前半:fs ogg+m4a 双格式零缺失) ================= +console.log('\n[资产校验 A20] game.js 引用的 id 对照 audio/ 目录'); { const src = readFileSync(new URL('../game.js', import.meta.url), 'utf8'); const ids = new Set(); @@ -697,11 +744,11 @@ console.log('\n[资产校验] game.js 引用的 id 对照 audio/ 目录'); } } console.log(` 共引用 ${ids.size} 个资产 id`); - assert(missing.length === 0, `资产零缺失${missing.length ? '——缺失: ' + missing.join(', ') : ''}`); + assert(missing.length === 0, `A20 资产零缺失${missing.length ? '——缺失: ' + missing.join(', ') : ''}`); } // ================= 汇总 ================= console.log('\n========== 汇总 =========='); -assert(uncaught.length === 0, `全程无未捕获异常(§7.2)${uncaught.length ? '——' + uncaught.map(String).join('; ') : ''}`); -console.log(`通过 ${passed} 项,失败 ${failed} 项`); +assert(uncaught.length === 0, `全程无未捕获异常${uncaught.length ? '——' + uncaught.map(String).join('; ') : ''}`); +console.log(`通过 ${passed} 项,失败 ${failed} 项,注入手势/按键 ${gestures} 次`); process.exit(failed > 0 ? 1 : 0);