# 更新日志 版本号同时出现在 `package.json`、`client.js` 的 `VERSION` 常量与设置卡片右下角,三处必须一致 (`node --test` 会校验前两处)。 > 0.1.0 – 0.1.9 是 2026-10-02 至 2026-10-06 之间的密集迭代,当时未逐版记录日期, > 这里按版本列出各自的改动。 ## 0.2.7 — 2026-10-13 ### 变更 - **MiMo 音色响度对齐其它音色**:v0.2.5 起该音色沿用 mimo 原文的绝对增益 0.13 / 0.3 (mimo 本体没有音量概念),比本项目其它音色(sine 0.88 / piano 0.70 / chime 0.79 满音量峰值)轻约 7~13dB —— 切到 MiMo 会明显偏小。现新增两个**生效峰值**常量 `MIMO_HIT_PEAK = PEAK_FACTOR`(强音档:与 sine 100% 等峰)与 `MIMO_HOVER_PEAK = 0.6` (弱音档:取其 7 成),单频、刮弦、彩蛋旋律、快速甩动所有路径一次抬齐; 两档仍保留「点击比划过响」的层级(约 +3.4dB)。原文常量降为记录值不再直接使用。 音色、音高、包络形状、双档比例逻辑均不变,只抬电平;两档满音量仍 <1 不削波。 这是本项目对原文的一处**响度适配**(README「响度」行同步改写)。 ### 验证 - 测试 42 例全绿(电平断言改钉生效峰值,新增「峰值对齐 PEAK_FACTOR / 0.6」「两档都高于 原文裸值」「强 > 弱」「<1 不削波」断言)。反向注入:两个 `*_PEAK` 任一被改回原文裸值 → 电平断言失败。版本三处同步 0.2.7。实机需与其他音色来回切换试听响度是否接近。 ## 0.2.6 — 2026-10-13 ### 新增 - **彩蛋旋律(mimo 音色专用)**:逐字移植原文的 `S/U/V` 与 `R/x/Y/k/j` 状态机 (`createMimoEgg`)—— 划过导轨时掷骰:每轮互动只掷一次(离开导轨重新获得资格)、 冷却 180s、概率 5%;命中随机选一首,**整曲由后续手势逐音奏出**(原文语义:每次 hover/hit 推进一个音符,而不是后台自动播放),播完整曲自动结束并重计冷却。 曲库 `MIMO_EGG_SONGS` 5 首逐音预解析(唱名/音名记法 → 频率数组 + 秒数,休止 = 空数组): ① 致爱丽丝(76bpm·40 音·19.7s)② F# 小调练习曲(150bpm·77 音·20.4s,含 2 个和弦) ③ 卡农式主题(120bpm·105 音·20.0s,原文由 g/P 两段交错组合)④ 波莱罗片段(72bpm·34 音) ⑤ 欢乐颂变奏(72bpm·82 音·20.2s)。原文被 `H=()=>!1` 封死的 `q/w()` 短句路径依旧不移植 (shipped 代码不可达)。 - **刮弦合成器 `playMimoStrum`**:逐字移植原文 `i(freqs, strong, ring)` —— 每频率叠 5 个 分音(基频三角波 + 2/3/4/6 次正弦泛音,增益 [1, .42, .22, .12, .06]),并联进 master gain 后串 lowpass(4200Hz 在 0.85F 处指数扫到 1400Hz),峰值按 `1/√len` 归一(多音同响不削波), 总时长 `F = clamp(0.25+ring, 0.4, 2.6)`、停振 +0.05s,首分音失谐 1.5 音分;休止(空频率) 不建节点。彩蛋激活期间由它奏出旋律音;和弦 token 会一次叠满 5×N 个振荡器。 - **快速滑动改播刮弦音(本项目增强,README 标注)**:mimo 原文对「窗口内速度 ≥600px/s」 的划过是**静音**的;本项目的门槛裁决由布尔改为三态(`'mute' / 'strum' / 'pass'`), 起步 100ms 窗内保持原文静音,快速甩动则改派**单频刮弦音**(`ring=1.1` 默认值,弱音档) —— 飞速翻阅导轨时不再无声,而是一串刮弦。快速帧不掷骰也不推进彩蛋(原文根本不会 调 `hover()`,彩蛋语义保持不变)。 ### 变更 - 合成器分发:`playMimo` 见 `meta.strum = {freqs, ring, strong}` 即转 `playMimoStrum`, 常规路径不变;既有三音色零改动。 - 监听层 `resetAnchor` 新增 `env.egg.endEngagement()`(原文离开导轨时 `k=false`); `disposeAll` 新增 `mimoEgg.reset()`(原文组件卸载 `resetEgg()`:打断则冷却从当下重算)。 - `__dshRailTones.status()` 新增 `egg` 字段(`{active, songIndex, noteIndex, loops, rolled, lastFinishedAt}`); `log()` 每条新增 `egg` / `strum` 标记。 ### 验证 - 测试 38 → 42 例:门槛三态裁决全谱(含 583/600/617px/s 临界与单帧语义)、刮弦形状 (分音矩阵逐字、`1/√len` 归一、lowpass 4200→1400、detune 只作用首分音、休止早退、 clamp 边界、停振点)、彩蛋状态机(桩 random:5% 掷骰一次性、180s 冷却、逐音推进与强音 推进、曲库完整性 + 致爱丽丝开头 9 音、播完自结束、打断推冷却)、走完整 apply 的两条 端到端(快速甩动 → 刮弦音;命中 → 逐音推进旋律)。 - **8 项反向注入全部被捕获**:分音矩阵改错 / √len 删除 / lowpass 终点改 2000 / detune 扩散 全部分音 / 休止不早退 / 概率改 1 / 冷却改 0 / 快速分支退回静音 —— 每项都有用例失败。 - 版本号三处同步 0.2.6。**实机验收待做**:选 MiMo 音色反复划过导轨攒「互动轮次」, 每轮 5% 概率触发一段旋律(每次划过/点击推进一个音);快速甩动应听到刮弦音而非静音。 ## 0.2.5 — 2026-10-13 ### 修复(v0.2.4「MiMo 复刻」听感相差甚远的 5 处根因) 用户实测反馈「mimo 复刻效果与实际效果相差甚远」。逐行比对 `SessionSpine-CiwyKkwv.js` 原文后 定位到 5 处偏差,v0.2.5 全部按原文改正(该音色自此改为**逐行移植**,设置项名称也由 「MiMo 复刻」改为「MiMo」): 1. **音高映射用错了分母与分子(最主要)**:原文 `s(d,m)` 的 `d` 是**刻度下标**、`m` 是**刻度总数**; v0.2.4 却把 0…1 的指针位置当 `d`、又把本项目音级数 `DEGREES = 10` 当 `m` —— 于是 `C = round(pos/9×18) = round(pos×2)`,**整条 18 档阶梯被压进最低 3 个半音** (0.5 处只有 146.83Hz,而原文 5 刻度时 0.5 在 A4 440Hz 附近)。现在 `emit` 把 `data-index` 与刻度总数一起交给调度器,按原文公式逐档铺满(5 刻度 → C3 C4 A4 A5 G6)。 2. **滑动速度门槛方向写反**:原文 `Ve` 返回 `speed < 600px/s` —— 是「**慢速放行、快速甩动静音**」 (防止飞速划过时音符连成一片);v0.2.4 写成「≥100px/s 才放行」,方向完全相反,还自创了 阈值 100 与「停顿时重新起手」的差异。现在常量回归原文 `100 / 80 / 600`(进入窗口 / 取样窗口 / 速度上限),只在 `reset()`(真离开导轨 / blur,原文 `fe`)时复位采样。 3. **点击刻度走错包络档**:原文 `hit` 里被 `H = () => !1` 封掉的只是彩蛋短句分支,紧随其后的 `p(s(d,m), !0)` 是**正常路径** —— 点击本就该响 0.9s 的强音(0.3)。v0.2.4 误判成 「点击也是弱音」并加了 `MIMO_HIT_ON_STRONG_GESTURE = false`,导致点击不响亮、没有确认感。 现在 `meta.strong`(按下 / 键盘激活 / 试听 = 明确手势)→ 强音档,划过 / 拖拽 / 滚轮 → 弱音档。 4. **响度被放大 2.93×**:v0.2.4 把原文绝对增益 0.13 / 0.3 乘了 `PEAK_FACTOR/0.3` 换算成 「100% = 0.88」。现在峰值就是原文的 0.13 / 0.3,**音量 100% = 与 mimo 等响**。 5. **去重粒度不对**:原文按**刻度下标**去重(`c !== w.current`,每个刻度各响一声);v0.2.4 按 本项目 10 档音级去重,长会话里相邻刻度被合并、漏音。新增 `dedupKeyFor`:mimo 用刻度下标、 其它音色仍按音级;**监听层与控制器注入同一个 `keyFor`**(只改控制器不改监听层的话, mimo 会在控制器之前就被 10 档闸门拦掉)。 ### 变更 - 调度器 `pitchAt` 签名由 `(degree, position)` 改为 `pitchAt(context)`, `context = { degree, position, tick, tickCount, source }`;`resolvePitch(pitch, context)` 随之调整。 - 监听层 `signal` 第 5 参由 `gesture {x,y}` 扩展为 `context {x, y, tick, tickCount, hover}`: `tick / tickCount` 对所有来源都有意义(音高要用),`hover` 决定是否过速度门槛。 - `tickAt()` 返回值新增 `count`:虚拟滚动下取 `max(渲染刻度数, 最大 data-index + 1)` (原文用消息数组长度;本项目只能从 DOM 估计,README 已注明)。 - 新增控制器注入 `keyFor(degree, tick)`;新增导出 `mimoHzFor` / `dedupKeyFor` / `MIMO_GATE_ENTRY_MS` / `MIMO_GATE_WINDOW_MS` / `MIMO_GATE_MAX_SPEED` / `MIMO_STOP_PAD_S`。 - 删除 `MIMO_PEAK_SCALE` / `MIMO_HOVER_PEAK` / `MIMO_HIT_PEAK` / `MIMO_HIT_ON_STRONG_GESTURE` / `MIMO_GATE_MS` / `MIMO_GATE_SPEED`(被原文常量取代)。 - 试听改为带 `{ repeat: true }`:模拟「点击刻度」,让用户听到强音档。 ### 验证 - 测试 33 → 36 例,全部按移植语义重写:音高(5 刻度逐档 = 130.815 / 261.63 / 440 / 880 / 1568, 18 刻度顶档 1568,单刻度 `max(1,m-1)` 退化,单调性与非法输入)、音高兜底 (有刻度用刻度、无刻度退化为铺满形态)、速度门槛(原文三常量、进入 100ms 静音、 **慢速放行 / 1000px/s 静音 / 恰好 600px/s 静音 / 583px/s 放行**、单帧放行、`reset()` 后重计时、 `stats()`)、合成器(三角波、hover 0.13/0.42/0.47、点击 0.3/0.9/0.95、强音明显响于弱音、 滚轮走弱音、音量线性、音量 0 静音)、调度器(context 形式映射、映射抛错回退、 去重键可注入的两档粒度、`meta.strong` 只在 repeat 时为真、门槛只作用于 `hover`)、 监听层(`tickCount` / `hover` 标记透传、mimo 相邻刻度各自发声、sine 同刻度不响、离开复位门槛)。 - 版本号三处同步 0.2.5(`package.json` / `client.js VERSION` / 设置卡片 footer)。 - **实机验收待做**:`node --check` + `node --test`(36/36)只证明逻辑与契约; 听感需重启 DSH Desktop 后在设置卡片确认 `dsh-rail-tones v0.2.5`,再选「MiMo」实测: 慢速划过每刻度一声(随刻度铺满 3.6 个八度)、快速甩动静音、点击刻度是更长的强音、 100% 音量与 mimo 等响。 ## 0.2.4 — 2026-10-13 > ⚠️ 本版的「MiMo 复刻」音色有 5 处实现偏差(音高映射、门槛方向、点击包络、响度换算、去重粒度), > 实测听感与 mimo 相差甚远。**已被 0.2.5 全部修正**(音色改名为「MiMo」),下面的描述只保留 > 历史记录,勿据此理解当前行为 —— 请看 0.2.5 条目。 ### 新增 - **MiMo 复刻音色(`mimo`)**:逐项照抄逆向出的 mimo 导航条发声函数 (`resources/app.asar` → `out/renderer/assets/SessionSpine-CiwyKkwv.js` 里那个返回 `{hover, hit, endEngagement, resetEgg}` 的 IIFE): - 波形:单个 **三角波** 振荡器(mimo `F.type="triangle"`)—— 3 次谐波 ≈ −19dB, 这正是比对报告 2 里那枚「突出的高次亮泛音」,此前的 `chime` 是用 1.5× 分音近似它的; - 包络:`1e-4` → 8ms 指数升到峰值 → 指数衰减回 `1e-4`,弱音档 0.13 / 0.42s(停振 +0.46s), 强音档 0.3 / 0.9s(停振 +0.95s); - 音高:C 大调五声 `[261.63,293.66,329.63,392,440]`(C4 D4 E4 G4 A4), 档位 `round(pos/(n-1) × 18)`、`× 2^(⌊C/5⌋−1)` → 18 档、含向下一个八度(C3 起); - 滑动速度门槛:起手后 ≥100ms 且最近位移速度 ≥100px/s 才发声,慢速悬停静音(mimo 的 `Ve`)。 - **音色条目可自带音高映射**:注册表条目新增可选 `pitchAt`,`createController` 的 `pitchAt` 注入优先于全局 `noteHz`。既保证其它音色行为一字不改(缺省回退 `noteHz`), 又让「音色 + 音高体系」作为一个整体扩展 —— mimo 的 C 大调五声与本项目的 A 大调五声 是两套体系,只换音色不换映射会得到不伦不类的听感。 ### 变更 - **`engine.play(hz)` → `engine.play(hz, meta)`**,并把 `meta` 作为第 4 参传给合成器 (`synth(audio, hz, volume, meta)`)。既有三个合成器完全忽略该参数,签名向后兼容。 - **`attachRailListeners` 的 `signal` 增加第 5 参 gesture**:只有 `pointermove`(划过) 带 `{x, y}`,点击 / 键盘 / 滚轮 / 试听不带 —— 对应「速度门槛只作用于悬停」。 同时 `env` 新增可选 `gate`,在清去重锚点(真离开导轨 / 换导轨)时一并 `reset()` 采样。 ### 决策与取舍 - **默认只启用弱音档,与 mimo 一致**:逆向发现 mimo `hit()` 里那条强音分支的开关是 `H = () => !1`,**恒假** —— mimo 导轨上的真实发声只有一种形态,**点击刻度也是 0.42s 弱音**。 为「效果一致」,本项目保留 `MIMO_HIT_ON_STRONG_GESTURE = false`;想要点击更响可改成 `true` (那是本项目增强,不再是 mimo 行为)。 - **音量换算**:mimo 的 0.13 / 0.3 是绝对增益(它的 AudioContext 无音量概念)。本项目沿用 「音量 100% 时包络峰值 = `PEAK_FACTOR` 0.88」的既有约定,再按 mimo 的 0.13 : 0.3 分档 (`MIMO_PEAK_SCALE = PEAK_FACTOR / 0.3`),于是 100% 音量手感与其它音色一致 (sine 0.88 / piano 0.70 / chime 0.79 / mimo 0.88,弱音档 ≈ 0.381),且不削波。 - **两处有意差异(音频一致、交互不退化,README 有专节)**: 1. 音高按**指针位置**换算而非 mimo 的刻度总数 —— 本项目以刻度为单位(位置 = 刻度中线 / 内容高度),两者都是连续升序音阶; 2. 滑动门槛的重新起手改为「距上次划过事件超过 `max(门槛, 取样窗口) + 20ms` 即视为停顿」。 mimo 把 `entryAt` 只在 `pointerover`/离开时复位,而取样窗口只有 80ms:**照搬会让 指针事件节奏慢于窗口时永远攒不满两个采样**,速度判不出来、慢速悬停被误放行 (实测复现后改为此式;持续划过时两者行为一致)。 - **未复刻**:彩蛋旋律(每互动周期 5% 概率、180s 冷却,播《致爱丽丝》等 5 首)与快速滑动的 多音刮弦音(泛音堆叠 + 低通 4200→1400Hz 扫频)—— 前者是产品彩蛋、后者是另一套发声路径。 ### 验证 - 测试 24 → 33 例,新增 9 例:mimo 音高映射(含单调性、钳制、非法输入)、滑动速度门槛 (快速划过放行 / 慢速悬停静音 / 停顿后重新起手 / 复位后重新计时 / 缺手势一律放行)、 mimo 合成器形状(三角波、三段包络值、两档常量、停振点、音量线性、音量 0 静音、不削波)、 存量音色忽略第 4 参 meta(回归保护)、调度器的映射优先级与抛错回退、 `pitchForVoice`/`resolvePitch` 的音色选择、门槛只作用于带手势上下文的划过、 监听层把手势坐标交给调度器且离开时复位、以及**走完整 apply** 的 `debug().play()` 音高断言 (这一条专门盯 apply 里 `pitchForVoice(state.get().voice)` 的接线)。 - 契约更新:注册表顺序断言改为 `['sine','piano','chime','mimo']`,版本号三处同步 0.2.4。 - **反向注入验证(4 项,均确认用例会失败)**:① `MIMO_GATE_SPEED` 改 0 → 慢速悬停用例失败; ② 合成器波形改回 `sine` → 三角波断言失败;③ apply 里 `pitchAt` 改成 `() => null` → 走完整 apply 的音高断言失败;④ 注册表条目 `synth` 置为 undefined → 派生断言失败。 - **实机验收待做**:`node --check client.js` / `node --test test/`(33/33 全绿)只证明逻辑与产物契约; 「与 mimo 听感一致」需在 DSH Desktop 上选 MiMo 复刻音色实际划过 / 点击确认 (设置卡片右下角应显示 `dsh-rail-tones v0.2.4`)。 - 过程记录:本轮实现曾用 PowerShell `Set-Content` 做临时注入验证,破坏了 client.js 的 UTF-8 多字节字符(语法直接失败),随后误用 `git checkout` 把未提交的实现一并回滚。最终改为 **用 node 脚本(UTF-8 读写)做注入、并在每次注入后立即还原**,四类注入验证才可信。 ## 0.2.3 — 2026-10-10 ### 修复 - **suspended 上下文不再堆叠音符**:`play()` 在 `audio.state !== 'running'` 时直接丢弃本次 调度并返回 false。此前 `ensure()` 只发起 `resume()`(异步、不等待)就照常调度,而 suspended 时 `currentTime` 是冻结的 —— 整段时间窗内的音符全部堆在同一个音频时间戳上, 上下文恢复的瞬间一齐鸣响(实测形态:滚轮翻阅几秒后第一次点击炸出一串和弦)。 滚轮在浏览器里**不被计为用户激活手势**,`onWheel` 此前也不 `arm()`,所以「纯滚轮起手」 的用户第一声必落进 suspended;现在 `onWheel` 也补一次 `arm()` 尝试唤醒。 取舍:唤醒失败时**少响一声**,也好过恢复后炸响一片。 - **设置卡片不再被重复注册误报成失败**:`ctx.inject(['slots'], …)` 在服务重复就绪时会 再次触发回调,`startSettings` 于是开出一个全新的 `registerSettingsSection` 闭包(连同 全新的 `status`),对同一个 `id: 'rail-tones'` 再 `slots.register` 必抛错 → 内层 12 次 重试全失败 → 上报 `settings-failed`,而界面上卡片其实一直是好的(自报通道假阴性)。 apply 层加 `settingsRegistered` 守卫,**只挡已成功的路径**:失败 / 未尝试时二次进入 恰好是恢复途径,不能挡。 ### 变更 - **发声结果回传诊断通道**:`engine.play()` 的返回值此前在 `controller` 的 `play` 回调里 被丢弃,音量为 0、上下文不可用、suspended 吞掉的声音在 `__dshRailTones.log()` 里与 真正出声的记录**完全无法区分** —— 用户报「没声音」时自报通道恰好在这类最常见故障上失明。 现在 `log()` 每条带 `played: true/false`,`status()` 新增 `mutedCount`(被音频层丢弃的 调度计数);`play()` 调用也包进 `try/catch`,合成器抛错同样计入而不打断回调链。 ### 新增 - 测试 23 → 24 例,另加一条派生断言: (1) **注册表↔合成器派生断言**(并入「清单契约」用例)—— 遍历 `VOICE_OPTIONS` 每个 id,断言 `SYNTHESIZERS` 源码里有对应项(含默认音色)。此前新增音效只改注册表、忘了挂合成器会被 `play()` 的 兜底静默吞掉(响成 sine 而不是报错),漂移无法暴露。 (2) **suspended 回归用例**(新增一例)—— 假上下文 `state='suspended'` 时断言 `play()` 返回 false 且**一个振荡器都不创建**,改回 running 后恢复正常。 两项修复均做反向注入验证:删掉 chime 合成器 → 派生断言失败;去掉 state 检查 → suspended 用例失败。 - README 调试段同步:`status()` 示例补 `mutedCount`、`log()` 示例补 `played`, 排查清单新增一条「没声音先看 `mutedCount` 增长 + `played:false` → 查 `audioState`」; 「已知限制」里 v0.2.1 前遗留的「音色为合成正弦」等过时描述一并更新。 ## 0.2.2 — 2026-10-10 ### 新增 - **清音(chime)音色**:`createEngine` 新增 `playChime` —— 主音(基频 + 1.5× 钟类质感分音, 权重 `[1, 0.041]`)+ **上方五度 40ms 引导音**(复刻实测的快速下行琶音:mimo 每次发声前 有一个 ≈40ms 的前级短音,精细频率跟踪测得 586→523→390→327→260Hz 逐级下行)。 30ms 软起音、master 指数衰减到 −80dB(−20dB 点 ≈ 0.22s,实测 215ms),峰值 = 音量 × 0.88 × 0.9,权重按 Σ 归一不削波。参数逆向自对 mimo 桌面端提示音的实测比对, 听感为「叮-咚」式水滴软钟音。 - **验证过程发现的两个误判(已修正)**:(1) 初版以为的高次「6×f0 亮泛音」实为录屏背景声 污染——对齐到真实音起点后 6×f0 处仅剩 −38dB;(2) 1.19×f0 处的强峰也不是分音,而是 录屏里人声/背景音恰好落在 G4(琶音前级音频率)。逐帧频率跟踪(50ms 窗)确认了 「下行琶音」才是真实结构。 - 注册表追加第三项:`VOICE_OPTIONS` 顺序即设置页「音效选择」列表顺序(正弦音 / 钢琴 / 清音); 存储、UI、分发仍零改动,`__dshRailTones.setVoice('chime')` 可直接切换。 - 测试 22 → 23 例:清音引擎用例(主音 2 分音 + 引导音频率/权重归一/master 峰值/ 起音余韵区间/引导音短促性)+ 注册表顺序断言。sine 与 piano 的既有断言一字未动(回归保护)。 - 离线听感复核(同一分析管线):t20 225ms vs 实测 215ms;1.5× 分音 + 引导音落在 实测同位置(−25dB vs 实测 −4.5dB 处的琶音残响+背景能量);谱质心差异来自录屏背景 噪声底(未刻意模拟)。 ## 0.2.1 — 2026-10-10 ### 变更 - **「钢琴音色」开关 → 「音效选择」展开列表**:设置页该行改为点击展开的候选列表 (当前音效名 + ▾ 按钮,面板逐项显示名称与简介,当前项打勾高亮;点选即生效并收起, 点击面板外或按 Esc 收起)。 - **音效注册表**:新增模块级 `VOICE_OPTIONS`(每项 `{id, labelKey, descKey}`,顺序即列表顺序)。 存储白名单(`normalizeVoice`)、设置页列表、`VOICES` 符号表全部由它派生; 引擎分发查 `SYNTHESIZERS` 注册表,注册表外的未知 id 兜底默认合成器、绝不抛错。 **新增音效 = 注册表加一条 + 挂一个合成函数 + 中英文案各一条,UI 与存储代码零改动** —— 为后续更多音效与自定义音效预留的扩展点。 - 测试 21 → 22 例(注册表结构断言并入音色用例;新增「未知音色 id 兜底默认合成器」)。 存储值域不变(`sine | piano`),存量数据不受影响。 ## 0.2.0 — 2026-10-10 ### 新增 - **钢琴音色**:`createEngine` 在正弦音之外新增 `playPiano` —— 6 个 `sine` 分音并联进一条 master gain,权重按 Σ 归一(任意时刻 |Σ| ≤ 峰值,不削波)、频率带轻微失谐 (`f·n·(1+0.0002·n²)`)、高次分音按各自 τ 更快衰减(先亮后暗的琴声瞬态), master 峰值 = 音量 × 0.88 × 0.8 ≈ 0.70。仍为零素材、零依赖的 Web Audio 实时合成。 - **设置页「钢琴音色」开关**:音量行下方新增一行 Switch,默认关闭(= 正弦音,不打扰存量 听感);切换即时生效,「试听」按钮直接验证当前音色。 - **`voice` 进设置存储**:`{enabled, volume, voice}`,读写都走 `{"sine","piano"}` 白名单, 非法/缺失回退默认 —— 存量数据没有该字段时自动落正弦音,向后兼容。 - **`__dshRailTones.setVoice('piano'|'sine')`**:与 `setVolume/setEnabled` 对称,返回生效值; `status()` 自动带上 `voice`。 - 测试 19 → 21 例:音色切换状态(默认值/白名单/存量兼容/持久化)与音频引擎 (假 AudioContext 断言正弦恰 1 振荡器、钢琴 6 分音 + master、失谐频率、权重归一、不削波、 音量 0 与取值抛错均静音)。 ## 0.1.14 — 2026-10-10 ### 变更 - **音量说明文案简化**:设置页音量行的描述由「提示音音量(0–100%,0% 为无声、100% 为最大; 整体响度较此前翻倍)。只影响导航条音效。」简化为「调整导航条音量大小」(英文同步为 "Adjust the volume of the navigation rail tones")。纯文案改动,音量行为不变。 ## 0.1.13 — 2026-10-10 ### 变更 - **音量范围由 0–200% 收窄为 0–100%**:`MAX_VOLUME` 2 → 1。0% 为无声、100% 为最大声音。 读取(含存量越界旧值钳到 100%)、`set()`、`__dshRailTones.setVolume()`、设置页滑杆四处 统一按新上限钳制。 - **整体响度翻倍**:包络峰值系数 `PEAK_FACTOR` 0.22 → 0.88,即 100% 时峰值为 0.88, 是旧 200% 上限峰值 0.44 的 2 倍 —— 0%→100% 的音量差为原 0%→200% 的 2 倍 (最大声音再响一倍),仍在满幅之内、不会削波。设置卡片文案同步改为「0–100%」。 ## 0.1.12 — 2026-10-06 ### 变更 - **音量上限从 100% 提到 200%**:不动系统音量也能把提示音调得更响。设置页滑杆、 `__dshRailTones.setVolume()`、写入与读取四处统一按新上限钳制;存量里越界的旧值 在读取时被钳到 200%。 - 包络峰值仍是 `音量 × 0.22`,200% 时为 0.44,留在满幅之内、不会削波。 ## 0.1.11 — 2026-10-06 ### 修复 - **同一刻度上轻微移动会重复发声**(约每 250–350ms 一声)。根因是注册监听器时的信号适配器 漏了 `return`,监听层拿不到调度结果,去重锚点从不更新 —— 「音级不变就不重响」的闸门 自 v0.1.5 引入起从未生效,此前四轮修复都只是在治它的症状。 - 新增回归测试「同一刻度上抖动只响一声」(共 19 例)。 ## 0.1.10 — 2026-10-06 ### 修复 - **重启后设置里的「导航条音效」卡片消失**:`ctx.get('locale')` 在服务未就绪时返回 `undefined`, 而守卫只挡住了 `null`,读取 `locale.register` 抛错打断了整条注册链。现在同时挡 `null` / `undefined`,且注册链整体抛错也会进入有界重试(放弃时上报 `settings-gave-up`)。 - **「点击 / 键盘激活即使同音级也给确认音」实际不成立**:适配器丢掉了第 4 个参数 `repeat`, 现已透传。 - 槽位已声明但注册被拒时,自报里的错误原文会被后续步骤覆盖成 `null`,现已保留。 - 按住拖动时蹭到悬停预览气泡,会让待决的「离开导轨」倒计时在拖拽途中清掉去重锚点、 导致同一音重放。 ## 0.1.9 ### 修复 - **同一刻度轻微移动仍会重复发声**:判定「离开导轨」过于急躁(宿主重渲染、悬停预览气泡 都会让指针目标短暂落到导轨之外)。现在所有「离开」信号都要持续 300ms 才清空去重锚点, 期间回到导轨立即作废。 - **相邻刻度分界线上抖动会来回跳档**:刻度吸附加入 4px 滞回,另一个刻度明显更近才切换。 - 诊断信息新增 `resets` / `lastReset` / `offRailPending`。 ## 0.1.8 ### 修复 - 重启后设置页卡片消失:`apply` 执行时 `ctx.slots` 可能尚未就绪,改用作用域注入等待服务就绪, 并对未就绪状态做有界重试。 ## 0.1.7 ### 修复 - 捕获并上报设置注册的异常原文;React 获取单独兜底。 ## 0.1.6 ### 新增 - 宿主半边自报通道:`GET /rail-tones/status`、`POST /rail-tones/report`(并镜像到 `~/.dsh/rail-tones-report.json`),让「重启后到底有没有挂载」在终端可读。 - 设置卡片注册改为有界重试。 ### 修复 - 市场热挂载产生重复实例、同一指针动作响多声:挂载前先销毁残留实例。 ## 0.1.5 ### 变更 - 去重改用**音级闸门**(会话位置分成 10 档):`degree` 不变就不发声,与刻度节点身份、 虚拟滚动、宿主重渲染全部解耦。 - 增加单实例闸门。 ## 0.1.4 ### 修复 - `pointerout` 把宿主重渲染(`relatedTarget === null`)误判为「离开导轨」而清空去重锚点。 - 增加位置兜底与自诊断(`instances` / `dedup` / `log`)。 ## 0.1.3 ### 变更 - 去重从「时间窗口」改为「刻度身份」(`data-index`)。 ## 0.1.2 ### 变更 - 音符吸附到刻度**中线**:同一刻度上四种手势得到同一个音。 - 收紧导轨识别:只认直接拥有刻度的 `nav`,避免外层包裹元素被当成第二条导轨。 ## 0.1.1 ### 新增 - 光标划过(悬停)导轨即发声;此前只有点击刻度才有声音。 ## 0.1.0 - 初版:点击刻度播放对应音高的提示音。