@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/lib/semver.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* `omx-contract semver`: classify every change between two resolved declarations (spec §3.6) and
|
|
5
|
+
* refuse a version whose bump is shorter than the largest class found.
|
|
6
|
+
*
|
|
7
|
+
* major a field or alias removed; an item removed (MINOR in the development phase, below); a
|
|
8
|
+
* unit or kind changed; a travel narrowed; a constant a kernel computes with changed (a
|
|
9
|
+
* scalar, sheet leaf or derived value that is not a travel bound or default; a list member
|
|
10
|
+
* changed or removed)
|
|
11
|
+
* minor an item, field, sheet leaf or list member added (appended); a travel widened;
|
|
12
|
+
* a default moved inside its travel; a step changed
|
|
13
|
+
* patch doc and why text
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const RANK = { none: 0, patch: 1, minor: 2, major: 3 };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The development-phase rule (openmixer spec 2026-10-05-omx-contract §3.6): while every consumer of
|
|
20
|
+
* omx-contract is a FreeMixer repository named here, removing an item is MINOR. The rule ends, and
|
|
21
|
+
* this becomes null so a removal is MAJOR again, in the release that names the first consumer
|
|
22
|
+
* outside the list.
|
|
23
|
+
*/
|
|
24
|
+
export const DEVELOPMENT_PHASE = Object.freeze({ consumers: Object.freeze(['omx-dsp', 'omx-plugins', 'openmixer']) });
|
|
25
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
26
|
+
|
|
27
|
+
function travelChanges(at, a, b, say) {
|
|
28
|
+
if (b.min > a.min) say('major', `${at}: travel narrowed (min ${a.min} -> ${b.min})`);
|
|
29
|
+
if (b.max < a.max) say('major', `${at}: travel narrowed (max ${a.max} -> ${b.max})`);
|
|
30
|
+
if (b.min < a.min || b.max > a.max) say('minor', `${at}: travel widened ([${a.min}, ${a.max}] -> [${b.min}, ${b.max}])`);
|
|
31
|
+
if (a.unit !== b.unit) say('major', `${at}: unit ${JSON.stringify(a.unit)} -> ${JSON.stringify(b.unit)}`);
|
|
32
|
+
if (a.default !== b.default) say(a.default === undefined || b.default === undefined ? 'major' : 'minor', `${at}: default ${a.default} -> ${b.default}`);
|
|
33
|
+
if (a.step !== b.step) say('minor', `${at}: step ${a.step} -> ${b.step}`);
|
|
34
|
+
if (a.defaultFrom !== b.defaultFrom) say('minor', `${at}: defaultFrom ${a.defaultFrom} -> ${b.defaultFrom}`);
|
|
35
|
+
const ka = a.byKind ?? {};
|
|
36
|
+
const kb = b.byKind ?? {};
|
|
37
|
+
for (const k of Object.keys(ka)) if (!(k in kb)) say('major', `${at}.byKind.${k} removed`);
|
|
38
|
+
for (const k of Object.keys(kb)) {
|
|
39
|
+
if (!(k in ka)) say('minor', `${at}.byKind.${k} added`);
|
|
40
|
+
else if (ka[k] !== kb[k]) say('minor', `${at}.byKind.${k}: ${ka[k]} -> ${kb[k]}`);
|
|
41
|
+
}
|
|
42
|
+
if ((a.doc ?? '') !== (b.doc ?? '') || (a.why ?? '') !== (b.why ?? '')) say('patch', `${at}: doc`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function leafChanges(at, a, b, say) {
|
|
46
|
+
if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
|
|
47
|
+
for (const k of Object.keys(a)) {
|
|
48
|
+
if (!(k in b)) say('major', `${at}.${k} removed`);
|
|
49
|
+
else leafChanges(`${at}.${k}`, a[k], b[k], say);
|
|
50
|
+
}
|
|
51
|
+
for (const k of Object.keys(b)) if (!(k in a)) say('minor', `${at}.${k} added`);
|
|
52
|
+
} else if (a !== b) {
|
|
53
|
+
say('major', `${at}: ${JSON.stringify(a)} -> ${JSON.stringify(b)} (a constant a kernel computes with)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `{ level, changes: [{ level, item, what }] }` from `prev` to `next` (Maps name -> resolved item). */
|
|
58
|
+
export function classify(prev, next, { developmentPhase = DEVELOPMENT_PHASE } = {}) {
|
|
59
|
+
const changes = [];
|
|
60
|
+
for (const [name, a] of prev) {
|
|
61
|
+
const say = (level, what) => changes.push({ level, item: name, what });
|
|
62
|
+
const b = next.get(name);
|
|
63
|
+
if (!b) {
|
|
64
|
+
if (developmentPhase) say('minor', `item removed or renamed (MINOR in the development phase: every consumer is ${developmentPhase.consumers.join(', ')})`);
|
|
65
|
+
else say('major', 'item removed or renamed');
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (a.kind !== b.kind || (a.shape ?? '') !== (b.shape ?? '')) { say('major', `kind ${a.kind} -> ${b.kind}`); continue; }
|
|
69
|
+
if ((a.unit ?? '') !== (b.unit ?? '')) say('major', `unit ${JSON.stringify(a.unit)} -> ${JSON.stringify(b.unit)}`);
|
|
70
|
+
if (a.doc !== b.doc || (a.why ?? '') !== (b.why ?? '')) say('patch', 'doc');
|
|
71
|
+
if (!same(a.c?.alias, b.c?.alias)) say(a.c?.alias ? 'major' : 'minor', a.c?.alias ? 'c.alias removed or renamed' : 'c.alias added');
|
|
72
|
+
if (!same(a.c?.render, b.c?.render)) say(b.c?.render === false ? 'major' : 'minor', 'C render of the list changed');
|
|
73
|
+
switch (a.kind) {
|
|
74
|
+
case 'travels':
|
|
75
|
+
if (a.shape === 'travel') travelChanges(name, a.value, b.value, say);
|
|
76
|
+
else {
|
|
77
|
+
for (const f of Object.keys(a.value)) {
|
|
78
|
+
if (!(f in b.value)) say('major', `${name}.${f} removed`);
|
|
79
|
+
else travelChanges(`${name}.${f}`, a.value[f], b.value[f], say);
|
|
80
|
+
}
|
|
81
|
+
for (const f of Object.keys(b.value)) if (!(f in a.value)) say('minor', `${name}.${f} added`);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case 'scalar':
|
|
85
|
+
case 'sheet':
|
|
86
|
+
leafChanges(name, a.value, b.value, say);
|
|
87
|
+
break;
|
|
88
|
+
case 'list':
|
|
89
|
+
a.value.forEach((v, i) => {
|
|
90
|
+
if (i >= b.value.length) say('major', `${name}[${i}] removed`);
|
|
91
|
+
else if (b.value[i] !== v) say('major', `${name}[${i}]: ${v} -> ${b.value[i]}`);
|
|
92
|
+
});
|
|
93
|
+
if (b.value.length > a.value.length) say('minor', `${name}: ${b.value.length - a.value.length} member(s) appended`);
|
|
94
|
+
break;
|
|
95
|
+
default:
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const name of next.keys()) if (!prev.has(name)) changes.push({ level: 'minor', item: name, what: 'item added' });
|
|
100
|
+
const level = changes.reduce((m, c) => (RANK[c.level] > RANK[m] ? c.level : m), 'none');
|
|
101
|
+
return { level, changes };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parse = (v) => {
|
|
105
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(v);
|
|
106
|
+
if (!m) throw new Error(`not a version: ${v}`);
|
|
107
|
+
return m.slice(1).map(Number);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** The class of the bump from version `from` to `to`: 'major' | 'minor' | 'patch' | 'none'; throws if `to` is lower. */
|
|
111
|
+
export function bumpOf(from, to) {
|
|
112
|
+
const [a, b] = [parse(from), parse(to)];
|
|
113
|
+
for (let i = 0; i < 3; i++) {
|
|
114
|
+
if (b[i] > a[i]) return ['major', 'minor', 'patch'][i];
|
|
115
|
+
if (b[i] < a[i]) throw new Error(`version ${to} is lower than ${from}`);
|
|
116
|
+
}
|
|
117
|
+
return 'none';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** null when bumping `from` -> `to` covers `level`, else the refusal. */
|
|
121
|
+
export function refusal(level, from, to) {
|
|
122
|
+
let bump;
|
|
123
|
+
try { bump = bumpOf(from, to); } catch (e) { return e.message; }
|
|
124
|
+
return RANK[bump] >= RANK[level] ? null : `the changes since ${from} are ${level.toUpperCase()}, and ${from} -> ${to} is a ${bump} bump`;
|
|
125
|
+
}
|
package/lib/validate.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* `omx-contract validate`: the schema, then the checks a schema cannot express (spec §3.3): every
|
|
5
|
+
* reference resolves with no cycle; every default inside its travel; every C name the render spells
|
|
6
|
+
* is unique; an item's `c.alias` names no other item. Each failure names file, item and rule.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { posix } from 'node:path';
|
|
10
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
11
|
+
import { resolveData } from './data.mjs';
|
|
12
|
+
import { renderC } from '../render/c.mjs';
|
|
13
|
+
import { renderTs } from '../render/ts.mjs';
|
|
14
|
+
|
|
15
|
+
const SCHEMA = JSON.parse(readFileSync(new URL('../schema/omx-contract.schema.json', import.meta.url), 'utf8'));
|
|
16
|
+
let compiled;
|
|
17
|
+
/** The compiled schema validator. */
|
|
18
|
+
export function schemaValidator() {
|
|
19
|
+
if (!compiled) compiled = new Ajv2020({ allErrors: true, strict: true, strictRequired: false, allowUnionTypes: true }).compile(SCHEMA);
|
|
20
|
+
return compiled;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const constCase = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
24
|
+
|
|
25
|
+
/** Every problem of the loaded data: `[{ file, item, rule, message }]`; empty is valid. */
|
|
26
|
+
export function validateData(data) {
|
|
27
|
+
const problems = [];
|
|
28
|
+
const say = (file, item, rule, message) => problems.push({ file, item, rule, message });
|
|
29
|
+
const validate = schemaValidator();
|
|
30
|
+
for (const f of data.files) {
|
|
31
|
+
const depth = f.rel.split('/').length - 1;
|
|
32
|
+
const want = posix.join(...Array(depth).fill('..'), 'schema/omx-contract.schema.json');
|
|
33
|
+
if (f.doc.$schema !== want) say(f.rel, '-', 'schema-path', `"$schema" must be "${want}"`);
|
|
34
|
+
if (!validate(f.doc)) {
|
|
35
|
+
for (const e of validate.errors) {
|
|
36
|
+
const item = e.instancePath.split('/')[1] || '-';
|
|
37
|
+
say(f.rel, item, 'schema', `${e.instancePath || '/'} ${e.message}${e.params?.allowedValues ? ` (${e.params.allowedValues.join(', ')})` : ''}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (problems.length) return problems;
|
|
42
|
+
|
|
43
|
+
let resolved;
|
|
44
|
+
try {
|
|
45
|
+
resolved = resolveData(data);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
const name = /^(\w+)/.exec(e.message)?.[1] ?? '-';
|
|
48
|
+
say(data.items.get(name)?.rel ?? '-', name, 'reference', e.message);
|
|
49
|
+
return problems;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const r of resolved.values()) {
|
|
53
|
+
const travels = r.kind === 'travels' ? (r.shape === 'table' ? Object.entries(r.value) : [['', r.value]]) : [];
|
|
54
|
+
for (const [field, t] of travels) {
|
|
55
|
+
const at = field ? `${r.name}.${field}` : r.name;
|
|
56
|
+
for (const k of ['min', 'max', 'step', 'default']) {
|
|
57
|
+
if (t[k] !== undefined && (typeof t[k] !== 'number' || !Number.isFinite(t[k]))) say(r.rel, r.name, 'finite', `${at}.${k} resolves to ${JSON.stringify(t[k])}`);
|
|
58
|
+
}
|
|
59
|
+
if (!(t.min <= t.max)) say(r.rel, r.name, 'travel-order', `${at}: min ${t.min} > max ${t.max}`);
|
|
60
|
+
if (t.step !== undefined && t.step < 0) say(r.rel, r.name, 'step', `${at}: step ${t.step} < 0`);
|
|
61
|
+
if (t.default !== undefined && !(t.default >= t.min && t.default <= t.max)) say(r.rel, r.name, 'default-inside', `${at}: default ${t.default} outside [${t.min}, ${t.max}]`);
|
|
62
|
+
for (const [kind, v] of Object.entries(t.byKind ?? {})) {
|
|
63
|
+
if (!(v >= t.min && v <= t.max)) say(r.rel, r.name, 'default-inside', `${at}.byKind.${kind}: ${v} outside [${t.min}, ${t.max}]`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (r.c?.alias) {
|
|
67
|
+
const spelled = `${r.c.alias.fact.toUpperCase()}_${constCase(r.c.alias.field)}`;
|
|
68
|
+
if (resolved.has(spelled)) say(r.rel, r.name, 'alias', `c.alias spells ${spelled}, which is another item`);
|
|
69
|
+
if (r.shape !== 'travel') say(r.rel, r.name, 'alias', 'c.alias is for a single travel');
|
|
70
|
+
}
|
|
71
|
+
if (typeof r.doc !== 'string' || !r.doc.trim()) say(r.rel, r.name, 'doc', 'every item carries its doc');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const [target, files] of [['c', () => renderC(resolved)], ['ts', () => renderTs(resolved)]]) {
|
|
75
|
+
let out;
|
|
76
|
+
try {
|
|
77
|
+
out = files();
|
|
78
|
+
} catch (e) {
|
|
79
|
+
say('-', '-', `render-${target}`, e.message);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (target !== 'c') continue;
|
|
83
|
+
for (const f of out) {
|
|
84
|
+
const seen = new Map();
|
|
85
|
+
for (const m of f.text.matchAll(/^#define (\w+)/gm)) {
|
|
86
|
+
if (seen.has(m[1])) say('-', m[1], 'c-name-unique', `${f.path} defines ${m[1]} twice`);
|
|
87
|
+
seen.set(m[1], true);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return problems;
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmixer/omx-contract",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "The FreeMixer declarations as data: a JSON Schema, the data, and one CLI that renders them into C, TypeScript and JSON",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "GPL-3.0-or-later",
|
|
6
|
-
"
|
|
7
|
+
"author": "Pau Aliagas <linuxnow@gmail.com>",
|
|
7
8
|
"homepage": "https://github.com/FreeMixer/omx-contract",
|
|
8
|
-
"
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/FreeMixer/omx-contract.git"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"omx-contract": "bin/omx-contract.mjs"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/ts/omx-contract.d.ts",
|
|
19
|
+
"import": "./dist/ts/omx-contract.js"
|
|
20
|
+
},
|
|
21
|
+
"./resolved.json": "./share/omx-contract/omx-contract.json",
|
|
22
|
+
"./schema.json": "./schema/omx-contract.schema.json",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"bin/",
|
|
27
|
+
"lib/",
|
|
28
|
+
"render/",
|
|
29
|
+
"data/",
|
|
30
|
+
"schema/",
|
|
31
|
+
"include/",
|
|
32
|
+
"share/",
|
|
33
|
+
"dist/ts/"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test test/*.test.mjs",
|
|
37
|
+
"prepack": "node bin/omx-contract.mjs render --target ts --out dist/ts"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"ajv": "^8.17.1"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"registry": "https://registry.npmjs.org/"
|
|
48
|
+
}
|
|
9
49
|
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
// Moved unchanged from openmixer harness/contract-limits-render.mjs at 6c6e709f4429 (omx-contract spec
|
|
4
|
+
// §3.4): a frozen export of openmixer's whole sheet renders through it byte for byte (test/round-trip.test.mjs).
|
|
5
|
+
/**
|
|
6
|
+
* The RENDER half of `contract-limits-gen.mjs`: the header text as a pure function of the
|
|
7
|
+
* declaration sheet it is handed. Split out so the declaration-perturbation ratchet (R-094,
|
|
8
|
+
* `packages/server/src/declaration-perturbation-conformance.test.ts`) can render the header over
|
|
9
|
+
* a PERTURBED sheet and see whether every define follows it — the generator itself imports the
|
|
10
|
+
* built modules and calls this, so there is one renderer and no second copy.
|
|
11
|
+
*/
|
|
12
|
+
/** The `coreLimits`-sourced facts with a native (C) meaning — the four F7 names: pan, delay,
|
|
13
|
+
* reverb, drive. Other `coreLimits` facts (eq, fbs, hrp, gateKey, align) have no stage header
|
|
14
|
+
* counterpart to resolve here and are out of this generator's scope. */
|
|
15
|
+
const CORE_STAGE_FACTS = ['pan', 'delay', 'reverb', 'drive'];
|
|
16
|
+
|
|
17
|
+
/** law kebab-case token -> C enum member (`no-gain-added` -> `OMX_LAW_NO_GAIN_ADDED`). */
|
|
18
|
+
function lawEnumName(law) {
|
|
19
|
+
return `OMX_LAW_${law.toUpperCase().replace(/-/g, '_')}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** fact name -> C array name (`dynamics` -> `OMX_STAGE_LAWS_DYNAMICS`). */
|
|
23
|
+
function stageArrayName(fact) {
|
|
24
|
+
return `OMX_STAGE_LAWS_${fact.toUpperCase()}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** a limit-table field name -> a C constant prefix (`thresholdDb` -> `THRESHOLD_DB`). */
|
|
28
|
+
function fieldConstName(field) {
|
|
29
|
+
return field.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function numLiteral(n) {
|
|
33
|
+
// A whole number still gets a decimal point — this header feeds float comparisons in C, and
|
|
34
|
+
// an integer literal compared against a float is a silent promotion, never a typo to chase.
|
|
35
|
+
return Number.isInteger(n) ? `${n}.0` : `${n}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function genLimitBlock(prefix, table) {
|
|
39
|
+
const lines = [];
|
|
40
|
+
for (const [field, limit] of Object.entries(table)) {
|
|
41
|
+
const c = fieldConstName(field);
|
|
42
|
+
lines.push(`#define OMX_${prefix}_${c}_MIN ${numLiteral(limit.min)}f`);
|
|
43
|
+
lines.push(`#define OMX_${prefix}_${c}_MAX ${numLiteral(limit.max)}f`);
|
|
44
|
+
lines.push(`#define OMX_${prefix}_${c}_DEFAULT ${numLiteral(limit.default)}f`);
|
|
45
|
+
}
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Same shape as {@link genLimitBlock}, for a `coreLimits` fact's entry in `CORE_LIMITS` — DEFAULT
|
|
51
|
+
* is emitted only where the travel declares one (`PAN_RANGE`/`DIVERGENCE_RANGE` and the reverb
|
|
52
|
+
* cut corners carry no come-up value; a control's default is a presentation fact, not every
|
|
53
|
+
* `coreLimits` travel's business).
|
|
54
|
+
*/
|
|
55
|
+
function genCoreLimitBlock(CORE_LIMITS, fact) {
|
|
56
|
+
const table = CORE_LIMITS.get(fact);
|
|
57
|
+
if (!table) throw new Error(`contract-limits-gen: CORE_LIMITS has no entry for '${fact}'`);
|
|
58
|
+
const prefix = fact.toUpperCase();
|
|
59
|
+
const lines = [];
|
|
60
|
+
for (const [field, limit] of Object.entries(table)) {
|
|
61
|
+
const c = fieldConstName(field);
|
|
62
|
+
lines.push(`#define OMX_${prefix}_${c}_MIN ${numLiteral(limit.min)}f`);
|
|
63
|
+
lines.push(`#define OMX_${prefix}_${c}_MAX ${numLiteral(limit.max)}f`);
|
|
64
|
+
if (limit.default !== undefined) {
|
|
65
|
+
lines.push(`#define OMX_${prefix}_${c}_DEFAULT ${numLiteral(limit.default)}f`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The CLAP host object's extension ids (plugin-qualify's OMX_CLAP_HOST_EXTENSIONS; clap-hosting-path §5):
|
|
73
|
+
* what the live host provides, pinned so C can never provide less than the verdict promised. A
|
|
74
|
+
* NULL-terminated array of string literals, one per id, in the declared order.
|
|
75
|
+
*/
|
|
76
|
+
function genClapHostExtensionsBlock(OMX_CLAP_HOST_EXTENSIONS) {
|
|
77
|
+
if (!Array.isArray(OMX_CLAP_HOST_EXTENSIONS) || OMX_CLAP_HOST_EXTENSIONS.length === 0) {
|
|
78
|
+
throw new Error('contract-limits-gen: OMX_CLAP_HOST_EXTENSIONS is missing from the sheet');
|
|
79
|
+
}
|
|
80
|
+
for (const id of OMX_CLAP_HOST_EXTENSIONS) {
|
|
81
|
+
if (typeof id !== 'string' || !/^[a-z][a-z0-9.\/-]*$/.test(id)) throw new Error(`contract-limits-gen: OMX_CLAP_HOST_EXTENSIONS carries a non-id: ${JSON.stringify(id)}`);
|
|
82
|
+
}
|
|
83
|
+
return [
|
|
84
|
+
`#define OMX_CLAP_HOST_EXTENSION_COUNT ${OMX_CLAP_HOST_EXTENSIONS.length}u`,
|
|
85
|
+
`#define OMX_CLAP_HOST_EXTENSIONS_INIT { ${OMX_CLAP_HOST_EXTENSIONS.map((id) => JSON.stringify(id)).join(', ')}, NULL }`,
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The declared sample rates (core's `STANDARD_SAMPLE_RATES`, row-codecs.ts) as a C array the
|
|
91
|
+
* contracts' `rate-is-declared` PRE reads through `omx_rate_is_declared` — never a literal list.
|
|
92
|
+
*/
|
|
93
|
+
function genDeclaredRates(STANDARD_SAMPLE_RATES) {
|
|
94
|
+
if (!Array.isArray(STANDARD_SAMPLE_RATES) || STANDARD_SAMPLE_RATES.length === 0) {
|
|
95
|
+
throw new Error('contract-limits-gen: STANDARD_SAMPLE_RATES is missing from the sheet');
|
|
96
|
+
}
|
|
97
|
+
for (const r of STANDARD_SAMPLE_RATES) {
|
|
98
|
+
if (!Number.isInteger(r) || r <= 0) throw new Error(`contract-limits-gen: a declared rate must be a positive integer, got ${r}`);
|
|
99
|
+
}
|
|
100
|
+
return [
|
|
101
|
+
`#define OMX_DECLARED_RATE_COUNT ${STANDARD_SAMPLE_RATES.length}u`,
|
|
102
|
+
`static const float OMX_DECLARED_RATES[OMX_DECLARED_RATE_COUNT] = { ${STANDARD_SAMPLE_RATES.map((r) => `${numLiteral(r)}f`).join(', ')} };`,
|
|
103
|
+
].join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The scalar door's literal law (dsp-primitives §7): an integer value is an integer literal, so it
|
|
108
|
+
* can size a ring or count; any other number is a float literal. A C reader dividing two
|
|
109
|
+
* integer-valued declarations promotes one itself — this generator knows no units.
|
|
110
|
+
*/
|
|
111
|
+
export function scalarLiteral(n) {
|
|
112
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) throw new Error(`contract-limits-gen: a declared scalar must be a finite number, got ${n}`);
|
|
113
|
+
return Number.isInteger(n) ? `${n}` : `${n}f`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The same non-integer as C's own double literal — `OMX_<macro>_DOUBLE`, for a consumer that designs in double. */
|
|
117
|
+
export function scalarDoubleLiteral(n) {
|
|
118
|
+
if (typeof n !== 'number' || !Number.isFinite(n) || Number.isInteger(n)) throw new Error(`contract-limits-gen: a double spelling is for a non-integer, got ${n}`);
|
|
119
|
+
return `${n}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* ONE block for every declared scalar and sheet leaf (core's declaredScalars over the core barrel
|
|
124
|
+
* and the declarations): `#define OMX_<macro> <literal>`, sorted by macro. This generator names
|
|
125
|
+
* none of them — a kernel's scalars are its exports. A macro another block already defined is a
|
|
126
|
+
* collision, refused.
|
|
127
|
+
*/
|
|
128
|
+
function genDeclaredScalarsBlock(DECLARED_SCALARS, definedBefore) {
|
|
129
|
+
if (!Array.isArray(DECLARED_SCALARS) || DECLARED_SCALARS.length === 0) {
|
|
130
|
+
throw new Error('contract-limits-gen: the declared-scalar discovery is empty — nothing declared');
|
|
131
|
+
}
|
|
132
|
+
return DECLARED_SCALARS.flatMap(({ macro, value }) => {
|
|
133
|
+
for (const name of [`OMX_${macro}`, `OMX_${macro}_DOUBLE`]) {
|
|
134
|
+
if (definedBefore.has(name)) throw new Error(`contract-limits-gen: ${name} is a declared scalar AND another block's define — rename one`);
|
|
135
|
+
}
|
|
136
|
+
return Number.isInteger(value)
|
|
137
|
+
? [`#define OMX_${macro} ${scalarLiteral(value)}`]
|
|
138
|
+
: [`#define OMX_${macro} ${scalarLiteral(value)}`, `#define OMX_${macro}_DOUBLE ${scalarDoubleLiteral(value)}`];
|
|
139
|
+
}).join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A string as a C string literal (dsp-primitives §7, record sets): `\\`, `\"` and `\?` (no trigraph
|
|
144
|
+
* can form) escaped, `\n`/`\t`/`\r` by name, every other byte outside printable ASCII as a
|
|
145
|
+
* three-digit octal escape of its UTF-8 encoding.
|
|
146
|
+
*/
|
|
147
|
+
export function cStringLiteral(s) {
|
|
148
|
+
if (typeof s !== 'string') throw new Error(`contract-limits-gen: a C string literal needs a string, got ${typeof s}`);
|
|
149
|
+
const named = { 0x5c: '\\\\', 0x22: '\\"', 0x3f: '\\?', 0x0a: '\\n', 0x09: '\\t', 0x0d: '\\r' };
|
|
150
|
+
let out = '"';
|
|
151
|
+
for (const b of new TextEncoder().encode(s)) {
|
|
152
|
+
if (named[b] !== undefined) out += named[b];
|
|
153
|
+
else if (b >= 0x20 && b <= 0x7e) out += String.fromCharCode(b);
|
|
154
|
+
else out += `\\${b.toString(8).padStart(3, '0')}`;
|
|
155
|
+
}
|
|
156
|
+
return `${out}"`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** One record-set field as its C initialiser value: a string literal, a 0/1 boolean or the scalar literal. */
|
|
160
|
+
function setFieldLiteral(v) {
|
|
161
|
+
if (typeof v === 'string') return cStringLiteral(v);
|
|
162
|
+
if (typeof v === 'boolean') return v ? '1' : '0';
|
|
163
|
+
return scalarLiteral(v);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* ONE block for every declared record set (core's declaredSets over the core barrel and the
|
|
168
|
+
* declarations): `OMX_<SET>_COUNT` and `OMX_<SET>_INIT`, one `{ … }` row per declared row, rows and
|
|
169
|
+
* fields in declaration order. A set with a field this door cannot render is refused whole, and a
|
|
170
|
+
* macro another block defines is a collision, refused.
|
|
171
|
+
*/
|
|
172
|
+
function genDeclaredSetsBlock(DECLARED_SETS, definedElsewhere) {
|
|
173
|
+
if (!Array.isArray(DECLARED_SETS) || DECLARED_SETS.length === 0) {
|
|
174
|
+
throw new Error('contract-limits-gen: the declared-set discovery is empty — nothing declared');
|
|
175
|
+
}
|
|
176
|
+
return DECLARED_SETS.map(({ name, fields, rows }) => {
|
|
177
|
+
for (const macro of [`OMX_${name}_COUNT`, `OMX_${name}_INIT`]) {
|
|
178
|
+
if (definedElsewhere.has(macro)) throw new Error(`contract-limits-gen: ${macro} is a declared set AND another block's define — rename one`);
|
|
179
|
+
}
|
|
180
|
+
let body;
|
|
181
|
+
try {
|
|
182
|
+
body = rows.map((row) => {
|
|
183
|
+
if (!Array.isArray(row) || row.length !== fields.length) throw new Error(`a row carries ${row?.length} values for ${fields.length} fields`);
|
|
184
|
+
return `{ ${row.map(setFieldLiteral).join(', ')} }`;
|
|
185
|
+
});
|
|
186
|
+
} catch (e) {
|
|
187
|
+
throw new Error(`contract-limits-gen: OMX_${name}_INIT refused — ${e.message}`);
|
|
188
|
+
}
|
|
189
|
+
return [`#define OMX_${name}_COUNT ${rows.length}u`, `#define OMX_${name}_INIT { ${body.join(', ')} }`].join('\n');
|
|
190
|
+
}).join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* ONE block for every declared number list (core's declaredLists; dsp-primitives §7):
|
|
195
|
+
* `OMX_<LIST>_COUNT` and `OMX_<LIST>_INIT { a, b, … }`, values in declaration order, each the
|
|
196
|
+
* scalar literal. A list with a non-finite element is refused whole; a macro another block defines
|
|
197
|
+
* is a collision, refused.
|
|
198
|
+
*/
|
|
199
|
+
function genDeclaredListsBlock(DECLARED_LISTS, definedElsewhere) {
|
|
200
|
+
if (!Array.isArray(DECLARED_LISTS) || DECLARED_LISTS.length === 0) {
|
|
201
|
+
throw new Error('contract-limits-gen: the declared-list discovery is empty — nothing declared');
|
|
202
|
+
}
|
|
203
|
+
return DECLARED_LISTS.map(({ name, values }) => {
|
|
204
|
+
for (const macro of [`OMX_${name}_COUNT`, `OMX_${name}_INIT`]) {
|
|
205
|
+
if (definedElsewhere.has(macro)) throw new Error(`contract-limits-gen: ${macro} is a declared list AND another block's define — rename one`);
|
|
206
|
+
}
|
|
207
|
+
if (!values.every((v) => typeof v === 'number' && Number.isFinite(v))) {
|
|
208
|
+
throw new Error(`contract-limits-gen: OMX_${name}_INIT refused — an element is not a finite number`);
|
|
209
|
+
}
|
|
210
|
+
return [`#define OMX_${name}_COUNT ${values.length}u`, `#define OMX_${name}_INIT { ${values.map(scalarLiteral).join(', ')} }`].join('\n');
|
|
211
|
+
}).join('\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The ONE sheet `renderContractLimits` is handed: the core barrel namespace, every door DISCOVERED
|
|
216
|
+
* over it and the declarations barrel by the barrel's own discovery functions (`nativeLimitTables`
|
|
217
|
+
* over `<PREFIX>_LIMITS`, `declaredScalars`, `declaredSets`, `declaredLists`), then `extra` — the
|
|
218
|
+
* names that are not in the core barrel (`OMX_CLAP_HOST_EXTENSIONS`) or a test's override of one
|
|
219
|
+
* door. The generator and every render test build the sheet here, so a new door is added once,
|
|
220
|
+
* never appended to a hand list per caller (2026-09-15-no-stored-derivations.md §9).
|
|
221
|
+
*/
|
|
222
|
+
export function contractLimitsSheet(coreNs, declarations, extra = {}) {
|
|
223
|
+
return {
|
|
224
|
+
...coreNs,
|
|
225
|
+
NATIVE_LIMIT_TABLES: coreNs.nativeLimitTables(coreNs, declarations),
|
|
226
|
+
DECLARED_SCALARS: coreNs.declaredScalars(coreNs, declarations),
|
|
227
|
+
DECLARED_SETS: coreNs.declaredSets(coreNs, declarations),
|
|
228
|
+
DECLARED_LISTS: coreNs.declaredLists(coreNs, declarations),
|
|
229
|
+
...extra,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function renderContractLimits({
|
|
234
|
+
CHANNEL_CONTRACT, LAW_KINDS, NATIVE_LIMIT_TABLES, DECLARED_SCALARS, CORE_LIMITS, OMX_CLAP_HOST_EXTENSIONS, STANDARD_SAMPLE_RATES,
|
|
235
|
+
DECLARED_SETS, DECLARED_LISTS,
|
|
236
|
+
}) {
|
|
237
|
+
if (!Array.isArray(NATIVE_LIMIT_TABLES) || NATIVE_LIMIT_TABLES.length === 0) {
|
|
238
|
+
throw new Error('contract-limits-gen: the native limit table registry is empty — nothing registered');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const facts = Object.keys(CHANNEL_CONTRACT).filter(
|
|
242
|
+
(f) => (CHANNEL_CONTRACT[f].laws ?? []).length > 0,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const enumLines = LAW_KINDS.map((law, i) => ` ${lawEnumName(law)}${i === 0 ? ' = 0' : ''},`);
|
|
246
|
+
|
|
247
|
+
const stageBlocks = facts.map((fact) => {
|
|
248
|
+
const laws = CHANNEL_CONTRACT[fact].laws;
|
|
249
|
+
const names = laws.map((l) => lawEnumName(l.law));
|
|
250
|
+
const arr = stageArrayName(fact);
|
|
251
|
+
return (
|
|
252
|
+
`static const omx_contract_law_t ${arr}[] = { ${names.join(', ')} };\n` +
|
|
253
|
+
`#define ${arr}_COUNT ${names.length}`
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const above = [
|
|
258
|
+
...NATIVE_LIMIT_TABLES.map(([prefix, table]) => genLimitBlock(prefix, table)),
|
|
259
|
+
...CORE_STAGE_FACTS.map((f) => genCoreLimitBlock(CORE_LIMITS, f)),
|
|
260
|
+
].join('\n');
|
|
261
|
+
const definedAbove = new Set([...above.matchAll(/^#define (OMX_\w+) /gm)].map((m) => m[1]));
|
|
262
|
+
const scalarsBlock = genDeclaredScalarsBlock(DECLARED_SCALARS, definedAbove);
|
|
263
|
+
const clapBlock = genClapHostExtensionsBlock(OMX_CLAP_HOST_EXTENSIONS);
|
|
264
|
+
const ratesBlock = genDeclaredRates(STANDARD_SAMPLE_RATES);
|
|
265
|
+
const definedElsewhere = new Set([...[above, scalarsBlock, clapBlock, ratesBlock].join('\n').matchAll(/^#define (OMX_\w+)/gm)].map((m) => m[1]));
|
|
266
|
+
const setsBlock = genDeclaredSetsBlock(DECLARED_SETS, definedElsewhere);
|
|
267
|
+
const listsBlock = genDeclaredListsBlock(DECLARED_LISTS, new Set([...definedElsewhere, ...[...setsBlock.matchAll(/^#define (OMX_\w+)/gm)].map((m) => m[1])]));
|
|
268
|
+
|
|
269
|
+
return `// SPDX-License-Identifier: GPL-3.0-or-later
|
|
270
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
271
|
+
#ifndef OMX_CONTRACT_LIMITS_H
|
|
272
|
+
#define OMX_CONTRACT_LIMITS_H
|
|
273
|
+
/*
|
|
274
|
+
* GENERATED — DO NOT EDIT BY HAND.
|
|
275
|
+
* Produced by harness/contract-limits-gen.mjs from packages/core/src/channel-contract.ts's
|
|
276
|
+
* \`laws\` column (R-056), every table packages/core/src/native-limit-tables.ts's registry
|
|
277
|
+
* holds, packages/core/src/control-bounds.ts's CORE_LIMITS for the pan/delay/reverb/drive
|
|
278
|
+
* facts, EVERY declared scalar and all-number sheet packages/core/src/declared-scalars.ts
|
|
279
|
+
* discovers over the core barrel and @freemixer/declarations (dsp-primitives §7), and
|
|
280
|
+
* packages/core/src/row-codecs.ts's STANDARD_SAMPLE_RATES. Regenerate:
|
|
281
|
+
* \`node harness/contract-limits-gen.mjs\`, then commit the result — see this file's own
|
|
282
|
+
* generator for why it is committed rather than gitignored.
|
|
283
|
+
*
|
|
284
|
+
* A C contract (OMX_PRE/OMX_POST/OMX_INVARIANT) reads a limit from HERE, never a literal:
|
|
285
|
+
* packages/server/src/contract-limits-generated-conformance.test.ts refuses both a stale
|
|
286
|
+
* header and a C literal that duplicates a number this file already declares.
|
|
287
|
+
*/
|
|
288
|
+
|
|
289
|
+
/* The closed law vocabulary (core's LAW_KINDS, fact-declaration.ts) — one member per law. */
|
|
290
|
+
typedef enum {
|
|
291
|
+
${enumLines.join('\n')}
|
|
292
|
+
} omx_contract_law_t;
|
|
293
|
+
|
|
294
|
+
/* Per-stage law lists, one per channel fact whose row declares \`laws\` — the SAME array
|
|
295
|
+
* harness/contracts-scaffold.mjs's applier and the MCP contract_laws tool read; a stage's
|
|
296
|
+
* OMX_POST tokens implement one or more of these, never a second spelling of the law. */
|
|
297
|
+
${stageBlocks.join('\n\n')}
|
|
298
|
+
|
|
299
|
+
/* Per-parameter numeric travel, one block per table registered in
|
|
300
|
+
* packages/core/src/native-limit-tables.ts (GATE/COMP today; a kernel that registers a table
|
|
301
|
+
* lands here because it is REGISTERED, not because this generator names it). */
|
|
302
|
+
${NATIVE_LIMIT_TABLES.map(([prefix, table]) => genLimitBlock(prefix, table)).join('\n\n')}
|
|
303
|
+
|
|
304
|
+
/* The four coreLimits-sourced facts with a native meaning — pan, delay, reverb, drive
|
|
305
|
+
* (CORE_LIMITS, control-bounds.ts; F7). Not every field here has a C-side literal to replace:
|
|
306
|
+
* the reverb cut corners and the drive stage's five numerics are clamped only on the TS side
|
|
307
|
+
* today (mix_reverb.h's own header comment: "there is no 20 kHz anywhere in the C"), so those
|
|
308
|
+
* defines exist for completeness and for the day a native clamp is added, without yet being
|
|
309
|
+
* read by a stage. */
|
|
310
|
+
${CORE_STAGE_FACTS.map((f) => genCoreLimitBlock(CORE_LIMITS, f)).join('\n\n')}
|
|
311
|
+
|
|
312
|
+
/* EVERY declared scalar and sheet leaf (packages/core/src/declared-scalars.ts over the core barrel
|
|
313
|
+
* and @freemixer/declarations; dsp-primitives §7 the scalar door) — one block, sorted by macro,
|
|
314
|
+
* named by nobody: a kernel's constants are its exports (ROTARY_*, TRANSIENT_*, PITCH_KERNEL,
|
|
315
|
+
* PROGRAM_RELEASE, HOSTED_STAGE_LIMITS, ALLPASS/XOVER_LIMITS, the fdelay kernel's norm and order, …).
|
|
316
|
+
* An integer value is an integer literal, any other a float literal AND its OMX_<macro>_DOUBLE spelling. */
|
|
317
|
+
${scalarsBlock}
|
|
318
|
+
|
|
319
|
+
/* The CLAP host object's extension ids (packages/plugin-qualify/src/hosting-suitability.ts,
|
|
320
|
+
* OMX_CLAP_HOST_EXTENSIONS; 2026-09-26-clap-hosting-path.md §5) — read by mix_clap_host.c. */
|
|
321
|
+
${clapBlock}
|
|
322
|
+
|
|
323
|
+
/* EVERY declared record set (packages/core/src/declared-sets.ts over the core barrel and
|
|
324
|
+
* @freemixer/declarations; dsp-primitives §7) — one block, sorted by set, named by nobody: HOST_VERBS
|
|
325
|
+
* (2026-09-29-host-backend-one-contract.md §4, read by mix_host_backend.test.c against the fork's
|
|
326
|
+
* scenario list), … OMX_<SET>_INIT is one { … } row per declared row, fields in declaration order: a
|
|
327
|
+
* string as a C string literal, a boolean as 0/1, a number as the scalar literal. */
|
|
328
|
+
${setsBlock}
|
|
329
|
+
|
|
330
|
+
/* EVERY declared number list (packages/core/src/declared-lists.ts; dsp-primitives §7) — sorted by
|
|
331
|
+
* list, named by nobody: LATENCY_QUANTA (the quantum grid a bench iterates), STANDARD_SAMPLE_RATES,
|
|
332
|
+
* … A C grid is int qs[OMX_<LIST>_COUNT] = OMX_<LIST>_INIT; a script reads the _INIT line through
|
|
333
|
+
* harness/lib/declared-list.sh. */
|
|
334
|
+
${listsBlock}
|
|
335
|
+
|
|
336
|
+
/* The running check (dsp-primitives §7): every declared scalar by name with the macro ITSELF, so an
|
|
337
|
+
* entry carries what the including binary COMPILED. ONE translation unit defines
|
|
338
|
+
* OMX_DECLARED_SCALARS_TABLE before including this header (declared_scalars.c) and exports it. */
|
|
339
|
+
#ifdef OMX_DECLARED_SCALARS_TABLE
|
|
340
|
+
struct omx_declared_scalar { const char *name; double value; };
|
|
341
|
+
static const struct omx_declared_scalar OMX_DECLARED_SCALARS[] = {
|
|
342
|
+
${DECLARED_SCALARS.map(({ macro }) => ` { "${macro}", (double)OMX_${macro} },`).join('\n')}
|
|
343
|
+
};
|
|
344
|
+
#define OMX_DECLARED_SCALAR_COUNT ${DECLARED_SCALARS.length}u
|
|
345
|
+
#endif
|
|
346
|
+
|
|
347
|
+
#endif /* OMX_CONTRACT_LIMITS_H */
|
|
348
|
+
|
|
349
|
+
/* The sample rates the console declares (core's STANDARD_SAMPLE_RATES, row-codecs.ts): the set a
|
|
350
|
+
* contract's rate-is-declared precondition accepts, through omx_rate_is_declared(), and the grid a
|
|
351
|
+
* native tool iterates. Guarded on its own so a tool built against a pinned copy of this header
|
|
352
|
+
* (lv2-inprocess-pin.sh) can include this tree's copy after it and gain the rates without mixing
|
|
353
|
+
* the pin's limits. */
|
|
354
|
+
#ifndef OMX_DECLARED_RATE_COUNT
|
|
355
|
+
${ratesBlock}
|
|
356
|
+
/* The declared rates that are whole multiples of \`base\` (a tool's default grid: the 48 kHz family),
|
|
357
|
+
* in declaration order, at most \`cap\` of them; returns how many were written to \`out\`. */
|
|
358
|
+
static inline int omx_declared_rates_multiple_of(unsigned base, int *out, int cap) {
|
|
359
|
+
int n = 0;
|
|
360
|
+
for (unsigned k = 0; k < OMX_DECLARED_RATE_COUNT && n < cap; k++)
|
|
361
|
+
if ((unsigned)OMX_DECLARED_RATES[k] % base == 0) out[n++] = (int)OMX_DECLARED_RATES[k];
|
|
362
|
+
return n;
|
|
363
|
+
}
|
|
364
|
+
#endif
|
|
365
|
+
`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
// omx-contract: the block renderers above, exported for the data path's header (render/c.mjs). Nothing above changed.
|
|
370
|
+
export { genLimitBlock, genCoreLimitBlock, genDeclaredScalarsBlock, genDeclaredListsBlock, genDeclaredRates };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
* Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
*
|
|
4
|
+
* omx_plugin_param.h — the SHAPE of one generated plugin parameter, and nothing else: no number
|
|
5
|
+
* lives here. The rows are rendered by `omx-contract render --target c` into omx_<plugin>_params.h
|
|
6
|
+
* beside this file, from FreeMixer/omx-contract's data/plugins/ (omx-contract spec §4.2); a DPF
|
|
7
|
+
* plugin's initParameter reads them and types no range of its own.
|
|
8
|
+
*/
|
|
9
|
+
#ifndef OMX_PLUGIN_PARAM_H
|
|
10
|
+
#define OMX_PLUGIN_PARAM_H
|
|
11
|
+
|
|
12
|
+
/** A whole-number travel (declared `step` >= 1 over integer bounds). */
|
|
13
|
+
#define OMX_PLUGIN_PARAM_INTEGER 1u
|
|
14
|
+
/** A declared boolean: domain {0, 1}, the come-up value declared. */
|
|
15
|
+
#define OMX_PLUGIN_PARAM_TOGGLE 2u
|
|
16
|
+
|
|
17
|
+
typedef struct {
|
|
18
|
+
const char *symbol; /* the declared field name — the LV2 symbol, the CLAP param's identity */
|
|
19
|
+
const char *name; /* derived from the symbol, never a label table */
|
|
20
|
+
const char *unit;
|
|
21
|
+
float min, max, def;
|
|
22
|
+
unsigned flags;
|
|
23
|
+
} omx_plugin_param;
|
|
24
|
+
|
|
25
|
+
#endif /* OMX_PLUGIN_PARAM_H */
|