# Sonir Bench — Module Specification (Canonical)

> Everything needed to write a DSP module (a graph) and the resample design around it, in one file: the prose, both JSON Schemas, the complete error catalog, and the reference examples.

- Module (graph) `spec_version`: `0.1.0`
- Resample design `spec_version` (moves independently): `0.1.0`
- Control API protocol version: `1`
- Generated file (do not edit by hand)

**Only what you need in order to write one module is here.** The app's internal structure, its audio I/O and the history of its development are not included.

🔴 **Every JSON in this document has been fed to the implementation and passed.** The Schema and the error catalog are written out from the implementation, and each reference module is carried through all four stages (shape, validate, simulate, arm) by a test, as is every reference resample design on every selectable entry rate.

⚠️ **The appendices are shown exactly as the implementation emits them, and they are not translated here.** The Schema's `description`s are English (with the Japanese kept alongside in `x-description-ja`), every error carries both languages under `messages`, and the reference-module notes are Japanese. This is deliberate: an error message is something you paste back, so it has to be the string the app actually returns. The `code` of every error is language-neutral and is what the prose refers to.

---
## 1. How to use this specification

A module is **one JSON file**. There are only three things to do.

1. **Write it.** Produce a JSON document with `spec_version` and `nodes`.
2. **Hand it over.** Drop it into the file field under "New" in the app (picking a file works too). If it parses, it lands in the field; **saving runs the shape and coefficient checks** and puts it in the library, and **loading it to listen runs the remaining checks** (see "6. What it takes to pass").
3. **Fix it.** If a check refuses the module, you get back the stage it failed at, the error code, and the numbers that were actually measured. Hand those straight back to whoever wrote it.

- **Failing a check is not an error case.** It is one of the two expected answers, and the measurements come back either way, so you can see what exceeded what and by how much.
- **A file written by a person and a file written by an AI go through the same entrance.** There is no import-only path, and both are refused by the same judgement.
- **No machine has a way to make sound.** Only a person's action starts playback, so when an AI writes a module, **the AI cannot check its own work**. Handing the reasons back to the writer is a person's job.
- **Ranges, units, defaults and examples live in Appendix A (the JSON Schema).** They are deliberately not copied into this prose (the same number in two places always drifts).
- **Appendix B is the complete error catalog.** A code that is not there does not exist in the implementation.
- The file name is the identifier. **Handing over the same name again overwrites, which doubles as renaming.**

## 2. The shape of a graph

```jsonc
{
  "spec_version": "…",   // Required. The value is the const in Appendix A (also printed at the top of this document)
  "nodes": [             // Upstream to downstream. An empty array is a bypass
    { "type": "biquad", "shape": "bell", "cutoff_hz": 120.0, "q": 1.0, "gain_db": 3.0 }
  ]
}
```

- Nodes form **a single series chain**, traversed upstream to downstream.
- There is also **one aux bus**. Some nodes read the main signal and write to aux; others read aux and act on the main signal. That is how you build a side chain (see "4. The aux bus").
- **You cannot write a graph that splits and rejoins.** Multiband shapes cannot be expressed; what you can write is one chain plus one aux bus.
- The maximum number of nodes is in Appendix A.
- **Order matters.** Put `dc_blocker` after whatever creates DC, put `envelope_follower` before `signal_gain`; those relationships are exactly what takes effect.

### Nothing depends on the rate

- Every node is written in Hz and dB. The internal processing rate is **705,600 Hz** (for 44.1 kHz-family material) or **768,000 Hz** (for the 48 kHz family), and **the same graph loads unchanged on both**.
- **Passing on both rates is a condition.** The material's rate family can change mid-playback while the same graph stays loaded, so a design that only holds on one of them cannot be built.
- **Band limiting at the top end (the low-pass that stops aliasing) is not the graph's job.** The resample stage always carries it. What you write in a graph is the colouring, and the band limit cannot be removed.

## 3. The nodes

`type` is one of these fourteen. Only **what the stage does, which bus it connects to, whether it is non-linear, and what it needs alongside** is listed here. Parameter names, ranges, units, defaults and examples are in Appendix A.

