# Sonir Bench — モジュール仕様（正典）

> DSP モジュール（グラフ）と、その前後のレート変換段の設計を書くために必要な仕様を1ファイルにまとめたもの。本文・JSON Schema 2本・エラーコード全件・リファレンスの実例が入っている。

- モジュール（グラフ）の `spec_version`: `0.1.0`
- レート変換設計の `spec_version`（独立に上がる）: `0.1.0`
- 制御 API のプロトコル版: `1`
- 生成物（手で編集しない）

**載っているのは、モジュールを1つ書くために要るものだけ。** アプリの内部構造・入出力の実装・開発の経緯は含まれていない。

🔴 **ここに出てくる JSON はすべて実装に食わせて通したもの。** Schema とエラーカタログは実装から書き出し、リファレンスモジュールは形 → 検証 → 刺激 → 起動の4段すべてを、レート変換設計は入口として選べるレートすべてを、テストが通している。

---
## 1. この仕様の使い方

モジュールは **JSON ファイル1つ**。やることは3つしかない。

1. **書く。** `spec_version` と `nodes` を持つ JSON を作る。
2. **渡す。** アプリの「新しく作る」にあるファイルの欄へ落とす（ファイルを選んでもよい）。読めれば欄に入り、**保存すると形と係数の検証が走って**ライブラリに入る。**聴くために載せると残りの検証が走る**（「6. 通るための条件」）。
3. **直す。** 検証に落ちたら、落ちた段・エラーコード・実際に測った数字が返る。それをそのまま書き手へ戻して直す。

- **検証に落ちることは異常ではない。** 期待される2つの答えの一方で、落ちたときも測定値は返る。何がどれだけ超えたかが読めるので、そこを直せばよい。
- **人が書いたファイルと AI が書いたファイルは同じ入口を通る。** 取り込み専用の経路は無く、同じ判定で断られる。
- **機械が音を出す口は無い。** 鳴らすのは人の操作だけなので、AI に書かせた場合、**AI は自分が書いたものを自分で確かめられない**。返ってきた理由を書き手へ戻すのは人の仕事になる。
- **値の範囲・単位・既定値・実例は付録A（JSON Schema）が正。** この本文には写していない（同じ数字を2箇所に置くと必ずずれる）。
- **エラーコードは付録B に全件ある。** ここに無いコードは実装にも無い。
- ファイル名がそのまま識別子。**同じ名前で渡し直すと上書きが改名を兼ねる。**

## 2. グラフの形

```jsonc
{
  "spec_version": "…",   // 必須。値は付録A の const（この文書の先頭にも出ている）
  "nodes": [             // 上流から下流への順。空の配列なら素通し
    { "type": "biquad", "shape": "bell", "cutoff_hz": 120.0, "q": 1.0, "gain_db": 3.0 }
  ]
}
```

- ノードは**一列**に並ぶ。上流から下流へ順に通る。
- ほかに**補助バス（aux）が1本**ある。主信号を読んで aux へ書くノードと、aux を読んで主信号に効かせるノードがあり、これで側鎖を組む（「4. 補助バス」）。
- **分岐して合流する構成は書けない。** マルチバンドのような形は表現できず、書けるのは一列 + 補助バス1本だけ。
- ノード数の上限は付録A。
- **並べる順に意味がある。** 直流成分を作るノードの後ろに `dc_blocker` を置く、`signal_gain` の前に `envelope_follower` を置く、といった前後関係がそのまま効く。

### レートには依存しない

- ノードはすべて Hz と dB で書く。内部の処理レートは **705,600 Hz**（44.1 kHz 系の音源）か **768,000 Hz**（48 kHz 系）で、**同じグラフが両方にそのまま載る**。
- **両方のレートで通ることが条件。** 再生中に音源のレート系列が入れ替わっても同じグラフが載り続けるので、片方でだけ成り立つ設計は組めない。
- **上端の帯域制限（折り返しを止めるローパス）はグラフの仕事ではない。** レート変換段が必ず持っている。グラフに書くのは色付けのほうで、帯域制限を外すことはできない。

## 3. ノード一覧

`type` は次の14種。**何をする段か・どのバスに繋がるか・非線形かどうか・併せて要るもの**だけを挙げる。パラメータの名前・範囲・単位・既定値・実例は付録A を見ること。

| `type` | 何をする段か | バス | 非線形 | 併せて要るもの |
|---|---|---|---|---|
| `gain` | 音量を上下させる。音色は変わらない | 主 | いいえ | |
| `biquad` | EQ / フィルタ。帯域を持ち上げる・抑える・切る | 主 | いいえ | |
| `dc_blocker` | 直流成分を取る。可聴域には効かない | 主 | いいえ | |
| `waveshaper` | サチュレーション。ピークを丸めて倍音を付ける | 主 | **はい** | 非対称の形は直流を作るので**後ろに** `dc_blocker` |
| `harmonic_shaper` | 2〜5次の倍音を次数ごとの割合で足す | 主 | **はい** | 偶数次を使うなら**後ろに** `dc_blocker` |
| `hysteresis` | 磁気飽和。履歴を引きずる粘りを付ける | 主 | **はい** | |
| `envelope_follower` | 主信号の音量を検出して aux へ書く | 主を読み aux へ書く | いいえ（主信号は素通し） | 受け取る側（`signal_gain`）を後ろに |
| `one_pole_smoother` | aux を通る制御信号をなめらかにする | aux のみ | いいえ（主信号に触らない） | |
| `signal_gain` | aux の値で主信号の音量を引き下げる（電源サグ） | aux を読み主へ効かせる | **はい**（時変） | **前に** `envelope_follower` |
| `elliptical_eq` | 低域の Side を切って重心を中央へ寄せる。高域 Side のシェルフとステレオ幅も持つ | 主 | いいえ | |
| `allpass_decorrelator` | 振幅特性を変えずに左右へ位相差を作る | 主 | いいえ | |
| `crosstalk_canceller` | 左右スピーカーの逆耳への回り込みを打ち消す | 主 | いいえ | |
| `directional_eq` | Side 成分だけに仰角知覚帯域の山と奥行きのディップを掛ける | 主 | いいえ | |
| `early_reflector` | Side 成分から初期反射を作って部屋の容積感を足す | 主 | いいえ | |

### 非線形を1つでも積むと変わること

| | 線形だけのグラフ | 非線形を含むグラフ |
|---|---|---|
| 刺激を流す検証（「6. 通るための条件」の `simulate`） | 走らない | **必ず走る**。通った証が無いと鳴らせない |
| 最終段のリミッター | 入らない | **必ず入り、セッション中は切れない** |
| 鳴り始めのソフトスタート | 無し | **必ず掛かる** |
| A/B の音量合わせ | 段ごとに計算して揃える | **非線形段は 0 dB として数える**（「5. レベルの規約」） |

線形だけのグラフに検証や保護を一律で掛けることはしない。**素通しに一番近い経路が一番信号を触られる**という逆立ちになるため。

## 4. 補助バス

`envelope_follower` → `one_pole_smoother` → `signal_gain` が典型で、音量に追従して主信号を沈ませる側鎖になる。

- **補助バスは1本しかない。** `envelope_follower` を2つ置くと後のほうが前のほうを上書きする。
- **aux に何も書かれていなければ `signal_gain` の利得は 1.0。** つまり繋ぎ忘れても無音にはならない代わりに、**何も起きない**（形の検証も刺激の検証も通ってしまうので、意図した音が出ないときはまずここを見る）。
- 音量の検出は**チャンネル連動**。全チャンネルの最大を採って同じ値を使う（独立させると定位が信号レベルで動く）。
- **戻りの遅さは `one_pole_smoother` で作る。** `signal_gain` 自身は時定数を持たない。

## 5. レベルの規約

版で固定してある規約で、後から変わらない。

| # | 規約 |
|---|---|
| 1 | 信号は浮動小数点で `1.0 = 0 dBFS`。**暗黙のゲインはどこにも無い** |
| 2 | アナログ対応の基準点は **-20 dBFS RMS ≡ 1.0 V RMS**（モデルしている回路の入力ノードで）。ドライブや倍音の割合はこの基準に対して決まる |
| 3 | どのモジュールも**基準レベルでは通過利得 1**（±0.1 dB 以内）。検証が見る |
| 4 | 非線形ノードの `drive_db` は**出力側で自動的に補償される**。ドライブを変えても音量は変わらず、音色だけが変わる |
| 5 | 内部は浮動小数点なのでクリップしない。**リミッターは最終段だけ** |
| 6 | A/B の切り替えでは**音量を自動で揃える**（基準は素通し = 0 dB） |

規約4 と規約6 があるから、A/B が「音が大きいほうが良く聴こえる」道具にならない。**ドライブを上げて音量が上がったなら、それは実装の不具合**（規約4 は基準レベルの正弦波で ±0.1 dB 以内に揃うことをテストで固定している）。

⚠️ **音量合わせの合成に入るのは線形の基本段だけ**（`gain` / `biquad` / `dc_blocker`）。**非線形段と空間・定位系の段は 0 dB として数える。** 非線形の音量は入力レベルに依存するので周波数特性では表せず、規約4 が「基準レベルで利得1」を保証している前提に乗っている。裏返すと、**空間・定位系の段でレベルが動いても、その分は自動では補われない。**

⚠️ 規約4 の補償は**基準レベルの正弦波**で揃えてある。クレストファクターの大きい実際の音楽ではぴたりとは揃わない。

## 6. 通るための条件

4段ある。**前の段を通らなければ次の段は走らない**（落ちた段の名前が返るので、どこで止まったかは必ず分かる）。

| 段 | いつ走るか | 何を見るか | コード |
|---|---|---|---|
| 形 | 読み込んだ時点 | 欄の名前・型・範囲・必須。知らない欄があれば**候補の一覧**が返る | `graph.malformed` |
| `validate` | 保存するとき（と載せるとき） | 各ノードの値から係数を設計し、**内部レートの両方で**確かめる。範囲外・不安定・ピーク利得が大きすぎる・超音波を増幅している | `biquad.*` / `onepole.*` |
| `simulate` | 聴くために載せるとき。**非線形を含むときだけ** | 刺激を実際に通して、非数や無限大・ピーク・直流成分・超音波の量を測る | `simulate.*` |
| `arm` | 鳴り始める直前 | 非線形を含むなら `simulate` の証・最終リミッター・ソフトスタートが立っているか | `arm.*` |

**形と `validate` を通ったものだけがライブラリに入る。** 一覧に出るのに選ぶと必ず落ちる項目を作らないため、そこまでは保存の時点で通す。⭐ **刺激を流す検証は載せるときに走り、落ちれば載らない**（鳴ってから止めるのではなく、先に止める）。

