@openmixer/omx-contract 2.3.0 → 2.5.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 +39 -119
- package/data/kernels/comp.json +32 -0
- package/data/kernels/deesser.json +15 -0
- package/data/kernels/gate.json +15 -0
- package/data/kernels/limiter.json +15 -0
- package/data/primitives.json +7 -0
- package/dist/ts/omx-contract.d.ts +62 -0
- package/dist/ts/omx-contract.js +73 -0
- package/include/omxcontract/omx_contract_limits.h +48 -0
- package/lib/data.mjs +13 -2
- package/lib/fmt.mjs +14 -3
- package/lib/semver.mjs +28 -0
- package/lib/validate.mjs +47 -5
- package/package.json +1 -1
- package/render/c.mjs +29 -1
- package/render/json.mjs +2 -1
- package/render/ts.mjs +17 -2
- package/schema/omx-contract.schema.json +15 -0
- package/share/omx-contract/omx-contract.json +74 -1
package/README.md
CHANGED
|
@@ -2,141 +2,61 @@
|
|
|
2
2
|
<!-- Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com> -->
|
|
3
3
|
# omx-contract
|
|
4
4
|
|
|
5
|
-
The FreeMixer declarations as data: what a value may be (a travel, a default, a kernel constant, a
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
2.2.0 declares the controls an instance has that were missing: the delay's ping-pong, the drive's auto-gain, stereo
|
|
18
|
-
link and HF roll-off, the compressor's kind and detector oversampling, and an EQ instance's switches, slopes and
|
|
19
|
-
per-band controls (a control's `count`, a use, `of` and `when`, and a default by reference).
|
|
20
|
-
2.3.0 declares the input trim as a kernel, `trim`, whose one control is the console's TRIM_RANGE.
|
|
5
|
+
The FreeMixer declarations as data: what a value may be (a travel, a default, a kernel constant, a list),
|
|
6
|
+
written once as JSON, checked by a JSON Schema and rendered into the languages that read it. The mixer
|
|
7
|
+
engine, the DSP library and the plugins all take their ranges, defaults and constants from here, so a knob
|
|
8
|
+
means the same thing on the console, in a plugin and in a test.
|
|
9
|
+
|
|
10
|
+
- One declaration per value, one file per group (`data/`), validated against a closed schema.
|
|
11
|
+
- Renders for C (a header), TypeScript and resolved JSON, each held equal to the data by CI.
|
|
12
|
+
- Semantic versioning enforced by a check: a change that narrows or removes a declaration needs a major bump.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Fedora 44:
|
|
21
17
|
|
|
22
18
|
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
schema/omx-contract.schema.json JSON Schema 2020-12: five kinds (scalar, travels, sheet, list, set), closed units, refs, closed derivations
|
|
26
|
-
lib/eq-defaults.mjs the one default rule for a fresh EQ's bands; the ts and c renders carry it
|
|
27
|
-
bin/omx-contract.mjs the CLI
|
|
28
|
-
render/ c.mjs (openmixer's renderer, moved unchanged, under c/), ts.mjs, json.mjs
|
|
29
|
-
include/omxcontract/ the committed c render (what omx-contract-devel installs)
|
|
30
|
-
share/omx-contract/ the committed json render (resolved data)
|
|
31
|
-
test/ schema, semantic checks, goldens, perturbation, semver, round trip
|
|
32
|
-
tools/proof/round-trip.mjs the spec §4.4 steps 1 and 2 proof against omx-dsp v0.1.3
|
|
33
|
-
recipes/kernel.recipe.json what a complete kernel has; recipes/answers/ cites each value in the engine
|
|
34
|
-
tools/omx-new-kernel.mjs the kernel wizard and importer; tools/kernel-recipe.mjs is `make completeness`
|
|
35
|
-
tools/seed/ the one-time seed from openmixer's built core (provenance; never run by CI)
|
|
36
|
-
packaging/ debian/ omx-contract-devel (RPM), libomx-contract-dev (deb)
|
|
19
|
+
sudo dnf config-manager addrepo --from-repofile=https://freemixer.github.io/rpm/freemixer.repo
|
|
20
|
+
sudo dnf install omx-contract-devel
|
|
37
21
|
```
|
|
38
22
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
"$schema": "../../schema/omx-contract.schema.json",
|
|
47
|
-
"controls": [
|
|
48
|
-
{ "name": "rateHz", "kind": "travel", "global": "TREMOLO_RATE_RANGE", "doc": "…", "travel": { "min": 0.1, "max": 20, "unit": "Hz", "default": 5 } },
|
|
49
|
-
{ "name": "depth", "kind": "travel", "doc": "…", "travel": { "min": 0, "max": 1, "unit": "", "default": 0.5 } },
|
|
50
|
-
{ "name": "mode", "kind": "choice", "doc": "…", "ids": ["tremolo", "pan"], "labels": ["Tremolo", "Pan"] },
|
|
51
|
-
{ "name": "thresholdDb", "kind": "travel", "table": "GATE_LIMITS", "travel": { "min": -80, "max": 0, "unit": "dB" } }
|
|
52
|
-
],
|
|
53
|
-
"tables": { "GATE_LIMITS": { "doc": "…" } },
|
|
54
|
-
"aggregates": { "TREMOLO_TRAVELS": { "doc": "…", "fields": ["rateHz", "depth"] } },
|
|
55
|
-
"constants": { "CHORUS_BASE_MS": { "kind": "scalar", "doc": "…", "unit": "ms", "value": 10 } }
|
|
56
|
-
}
|
|
23
|
+
Debian bookworm and trixie (also Raspberry Pi OS and Zynthian):
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
curl -fsSL https://freemixer.github.io/deb/freemixer.asc | sudo tee /usr/share/keyrings/freemixer.asc >/dev/null
|
|
27
|
+
echo "deb [signed-by=/usr/share/keyrings/freemixer.asc] https://freemixer.github.io/deb/debian/$(. /etc/os-release; echo $VERSION_CODENAME) ./" | sudo tee /etc/apt/sources.list.d/freemixer.list
|
|
28
|
+
sudo apt update && sudo apt install libomx-contract-dev
|
|
57
29
|
```
|
|
58
30
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
kernel's state, so it is not a smooth parameter (the limiter's `lookaheadMs`: omx-dsp's limiter face re-arms
|
|
64
|
-
the state and restarts the gain at unity when the look-ahead moves by a frame). It is a boolean, only on a
|
|
65
|
-
control; it is no item, so the C and TypeScript renders do not carry it.
|
|
66
|
-
- A USE is a control with a `global` and no `travel` or `ids`: it takes the item another control of the kernel
|
|
67
|
-
declares, of its kind, and adds no item (the EQ's `lpfSlope` takes FILTER_SLOPES, which `hpfSlope` declares).
|
|
68
|
-
- `count` names the item that counts a control the kernel takes once per band: a positive integer scalar
|
|
69
|
-
(`GEQ_BANDS`), or a sheet whose every variant gives its count as `max` (`EQ_BAND_COUNTS.eq8.max`).
|
|
70
|
-
- `of` and `when` mark a travel that is no control of its own but the travel the control `of` names reaches while
|
|
71
|
-
the choice `when.control` holds `when.is` (the EQ's `notchQ`: the Q's travel while the band is a notch).
|
|
72
|
-
- A choice's `default` may be `{ "ref": … }` to the scalar that states it; a boolean names the second id of a
|
|
73
|
-
two-id switch when true, the first when false (the delay's `pingpong` comes up at FX_DELAY_PINGPONG_DEFAULT).
|
|
74
|
-
- `tables`: a travels table whose fields are the controls that name it, in control order (`GATE_LIMITS`).
|
|
75
|
-
- `aggregates`: a travels table whose fields are references to controls declared once above, listed by name.
|
|
76
|
-
- `constants`: scalars, lists and sheets, as items.
|
|
77
|
-
|
|
78
|
-
`omx-contract validate` refuses any other key, a constant that is a travel or a set, and a kernel file written
|
|
79
|
-
as a flat item map (the 1.x shape). `rates.json` and `primitives.json` stay item maps.
|
|
80
|
-
|
|
81
|
-
## The JSON render's kernels
|
|
82
|
-
|
|
83
|
-
Beside `items`, `share/omx-contract/omx-contract.json` carries `kernels`: for every kernel file, its controls in
|
|
84
|
-
the order the file declares them. A consumer reads control order and kinds here, never from a kernel file.
|
|
85
|
-
|
|
86
|
-
```json
|
|
87
|
-
"kernels": {
|
|
88
|
-
"limiter": {
|
|
89
|
-
"controls": [
|
|
90
|
-
{ "name": "ceilingDb", "kind": "travel", "global": "LIMITER_LIMITS", "table": "LIMITER_LIMITS" },
|
|
91
|
-
{ "name": "lookaheadMs", "kind": "travel", "global": "LIMITER_LIMITS", "table": "LIMITER_LIMITS", "rearms": true },
|
|
92
|
-
{ "name": "releaseMs", "kind": "travel", "global": "LIMITER_LIMITS", "table": "LIMITER_LIMITS" }
|
|
93
|
-
]
|
|
94
|
-
},
|
|
95
|
-
"tremolo": {
|
|
96
|
-
"controls": [
|
|
97
|
-
{ "name": "rateHz", "kind": "travel", "global": "TREMOLO_RATE_RANGE" },
|
|
98
|
-
{ "name": "mode", "kind": "choice", "global": "TREMOLO_MODES" }
|
|
99
|
-
]
|
|
100
|
-
}
|
|
101
|
-
}
|
|
31
|
+
JavaScript and TypeScript projects (the `ts` render is the main export, the CLI is `omx-contract`):
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
npm install @openmixer/omx-contract
|
|
102
35
|
```
|
|
103
36
|
|
|
104
|
-
|
|
105
|
-
- `global`: the item the control's value lives in: `items[global].value` for a travel or a choice, and
|
|
106
|
-
`items[global].value[name]` for a field of a table, when `table` (the same item) is present.
|
|
107
|
-
- `rearms`: present, and `true`, only on a control that declares it.
|
|
108
|
-
- `count`: present on a control taken once per band, the item that counts it.
|
|
109
|
-
- `when`: on a control another one is `of`, the list `{ control, is, global }`: while `control` holds `is`, the
|
|
110
|
-
control's travel is `items[global]` (the EQ's `q` reaches EQ_NOTCH_Q_RANGE on a notch). A control that is `of`
|
|
111
|
-
another is not listed on its own. A use is listed like any control, its `global` the item it takes.
|
|
37
|
+
Every GitHub release also carries the `npm pack` tarball, the RPMs and the debs.
|
|
112
38
|
|
|
113
|
-
|
|
39
|
+
## Use
|
|
114
40
|
|
|
115
|
-
|
|
41
|
+
Include the C header and read the constants by name:
|
|
116
42
|
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
node bin/omx-contract.mjs validate # schema + semantic checks
|
|
120
|
-
node bin/omx-contract.mjs fmt [--check] # the one formatting
|
|
121
|
-
node bin/omx-contract.mjs render --target c|ts|json --out <dir> [--check]
|
|
122
|
-
node bin/omx-contract.mjs semver <previous tag> # exit 1 if the version bump is short
|
|
43
|
+
```c
|
|
44
|
+
#include <omxcontract/omx_contract_limits.h>
|
|
123
45
|
```
|
|
124
46
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
47
|
+
```
|
|
48
|
+
pkg-config --cflags omx-contract
|
|
49
|
+
```
|
|
128
50
|
|
|
129
|
-
|
|
51
|
+
Pin one version and build against its renders instead of keeping a copy: the installed package at exactly
|
|
52
|
+
that version (`pkg-config --exact-version=2.5.0 omx-contract`), else the npm tarball, unpacked, and
|
|
53
|
+
`omx-contract render --target c --out build/omx-contract/include`. A kernel's controls and readouts, in their
|
|
54
|
+
order, are in `share/omx-contract/omx-contract.json`; how a declaration is written is in
|
|
55
|
+
[docs/kernel-files.md](docs/kernel-files.md), and what a readout is in [docs/readouts.md](docs/readouts.md).
|
|
130
56
|
|
|
131
|
-
|
|
132
|
-
none: the installed `omx-contract-devel` / `libomx-contract-dev` at exactly that version
|
|
133
|
-
(`pkg-config --exact-version=2.3.0 omx-contract`), else the release's npm tarball, unpacked, and
|
|
134
|
-
`omx-contract render --target c --out build/omx-contract/include`. Include
|
|
135
|
-
`<omxcontract/omx_contract_limits.h>`. A plugin's parameter header is rendered by omx-plugins from its
|
|
136
|
-
declaration, by reference to the kernel travels here (since 1.1.0).
|
|
57
|
+
## Building
|
|
137
58
|
|
|
138
|
-
|
|
139
|
-
GitHub release also carries the `npm pack` tarball, the RPMs and the debs.
|
|
59
|
+
See [BUILDING.md](BUILDING.md).
|
|
140
60
|
|
|
141
61
|
## License
|
|
142
62
|
|
package/data/kernels/comp.json
CHANGED
|
@@ -122,6 +122,32 @@
|
|
|
122
122
|
"Off",
|
|
123
123
|
"4x"
|
|
124
124
|
]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "detectorTopology",
|
|
128
|
+
"kind": "choice",
|
|
129
|
+
"global": "DETECTOR_TOPOLOGY",
|
|
130
|
+
"doc": "Where the compressor's detector listens: `feedforward` hears the slot's input (or its key), `feedback` hears the slot's own pre-make-up output DYN_FB_LOOP_DELAY_SAMPLES late, through one envelope stage, and a keyed slot stays feed-forward (omx-dsp's OMX_DYN_TOPOLOGY_FEEDFORWARD 0 and _FEEDBACK 1, in that order; the set is named for its control, so C spells them OMX_DETECTOR_TOPOLOGY_*). The console's comp row carries it (`detectorTopology`, openmixer's ssl-bus-compressor spec §3a) and comes up `feedforward`, so a zeroed atom is the compressor every slot runs.",
|
|
131
|
+
"ids": [
|
|
132
|
+
"feedforward",
|
|
133
|
+
"feedback"
|
|
134
|
+
],
|
|
135
|
+
"default": "feedforward"
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
"readouts": [
|
|
139
|
+
{
|
|
140
|
+
"name": "gainReductionDb",
|
|
141
|
+
"doc": "The compressor's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to COMP_LIMITS.thresholdDb.min: on a signal at or below 0 dBFS the reduction (x - T)(1 - 1/R) never exceeds -T, and T is at least the threshold's minimum.",
|
|
142
|
+
"unit": "dB",
|
|
143
|
+
"min": {
|
|
144
|
+
"ref": "COMP_LIMITS.thresholdDb.min"
|
|
145
|
+
},
|
|
146
|
+
"max": 0,
|
|
147
|
+
"scale": "linear",
|
|
148
|
+
"rateHz": {
|
|
149
|
+
"ref": "READOUT_RATE_HZ"
|
|
150
|
+
}
|
|
125
151
|
}
|
|
126
152
|
],
|
|
127
153
|
"tables": {
|
|
@@ -145,6 +171,12 @@
|
|
|
145
171
|
}
|
|
146
172
|
}
|
|
147
173
|
}
|
|
174
|
+
},
|
|
175
|
+
"DYN_FB_LOOP_DELAY_SAMPLES": {
|
|
176
|
+
"kind": "scalar",
|
|
177
|
+
"doc": "The feedback detector's loop delay, in samples: the output y[n] depends on the gain at n, so the detector can only have heard y[n - 1]. Not a latency: the audio is never delayed by it. omx-dsp's feedback state holds exactly this many output samples per leg.",
|
|
178
|
+
"unit": "",
|
|
179
|
+
"value": 1
|
|
148
180
|
}
|
|
149
181
|
}
|
|
150
182
|
}
|
|
@@ -110,6 +110,21 @@
|
|
|
110
110
|
]
|
|
111
111
|
}
|
|
112
112
|
],
|
|
113
|
+
"readouts": [
|
|
114
|
+
{
|
|
115
|
+
"name": "gainReductionDb",
|
|
116
|
+
"doc": "The de-esser's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to DEESS_RANGE_RANGE.min: the de-esser reduces by its range at most.",
|
|
117
|
+
"unit": "dB",
|
|
118
|
+
"min": {
|
|
119
|
+
"ref": "DEESS_RANGE_RANGE.min"
|
|
120
|
+
},
|
|
121
|
+
"max": 0,
|
|
122
|
+
"scale": "linear",
|
|
123
|
+
"rateHz": {
|
|
124
|
+
"ref": "READOUT_RATE_HZ"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
],
|
|
113
128
|
"constants": {
|
|
114
129
|
"DEESS_KNEE_DB": {
|
|
115
130
|
"kind": "scalar",
|
package/data/kernels/gate.json
CHANGED
|
@@ -128,6 +128,21 @@
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
],
|
|
131
|
+
"readouts": [
|
|
132
|
+
{
|
|
133
|
+
"name": "gainReductionDb",
|
|
134
|
+
"doc": "The gate's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to GATE_LIMITS.rangeDb.min: the gate attenuates by its range at most.",
|
|
135
|
+
"unit": "dB",
|
|
136
|
+
"min": {
|
|
137
|
+
"ref": "GATE_LIMITS.rangeDb.min"
|
|
138
|
+
},
|
|
139
|
+
"max": 0,
|
|
140
|
+
"scale": "linear",
|
|
141
|
+
"rateHz": {
|
|
142
|
+
"ref": "READOUT_RATE_HZ"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
],
|
|
131
146
|
"tables": {
|
|
132
147
|
"GATE_LIMITS": {
|
|
133
148
|
"doc": "The legal range of each gate field (engineering units). The widget clamps drags to these, `normalizeGate` enforces them, the row's codec refuses by them and the travel sheet publishes them — one source of truth. `rangeDb` reaches GATE_RANGE_FLOOR_DB, shown as \"−∞\" in readouts (a gate fully closed is silence)."
|
|
@@ -42,6 +42,21 @@
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
],
|
|
45
|
+
"readouts": [
|
|
46
|
+
{
|
|
47
|
+
"name": "gainReductionDb",
|
|
48
|
+
"doc": "The limiter's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to LIMITER_LIMITS.ceilingDb.min: on a signal at or below 0 dBFS the limiter pulls the peak down to its ceiling at most, and the ceiling is at least its minimum.",
|
|
49
|
+
"unit": "dB",
|
|
50
|
+
"min": {
|
|
51
|
+
"ref": "LIMITER_LIMITS.ceilingDb.min"
|
|
52
|
+
},
|
|
53
|
+
"max": 0,
|
|
54
|
+
"scale": "linear",
|
|
55
|
+
"rateHz": {
|
|
56
|
+
"ref": "READOUT_RATE_HZ"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
],
|
|
45
60
|
"tables": {
|
|
46
61
|
"LIMITER_LIMITS": {
|
|
47
62
|
"doc": "The precision limiter's travels (`2026-09-27-precision-limiter.md` §2), rendered into `omx_contract_limits.h` as `OMX_LIMITER_*` by `harness/contract-limits-gen.mjs`, so `mix_limiter.h` reads them and never restates one (R-094). The ceiling is a TRUE peak in dBFS (the ×4 detector); the look-ahead sets the declared latency; the release is the envelope's time constant on the gain depth."
|
package/data/primitives.json
CHANGED
|
@@ -154,5 +154,12 @@
|
|
|
154
154
|
"scale": "log"
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
},
|
|
158
|
+
"READOUT_RATE_HZ": {
|
|
159
|
+
"kind": "scalar",
|
|
160
|
+
"since": "2.4.0",
|
|
161
|
+
"doc": "How often a readout's value is published: 50 Hz, one value every 20 ms, the rate a host sends a moving meter at. Every readout reads it as its `rateHz` (docs/readouts.md).",
|
|
162
|
+
"unit": "Hz",
|
|
163
|
+
"value": 50
|
|
157
164
|
}
|
|
158
165
|
}
|
|
@@ -254,6 +254,12 @@ export declare const COMP_KINDS: readonly ["comp", "limiter"];
|
|
|
254
254
|
*/
|
|
255
255
|
export declare const DETECTOR_OVERSAMPLINGS: readonly ["auto", "off", "x4"];
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Where the compressor's detector listens: `feedforward` hears the slot's input (or its key), `feedback` hears the slot's own pre-make-up output DYN_FB_LOOP_DELAY_SAMPLES late, through one envelope stage, and a keyed slot stays feed-forward (omx-dsp's OMX_DYN_TOPOLOGY_FEEDFORWARD 0 and _FEEDBACK 1, in that order; the set is named for its control, so C spells them OMX_DETECTOR_TOPOLOGY_*). The console's comp row carries it (`detectorTopology`, openmixer's ssl-bus-compressor spec §3a) and comes up `feedforward`, so a zeroed atom is the compressor every slot runs.
|
|
259
|
+
* @see data/kernels/comp.json
|
|
260
|
+
*/
|
|
261
|
+
export declare const DETECTOR_TOPOLOGY: readonly ["feedforward", "feedback"];
|
|
262
|
+
|
|
257
263
|
/**
|
|
258
264
|
* 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.
|
|
259
265
|
* @see data/kernels/comp.json
|
|
@@ -271,6 +277,12 @@ export declare const PROGRAM_RELEASE: {
|
|
|
271
277
|
};
|
|
272
278
|
};
|
|
273
279
|
|
|
280
|
+
/**
|
|
281
|
+
* The feedback detector's loop delay, in samples: the output y[n] depends on the gain at n, so the detector can only have heard y[n - 1]. Not a latency: the audio is never delayed by it. omx-dsp's feedback state holds exactly this many output samples per leg.
|
|
282
|
+
* @see data/kernels/comp.json
|
|
283
|
+
*/
|
|
284
|
+
export declare const DYN_FB_LOOP_DELAY_SAMPLES: 1;
|
|
285
|
+
|
|
274
286
|
/**
|
|
275
287
|
* The detector band's CENTRE, in Hz. Sibilance lives at 5–10 kHz on most voices; 7 kHz with the default width covers 4 950–9 900 Hz, which is where an "s" and a "t" sit on a close vocal mic. The travel is wider than the come-up on both sides on purpose: a bright female vocal wants the band up near 9 kHz and a chesty male one down at 5, and a surface narrower than the row removes reach the console genuinely has (the same reasoning core's `REVERB_CUT_RANGE` records).
|
|
276
288
|
* @see data/kernels/deesser.json
|
|
@@ -2020,6 +2032,12 @@ export declare const EQ_CONTINUOUS_TRAVELS: {
|
|
|
2020
2032
|
};
|
|
2021
2033
|
};
|
|
2022
2034
|
|
|
2035
|
+
/**
|
|
2036
|
+
* How often a readout's value is published: 50 Hz, one value every 20 ms, the rate a host sends a moving meter at. Every readout reads it as its `rateHz` (docs/readouts.md).
|
|
2037
|
+
* @see data/primitives.json
|
|
2038
|
+
*/
|
|
2039
|
+
export declare const READOUT_RATE_HZ: 50;
|
|
2040
|
+
|
|
2023
2041
|
/**
|
|
2024
2042
|
* The standard sample rates (Hz) the desk recognises — the pro-audio superset. One source of truth for the clock controller's `clock.allowed-rates` seed, the force row's validation ceiling and the device-rate probe fallback. What the operator is OFFERED is always the DEVICE's own `supportedRates` (`2026-07-15-clock-rate-honest-force-rate.md`) — this is the ceiling a write is refused by, never a menu.
|
|
2025
2043
|
* @see data/rates.json
|
|
@@ -2053,6 +2071,50 @@ export declare const ORACLE_FLOOR_RATES: readonly [44100, 48000, 96000, 192000];
|
|
|
2053
2071
|
*/
|
|
2054
2072
|
export declare const RME_RATES: readonly [32000, 44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000];
|
|
2055
2073
|
|
|
2074
|
+
/**
|
|
2075
|
+
* Every kernel's readouts, the meters a host draws, each kernel's in its declared order: the position is the
|
|
2076
|
+
* readout's id. Every reference is resolved.
|
|
2077
|
+
* @see docs/readouts.md
|
|
2078
|
+
*/
|
|
2079
|
+
export declare const READOUTS: {
|
|
2080
|
+
readonly comp: readonly [{
|
|
2081
|
+
readonly name: "gainReductionDb";
|
|
2082
|
+
readonly doc: "The compressor's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to COMP_LIMITS.thresholdDb.min: on a signal at or below 0 dBFS the reduction (x - T)(1 - 1/R) never exceeds -T, and T is at least the threshold's minimum.";
|
|
2083
|
+
readonly unit: "dB";
|
|
2084
|
+
readonly min: -60;
|
|
2085
|
+
readonly max: 0;
|
|
2086
|
+
readonly scale: "linear";
|
|
2087
|
+
readonly rateHz: 50;
|
|
2088
|
+
}];
|
|
2089
|
+
readonly deesser: readonly [{
|
|
2090
|
+
readonly name: "gainReductionDb";
|
|
2091
|
+
readonly doc: "The de-esser's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to DEESS_RANGE_RANGE.min: the de-esser reduces by its range at most.";
|
|
2092
|
+
readonly unit: "dB";
|
|
2093
|
+
readonly min: -24;
|
|
2094
|
+
readonly max: 0;
|
|
2095
|
+
readonly scale: "linear";
|
|
2096
|
+
readonly rateHz: 50;
|
|
2097
|
+
}];
|
|
2098
|
+
readonly gate: readonly [{
|
|
2099
|
+
readonly name: "gainReductionDb";
|
|
2100
|
+
readonly doc: "The gate's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to GATE_LIMITS.rangeDb.min: the gate attenuates by its range at most.";
|
|
2101
|
+
readonly unit: "dB";
|
|
2102
|
+
readonly min: -90;
|
|
2103
|
+
readonly max: 0;
|
|
2104
|
+
readonly scale: "linear";
|
|
2105
|
+
readonly rateHz: 50;
|
|
2106
|
+
}];
|
|
2107
|
+
readonly limiter: readonly [{
|
|
2108
|
+
readonly name: "gainReductionDb";
|
|
2109
|
+
readonly doc: "The limiter's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to LIMITER_LIMITS.ceilingDb.min: on a signal at or below 0 dBFS the limiter pulls the peak down to its ceiling at most, and the ceiling is at least its minimum.";
|
|
2110
|
+
readonly unit: "dB";
|
|
2111
|
+
readonly min: -12;
|
|
2112
|
+
readonly max: 0;
|
|
2113
|
+
readonly scale: "linear";
|
|
2114
|
+
readonly rateHz: 50;
|
|
2115
|
+
}];
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2056
2118
|
/** The default band centres (Hz) of an EQ with `count` bands: THE one rule, for every strip, host and count. */
|
|
2057
2119
|
export declare function eqDefaultCentres(count: number): number[];
|
|
2058
2120
|
/** The default band types, by EQ_BAND_TYPES id: a low shelf, bells, a high shelf (a single band is a bell). */
|
package/dist/ts/omx-contract.js
CHANGED
|
@@ -264,6 +264,15 @@ export const DETECTOR_OVERSAMPLINGS = [
|
|
|
264
264
|
"x4"
|
|
265
265
|
];
|
|
266
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Where the compressor's detector listens: `feedforward` hears the slot's input (or its key), `feedback` hears the slot's own pre-make-up output DYN_FB_LOOP_DELAY_SAMPLES late, through one envelope stage, and a keyed slot stays feed-forward (omx-dsp's OMX_DYN_TOPOLOGY_FEEDFORWARD 0 and _FEEDBACK 1, in that order; the set is named for its control, so C spells them OMX_DETECTOR_TOPOLOGY_*). The console's comp row carries it (`detectorTopology`, openmixer's ssl-bus-compressor spec §3a) and comes up `feedforward`, so a zeroed atom is the compressor every slot runs.
|
|
269
|
+
* @see data/kernels/comp.json
|
|
270
|
+
*/
|
|
271
|
+
export const DETECTOR_TOPOLOGY = [
|
|
272
|
+
"feedforward",
|
|
273
|
+
"feedback"
|
|
274
|
+
];
|
|
275
|
+
|
|
267
276
|
/**
|
|
268
277
|
* 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.
|
|
269
278
|
* @see data/kernels/comp.json
|
|
@@ -281,6 +290,12 @@ export const PROGRAM_RELEASE = {
|
|
|
281
290
|
}
|
|
282
291
|
};
|
|
283
292
|
|
|
293
|
+
/**
|
|
294
|
+
* The feedback detector's loop delay, in samples: the output y[n] depends on the gain at n, so the detector can only have heard y[n - 1]. Not a latency: the audio is never delayed by it. omx-dsp's feedback state holds exactly this many output samples per leg.
|
|
295
|
+
* @see data/kernels/comp.json
|
|
296
|
+
*/
|
|
297
|
+
export const DYN_FB_LOOP_DELAY_SAMPLES = 1;
|
|
298
|
+
|
|
284
299
|
/**
|
|
285
300
|
* The detector band's CENTRE, in Hz. Sibilance lives at 5–10 kHz on most voices; 7 kHz with the default width covers 4 950–9 900 Hz, which is where an "s" and a "t" sit on a close vocal mic. The travel is wider than the come-up on both sides on purpose: a bright female vocal wants the band up near 9 kHz and a chesty male one down at 5, and a surface narrower than the row removes reach the console genuinely has (the same reasoning core's `REVERB_CUT_RANGE` records).
|
|
286
301
|
* @see data/kernels/deesser.json
|
|
@@ -2201,6 +2216,12 @@ export const EQ_CONTINUOUS_TRAVELS = {
|
|
|
2201
2216
|
}
|
|
2202
2217
|
};
|
|
2203
2218
|
|
|
2219
|
+
/**
|
|
2220
|
+
* How often a readout's value is published: 50 Hz, one value every 20 ms, the rate a host sends a moving meter at. Every readout reads it as its `rateHz` (docs/readouts.md).
|
|
2221
|
+
* @see data/primitives.json
|
|
2222
|
+
*/
|
|
2223
|
+
export const READOUT_RATE_HZ = 50;
|
|
2224
|
+
|
|
2204
2225
|
/**
|
|
2205
2226
|
* The standard sample rates (Hz) the desk recognises — the pro-audio superset. One source of truth for the clock controller's `clock.allowed-rates` seed, the force row's validation ceiling and the device-rate probe fallback. What the operator is OFFERED is always the DEVICE's own `supportedRates` (`2026-07-15-clock-rate-honest-force-rate.md`) — this is the ceiling a write is refused by, never a menu.
|
|
2206
2227
|
* @see data/rates.json
|
|
@@ -2256,6 +2277,58 @@ export const RME_RATES = [
|
|
|
2256
2277
|
192000
|
|
2257
2278
|
];
|
|
2258
2279
|
|
|
2280
|
+
/**
|
|
2281
|
+
* Every kernel's readouts, the meters a host draws, each kernel's in its declared order: the position is the
|
|
2282
|
+
* readout's id. Every reference is resolved.
|
|
2283
|
+
* @see docs/readouts.md
|
|
2284
|
+
*/
|
|
2285
|
+
export const READOUTS = {
|
|
2286
|
+
"comp": [
|
|
2287
|
+
{
|
|
2288
|
+
"name": "gainReductionDb",
|
|
2289
|
+
"doc": "The compressor's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to COMP_LIMITS.thresholdDb.min: on a signal at or below 0 dBFS the reduction (x - T)(1 - 1/R) never exceeds -T, and T is at least the threshold's minimum.",
|
|
2290
|
+
"unit": "dB",
|
|
2291
|
+
"min": -60,
|
|
2292
|
+
"max": 0,
|
|
2293
|
+
"scale": "linear",
|
|
2294
|
+
"rateHz": 50
|
|
2295
|
+
}
|
|
2296
|
+
],
|
|
2297
|
+
"deesser": [
|
|
2298
|
+
{
|
|
2299
|
+
"name": "gainReductionDb",
|
|
2300
|
+
"doc": "The de-esser's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to DEESS_RANGE_RANGE.min: the de-esser reduces by its range at most.",
|
|
2301
|
+
"unit": "dB",
|
|
2302
|
+
"min": -24,
|
|
2303
|
+
"max": 0,
|
|
2304
|
+
"scale": "linear",
|
|
2305
|
+
"rateHz": 50
|
|
2306
|
+
}
|
|
2307
|
+
],
|
|
2308
|
+
"gate": [
|
|
2309
|
+
{
|
|
2310
|
+
"name": "gainReductionDb",
|
|
2311
|
+
"doc": "The gate's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to GATE_LIMITS.rangeDb.min: the gate attenuates by its range at most.",
|
|
2312
|
+
"unit": "dB",
|
|
2313
|
+
"min": -90,
|
|
2314
|
+
"max": 0,
|
|
2315
|
+
"scale": "linear",
|
|
2316
|
+
"rateHz": 50
|
|
2317
|
+
}
|
|
2318
|
+
],
|
|
2319
|
+
"limiter": [
|
|
2320
|
+
{
|
|
2321
|
+
"name": "gainReductionDb",
|
|
2322
|
+
"doc": "The limiter's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to LIMITER_LIMITS.ceilingDb.min: on a signal at or below 0 dBFS the limiter pulls the peak down to its ceiling at most, and the ceiling is at least its minimum.",
|
|
2323
|
+
"unit": "dB",
|
|
2324
|
+
"min": -12,
|
|
2325
|
+
"max": 0,
|
|
2326
|
+
"scale": "linear",
|
|
2327
|
+
"rateHz": 50
|
|
2328
|
+
}
|
|
2329
|
+
]
|
|
2330
|
+
};
|
|
2331
|
+
|
|
2259
2332
|
/** The declared parameters of the default EQ rule: the four-band set, the preferred series, the band frequency travel, the band gain and Q. */
|
|
2260
2333
|
const EQ_DEFAULT_RULE = {
|
|
2261
2334
|
"four": [
|
|
@@ -279,6 +279,7 @@
|
|
|
279
279
|
#define OMX_DRIVE_TRIM_RANGE_STEP 0.1f
|
|
280
280
|
#define OMX_DRIVE_TRIM_RANGE_STEP_DOUBLE 0.1
|
|
281
281
|
#define OMX_DSP_KNOB_REFERENCE_RATE 96000
|
|
282
|
+
#define OMX_DYN_FB_LOOP_DELAY_SAMPLES 1
|
|
282
283
|
#define OMX_EQ_BAND_COUNTS_CHANNEL_DEFAULT 4
|
|
283
284
|
#define OMX_EQ_BAND_COUNTS_CHANNEL_MAX 24
|
|
284
285
|
#define OMX_EQ_BAND_COUNTS_EQ16_DEFAULT 16
|
|
@@ -450,6 +451,7 @@
|
|
|
450
451
|
#define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SLOW_MAX_MS 1100
|
|
451
452
|
#define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SLOW_MIN_MS 110
|
|
452
453
|
#define OMX_PSHR_MIN_DB 10
|
|
454
|
+
#define OMX_READOUT_RATE_HZ 50
|
|
453
455
|
#define OMX_REVERB_DAMPING_RANGE_DEFAULT 0.5f
|
|
454
456
|
#define OMX_REVERB_DAMPING_RANGE_DEFAULT_DOUBLE 0.5
|
|
455
457
|
#define OMX_REVERB_DAMPING_RANGE_MAX 1
|
|
@@ -651,6 +653,12 @@ enum omx_detector_oversamplings {
|
|
|
651
653
|
};
|
|
652
654
|
#define OMX_DETECTOR_OVERSAMPLINGS_COUNT 3u
|
|
653
655
|
#define OMX_DETECTOR_OVERSAMPLINGS_DEFAULT OMX_DETECTOR_OVERSAMPLINGS_AUTO
|
|
656
|
+
enum omx_detector_topology {
|
|
657
|
+
OMX_DETECTOR_TOPOLOGY_FEEDFORWARD = 0,
|
|
658
|
+
OMX_DETECTOR_TOPOLOGY_FEEDBACK = 1,
|
|
659
|
+
};
|
|
660
|
+
#define OMX_DETECTOR_TOPOLOGY_COUNT 2u
|
|
661
|
+
#define OMX_DETECTOR_TOPOLOGY_DEFAULT OMX_DETECTOR_TOPOLOGY_FEEDFORWARD
|
|
654
662
|
enum omx_drive_auto_gains {
|
|
655
663
|
OMX_DRIVE_AUTO_GAINS_OFF = 0,
|
|
656
664
|
OMX_DRIVE_AUTO_GAINS_ON = 1,
|
|
@@ -750,6 +758,46 @@ enum omx_tremolo_modes {
|
|
|
750
758
|
#define OMX_TREMOLO_MODES_COUNT 2u
|
|
751
759
|
#define OMX_TREMOLO_MODES_DEFAULT OMX_TREMOLO_MODES_TREMOLO
|
|
752
760
|
|
|
761
|
+
/* Every kernel's readouts, the meters a host draws (docs/readouts.md): an enum of the readouts at their position,
|
|
762
|
+
* `OMX_<KERNEL>_READOUT_COUNT`, and per readout its display range (_MIN, _MAX), _RATE_HZ, _SCALE (an OMX_PARAM_SCALES_*
|
|
763
|
+
* member) and _UNIT. */
|
|
764
|
+
enum omx_comp_readouts {
|
|
765
|
+
OMX_COMP_READOUT_GAIN_REDUCTION_DB = 0,
|
|
766
|
+
};
|
|
767
|
+
#define OMX_COMP_READOUT_COUNT 1u
|
|
768
|
+
#define OMX_COMP_READOUT_GAIN_REDUCTION_DB_MIN -60.0f
|
|
769
|
+
#define OMX_COMP_READOUT_GAIN_REDUCTION_DB_MAX 0.0f
|
|
770
|
+
#define OMX_COMP_READOUT_GAIN_REDUCTION_DB_RATE_HZ 50
|
|
771
|
+
#define OMX_COMP_READOUT_GAIN_REDUCTION_DB_SCALE OMX_PARAM_SCALES_LINEAR
|
|
772
|
+
#define OMX_COMP_READOUT_GAIN_REDUCTION_DB_UNIT "dB"
|
|
773
|
+
enum omx_deesser_readouts {
|
|
774
|
+
OMX_DEESSER_READOUT_GAIN_REDUCTION_DB = 0,
|
|
775
|
+
};
|
|
776
|
+
#define OMX_DEESSER_READOUT_COUNT 1u
|
|
777
|
+
#define OMX_DEESSER_READOUT_GAIN_REDUCTION_DB_MIN -24.0f
|
|
778
|
+
#define OMX_DEESSER_READOUT_GAIN_REDUCTION_DB_MAX 0.0f
|
|
779
|
+
#define OMX_DEESSER_READOUT_GAIN_REDUCTION_DB_RATE_HZ 50
|
|
780
|
+
#define OMX_DEESSER_READOUT_GAIN_REDUCTION_DB_SCALE OMX_PARAM_SCALES_LINEAR
|
|
781
|
+
#define OMX_DEESSER_READOUT_GAIN_REDUCTION_DB_UNIT "dB"
|
|
782
|
+
enum omx_gate_readouts {
|
|
783
|
+
OMX_GATE_READOUT_GAIN_REDUCTION_DB = 0,
|
|
784
|
+
};
|
|
785
|
+
#define OMX_GATE_READOUT_COUNT 1u
|
|
786
|
+
#define OMX_GATE_READOUT_GAIN_REDUCTION_DB_MIN -90.0f
|
|
787
|
+
#define OMX_GATE_READOUT_GAIN_REDUCTION_DB_MAX 0.0f
|
|
788
|
+
#define OMX_GATE_READOUT_GAIN_REDUCTION_DB_RATE_HZ 50
|
|
789
|
+
#define OMX_GATE_READOUT_GAIN_REDUCTION_DB_SCALE OMX_PARAM_SCALES_LINEAR
|
|
790
|
+
#define OMX_GATE_READOUT_GAIN_REDUCTION_DB_UNIT "dB"
|
|
791
|
+
enum omx_limiter_readouts {
|
|
792
|
+
OMX_LIMITER_READOUT_GAIN_REDUCTION_DB = 0,
|
|
793
|
+
};
|
|
794
|
+
#define OMX_LIMITER_READOUT_COUNT 1u
|
|
795
|
+
#define OMX_LIMITER_READOUT_GAIN_REDUCTION_DB_MIN -12.0f
|
|
796
|
+
#define OMX_LIMITER_READOUT_GAIN_REDUCTION_DB_MAX 0.0f
|
|
797
|
+
#define OMX_LIMITER_READOUT_GAIN_REDUCTION_DB_RATE_HZ 50
|
|
798
|
+
#define OMX_LIMITER_READOUT_GAIN_REDUCTION_DB_SCALE OMX_PARAM_SCALES_LINEAR
|
|
799
|
+
#define OMX_LIMITER_READOUT_GAIN_REDUCTION_DB_UNIT "dB"
|
|
800
|
+
|
|
753
801
|
/* The default bands of an EQ with `count` bands: THE one rule, for every strip, host and count (omx-contract's
|
|
754
802
|
* lib/eq-defaults.mjs; the ts render exports the same as eqDefaultCentres and eqDefaultTypes). Needs -lm. */
|
|
755
803
|
static inline unsigned omx_eq_default_centres(unsigned count, float *out) {
|
package/lib/data.mjs
CHANGED
|
@@ -45,9 +45,12 @@ export function loadData(dir, texts) {
|
|
|
45
45
|
const snakeOf = (n) => n.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
46
46
|
const camelOf = (u) => u.toLowerCase().replace(/_([a-z0-9])/g, (_, x) => x.toUpperCase());
|
|
47
47
|
|
|
48
|
-
/** The four sections of a kernel file, in their order. */
|
|
48
|
+
/** The four sections of a kernel file that declare items, in their order. */
|
|
49
49
|
export const KERNEL_SECTIONS = ['controls', 'tables', 'aggregates', 'constants'];
|
|
50
50
|
|
|
51
|
+
/** Every top-level key a kernel file may hold, in their order: the four sections and `readouts`, which declares no item. */
|
|
52
|
+
export const KERNEL_KEYS = ['controls', 'readouts', 'tables', 'aggregates', 'constants'];
|
|
53
|
+
|
|
51
54
|
/** Whether a data file is a kernel file (`kernels/<kernel>.json`), which holds the four sections, never an item map. */
|
|
52
55
|
export const isKernelFile = (rel) => /(^|\/)kernels\/[^/]+\.json$/.test(rel);
|
|
53
56
|
|
|
@@ -261,7 +264,8 @@ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
|
261
264
|
/**
|
|
262
265
|
* Resolve every item: `{ name, rel, kind, doc, why?, unit?, c?, shape?, default?, labels?, value }` (default and labels: a set's), where `value` has
|
|
263
266
|
* every `{ref}` and `{derive}` evaluated. The returned map also carries `kernels`, every kernel's ordered controls
|
|
264
|
-
* (kernelControls),
|
|
267
|
+
* (kernelControls) and, for a kernel that declares them, its ordered `readouts` `{ name, doc, unit, min, max, scale,
|
|
268
|
+
* rateHz }` with every reference resolved, which the json render publishes beside the items.
|
|
265
269
|
* @throws naming the item: a reference that does not resolve, a cycle, an unknown derivation.
|
|
266
270
|
*/
|
|
267
271
|
export function resolveData(data) {
|
|
@@ -311,6 +315,13 @@ export function resolveData(data) {
|
|
|
311
315
|
const out = new Map();
|
|
312
316
|
for (const name of data.items.keys()) out.set(name, resolveItem(name));
|
|
313
317
|
out.kernels = kernelControls(data);
|
|
318
|
+
for (const f of data.files) {
|
|
319
|
+
if (!isKernelFile(f.rel) || !Array.isArray(f.doc.readouts)) continue;
|
|
320
|
+
out.kernels[kernelName(f.rel)].readouts = f.doc.readouts.map((ro) => {
|
|
321
|
+
const from = `${kernelName(f.rel)}.readouts.${ro.name}`;
|
|
322
|
+
return { name: ro.name, doc: ro.doc, unit: ro.unit, min: val(ro.min, from), max: val(ro.max, from), scale: ro.scale, rateHz: val(ro.rateHz, from) };
|
|
323
|
+
});
|
|
324
|
+
}
|
|
314
325
|
return out;
|
|
315
326
|
}
|
|
316
327
|
|
package/lib/fmt.mjs
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* The one formatting of a data file (spec §3.2): two-space indent, one key per line, keys in schema
|
|
5
5
|
* order, a trailing newline. Names (items, fields, sheet keys) keep the order the author gave them:
|
|
6
6
|
* a travels table's field order is its C block order. A kernel file's sections come in their order (controls,
|
|
7
|
-
* tables, aggregates, constants), its controls in the author's order, each
|
|
7
|
+
* readouts, tables, aggregates, constants), its controls and readouts in the author's order, each one's keys in
|
|
8
|
+
* schema order.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const ORDER = {
|
|
11
12
|
control: ['name', 'kind', 'global', 'table', 'rearms', 'since', 'doc', 'why', 'unit', 'ids', 'default', 'labels', 'travel', 'c'],
|
|
12
|
-
sections: ['controls', 'tables', 'aggregates', 'constants'],
|
|
13
|
+
sections: ['controls', 'readouts', 'tables', 'aggregates', 'constants'],
|
|
14
|
+
readout: ['name', 'doc', 'unit', 'min', 'max', 'scale', 'rateHz'],
|
|
13
15
|
item: ['kind', 'since', 'doc', 'why', 'unit', 'value', 'values', 'ids', 'default', 'labels', 'fields', 'travel', 'c'],
|
|
14
16
|
travel: ['min', 'max', 'step', 'unit', 'default', 'defaultFrom', 'scale', 'byKind', 'doc', 'why'],
|
|
15
17
|
c: ['alias', 'render'],
|
|
@@ -63,6 +65,13 @@ function control(c) {
|
|
|
63
65
|
return o;
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
function readout(r) {
|
|
69
|
+
if (!isObj(r)) return r;
|
|
70
|
+
const o = ordered(r, ORDER.readout);
|
|
71
|
+
for (const k of ['min', 'max', 'rateHz']) if (k in o) o[k] = number(o[k]);
|
|
72
|
+
return o;
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
const items = (m) => (isObj(m) ? Object.fromEntries(Object.entries(m).map(([k, v]) => [k, item(v)])) : m);
|
|
67
76
|
|
|
68
77
|
/** The canonical text of a parsed data file. */
|
|
@@ -72,7 +81,9 @@ export function formatDoc(doc) {
|
|
|
72
81
|
if (ORDER.sections.some((k) => k in doc)) {
|
|
73
82
|
for (const [k, v] of Object.entries(ordered(doc, ORDER.sections))) {
|
|
74
83
|
if (k === '$schema') continue;
|
|
75
|
-
|
|
84
|
+
if (k === 'controls' && Array.isArray(v)) out[k] = v.map(control);
|
|
85
|
+
else if (k === 'readouts' && Array.isArray(v)) out[k] = v.map(readout);
|
|
86
|
+
else out[k] = ORDER.sections.includes(k) ? items(v) : item(v);
|
|
76
87
|
}
|
|
77
88
|
} else {
|
|
78
89
|
for (const [k, v] of Object.entries(doc)) if (k !== '$schema') out[k] = item(v);
|
package/lib/semver.mjs
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* minor an item, field, sheet leaf, list member or set id added (appended); a travel widened;
|
|
12
12
|
* a default moved inside its travel; a step changed
|
|
13
13
|
* patch doc and why text
|
|
14
|
+
*
|
|
15
|
+
* A kernel's readouts (docs/readouts.md): one removed, renamed or moved, or its unit changed, is major (a removal is
|
|
16
|
+
* minor in the development phase, as an item's is); one appended, or its range, scale or rate changed, is minor; its
|
|
17
|
+
* doc is patch.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
const RANK = { none: 0, patch: 1, minor: 2, major: 3 };
|
|
@@ -55,6 +59,29 @@ function leafChanges(at, a, b, say) {
|
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
function readoutChanges(prev, next, developmentPhase, say) {
|
|
63
|
+
for (const k of new Set([...Object.keys(prev), ...Object.keys(next)])) {
|
|
64
|
+
const a = prev[k]?.readouts ?? [];
|
|
65
|
+
const b = next[k]?.readouts ?? [];
|
|
66
|
+
a.forEach((x, i) => {
|
|
67
|
+
const at = `${k}.readouts.${x.name}`;
|
|
68
|
+
const j = b.findIndex((y) => y.name === x.name);
|
|
69
|
+
if (j < 0) {
|
|
70
|
+
say(developmentPhase ? 'minor' : 'major', at, `readout removed or renamed${developmentPhase ? ' (MINOR in the development phase)' : ''}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const y = b[j];
|
|
74
|
+
if (j !== i) say('major', at, `readout moved (id ${i} -> ${j})`);
|
|
75
|
+
if (x.unit !== y.unit) say('major', at, `unit ${JSON.stringify(x.unit)} -> ${JSON.stringify(y.unit)}`);
|
|
76
|
+
if (x.min !== y.min || x.max !== y.max) say('minor', at, `range [${x.min}, ${x.max}] -> [${y.min}, ${y.max}]`);
|
|
77
|
+
if (x.scale !== y.scale) say('minor', at, `scale ${x.scale} -> ${y.scale}`);
|
|
78
|
+
if (x.rateHz !== y.rateHz) say('minor', at, `rateHz ${x.rateHz} -> ${y.rateHz}`);
|
|
79
|
+
if (x.doc !== y.doc) say('patch', at, 'doc');
|
|
80
|
+
});
|
|
81
|
+
for (const y of b) if (!a.some((x) => x.name === y.name)) say('minor', `${k}.readouts.${y.name}`, 'readout added');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
58
85
|
/** `{ level, changes: [{ level, item, what }] }` from `prev` to `next` (Maps name -> resolved item). */
|
|
59
86
|
export function classify(prev, next, { developmentPhase = DEVELOPMENT_PHASE } = {}) {
|
|
60
87
|
const changes = [];
|
|
@@ -107,6 +134,7 @@ export function classify(prev, next, { developmentPhase = DEVELOPMENT_PHASE } =
|
|
|
107
134
|
}
|
|
108
135
|
}
|
|
109
136
|
for (const name of next.keys()) if (!prev.has(name)) changes.push({ level: 'minor', item: name, what: 'item added' });
|
|
137
|
+
readoutChanges(prev.kernels ?? {}, next.kernels ?? {}, developmentPhase, (level, item, what) => changes.push({ level, item, what }));
|
|
110
138
|
const level = changes.reduce((m, c) => (RANK[c.level] > RANK[m] ? c.level : m), 'none');
|
|
111
139
|
return { level, changes };
|
|
112
140
|
}
|
package/lib/validate.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { readFileSync } from 'node:fs';
|
|
9
9
|
import { posix } from 'node:path';
|
|
10
10
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
11
|
-
import { KERNEL_SECTIONS, controlGlobal, entriesOf, isKernelFile, isUse, resolveData } from './data.mjs';
|
|
11
|
+
import { KERNEL_KEYS, KERNEL_SECTIONS, controlGlobal, entriesOf, isKernelFile, isUse, resolveData } from './data.mjs';
|
|
12
12
|
import { eqDefaultBands, eqDefaultParams } from './eq-defaults.mjs';
|
|
13
13
|
import { renderC } from '../render/c.mjs';
|
|
14
14
|
import { renderTs } from '../render/ts.mjs';
|
|
@@ -21,18 +21,28 @@ let compiled;
|
|
|
21
21
|
* `table` declared under `tables`; `rearms` a boolean, and only on a control; a use (a control with a `global` and no
|
|
22
22
|
* travel or ids) naming an item another control of the kernel declares, of its kind; `count` an item name; `of` naming
|
|
23
23
|
* another control of its kind with `when` a { control, is } of a choice of the kernel; an aggregate's fields naming
|
|
24
|
-
* controls; a constant a scalar, a list or a sheet.
|
|
24
|
+
* controls; a constant a scalar, a list or a sheet; `readouts` a list of readouts, unique by name.
|
|
25
25
|
* A kernel file written as a flat item map (the 1.x shape) is refused as a whole. Any other data file returns [].
|
|
26
26
|
*/
|
|
27
27
|
export function shapeProblems(rel, doc) {
|
|
28
28
|
if (!isKernelFile(rel)) return [];
|
|
29
29
|
const sections = KERNEL_SECTIONS;
|
|
30
30
|
const keys = Object.keys(doc).filter((k) => k !== '$schema');
|
|
31
|
-
if (keys.length && !keys.some((k) =>
|
|
31
|
+
if (keys.length && !keys.some((k) => KERNEL_KEYS.includes(k))) {
|
|
32
32
|
return [`${rel} is a flat item map (the 1.x shape): a kernel file declares its items under ${sections.join(', ')}`];
|
|
33
33
|
}
|
|
34
34
|
const out = [];
|
|
35
|
-
for (const k of keys) if (!
|
|
35
|
+
for (const k of keys) if (!KERNEL_KEYS.includes(k)) out.push(`unknown top-level key ${k}: a kernel file holds ${KERNEL_KEYS.join(', ')}`);
|
|
36
|
+
if ('readouts' in doc) {
|
|
37
|
+
if (!Array.isArray(doc.readouts)) out.push('readouts must be a list');
|
|
38
|
+
else {
|
|
39
|
+
const names = new Set();
|
|
40
|
+
for (const [i, ro] of doc.readouts.entries()) {
|
|
41
|
+
if (names.has(ro?.name)) out.push(`readouts[${i}]: name ${ro.name} declared twice`);
|
|
42
|
+
names.add(ro?.name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
36
46
|
const controls = doc.controls ?? [];
|
|
37
47
|
if (!Array.isArray(controls)) return [...out, 'controls must be a list'];
|
|
38
48
|
const seen = new Set();
|
|
@@ -73,12 +83,22 @@ export function shapeProblems(rel, doc) {
|
|
|
73
83
|
return out;
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
let ajv;
|
|
76
87
|
/** The compiled schema validator. */
|
|
77
88
|
export function schemaValidator() {
|
|
78
|
-
if (!compiled)
|
|
89
|
+
if (!compiled) {
|
|
90
|
+
ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false, allowUnionTypes: true });
|
|
91
|
+
compiled = ajv.compile(SCHEMA);
|
|
92
|
+
}
|
|
79
93
|
return compiled;
|
|
80
94
|
}
|
|
81
95
|
|
|
96
|
+
/** The validator of one readout, the schema's `$defs/readout`. */
|
|
97
|
+
export function readoutValidator() {
|
|
98
|
+
schemaValidator();
|
|
99
|
+
return ajv.getSchema(`${SCHEMA.$id}#/$defs/readout`);
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
const constCase = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
83
103
|
|
|
84
104
|
/**
|
|
@@ -99,6 +119,21 @@ function semanticRules(resolved, say) {
|
|
|
99
119
|
}
|
|
100
120
|
}
|
|
101
121
|
|
|
122
|
+
// A readout's numbers are finite, its range runs upward, its rate is positive and its scale is a PARAM_SCALES id.
|
|
123
|
+
for (const [k, { readouts }] of Object.entries(resolved.kernels ?? {})) {
|
|
124
|
+
for (const ro of readouts ?? []) {
|
|
125
|
+
const rel = `data/kernels/${k}.json`;
|
|
126
|
+
const at = `${k}.readouts.${ro.name}`;
|
|
127
|
+
for (const f of ['min', 'max', 'rateHz']) if (typeof ro[f] !== 'number' || !Number.isFinite(ro[f])) say(rel, at, 'finite', `${at}.${f} resolves to ${JSON.stringify(ro[f])}`);
|
|
128
|
+
if (!(ro.min < ro.max)) say(rel, at, 'readout-range', `${at}: min ${ro.min} is not below max ${ro.max}`);
|
|
129
|
+
if (!(ro.rateHz > 0)) say(rel, at, 'readout-rate', `${at}: rateHz ${ro.rateHz} is not positive`);
|
|
130
|
+
if (!(val('PARAM_SCALES') ?? []).includes(ro.scale)) say(rel, at, 'scale', `${at}: scale ${JSON.stringify(ro.scale)} is not an id of PARAM_SCALES`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (resolved.has('READOUTS') && Object.values(resolved.kernels ?? {}).some((k) => k.readouts)) {
|
|
134
|
+
say(resolved.get('READOUTS').rel, 'READOUTS', 'readouts-name', 'READOUTS is the ts render\'s export of the readouts: no item may take the name');
|
|
135
|
+
}
|
|
136
|
+
|
|
102
137
|
// A per-band control's count: a positive integer scalar, or a sheet whose every variant has a positive integer max.
|
|
103
138
|
for (const [k, { controls }] of Object.entries(resolved.kernels ?? {})) {
|
|
104
139
|
for (const c of controls) {
|
|
@@ -162,6 +197,13 @@ export function validateData(data) {
|
|
|
162
197
|
for (const m of shapeProblems(f.rel, f.doc)) say(f.rel, '-', 'shape', m);
|
|
163
198
|
// The schema reads items: a kernel file is checked as the items it expands to, so no rule is lost.
|
|
164
199
|
const items = { $schema: f.doc.$schema, ...Object.fromEntries(entriesOf(f.rel, f.doc)) };
|
|
200
|
+
if (isKernelFile(f.rel) && Array.isArray(f.doc.readouts)) {
|
|
201
|
+
const readout = readoutValidator();
|
|
202
|
+
for (const [i, ro] of f.doc.readouts.entries()) {
|
|
203
|
+
if (readout(ro)) continue;
|
|
204
|
+
for (const e of readout.errors) say(f.rel, `readouts[${i}]`, 'schema', `/readouts/${i}${e.instancePath} ${e.message}${e.params?.allowedValues ? ` (${e.params.allowedValues.join(', ')})` : ''}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
165
207
|
if (!validate(items)) {
|
|
166
208
|
for (const e of validate.errors) {
|
|
167
209
|
const item = e.instancePath.split('/')[1] || '-';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmixer/omx-contract",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "The FreeMixer declarations as data: a JSON Schema, the data, and one CLI that renders them into C, TypeScript and JSON",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
package/render/c.mjs
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* declaration, by reference to the items here.
|
|
8
8
|
*/
|
|
9
9
|
import {
|
|
10
|
-
genCoreLimitBlock, genDeclaredListsBlock, genDeclaredRates, genDeclaredScalarsBlock, genLimitBlock, renderContractLimits,
|
|
10
|
+
cStringLiteral, genCoreLimitBlock, genDeclaredListsBlock, genDeclaredRates, genDeclaredScalarsBlock, genLimitBlock, renderContractLimits,
|
|
11
|
+
scalarLiteral,
|
|
11
12
|
} from './c/contract-limits-render.mjs';
|
|
12
13
|
import { doors } from './doors.mjs';
|
|
13
14
|
|
|
@@ -55,6 +56,29 @@ function genDeclaredSetsEnums(sets) {
|
|
|
55
56
|
}).join('\n');
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/** A display bound: a float literal, a whole number with its decimal point. */
|
|
60
|
+
const floatLiteral = (n) => `${Number.isInteger(n) ? `${n}.0` : n}f`;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every kernel's readouts (docs/readouts.md), kernels in file order: an enum of the readouts, each at its position,
|
|
64
|
+
* `OMX_<KERNEL>_READOUT_COUNT`, and per readout `_MIN`, `_MAX`, `_RATE_HZ`, `_SCALE` and `_UNIT`.
|
|
65
|
+
*/
|
|
66
|
+
function genReadouts(kernels) {
|
|
67
|
+
return Object.entries(kernels ?? {}).filter(([, k]) => k.readouts?.length).map(([kernel, { readouts }]) => {
|
|
68
|
+
const K = kernel.toUpperCase();
|
|
69
|
+
const names = readouts.map((ro) => `OMX_${K}_READOUT_${constCase(ro.name)}`);
|
|
70
|
+
const lines = [`enum omx_${kernel}_readouts {`, ...names.map((n, i) => ` ${n} = ${i},`), '};', `#define OMX_${K}_READOUT_COUNT ${readouts.length}u`];
|
|
71
|
+
readouts.forEach((ro, i) => lines.push(
|
|
72
|
+
`#define ${names[i]}_MIN ${floatLiteral(ro.min)}`,
|
|
73
|
+
`#define ${names[i]}_MAX ${floatLiteral(ro.max)}`,
|
|
74
|
+
`#define ${names[i]}_RATE_HZ ${scalarLiteral(ro.rateHz)}`,
|
|
75
|
+
`#define ${names[i]}_SCALE OMX_PARAM_SCALES_${constCase(ro.scale)}`,
|
|
76
|
+
`#define ${names[i]}_UNIT ${cStringLiteral(ro.unit)}`,
|
|
77
|
+
));
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}).join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
/**
|
|
59
83
|
* The default band centres of an EQ (lib/eq-defaults.mjs is the rule; this is its C spelling, held
|
|
60
84
|
* equal to it for every count by test/eq-defaults.test.mjs). Writes \`count\` centres to \`out\` and returns
|
|
@@ -110,6 +134,7 @@ export function renderLimitsHeader(resolved) {
|
|
|
110
134
|
const scalars = d.DECLARED_SCALARS.length ? genDeclaredScalarsBlock(d.DECLARED_SCALARS, above) : '';
|
|
111
135
|
const lists = d.DECLARED_LISTS.length ? genDeclaredListsBlock(d.DECLARED_LISTS, defined([tables, aliases, scalars].join('\n'))) : '';
|
|
112
136
|
const sets = d.DECLARED_SET_ITEMS.length ? genDeclaredSetsEnums(d.DECLARED_SET_ITEMS) : '';
|
|
137
|
+
const readouts = genReadouts(resolved.kernels);
|
|
113
138
|
const centres = resolved.has('EQ_DEFAULT_CENTRES_FOUR_BAND_HZ') ? eqCentresFunction() : '';
|
|
114
139
|
const sections = [
|
|
115
140
|
[tables, `/* Per-parameter numeric travel, one block per \`travels\` table named <PREFIX>_LIMITS:
|
|
@@ -121,6 +146,9 @@ export function renderLimitsHeader(resolved) {
|
|
|
121
146
|
[lists, `/* Every list rendered to C, sorted by list: OMX_<LIST>_COUNT and OMX_<LIST>_INIT { a, b, … }. */`],
|
|
122
147
|
[sets, `/* Every set rendered to C, sorted by set: an enum of the ids (a named id's position, or a numeric id's number), \`OMX_<SET>_COUNT\` and,
|
|
123
148
|
* where the set names a default, \`OMX_<SET>_DEFAULT\`. */`],
|
|
149
|
+
[readouts, `/* Every kernel's readouts, the meters a host draws (docs/readouts.md): an enum of the readouts at their position,
|
|
150
|
+
* \`OMX_<KERNEL>_READOUT_COUNT\`, and per readout its display range (_MIN, _MAX), _RATE_HZ, _SCALE (an OMX_PARAM_SCALES_*
|
|
151
|
+
* member) and _UNIT. */`],
|
|
124
152
|
[centres, `/* The default bands of an EQ with \`count\` bands: THE one rule, for every strip, host and count (omx-contract's
|
|
125
153
|
* lib/eq-defaults.mjs; the ts render exports the same as eqDefaultCentres and eqDefaultTypes). Needs -lm. */`],
|
|
126
154
|
].filter(([body]) => body);
|
package/render/json.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* The `json` target: the resolved data (every reference and derivation evaluated) for readers in
|
|
5
5
|
* other languages, which need no resolver of their own. Beside the items, `kernels` lists each
|
|
6
6
|
* kernel's controls in their declared order (name, kind, the global each resolves to, the table of a
|
|
7
|
-
* table field, `rearms` when changing it re-arms the kernel's state),
|
|
7
|
+
* table field, `rearms` when changing it re-arms the kernel's state) and, where it declares any, its readouts, so a
|
|
8
|
+
* reader needs no kernel file.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
/** Every file of the `json` target: `[{ path, text }]`. */
|
package/render/ts.mjs
CHANGED
|
@@ -69,11 +69,26 @@ export declare function eqDefaultBands(count: number): { type: string; freqHz: n
|
|
|
69
69
|
return { js, dts };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Every kernel's readouts as one export, READOUTS: `{ <kernel>: [{ name, doc, unit, min, max, scale, rateHz }] }`,
|
|
74
|
+
* kernels in file order, absent when no kernel declares one.
|
|
75
|
+
*/
|
|
76
|
+
function readoutsExport(resolved) {
|
|
77
|
+
const value = Object.fromEntries(Object.entries(resolved.kernels ?? {}).filter(([, k]) => k.readouts?.length).map(([n, k]) => [n, k.readouts]));
|
|
78
|
+
if (!Object.keys(value).length) return { js: '', dts: '' };
|
|
79
|
+
const doc = '/**\n * Every kernel\'s readouts, the meters a host draws, each kernel\'s in its declared order: the position is the\n * readout\'s id. Every reference is resolved.\n * @see docs/readouts.md\n */';
|
|
80
|
+
return {
|
|
81
|
+
js: `\n${doc}\nexport const READOUTS = ${JSON.stringify(value, null, 2)};\n`,
|
|
82
|
+
dts: `\n${doc}\nexport declare const READOUTS: ${tsType(value)};\n`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
/** Every file of the `ts` target: `[{ path, text }]`. */
|
|
73
87
|
export function renderTs(resolved) {
|
|
74
88
|
const items = [...resolved.values()];
|
|
75
89
|
const rule = eqRule(resolved);
|
|
76
|
-
const
|
|
77
|
-
const
|
|
90
|
+
const readouts = readoutsExport(resolved);
|
|
91
|
+
const js = BANNER + items.map((r) => `\n${docComment(r)}\nexport const ${r.name} = ${JSON.stringify(tsValue(r), null, 2)};\n`).join('') + readouts.js + rule.js;
|
|
92
|
+
const dts = BANNER + items.map((r) => `\n${docComment(r)}\nexport declare const ${r.name}: ${tsType(tsValue(r))};\n`).join('') + readouts.dts + rule.dts;
|
|
78
93
|
return [{ path: 'omx-contract.js', text: js }, { path: 'omx-contract.d.ts', text: dts }];
|
|
79
94
|
}
|
|
@@ -81,6 +81,21 @@
|
|
|
81
81
|
"required": ["min", "max"],
|
|
82
82
|
"additionalProperties": false
|
|
83
83
|
},
|
|
84
|
+
"readout": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"description": "A value a kernel reports while it runs and a host draws: a meter. Declared in a kernel file's `readouts`, in order; its position is its id (docs/readouts.md).",
|
|
87
|
+
"properties": {
|
|
88
|
+
"name": { "type": "string", "pattern": "^[a-z][A-Za-z0-9]*$" },
|
|
89
|
+
"doc": { "$ref": "#/$defs/text" },
|
|
90
|
+
"unit": { "$ref": "#/$defs/unit" },
|
|
91
|
+
"min": { "$ref": "#/$defs/number" },
|
|
92
|
+
"max": { "$ref": "#/$defs/number" },
|
|
93
|
+
"scale": { "type": "string", "pattern": "^[a-z]+$", "description": "How a host draws the readout: an id of PARAM_SCALES." },
|
|
94
|
+
"rateHz": { "$ref": "#/$defs/number" }
|
|
95
|
+
},
|
|
96
|
+
"required": ["name", "doc", "unit", "min", "max", "scale", "rateHz"],
|
|
97
|
+
"additionalProperties": false
|
|
98
|
+
},
|
|
84
99
|
"sheetValue": {
|
|
85
100
|
"type": "object",
|
|
86
101
|
"propertyNames": { "pattern": "^[a-z][A-Za-z0-9]*$" },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmixer/omx-contract",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"items": {
|
|
5
5
|
"PAN_RANGE": {
|
|
6
6
|
"rel": "data/kernels/balance.json",
|
|
@@ -299,6 +299,16 @@
|
|
|
299
299
|
"x4"
|
|
300
300
|
]
|
|
301
301
|
},
|
|
302
|
+
"DETECTOR_TOPOLOGY": {
|
|
303
|
+
"rel": "data/kernels/comp.json",
|
|
304
|
+
"kind": "set",
|
|
305
|
+
"doc": "Where the compressor's detector listens: `feedforward` hears the slot's input (or its key), `feedback` hears the slot's own pre-make-up output DYN_FB_LOOP_DELAY_SAMPLES late, through one envelope stage, and a keyed slot stays feed-forward (omx-dsp's OMX_DYN_TOPOLOGY_FEEDFORWARD 0 and _FEEDBACK 1, in that order; the set is named for its control, so C spells them OMX_DETECTOR_TOPOLOGY_*). The console's comp row carries it (`detectorTopology`, openmixer's ssl-bus-compressor spec §3a) and comes up `feedforward`, so a zeroed atom is the compressor every slot runs.",
|
|
306
|
+
"default": "feedforward",
|
|
307
|
+
"value": [
|
|
308
|
+
"feedforward",
|
|
309
|
+
"feedback"
|
|
310
|
+
]
|
|
311
|
+
},
|
|
302
312
|
"PROGRAM_RELEASE": {
|
|
303
313
|
"rel": "data/kernels/comp.json",
|
|
304
314
|
"kind": "sheet",
|
|
@@ -316,6 +326,13 @@
|
|
|
316
326
|
}
|
|
317
327
|
}
|
|
318
328
|
},
|
|
329
|
+
"DYN_FB_LOOP_DELAY_SAMPLES": {
|
|
330
|
+
"rel": "data/kernels/comp.json",
|
|
331
|
+
"kind": "scalar",
|
|
332
|
+
"doc": "The feedback detector's loop delay, in samples: the output y[n] depends on the gain at n, so the detector can only have heard y[n - 1]. Not a latency: the audio is never delayed by it. omx-dsp's feedback state holds exactly this many output samples per leg.",
|
|
333
|
+
"unit": "",
|
|
334
|
+
"value": 1
|
|
335
|
+
},
|
|
319
336
|
"DEESS_FREQ_RANGE": {
|
|
320
337
|
"rel": "data/kernels/deesser.json",
|
|
321
338
|
"kind": "travels",
|
|
@@ -2453,6 +2470,13 @@
|
|
|
2453
2470
|
}
|
|
2454
2471
|
}
|
|
2455
2472
|
},
|
|
2473
|
+
"READOUT_RATE_HZ": {
|
|
2474
|
+
"rel": "data/primitives.json",
|
|
2475
|
+
"kind": "scalar",
|
|
2476
|
+
"doc": "How often a readout's value is published: 50 Hz, one value every 20 ms, the rate a host sends a moving meter at. Every readout reads it as its `rateHz` (docs/readouts.md).",
|
|
2477
|
+
"unit": "Hz",
|
|
2478
|
+
"value": 50
|
|
2479
|
+
},
|
|
2456
2480
|
"STANDARD_SAMPLE_RATES": {
|
|
2457
2481
|
"rel": "data/rates.json",
|
|
2458
2482
|
"kind": "list",
|
|
@@ -2639,6 +2663,22 @@
|
|
|
2639
2663
|
"name": "detectorOversampling",
|
|
2640
2664
|
"kind": "choice",
|
|
2641
2665
|
"global": "DETECTOR_OVERSAMPLINGS"
|
|
2666
|
+
},
|
|
2667
|
+
{
|
|
2668
|
+
"name": "detectorTopology",
|
|
2669
|
+
"kind": "choice",
|
|
2670
|
+
"global": "DETECTOR_TOPOLOGY"
|
|
2671
|
+
}
|
|
2672
|
+
],
|
|
2673
|
+
"readouts": [
|
|
2674
|
+
{
|
|
2675
|
+
"name": "gainReductionDb",
|
|
2676
|
+
"doc": "The compressor's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to COMP_LIMITS.thresholdDb.min: on a signal at or below 0 dBFS the reduction (x - T)(1 - 1/R) never exceeds -T, and T is at least the threshold's minimum.",
|
|
2677
|
+
"unit": "dB",
|
|
2678
|
+
"min": -60,
|
|
2679
|
+
"max": 0,
|
|
2680
|
+
"scale": "linear",
|
|
2681
|
+
"rateHz": 50
|
|
2642
2682
|
}
|
|
2643
2683
|
]
|
|
2644
2684
|
},
|
|
@@ -2684,6 +2724,17 @@
|
|
|
2684
2724
|
"kind": "choice",
|
|
2685
2725
|
"global": "DEESS_MODES"
|
|
2686
2726
|
}
|
|
2727
|
+
],
|
|
2728
|
+
"readouts": [
|
|
2729
|
+
{
|
|
2730
|
+
"name": "gainReductionDb",
|
|
2731
|
+
"doc": "The de-esser's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to DEESS_RANGE_RANGE.min: the de-esser reduces by its range at most.",
|
|
2732
|
+
"unit": "dB",
|
|
2733
|
+
"min": -24,
|
|
2734
|
+
"max": 0,
|
|
2735
|
+
"scale": "linear",
|
|
2736
|
+
"rateHz": 50
|
|
2737
|
+
}
|
|
2687
2738
|
]
|
|
2688
2739
|
},
|
|
2689
2740
|
"delay": {
|
|
@@ -2928,6 +2979,17 @@
|
|
|
2928
2979
|
"global": "GATE_LIMITS",
|
|
2929
2980
|
"table": "GATE_LIMITS"
|
|
2930
2981
|
}
|
|
2982
|
+
],
|
|
2983
|
+
"readouts": [
|
|
2984
|
+
{
|
|
2985
|
+
"name": "gainReductionDb",
|
|
2986
|
+
"doc": "The gate's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to GATE_LIMITS.rangeDb.min: the gate attenuates by its range at most.",
|
|
2987
|
+
"unit": "dB",
|
|
2988
|
+
"min": -90,
|
|
2989
|
+
"max": 0,
|
|
2990
|
+
"scale": "linear",
|
|
2991
|
+
"rateHz": 50
|
|
2992
|
+
}
|
|
2931
2993
|
]
|
|
2932
2994
|
},
|
|
2933
2995
|
"geq": {
|
|
@@ -2970,6 +3032,17 @@
|
|
|
2970
3032
|
"global": "LIMITER_LIMITS",
|
|
2971
3033
|
"table": "LIMITER_LIMITS"
|
|
2972
3034
|
}
|
|
3035
|
+
],
|
|
3036
|
+
"readouts": [
|
|
3037
|
+
{
|
|
3038
|
+
"name": "gainReductionDb",
|
|
3039
|
+
"doc": "The limiter's gain reduction, in dB with CLAP's sign: 0 while it reduces nothing, negative the reduction it applies. The display range runs down to LIMITER_LIMITS.ceilingDb.min: on a signal at or below 0 dBFS the limiter pulls the peak down to its ceiling at most, and the ceiling is at least its minimum.",
|
|
3040
|
+
"unit": "dB",
|
|
3041
|
+
"min": -12,
|
|
3042
|
+
"max": 0,
|
|
3043
|
+
"scale": "linear",
|
|
3044
|
+
"rateHz": 50
|
|
3045
|
+
}
|
|
2973
3046
|
]
|
|
2974
3047
|
},
|
|
2975
3048
|
"mixmatrix": {
|