@openmixer/omx-contract 2.2.0 → 2.4.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 -118
- package/data/kernels/comp.json +15 -0
- package/data/kernels/deesser.json +15 -0
- package/data/kernels/gate.json +15 -0
- package/data/kernels/limiter.json +15 -0
- package/data/kernels/trim.json +27 -0
- package/data/primitives.json +7 -14
- package/dist/ts/omx-contract.d.ts +78 -15
- package/dist/ts/omx-contract.js +86 -15
- package/include/omxcontract/omx_contract_limits.h +46 -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 +91 -17
package/README.md
CHANGED
|
@@ -2,140 +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).
|
|
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:
|
|
20
17
|
|
|
21
18
|
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
schema/omx-contract.schema.json JSON Schema 2020-12: five kinds (scalar, travels, sheet, list, set), closed units, refs, closed derivations
|
|
25
|
-
lib/eq-defaults.mjs the one default rule for a fresh EQ's bands; the ts and c renders carry it
|
|
26
|
-
bin/omx-contract.mjs the CLI
|
|
27
|
-
render/ c.mjs (openmixer's renderer, moved unchanged, under c/), ts.mjs, json.mjs
|
|
28
|
-
include/omxcontract/ the committed c render (what omx-contract-devel installs)
|
|
29
|
-
share/omx-contract/ the committed json render (resolved data)
|
|
30
|
-
test/ schema, semantic checks, goldens, perturbation, semver, round trip
|
|
31
|
-
tools/proof/round-trip.mjs the spec §4.4 steps 1 and 2 proof against omx-dsp v0.1.3
|
|
32
|
-
recipes/kernel.recipe.json what a complete kernel has; recipes/answers/ cites each value in the engine
|
|
33
|
-
tools/omx-new-kernel.mjs the kernel wizard and importer; tools/kernel-recipe.mjs is `make completeness`
|
|
34
|
-
tools/seed/ the one-time seed from openmixer's built core (provenance; never run by CI)
|
|
35
|
-
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
|
|
36
21
|
```
|
|
37
22
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
"$schema": "../../schema/omx-contract.schema.json",
|
|
46
|
-
"controls": [
|
|
47
|
-
{ "name": "rateHz", "kind": "travel", "global": "TREMOLO_RATE_RANGE", "doc": "…", "travel": { "min": 0.1, "max": 20, "unit": "Hz", "default": 5 } },
|
|
48
|
-
{ "name": "depth", "kind": "travel", "doc": "…", "travel": { "min": 0, "max": 1, "unit": "", "default": 0.5 } },
|
|
49
|
-
{ "name": "mode", "kind": "choice", "doc": "…", "ids": ["tremolo", "pan"], "labels": ["Tremolo", "Pan"] },
|
|
50
|
-
{ "name": "thresholdDb", "kind": "travel", "table": "GATE_LIMITS", "travel": { "min": -80, "max": 0, "unit": "dB" } }
|
|
51
|
-
],
|
|
52
|
-
"tables": { "GATE_LIMITS": { "doc": "…" } },
|
|
53
|
-
"aggregates": { "TREMOLO_TRAVELS": { "doc": "…", "fields": ["rateHz", "depth"] } },
|
|
54
|
-
"constants": { "CHORUS_BASE_MS": { "kind": "scalar", "doc": "…", "unit": "ms", "value": 10 } }
|
|
55
|
-
}
|
|
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
|
|
56
29
|
```
|
|
57
30
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
kernel's state, so it is not a smooth parameter (the limiter's `lookaheadMs`: omx-dsp's limiter face re-arms
|
|
63
|
-
the state and restarts the gain at unity when the look-ahead moves by a frame). It is a boolean, only on a
|
|
64
|
-
control; it is no item, so the C and TypeScript renders do not carry it.
|
|
65
|
-
- A USE is a control with a `global` and no `travel` or `ids`: it takes the item another control of the kernel
|
|
66
|
-
declares, of its kind, and adds no item (the EQ's `lpfSlope` takes FILTER_SLOPES, which `hpfSlope` declares).
|
|
67
|
-
- `count` names the item that counts a control the kernel takes once per band: a positive integer scalar
|
|
68
|
-
(`GEQ_BANDS`), or a sheet whose every variant gives its count as `max` (`EQ_BAND_COUNTS.eq8.max`).
|
|
69
|
-
- `of` and `when` mark a travel that is no control of its own but the travel the control `of` names reaches while
|
|
70
|
-
the choice `when.control` holds `when.is` (the EQ's `notchQ`: the Q's travel while the band is a notch).
|
|
71
|
-
- A choice's `default` may be `{ "ref": … }` to the scalar that states it; a boolean names the second id of a
|
|
72
|
-
two-id switch when true, the first when false (the delay's `pingpong` comes up at FX_DELAY_PINGPONG_DEFAULT).
|
|
73
|
-
- `tables`: a travels table whose fields are the controls that name it, in control order (`GATE_LIMITS`).
|
|
74
|
-
- `aggregates`: a travels table whose fields are references to controls declared once above, listed by name.
|
|
75
|
-
- `constants`: scalars, lists and sheets, as items.
|
|
76
|
-
|
|
77
|
-
`omx-contract validate` refuses any other key, a constant that is a travel or a set, and a kernel file written
|
|
78
|
-
as a flat item map (the 1.x shape). `rates.json` and `primitives.json` stay item maps.
|
|
79
|
-
|
|
80
|
-
## The JSON render's kernels
|
|
81
|
-
|
|
82
|
-
Beside `items`, `share/omx-contract/omx-contract.json` carries `kernels`: for every kernel file, its controls in
|
|
83
|
-
the order the file declares them. A consumer reads control order and kinds here, never from a kernel file.
|
|
84
|
-
|
|
85
|
-
```json
|
|
86
|
-
"kernels": {
|
|
87
|
-
"limiter": {
|
|
88
|
-
"controls": [
|
|
89
|
-
{ "name": "ceilingDb", "kind": "travel", "global": "LIMITER_LIMITS", "table": "LIMITER_LIMITS" },
|
|
90
|
-
{ "name": "lookaheadMs", "kind": "travel", "global": "LIMITER_LIMITS", "table": "LIMITER_LIMITS", "rearms": true },
|
|
91
|
-
{ "name": "releaseMs", "kind": "travel", "global": "LIMITER_LIMITS", "table": "LIMITER_LIMITS" }
|
|
92
|
-
]
|
|
93
|
-
},
|
|
94
|
-
"tremolo": {
|
|
95
|
-
"controls": [
|
|
96
|
-
{ "name": "rateHz", "kind": "travel", "global": "TREMOLO_RATE_RANGE" },
|
|
97
|
-
{ "name": "mode", "kind": "choice", "global": "TREMOLO_MODES" }
|
|
98
|
-
]
|
|
99
|
-
}
|
|
100
|
-
}
|
|
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
|
|
101
35
|
```
|
|
102
36
|
|
|
103
|
-
|
|
104
|
-
- `global`: the item the control's value lives in: `items[global].value` for a travel or a choice, and
|
|
105
|
-
`items[global].value[name]` for a field of a table, when `table` (the same item) is present.
|
|
106
|
-
- `rearms`: present, and `true`, only on a control that declares it.
|
|
107
|
-
- `count`: present on a control taken once per band, the item that counts it.
|
|
108
|
-
- `when`: on a control another one is `of`, the list `{ control, is, global }`: while `control` holds `is`, the
|
|
109
|
-
control's travel is `items[global]` (the EQ's `q` reaches EQ_NOTCH_Q_RANGE on a notch). A control that is `of`
|
|
110
|
-
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.
|
|
111
38
|
|
|
112
|
-
|
|
39
|
+
## Use
|
|
113
40
|
|
|
114
|
-
|
|
41
|
+
Include the C header and read the constants by name:
|
|
115
42
|
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
node bin/omx-contract.mjs validate # schema + semantic checks
|
|
119
|
-
node bin/omx-contract.mjs fmt [--check] # the one formatting
|
|
120
|
-
node bin/omx-contract.mjs render --target c|ts|json --out <dir> [--check]
|
|
121
|
-
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>
|
|
122
45
|
```
|
|
123
46
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
47
|
+
```
|
|
48
|
+
pkg-config --cflags omx-contract
|
|
49
|
+
```
|
|
127
50
|
|
|
128
|
-
|
|
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.4.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).
|
|
129
56
|
|
|
130
|
-
|
|
131
|
-
none: the installed `omx-contract-devel` / `libomx-contract-dev` at exactly that version
|
|
132
|
-
(`pkg-config --exact-version=2.2.0 omx-contract`), else the release's npm tarball, unpacked, and
|
|
133
|
-
`omx-contract render --target c --out build/omx-contract/include`. Include
|
|
134
|
-
`<omxcontract/omx_contract_limits.h>`. A plugin's parameter header is rendered by omx-plugins from its
|
|
135
|
-
declaration, by reference to the kernel travels here (since 1.1.0).
|
|
57
|
+
## Building
|
|
136
58
|
|
|
137
|
-
|
|
138
|
-
GitHub release also carries the `npm pack` tarball, the RPMs and the debs.
|
|
59
|
+
See [BUILDING.md](BUILDING.md).
|
|
139
60
|
|
|
140
61
|
## License
|
|
141
62
|
|
package/data/kernels/comp.json
CHANGED
|
@@ -124,6 +124,21 @@
|
|
|
124
124
|
]
|
|
125
125
|
}
|
|
126
126
|
],
|
|
127
|
+
"readouts": [
|
|
128
|
+
{
|
|
129
|
+
"name": "gainReductionDb",
|
|
130
|
+
"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.",
|
|
131
|
+
"unit": "dB",
|
|
132
|
+
"min": {
|
|
133
|
+
"ref": "COMP_LIMITS.thresholdDb.min"
|
|
134
|
+
},
|
|
135
|
+
"max": 0,
|
|
136
|
+
"scale": "linear",
|
|
137
|
+
"rateHz": {
|
|
138
|
+
"ref": "READOUT_RATE_HZ"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
],
|
|
127
142
|
"tables": {
|
|
128
143
|
"COMP_LIMITS": {
|
|
129
144
|
"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."
|
|
@@ -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."
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/omx-contract.schema.json",
|
|
3
|
+
"controls": [
|
|
4
|
+
{
|
|
5
|
+
"name": "trimDb",
|
|
6
|
+
"kind": "travel",
|
|
7
|
+
"global": "TRIM_RANGE",
|
|
8
|
+
"doc": "The digital input trim, in dB: the console's fine head gain, applied first in a channel's lane as a click-free ramp from the last block's gain to this one (openmixer's TRIM_RANGE, -24..+24 dB in 0.1 dB steps, spelled minDb/maxDb/stepDb there). It comes up at 0 dB, the trim a channel has when none is set. omx-dsp's trim face takes it as `trim_db`.",
|
|
9
|
+
"travel": {
|
|
10
|
+
"min": -24,
|
|
11
|
+
"max": 24,
|
|
12
|
+
"step": 0.1,
|
|
13
|
+
"unit": "dB",
|
|
14
|
+
"default": 0,
|
|
15
|
+
"defaultFrom": "desk"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"aggregates": {
|
|
20
|
+
"TRIM_TRAVELS": {
|
|
21
|
+
"doc": "The trim as a table with the one field `trimDb`, the name the trim row and the omx-strip plugin store; generated from the trim kernel's control, so the travel has one home.",
|
|
22
|
+
"fields": [
|
|
23
|
+
"trimDb"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/data/primitives.json
CHANGED
|
@@ -104,20 +104,6 @@
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
|
-
"TRIM_TRAVELS": {
|
|
108
|
-
"kind": "travels",
|
|
109
|
-
"doc": "The digital input trim as a travel: openmixer's TRIM_RANGE (packages/declarations, -24..+24 dB in 0.1 dB steps, spelled minDb/maxDb/stepDb there and OMX_TRIM_RANGE_*_DB in omx-dsp's header), coming up at 0 dB, the trim a channel has when none is set. The omx-strip plugin's trim reads it. A table with the one field `trimDb`, the name the trim row and the plugin store; not rendered to C, which already spells TRIM_RANGE its own way.",
|
|
110
|
-
"fields": {
|
|
111
|
-
"trimDb": {
|
|
112
|
-
"min": -24,
|
|
113
|
-
"max": 24,
|
|
114
|
-
"step": 0.1,
|
|
115
|
-
"unit": "dB",
|
|
116
|
-
"default": 0,
|
|
117
|
-
"defaultFrom": "desk"
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
107
|
"PARAM_SCALES": {
|
|
122
108
|
"kind": "set",
|
|
123
109
|
"since": "1.3.0",
|
|
@@ -168,5 +154,12 @@
|
|
|
168
154
|
"scale": "log"
|
|
169
155
|
}
|
|
170
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
|
|
171
164
|
}
|
|
172
165
|
}
|
|
@@ -1860,6 +1860,34 @@ export declare const TREMOLO_TRAVELS: {
|
|
|
1860
1860
|
};
|
|
1861
1861
|
};
|
|
1862
1862
|
|
|
1863
|
+
/**
|
|
1864
|
+
* The digital input trim, in dB: the console's fine head gain, applied first in a channel's lane as a click-free ramp from the last block's gain to this one (openmixer's TRIM_RANGE, -24..+24 dB in 0.1 dB steps, spelled minDb/maxDb/stepDb there). It comes up at 0 dB, the trim a channel has when none is set. omx-dsp's trim face takes it as `trim_db`.
|
|
1865
|
+
* @see data/kernels/trim.json
|
|
1866
|
+
*/
|
|
1867
|
+
export declare const TRIM_RANGE: {
|
|
1868
|
+
readonly min: -24;
|
|
1869
|
+
readonly max: 24;
|
|
1870
|
+
readonly step: 0.1;
|
|
1871
|
+
readonly unit: "dB";
|
|
1872
|
+
readonly default: 0;
|
|
1873
|
+
readonly defaultFrom: "desk";
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* The trim as a table with the one field `trimDb`, the name the trim row and the omx-strip plugin store; generated from the trim kernel's control, so the travel has one home.
|
|
1878
|
+
* @see data/kernels/trim.json
|
|
1879
|
+
*/
|
|
1880
|
+
export declare const TRIM_TRAVELS: {
|
|
1881
|
+
readonly trimDb: {
|
|
1882
|
+
readonly min: -24;
|
|
1883
|
+
readonly max: 24;
|
|
1884
|
+
readonly step: 0.1;
|
|
1885
|
+
readonly unit: "dB";
|
|
1886
|
+
readonly default: 0;
|
|
1887
|
+
readonly defaultFrom: "desk";
|
|
1888
|
+
};
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1863
1891
|
/**
|
|
1864
1892
|
* The Q of a second-order Butterworth section, 1/√2 — maximally flat, the one value every Butterworth biquad in the tree designs at: the LR4 crossover section, the pitch pre-filter and the drive's tilt / band / HF sections, in TS and (as `OMX_BUTTERWORTH_Q`) in C (§7, the scalar door).
|
|
1865
1893
|
* @see data/primitives.json
|
|
@@ -1958,21 +1986,6 @@ export declare const COMP_CONTINUOUS_TRAVELS: {
|
|
|
1958
1986
|
};
|
|
1959
1987
|
};
|
|
1960
1988
|
|
|
1961
|
-
/**
|
|
1962
|
-
* The digital input trim as a travel: openmixer's TRIM_RANGE (packages/declarations, -24..+24 dB in 0.1 dB steps, spelled minDb/maxDb/stepDb there and OMX_TRIM_RANGE_*_DB in omx-dsp's header), coming up at 0 dB, the trim a channel has when none is set. The omx-strip plugin's trim reads it. A table with the one field `trimDb`, the name the trim row and the plugin store; not rendered to C, which already spells TRIM_RANGE its own way.
|
|
1963
|
-
* @see data/primitives.json
|
|
1964
|
-
*/
|
|
1965
|
-
export declare const TRIM_TRAVELS: {
|
|
1966
|
-
readonly trimDb: {
|
|
1967
|
-
readonly min: -24;
|
|
1968
|
-
readonly max: 24;
|
|
1969
|
-
readonly step: 0.1;
|
|
1970
|
-
readonly unit: "dB";
|
|
1971
|
-
readonly default: 0;
|
|
1972
|
-
readonly defaultFrom: "desk";
|
|
1973
|
-
};
|
|
1974
|
-
};
|
|
1975
|
-
|
|
1976
1989
|
/**
|
|
1977
1990
|
* How a host draws a parameter: linear, logarithmic (a frequency, a time) or in discrete steps. The words omx-plugins' declarations and plugin-hostd's protocol use; a travel names its own with `scale`.
|
|
1978
1991
|
* @see data/primitives.json
|
|
@@ -2007,6 +2020,12 @@ export declare const EQ_CONTINUOUS_TRAVELS: {
|
|
|
2007
2020
|
};
|
|
2008
2021
|
};
|
|
2009
2022
|
|
|
2023
|
+
/**
|
|
2024
|
+
* 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).
|
|
2025
|
+
* @see data/primitives.json
|
|
2026
|
+
*/
|
|
2027
|
+
export declare const READOUT_RATE_HZ: 50;
|
|
2028
|
+
|
|
2010
2029
|
/**
|
|
2011
2030
|
* 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.
|
|
2012
2031
|
* @see data/rates.json
|
|
@@ -2040,6 +2059,50 @@ export declare const ORACLE_FLOOR_RATES: readonly [44100, 48000, 96000, 192000];
|
|
|
2040
2059
|
*/
|
|
2041
2060
|
export declare const RME_RATES: readonly [32000, 44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000];
|
|
2042
2061
|
|
|
2062
|
+
/**
|
|
2063
|
+
* Every kernel's readouts, the meters a host draws, each kernel's in its declared order: the position is the
|
|
2064
|
+
* readout's id. Every reference is resolved.
|
|
2065
|
+
* @see docs/readouts.md
|
|
2066
|
+
*/
|
|
2067
|
+
export declare const READOUTS: {
|
|
2068
|
+
readonly comp: readonly [{
|
|
2069
|
+
readonly name: "gainReductionDb";
|
|
2070
|
+
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.";
|
|
2071
|
+
readonly unit: "dB";
|
|
2072
|
+
readonly min: -60;
|
|
2073
|
+
readonly max: 0;
|
|
2074
|
+
readonly scale: "linear";
|
|
2075
|
+
readonly rateHz: 50;
|
|
2076
|
+
}];
|
|
2077
|
+
readonly deesser: readonly [{
|
|
2078
|
+
readonly name: "gainReductionDb";
|
|
2079
|
+
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.";
|
|
2080
|
+
readonly unit: "dB";
|
|
2081
|
+
readonly min: -24;
|
|
2082
|
+
readonly max: 0;
|
|
2083
|
+
readonly scale: "linear";
|
|
2084
|
+
readonly rateHz: 50;
|
|
2085
|
+
}];
|
|
2086
|
+
readonly gate: readonly [{
|
|
2087
|
+
readonly name: "gainReductionDb";
|
|
2088
|
+
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.";
|
|
2089
|
+
readonly unit: "dB";
|
|
2090
|
+
readonly min: -90;
|
|
2091
|
+
readonly max: 0;
|
|
2092
|
+
readonly scale: "linear";
|
|
2093
|
+
readonly rateHz: 50;
|
|
2094
|
+
}];
|
|
2095
|
+
readonly limiter: readonly [{
|
|
2096
|
+
readonly name: "gainReductionDb";
|
|
2097
|
+
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.";
|
|
2098
|
+
readonly unit: "dB";
|
|
2099
|
+
readonly min: -12;
|
|
2100
|
+
readonly max: 0;
|
|
2101
|
+
readonly scale: "linear";
|
|
2102
|
+
readonly rateHz: 50;
|
|
2103
|
+
}];
|
|
2104
|
+
};
|
|
2105
|
+
|
|
2043
2106
|
/** The default band centres (Hz) of an EQ with `count` bands: THE one rule, for every strip, host and count. */
|
|
2044
2107
|
export declare function eqDefaultCentres(count: number): number[];
|
|
2045
2108
|
/** 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
|
@@ -2037,6 +2037,34 @@ export const TREMOLO_TRAVELS = {
|
|
|
2037
2037
|
}
|
|
2038
2038
|
};
|
|
2039
2039
|
|
|
2040
|
+
/**
|
|
2041
|
+
* The digital input trim, in dB: the console's fine head gain, applied first in a channel's lane as a click-free ramp from the last block's gain to this one (openmixer's TRIM_RANGE, -24..+24 dB in 0.1 dB steps, spelled minDb/maxDb/stepDb there). It comes up at 0 dB, the trim a channel has when none is set. omx-dsp's trim face takes it as `trim_db`.
|
|
2042
|
+
* @see data/kernels/trim.json
|
|
2043
|
+
*/
|
|
2044
|
+
export const TRIM_RANGE = {
|
|
2045
|
+
"min": -24,
|
|
2046
|
+
"max": 24,
|
|
2047
|
+
"step": 0.1,
|
|
2048
|
+
"unit": "dB",
|
|
2049
|
+
"default": 0,
|
|
2050
|
+
"defaultFrom": "desk"
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* The trim as a table with the one field `trimDb`, the name the trim row and the omx-strip plugin store; generated from the trim kernel's control, so the travel has one home.
|
|
2055
|
+
* @see data/kernels/trim.json
|
|
2056
|
+
*/
|
|
2057
|
+
export const TRIM_TRAVELS = {
|
|
2058
|
+
"trimDb": {
|
|
2059
|
+
"min": -24,
|
|
2060
|
+
"max": 24,
|
|
2061
|
+
"step": 0.1,
|
|
2062
|
+
"unit": "dB",
|
|
2063
|
+
"default": 0,
|
|
2064
|
+
"defaultFrom": "desk"
|
|
2065
|
+
}
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2040
2068
|
/**
|
|
2041
2069
|
* The Q of a second-order Butterworth section, 1/√2 — maximally flat, the one value every Butterworth biquad in the tree designs at: the LR4 crossover section, the pitch pre-filter and the drive's tilt / band / HF sections, in TS and (as `OMX_BUTTERWORTH_Q`) in C (§7, the scalar door).
|
|
2042
2070
|
* @see data/primitives.json
|
|
@@ -2135,21 +2163,6 @@ export const COMP_CONTINUOUS_TRAVELS = {
|
|
|
2135
2163
|
}
|
|
2136
2164
|
};
|
|
2137
2165
|
|
|
2138
|
-
/**
|
|
2139
|
-
* The digital input trim as a travel: openmixer's TRIM_RANGE (packages/declarations, -24..+24 dB in 0.1 dB steps, spelled minDb/maxDb/stepDb there and OMX_TRIM_RANGE_*_DB in omx-dsp's header), coming up at 0 dB, the trim a channel has when none is set. The omx-strip plugin's trim reads it. A table with the one field `trimDb`, the name the trim row and the plugin store; not rendered to C, which already spells TRIM_RANGE its own way.
|
|
2140
|
-
* @see data/primitives.json
|
|
2141
|
-
*/
|
|
2142
|
-
export const TRIM_TRAVELS = {
|
|
2143
|
-
"trimDb": {
|
|
2144
|
-
"min": -24,
|
|
2145
|
-
"max": 24,
|
|
2146
|
-
"step": 0.1,
|
|
2147
|
-
"unit": "dB",
|
|
2148
|
-
"default": 0,
|
|
2149
|
-
"defaultFrom": "desk"
|
|
2150
|
-
}
|
|
2151
|
-
};
|
|
2152
|
-
|
|
2153
2166
|
/**
|
|
2154
2167
|
* How a host draws a parameter: linear, logarithmic (a frequency, a time) or in discrete steps. The words omx-plugins' declarations and plugin-hostd's protocol use; a travel names its own with `scale`.
|
|
2155
2168
|
* @see data/primitives.json
|
|
@@ -2188,6 +2201,12 @@ export const EQ_CONTINUOUS_TRAVELS = {
|
|
|
2188
2201
|
}
|
|
2189
2202
|
};
|
|
2190
2203
|
|
|
2204
|
+
/**
|
|
2205
|
+
* 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).
|
|
2206
|
+
* @see data/primitives.json
|
|
2207
|
+
*/
|
|
2208
|
+
export const READOUT_RATE_HZ = 50;
|
|
2209
|
+
|
|
2191
2210
|
/**
|
|
2192
2211
|
* 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.
|
|
2193
2212
|
* @see data/rates.json
|
|
@@ -2243,6 +2262,58 @@ export const RME_RATES = [
|
|
|
2243
2262
|
192000
|
|
2244
2263
|
];
|
|
2245
2264
|
|
|
2265
|
+
/**
|
|
2266
|
+
* Every kernel's readouts, the meters a host draws, each kernel's in its declared order: the position is the
|
|
2267
|
+
* readout's id. Every reference is resolved.
|
|
2268
|
+
* @see docs/readouts.md
|
|
2269
|
+
*/
|
|
2270
|
+
export const READOUTS = {
|
|
2271
|
+
"comp": [
|
|
2272
|
+
{
|
|
2273
|
+
"name": "gainReductionDb",
|
|
2274
|
+
"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.",
|
|
2275
|
+
"unit": "dB",
|
|
2276
|
+
"min": -60,
|
|
2277
|
+
"max": 0,
|
|
2278
|
+
"scale": "linear",
|
|
2279
|
+
"rateHz": 50
|
|
2280
|
+
}
|
|
2281
|
+
],
|
|
2282
|
+
"deesser": [
|
|
2283
|
+
{
|
|
2284
|
+
"name": "gainReductionDb",
|
|
2285
|
+
"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.",
|
|
2286
|
+
"unit": "dB",
|
|
2287
|
+
"min": -24,
|
|
2288
|
+
"max": 0,
|
|
2289
|
+
"scale": "linear",
|
|
2290
|
+
"rateHz": 50
|
|
2291
|
+
}
|
|
2292
|
+
],
|
|
2293
|
+
"gate": [
|
|
2294
|
+
{
|
|
2295
|
+
"name": "gainReductionDb",
|
|
2296
|
+
"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.",
|
|
2297
|
+
"unit": "dB",
|
|
2298
|
+
"min": -90,
|
|
2299
|
+
"max": 0,
|
|
2300
|
+
"scale": "linear",
|
|
2301
|
+
"rateHz": 50
|
|
2302
|
+
}
|
|
2303
|
+
],
|
|
2304
|
+
"limiter": [
|
|
2305
|
+
{
|
|
2306
|
+
"name": "gainReductionDb",
|
|
2307
|
+
"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.",
|
|
2308
|
+
"unit": "dB",
|
|
2309
|
+
"min": -12,
|
|
2310
|
+
"max": 0,
|
|
2311
|
+
"scale": "linear",
|
|
2312
|
+
"rateHz": 50
|
|
2313
|
+
}
|
|
2314
|
+
]
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2246
2317
|
/** The declared parameters of the default EQ rule: the four-band set, the preferred series, the band frequency travel, the band gain and Q. */
|
|
2247
2318
|
const EQ_DEFAULT_RULE = {
|
|
2248
2319
|
"four": [
|
|
@@ -450,6 +450,7 @@
|
|
|
450
450
|
#define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SLOW_MAX_MS 1100
|
|
451
451
|
#define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SLOW_MIN_MS 110
|
|
452
452
|
#define OMX_PSHR_MIN_DB 10
|
|
453
|
+
#define OMX_READOUT_RATE_HZ 50
|
|
453
454
|
#define OMX_REVERB_DAMPING_RANGE_DEFAULT 0.5f
|
|
454
455
|
#define OMX_REVERB_DAMPING_RANGE_DEFAULT_DOUBLE 0.5
|
|
455
456
|
#define OMX_REVERB_DAMPING_RANGE_MAX 1
|
|
@@ -591,6 +592,11 @@
|
|
|
591
592
|
#define OMX_TREMOLO_RATE_RANGE_MIN_DOUBLE 0.1
|
|
592
593
|
#define OMX_TREMOLO_RATE_RANGE_STEP 0.01f
|
|
593
594
|
#define OMX_TREMOLO_RATE_RANGE_STEP_DOUBLE 0.01
|
|
595
|
+
#define OMX_TRIM_RANGE_DEFAULT 0
|
|
596
|
+
#define OMX_TRIM_RANGE_MAX 24
|
|
597
|
+
#define OMX_TRIM_RANGE_MIN -24
|
|
598
|
+
#define OMX_TRIM_RANGE_STEP 0.1f
|
|
599
|
+
#define OMX_TRIM_RANGE_STEP_DOUBLE 0.1
|
|
594
600
|
#define OMX_XOVER_LR2_SECTION_Q 0.5f
|
|
595
601
|
#define OMX_XOVER_LR2_SECTION_Q_DOUBLE 0.5
|
|
596
602
|
#define OMX_XOVER_LR4_SECTION_Q 0.7071067811865476f
|
|
@@ -745,6 +751,46 @@ enum omx_tremolo_modes {
|
|
|
745
751
|
#define OMX_TREMOLO_MODES_COUNT 2u
|
|
746
752
|
#define OMX_TREMOLO_MODES_DEFAULT OMX_TREMOLO_MODES_TREMOLO
|
|
747
753
|
|
|
754
|
+
/* Every kernel's readouts, the meters a host draws (docs/readouts.md): an enum of the readouts at their position,
|
|
755
|
+
* `OMX_<KERNEL>_READOUT_COUNT`, and per readout its display range (_MIN, _MAX), _RATE_HZ, _SCALE (an OMX_PARAM_SCALES_*
|
|
756
|
+
* member) and _UNIT. */
|
|
757
|
+
enum omx_comp_readouts {
|
|
758
|
+
OMX_COMP_READOUT_GAIN_REDUCTION_DB = 0,
|
|
759
|
+
};
|
|
760
|
+
#define OMX_COMP_READOUT_COUNT 1u
|
|
761
|
+
#define OMX_COMP_READOUT_GAIN_REDUCTION_DB_MIN -60.0f
|
|
762
|
+
#define OMX_COMP_READOUT_GAIN_REDUCTION_DB_MAX 0.0f
|
|
763
|
+
#define OMX_COMP_READOUT_GAIN_REDUCTION_DB_RATE_HZ 50
|
|
764
|
+
#define OMX_COMP_READOUT_GAIN_REDUCTION_DB_SCALE OMX_PARAM_SCALES_LINEAR
|
|
765
|
+
#define OMX_COMP_READOUT_GAIN_REDUCTION_DB_UNIT "dB"
|
|
766
|
+
enum omx_deesser_readouts {
|
|
767
|
+
OMX_DEESSER_READOUT_GAIN_REDUCTION_DB = 0,
|
|
768
|
+
};
|
|
769
|
+
#define OMX_DEESSER_READOUT_COUNT 1u
|
|
770
|
+
#define OMX_DEESSER_READOUT_GAIN_REDUCTION_DB_MIN -24.0f
|
|
771
|
+
#define OMX_DEESSER_READOUT_GAIN_REDUCTION_DB_MAX 0.0f
|
|
772
|
+
#define OMX_DEESSER_READOUT_GAIN_REDUCTION_DB_RATE_HZ 50
|
|
773
|
+
#define OMX_DEESSER_READOUT_GAIN_REDUCTION_DB_SCALE OMX_PARAM_SCALES_LINEAR
|
|
774
|
+
#define OMX_DEESSER_READOUT_GAIN_REDUCTION_DB_UNIT "dB"
|
|
775
|
+
enum omx_gate_readouts {
|
|
776
|
+
OMX_GATE_READOUT_GAIN_REDUCTION_DB = 0,
|
|
777
|
+
};
|
|
778
|
+
#define OMX_GATE_READOUT_COUNT 1u
|
|
779
|
+
#define OMX_GATE_READOUT_GAIN_REDUCTION_DB_MIN -90.0f
|
|
780
|
+
#define OMX_GATE_READOUT_GAIN_REDUCTION_DB_MAX 0.0f
|
|
781
|
+
#define OMX_GATE_READOUT_GAIN_REDUCTION_DB_RATE_HZ 50
|
|
782
|
+
#define OMX_GATE_READOUT_GAIN_REDUCTION_DB_SCALE OMX_PARAM_SCALES_LINEAR
|
|
783
|
+
#define OMX_GATE_READOUT_GAIN_REDUCTION_DB_UNIT "dB"
|
|
784
|
+
enum omx_limiter_readouts {
|
|
785
|
+
OMX_LIMITER_READOUT_GAIN_REDUCTION_DB = 0,
|
|
786
|
+
};
|
|
787
|
+
#define OMX_LIMITER_READOUT_COUNT 1u
|
|
788
|
+
#define OMX_LIMITER_READOUT_GAIN_REDUCTION_DB_MIN -12.0f
|
|
789
|
+
#define OMX_LIMITER_READOUT_GAIN_REDUCTION_DB_MAX 0.0f
|
|
790
|
+
#define OMX_LIMITER_READOUT_GAIN_REDUCTION_DB_RATE_HZ 50
|
|
791
|
+
#define OMX_LIMITER_READOUT_GAIN_REDUCTION_DB_SCALE OMX_PARAM_SCALES_LINEAR
|
|
792
|
+
#define OMX_LIMITER_READOUT_GAIN_REDUCTION_DB_UNIT "dB"
|
|
793
|
+
|
|
748
794
|
/* The default bands of an EQ with `count` bands: THE one rule, for every strip, host and count (omx-contract's
|
|
749
795
|
* lib/eq-defaults.mjs; the ts render exports the same as eqDefaultCentres and eqDefaultTypes). Needs -lm. */
|
|
750
796
|
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.4.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.4.0",
|
|
4
4
|
"items": {
|
|
5
5
|
"PAN_RANGE": {
|
|
6
6
|
"rel": "data/kernels/balance.json",
|
|
@@ -2280,6 +2280,36 @@
|
|
|
2280
2280
|
}
|
|
2281
2281
|
}
|
|
2282
2282
|
},
|
|
2283
|
+
"TRIM_RANGE": {
|
|
2284
|
+
"rel": "data/kernels/trim.json",
|
|
2285
|
+
"kind": "travels",
|
|
2286
|
+
"doc": "The digital input trim, in dB: the console's fine head gain, applied first in a channel's lane as a click-free ramp from the last block's gain to this one (openmixer's TRIM_RANGE, -24..+24 dB in 0.1 dB steps, spelled minDb/maxDb/stepDb there). It comes up at 0 dB, the trim a channel has when none is set. omx-dsp's trim face takes it as `trim_db`.",
|
|
2287
|
+
"shape": "travel",
|
|
2288
|
+
"value": {
|
|
2289
|
+
"min": -24,
|
|
2290
|
+
"max": 24,
|
|
2291
|
+
"step": 0.1,
|
|
2292
|
+
"unit": "dB",
|
|
2293
|
+
"default": 0,
|
|
2294
|
+
"defaultFrom": "desk"
|
|
2295
|
+
}
|
|
2296
|
+
},
|
|
2297
|
+
"TRIM_TRAVELS": {
|
|
2298
|
+
"rel": "data/kernels/trim.json",
|
|
2299
|
+
"kind": "travels",
|
|
2300
|
+
"doc": "The trim as a table with the one field `trimDb`, the name the trim row and the omx-strip plugin store; generated from the trim kernel's control, so the travel has one home.",
|
|
2301
|
+
"shape": "table",
|
|
2302
|
+
"value": {
|
|
2303
|
+
"trimDb": {
|
|
2304
|
+
"min": -24,
|
|
2305
|
+
"max": 24,
|
|
2306
|
+
"step": 0.1,
|
|
2307
|
+
"unit": "dB",
|
|
2308
|
+
"default": 0,
|
|
2309
|
+
"defaultFrom": "desk"
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
},
|
|
2283
2313
|
"BUTTERWORTH_Q": {
|
|
2284
2314
|
"rel": "data/primitives.json",
|
|
2285
2315
|
"kind": "scalar",
|
|
@@ -2383,22 +2413,6 @@
|
|
|
2383
2413
|
}
|
|
2384
2414
|
}
|
|
2385
2415
|
},
|
|
2386
|
-
"TRIM_TRAVELS": {
|
|
2387
|
-
"rel": "data/primitives.json",
|
|
2388
|
-
"kind": "travels",
|
|
2389
|
-
"doc": "The digital input trim as a travel: openmixer's TRIM_RANGE (packages/declarations, -24..+24 dB in 0.1 dB steps, spelled minDb/maxDb/stepDb there and OMX_TRIM_RANGE_*_DB in omx-dsp's header), coming up at 0 dB, the trim a channel has when none is set. The omx-strip plugin's trim reads it. A table with the one field `trimDb`, the name the trim row and the plugin store; not rendered to C, which already spells TRIM_RANGE its own way.",
|
|
2390
|
-
"shape": "table",
|
|
2391
|
-
"value": {
|
|
2392
|
-
"trimDb": {
|
|
2393
|
-
"min": -24,
|
|
2394
|
-
"max": 24,
|
|
2395
|
-
"step": 0.1,
|
|
2396
|
-
"unit": "dB",
|
|
2397
|
-
"default": 0,
|
|
2398
|
-
"defaultFrom": "desk"
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
},
|
|
2402
2416
|
"PARAM_SCALES": {
|
|
2403
2417
|
"rel": "data/primitives.json",
|
|
2404
2418
|
"kind": "set",
|
|
@@ -2439,6 +2453,13 @@
|
|
|
2439
2453
|
}
|
|
2440
2454
|
}
|
|
2441
2455
|
},
|
|
2456
|
+
"READOUT_RATE_HZ": {
|
|
2457
|
+
"rel": "data/primitives.json",
|
|
2458
|
+
"kind": "scalar",
|
|
2459
|
+
"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).",
|
|
2460
|
+
"unit": "Hz",
|
|
2461
|
+
"value": 50
|
|
2462
|
+
},
|
|
2442
2463
|
"STANDARD_SAMPLE_RATES": {
|
|
2443
2464
|
"rel": "data/rates.json",
|
|
2444
2465
|
"kind": "list",
|
|
@@ -2626,6 +2647,17 @@
|
|
|
2626
2647
|
"kind": "choice",
|
|
2627
2648
|
"global": "DETECTOR_OVERSAMPLINGS"
|
|
2628
2649
|
}
|
|
2650
|
+
],
|
|
2651
|
+
"readouts": [
|
|
2652
|
+
{
|
|
2653
|
+
"name": "gainReductionDb",
|
|
2654
|
+
"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.",
|
|
2655
|
+
"unit": "dB",
|
|
2656
|
+
"min": -60,
|
|
2657
|
+
"max": 0,
|
|
2658
|
+
"scale": "linear",
|
|
2659
|
+
"rateHz": 50
|
|
2660
|
+
}
|
|
2629
2661
|
]
|
|
2630
2662
|
},
|
|
2631
2663
|
"deesser": {
|
|
@@ -2670,6 +2702,17 @@
|
|
|
2670
2702
|
"kind": "choice",
|
|
2671
2703
|
"global": "DEESS_MODES"
|
|
2672
2704
|
}
|
|
2705
|
+
],
|
|
2706
|
+
"readouts": [
|
|
2707
|
+
{
|
|
2708
|
+
"name": "gainReductionDb",
|
|
2709
|
+
"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.",
|
|
2710
|
+
"unit": "dB",
|
|
2711
|
+
"min": -24,
|
|
2712
|
+
"max": 0,
|
|
2713
|
+
"scale": "linear",
|
|
2714
|
+
"rateHz": 50
|
|
2715
|
+
}
|
|
2673
2716
|
]
|
|
2674
2717
|
},
|
|
2675
2718
|
"delay": {
|
|
@@ -2914,6 +2957,17 @@
|
|
|
2914
2957
|
"global": "GATE_LIMITS",
|
|
2915
2958
|
"table": "GATE_LIMITS"
|
|
2916
2959
|
}
|
|
2960
|
+
],
|
|
2961
|
+
"readouts": [
|
|
2962
|
+
{
|
|
2963
|
+
"name": "gainReductionDb",
|
|
2964
|
+
"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.",
|
|
2965
|
+
"unit": "dB",
|
|
2966
|
+
"min": -90,
|
|
2967
|
+
"max": 0,
|
|
2968
|
+
"scale": "linear",
|
|
2969
|
+
"rateHz": 50
|
|
2970
|
+
}
|
|
2917
2971
|
]
|
|
2918
2972
|
},
|
|
2919
2973
|
"geq": {
|
|
@@ -2956,6 +3010,17 @@
|
|
|
2956
3010
|
"global": "LIMITER_LIMITS",
|
|
2957
3011
|
"table": "LIMITER_LIMITS"
|
|
2958
3012
|
}
|
|
3013
|
+
],
|
|
3014
|
+
"readouts": [
|
|
3015
|
+
{
|
|
3016
|
+
"name": "gainReductionDb",
|
|
3017
|
+
"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.",
|
|
3018
|
+
"unit": "dB",
|
|
3019
|
+
"min": -12,
|
|
3020
|
+
"max": 0,
|
|
3021
|
+
"scale": "linear",
|
|
3022
|
+
"rateHz": 50
|
|
3023
|
+
}
|
|
2959
3024
|
]
|
|
2960
3025
|
},
|
|
2961
3026
|
"mixmatrix": {
|
|
@@ -3190,6 +3255,15 @@
|
|
|
3190
3255
|
"global": "TREMOLO_MODES"
|
|
3191
3256
|
}
|
|
3192
3257
|
]
|
|
3258
|
+
},
|
|
3259
|
+
"trim": {
|
|
3260
|
+
"controls": [
|
|
3261
|
+
{
|
|
3262
|
+
"name": "trimDb",
|
|
3263
|
+
"kind": "travel",
|
|
3264
|
+
"global": "TRIM_RANGE"
|
|
3265
|
+
}
|
|
3266
|
+
]
|
|
3193
3267
|
}
|
|
3194
3268
|
}
|
|
3195
3269
|
}
|