**形の検査を通っても鳴るとは限らない。** 形は形でしかなく、判定の正は `validate` から先にある。

### 刺激の検証（`simulate`）が見ているもの

流すのは3本。**どれにも超音波は入っていない**ので、出口に出た超音波は全部ノードが作った量になる。

| 刺激 | 何が分かるか |
|---|---|
| 基準レベルの正弦波（整数周期） | 通過利得（規約3 / 規約4 の効き）と**直流成分** |
| 対数スイープ（可聴帯域） | 高調波が超音波域に作る量とピーク |
| 帯域制限ノイズ | 相互変調と非数 |

- **直流成分の判定は正弦波だけで行う。** ほかの2本は推定誤差のほうが大きく、「直流を作っていないノードを作ったと言う」ことになるため、数字としては返るが判定には使わない。
- **畳み込みを含むグラフは暖機してから測る。** 長いほど時間が掛かるので、非線形を含むグラフを載せる操作はそのぶん待たされる。
- 結果は順番に読むこと。非数が出ているのに「超音波が多い」とだけ見ても直せない。

### 超音波について

内部は 705,600 Hz / 768,000 Hz で動くので、**フルスケールの超音波が出ていても人間には聞こえない。** そのまま DAC からアンプ、ツイーターへ入る。可聴の異常は耳で気づけるが、これは気づけないまま機器を壊せる。だから既定で止める側に倒してある。

⭐ **止めているのは「超音波を通すこと」ではなく「増幅すること」。** 基準は通過帯域との比ではなく**絶対値**なので、きちんと窓を掛けた高域通過フィルタは通る（通過帯域が 0 dB に正規化されているため）。相対にすると、可聴帯域を持ち上げるルーム補正が超音波も一緒に持ち上げたときに通ってしまう。

### 直流成分について

**非対称な波形整形と偶数次の倍音は直流を作る。** スピーカーを押し続ける成分なので、そのままでは `simulate` が落とす。

🔴 **`dc_blocker` は作る側の後ろに置く。** 前に置いても落ちたまま（前段で作られた直流は、その手前にあるブロッカーでは取れない）。

## 7. レート変換段の設計

入口のレートを内部レートへ上げ、出口で戻す段。**グラフとは別のファイルで、版も別に上がる。**

```jsonc
{
  "spec_version": "…",        // 必須。値は付録A の const。グラフのものとは別
  "kind": "resample-design",  // 必須。グラフと取り違えないため
  "name": "…",                // 表示名（自由）。⚠️ 識別子はファイル名のほう
  "design": {                 // 設計そのもの。どの欄も省略でき、省略すると出荷時の設計
    "pass_hz": { "of_nyquist": 0.9 }
  },
  "targets": [],              // 省略可。使うつもりの入口レート。空なら「どのレートでも」
  "min_partition": null       // 省略可。回すつもりの最小のブロック長
}
```

作業フォルダの `resample/` へ置けば一覧に出る。**ファイル名がそのまま識別子**なのはグラフと同じで、`name` は表示名、`kind` はグラフと取り違えないための種別。

### 何を書く段なのか

**帯域制限を書けるのはここだけ。** グラフに書けるのは色付けで、折り返しを止める上端のローパスはこの段が必ず持っている（「2. グラフの形」）。任意位置の低域通過・高域通過もここで書く。

| 欄 | 何を決めるか |
|---|---|
| `pass_hz` | 通過帯域の上端。ここまで平坦に通す |
| `stop_hz` | 阻止域の下端。**省略するとレートから導く**（入口レート − 通過帯域 = 第1イメージの立つ位置） |
| `shape` | 上端より下に載せる整形（超低域を落とす・特定の帯域を落とす）。省略なら素の低域通過 |
| `stopband_db` | 窓に**要求する**阻止域減衰 |
| `window` | 窓の種類 |
| `phase` | 直線位相か最小位相か |
| `taps` | タップ数を遷移幅から解くか、直接指定するか |

### 🔴 設計はレートを持たない

`design` の中に入口レートも係数も無い。**同じ設計が段ごと・入口レートごとに解き直される**（阻止域の下端がそのレートで決まるため）。だから設計を1つ書けば、どのレートの音源にもそのまま載る。

- **上端は比でも書ける。** 絶対値のほかに「入口ナイキストに対する比」で書けるので、「音源の上端に合わせる」という意図がレートごとのファイルに分裂しない。
- **絶対値で書いた周波数とタップ数が効くのは入口段だけ。** 出口段は自分の遷移幅から解き直す。持ち込むと帯域制限が二度掛かり、タップ数も桁で膨れる。
- `targets` と `min_partition` が**設計の中ではなく隣に在る**のはこのため。どちらも係数を1ビットも変えない宣言で、`targets` は「どのレートで使うつもりか」、`min_partition` は「消費率の予報をどの行で代表させるか」だけを決める。

### 通るための条件

グラフと同じく、**音を鳴らさずに試せる**（落ちても異常ではなく、測った数字が返る）。断る理由は付録B の `resample.*` に全件ある。

- **遷移域が取れなければ断る**（`resample.no_transition_band`）。上端がそのレートに対して広すぎる場合。
- **阻止域の抑圧は測ってから受け取る**（`resample.image_leak`）。`stopband_db` は要求であって結果ではないので、減衰が固定の窓では届かないことがある。折り返した成分は元の音と無関係な周波数に立つので、**「掛けた覚えのない音」**として出る。
- **阻止域の下端を導出値より上げることだけを断る**（`resample.stopband_too_high`）。下げるぶんにはイメージ抑圧が改善するだけ。
- **直線位相ならタップ数は奇数**（`resample.taps_not_odd`）。群遅延を整数サンプルにするため。
- **整形は帯域制限の内側に収める**（`resample.shape_out_of_range`）。
- `targets` が空、つまり「どのレートでも」と宣言したものは、**保存の時点で両方のレートファミリーについて解く。** 一覧に出るのに選ぶと必ず落ちる項目を作らないため。

⚠️ **消費率（CPU）では断らない。** 同じ設計が機械と熱で通ったり落ちたりするので、予報は出すが拒否の理由にはしない。**遅延も断らない** —— 直線位相の群遅延は全帯域一定の純粋な遅れで音そのものは変わらないので、超えていることを常に表示するほうに倒してある。

### 位相の選び方

**この段のプリリンギングを避けられるのはここだけ。** グラフの色付けは補間の**後**に掛かるので、そこで最小位相を選んでも変換段のカーネルには効かない。代わりに直線位相は群遅延が全帯域一定で、位相の歪みを持たない。**どちらを取るかはこの欄で決める。**

## 8. 版

- `spec_version` は**必須**。`X.Y.Z` の文字列で、省略すると形の段で落ちる。
- **読めるのは「X が同じで、Y.Z がこの実装以下」のファイル。**
- **新しい版のファイルは読まない。** 知らない欄を無視して別の意味で鳴らすことになるため。

| 桁 | 上げるとき | 古いファイルは |
|---|---|---|
| **X** | 既存の欄の意味・単位・既定値を変える / 消す・改名する / 必須の欄を足す | **読めない**（版を名指しして断る） |
| **Y** | 省略できる欄を足す（省いたときの挙動が今と同じ）/ ノード種別を足す | 読める |
| **Z** | 説明だけ変えた（形は同じ） | 読める |

- **読めない版のファイルは消さない・上書きしない。** 理由を添えて一覧に残す。名前も押さえるので、あとから同じ名前のものを作っても踏み潰されない。
- 読めたファイルはその場で最新の形へ引き上げ、**次に保存されるときは最新の版で書かれる。**

## 9. 書けないもの

範囲の外に置いてあるもの。能力が無いのではなく、置き場所を決めた結果。

- **分岐して合流する構成**（一列 + 補助バス1本が書ける形の全部）。
- **外で作った係数を持ち込むこと。** 測定した室内音響の応答や、ほかのツールが出したフィルタ係数を畳み込む口は無い。帯域の上端・任意位置の低域通過・高域通過は、グラフではなくレート変換段の設計で書く（「7. レート変換段の設計」）。
- **鳴らしたまま連続して動かせる値は3つだけ**（`gain` の利得、`waveshaper` と `hysteresis` のドライブ）。ほかの値は載せ直して変える。**載せ直しは切れ目なく差し替わる**ので、つまみとして動かしたいわけでなければこれで足りる。
- **実時間で走る自作コード。** 音色は用意されたノードの組み合わせで書く。
- ⚠️ **載せ直すと、動かしていた値はファイルに書かれた値へ戻る。** ファイルが正典で、つまみはその上の一時的な状態。

### この経路で再現できないもの

DAC より前の DSP なので、**その先のパワーアンプとスピーカーの相互作用**（逆起電力、実際の負荷応答）は再現できない。想定インピーダンス曲線による近似が上限。

---

## 付録A. JSON Schema（実装から生成）

**範囲・単位・既定値の正はここだけ。** 数字は実装の定数から組まれていて、本文には書かれていない（写すと2箇所になる）。

#### `graph-v1.schema.json`

