{
  "lead": "任何声音都可以分解成许多不同频率的正弦波的叠加。快速傅里叶变换（FFT）就是把一段波形（时域）转换成各个频率成分的强度（频域）的算法。这个示例用 Rust 手写了 FFT，每一帧分析一段声音，画出波形、频谱和随时间滚动的时频图。",
  "formulas": [
    [
      "X<sub>k</sub> = Σ<sub>n</sub> x<sub>n</sub> · e<sup>−2πikn/N</sup>",
      "离散傅里叶变换：第 k 个频率分量"
    ]
  ],
  "terms": [
    [
      "从 O(N²) 到 O(N log N)",
      "直接按定义计算 DFT，N 个频率分量每个都要把 N 个采样加一遍，一共 N² 次运算。Cooley-Tukey 算法把长度为 N 的变换拆成偶数下标和奇数下标两个长度为 N/2 的变换，再用「蝶形运算」合并；递归拆到长度 1，总共 log₂N 层，每层 N 次运算。N = 2048 时，运算量从约 420 万次降到约 2.2 万次。"
    ],
    [
      "迭代实现：位反转 + 蝶形",
      "代码没有用递归：先把输入按下标的二进制位反转重新排列（例如 N=8 时，下标 1 = 001 与 4 = 100 交换位置），然后从长度 2 开始，一层层用蝶形运算合并，直到长度 N。整个过程在原数组上完成，不需要额外内存。"
    ],
    [
      "Hann 窗",
      "FFT 假设输入是无限重复的周期信号，一段波形的首尾接不上时，频谱会「泄漏」到很宽的范围。计算前先乘以 Hann 窗 <code>w(n) = 0.5·(1 − cos(2πn/N))</code>，让两端平滑地降到零，泄漏基本只影响峰值两边相邻的频率格子。"
    ],
    [
      "峰值频率与音名",
      "频率分辨率是 <code>采样率 / N</code>（48 kHz、N = 2048 时约 23 Hz）。为了比这更准，对峰值及其左右两个格子的 dB 值做抛物线插值，估计真正的峰值位置，再换算成最接近的音名和偏差（单位：音分 ¢，1 个半音 = 100 音分）。"
    ]
  ],
  "flow": [
    [
      "js",
      "每一帧取得一段长度为 N 的采样：合成信号由 JS 按公式实时计算（C 大三和弦，或 100 Hz → 10 kHz 的指数扫频）；麦克风信号从 Web Audio 的 <code>AnalyserNode.getFloatTimeDomainData()</code> 读取。"
    ],
    [
      "rust",
      "JS 调用 <code>fft_magnitudes(采样)</code>。Rust 加 Hann 窗、做 FFT，返回 N/2 + 1 个频率格子的幅度，已经归一化：振幅为 A 的正弦波，峰值读数约等于 A。"
    ],
    [
      "js",
      "JS 把幅度换算成分贝，按对数频率轴画出波形、频谱柱状图和时频图，并插值计算峰值频率。"
    ]
  ],
  "flow_notes": [
    "浏览器的 <code>AnalyserNode</code> 其实自带 FFT（<code>getFloatFrequencyData</code>）。这里故意只从它取原始波形，由 Rust 来做变换，这样合成信号和麦克风信号走的是同一套算法。"
  ],
  "perf": [
    "FFT 是整个示例里唯一的重计算。2048 点一次通常远小于 1 毫秒，底部会显示实际耗时；即使 4096 点，也远低于一帧 16.7 毫秒的预算。",
    "FFT 长度越大，频率分辨率越高，但每次分析覆盖的时间也越长（4096 点 @ 48 kHz ≈ 85 毫秒），对快速变化的声音反应会变慢。这是时间分辨率与频率分辨率之间固有的取舍。",
    "蝶形运算的旋转因子在每一层按 <code>f64</code> 计算一次再转成 <code>f32</code>，避免逐步累乘带来的误差积累；测试里把结果与 O(N²) 的朴素 DFT 逐项对比，误差小于 10⁻³。"
  ],
  "src": [
    [
      "crates/media/spectrum/src/lib.rs",
      ""
    ],
    [
      "www/spectrum/index.js",
      ""
    ]
  ]
}
