# 频线索引与输出频率的映射（实测）

阶段 1 第 0 步的产出：一把在调试"能出声但频率不对"时用来定位问题的尺子。
这张表不是推导出来的，是**测出来的**；生成命令与数据都在文末。

## 1. 结论

128 kbps / 44100 Hz 的一个 granule 有 576 条频率线，按**子带优先**排列：

```text
line k  ->  subband = k / 18,  bin = k % 18
subband 宽度 = 44100 / 64 = 689.0625 Hz
每个子带 18 条长块 bin，line 间隔 = 44100 / 1152 = 38.28125 Hz
```

因此"这条线对应多少赫兹"只有一个数字要记：

**`f(line) = (line + 0.5) × 38.28125 Hz` 是 bin 中心；用在实信号上时，`line ≈ f / 38.28125`，精度不超过一行（±38.28 Hz）。**

## 2. 为什么之前 line 11 和 line 12 都测出 459 Hz

这不是测量误差，也不是解码器的问题，而是**探针信号本身**的退化。

`tone` 命令在每一帧、每个 granule 里都写入同一组 576 条系数。采样率 44100 Hz、
子带采样率 1378.125 Hz，一个 granule 的 36 个 子带样本、hop 18，所以重复同一个
频谱会让子带信号变成周期为 18（或 18 的因子）的周期信号。周期 18 的信号在子带
采样率下的谐波间隔是 `1378.125 / 18 = 76.5625 Hz`，也就是**只有 38.28125 的偶数倍
能出现**，奇数 bin 根本不可探。

于是单线探针的实际落点是被"向上取到偶数线"的：

```text
line k  ->  2 * ceil(k / 2) * 38.28125 Hz
```

line 11 → 12 × 38.28125 = 459.375 Hz，line 12 → 459.375 Hz。两条线当然一样。

用相邻两条线同时激励（`tone 16 12,13 206 1`）也救不回来：输出峰仍然是
459.334 与 535.961 Hz，都在 76.5625 Hz 网格上。**这个探针能给上界，不能分辨奇数 bin。**

## 3. 实测表（line 0–35，逐条单线激励）

激励：`moon run cmd/main -- tone 16 <line> 206 1`，解码：libsndfile / mpg123，
峰值估计：16384 点 FFT + 对数幅度抛物线插值（bin 间隔 2.691 Hz，估计不确定度 ±1.346 Hz）。

| line | subband | bin | bin 中心 Hz | 粗网格模型 Hz | 实测 Hz | 误差 Hz | 峰值 |
| ---: | ------: | --: | ----------: | ------------: | ------: | ------: | ---: |
| 0 | 0 | 0 | 19.1406 | 0.0000 | 1.3458 | +1.3458 | 0.4141 |
| 1 | 0 | 1 | 57.4219 | 76.5625 | 76.5832 | +0.0207 | 0.5007 |
| 2 | 0 | 2 | 95.7031 | 76.5625 | 76.5846 | +0.0221 | 0.5020 |
| 3 | 0 | 3 | 133.9844 | 153.1250 | 153.1013 | -0.0237 | 0.5011 |
| 4 | 0 | 4 | 172.2656 | 153.1250 | 153.1018 | -0.0232 | 0.5080 |
| 5 | 0 | 5 | 210.5469 | 229.6875 | 229.7288 | +0.0413 | 0.5008 |
| 6 | 0 | 6 | 248.8281 | 229.6875 | 229.7299 | +0.0424 | 0.5065 |
| 7 | 0 | 7 | 287.1094 | 306.2500 | 306.2097 | -0.0403 | 0.5006 |
| 8 | 0 | 8 | 325.3906 | 306.2500 | 306.2105 | -0.0395 | 0.5006 |
| 9 | 0 | 9 | 363.6719 | 382.8125 | 382.8520 | +0.0395 | 0.5007 |
| 10 | 0 | 10 | 401.9531 | 382.8125 | 382.8528 | +0.0403 | 0.5019 |
| 11 | 0 | 11 | 440.2344 | 459.3750 | 459.3326 | -0.0424 | 0.5048 |
| 12 | 0 | 12 | 478.5156 | 459.3750 | 459.3337 | -0.0413 | 0.5033 |
| 13 | 0 | 13 | 516.7969 | 535.9375 | 535.9607 | +0.0232 | 0.5160 |
| 14 | 0 | 14 | 555.0781 | 535.9375 | 535.9612 | +0.0237 | 0.5270 |
| 15 | 0 | 15 | 593.3594 | 612.5000 | 612.4778 | -0.0222 | 0.5327 |
| 16 | 0 | 16 | 631.6406 | 612.5000 | 612.4794 | -0.0206 | 0.5472 |
| 17 | 0 | 17 | 669.9219 | 689.0625 | 689.0623 | -0.0002 | 0.5072 |
| 18 | 1 | 0 | 708.2031 | 689.0625 | 689.0627 | +0.0002 | 0.4997 |
| 19 | 1 | 1 | 746.4844 | 765.6250 | 765.6456 | +0.0206 | 0.5473 |
| 20 | 1 | 2 | 784.7656 | 765.6250 | 765.6473 | +0.0223 | 0.5332 |
| 21 | 1 | 3 | 823.0469 | 842.1875 | 842.1638 | -0.0237 | 0.5296 |
| 22 | 1 | 4 | 861.3281 | 842.1875 | 842.1643 | -0.0232 | 0.5153 |
| 23 | 1 | 5 | 899.6094 | 918.7500 | 918.7913 | +0.0413 | 0.5137 |
| 24 | 1 | 6 | 937.8906 | 918.7500 | 918.7924 | +0.0424 | 0.5057 |
| 25 | 1 | 7 | 976.1719 | 995.3125 | 995.2722 | -0.0403 | 0.5041 |
| 26 | 1 | 8 | 1014.4531 | 995.3125 | 995.2730 | -0.0395 | 0.5011 |
| 27 | 1 | 9 | 1052.7344 | 1071.8750 | 1071.9145 | +0.0395 | 0.5004 |
| 28 | 1 | 10 | 1091.0156 | 1071.8750 | 1071.9153 | +0.0403 | 0.5015 |
| 29 | 1 | 11 | 1129.2969 | 1148.4375 | 1148.3951 | -0.0424 | 0.5035 |
| 30 | 1 | 12 | 1167.5781 | 1148.4375 | 1148.3962 | -0.0413 | 0.5107 |
| 31 | 1 | 13 | 1205.8594 | 1225.0000 | 1225.0232 | +0.0232 | 0.5150 |
| 32 | 1 | 14 | 1244.1406 | 1225.0000 | 1225.0237 | +0.0237 | 0.5279 |
| 33 | 1 | 15 | 1282.4219 | 1301.5625 | 1301.5403 | -0.0222 | 0.5349 |
| 34 | 1 | 16 | 1320.7031 | 1301.5625 | 1301.5419 | -0.0206 | 0.5465 |
| 35 | 1 | 17 | 1358.9844 | 1378.1250 | 1378.1248 | -0.0002 | 0.5060 |