| `type` | What the stage does | Bus | Non-linear | Needs alongside |
|---|---|---|---|---|
| `gain` | Moves the level up or down. The timbre does not change | main | no | |
| `biquad` | EQ / filter. Lifts, cuts or removes a band | main | no | |
| `dc_blocker` | Removes DC. Does nothing in the audible band | main | no | |
| `waveshaper` | Saturation. Rounds peaks and adds harmonics | main | **yes** | The asymmetric shape creates DC, so a `dc_blocker` **after** it |
| `harmonic_shaper` | Adds 2nd to 5th harmonics by per-order amount | main | **yes** | A `dc_blocker` **after** it if any even order is used |
| `hysteresis` | Magnetic saturation. Adds a history-dragging thickness | main | **yes** | |
| `envelope_follower` | Detects the main signal's level and writes it to aux | reads main, writes aux | no (the main signal passes through) | The consumer (`signal_gain`) after it |
| `one_pole_smoother` | Smooths the control signal travelling on aux | aux only | no (never touches the main signal) | |
| `signal_gain` | Pulls the main level down according to aux (power sag) | reads aux, acts on main | **yes** (time varying) | An `envelope_follower` **before** it |
| `elliptical_eq` | Cuts low-frequency Side content to centre the low end. Also carries a high Side shelf and a stereo width control | main | no | |
| `allpass_decorrelator` | Creates left/right phase difference without changing the magnitude response | main | no | |
| `crosstalk_canceller` | Cancels each speaker's leakage into the opposite ear | main | no | |
| `directional_eq` | Applies an elevation-cue peak and a depth dip to the Side content only | main | no | |
| `early_reflector` | Builds early reflections from the Side content to add room volume | main | no | |

### What changes as soon as one non-linear node is in the chain

| | A linear-only graph | A graph containing non-linear nodes |
|---|---|---|
| The stimulus check (`simulate`, see "6. What it takes to pass") | does not run | **always runs**. Without its verdict the graph cannot play |
| The final limiter | not inserted | **always inserted, and never switched off for the session** |
| Soft start at the beginning | none | **always applied** |
| A/B level matching | computed stage by stage | **non-linear stages count as 0 dB** (see "5. Level conventions") |

Checks and protections are never imposed on a linear-only graph. Doing so would stand things on their head: **the path closest to a straight wire would be the one whose signal is touched the most.**

## 4. The aux bus

`envelope_follower` → `one_pole_smoother` → `signal_gain` is the canonical use: a side chain that pushes the main signal down as the level rises.

- **There is only one aux bus.** Place two `envelope_follower` nodes and the later one overwrites the earlier one.
- **If nothing has been written to aux, `signal_gain` sits at unity.** Forgetting to wire it up therefore does not produce silence; it produces **nothing at all**. Since both the shape check and the stimulus check still pass, this is the first thing to look at when the sound is not what you intended.
- Level detection is **linked across channels**: the maximum across channels is taken and the same value is used (making them independent would move the image with the signal level).
- **The release time is built with `one_pole_smoother`.** `signal_gain` itself has no time constant.

## 5. Level conventions

These are fixed by the format version and do not change later.

| # | Convention |
|---|---|
| 1 | Signals are floating point with `1.0 = 0 dBFS`. **There is no implicit gain anywhere** |
| 2 | The analogue reference point is **-20 dBFS RMS ≡ 1.0 V RMS** (at the input node of the circuit being modelled). Drive amounts and harmonic percentages are relative to this reference |
| 3 | Every module has **unity gain at the reference level** (within ±0.1 dB). The checks enforce it |
| 4 | A non-linear node's `drive_db` is **compensated automatically on the output side**. Changing the drive changes the character, not the level |
| 5 | The interior is floating point, so it does not clip. **The limiter is only in the final stage** |
| 6 | An A/B switch **matches the level automatically** (the reference is bypass = 0 dB) |

Conventions 4 and 6 are what keep A/B from becoming a "louder sounds better" instrument. **If raising the drive raises the level, that is a defect** (convention 4 is pinned by a test that holds within ±0.1 dB on a reference-level sine).

⚠️ **Only the basic linear stages take part in the level-matching computation** (`gain` / `biquad` / `dc_blocker`). **Non-linear stages and the spatial / imaging stages count as 0 dB.** The level of a non-linear stage depends on the input level, so it cannot be expressed as a frequency response; the assumption rests on convention 4 guaranteeing unity at the reference level. The flip side: **if a spatial or imaging stage moves the level, that change is not compensated.**

⚠️ The compensation in convention 4 is aligned on a **reference-level sine**. On real music with a high crest factor it will not line up exactly.

## 6. What it takes to pass

There are four stages. **A stage does not run unless the previous one passed** (the name of the stage that refused you always comes back, so where it stopped is never in doubt).

| Stage | When it runs | What it looks at | Codes |
|---|---|---|---|
| shape | when the file is parsed | Field names, types, ranges, required fields. An unknown field comes back with **a list of candidates** | `graph.malformed` |
| `validate` | on save (and on load) | Designs the coefficients from each node's values and checks them **on both internal rates**: out of range, unstable, peak gain too high, ultrasonic content being amplified | `biquad.*` / `onepole.*` |
| `simulate` | when the graph is loaded to listen. **Only when non-linear nodes are present** | Runs actual stimuli through the chain and measures NaN/Inf, peak, DC and ultrasonic content | `simulate.*` |
| `arm` | right before the sound starts | Whether the `simulate` verdict, the final limiter and the soft start are in place for a non-linear chain | `arm.*` |

