@openmixer/omx-contract 0.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +53 -5
- package/bin/omx-contract.mjs +175 -0
- package/data/kernels/chorus.json +15 -0
- package/data/kernels/comp.json +81 -0
- package/data/kernels/deesser.json +93 -0
- package/data/kernels/delay.json +66 -0
- package/data/kernels/drive.json +12 -0
- package/data/kernels/eq.json +59 -0
- package/data/kernels/flanger.json +51 -0
- package/data/kernels/gate.json +81 -0
- package/data/kernels/geq.json +33 -0
- package/data/kernels/limiter.json +33 -0
- package/data/kernels/phaser.json +109 -0
- package/data/kernels/pitch.json +35 -0
- package/data/kernels/reverb.json +15 -0
- package/data/kernels/rotary.json +186 -0
- package/data/kernels/transient.json +73 -0
- package/data/kernels/tremolo.json +54 -0
- package/data/primitives.json +47 -0
- package/data/rates.json +30 -0
- package/dist/ts/omx-contract.d.ts +1071 -0
- package/dist/ts/omx-contract.js +1117 -0
- package/include/omxcontract/omx_contract_limits.h +381 -0
- package/lib/data.mjs +159 -0
- package/lib/fmt.mjs +61 -0
- package/lib/semver.mjs +125 -0
- package/lib/validate.mjs +92 -0
- package/package.json +44 -4
- package/render/c/contract-limits-render.mjs +370 -0
- package/render/c/omx_plugin_param.h +25 -0
- package/render/c/params-generate.mjs +83 -0
- package/render/c.mjs +94 -0
- package/render/doors.mjs +63 -0
- package/render/json.mjs +16 -0
- package/render/ts.mjs +41 -0
- package/schema/omx-contract.schema.json +164 -0
- package/share/omx-contract/omx-contract.json +1202 -0
package/README.md
CHANGED
|
@@ -1,7 +1,55 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
|
2
|
+
<!-- Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com> -->
|
|
3
|
+
# omx-contract
|
|
2
4
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
+
The FreeMixer declarations as data: what a value may be (a travel, a default, a kernel constant, a
|
|
6
|
+
list), as JSON validated by a JSON Schema, rendered by one CLI into
|
|
7
|
+
every language that reads it. Governed by openmixer's
|
|
8
|
+
`docs/design/specs/2026-10-05-omx-contract.md`; 1.0.0 carries the phase-1 items of its §4.1, what
|
|
9
|
+
omx-dsp needs, and 1.1.0 adds the flanger, de-esser, phaser, rotary, tremolo and graphic EQ kernels.
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
```
|
|
12
|
+
data/ the declaration, one file per group, edited by hand
|
|
13
|
+
rates.json primitives.json kernels/<kernel>.json
|
|
14
|
+
schema/omx-contract.schema.json JSON Schema 2020-12: five kinds, closed units, refs, closed derivations
|
|
15
|
+
bin/omx-contract.mjs the CLI
|
|
16
|
+
render/ c.mjs (openmixer's renderer, moved unchanged, under c/), ts.mjs, json.mjs
|
|
17
|
+
include/omxcontract/ the committed c render (what omx-contract-devel installs)
|
|
18
|
+
share/omx-contract/ the committed json render (resolved data)
|
|
19
|
+
test/ schema, semantic checks, goldens, perturbation, semver, round trip
|
|
20
|
+
tools/proof/round-trip.mjs the spec §4.4 steps 1 and 2 proof against omx-dsp v0.1.3
|
|
21
|
+
recipes/kernel.recipe.json what a complete kernel has; recipes/answers/ cites each value in the engine
|
|
22
|
+
tools/omx-new-kernel.mjs the kernel wizard and importer; tools/kernel-recipe.mjs is `make completeness`
|
|
23
|
+
tools/seed/ the one-time seed from openmixer's built core (provenance; never run by CI)
|
|
24
|
+
packaging/ debian/ omx-contract-devel (RPM), libomx-contract-dev (deb)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## The CLI
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
npm ci
|
|
31
|
+
node bin/omx-contract.mjs validate # schema + semantic checks
|
|
32
|
+
node bin/omx-contract.mjs fmt [--check] # the one formatting
|
|
33
|
+
node bin/omx-contract.mjs render --target c|ts|json --out <dir> [--check]
|
|
34
|
+
node bin/omx-contract.mjs semver <previous tag> # exit 1 if the version bump is short
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Exit 0 is a pass, 1 a failure naming file, item and rule, 2 a usage or environment error. A change to
|
|
38
|
+
`data/` is followed by `render --target c --out include`, `render --target json --out
|
|
39
|
+
share/omx-contract` and `render --target ts --out test/golden/ts`; CI holds all three to the data.
|
|
40
|
+
|
|
41
|
+
## Consuming it
|
|
42
|
+
|
|
43
|
+
Pin one version (`omx-contract 1.1.0` in `.github/pins.txt`) and build against its renders, committing
|
|
44
|
+
none: the installed `omx-contract-devel` / `libomx-contract-dev` at exactly that version
|
|
45
|
+
(`pkg-config --exact-version=1.1.0 omx-contract`), else the release's npm tarball, unpacked, and
|
|
46
|
+
`omx-contract render --target c --out build/omx-contract/include`. Include
|
|
47
|
+
`<omxcontract/omx_contract_limits.h>`. A plugin's parameter header is rendered by omx-plugins from its
|
|
48
|
+
declaration, by reference to the kernel travels here (since 1.1.0).
|
|
49
|
+
|
|
50
|
+
The npm package is `@openmixer/omx-contract` on npmjs.org (the `ts` render is its main export); every
|
|
51
|
+
GitHub release also carries the `npm pack` tarball, the RPMs and the debs.
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
GPL-3.0-or-later. JSON cannot carry an SPDX comment; every data file is covered by `LICENSE`.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
4
|
+
/**
|
|
5
|
+
* omx-contract — the declarations as data, validated and rendered (omx-contract spec §3.4).
|
|
6
|
+
*
|
|
7
|
+
* omx-contract validate [--data <dir>] schema + semantic checks
|
|
8
|
+
* omx-contract fmt [--check] [--data <dir>] the one formatting
|
|
9
|
+
* omx-contract render --target c|ts|json --out <dir> [--check] [--data <dir> | --sheet <fixture>]
|
|
10
|
+
* omx-contract semver <previous tag> | --from-dir <data dir> --from-version <x.y.z>
|
|
11
|
+
*
|
|
12
|
+
* Exit 0 is a pass, 1 a failure naming what failed, 2 a usage or environment error (never a pass).
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { loadData, packageVersion, resolveData } from '../lib/data.mjs';
|
|
19
|
+
import { formatDoc } from '../lib/fmt.mjs';
|
|
20
|
+
import { classify, refusal } from '../lib/semver.mjs';
|
|
21
|
+
import { validateData } from '../lib/validate.mjs';
|
|
22
|
+
import { renderC, renderSheetC } from '../render/c.mjs';
|
|
23
|
+
import { renderJson } from '../render/json.mjs';
|
|
24
|
+
import { renderTs } from '../render/ts.mjs';
|
|
25
|
+
|
|
26
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
|
|
28
|
+
class Usage extends Error {}
|
|
29
|
+
|
|
30
|
+
function opt(argv, name) {
|
|
31
|
+
const i = argv.indexOf(name);
|
|
32
|
+
if (i < 0) return undefined;
|
|
33
|
+
if (i + 1 >= argv.length || argv[i + 1].startsWith('--')) throw new Usage(`${name} needs a value`);
|
|
34
|
+
return argv[i + 1];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function load(argv) {
|
|
38
|
+
const dir = resolve(opt(argv, '--data') ?? join(ROOT, 'data'));
|
|
39
|
+
if (!existsSync(dir)) throw new Usage(`no data directory ${dir}`);
|
|
40
|
+
return loadData(dir);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Every file under `dir`, relative to it. */
|
|
44
|
+
function filesUnder(dir) {
|
|
45
|
+
if (!existsSync(dir)) return [];
|
|
46
|
+
const out = [];
|
|
47
|
+
const walk = (d) => {
|
|
48
|
+
for (const f of readdirSync(d)) {
|
|
49
|
+
const p = join(d, f);
|
|
50
|
+
if (statSync(p).isDirectory()) walk(p);
|
|
51
|
+
else out.push(relative(dir, p));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
walk(dir);
|
|
55
|
+
return out.sort();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cmdValidate(argv) {
|
|
59
|
+
const data = load(argv);
|
|
60
|
+
const problems = validateData(data);
|
|
61
|
+
for (const p of problems) console.error(`${p.file}: ${p.item}: ${p.rule}: ${p.message}`);
|
|
62
|
+
if (problems.length) return 1;
|
|
63
|
+
console.log(`omx-contract validate: ${data.items.size} items in ${data.files.length} files, valid`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cmdFmt(argv) {
|
|
68
|
+
const dir = resolve(opt(argv, '--data') ?? join(ROOT, 'data'));
|
|
69
|
+
const data = load(argv);
|
|
70
|
+
const check = argv.includes('--check');
|
|
71
|
+
let bad = 0;
|
|
72
|
+
for (const f of data.files) {
|
|
73
|
+
const want = formatDoc(f.doc);
|
|
74
|
+
if (want === f.text) continue;
|
|
75
|
+
if (check) { console.error(`omx-contract fmt --check: ${f.rel} is not in the one formatting`); bad++; } else writeFileSync(join(dir, '..', f.rel), want);
|
|
76
|
+
}
|
|
77
|
+
if (bad) return 1;
|
|
78
|
+
console.log(`omx-contract fmt${check ? ' --check' : ''}: ${data.files.length} files formatted`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function renderTarget(target, argv) {
|
|
83
|
+
const sheet = opt(argv, '--sheet');
|
|
84
|
+
if (sheet) {
|
|
85
|
+
if (target !== 'c') throw new Usage('--sheet renders the c target only');
|
|
86
|
+
return renderSheetC(JSON.parse(readFileSync(resolve(sheet), 'utf8')));
|
|
87
|
+
}
|
|
88
|
+
const data = load(argv);
|
|
89
|
+
const resolved = resolveData(data);
|
|
90
|
+
if (target === 'c') return renderC(resolved);
|
|
91
|
+
if (target === 'ts') return renderTs(resolved);
|
|
92
|
+
if (target === 'json') return renderJson(resolved, packageVersion(ROOT));
|
|
93
|
+
throw new Usage(`unknown target ${target} (c, ts, json)`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function cmdRender(argv) {
|
|
97
|
+
const target = opt(argv, '--target');
|
|
98
|
+
const out = opt(argv, '--out');
|
|
99
|
+
if (!target || !out) throw new Usage('render needs --target <c|ts|json> and --out <dir>');
|
|
100
|
+
const files = renderTarget(target, argv);
|
|
101
|
+
const dir = resolve(out);
|
|
102
|
+
if (argv.includes('--check')) {
|
|
103
|
+
const bad = [];
|
|
104
|
+
for (const f of files) {
|
|
105
|
+
const p = join(dir, f.path);
|
|
106
|
+
if (!existsSync(p)) bad.push(`${f.path} is missing`);
|
|
107
|
+
else if (readFileSync(p, 'utf8') !== f.text) bad.push(`${f.path} differs`);
|
|
108
|
+
}
|
|
109
|
+
const scope = target === 'c' ? 'omxcontract' : '';
|
|
110
|
+
const named = new Set(files.map((f) => f.path));
|
|
111
|
+
for (const rel of filesUnder(join(dir, scope))) if (!named.has(join(scope, rel))) bad.push(`${join(scope, rel)} is extra`);
|
|
112
|
+
for (const b of bad) console.error(`omx-contract render --check: ${out.replace(/\/$/, '')}/${b}`);
|
|
113
|
+
if (bad.length) return 1;
|
|
114
|
+
console.log(`omx-contract render --target ${target} --check: ${files.length} files byte-identical under ${out}`);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
for (const f of files) {
|
|
118
|
+
mkdirSync(dirname(join(dir, f.path)), { recursive: true });
|
|
119
|
+
writeFileSync(join(dir, f.path), f.text);
|
|
120
|
+
}
|
|
121
|
+
console.log(`omx-contract render --target ${target}: wrote ${files.length} files under ${out}`);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The data of a git tag, as `{ rel: text }`. */
|
|
126
|
+
function dataAtTag(tag) {
|
|
127
|
+
const git = (...a) => execFileSync('git', ['-C', ROOT, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
128
|
+
try { git('rev-parse', '--verify', `${tag}^{commit}`); } catch { throw new Usage(`no tag ${tag} in ${ROOT}`); }
|
|
129
|
+
const texts = {};
|
|
130
|
+
for (const rel of git('ls-tree', '-r', '--name-only', tag, '--', 'data').split('\n').filter((p) => p.endsWith('.json'))) texts[rel] = git('show', `${tag}:${rel}`);
|
|
131
|
+
return texts;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function cmdSemver(argv) {
|
|
135
|
+
const fromDir = opt(argv, '--from-dir');
|
|
136
|
+
let prev;
|
|
137
|
+
let from;
|
|
138
|
+
if (fromDir) {
|
|
139
|
+
from = opt(argv, '--from-version');
|
|
140
|
+
if (!from) throw new Usage('--from-dir needs --from-version');
|
|
141
|
+
prev = loadData(resolve(fromDir));
|
|
142
|
+
} else {
|
|
143
|
+
const tag = argv[1];
|
|
144
|
+
if (!tag || tag.startsWith('--')) throw new Usage('semver needs the previous tag');
|
|
145
|
+
from = tag;
|
|
146
|
+
prev = loadData(null, dataAtTag(tag));
|
|
147
|
+
}
|
|
148
|
+
const to = opt(argv, '--version') ?? packageVersion(ROOT);
|
|
149
|
+
const { level, changes } = classify(resolveData(prev), resolveData(load(argv)));
|
|
150
|
+
for (const c of changes) console.log(`${c.level.toUpperCase()} ${c.item}: ${c.what}`);
|
|
151
|
+
const why = refusal(level, from, to);
|
|
152
|
+
console.log(JSON.stringify({ from: from.replace(/^v/, ''), to, level, changes: changes.length, ok: !why }));
|
|
153
|
+
if (why) { console.error(`omx-contract semver: ${why}`); return 1; }
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const USAGE = `usage: omx-contract validate [--data <dir>]
|
|
158
|
+
omx-contract fmt [--check] [--data <dir>]
|
|
159
|
+
omx-contract render --target c|ts|json --out <dir> [--check] [--data <dir> | --sheet <fixture.json>]
|
|
160
|
+
omx-contract semver <previous tag> | --from-dir <data dir> --from-version <x.y.z> [--version <x.y.z>]`;
|
|
161
|
+
|
|
162
|
+
export function main(argv) {
|
|
163
|
+
const cmds = { validate: cmdValidate, fmt: cmdFmt, render: cmdRender, semver: cmdSemver };
|
|
164
|
+
try {
|
|
165
|
+
const cmd = cmds[argv[0]];
|
|
166
|
+
if (!cmd) throw new Usage(argv[0] ? `unknown command ${argv[0]}` : 'no command');
|
|
167
|
+
return cmd(argv);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
if (e instanceof Usage) { console.error(`omx-contract: ${e.message}\n${USAGE}`); return 2; }
|
|
170
|
+
console.error(`omx-contract: ${e.message}`);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/omx-contract.schema.json",
|
|
3
|
+
"CHORUS_SPREAD_RANGE": {
|
|
4
|
+
"kind": "travels",
|
|
5
|
+
"doc": "The chorus's right-leg phase offset, in TURNS of its one oscillator (§2, §3a; the native-FX catalogue's M32 Dimension row). The right leg's voice k reads the LFO at k/N + spread: 0 is both legs one phase (the stage without the field, bit for bit), 0.25 quadrature, 0.5 the Dimension's opposed sweep. A fraction of a cycle carries no engineering symbol, so the unit is ''.",
|
|
6
|
+
"travel": {
|
|
7
|
+
"min": 0,
|
|
8
|
+
"max": 0.5,
|
|
9
|
+
"step": 0.01,
|
|
10
|
+
"unit": "",
|
|
11
|
+
"default": 0,
|
|
12
|
+
"defaultFrom": "desk"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/omx-contract.schema.json",
|
|
3
|
+
"COMP_LIMITS": {
|
|
4
|
+
"kind": "travels",
|
|
5
|
+
"doc": "The legal range of each compressor field (engineering units). The widget clamps drags to these, `normalizeComp` enforces them, the row's codec refuses by them and the travel sheet publishes them. `default`/`defaultFrom` are the come-up values `defaultCompState` seeds a fresh strip with, read BACK from here rather than restated there.",
|
|
6
|
+
"fields": {
|
|
7
|
+
"thresholdDb": {
|
|
8
|
+
"min": -60,
|
|
9
|
+
"max": 0,
|
|
10
|
+
"step": 1,
|
|
11
|
+
"unit": "dB",
|
|
12
|
+
"default": -18,
|
|
13
|
+
"defaultFrom": "desk"
|
|
14
|
+
},
|
|
15
|
+
"ratio": {
|
|
16
|
+
"min": 1,
|
|
17
|
+
"max": 20,
|
|
18
|
+
"step": 0.5,
|
|
19
|
+
"unit": "",
|
|
20
|
+
"default": 4,
|
|
21
|
+
"defaultFrom": "desk"
|
|
22
|
+
},
|
|
23
|
+
"kneeDb": {
|
|
24
|
+
"min": 0,
|
|
25
|
+
"max": 24,
|
|
26
|
+
"step": 0.5,
|
|
27
|
+
"unit": "dB",
|
|
28
|
+
"default": 6,
|
|
29
|
+
"defaultFrom": "desk"
|
|
30
|
+
},
|
|
31
|
+
"attackMs": {
|
|
32
|
+
"min": 0.1,
|
|
33
|
+
"max": 100,
|
|
34
|
+
"step": 1,
|
|
35
|
+
"unit": "ms",
|
|
36
|
+
"default": 5,
|
|
37
|
+
"defaultFrom": "desk"
|
|
38
|
+
},
|
|
39
|
+
"releaseMs": {
|
|
40
|
+
"min": 5,
|
|
41
|
+
"max": 3000,
|
|
42
|
+
"step": 10,
|
|
43
|
+
"unit": "ms",
|
|
44
|
+
"default": 200,
|
|
45
|
+
"defaultFrom": "desk"
|
|
46
|
+
},
|
|
47
|
+
"makeupDb": {
|
|
48
|
+
"min": 0,
|
|
49
|
+
"max": 24,
|
|
50
|
+
"step": 0.5,
|
|
51
|
+
"unit": "dB",
|
|
52
|
+
"default": 0,
|
|
53
|
+
"defaultFrom": "desk"
|
|
54
|
+
},
|
|
55
|
+
"mixPct": {
|
|
56
|
+
"min": 0,
|
|
57
|
+
"max": 100,
|
|
58
|
+
"step": 5,
|
|
59
|
+
"unit": "%",
|
|
60
|
+
"default": 100,
|
|
61
|
+
"defaultFrom": "desk"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"PROGRAM_RELEASE": {
|
|
66
|
+
"kind": "sheet",
|
|
67
|
+
"doc": "The program-dependent release's model constants — the LA-2A's opto cell as DATA (`docs/design/specs/2026-09-26-compressor-models.md` §3b). `chargeMs`/`dischargeMs` are the word's memory constants, shared by every profile; each profile's `fastMs`, `slowMinMs`, `slowMaxMs` and `share` fit the unit's published two-stage recovery. Not operator controls: generated into `omx_contract_limits.h` as `OMX_PROGRAM_RELEASE_*`, which libomxdsp's `omx_env_program_release` reads.",
|
|
68
|
+
"value": {
|
|
69
|
+
"chargeMs": 1000,
|
|
70
|
+
"dischargeMs": 5000,
|
|
71
|
+
"profiles": {
|
|
72
|
+
"opto": {
|
|
73
|
+
"fastMs": 70,
|
|
74
|
+
"slowMinMs": 110,
|
|
75
|
+
"slowMaxMs": 1100,
|
|
76
|
+
"share": 0.5
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/omx-contract.schema.json",
|
|
3
|
+
"DEESS_FREQ_RANGE": {
|
|
4
|
+
"kind": "travels",
|
|
5
|
+
"doc": "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).",
|
|
6
|
+
"travel": {
|
|
7
|
+
"min": 2000,
|
|
8
|
+
"max": 16000,
|
|
9
|
+
"step": 10,
|
|
10
|
+
"unit": "Hz",
|
|
11
|
+
"default": 7000,
|
|
12
|
+
"defaultFrom": "desk"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"DEESS_WIDTH_RANGE": {
|
|
16
|
+
"kind": "travels",
|
|
17
|
+
"doc": "The band's WIDTH, in OCTAVES around the centre: the edges are `f / 2^(w/2)` and `f · 2^(w/2)`, which is what makes the centre the geometric middle at every width. Octaves and not a Q, because a Q is a property of one section and this band is TWO (a highpass and a lowpass, `mix_deesser.h`) — an operator asked to dial a Q would be dialling a number neither filter carries. One octave is the classic de-esser band: wide enough for a sibilant that moves with the vowel, narrow enough that a 1 kHz vowel sits 27.8 dB down the skirt.",
|
|
18
|
+
"travel": {
|
|
19
|
+
"min": 0.25,
|
|
20
|
+
"max": 4,
|
|
21
|
+
"step": 0.05,
|
|
22
|
+
"unit": "oct",
|
|
23
|
+
"default": 1,
|
|
24
|
+
"defaultFrom": "desk"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"DEESS_THRESHOLD_RANGE": {
|
|
28
|
+
"kind": "travels",
|
|
29
|
+
"doc": "Where reduction starts — the level of the BAND, not of the strip, which is why the come-up is so much lower than the compressor's. A vocal peaking −12 dBFS carries roughly −30 dBFS in a one-octave band at 7 kHz on its sibilants and under −40 on its vowels, so −30 sits between the two and the stage opens on an \"s\" and stays shut on an \"ah\".",
|
|
30
|
+
"travel": {
|
|
31
|
+
"min": -60,
|
|
32
|
+
"max": 0,
|
|
33
|
+
"step": 0.5,
|
|
34
|
+
"unit": "dB",
|
|
35
|
+
"default": -30,
|
|
36
|
+
"defaultFrom": "desk"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"DEESS_RATIO_RANGE": {
|
|
40
|
+
"kind": "travels",
|
|
41
|
+
"doc": "How hard the band is held once it is over the threshold. 4:1 is the textbook de-esser ratio: firm on the \"s\", short of a limiter.",
|
|
42
|
+
"travel": {
|
|
43
|
+
"min": 1,
|
|
44
|
+
"max": 20,
|
|
45
|
+
"step": 0.1,
|
|
46
|
+
"unit": "",
|
|
47
|
+
"default": 4,
|
|
48
|
+
"defaultFrom": "desk"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"DEESS_RANGE_RANGE": {
|
|
52
|
+
"kind": "travels",
|
|
53
|
+
"doc": "The FLOOR on the reduction, in dB and never positive — the deepest cut the stage may reach however loud the sibilant is (`mix_deesser.h`'s clamp, §4c). 12 dB tames a hard sibilant; past that a voice starts to lisp, and a de-esser with NO floor turns a shouted \"s\" into a hole. The ceiling is 0, which switches the reduction off without switching the stage off — a legitimate thing to want while dialling the band in by ear.",
|
|
54
|
+
"travel": {
|
|
55
|
+
"min": -24,
|
|
56
|
+
"max": 0,
|
|
57
|
+
"step": 0.5,
|
|
58
|
+
"unit": "dB",
|
|
59
|
+
"default": -12,
|
|
60
|
+
"defaultFrom": "desk"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"DEESS_ATTACK_RANGE": {
|
|
64
|
+
"kind": "travels",
|
|
65
|
+
"doc": "The detector's ATTACK, in ms. A sibilant's onset is a few milliseconds; 1 ms catches it without the detector chattering on the band's own waveform.",
|
|
66
|
+
"travel": {
|
|
67
|
+
"min": 0.1,
|
|
68
|
+
"max": 50,
|
|
69
|
+
"step": 0.1,
|
|
70
|
+
"unit": "ms",
|
|
71
|
+
"default": 1,
|
|
72
|
+
"defaultFrom": "desk"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"DEESS_RELEASE_RANGE": {
|
|
76
|
+
"kind": "travels",
|
|
77
|
+
"doc": "The detector's RELEASE, in ms. 60 ms is long enough not to modulate inside one \"s\" and short enough to be open again for the next syllable.",
|
|
78
|
+
"travel": {
|
|
79
|
+
"min": 5,
|
|
80
|
+
"max": 500,
|
|
81
|
+
"step": 1,
|
|
82
|
+
"unit": "ms",
|
|
83
|
+
"default": 60,
|
|
84
|
+
"defaultFrom": "desk"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"DEESS_KNEE_DB": {
|
|
88
|
+
"kind": "scalar",
|
|
89
|
+
"doc": "The knee, in dB — a CONSTANT of the stage and not a row field (`2026-09-22-native-deesser-stage.md` §4b): a de-esser without a soft knee latches audibly on the first sibilant of a phrase, and no other value of this number is a de-esser anyone would dial. The controller pushes it to the node and this is the same number; a second spelling would be a picture that disagrees with the audio about where the reduction starts.",
|
|
90
|
+
"unit": "dB",
|
|
91
|
+
"value": 6
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/omx-contract.schema.json",
|
|
3
|
+
"FX_DELAY_TIME_RANGE": {
|
|
4
|
+
"kind": "travels",
|
|
5
|
+
"doc": "The FX delay's time travel. `max` is the NATIVE ring's ceiling — the same number as `OMX_FXDELAY_MAX_MS` in `omx_delay.h`, pinned to it by `output-delay-ceiling.test.ts`. Distinct from the per-route ALIGNMENT delay (`OUTPUT_DELAY_RANGE`, 1 s, `OMX_DELAY_MAX_MS`): one is a musical echo with feedback and mix, the other is a pure latency. Two facts, two rings, two ceilings — sharing the word \"delay\" is why they keep being confused for each other.",
|
|
6
|
+
"travel": {
|
|
7
|
+
"min": 0,
|
|
8
|
+
"max": 2000,
|
|
9
|
+
"step": 1,
|
|
10
|
+
"unit": "ms",
|
|
11
|
+
"default": 300,
|
|
12
|
+
"defaultFrom": "desk"
|
|
13
|
+
},
|
|
14
|
+
"c": {
|
|
15
|
+
"alias": {
|
|
16
|
+
"fact": "delay",
|
|
17
|
+
"field": "timeMs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"FX_DELAY_FEEDBACK_RANGE": {
|
|
22
|
+
"kind": "travels",
|
|
23
|
+
"doc": "The FX delay's REGENERATION travel — how much of the wet tail feeds back in. Stops short of 1: unity feedback is a runaway the kernel hard-clamps anyway, so offering it would draw travel the DSP refuses. Not NORMALIZED_RANGE for exactly that reason — it is a 0..1 quantity whose top the DSP does not accept, and a control must not offer a value its actuator will eat.",
|
|
24
|
+
"travel": {
|
|
25
|
+
"min": 0,
|
|
26
|
+
"max": 0.99,
|
|
27
|
+
"step": 0,
|
|
28
|
+
"unit": "",
|
|
29
|
+
"default": 0.3,
|
|
30
|
+
"defaultFrom": "desk"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"DELAY_TONE_RANGE": {
|
|
34
|
+
"kind": "travels",
|
|
35
|
+
"doc": "The FX delay's TONE — a NORMALIZED_RANGE instance with its own come-up value, matching `DEFAULT_DELAY_STATE.tone`. See REVERB_SIZE_RANGE for why this is a named constant rather than a default on the shared shape.",
|
|
36
|
+
"travel": {
|
|
37
|
+
"min": 0,
|
|
38
|
+
"max": 1,
|
|
39
|
+
"step": 0,
|
|
40
|
+
"unit": "",
|
|
41
|
+
"default": 0.3,
|
|
42
|
+
"defaultFrom": "desk"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"DELAY_MIX_RANGE": {
|
|
46
|
+
"kind": "travels",
|
|
47
|
+
"doc": "The FX DELAY's wet/dry — the same split as REVERB_MIX_RANGE, its own declaration.",
|
|
48
|
+
"travel": {
|
|
49
|
+
"min": 0,
|
|
50
|
+
"max": 1,
|
|
51
|
+
"step": 0,
|
|
52
|
+
"unit": "",
|
|
53
|
+
"default": 0.3,
|
|
54
|
+
"defaultFrom": "desk",
|
|
55
|
+
"byKind": {
|
|
56
|
+
"fxReturn": 1
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"FX_DELAY_PINGPONG_DEFAULT": {
|
|
61
|
+
"kind": "scalar",
|
|
62
|
+
"doc": "The FX delay's PING-PONG come-up value — a toggle, so it has no travel, only this. Declared here rather than as a literal in the server's `DEFAULT_DELAY_STATE` (which now reads it) so the DPF plugin generator (`packages/omx-plugins/tools/params-gen.mjs`, `2026-09-25-omx-plugins-dpf.md` §3a) reads the same fact the desk seeds a fresh strip with.",
|
|
63
|
+
"unit": "",
|
|
64
|
+
"value": false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/omx-contract.schema.json",
|
|
3
|
+
"DRIVE_BIAS_MAX": {
|
|
4
|
+
"kind": "scalar",
|
|
5
|
+
"doc": "The bias at `character = +1`, in the SHAPER's own input units — `OMX_DRIVE_BIAS_MAX`. DERIVED, not chosen (§4b). In the saturating limit every knee tends to `sgn`, the output is a rectangular wave of duty `d = ½ + asin(b)/π`, and its `n`th harmonic goes as `sin(nπd)/n` — so `|H2|` is maximal at `d = ¾`, which is `b = sin(π/4) = 1/√2` exactly. Measured too: the H2-maximising bias for a full-scale sine at unity drive is 0.712 (soft), 0.658 (tape), 0.645 (tube), and this one value costs 0.000 / 0.035 / 0.088 dB of H2 against each curve's own optimum. ONE normalised bias for all three knees, not three.",
|
|
6
|
+
"unit": "",
|
|
7
|
+
"value": {
|
|
8
|
+
"derive": "sqrt",
|
|
9
|
+
"of": 0.5
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/omx-contract.schema.json",
|
|
3
|
+
"EQ_MAX_BANDS": {
|
|
4
|
+
"kind": "scalar",
|
|
5
|
+
"doc": "The maximum number of parametric bands a channel EQ may hold — the operator can add up to this many. Generated into `omx_contract_limits.h` as `OMX_EQ_MAX_BANDS`, which `omx_biquad.h` reads (`output-delay-ceiling.test.ts` reads the header): the native lane allocates exactly this many biquad slots per channel and silently clamps above it, so the model, the wire, and the DSP agree by test, not by memory.",
|
|
6
|
+
"unit": "",
|
|
7
|
+
"value": 24
|
|
8
|
+
},
|
|
9
|
+
"ISO_THIRD_OCTAVE_CENTRES_HZ": {
|
|
10
|
+
"kind": "list",
|
|
11
|
+
"doc": "The standard ISO 266 one-third-octave centre frequencies, 20 Hz–20 kHz (31 bands) — the classic 31-band graphic-EQ layout's centres.",
|
|
12
|
+
"unit": "Hz",
|
|
13
|
+
"values": [
|
|
14
|
+
20,
|
|
15
|
+
25,
|
|
16
|
+
31.5,
|
|
17
|
+
40,
|
|
18
|
+
50,
|
|
19
|
+
63,
|
|
20
|
+
80,
|
|
21
|
+
100,
|
|
22
|
+
125,
|
|
23
|
+
160,
|
|
24
|
+
200,
|
|
25
|
+
250,
|
|
26
|
+
315,
|
|
27
|
+
400,
|
|
28
|
+
500,
|
|
29
|
+
630,
|
|
30
|
+
800,
|
|
31
|
+
1000,
|
|
32
|
+
1250,
|
|
33
|
+
1600,
|
|
34
|
+
2000,
|
|
35
|
+
2500,
|
|
36
|
+
3150,
|
|
37
|
+
4000,
|
|
38
|
+
5000,
|
|
39
|
+
6300,
|
|
40
|
+
8000,
|
|
41
|
+
10000,
|
|
42
|
+
12500,
|
|
43
|
+
16000,
|
|
44
|
+
20000
|
|
45
|
+
],
|
|
46
|
+
"c": {
|
|
47
|
+
"render": false
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"GEQ_BANDS": {
|
|
51
|
+
"kind": "scalar",
|
|
52
|
+
"doc": "The graphic EQ's section count: DERIVED from ISO_THIRD_OCTAVE_CENTRES_HZ, never a second literal. Generated into `omx_contract_limits.h` as `OMX_GEQ_BANDS`, which sizes `mix_geq.h`'s section array — its OWN budget, never a claimant on EQ_MAX_BANDS (graphic-eq-31 spec §3, roster ruling 3).",
|
|
53
|
+
"unit": "",
|
|
54
|
+
"value": {
|
|
55
|
+
"derive": "count",
|
|
56
|
+
"of": "ISO_THIRD_OCTAVE_CENTRES_HZ"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schema/omx-contract.schema.json",
|
|
3
|
+
"FLANGER_RATE_RANGE": {
|
|
4
|
+
"kind": "travels",
|
|
5
|
+
"doc": "The flanger's jet sweep (§3b).",
|
|
6
|
+
"travel": {
|
|
7
|
+
"min": 0.05,
|
|
8
|
+
"max": 5,
|
|
9
|
+
"step": 0.01,
|
|
10
|
+
"unit": "Hz",
|
|
11
|
+
"default": 0.25,
|
|
12
|
+
"defaultFrom": "desk"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"FLANGER_DEPTH_RANGE": {
|
|
16
|
+
"kind": "travels",
|
|
17
|
+
"doc": "The flanger's peak-to-peak excursion above its declared 0.5 ms base (§3b).",
|
|
18
|
+
"travel": {
|
|
19
|
+
"min": 0,
|
|
20
|
+
"max": 5,
|
|
21
|
+
"step": 0.1,
|
|
22
|
+
"unit": "ms",
|
|
23
|
+
"default": 2,
|
|
24
|
+
"defaultFrom": "desk"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"FLANGER_FEEDBACK_RANGE": {
|
|
28
|
+
"kind": "travels",
|
|
29
|
+
"doc": "The flanger's SIGNED resonance (§3b). The ±0.95 clamp is DECLARED here — a PATCH asking for 1.0 is answered by this travel — and the kernel's own clamp is the enforcement half.",
|
|
30
|
+
"travel": {
|
|
31
|
+
"min": -0.95,
|
|
32
|
+
"max": 0.95,
|
|
33
|
+
"step": 0.01,
|
|
34
|
+
"unit": "",
|
|
35
|
+
"default": 0.6,
|
|
36
|
+
"defaultFrom": "desk"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"FLANGER_MIX_RANGE": {
|
|
40
|
+
"kind": "travels",
|
|
41
|
+
"doc": "The flanger's wet/dry, as an instance of DRIVE_MIX_RANGE: 50 %, equal dry and wet, where the comb is deepest (§3b).",
|
|
42
|
+
"travel": {
|
|
43
|
+
"min": 0,
|
|
44
|
+
"max": 100,
|
|
45
|
+
"step": 0.1,
|
|
46
|
+
"unit": "%",
|
|
47
|
+
"default": 50,
|
|
48
|
+
"defaultFrom": "desk"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|