读法：

- **粗网格模型（`2 ceil(k/2) × 38.28125`）在 35/35 条线上都成立**，最大偏差 0.042 Hz，
  远小于峰值估计的 ±1.346 Hz 不确定度。这张表因此是可回归的，不是"看着差不多"。
- line 0 是 DC，去均值后被压掉，实测 1.3458 Hz 就是 0 Hz 的估计下限，按 DC 处理。
- 子带边界（line 17/18 = 689.0625 Hz，line 35 = 1378.125 Hz）精确落在
  `(subband + 1) × 689.0625`，说明子带划分与子带顺序没有错位。

## 4. 用这把尺子检查真实 PCM 路径

真实 PCM 不是重复单线，子带信号不再退化，因此可以用完整频率分辨率检查。
`src/mp3/analysis.mbt` 接上之后，`sine` 命令生成的真实正弦在解码端的落点如下
（`moon run cmd/main -- sine 16 <freq> 50 1`，8192 点以上 FFT）：

| 输入 Hz | 解码主频 Hz | 偏差 Hz | 说明 |
| ---: | ------: | ------: | --- |
| 440 | 439.91 | -0.09 | 子带 0 |
| 900 | 900.07 | +0.07 | 子带 1，奇数子带 |
| 1000 | 999.88 | -0.12 | 子带 1，落在两条 bin 之间 |
| 1100 | 1100.09 | +0.09 | 子带 1 |
| 2000 | 1999.98 | -0.02 | 子带 2 |
| 3000 | 3000.10 | +0.10 | 子带 4 |
| 5000 | 4999.91 | -0.09 | 子带 7，奇数子带 |
| 8000 | 8000.06 | +0.06 | 子带 11，奇数子带 |

偏差在 0.12 Hz 以内（FFT 网格的千分位），不再是"一条频线"量级。作为对照，在分析矩阵
修正之前（2026-11-02 夜前），同一组测试的 8000 Hz 会解码成 7847.66 Hz（偏差 -152 Hz，
约 4 条线），1000 Hz 会解码成 1077.34 Hz，奇数子带的落点整体偏移一个镜像位置。

## 5. 复现

```bash
# 单线探针，含相邻线对；写出 JSON 与 Markdown 表
python tools/probe_lines.py --lines 0-35 --pairs \
    --json target/line-frequencies.json --markdown target/line-freq-table.md

# 真实 PCM 路径的频率检查
moon run cmd/main -- sine 16 8000 50 1    # 输出 base64，交给 tools/spectrum.py 或上面的脚本
```

`tools/probe_lines.py` 的退出码非 0 表示有线的实测值偏离粗网格模型超过峰值估计
的不确定度，可以直接当回归门禁用。

## 6. 已知限制

- 单线探针无法分辨奇数 bin（第 2 节），要分辨必须让频谱随 block 变化；真实 PCM
  路径（`sine`、`encode`）没有这个限制。
- 峰值估计的不确定度由 FFT 长度决定：16384 点时 ±1.346 Hz，8192 点时 ±2.69 Hz。
- 8 kHz 以上的偏差只做过抽样检查；高频处子带内频线更密，抗混叠余量更小。