```json
{
  "$id": "https://sonir.app/schema/bench/graph-v1.json",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": false,
  "description": "A Sonir Bench DSP graph. A series chain of nodes plus one aux bus. This is a shape check only: passing it does not mean the graph will play (coefficients are checked by validate, non-linear chains by simulate, and arm runs right before playback). Every node is rate independent (written in Hz and dB), so the same graph loads unchanged when the internal rate (705,600 / 768,000 Hz) changes. Band limiting itself is not part of the graph: it is set by the resample design (resample.*).",
  "examples": [
    {
      "nodes": [],
      "spec_version": "0.1.0"
    },
    {
      "nodes": [
        {
          "drive_db": 12.0,
          "shape": "asymmetric",
          "type": "waveshaper"
        },
        {
          "cutoff_hz": 5.0,
          "type": "dc_blocker"
        }
      ],
      "spec_version": "0.1.0"
    }
  ],
  "properties": {
    "nodes": {
      "description": "Upstream to downstream order. An empty list is a passthrough (bypass). The order matters: an asymmetric waveshaper creates DC, so unless a dc_blocker sits after it, simulate refuses the chain.",
      "items": {
        "oneOf": [
          {
            "additionalProperties": false,
            "description": "Gain (volume adjustment). Adjusts the overall level of the chain by a fixed amount without altering timbre. During A/B comparisons, levels are compensated automatically to evaluate pure tonal differences.",
            "examples": [
              {
                "gain_db": -3.0,
                "type": "gain"
              }
            ],
            "properties": {
              "gain_db": {
                "description": "Gain [dB].",
                "maximum": 24.0,
                "minimum": -60.0,
                "type": "number",
                "x-description-ja": "利得 [dB]。"
              },
              "type": {
                "const": "gain",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type",
              "gain_db"
            ],
            "title": "gain",
            "type": "object",
            "x-description-ja": "ゲイン（音量調整）。全体の音量を指定した分だけ調整する。音色自体は変化しない。A/B 比較時は音量が自動補償されるため、音量差に惑わされず純粋な音色の違いを評価できる。"
          },
          {
            "additionalProperties": false,
            "allOf": [
              {
                "else": {
                  "properties": {
                    "gain_db": {
                      "type": "null"
                    }
                  }
                },
                "if": {
                  "properties": {
                    "shape": {
                      "enum": [
                        "bell",
                        "low_shelf",
                        "high_shelf"
                      ]
                    }
                  },
                  "required": [
                    "shape"
                  ]
                },
                "then": {
                  "properties": {
                    "gain_db": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "gain_db"
                  ]
                }
              }
            ],
            "description": "EQ / Filter (Biquad). Boosts, cuts, or filters specific frequency bands across low, mid, and high ranges. gain_db is only applicable for bell, low_shelf, and high_shelf shapes, and must not be written for other shapes.",
            "examples": [
              {
                "cutoff_hz": 1000.0,
                "gain_db": 3.0,
                "q": 1.0,
                "shape": "bell",
                "type": "biquad"
              }
            ],
            "properties": {
              "cutoff_hz": {
                "description": "Cutoff / centre frequency [Hz]. The maximum is the internal Nyquist of the lower rate family.",
                "maximum": 352800.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "遮断 / 中心周波数 [Hz]。上限は内部レートのナイキスト（低いファミリー側）。"
              },
              "gain_db": {
                "description": "Gain [dB]. Required for bell and the shelves, null or omitted for the other shapes.",
                "maximum": 24.0,
                "minimum": -24.0,
                "type": [
                  "number",
                  "null"
                ],
                "x-description-ja": "利得 [dB]。bell / シェルフでは必須、他の型では null か省略。"
              },
              "q": {
                "description": "Q.",
                "maximum": 40.0,
                "minimum": 0.05,
                "type": "number",
                "x-description-ja": "Q。"
              },
              "shape": {
                "description": "Response shape.",
                "enum": [
                  "low_pass",
                  "high_pass",
                  "band_pass",
                  "notch",
                  "all_pass",
                  "bell",
                  "low_shelf",
                  "high_shelf"
                ],
                "x-description-ja": "応答の型。"
              },
              "type": {
                "const": "biquad",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type",
              "cutoff_hz",
              "q",
              "shape"
            ],
            "title": "biquad",
            "type": "object",
            "x-description-ja": "EQ / フィルタ（Biquad）。低域・中域・高域の特定の周波数帯を持ち上げたり抑えたり、不要な帯域をカット（ローパス/ハイパスなど）する。⚠️ gain_db は bell / low_shelf / high_shelf でのみ指定でき、他の型では指定しない。"
          },
          {
            "additionalProperties": false,
            "description": "Saturator (waveform distortion and harmonics). Softly rounds waveform peaks like tubes or tape, adding harmonics and warmth. Raising drive_db does not increase level as compensation keeps it constant. asymmetric creates DC, so follow it with a dc_blocker.",
            "examples": [
              {
                "drive_db": 12.0,
                "shape": "tanh",
                "type": "waveshaper"
              }
            ],
            "properties": {
              "drive_db": {
                "description": "Drive [dB]. The level does not change (rule 4).",
                "maximum": 48.0,
                "minimum": -12.0,
                "type": "number",
                "x-description-ja": "ドライブ [dB]。音量は変わらない（規約4）。"
              },
              "shape": {
                "description": "Shape. tanh is symmetric, asymmetric is not (it creates DC).",
                "enum": [
                  "tanh",
                  "asymmetric"
                ],
                "x-description-ja": "形。tanh は対称、asymmetric は非対称（DC を作る）。"
              },
              "type": {
                "const": "waveshaper",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type",
              "drive_db",
              "shape"
            ],
            "title": "waveshaper",
            "type": "object",
            "x-description-ja": "サチュレーター（波形歪み・倍音）。真空管やテープのように波形のピークをやわらかく丸め、倍音と温かみを付加する。ドライブを上げても出力音量は自動的に一定に保たれる。🔴 非対称（asymmetric）は直流成分（DC）を発生させるため、後段に dc_blocker が要る。"
          },
          {
            "additionalProperties": false,
            "description": "Harmonic shaper (order-specified harmonics). Adds 2nd, 3rd, 4th, and 5th harmonics to the fundamental tone at precisely specified percentages relative to the reference level (-20 dBFS RMS). Even harmonics (h2, h4) create DC, so follow with a dc_blocker.",
            "examples": [
              {
                "h2_pct": 1.5,
                "h3_pct": 0.3,
                "type": "harmonic_shaper"
              }
            ],
            "properties": {
              "h2_pct": {
                "default": 0.0,
                "description": "2nd harmonic percentage [%].",
                "maximum": 20.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "2次倍音付与率 [%]。"
              },
              "h3_pct": {
                "default": 0.0,
                "description": "3rd harmonic percentage [%].",
                "maximum": 20.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "3次倍音付与率 [%]。"
              },
              "h4_pct": {
                "default": 0.0,
                "description": "4th harmonic percentage [%].",
                "maximum": 20.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "4次倍音付与率 [%]。"
              },
              "h5_pct": {
                "default": 0.0,
                "description": "5th harmonic percentage [%].",
                "maximum": 20.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "5次倍音付与率 [%]。"
              },
              "type": {
                "const": "harmonic_shaper",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type"
            ],
            "title": "harmonic_shaper",
            "type": "object",
            "x-description-ja": "倍音シェイパー（次数指定）。基準レベル（-20 dBFS RMS）の基音に対して、2次・3次・4次・5次倍音を指定した割合（%）で正確に加算する。真空管の温かみ（偶数次主体）やテープの飽和感（奇数次）を狙い通りに再現できる。🔴 偶数次（h2, h4）は直流成分（DC）を発生させるため、後段に dc_blocker が要る。"
          },
          {
            "additionalProperties": false,
            "description": "DC Blocker (DC removal). Removes inaudible DC offset and ultra-low frequency drift around 0 Hz without affecting audible content. Place immediately after asymmetric saturation or even-harmonic shaping.",
            "examples": [
              {
                "cutoff_hz": 5.0,
                "type": "dc_blocker"
              }
            ],
            "properties": {
              "cutoff_hz": {
                "description": "Cutoff frequency [Hz].",
                "maximum": 100.0,
                "minimum": 0.05,
                "type": "number",
                "x-description-ja": "遮断周波数 [Hz]。"
              },
              "type": {
                "const": "dc_blocker",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type",
              "cutoff_hz"
            ],
            "title": "dc_blocker",
            "type": "object",
            "x-description-ja": "DCブロッカー（直流除去）。可聴域に影響を与えずに、ヘッドルームを圧迫する不要な直流成分（0 Hz付近の超低域・DCオフセット）を除去する。非対称な歪みや倍音を付加したノードの直後に配置して使用する。"
          },
          {
            "additionalProperties": false,
            "description": "Envelope Follower (level detection to aux bus). Tracks the envelope of the main signal and writes control values to the aux bus while passing the main signal untouched. Used together with signal_gain to create dynamic level-dependent effects.",
            "examples": [
              {
                "attack_ms": 1.0,
                "detector": "rms",
                "release_ms": 50.0,
                "type": "envelope_follower"
              }
            ],
            "properties": {
              "attack_ms": {
                "description": "Attack [ms].",
                "maximum": 5000.0,
                "minimum": 0.01,
                "type": "number",
                "x-description-ja": "アタック [ms]。"
              },
              "detector": {
                "description": "Detector type.",
                "enum": [
                  "peak",
                  "rms"
                ],
                "x-description-ja": "検波方式。"
              },
              "release_ms": {
                "description": "Release [ms].",
                "maximum": 5000.0,
                "minimum": 0.01,
                "type": "number",
                "x-description-ja": "リリース [ms]。"
              },
              "type": {
                "const": "envelope_follower",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type",
              "attack_ms",
              "detector",
              "release_ms"
            ],
            "title": "envelope_follower",
            "type": "object",
            "x-description-ja": "エンベロープフォロワー（音量検出 → 補助バス）。主信号の音量変化（包絡）を検出し、制御信号として補助バス（aux）へ送る。主信号の音自体はそのまま通過する。後段の signal_gain と組み合わせて、音量に応じた動的な効果を作るために使用する。"
          },
          {
            "additionalProperties": false,
            "description": "Smoother (aux bus smoothing). Smooths control signals on the aux bus to moderate rapid changes such as envelope follower outputs. Does not alter the main audio signal. Placed between envelope_follower and signal_gain.",
            "examples": [
              {
                "time_ms": 20.0,
                "type": "one_pole_smoother"
              }
            ],
            "properties": {
              "time_ms": {
                "description": "Time constant [ms].",
                "maximum": 5000.0,
                "minimum": 0.01,
                "type": "number",
                "x-description-ja": "時定数 [ms]。"
              },
              "type": {
                "const": "one_pole_smoother",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type",
              "time_ms"
            ],
            "title": "one_pole_smoother",
            "type": "object",
            "x-description-ja": "スムーザー（補助バス平滑化）。補助バス（aux）を通る制御信号の変化をなめらかにし、音量変化などの反応速度を調節する。主信号（音声そのもの）には影響を与えない。envelope_follower と signal_gain の間に挟んで使用する。"
          },
          {
            "additionalProperties": false,
            "description": "Power Sag (aux-controlled dynamic gain). Dynamically pulls down main signal level according to control values on the aux bus, reproducing power amplifier sag compression. Requires an envelope_follower before it.",
            "examples": [
              {
                "depth_db": 6.0,
                "type": "signal_gain"
              }
            ],
            "properties": {
              "depth_db": {
                "description": "Sag depth [dB].",
                "maximum": 24.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "サグ量 [dB]。"
              },
              "type": {
                "const": "signal_gain",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type",
              "depth_db"
            ],
            "title": "signal_gain",
            "type": "object",
            "x-description-ja": "電源サグ（補助バス制御の音量変化）。補助バス（aux）から受け取った音量情報に応じて、主信号の音量を動的に引き下げる。大音量時にアンプの電源電圧が一時的に落ち込む「サグ」特有のコンプレッション感を再現する。前段に envelope_follower が要る。"
          },
          {
            "additionalProperties": false,
            "description": "Magnetic Hysteresis (tape and transformer saturation). Models magnetic saturation in tape or transformer cores using the Jiles-Atherton physical model, adding rich analog texture with memory. Raising drive_db does not increase level (compensated automatically).",
            "examples": [
              {
                "drive_db": 12.0,
                "type": "hysteresis",
                "width": 0.5
              }
            ],
            "properties": {
              "drive_db": {
                "description": "Drive [dB].",
                "maximum": 48.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "ドライブ [dB]。"
              },
              "type": {
                "const": "hysteresis",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              },
              "width": {
                "description": "Loop width (0 is almost linear, 1 is the widest).",
                "maximum": 1.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "ループの太さ（0 = ほぼ線形、1 = 最も太い）。"
              }
            },
            "required": [
              "type",
              "drive_db",
              "width"
            ],
            "title": "hysteresis",
            "type": "object",
            "x-description-ja": "磁気ヒステリシス（テープ・トランス飽和）。磁気テープやトランスのコアで生じる磁気飽和（ヒステリシス特性）を物理モデルで再現する。信号の履歴を引きずるような独特の粘りとアナログ感を付加する。ドライブを上げても音量は自動で保たれる。"
          },
          {
            "additionalProperties": false,
            "description": "Elliptical EQ (low-frequency mono maker and high-frequency side shaper). Filters the Side component below mono_cutoff_hz to center low bass, optionally boosts or cuts high-frequency Side component, and scales overall stereo width. Essential for tightening stereo image and controlling stereo spread without phase issues.",
            "examples": [
              {
                "high_side_gain_db": 2.0,
                "mono_cutoff_hz": 120.0,
                "type": "elliptical_eq",
                "width": 1.2
              }
            ],
            "properties": {
              "high_side_freq_hz": {
                "default": 8000.0,
                "description": "High-frequency Side shelf corner frequency [Hz].",
                "maximum": 20000.0,
                "minimum": 1000.0,
                "type": "number",
                "x-description-ja": "高域Sideシェルフ周波数 [Hz]。"
              },
              "high_side_gain_db": {
                "default": 0.0,
                "description": "High-frequency Side shelf gain [dB] (air/width boost/cut).",
                "maximum": 24.0,
                "minimum": -24.0,
                "type": "number",
                "x-description-ja": "高域Sideシェルフ利得 [dB]（空間の広がり/空気感の調整）。"
              },
              "high_side_q": {
                "default": 0.7071,
                "description": "Q factor for high-frequency Side shelf filter.",
                "maximum": 40.0,
                "minimum": 0.05,
                "type": "number",
                "x-description-ja": "高域SideシェルフフィルタのQ値。"
              },
              "mono_cutoff_hz": {
                "default": 120.0,
                "description": "Low-cut frequency for Side component [Hz] (mono maker cutoff).",
                "maximum": 2000.0,
                "minimum": 20.0,
                "type": "number",
                "x-description-ja": "Side成分のローカット周波数 [Hz]（低域モノラル化）。"
              },
              "mono_q": {
                "default": 0.7071,
                "description": "Q factor for low-cut Side filter.",
                "maximum": 40.0,
                "minimum": 0.05,
                "type": "number",
                "x-description-ja": "低域SideローカットフィルタのQ値。"
              },
              "type": {
                "const": "elliptical_eq",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              },
              "width": {
                "default": 1.0,
                "description": "Stereo width factor (0.0 = mono, 1.0 = original, 2.0 = extra wide).",
                "maximum": 2.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "ステレオ幅係数（0.0 = モノラル, 1.0 = 原音, 2.0 = ワイド）。"
              }
            },
            "required": [
              "type"
            ],
            "title": "elliptical_eq",
            "type": "object",
            "x-description-ja": "楕円EQ（低域モノラル化・高域Sideシェイパー）。低域のSide成分をカットして低音の定位を中央に集め、タイトで濁りのない重心を作る。高域Side成分のシェルフ調整および全体のステレオ幅調整も備え、位相崩れを起こさずに音場空間を広げる。"
          },
          {
            "additionalProperties": false,
            "description": "Allpass Decorrelator (comb-filter-free phase diffusor). Diffuses phase between left and right channels using multi-stage allpass filters with reciprocal detuning. Broadens stereo image and spaciousness without changing frequency response or causing comb-filtering coloration in mono downmix.",
            "examples": [
              {
                "depth": 0.5,
                "low_cutoff_hz": 500.0,
                "type": "allpass_decorrelator"
              }
            ],
            "properties": {
              "depth": {
                "description": "Diffusion depth (0.0 = bypassed, 1.0 = maximum diffusion).",
                "maximum": 1.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "拡散深度（0.0 = バイパス、1.0 = 最大拡散）。"
              },
              "low_cutoff_hz": {
                "default": 500.0,
                "description": "Lower corner frequency for allpass diffusion [Hz]. Frequencies below this remain phase-aligned.",
                "maximum": 5000.0,
                "minimum": 20.0,
                "type": "number",
                "x-description-ja": "オールパス拡散の下限周波数 [Hz]。この周波数より下は同相のまま維持される。"
              },
              "type": {
                "const": "allpass_decorrelator",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type",
              "depth"
            ],
            "title": "allpass_decorrelator",
            "type": "object",
            "x-description-ja": "オールパス・デコリレータ（コムフィルタ歪みのない位相拡散）。左右チャンネルで逆方向に離調した多段オールパスフィルタを通し、振幅特性を一切変えずに位相差（デコリレーション）を作り出す。モノラル合成時のコムフィルタ歪み（干渉縞）を起こさずに、自然で包み込まれるような音場の広がりを実現する。"
          },
          {
            "additionalProperties": false,
            "description": "Crosstalk Canceller (inter-aural crosstalk canceller for binaural expansion). Cancels acoustic crosstalk between left and right speakers using a Woodworth spherical head model delay and shadow filter, extending apparent soundstage far beyond the physical speaker positions.",
            "examples": [
              {
                "depth": 0.5,
                "speaker_angle_deg": 30.0,
                "type": "crosstalk_canceller"
              }
            ],
            "properties": {
              "band_low_hz": {
                "description": "Highpass corner frequency [Hz] for the cancellation signal to protect bass punch (optional, 50-2000 Hz).",
                "maximum": 2000.0,
                "minimum": 50.0,
                "type": "number",
                "x-description-ja": "低音のキックを保護するためのキャンセル信号用ハイパス遮断周波数 [Hz]（任意、50〜2000 Hz）。"
              },
              "depth": {
                "description": "Crosstalk cancellation depth (0.0 = bypassed, 1.0 = full cancellation).",
                "maximum": 1.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "相殺深度（0.0 = バイパス、1.0 = 完全相殺）。"
              },
              "speaker_angle_deg": {
                "default": 30.0,
                "description": "Half-angle between listener and speakers [deg] (e.g. 30 deg for standard 60-deg equilateral triangle).",
                "maximum": 90.0,
                "minimum": 10.0,
                "type": "number",
                "x-description-ja": "リスナー正面とスピーカーの挟み角 [度]（正三角形配置なら 30度）。"
              },
              "type": {
                "const": "crosstalk_canceller",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type",
              "depth"
            ],
            "title": "crosstalk_canceller",
            "type": "object",
            "x-description-ja": "クロストーク・キャンセラー（スピーカー外側定位・両耳間クロストーク除去）。頭部音響モデル（Woodworth球頭遅延＋シャドウLPF）に基づき、左右スピーカーからの音がお互いの反対耳に回り込むクロストークを逆相で相殺する。音像を物理スピーカーの外側まで大きく広げる。"
          },
          {
            "additionalProperties": false,
            "description": "Directional EQ (Blauert directional bands height perceptual shaper). Applies elevation boost (around 8 kHz) and depth dip (around 3.5 kHz) strictly to Side (ambience) channels without coloring direct sound or center vocals, expanding apparent vertical height.",
            "examples": [
              {
                "depth_cut_db": -2.0,
                "height_freq_hz": 8000.0,
                "height_gain_db": 3.0,
                "height_q": 2.0,
                "type": "directional_eq"
              }
            ],
            "properties": {
              "depth_cut_db": {
                "default": -2.0,
                "description": "Frontal depth dip gain on Side channel [dB] (-6.0 to 0.0 dB, 0.0 = bypassed).",
                "maximum": 0.0,
                "minimum": -6.0,
                "type": "number",
                "x-description-ja": "Side成分への奥行きディップ利得 [dB]（-6.0〜0.0 dB、0.0 = バイパス）。"
              },
              "height_freq_hz": {
                "default": 8000.0,
                "description": "Elevation peaking center frequency [Hz] (Blauert height band: 4000 to 14000 Hz).",
                "maximum": 14000.0,
                "minimum": 4000.0,
                "type": "number",
                "x-description-ja": "ハイトピーキング中心周波数 [Hz]（Blauert仰角帯域: 4000〜14000 Hz）。"
              },
              "height_gain_db": {
                "default": 3.0,
                "description": "Elevation peaking gain on Side channel [dB] (0.0 = bypassed, up to 12.0 dB).",
                "maximum": 12.0,
                "minimum": 0.0,
                "type": "number",
                "x-description-ja": "Side成分へのハイトピーキング利得 [dB]（0.0 = バイパス、最大 12.0 dB）。"
              },
              "height_q": {
                "default": 2.0,
                "description": "Elevation peaking filter Q factor (0.5 to 5.0).",
                "maximum": 5.0,
                "minimum": 0.5,
                "type": "number",
                "x-description-ja": "ハイトピーキングのQ値（0.5〜5.0）。"
              },
              "type": {
                "const": "directional_eq",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type"
            ],
            "title": "directional_eq",
            "type": "object",
            "x-description-ja": "ディレクショナルEQ（Blauert方向帯域・仰角知覚シェイパー）。耳介の頭上知覚帯域（8kHz付近のブースト）と前方定位を和らげる奥行きディップ（3.5kHz付近のカット）をSide（広がり）成分にのみ適用し、中央のボーカルや直接音の音色を100%保持したまま天井を高める。"
          },
          {
            "additionalProperties": false,
            "description": "Early Reflector (sparse early reflections generator for room volume perception). Synthesizes 4 prime-ratio reflections with high-frequency absorption from Side channels and differential-blends them into L/R, providing physical room height and volume while preserving 100% mono compatibility.",
            "examples": [
              {
                "damping_hz": 6000.0,
                "reflections_mix_db": -20.0,
                "room_scale": 1.0,
                "type": "early_reflector"
              }
            ],
            "properties": {
              "damping_hz": {
                "default": 6000.0,
                "description": "Air and wall absorption damping lowpass frequency [Hz] (1000 to 16000 Hz).",
                "maximum": 16000.0,
                "minimum": 1000.0,
                "type": "number",
                "x-description-ja": "壁面・空気吸音のダンピングローパス周波数 [Hz]（1000〜16000 Hz）。"
              },
              "reflections_mix_db": {
                "default": -20.0,
                "description": "Reflections blend level [dB] (-36.0 to -12.0 dB).",
                "maximum": -12.0,
                "minimum": -36.0,
                "type": "number",
                "x-description-ja": "反射音ブレンド利得 [dB]（-36.0〜-12.0 dB）。"
              },
              "room_scale": {
                "default": 1.0,
                "description": "Room dimension scaling multiplier (0.5 = small intimate room, 2.0 = large concert hall).",
                "maximum": 2.0,
                "minimum": 0.5,
                "type": "number",
                "x-description-ja": "部屋の寸法スケーリング倍率（0.5 = 小部屋、2.0 = 大ホール）。"
              },
              "type": {
                "const": "early_reflector",
                "description": "Node kind.",
                "x-description-ja": "ノードの種別。"
              }
            },
            "required": [
              "type"
            ],
            "title": "early_reflector",
            "type": "object",
            "x-description-ja": "アーリーリフレクター（疎な初期反射アンビエンス生成器）。Side成分から素数比の4タップ（11.3, 17.8, 24.1, 31.7 ms）で初期反射音を生成し、高域吸音LPFを施して左右に逆相差動ブレンドする。モノラル加算時に完全相殺され原音崩壊ゼロのまま、天井の高さと部屋の容積感を付与する。"
          }
        ]
      },
      "maxItems": 64,
      "type": "array",
      "x-description-ja": "上流から下流への順。空なら素通し（バイパス）。⭐ 順番に意味がある —— 非対称な波形整形は DC を作るので、その**後ろ**に dc_blocker を置かないと simulate が落とす。"
    },
    "spec_version": {
      "const": "0.1.0",
      "description": "Version of this format. Required. The daemon refuses any other version.",
      "type": "string",
      "x-description-ja": "この形式の版。**必須**。違う版は daemon が受け付けない。"
    }
  },
  "required": [
    "spec_version",
    "nodes"
  ],
  "title": "Sonir Bench graph definition v1",
  "type": "object",
  "x-description-ja": "Sonir Bench の DSP グラフ定義。直列のノード列 + 補助バス1本。🔴 これは形の検査でしかない —— 通っても鳴るとは限らない（係数の検証は validate、非線形は simulate、鳴らす直前に arm が掛かる）。⚠️ ノードはすべてレート非依存（Hz や dB で書く）——内部レート（705,600 / 768,000 Hz）が変わっても同じグラフがそのまま載る。帯域制限そのものはグラフではなくリサンプル段の設計（resample.*）で決める。",
  "x-title-ja": "Sonir Bench グラフ定義 v1"
}
```