**Only what passed the shape check and `validate` enters the library.** Those two run at save time so that no entry can appear in the list and then always fail when picked. ⭐ **The stimulus check runs when the graph is loaded, and a failure means it is not loaded** (stopped beforehand rather than after it is already making sound).

**Passing the shape check does not mean it will play.** Shape is only shape; the authority starts at `validate`.

### What the stimulus check looks at

Three stimuli are used. **None of them contains ultrasonic content**, so anything above the audible band at the output was created by the nodes.

| Stimulus | What it reveals |
|---|---|
| Reference-level sine (integer periods) | Pass-through gain (conventions 3 and 4 at work) and **DC** |
| Logarithmic sweep (audible band) | How much the harmonics put into the ultrasonic band, plus peak |
| Band-limited noise | Intermodulation and NaN |

- **DC is judged on the sine alone.** On the other two the estimation error is larger than the thing being measured, which would amount to claiming a node creates DC when it does not; those numbers come back as data but are not used for the verdict.
- **A graph containing convolution is warmed up before being measured.** The longer it is, the longer this takes, so loading a graph that contains non-linear nodes makes you wait accordingly.
- Read the results in order. Knowing "there is too much ultrasonic content" does not help while NaN is present.

### About ultrasonic content

The interior runs at 705,600 Hz / 768,000 Hz, so **full-scale ultrasonic output is inaudible to a human.** It goes straight out of the DAC into the amplifier and the tweeter. An audible fault can be noticed by ear; this one can destroy equipment without ever being noticed. That is why the default is to refuse it.

⭐ **What is being stopped is not "passing ultrasonic content" but "amplifying it".** The criterion is absolute rather than relative to the passband, so a properly windowed high-pass filter passes (its passband is normalised to 0 dB). Were it relative, a room correction that lifts the audible band would pass while lifting the ultrasonic band along with it.

### About DC

**Asymmetric shaping and even-order harmonics create DC.** It is a component that pushes the speaker one way continuously, so `simulate` refuses it as it stands.

🔴 **Put `dc_blocker` after whatever creates the DC.** Placing it before leaves the graph failing (DC created by a later stage cannot be removed by a blocker that sits ahead of it).

## 7. The resample design

The stage that takes the incoming rate up to the internal rate and brings it back down at the exit. **It is a separate file from a graph, and its version moves separately too.**

```jsonc
{
  "spec_version": "…",        // Required. The value is the const in Appendix A. Not the graph's
  "kind": "resample-design",  // Required, so it cannot be mistaken for a graph
  "name": "…",                // Display name (free text). ⚠️ The identifier is the file name
  "design": {                 // The design itself. Every field may be omitted (shipped defaults)
    "pass_hz": { "of_nyquist": 0.9 }
  },
  "targets": [],              // Optional. Entry rates it is meant for. Empty means any rate
  "min_partition": null       // Optional. Smallest block length it is meant to run at
}
```

Drop it into `resample/` in the working folder and it appears in the list. **The file name is the identifier**, exactly as with graphs; `name` is the display name, and `kind` is what keeps the file from being mistaken for a graph.

### What this stage is for

**Band limiting can only be written here.** What a graph writes is colouration; the top-end low pass that stops aliasing is always carried by this stage (see "2. The shape of a graph"). A low pass or high pass at an arbitrary position is written here too.

| Field | What it decides |
|---|---|
| `pass_hz` | Top of the passband: flat up to here |
| `stop_hz` | Bottom of the stopband. **Omitted, it is derived from the rate** (entry rate minus passband, which is where the first image starts) |
| `shape` | Extra shaping below the top edge (remove the subsonic band, remove a particular band). Omitted, it is a plain low pass |
| `stopband_db` | The stopband attenuation **asked of** the window |
| `window` | Window function |
| `phase` | Linear phase or minimum phase |
| `taps` | Whether the tap count is solved from the transition width or given directly |

### 🔴 The design carries no rate

There is no entry rate and no coefficient inside `design`. **The same design is solved again for every stage and every entry rate** (the bottom of the stopband is decided by that rate). So one design, written once, loads on a source at any rate.

- **The top edge can be written as a ratio.** As well as an absolute value it can be written as a ratio of the entry Nyquist, so the intent "follow the top of the source" does not split into one file per rate.
- **Absolute frequencies and the tap count only take effect on the entry stage.** The exit stage solves its own transition width; carrying them over band limits twice and inflates the tap count by orders of magnitude.
- That is why `targets` and `min_partition` sit **next to the design rather than inside it**. Neither changes a single coefficient: `targets` declares which rates it is meant for, `min_partition` only decides which row of the cost forecast is shown as representative.

### What it takes to pass

