@openmixer/omx-contract 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +41 -4
  2. package/bin/omx-contract.mjs +14 -3
  3. package/data/kernels/balance.json +18 -14
  4. package/data/kernels/band_dyn.json +132 -0
  5. package/data/kernels/chorus.json +87 -56
  6. package/data/kernels/comp.json +67 -29
  7. package/data/kernels/deesser.json +114 -86
  8. package/data/kernels/delay.json +67 -57
  9. package/data/kernels/drive.json +118 -87
  10. package/data/kernels/eq.json +284 -113
  11. package/data/kernels/fbs.json +150 -0
  12. package/data/kernels/flanger.json +74 -44
  13. package/data/kernels/gate.json +76 -21
  14. package/data/kernels/geq.json +32 -27
  15. package/data/kernels/hrp.json +68 -0
  16. package/data/kernels/limiter.json +25 -9
  17. package/data/kernels/mixmatrix.json +11 -0
  18. package/data/kernels/phaser.json +110 -100
  19. package/data/kernels/pitch.json +35 -17
  20. package/data/kernels/reverb.json +176 -142
  21. package/data/kernels/rotary.json +189 -177
  22. package/data/kernels/rta.json +17 -0
  23. package/data/kernels/transient.json +64 -36
  24. package/data/kernels/tremolo.json +59 -48
  25. package/data/primitives.json +51 -0
  26. package/data/rates.json +29 -0
  27. package/dist/ts/omx-contract.d.ts +527 -21
  28. package/dist/ts/omx-contract.js +832 -57
  29. package/include/omxcontract/omx_contract_limits.h +233 -0
  30. package/lib/data.mjs +115 -3
  31. package/lib/eq-defaults.mjs +87 -0
  32. package/lib/fmt.mjs +24 -4
  33. package/lib/semver.mjs +12 -2
  34. package/lib/validate.mjs +99 -3
  35. package/package.json +1 -1
  36. package/render/c.mjs +76 -10
  37. package/render/doors.mjs +6 -3
  38. package/render/ts.mjs +40 -2
  39. package/schema/omx-contract.schema.json +39 -3
  40. package/share/omx-contract/omx-contract.json +759 -71
  41. package/render/c/omx_plugin_param.h +0 -25
  42. package/render/c/params-generate.mjs +0 -83
package/README.md CHANGED
@@ -7,12 +7,18 @@ list), as JSON validated by a JSON Schema, rendered by one CLI into
7
7
  every language that reads it. Governed by openmixer's
8
8
  `docs/design/specs/2026-10-05-omx-contract.md`; 1.0.0 carries the phase-1 items of its §4.1, what
9
9
  omx-dsp needs, 1.1.0 adds the flanger, de-esser, phaser, rotary, tremolo and graphic EQ kernels, and
10
- 1.2.0 the chorus, drive, EQ, reverb and pan travels omx-dsp and omx-plugins still kept locally.
10
+ 1.2.0 the chorus, drive, EQ, reverb and pan travels omx-dsp and omx-plugins still kept locally, and
11
+ 1.3.0 everything else more than one repository reads: the feedback detector, ring correction and
12
+ RTA constants, the EQ band budget, the choice enums (as the `set` kind), the EQ band counts and the
13
+ one rule that gives a fresh EQ its bands, the measurement rate sets and the bus input cap. What is
14
+ declared here is declared nowhere else: the EQ band budget that 1.2.0 left in openmixer is here too.
15
+ 2.0.0 changes how a kernel file is written, not what it declares: every render is the same.
11
16
 
12
17
  ```
13
18
  data/ the declaration, one file per group, edited by hand
14
19
  rates.json primitives.json kernels/<kernel>.json
15
- schema/omx-contract.schema.json JSON Schema 2020-12: five kinds, closed units, refs, closed derivations
20
+ schema/omx-contract.schema.json JSON Schema 2020-12: five kinds (scalar, travels, sheet, list, set), closed units, refs, closed derivations
21
+ lib/eq-defaults.mjs the one default rule for a fresh EQ's bands; the ts and c renders carry it
16
22
  bin/omx-contract.mjs the CLI
17
23
  render/ c.mjs (openmixer's renderer, moved unchanged, under c/), ts.mjs, json.mjs
18
24
  include/omxcontract/ the committed c render (what omx-contract-devel installs)
@@ -25,6 +31,37 @@ tools/seed/ the one-time seed from openmixer's built core (prov
25
31
  packaging/ debian/ omx-contract-devel (RPM), libomx-contract-dev (deb)
26
32
  ```