#### `resample-v1.schema.json`

```json
{
  "$defs": {
    "design": {
      "additionalProperties": false,
      "description": "The design itself. Every field may be omitted; the defaults are the shipped design (a linear-phase Kaiser low pass). Absolute frequencies and the tap count only take effect on the entry stage: the exit stage solves its own transition width, so carrying them over would band limit twice and inflate the tap count.",
      "examples": [
        {
          "pass_hz": 20000.0,
          "stopband_db": 120.0
        }
      ],
      "properties": {
        "pass_hz": {
          "$ref": "#/$defs/freq",
          "default": 20000.0,
          "description": "Top of the passband: flat up to here. Absolute [Hz] or a ratio of the entry Nyquist.",
          "x-description-ja": "通過帯域の上端（ここまで平坦に通す）。絶対値 [Hz] でも入口ナイキストに対する比でも書ける。"
        },
        "phase": {
          "default": "linear",
          "description": "Phase type. linear is symmetric: its group delay is constant across the band, so it is pure latency. minimum has no bulk delay but is not symmetric. The graph's own fir node sits after the interpolation, so the pre-ringing of the resample kernel can only be avoided here.",
          "enum": [
            "linear",
            "minimum"
          ],
          "x-description-ja": "位相タイプ。linear は係数が対称で、群遅延は全帯域一定（= 純粋な遅れ）。minimum は bulk delay を持たない代わりに対称でなくなる。⭐ グラフの fir は補間の**後**に掛かるので、この段のプリリンギングはここでしか避けられない。"
        },
        "shape": {
          "default": null,
          "description": "Extra shaping below the top edge. Omitted (null) is a plain low pass. The band limit itself cannot be removed -- it is pass_hz, and it is always there. Frequencies are absolute, so this too is entry stage only.",
          "oneOf": [
            {
              "$ref": "#/$defs/shape_high_pass"
            },
            {
              "$ref": "#/$defs/shape_band_stop"
            },
            {
              "type": "null"
            }
          ],
          "x-description-ja": "上端より下に載せる整形。**省略（null）なら素の低域通過。**🔴 帯域制限そのものは外せない（それが pass_hz で、常に在る）。周波数は絶対値なので、これも入口段にしか掛からない。"
        },
        "stop_hz": {
          "default": null,
          "description": "Bottom of the stopband. Omitted (null) derives it from the rate (entry rate minus passband), which is where the first image starts. Moving it down only improves image rejection; moving it above the derived value is refused (resample.stopband_too_high). Entry stage only.",
          "oneOf": [
            {
              "$ref": "#/$defs/freq"
            },
            {
              "type": "null"
            }
          ],
          "x-description-ja": "阻止域の下端。**省略（null）ならそのレートから導く**（入口レート − 通過帯域 = 第1イメージの立つ位置）。⭐ 下げるぶんにはイメージ抑圧が改善するだけ。導出値より**上**へ動かすことだけを断る（resample.stopband_too_high）。🔴 入口段にしか効かない。"
        },
        "stopband_db": {
          "default": 120.0,
          "description": "Stopband attenuation asked of the window [dB]. For Kaiser and Gaussian it also decides the window parameter and, with the transition width, the tap count. It is a request, not a result: the attenuation actually reached is measured, and a design that does not clear the minimum image rejection is refused (resample.image_leak).",
          "type": "number",
          "x-description-ja": "窓に要求する阻止域減衰 [dB]。Kaiser とガウシアンではここから窓のパラメータが決まり、遷移幅と合わせてタップ数も決まる。⚠️ **これは要求であって結果ではない** —— 実際に届いた抑圧は測られ、最低イメージ抑圧に届かない設計は断られる（resample.image_leak）。"
        },
        "taps": {
          "default": {
            "type": "auto"
          },
          "description": "How the tap count is decided.",
          "oneOf": [
            {
              "$ref": "#/$defs/taps_auto"
            },
            {
              "$ref": "#/$defs/taps_fixed"
            }
          ],
          "x-description-ja": "タップ数の決め方。"
        },
        "window": {
          "default": "kaiser",
          "description": "Window function. Only kaiser and gaussian take their parameter from stopband_db; the fixed windows have a fixed attenuation, and the weakest of them cannot clear the minimum image rejection at all. kaiser and gaussian trade depth against ringing at the same tap count.",
          "enum": [
            "kaiser",
            "hann",
            "hamming",
            "blackman",
            "blackman_harris",
            "gaussian"
          ],
          "x-description-ja": "窓の種類。⚠️ stopband_db からパラメータが決まるのは kaiser と gaussian だけで、残りは減衰が固定 —— 弱いものは最低イメージ抑圧に**そもそも届かない**。kaiser と gaussian は同じタップ数で深さとリンギングを交換する。"
        }
      },
      "type": "object",
      "x-description-ja": "設計そのもの。**どの欄も省略できる**（既定は出荷時の設計 = 直線位相の Kaiser 低域通過）。🔴 絶対値 [Hz] の指定とタップ数が効くのは**入口段だけ** —— 出口段は自分の遷移幅から解き直すので、持ち込むと帯域制限が二度掛かり、タップ数も膨れる。"
    },
    "freq": {
      "description": "A frequency, written either as an absolute value [Hz] or as a ratio of the entry Nyquist. Written as a ratio, one design means the same thing on every entry rate instead of splitting into one file per rate; it is resolved once, at the entry rate, and is identical to a hand-written absolute value from then on.",
      "oneOf": [
        {
          "description": "Absolute frequency [Hz].",
          "type": "number",
          "x-description-ja": "絶対値 [Hz]。"
        },
        {
          "additionalProperties": false,
          "description": "A ratio of the entry Nyquist, plus an optional offset in Hz.",
          "properties": {
            "of_nyquist": {
              "description": "Ratio of the entry Nyquist (entry rate / 2). 1.0 is the entry Nyquist itself.",
              "type": "number",
              "x-description-ja": "入口ナイキスト（入口レート / 2）に対する比。1.0 で入口ナイキストちょうど。"
            },
            "plus_hz": {
              "default": 0,
              "description": "Offset from that ratio [Hz]. This is what lets the cutoff be a ratio while the transition width stays in Hz.",
              "type": "number",
              "x-description-ja": "比から動かす幅 [Hz]。⭐ **カットオフは比・遷移幅は [Hz]** という書き方のために在る。"
            }
          },
          "required": [
            "of_nyquist"
          ],
          "type": "object",
          "x-description-ja": "入口ナイキストに対する比（+ 絶対値の下駄）。"
        }
      ],
      "x-description-ja": "周波数。**絶対値 [Hz] か、入口ナイキストに対する比**で書く。⭐ 比で書くと、同じ「特性」が入口レートごとのファイルへ分裂しない。解くのは入口レートで1度だけで、その後は手で書いた絶対値とまったく同じ扱い。"
    },
    "shape_band_stop": {
      "additionalProperties": false,
      "description": "Band stop: removes low_hz to high_hz. Both edges have to sit inside the band limit and the transition width has to be positive (resample.shape_out_of_range).",
      "examples": [
        {
          "high_hz": 60.0,
          "low_hz": 50.0,
          "transition_hz": 10.0,
          "type": "band_stop"
        }
      ],
      "properties": {
        "high_hz": {
          "description": "Upper cutoff frequency [Hz].",
          "type": "number",
          "x-description-ja": "上側の遮断周波数 [Hz]。"
        },
        "low_hz": {
          "description": "Lower cutoff frequency [Hz].",
          "type": "number",
          "x-description-ja": "下側の遮断周波数 [Hz]。"
        },
        "transition_hz": {
          "description": "Transition width [Hz].",
          "type": "number",
          "x-description-ja": "遷移幅 [Hz]。"
        },
        "type": {
          "const": "band_stop"
        }
      },
      "required": [
        "type",
        "low_hz",
        "high_hz",
        "transition_hz"
      ],
      "title": "band_stop",
      "type": "object",
      "x-description-ja": "帯域阻止。low_hz〜high_hz を落とす。⚠️ 両端が帯域制限の内側にあり、遷移幅が正であること（resample.shape_out_of_range）。"
    },
    "shape_high_pass": {
      "additionalProperties": false,
      "description": "High pass: removes everything below cutoff_hz.",
      "examples": [
        {
          "cutoff_hz": 15.0,
          "transition_hz": 10.0,
          "type": "high_pass"
        }
      ],
      "properties": {
        "cutoff_hz": {
          "description": "Cutoff frequency (-6 dB point) [Hz]. Everything below it is removed, making the stage a band pass.",
          "type": "number",
          "x-description-ja": "遮断周波数（-6 dB 点）[Hz]。ここから下を落とす（結果は帯域通過）。"
        },
        "transition_hz": {
          "description": "Transition width [Hz]. It is what the tap count is solved from when taps is auto.",
          "type": "number",
          "x-description-ja": "遷移幅 [Hz]。taps が auto のとき、タップ数はここから解かれる。"
        },
        "type": {
          "const": "high_pass"
        }
      },
      "required": [
        "type",
        "cutoff_hz",
        "transition_hz"
      ],
      "title": "high_pass",
      "type": "object",
      "x-description-ja": "高域通過。cutoff_hz から下を落とす。"
    },
    "taps_auto": {
      "additionalProperties": false,
      "description": "Solve the tap count from the transition width and the stopband attenuation.",
      "examples": [
        {
          "type": "auto"
        }
      ],
      "properties": {
        "type": {
          "const": "auto"
        }
      },
      "required": [
        "type"
      ],
      "title": "auto",
      "type": "object",
      "x-description-ja": "遷移幅と阻止域減衰からタップ数を解く。"
    },
    "taps_fixed": {
      "additionalProperties": false,
      "description": "Give the tap count directly. Entry stage only: the number means sharpness relative to the entry stage's transition width, and the exit stage needs a width that differs by orders of magnitude.",
      "examples": [
        {
          "taps": 1537,
          "type": "fixed"
        }
      ],
      "properties": {
        "taps": {
          "description": "Tap count. Linear phase requires an odd number (resample.taps_not_odd).",
          "maximum": 1048576,
          "minimum": 1,
          "type": "integer",
          "x-description-ja": "タップ数。⚠️ 直線位相なら**奇数**（resample.taps_not_odd）。"
        },
        "type": {
          "const": "fixed"
        }
      },
      "required": [
        "type",
        "taps"
      ],
      "title": "fixed",
      "type": "object",
      "x-description-ja": "タップ数を直接指定する。🔴 **効くのは入口段だけ** —— この数字は「入口の遷移幅に対する鋭さ」で、必要な遷移幅が桁で違う出口段へ降ろすと CPU だけを払う。"
    }
  },
  "$id": "https://sonir.app/schema/bench/resample-v1.json",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": false,
  "description": "A Sonir Bench resample design: the filter that takes the incoming rate up to the internal rate (and back down at the exit). This is where band limiting lives -- the graph cannot express it. This is a shape check only: passing it does not mean the design can be used (the coefficients are solved per stage and per entry rate, and validate measures the stopband before accepting them). The design itself carries no sample rate and no coefficients, so the same design is solved again for every stage and every entry rate; targets and min_partition sit next to it, not inside it, because they declare intent and do not change a single coefficient. A file of this shape is what the app saves under resample/<name>.json, and dropping one there makes the design appear in the library.",
  "examples": [
    {
      "design": {
        "pass_hz": 20000.0,
        "stopband_db": 120.0
      },
      "kind": "resample-design",
      "name": "既定と同じ設計",
      "spec_version": "0.1.0"
    },
    {
      "design": {
        "pass_hz": {
          "of_nyquist": 0.9
        },
        "phase": "minimum",
        "stopband_db": 120.0
      },
      "kind": "resample-design",
      "min_partition": 16384,
      "name": "入口ナイキストに合わせた最小位相",
      "spec_version": "0.1.0",
      "targets": []
    }
  ],
  "properties": {
    "design": {
      "$ref": "#/$defs/design"
    },
    "kind": {
      "const": "resample-design",
      "description": "File kind. Required, so a file cannot be mistaken for a graph.",
      "type": "string",
      "x-description-ja": "ファイルの種別。**必須**（グラフと取り違えないため）。"
    },
    "min_partition": {
      "default": null,
      "description": "Smallest partition length B [frames] this design is meant to run at. Omitted means the shortest B. Like targets it changes no coefficient: it only decides which row of the cost forecast is shown as representative.",
      "enum": [
        4096,
        8192,
        16384,
        32768,
        65536,
        null
      ],
      "examples": [
        16384
      ],
      "type": [
        "integer",
        "null"
      ],
      "x-description-ja": "この設計を回すつもりの最小のパーティション長 B [frames]。**省略すると最短の B。**⚠️ targets と同じく係数は1ビットも変わらない —— 消費率の予報を**どの行で代表させるか**だけを決める。"
    },
    "name": {
      "description": "Display name. Free text. The identifier is the file name, not this.",
      "type": "string",
      "x-description-ja": "表示名（自由。日本語でよい）。⚠️ **識別子はファイル名のほうで、これではない。**"
    },
    "spec_version": {
      "const": "0.1.0",
      "description": "Version of this format. Required. It moves independently of the graph's spec_version.",
      "type": "string",
      "x-description-ja": "この形式の版。**必須**。グラフの spec_version とは独立に上がる。"
    },
    "targets": {
      "default": [],
      "description": "Entry rates this design is meant for [Hz]. Empty (or omitted) means any rate. It does not change the coefficients: it declares which rates the design is offered for, and an empty list is checked against both rate families before it is saved.",
      "items": {
        "enum": [
          44100,
          88200,
          176400,
          48000,
          96000,
          192000
        ],
        "type": "integer"
      },
      "type": "array",
      "x-description-ja": "この設計を使うつもりの入口レート [Hz]。**空（や省略）なら「どのレートでも」。**⚠️ 係数は1ビットも変わらない —— どのレートで出すかの宣言で、空のときは保存の前に両方のレートファミリーで解けることを確かめる。"
    }
  },
  "required": [
    "spec_version",
    "kind",
    "name",
    "design"
  ],
  "title": "Sonir Bench resample design v1",
  "type": "object",
  "x-description-ja": "Sonir Bench のレート変換段の設計。入口のレートを内部レートへ上げる（出口で戻す）フィルタで、**帯域制限を書けるのはここだけ**（グラフには書けない）。🔴 これは形の検査でしかない —— 通っても使えるとは限らない（係数は段ごと・入口レートごとに解き直され、阻止域の抑圧を測ってから受け取られる）。⚠️ 設計そのものはレートも係数も持たない。隣にある targets / min_partition は**どう使うつもりかの宣言**で、係数の解き方には1ビットも効かない（だから設計の中に無い）。この形のファイルがアプリの保存する resample/<名前>.json そのもので、そこへ置けば一覧に出る。",
  "x-title-ja": "Sonir Bench レート変換段の設計 v1"
}
```