As with a graph, **it can be tried without making a sound** (failing is not an error, and the measured numbers come back). The reasons for refusing are in Appendix B, in full, under `resample.*`.

- **No transition band, no design** (`resample.no_transition_band`) -- when the top edge is too wide for that rate.
- **The stopband attenuation is measured before the design is accepted** (`resample.image_leak`). `stopband_db` is a request, not a result, so a window with fixed attenuation may not reach it. Aliased content lands at frequencies unrelated to the original, so it comes out as **a sound nobody asked for**.
- **Only moving the bottom of the stopband above the derived value is refused** (`resample.stopband_too_high`). Moving it down only improves image rejection.
- **Linear phase needs an odd tap count** (`resample.taps_not_odd`), so that the group delay is an integer number of samples.
- **Shaping has to stay inside the band limit** (`resample.shape_out_of_range`).
- A design whose `targets` is empty -- that is, one that declares "any rate" -- **is solved for both rate families at the time it is saved**, so that the list never offers an entry that is certain to fail when selected.

⚠️ **CPU cost is never a reason to refuse.** The same design would pass or fail with the machine and its temperature, so the forecast is shown but never used to refuse. **Latency is not a reason either** -- the group delay of a linear-phase design is constant across the band and pure latency, so the sound itself does not change; it is always displayed instead.

### Choosing the phase

**This is the only place where the pre-ringing of this stage can be avoided.** A graph's colouration sits after the interpolation, so choosing minimum phase there does nothing to the conversion kernel. Linear phase, in exchange, has a group delay that is constant across the band and no phase distortion. **Which one you want is decided by this field.**

## 8. Versions

- `spec_version` is **required**. It is an `X.Y.Z` string, and omitting it fails at the shape stage.
- **A file is readable when X matches and Y.Z is at or below this implementation.**
- **Files from a newer version are not read.** Doing so would mean ignoring fields we do not know and playing the module with a different meaning.

| Digit | Raised when | Older files are |
|---|---|---|
| **X** | The meaning, unit or default of an existing field changes; a field is removed or renamed; a required field is added | **unreadable** (refused by name, citing the version) |
| **Y** | An optional field is added (behaving as today when omitted), or a node type is added | readable |
| **Z** | Only the wording changed (the shape is the same) | readable |

- **A file from an unreadable version is never deleted or overwritten.** It stays in the list with a reason attached, and its name is held too, so something created later cannot silently trample it.
- A file that could be read is lifted to the current shape immediately, and **the next save writes it at the current version.**

## 9. What cannot be written

These are outside the scope on purpose. It is where the boundary was drawn, not a missing capability.

- **A graph that splits and rejoins** (one chain plus one aux bus is the whole of what can be written).
- **Bringing in coefficients made elsewhere.** There is no way to convolve a measured room response or a filter another tool produced. The top-end band limit, a low-pass at an arbitrary position and a high-pass are written in the resample design rather than in a graph (see "7. The resample design").
- **Only three values can be moved continuously while the sound is playing** (the gain of `gain`, and the drive of `waveshaper` and `hysteresis`). Everything else is changed by loading the graph again, and **loading again is gapless**, so unless you want a knob to sweep, that is enough.
- **Your own code running in real time.** Timbre is written by combining the provided nodes.
- ⚠️ **Loading again returns any moved value to what the file says.** The file is the source of truth; a knob is a temporary state on top of it.

### What this path cannot reproduce

This is DSP ahead of the DAC, so **the interaction between the power amplifier and the speaker beyond it** (back-EMF, the real load response) cannot be reproduced. An approximation from an assumed impedance curve is the ceiling.

---

## Appendix A. JSON Schema (generated from the implementation)

**This is the only authority on ranges, units and defaults.** The numbers are assembled from the implementation's own constants and are deliberately absent from the prose (copying them would make two sources).

#### `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"
}
```

---

## Appendix B. Complete error catalog (generated from the implementation)

The layers are `arm` / `graph` / `node` / `resample` / `rpc` / `sdm` / `simulate`. `rpc` is the request envelope (numeric codes); the rest are the stages that can refuse a module. It is **not a list written separately by hand** (even the layer names are read out of that file).

🔴 **A code that is not here does not exist in the implementation.** Hand out a separately written list and an AI will try to fix codes that were never emitted.

#### `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": "テスト信号のシミュレーションを実行できませんでした（ブロック長またはサンプリングレートの不整合）"
      }
    }
  ]
}
```

---

## Appendix C. Reference modules

**Paste them as they are and they pass.** A test carries all four stages on both rate families.

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

そのまま渡せるグラフ定義。**白紙から書くより、近い形を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 }
  ]
}
```

---

## Appendix D. Reference resample designs

**Save them as they are and they work.** A test solves the stage for every entry rate that can be selected.

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

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

| ファイル | 何を見せているか |
|---|---|
| `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" }
  }
}
```
