@openmixer/omx-contract 1.3.0 → 2.1.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.
package/README.md CHANGED
@@ -12,6 +12,8 @@ omx-dsp needs, 1.1.0 adds the flanger, de-esser, phaser, rotary, tremolo and gra
12
12
  RTA constants, the EQ band budget, the choice enums (as the `set` kind), the EQ band counts and the
13
13
  one rule that gives a fresh EQ its bands, the measurement rate sets and the bus input cap. What is
14
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.
16
+ 2.1.0 adds each kernel's ordered controls to the JSON render, and a control's `rearms` flag.
15
17
 
16
18
  ```
17
19
  data/ the declaration, one file per group, edited by hand
@@ -30,6 +32,70 @@ tools/seed/ the one-time seed from openmixer's built core (prov
30
32
  packaging/ debian/ omx-contract-devel (RPM), libomx-contract-dev (deb)
31
33
  ```
32
34
 
35
+ ## The kernel file
36
+
37
+ A kernel file, `data/kernels/<kernel>.json`, holds four sections and nothing else. A sketch, values abridged
38
+ (the tremolo's controls, a gate table, a constant):
39
+
40
+ ```json
41
+ {
42
+ "$schema": "../../schema/omx-contract.schema.json",
43
+ "controls": [
44
+ { "name": "rateHz", "kind": "travel", "global": "TREMOLO_RATE_RANGE", "doc": "…", "travel": { "min": 0.1, "max": 20, "unit": "Hz", "default": 5 } },
45
+ { "name": "depth", "kind": "travel", "doc": "…", "travel": { "min": 0, "max": 1, "unit": "", "default": 0.5 } },
46
+ { "name": "mode", "kind": "choice", "doc": "…", "ids": ["tremolo", "pan"], "labels": ["Tremolo", "Pan"] },
47
+ { "name": "thresholdDb", "kind": "travel", "table": "GATE_LIMITS", "travel": { "min": -80, "max": 0, "unit": "dB" } }
48
+ ],
49
+ "tables": { "GATE_LIMITS": { "doc": "…" } },
50
+ "aggregates": { "TREMOLO_TRAVELS": { "doc": "…", "fields": ["rateHz", "depth"] } },
51
+ "constants": { "CHORUS_BASE_MS": { "kind": "scalar", "doc": "…", "unit": "ms", "value": 10 } }
52
+ }
53
+ ```
54
+
55
+ - `controls`, in order: a `travel` (one travel) or a `choice` (ordered `ids`, an optional `default` and `labels`).
56
+ Its item name is `<KERNEL>_<NAME>_RANGE` for a travel and `<KERNEL>_<NAME>S` for a choice (`depth` in
57
+ tremolo.json is `TREMOLO_DEPTH_RANGE`, `mode` is `TREMOLO_MODES`); `global` names it when it is anything else.
58
+ A control with a `table` is one field of that table. `"rearms": true` marks a control whose change re-arms the
59
+ kernel's state, so it is not a smooth parameter (the limiter's `lookaheadMs`: omx-dsp's limiter face re-arms
60
+ the state and restarts the gain at unity when the look-ahead moves by a frame). It is a boolean, only on a
61
+ control; it is no item, so the C and TypeScript renders do not carry it.
62
+ - `tables`: a travels table whose fields are the controls that name it, in control order (`GATE_LIMITS`).
63
+ - `aggregates`: a travels table whose fields are references to controls declared once above, listed by name.
64
+ - `constants`: scalars, lists and sheets, as items.
65
+
66
+ `omx-contract validate` refuses any other key, a constant that is a travel or a set, and a kernel file written
67
+ as a flat item map (the 1.x shape). `rates.json` and `primitives.json` stay item maps.
68
+
69
+ ## The JSON render's kernels
70
+
71
+ Beside `items`, `share/omx-contract/omx-contract.json` carries `kernels`: for every kernel file, its controls in
72
+ the order the file declares them. A consumer reads control order and kinds here, never from a kernel file.
73
+
74
+ ```json
75
+ "kernels": {
76
+ "limiter": {
77
+ "controls": [
78
+ { "name": "ceilingDb", "kind": "travel", "global": "LIMITER_LIMITS", "table": "LIMITER_LIMITS" },
79
+ { "name": "lookaheadMs", "kind": "travel", "global": "LIMITER_LIMITS", "table": "LIMITER_LIMITS", "rearms": true },
80
+ { "name": "releaseMs", "kind": "travel", "global": "LIMITER_LIMITS", "table": "LIMITER_LIMITS" }
81
+ ]
82
+ },
83
+ "tremolo": {
84
+ "controls": [
85
+ { "name": "rateHz", "kind": "travel", "global": "TREMOLO_RATE_RANGE" },
86
+ { "name": "mode", "kind": "choice", "global": "TREMOLO_MODES" }
87
+ ]
88
+ }
89
+ }
90
+ ```
91
+
92
+ - `kind`: `travel` or `choice`.
93
+ - `global`: the item the control's value lives in: `items[global].value` for a travel or a choice, and
94
+ `items[global].value[name]` for a field of a table, when `table` (the same item) is present.
95
+ - `rearms`: present, and `true`, only on a control that declares it.
96
+
97
+ A kernel with no controls (fbs, mixmatrix, rta) is listed with an empty `controls`.
98
+
33
99
  ## The CLI
34
100
 
35
101
  ```
@@ -46,9 +112,9 @@ share/omx-contract` and `render --target ts --out test/golden/ts`; CI holds all
46
112
 
47
113
  ## Consuming it
48
114
 
49
- Pin one version (`omx-contract 1.3.0` in `.github/pins.txt`) and build against its renders, committing
115
+ Pin one version (`omx-contract 2.1.0` in `.github/pins.txt`) and build against its renders, committing
50
116
  none: the installed `omx-contract-devel` / `libomx-contract-dev` at exactly that version
51
- (`pkg-config --exact-version=1.3.0 omx-contract`), else the release's npm tarball, unpacked, and
117
+ (`pkg-config --exact-version=2.1.0 omx-contract`), else the release's npm tarball, unpacked, and
52
118
  `omx-contract render --target c --out build/omx-contract/include`. Include
53
119
  `<omxcontract/omx_contract_limits.h>`. A plugin's parameter header is rendered by omx-plugins from its
54
120
  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,85 +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"
61
- }
62
- },
63
- "CHORUS_BASE_MS": {
64
- "kind": "scalar",
65
- "doc": "The chorus's base delay, ms: the voices swing around it by up to the depth.",
66
- "unit": "ms",
67
- "value": 10
68
- },
69
- "CHORUS_MAX_MS": {
70
- "kind": "scalar",
71
- "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.",
72
- "unit": "ms",
73
- "value": {
74
- "derive": "ceilSum",
75
- "of": [
76
- {
77
- "ref": "CHORUS_BASE_MS"
78
- },
79
- {
80
- "ref": "CHORUS_DEPTH_RANGE.max"
81
- }
82
- ]
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
+ }
83
92
  }
84
93
  }
85
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
  }