{
  "lead": "减法合成是模拟合成器最经典的做法：先用振荡器产生谐波丰富的波形，再用滤波器「减掉」一部分高频，最后用包络控制音量随时间的变化。这个合成器完全用 Rust 编写，编译成不依赖任何胶水代码的 .wasm，直接运行在浏览器的 AudioWorklet 音频线程里：每 128 个采样（48kHz 下约 2.7ms）调用一次 Rust 生成下一段声音。",
  "formulas": [
    [
      "f = 440 · 2<sup>(n − 69) / 12</sup>",
      "MIDI 音符号 n 换算成频率：69 是 A4 = 440Hz，每升一个半音乘以 ¹²√2"
    ],
    [
      "振荡器 → 滤波器 → 包络 → 混音",
      "每个声部独立完成这条链，8 个声部相加后软削波输出"
    ]
  ],
  "terms": [
    [
      "振荡器与 PolyBLEP",
      "锯齿波和方波在跳变处有无限高的谐波，直接按公式采样会产生刺耳的混叠（高于奈奎斯特频率的谐波「折返」成错误的音高）。PolyBLEP 在每个跳变附近叠加一小段多项式修正，把跳变「磨圆」，大部分混叠就消失了。正弦波没有谐波，三角波的谐波衰减得很快，都不需要修正。"
    ],
    [
      "状态变量滤波器",
      "低通滤波器保留截止频率以下的成分。这里用的是 Zavalishin 的 TPT 状态变量滤波器：截止频率和共振任意变化都保持稳定。共振提高截止频率附近的增益，调高后能听到典型的「哇」声。截止频率滑块按对数映射，每一段滑动距离对应相同的音程。"
    ],
    [
      "ADSR 包络",
      "起音（Attack）从 0 升到最大，衰减（Decay）降到延音电平，按住琴键时保持延音（Sustain），松开后在释音（Release）时间内线性降到 0，声部随之空闲。起音很短、释音很长像钢琴，起音很长像弦乐渐入。"
    ],
    [
      "复音与抢占",
      "最多 8 个声部同时发声。按下新音时优先使用空闲声部；都在忙就抢占最早的那个，并且优先抢已经在释音的声部。重复按同一个音会复用原来的声部，包络从当前电平继续，不会发出咔哒声。"
    ],
    [
      "软削波",
      "8 个声部叠加可能超过 ±1，直接截断会产生失真的方角。输出前经过 tanh，小信号几乎不变，大信号平滑地逼近 ±1。"
    ]
  ],
  "flow": [
    [
      "js",
      "点击「启动音频」后，主线程创建 <code>AudioContext</code>，加载 <code>processor.js</code>，并把 <code>pkg/synth/index.wasm</code> 的字节通过 <code>processorOptions</code> 传给音频线程。"
    ],
    [
      "js",
      "音频线程里的 <code>AudioWorkletProcessor</code> 用 <code>WebAssembly.instantiate</code> 实例化模块。这个 crate 不用 wasm-bindgen：音频线程不能加载 pkg 里的 ES 模块胶水，而裸的 <code>extern \"C\"</code> 导出只需要数字参数，<code>imports</code> 为空。"
    ],
    [
      "rust",
      "每个渲染量子调用 <code>synth_render(128)</code>，Rust 把 128 个采样写进自己内存里的缓冲区并返回指针；JS 用 <code>Float32Array</code> 直接读这块内存复制到输出，不经过任何序列化。"
    ],
    [
      "js",
      "琴键和滑块的变化通过 <code>port.postMessage</code> 发给音频线程，再调用 <code>synth_note_on</code> / <code>synth_note_off</code> / <code>synth_set_param</code>。示波器从 <code>AnalyserNode</code> 读取波形，按上升过零点对齐，周期波形就能稳定显示。"
    ]
  ],
  "perf": [
    "实测（Node 中运行同一个 .wasm）：8 个声部同时发声，生成 1 秒音频约 2.6ms，只占实时预算的 0.3% 左右。",
    "音频线程独立于页面主线程，主线程被耗时的 JS 卡住时声音也不会中断；反过来，音频代码里不能分配大块内存或做耗时操作，这里所有缓冲区都在初始化时分配好。",
    "AudioWorklet 只能在安全上下文中使用：<code>localhost</code> 可以，手机通过局域网 IP 访问需要 HTTPS。iPhone 的静音开关打开时网页没有声音。"
  ],
  "src": [
    [
      "crates/media/synth/src/lib.rs",
      ""
    ],
    [
      "www/synth/processor.js",
      ""
    ],
    [
      "www/synth/index.js",
      ""
    ]
  ]
}