---

## 付録B. エラーコード全件（実装から生成）

層は `arm` / `graph` / `node` / `resample` / `rpc` / `sdm` / `simulate`。`rpc` は要求の封筒（数値コード）で、残りはモジュールを断る段ごとの層。**人が別に書いた一覧ではない**（層の名前もこのファイルから読んでいる）。

🔴 **ここに無いコードは実装にも無い。** 別に書いた一覧を配ると、AI は存在しないコードを直そうとする。

#### `errors-v1.json`

```json
{
  "arm": [
    {
      "code": "arm.not_simulated",
      "messages": {
        "en": "<node> modifies the signal non-linearly. Please run simulation verification in the graph editor before playback",
        "ja": "<node> は音声を非線形に加工するため、再生前に事前シミュレーションによる安全検証が必要です。グラフ編集画面で「事前検証」を実行してください"
      }
    },
    {
      "code": "arm.no_limiter",
      "messages": {
        "en": "Using <node> requires a protection limiter. Please enable auto headroom in the output stage settings",
        "ja": "<node> を使用するには保護リミッターが必要です。再生設定の出力段で「オートヘッドルーム（auto）」を有効にしてください"
      }
    },
    {
      "code": "arm.soft_start_out_of_range",
      "messages": {
        "en": "Soft start duration must be between 20 and 500 ms",
        "ja": "ソフトスタート時間は 20〜500 ms の範囲で指定してください"
      }
    }
  ],
  "graph": [
    {
      "code": "graph.malformed",
      "messages": {
        "en": "The graph definition does not match the schema",
        "ja": "グラフ定義の形式がスキーマに合っていません"
      }
    }
  ],
  "node": [
    {
      "code": "biquad.rate_out_of_range",
      "messages": {
        "en": "Design rate is 0. Specify an internal rate (705600 / 768000)",
        "ja": "設計レートが 0。内部レート（705600 / 768000）を指定する"
      }
    },
    {
      "code": "biquad.cutoff_out_of_range",
      "messages": {
        "en": "The cutoff is at or below 0 Hz, or at or above Nyquist",
        "ja": "遮断周波数が 0 以下、またはナイキスト以上"
      }
    },
    {
      "code": "biquad.q_out_of_range",
      "messages": {
        "en": "Q is out of range (0.05 to 40)",
        "ja": "Q が範囲外（0.05 〜 40）"
      }
    },
    {
      "code": "biquad.gain_out_of_range",
      "messages": {
        "en": "The gain is out of range (+-24 dB)",
        "ja": "利得が範囲外（±24 dB）"
      }
    },
    {
      "code": "biquad.unstable",
      "messages": {
        "en": "The coefficients are not finite (g and k must be positive and finite)",
        "ja": "係数が有限でない（g・k は正の有限値でなければならない）"
      }
    },
    {
      "code": "biquad.excessive_gain",
      "messages": {
        "en": "The peak gain is too high",
        "ja": "ピーク利得が大きすぎる"
      }
    },
    {
      "code": "biquad.ultrasonic_boost",
      "messages": {
        "en": "It boosts above 25 kHz. That never becomes audible, yet it still reaches the DAC, the amplifier and the tweeter, so it is refused by default",
        "ja": "25 kHz 超を増幅している。可聴しないまま DAC → アンプ → ツイーターに入るため既定で拒否する"
      }
    },
    {
      "code": "onepole.rate_out_of_range",
      "messages": {
        "en": "Design rate is 0. Specify an internal rate (705600 / 768000)",
        "ja": "設計レートが 0。内部レート（705600 / 768000）を指定する"
      }
    },
    {
      "code": "onepole.cutoff_out_of_range",
      "messages": {
        "en": "The cutoff is out of range (0.05 Hz to Nyquist)",
        "ja": "遮断周波数が範囲外（0.05 Hz 〜 ナイキスト）"
      }
    },
    {
      "code": "onepole.unstable",
      "messages": {
        "en": "The coefficients are not finite (g must be positive and finite)",
        "ja": "係数が有限でない（g は正の有限値でなければならない）"
      }
    }
  ],
  "protocol": 1,
  "resample": [
    {
      "code": "resample.no_transition_band",
      "messages": {
        "en": "Passband is too wide for the entry rate. Lower the passband edge to prevent aliasing",
        "ja": "入力サンプリングレートに対して通過帯域が広すぎます。折り返し雑音（エイリアシング）を防ぐため、通過帯域の上限周波数を下げてください"
      }
    },
    {
      "code": "resample.rates_invalid",
      "messages": {
        "en": "Sample rate or conversion ratio is invalid (zero or not an integer ratio)",
        "ja": "サンプリングレートまたは変換比が不正です（0、または整数比ではありません）"
      }
    },
    {
      "code": "resample.taps_out_of_range",
      "messages": {
        "en": "Filter taps out of range (exceeds real-time processing limit or too short)",
        "ja": "タップ数が許容範囲外です（リアルタイム処理の負荷上限を超えているか、フィルタ長が不足しています）"
      }
    },
    {
      "code": "resample.taps_not_odd",
      "messages": {
        "en": "Linear phase requires an odd number of taps to maintain integer sample group delay",
        "ja": "直線位相フィルタではタップ数を奇数にしてください（群遅延を整数サンプルに保つため）"
      }
    },
    {
      "code": "resample.design_failed",
      "messages": {
        "en": "Window method failed to generate coefficients. Please check cutoff frequency and tap count",
        "ja": "窓関数法でフィルタ係数を生成できませんでした（遮断周波数やタップ数の設定値を見直してください）"
      }
    },
    {
      "code": "resample.minimum_phase_failed",
      "messages": {
        "en": "Conversion to minimum phase failed",
        "ja": "最小位相フィルタへの変換に失敗しました"
      }
    },
    {
      "code": "resample.not_finite",
      "messages": {
        "en": "Filter coefficients contain invalid values (NaN or Inf)",
        "ja": "フィルタ係数に不正な値（非数または無限大）が含まれています"
      }
    },
    {
      "code": "resample.phase_mismatch",
      "messages": {
        "en": "Declared phase type disagrees with coefficient symmetry (linear phase must be symmetric, minimum phase asymmetric)",
        "ja": "指定された位相タイプとフィルタ係数の対称性が一致しません（直線位相は対称、最小位相は非対称である必要があります）"
      }
    },
    {
      "code": "resample.gain_not_unity",
      "messages": {
        "en": "Passband gain deviates from unity (0 dB)",
        "ja": "通過帯域のゲインが目標値（0 dB）からずれています"
      }
    },
    {
      "code": "resample.image_leak",
      "messages": {
        "en": "Insufficient stopband attenuation causes aliasing in the audible band. Use a stronger window or increase tap count",
        "ja": "阻止域の減衰量が不足しています。折り返しノイズが可聴帯域に混入するのを防ぐため、減衰量の大きい窓関数を選ぶかタップ数を増やしてください"
      }
    },
    {
      "code": "resample.stopband_too_high",
      "messages": {
        "en": "Stopband edge is too high. It must not exceed the first image frequency (sample rate - passband)",
        "ja": "阻止域の開始周波数が高すぎます。折り返し歪みが発生する周波数（レート − 通過帯域）以下に設定してください"
      }
    },
    {
      "code": "resample.shape_out_of_range",
      "messages": {
        "en": "Shaping frequency must be within the valid band (above 0 and below passband top), or transition width must be positive",
        "ja": "整形の周波数が有効帯域外（0 より大きく通過帯域の上端より下である必要があります）か、遷移幅が 0 以下です"
      }
    }
  ],
  "rpc": [
    {
      "code": -32700,
      "messages": {
        "en": "Not readable as JSON",
        "ja": "JSON として解析できません"
      },
      "name": "parse_error"
    },
    {
      "code": -32600,
      "messages": {
        "en": "Not shaped like a JSON-RPC 2.0 request",
        "ja": "JSON-RPC 2.0 の要求形式に合っていません"
      },
      "name": "invalid_request"
    },
    {
      "code": -32601,
      "messages": {
        "en": "Method not found",
        "ja": "指定されたメソッドが見つかりません"
      },
      "name": "method_not_found"
    },
    {
      "code": -32602,
      "messages": {
        "en": "Argument has the wrong shape, or is out of range",
        "ja": "引数の形式が正しくないか、許容範囲外です"
      },
      "name": "invalid_params"
    },
    {
      "code": -32603,
      "messages": {
        "en": "Internal daemon error",
        "ja": "バックエンド処理の内部エラー"
      },
      "name": "internal_error"
    },
    {
      "code": 1001,
      "messages": {
        "en": "The audio engine is already running. Stop playback before changing this configuration",
        "ja": "オーディオエンジンが既に動作しています。設定変更の前に再生を停止してください"
      },
      "name": "already_running"
    },
    {
      "code": 1002,
      "messages": {
        "en": "The audio engine is not running. Start playback first",
        "ja": "オーディオエンジンが動作していません。先に再生を開始してください"
      },
      "name": "not_running"
    },
    {
      "code": 1003,
      "messages": {
        "en": "Cannot open audio device",
        "ja": "オーディオデバイスを開けません"
      },
      "name": "device_unavailable"
    },
    {
      "code": 1004,
      "messages": {
        "en": "Unknown parameter ID",
        "ja": "指定されたパラメータが見つかりません"
      },
      "name": "unknown_param"
    },
    {
      "code": 1005,
      "messages": {
        "en": "Real-time processing queue is full. Could not apply parameter update",
        "ja": "リアルタイム処理キューが満杯のため、パラメータ更新を適用できませんでした"
      },
      "name": "param_rejected"
    },
    {
      "code": 1006,
      "messages": {
        "en": "Sample rate must belong to the 44.1 kHz, 48 kHz, or 32 kHz family",
        "ja": "サンプリングレートは 44.1 kHz、48 kHz、または 32 kHz 系列を指定してください"
      },
      "name": "unknown_rate_family"
    },
    {
      "code": 1011,
      "messages": {
        "en": "Real-time processing queue is full. Could not apply graph swap",
        "ja": "リアルタイム処理キューが満杯のため、グラフの切り替えを適用できませんでした"
      },
      "name": "swap_rejected"
    },
    {
      "code": 1013,
      "messages": {
        "en": "Safety validation failed. Non-linear graphs require simulation verification and active protection limiter",
        "ja": "安全検証に合格していません。非線形ノードを含むグラフにはシミュレーション検証と保護リミッターが必要です"
      },
      "name": "not_armed"
    },
    {
      "code": 1016,
      "messages": {
        "en": "Unknown graph ID. Check available graphs in the library",
        "ja": "指定されたグラフがライブラリに見つかりません"
      },
      "name": "unknown_graph"
    },
    {
      "code": 1017,
      "messages": {
        "en": "No workspace folder configured. Changes will not be saved across restarts",
        "ja": "作業フォルダが設定されていません。作成した設定は再起動時に失われます"
      },
      "name": "workspace_unavailable"
    },
    {
      "code": 1018,
      "messages": {
        "en": "Cannot detect input sample rate. Specify an explicit sample rate to open the device",
        "ja": "入力サンプリングレートを自動検出できませんでした。明示的にレートを指定して開いてください"
      },
      "name": "entry_rate_unreadable"
    },
    {
      "code": 1019,
      "messages": {
        "en": "Unknown resample design name. Check available designs in the library",
        "ja": "指定されたリサンプル設計がライブラリに見つかりません"
      },
      "name": "unknown_resample_design"
    },
    {
      "code": 1020,
      "messages": {
        "en": "The resample design failed validation checks",
        "ja": "リサンプル設計の検証チェックに合格しませんでした"
      },
      "name": "resample_design_rejected"
    },
    {
      "code": 1021,
      "messages": {
        "en": "Invalid resample design configuration. Conflicting target sample rates detected",
        "ja": "リサンプル設計の構成が無効です。対象サンプリングレートの競合を確認してください"
      },
      "name": "resample_selection_invalid"
    },
    {
      "code": 1022,
      "messages": {
        "en": "The DSD modulator design failed validation checks. The audio device itself is operating normally",
        "ja": "DSD変調器の設計が検証チェックに合格しませんでした（オーディオデバイス自体は正常です）"
      },
      "name": "sdm_design_rejected"
    },
    {
      "code": 1023,
      "messages": {
        "en": "This operation cannot be performed over remote network connection",
        "ja": "この操作はリモートネットワーク接続経由では実行できません"
      },
      "name": "remote_not_allowed"
    },
    {
      "code": 1024,
      "messages": {
        "en": "A measurement sweep is currently playing. Wait for it to complete or stop it first",
        "ja": "音響測定スイープが既に再生中です。完了まで待つか、測定を停止してください"
      },
      "name": "measurement_busy"
    },
    {
      "code": 1025,
      "messages": {
        "en": "This beta build has expired. Download a newer build to keep using Sonir Bench",
        "ja": "このベータ版は使用期限を過ぎました。引き続き使うには新しい版を入手してください"
      },
      "name": "license_expired"
    },
    {
      "code": 1026,
      "messages": {
        "en": "The system clock is set earlier than a time this Mac has already passed. Correct the date and time to continue",
        "ja": "Mac の時刻が、既に過ぎた時点より前になっています。日付と時刻を正しく直すと使えるようになります"
      },
      "name": "clock_rolled_back"
    }
  ],
  "sdm": [
    {
      "code": "sdm.order_out_of_range",
      "messages": {
        "en": "Modulator order is out of the supported range",
        "ja": "変調器の次数が許容範囲外です"
      }
    },
    {
      "code": "sdm.obg_out_of_range",
      "messages": {
        "en": "Out-of-band gain is out of range. Above 1.5 a 1-bit modulator becomes unstable; at or below 1.0 noise shaping is ineffective",
        "ja": "帯域外利得（OBG）が許容範囲外です。1.5 を超えると変調器が発振・不安定になり、1.0 以下ではノイズシェーピング効果が得られません"
      }
    },
    {
      "code": "sdm.obg_unreachable",
      "messages": {
        "en": "Could not solve for target out-of-band gain at the specified order and sample rate",
        "ja": "指定された次数とサンプリングレートの組み合わせでは、目標の帯域外利得を満たす設計を生成できませんでした"
      }
    },
    {
      "code": "sdm.pole_outside_unit_circle",
      "messages": {
        "en": "A loop-filter pole lies on or outside the unit circle, causing instability. Please adjust order or out-of-band gain",
        "ja": "ループフィルタが発振条件（単位円外の極）に達しているため、安定した設計を生成できませんでした。次数や帯域外利得を見直してください"
      }
    },
    {
      "code": "sdm.msa_too_low",
      "messages": {
        "en": "Maximum stable amplitude is too low; required attenuation to stabilize the modulator leaves insufficient signal headroom",
        "ja": "最大安定振幅（MSA）が小さすぎます。変調器を安定させるために必要な減衰量が大きすぎるため、十分な信号レベルを確保できません"
      }
    },
    {
      "code": "sdm.attenuation_too_large",
      "messages": {
        "en": "Required attenuation for this design exceeds the maximum limit (12 dB)",
        "ja": "この設計に必要な減衰量が許容上限（12 dB）を超えています"
      }
    },
    {
      "code": "sdm.rate_family_unsupported",
      "messages": {
        "en": "Input sample rate family has no native DSD rate and family conversion is disabled. Enable conversion to 44.1 kHz family for DSD output",
        "ja": "入力音源のサンプリングレート系列に対応する DSD レートがありません。DSD 出力を行うには、設定で「44.1 kHz 系へ変換」を有効にしてください"
      }
    },
    {
      "code": "sdm.interpolation_design_failed",
      "messages": {
        "en": "Could not design internal interpolation filter for this sample rate",
        "ja": "このサンプリングレートに対応する内部補間フィルタを設計できませんでした"
      }
    },
    {
      "code": "sdm.family_conversion_failed",
      "messages": {
        "en": "Could not build rate-family conversion filter between these two sample rates",
        "ja": "変調器前段のレートファミリー変換フィルタを、指定されたレート間で設計できませんでした"
      }
    },
    {
      "code": "sdm.exit_rate_fixed",
      "messages": {
        "en": "Exit rate cannot be modified during DSD (DoP) output (fixed to DSD rate / 16 by DoP specification)",
        "ja": "DSD（DoP）出力時は出力サンプリングレートを変更できません（DoP 伝送仕様により DSD レートの 1/16 に固定されます）"
      }
    }
  ],
  "simulate": [
    {
      "code": "simulate.not_finite",
      "messages": {
        "en": "Calculation error (NaN or Inf detected). Check if filter or gain settings are causing oscillation",
        "ja": "計算エラー（非数または無限大）が発生しました。フィルタやゲインの設定値が発振していないか確認してください"
      }
    },
    {
      "code": "simulate.excessive_peak",
      "messages": {
        "en": "Output exceeded 0 dBFS for reference-level input. Gain is too high, please reduce gain settings",
        "ja": "基準レベルの入力に対して出力が 0 dBFS を超えました。ゲインが高すぎるため、ゲイン設定を下げてください"
      }
    },
    {
      "code": "simulate.dc_offset",
      "messages": {
        "en": "A DC offset was detected in the output. For speaker protection, place a DC Blocker node after the non-linear stage",
        "ja": "出力に直流成分（DCオフセット）が検出されました。スピーカー保護のため、非線形ノードの後ろに「DC Blocker」ノードを追加してください"
      }
    },
    {
      "code": "simulate.ultrasonic_energy",
      "messages": {
        "en": "Excessive ultrasonic energy above 25 kHz detected. To protect equipment, please add a low-pass filter to attenuate high frequencies",
        "ja": "25 kHz 超の超高周波成分が許容量を超えています。機器保護のため、ローパスフィルタ等を追加して高域を抑えてください"
      }
    },
    {
      "code": "simulate.not_runnable",
      "messages": {
        "en": "Could not run simulation stimulus (block length or sample rate mismatch)",
        "ja": "テスト信号のシミュレーションを実行できませんでした（ブロック長またはサンプリングレートの不整合）"
      }
    }
  ]
}
```