27
33
 
34
+ ## The kernel file
35
+
36
+ A kernel file, `data/kernels/<kernel>.json`, holds four sections and nothing else. A sketch, values abridged
37
+ (the tremolo's controls, a gate table, a constant):
38
+
39
+ ```json
40
+ {
41
+ "$schema": "../../schema/omx-contract.schema.json",
42
+ "controls": [
43
+ { "name": "rateHz", "kind": "travel", "global": "TREMOLO_RATE_RANGE", "doc": "…", "travel": { "min": 0.1, "max": 20, "unit": "Hz", "default": 5 } },
44
+ { "name": "depth", "kind": "travel", "doc": "…", "travel": { "min": 0, "max": 1, "unit": "", "default": 0.5 } },
45
+ { "name": "mode", "kind": "choice", "doc": "…", "ids": ["tremolo", "pan"], "labels": ["Tremolo", "Pan"] },
46
+ { "name": "thresholdDb", "kind": "travel", "table": "GATE_LIMITS", "travel": { "min": -80, "max": 0, "unit": "dB" } }
47
+ ],
48
+ "tables": { "GATE_LIMITS": { "doc": "…" } },
49
+ "aggregates": { "TREMOLO_TRAVELS": { "doc": "…", "fields": ["rateHz", "depth"] } },
50
+ "constants": { "CHORUS_BASE_MS": { "kind": "scalar", "doc": "…", "unit": "ms", "value": 10 } }
51
+ }
52
+ ```
53
+
54
+ - `controls`, in order: a `travel` (one travel) or a `choice` (ordered `ids`, an optional `default` and `labels`).
55
+ Its item name is `<KERNEL>_<NAME>_RANGE` for a travel and `<KERNEL>_<NAME>S` for a choice (`depth` in
56
+ tremolo.json is `TREMOLO_DEPTH_RANGE`, `mode` is `TREMOLO_MODES`); `global` names it when it is anything else.
57
+ A control with a `table` is one field of that table.
58
+ - `tables`: a travels table whose fields are the controls that name it, in control order (`GATE_LIMITS`).
59
+ - `aggregates`: a travels table whose fields are references to controls declared once above, listed by name.
60
+ - `constants`: scalars, lists and sheets, as items.
61
+
62
+ `omx-contract validate` refuses any other key, a constant that is a travel or a set, and a kernel file written
63
+ as a flat item map (the 1.x shape). `rates.json` and `primitives.json` stay item maps.
64
+
28
65
  ## The CLI
29
66
 
30
67
  ```
@@ -41,9 +78,9 @@ share/omx-contract` and `render --target ts --out test/golden/ts`; CI holds all
41
78
 
42
79
  ## Consuming it
43
80
 
44
- Pin one version (`omx-contract 1.2.1` in `.github/pins.txt`) and build against its renders, committing
81
+ Pin one version (`omx-contract 2.0.0` in `.github/pins.txt`) and build against its renders, committing
45
82
  none: the installed `omx-contract-devel` / `libomx-contract-dev` at exactly that version
46
- (`pkg-config --exact-version=1.2.1 omx-contract`), else the release's npm tarball, unpacked, and
83
+ (`pkg-config --exact-version=2.0.0 omx-contract`), else the release's npm tarball, unpacked, and
47
84
  `omx-contract render --target c --out build/omx-contract/include`. Include
48
85
  `<omxcontract/omx_contract_limits.h>`. A plugin's parameter header is rendered by omx-plugins from its
49
86
  declaration, by reference to the kernel travels here (since 1.1.0).
@@ -15,7 +15,7 @@ import { execFileSync } from 'node:child_process';
15
15
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
16
16
  import { dirname, join, relative, resolve } from 'node:path';
17
17
  import { fileURLToPath } from 'node:url';
18
- import { loadData, packageVersion, resolveData } from '../lib/data.mjs';
18
+ import { KERNEL_SECTIONS, isKernelFile, kernelDocOf, loadData, packageVersion, resolveData } from '../lib/data.mjs';
19
19
  import { formatDoc } from '../lib/fmt.mjs';
20
20
  import { classify, refusal } from '../lib/semver.mjs';
21
21
  import { validateData } from '../lib/validate.mjs';
@@ -122,12 +122,23 @@ function cmdRender(argv) {
122
122
  return 0;
123
123
  }
124
124
 
125
- /** The data of a git tag, as `{ rel: text }`. */
125
+ /**
126
+ * The data of a git tag, as `{ rel: text }`. A tag released before 2.0.0 wrote its kernel files as flat item maps;
127
+ * each is restated in the kernel-file shape (kernelDocOf, the inverse of the expansion the loader reads), so the
128
+ * comparison is between the items the two releases declare.
129
+ */
126
130
  function dataAtTag(tag) {
127
131
  const git = (...a) => execFileSync('git', ['-C', ROOT, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
128
132
  try { git('rev-parse', '--verify', `${tag}^{commit}`); } catch { throw new Usage(`no tag ${tag} in ${ROOT}`); }
129
133
  const texts = {};
130
- for (const rel of git('ls-tree', '-r', '--name-only', tag, '--', 'data').split('\n').filter((p) => p.endsWith('.json'))) texts[rel] = git('show', `${tag}:${rel}`);
134
+ for (const rel of git('ls-tree', '-r', '--name-only', tag, '--', 'data').split('\n').filter((p) => p.endsWith('.json'))) {
135
+ const text = git('show', `${tag}:${rel}`);
136
+ let doc;
137
+ try { doc = JSON.parse(text); } catch { texts[rel] = text; continue; }
138
+ if (!isKernelFile(rel) || KERNEL_SECTIONS.some((k) => k in doc)) { texts[rel] = text; continue; }
139
+ const { $schema, ...items } = doc;
140
+ texts[rel] = JSON.stringify({ $schema, ...kernelDocOf(rel, Object.entries(items)) });
141
+ }
131
142
  return texts;
132
143
  }
133
144
 
@@ -1,19 +1,23 @@
1
1
  {
2
2
  "$schema": "../../schema/omx-contract.schema.json",
3
- "PAN_RANGE": {
4
- "kind": "travels",
5
- "doc": "The PAN travel — −1 (hard left) … 0 (center) … +1 (hard right). A property of the pan law itself, not of any desk, so it is declared once. Continuous: a pan position has no quantum, and panGains clamps to these bounds.",
6
- "travel": {
7
- "min": -1,
8
- "max": 1,
9
- "step": 0,
10
- "unit": ""
11
- },
12
- "c": {
13
- "alias": {
14
- "fact": "pan",
15
- "field": "pan"
3
+ "controls": [
4
+ {
5
+ "name": "pan",
6
+ "kind": "travel",
7
+ "global": "PAN_RANGE",
8
+ "doc": "The PAN travel — −1 (hard left) … 0 (center) … +1 (hard right). A property of the pan law itself, not of any desk, so it is declared once. Continuous: a pan position has no quantum, and panGains clamps to these bounds.",
9
+ "travel": {
10
+ "min": -1,
11
+ "max": 1,
12
+ "step": 0,
13
+ "unit": ""
14
+ },
15
+ "c": {
16
+ "alias": {
17
+ "fact": "pan",
18
+ "field": "pan"
19
+ }
16
20
  }
17
21
  }
18
- }
22
+ ]
19
23
  }
@@ -0,0 +1,132 @@
1
+ {
2
+ "$schema": "../../schema/omx-contract.schema.json",
3
+ "controls": [
4
+ {
5
+ "name": "mode",
6
+ "kind": "choice",
7
+ "doc": "The side of the threshold the band acts on. Above acts when the band is loud, taming a resonance that only rings when pushed; below acts when it is quiet, lifting a band that drops out. The order is the integer the kernel reads: above is 0, below is 1.",
8
+ "ids": [
9
+ "above",
10
+ "below"
11
+ ],
12
+ "default": "above"
13
+ },
14
+ {
15
+ "name": "threshold",
16
+ "kind": "travel",
17
+ "doc": "Where the band starts to act, in dB of the band's own level and not the strip's. It is the same fact the de-esser's threshold is, so it carries the same travel.",
18
+ "travel": {
19
+ "min": {
20
+ "ref": "DEESS_THRESHOLD_RANGE.min"
21
+ },
22
+ "max": {
23
+ "ref": "DEESS_THRESHOLD_RANGE.max"
24
+ },
25
+ "step": {
26
+ "ref": "DEESS_THRESHOLD_RANGE.step"
27
+ },
28
+ "unit": "dB",
29
+ "default": {
30
+ "ref": "DEESS_THRESHOLD_RANGE.default"
31
+ },
32
+ "defaultFrom": "desk"
33
+ }
34
+ },
35
+ {
36
+ "name": "ratio",
37
+ "kind": "travel",
38
+ "doc": "How hard the band is held once it is past its threshold. 2:1 rather than the de-esser's 4:1, because a dynamic band is corrective, not a limiter for a sibilant.",
39
+ "travel": {
40
+ "min": {
41
+ "ref": "DEESS_RATIO_RANGE.min"
42
+ },
43
+ "max": {
44
+ "ref": "DEESS_RATIO_RANGE.max"
45
+ },
46
+ "step": {
47
+ "ref": "DEESS_RATIO_RANGE.step"
48
+ },
49
+ "unit": "",
50
+ "default": 2,
51
+ "defaultFrom": "desk"
52
+ }
53
+ },
54
+ {
55
+ "name": "range",
56
+ "kind": "travel",
57
+ "doc": "The most the band may add to its static gain, signed: below zero the deepest cut, above zero the largest boost. The ceiling of +12 keeps static plus dynamic inside the EQ's own travel. Zero makes the band inert without switching it off.",
58
+ "travel": {
59
+ "min": {
60
+ "ref": "DEESS_RANGE_RANGE.min"
61
+ },
62
+ "max": 12,
63
+ "step": {
64
+ "ref": "DEESS_RANGE_RANGE.step"
65
+ },
66
+ "unit": "dB",
67
+ "default": -6,
68
+ "defaultFrom": "desk"
69
+ }
70
+ },
71
+ {
72
+ "name": "attack",
73
+ "kind": "travel",
74
+ "doc": "The detector's attack, in ms. The travel stops at the oversampling floor, below which the band would need the compressor's delayed control path.",
75
+ "travel": {
76
+ "min": {
77
+ "ref": "DETECTOR_OVERSAMPLE_AUTO_MS"
78
+ },
79
+ "max": {
80
+ "ref": "COMP_LIMITS.attackMs.max"
81
+ },
82
+ "step": {
83
+ "ref": "DEESS_ATTACK_RANGE.step"
84
+ },
85
+ "unit": "ms",
86
+ "default": 5,
87
+ "defaultFrom": "desk"
88
+ }
89
+ },
90
+ {
91
+ "name": "release",
92
+ "kind": "travel",
93
+ "doc": "The detector's release, in ms. 100 ms is long enough not to modulate a 40 Hz band inside four cycles.",
94
+ "travel": {
95
+ "min": {
96
+ "ref": "DEESS_RELEASE_RANGE.min"
97
+ },
98
+ "max": {
99
+ "ref": "COMP_LIMITS.releaseMs.max"
100
+ },
101
+ "step": {
102
+ "ref": "DEESS_RELEASE_RANGE.step"
103
+ },
104
+ "unit": "ms",
105
+ "default": 100,
106
+ "defaultFrom": "desk"
107
+ }
108
+ }
109
+ ],
110
+ "constants": {
111
+ "BAND_DYN_KNEE_DB": {
112
+ "kind": "scalar",
113
+ "doc": "The soft knee of the band's gain computer, in dB. One knee for the one band-dynamics word, shared by every stage that runs it, the de-esser included: a second spelling of 6 would be a second place where the reduction starts.",
114
+ "unit": "dB",
115
+ "value": 6
116
+ },
117
+ "BAND_DYN_MAX_BANDS": {
118
+ "kind": "scalar",
119
+ "doc": "How many dynamic bands a channel may hold. A dynamic band is an operator band, so the capacity is the operator's reserve, read and not typed again.",
120
+ "unit": "",
121
+ "value": {
122
+ "ref": "OPERATOR_EQ_BANDS_RESERVE"
123
+ }
124
+ },
125
+ "DETECTOR_OVERSAMPLE_AUTO_MS": {
126
+ "kind": "scalar",
127
+ "doc": "The shortest attack, in ms, at which the compressor's detector does not alias. Below it the compressor engages its 4x control path, which adds 72 samples of latency; a zero-latency band has no such path, so its attack travel stops at this floor.",
128
+ "unit": "ms",
129
+ "value": 0.5
130
+ }
131
+ }
132
+ }
@@ -1,63 +1,94 @@
1
1
  {
2
2
  "$schema": "../../schema/omx-contract.schema.json",
3
- "CHORUS_SPREAD_RANGE": {
4
- "kind": "travels",
5
- "doc": "The chorus's right-leg phase offset, in TURNS of its one oscillator (§2, §3a; the native-FX catalogue's M32 Dimension row). The right leg's voice k reads the LFO at k/N + spread: 0 is both legs one phase (the stage without the field, bit for bit), 0.25 quadrature, 0.5 the Dimension's opposed sweep. A fraction of a cycle carries no engineering symbol, so the unit is ''.",
6
- "travel": {
7
- "min": 0,
8
- "max": 0.5,
9
- "step": 0.01,
10
- "unit": "",
11
- "default": 0,
12
- "defaultFrom": "desk"
3
+ "controls": [
4
+ {
5
+ "name": "spread",
6
+ "kind": "travel",
7
+ "doc": "The chorus's right-leg phase offset, in TURNS of its one oscillator (§2, §3a; the native-FX catalogue's M32 Dimension row). The right leg's voice k reads the LFO at k/N + spread: 0 is both legs one phase (the stage without the field, bit for bit), 0.25 quadrature, 0.5 the Dimension's opposed sweep. A fraction of a cycle carries no engineering symbol, so the unit is ''.",
8
+ "travel": {
9
+ "min": 0,
10
+ "max": 0.5,
11
+ "step": 0.01,
12
+ "unit": "",
13
+ "default": 0,
14
+ "defaultFrom": "desk"
15
+ }
16
+ },
17
+ {
18
+ "name": "rate",
19
+ "kind": "travel",
20
+ "doc": "The chorus sweep's speed (§3a). Above ~3 Hz it stops being a chorus and becomes a vibrato.",
21
+ "travel": {
22
+ "min": 0.05,
23
+ "max": 8,
24
+ "step": 0.01,
25
+ "unit": "Hz",
26
+ "default": 0.6,
27
+ "defaultFrom": "desk"
28
+ }
29
+ },
30
+ {
31
+ "name": "depth",
32
+ "kind": "travel",
33
+ "doc": "The chorus's peak-to-peak excursion above its declared 10 ms base (§3a).",
34
+ "travel": {
35
+ "min": 0,
36
+ "max": 12,
37
+ "step": 0.1,
38
+ "unit": "ms",
39
+ "default": 4,
40
+ "defaultFrom": "desk"
41
+ }
42
+ },
43
+ {
44
+ "name": "voices",
45
+ "kind": "travel",
46
+ "doc": "The chorus's tap count — one oscillator read at k/N (§3a).",
47
+ "travel": {
48
+ "min": 1,
49
+ "max": 4,
50
+ "step": 1,
51
+ "unit": "",
52
+ "default": 3,
53
+ "defaultFrom": "desk"
54
+ }
55
+ },
56
+ {
57
+ "name": "mix",
58
+ "kind": "travel",
59
+ "doc": "The chorus's wet/dry, as an instance of DRIVE_MIX_RANGE — the same percent travel, its own come-up value: 35 %, a colour under the dry on a strip (§3a). The REVERB_LOWCUT_RANGE-over- REVERB_CUT_RANGE shape: one travel, one declared default per field that reads it.",
60
+ "travel": {
61
+ "min": 0,
62
+ "max": 100,
63
+ "step": 0.1,
64
+ "unit": "%",
65
+ "default": 35,
66
+ "defaultFrom": "desk"
67
+ }
13
68
  }
14
- },
15
- "CHORUS_RATE_RANGE": {
16
- "kind": "travels",
17
- "doc": "The chorus sweep's speed (§3a). Above ~3 Hz it stops being a chorus and becomes a vibrato.",
18
- "travel": {
19
- "min": 0.05,
20
- "max": 8,
21
- "step": 0.01,
22
- "unit": "Hz",
23
- "default": 0.6,
24
- "defaultFrom": "desk"
25
- }
26
- },
27
- "CHORUS_DEPTH_RANGE": {
28
- "kind": "travels",
29
- "doc": "The chorus's peak-to-peak excursion above its declared 10 ms base (§3a).",
30
- "travel": {
31
- "min": 0,
32
- "max": 12,
33
- "step": 0.1,
69
+ ],
70
+ "constants": {
71
+ "CHORUS_BASE_MS": {
72
+ "kind": "scalar",
73
+ "doc": "The chorus's base delay, ms: the voices swing around it by up to the depth.",
34
74
  "unit": "ms",
35
- "default": 4,
36
- "defaultFrom": "desk"
37
- }
38
- },
39
- "CHORUS_VOICES_RANGE": {
40
- "kind": "travels",
41
- "doc": "The chorus's tap count — one oscillator read at k/N (§3a).",
42
- "travel": {
43
- "min": 1,
44
- "max": 4,
45
- "step": 1,
46
- "unit": "",
47
- "default": 3,
48
- "defaultFrom": "desk"
49
- }
50
- },
51
- "CHORUS_MIX_RANGE": {
52
- "kind": "travels",
53
- "doc": "The chorus's wet/dry, as an instance of DRIVE_MIX_RANGE — the same percent travel, its own come-up value: 35 %, a colour under the dry on a strip (§3a). The REVERB_LOWCUT_RANGE-over- REVERB_CUT_RANGE shape: one travel, one declared default per field that reads it.",
54
- "travel": {
55
- "min": 0,
56
- "max": 100,
57
- "step": 0.1,
58
- "unit": "%",
59
- "default": 35,
60
- "defaultFrom": "desk"
75
+ "value": 10
76
+ },
77
+ "CHORUS_MAX_MS": {
78
+ "kind": "scalar",
79
+ "doc": "The longest delay a chorus voice reaches, rounded up to whole ms: the base delay plus the depth maximum. The delay line is sized from it.",
80
+ "unit": "ms",
81
+ "value": {
82
+ "derive": "ceilSum",
83
+ "of": [
84
+ {
85
+ "ref": "CHORUS_BASE_MS"
86
+ },
87
+ {
88
+ "ref": "CHORUS_DEPTH_RANGE.max"
89
+ }
90
+ ]
91
+ }
61
92
  }
62
93
  }
63
94
  }
@@ -1,58 +1,89 @@
1
1
  {
2
2
  "$schema": "../../schema/omx-contract.schema.json",
3
- "COMP_LIMITS": {
4
- "kind": "travels",
5
- "doc": "The legal range of each compressor field (engineering units). The widget clamps drags to these, `normalizeComp` enforces them, the row's codec refuses by them and the travel sheet publishes them. `default`/`defaultFrom` are the come-up values `defaultCompState` seeds a fresh strip with, read BACK from here rather than restated there.",
6
- "fields": {
7
- "thresholdDb": {
3
+ "controls": [
4
+ {
5
+ "name": "thresholdDb",
6
+ "kind": "travel",
7
+ "table": "COMP_LIMITS",
8
+ "travel": {
8
9
  "min": -60,
9
10
  "max": 0,
10
11
  "step": 1,
11
12
  "unit": "dB",
12
13
  "default": -18,
13
14
  "defaultFrom": "desk"
14
- },
15
- "ratio": {
15
+ }
16
+ },
17
+ {
18
+ "name": "ratio",
19
+ "kind": "travel",
20
+ "table": "COMP_LIMITS",
21
+ "travel": {
16
22
  "min": 1,
17
23
  "max": 20,
18
24
  "step": 0.5,
19
25
  "unit": "",
20
26
  "default": 4,
21
27
  "defaultFrom": "desk"
22
- },
23
- "kneeDb": {
28
+ }
29
+ },
30
+ {
31
+ "name": "kneeDb",
32
+ "kind": "travel",
33
+ "table": "COMP_LIMITS",
34
+ "travel": {
24
35
  "min": 0,
25
36
  "max": 24,
26
37
  "step": 0.5,
27
38
  "unit": "dB",
28
39
  "default": 6,
29
40
  "defaultFrom": "desk"
30
- },
31
- "attackMs": {
41
+ }
42
+ },
43
+ {
44
+ "name": "attackMs",
45
+ "kind": "travel",
46
+ "table": "COMP_LIMITS",
47
+ "travel": {
32
48
  "min": 0.1,
33
49
  "max": 100,
34
50
  "step": 1,
35
51
  "unit": "ms",
36
52
  "default": 5,
37
53
  "defaultFrom": "desk"
38
- },
39
- "releaseMs": {
54
+ }
55
+ },
56
+ {
57
+ "name": "releaseMs",
58
+ "kind": "travel",
59
+ "table": "COMP_LIMITS",
60
+ "travel": {
40
61
  "min": 5,
41
62
  "max": 3000,
42
63
  "step": 10,
43
64
  "unit": "ms",
44
65
  "default": 200,
45
66
  "defaultFrom": "desk"
46
- },
47
- "makeupDb": {
67
+ }
68
+ },
69
+ {
70
+ "name": "makeupDb",
71
+ "kind": "travel",
72
+ "table": "COMP_LIMITS",
73
+ "travel": {
48
74
  "min": 0,
49
75
  "max": 24,
50
76
  "step": 0.5,
51
77
  "unit": "dB",
52
78
  "default": 0,
53
79
  "defaultFrom": "desk"
54
- },
55
- "mixPct": {
80
+ }
81
+ },
82
+ {
83
+ "name": "mixPct",
84
+ "kind": "travel",
85
+ "table": "COMP_LIMITS",
86
+ "travel": {
56
87
  "min": 0,
57
88
  "max": 100,
58
89
  "step": 5,
@@ -61,19 +92,26 @@
61
92
  "defaultFrom": "desk"
62
93
  }
63
94
  }
95
+ ],
96
+ "tables": {
97
+ "COMP_LIMITS": {
98
+ "doc": "The legal range of each compressor field (engineering units). The widget clamps drags to these, `normalizeComp` enforces them, the row's codec refuses by them and the travel sheet publishes them. `default`/`defaultFrom` are the come-up values `defaultCompState` seeds a fresh strip with, read BACK from here rather than restated there."
99
+ }
64
100
  },
65
- "PROGRAM_RELEASE": {
66
- "kind": "sheet",
67
- "doc": "The program-dependent release's model constants — the LA-2A's opto cell as DATA (`docs/design/specs/2026-09-26-compressor-models.md` §3b). `chargeMs`/`dischargeMs` are the word's memory constants, shared by every profile; each profile's `fastMs`, `slowMinMs`, `slowMaxMs` and `share` fit the unit's published two-stage recovery. Not operator controls: generated into `omx_contract_limits.h` as `OMX_PROGRAM_RELEASE_*`, which libomxdsp's `omx_env_program_release` reads.",
68
- "value": {
69
- "chargeMs": 1000,
70
- "dischargeMs": 5000,
71
- "profiles": {
72
- "opto": {
73
- "fastMs": 70,
74
- "slowMinMs": 110,
75
- "slowMaxMs": 1100,
76
- "share": 0.5
101
+ "constants": {
102
+ "PROGRAM_RELEASE": {
103
+ "kind": "sheet",
104
+ "doc": "The program-dependent release's model constants — the LA-2A's opto cell as DATA (`docs/design/specs/2026-09-26-compressor-models.md` §3b). `chargeMs`/`dischargeMs` are the word's memory constants, shared by every profile; each profile's `fastMs`, `slowMinMs`, `slowMaxMs` and `share` fit the unit's published two-stage recovery. Not operator controls: generated into `omx_contract_limits.h` as `OMX_PROGRAM_RELEASE_*`, which libomxdsp's `omx_env_program_release` reads.",
105
+ "value": {
106
+ "chargeMs": 1000,
107
+ "dischargeMs": 5000,
108
+ "profiles": {
109
+ "opto": {
110
+ "fastMs": 70,
111
+ "slowMinMs": 110,
112
+ "slowMaxMs": 1100,
113
+ "share": 0.5
114
+ }
77
115
  }
78
116
  }
79
117
  }