---

## 付録C. リファレンスモジュール

**そのまま貼れば通る。** テストが4段すべてを両レートファミリーで通している。

### リファレンスモジュール

そのまま渡せるグラフ定義。**白紙から書くより、近い形を1つ選んで直すほうが速い。**

| ファイル | 何を見せているか | 刺激の検証 |
|---|---|---|
| `bypass.json` | 空のノード列 = 素通し。**「何も積まない」も正当な定義** | 走らない |
| `tone-stack.json` | Biquad 3段（低域シェルフ + ベル + 高域シェルフ）。**線形だけで音色を作る** | 走らない |
| `pentode-se.json` | 非対称サチュレーション + **DC ブロッカー**。⭐ 順番に意味がある | 走る |
| `transformer-saturation.json` | ヒステリシス（履歴を持つ非線形）+ 前段の高域通過 | 走る |
| `power-sag.json` | 補助バスの側鎖（包絡 → 平滑 → サグ）+ 軽いサチュレーション | 走る |

#### 🔴 これらは「通ること」がテストで固定されている

全ファイルについて、形 → `validate` → `simulate` → `arm` の4段を**内部レートの両方で**通している。
**公開している実例がアプリに通らない**という状態を作らないため（最初に真似されるのは実例で、
通らない実例はそのまま増殖する）。

#### 名前の付け方

- **記述的に名付ける。** 何をする回路 / 構造なのかを書く（`pentode-se` = 五極管シングルエンド）
- **商標名・ブランド名・製品名を使わない。** 回路のトポロジ自体は誰のものでもないが、**名前は別**
- **「〜風」「〜系」も避ける。** 商標名を含む以上、回避にならない。型番・年式・シリーズ名も同じ

⚠️ 手元で好きな名前を付けるのは自由。これは**共有するもの**に掛かる目安。

#### `bypass.json`

```json
{
  "spec_version": "0.1.0",
  "nodes": []
}
```

#### `pentode-se.json`

```json
{
  "spec_version": "0.1.0",
  "nodes": [
    { "type": "waveshaper", "shape": "asymmetric", "drive_db": 14.0 },
    { "type": "dc_blocker", "cutoff_hz": 5.0 },
    { "type": "biquad", "shape": "high_shelf", "cutoff_hz": 6000.0, "q": 0.707, "gain_db": -2.0 }
  ]
}
```

#### `power-sag.json`

```json
{
  "spec_version": "0.1.0",
  "nodes": [
    { "type": "envelope_follower", "detector": "rms", "attack_ms": 2.0, "release_ms": 180.0 },
    { "type": "one_pole_smoother", "time_ms": 40.0 },
    { "type": "signal_gain", "depth_db": 4.0 },
    { "type": "waveshaper", "shape": "tanh", "drive_db": 6.0 }
  ]
}
```

#### `tone-stack.json`

```json
{
  "spec_version": "0.1.0",
  "nodes": [
    { "type": "biquad", "shape": "low_shelf", "cutoff_hz": 120.0, "q": 0.707, "gain_db": 3.0 },
    { "type": "biquad", "shape": "bell", "cutoff_hz": 2500.0, "q": 1.2, "gain_db": -2.5 },
    { "type": "biquad", "shape": "high_shelf", "cutoff_hz": 8000.0, "q": 0.707, "gain_db": -1.5 }
  ]
}
```

#### `transformer-saturation.json`

```json
{
  "spec_version": "0.1.0",
  "nodes": [
    { "type": "biquad", "shape": "high_pass", "cutoff_hz": 20.0, "q": 0.707 },
    { "type": "hysteresis", "drive_db": 10.0, "width": 0.45 },
    { "type": "dc_blocker", "cutoff_hz": 5.0 }
  ]
}
```

---

## 付録D. リファレンスのレート変換設計

**そのまま保存すれば使える。** テストが入口として選べるレートすべてで段を解いている。

### リファレンスのレート変換設計

そのまま保存して使える設計ファイル。**帯域制限を書けるのはここだけ**（グラフには書けない）。

| ファイル | 何を見せているか |
|---|---|
| `flat-20k.json` | 出荷時と同じ設計。**可聴帯域まで平坦・直線位相** |
| `nyquist-follow.json` | 上端を**入口ナイキストに対する比**で書く。同じ特性がレートごとのファイルに分裂しない |
| `minimum-phase-20k.json` | 最小位相。⭐ **この段のプリリンギングはここでしか避けられない**（グラフの `fir` は補間の後ろ） |
| `rumble-cut.json` | 上端の下に整形を1つ載せる（超低域を落として帯域通過にする） |
| `gaussian-soft.json` | 窓を替えて、同じタップ数で深さと尾の長さを交換する |

#### 🔴 これらは「解けること」がテストで固定されている

全ファイルについて、**入口として選べるレートすべて**で段を解いている（`targets` を宣言して
いるファイルがあれば、そのレートで）。公開している実例がアプリに通らない状態を作らないため。

#### 使い方

作業フォルダの `resample/` へ置けば一覧に出る。**識別子はファイル名**で、`name` は表示名。

#### 名前の付け方

- **何をする設計なのかを書く。** 数値そのものではなく、意図が読める名前にする
- **商標名・ブランド名・製品名を使わない**（リファレンスモジュールと同じ目安）

#### `resample/flat-20k.json`

```json
{
  "spec_version": "0.1.0",
  "kind": "resample-design",
  "name": "素の 20 kHz 帯域制限",
  "design": {
    "pass_hz": 20000.0,
    "stopband_db": 120.0,
    "window": "kaiser",
    "phase": "linear",
    "taps": { "type": "auto" }
  }
}
```

#### `resample/gaussian-soft.json`

```json
{
  "spec_version": "0.1.0",
  "kind": "resample-design",
  "name": "尾の短い窓（ガウシアン）",
  "design": {
    "pass_hz": 20000.0,
    "stopband_db": 120.0,
    "window": "gaussian",
    "phase": "linear",
    "taps": { "type": "auto" }
  }
}
```

#### `resample/minimum-phase-20k.json`

```json
{
  "spec_version": "0.1.0",
  "kind": "resample-design",
  "name": "プリリンギングの無い 20 kHz 帯域制限",
  "design": {
    "pass_hz": 20000.0,
    "stopband_db": 120.0,
    "window": "kaiser",
    "phase": "minimum",
    "taps": { "type": "auto" }
  }
}
```

#### `resample/nyquist-follow.json`

```json
{
  "spec_version": "0.1.0",
  "kind": "resample-design",
  "name": "音源の上端に合わせる（入口ナイキスト比）",
  "design": {
    "pass_hz": { "of_nyquist": 0.9 },
    "stop_hz": { "of_nyquist": 0.9, "plus_hz": 2000.0 },
    "stopband_db": 120.0,
    "window": "kaiser",
    "phase": "linear",
    "taps": { "type": "auto" }
  }
}
```

#### `resample/rumble-cut.json`

```json
{
  "spec_version": "0.1.0",
  "kind": "resample-design",
  "name": "超低域を落とす（帯域通過）",
  "design": {
    "pass_hz": 20000.0,
    "stopband_db": 120.0,
    "window": "kaiser",
    "phase": "linear",
    "shape": { "type": "high_pass", "cutoff_hz": 20.0, "transition_hz": 15.0 },
    "taps": { "type": "auto" }
  }
}
```
