@openmixer/omx-contract 2.10.1 → 2.12.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 +2 -2
- package/bin/omx-contract.mjs +1 -1
- package/data/hosting.json +93 -0
- package/data/kernels/chorus.json +5 -5
- package/data/kernels/comp.json +2 -2
- package/data/kernels/deesser.json +1 -1
- package/data/kernels/delay.json +1 -1
- package/data/kernels/drive.json +14 -8
- package/data/kernels/eq.json +1 -1
- package/data/kernels/fbs.json +5 -5
- package/data/kernels/flanger.json +4 -4
- package/data/kernels/geq.json +4 -4
- package/data/kernels/hrp.json +5 -5
- package/data/kernels/mixmatrix.json +1 -1
- package/data/kernels/phaser.json +7 -7
- package/data/kernels/pitch.json +12 -3
- package/data/kernels/rotary.json +1 -1
- package/data/kernels/transient.json +5 -5
- package/data/primitives.json +1 -1
- package/dist/ts/omx-contract.d.ts +136 -48
- package/dist/ts/omx-contract.js +141 -53
- package/include/omxcontract/clap_host_limits.h +66 -0
- package/include/omxcontract/omx_contract_limits.h +34 -4
- package/lib/data.mjs +5 -2
- package/lib/fmt.mjs +3 -2
- package/lib/semver.mjs +24 -7
- package/lib/validate.mjs +32 -1
- package/package.json +1 -1
- package/render/c/clap-host-limits.mjs +78 -0
- package/render/c.mjs +30 -6
- package/render/doors.mjs +1 -1
- package/render/ts.mjs +22 -4
- package/schema/omx-contract.schema.json +48 -6
- package/share/omx-contract/omx-contract.json +159 -54
|
@@ -387,6 +387,26 @@
|
|
|
387
387
|
#define OMX_GROWTH_MIN_DB_PER_FRAME_DOUBLE 0.8
|
|
388
388
|
#define OMX_GROWTH_MIN_TOTAL_RISE_DB 6
|
|
389
389
|
#define OMX_GROWTH_WINDOW_FRAMES 5
|
|
390
|
+
#define OMX_HOSTED_STAGE_AUX_INPUTS 8
|
|
391
|
+
#define OMX_HOSTED_STAGE_AUX_OUTPUTS 8
|
|
392
|
+
#define OMX_HOSTED_STAGE_CLAMP_DBFS 24
|
|
393
|
+
#define OMX_HOSTED_STAGE_EVENTS_PER_BLOCK 64
|
|
394
|
+
#define OMX_HOSTED_STAGE_LATENCY_HOLD_MS 250
|
|
395
|
+
#define OMX_HOSTED_STAGE_LOG_BYTES 256
|
|
396
|
+
#define OMX_HOSTED_STAGE_MAIN_PORT_CHANNELS 2
|
|
397
|
+
#define OMX_HOSTED_STAGE_NON_FINITE_STRIKES 3
|
|
398
|
+
#define OMX_HOSTED_STAGE_NOTES_PER_BLOCK 256
|
|
399
|
+
#define OMX_HOSTED_STAGE_PARAM_COUNT_MAX 2048
|
|
400
|
+
#define OMX_HOSTED_STAGE_PARAM_QUEUE_DEPTH 256
|
|
401
|
+
#define OMX_HOSTED_STAGE_ROLE_POLL_US 1000
|
|
402
|
+
#define OMX_HOSTED_STAGE_ROLE_TIMEOUT_US 200000
|
|
403
|
+
#define OMX_HOSTED_STAGE_STATE_MAX_BYTES 1048576
|
|
404
|
+
#define OMX_HOSTED_STAGE_UNPUBLISH_POLL_US 100
|
|
405
|
+
#define OMX_HOSTED_STAGE_UNPUBLISH_TIMEOUT_US 2000000
|
|
406
|
+
#define OMX_HOSTED_STAGE_WARMUP_BLOCKS 64
|
|
407
|
+
#define OMX_HOSTED_STAGE_WARMUP_LEVEL_DBFS -6
|
|
408
|
+
#define OMX_HOSTED_STAGE_WORKER_POLL_US 1000
|
|
409
|
+
#define OMX_HOSTED_STAGE_WORKER_RING_BYTES 16384
|
|
390
410
|
#define OMX_HPF_FREQ_RANGE_MAX 1000
|
|
391
411
|
#define OMX_HPF_FREQ_RANGE_MIN 20
|
|
392
412
|
#define OMX_HPF_FREQ_RANGE_STEP 10
|
|
@@ -690,12 +710,22 @@ enum omx_drive_curves {
|
|
|
690
710
|
};
|
|
691
711
|
#define OMX_DRIVE_CURVES_COUNT 4u
|
|
692
712
|
enum omx_drive_hf_rolloffs {
|
|
693
|
-
|
|
694
|
-
OMX_DRIVE_HF_ROLLOFFS_12000 =
|
|
695
|
-
OMX_DRIVE_HF_ROLLOFFS_16000 =
|
|
713
|
+
OMX_DRIVE_HF_ROLLOFFS_OFF = 0,
|
|
714
|
+
OMX_DRIVE_HF_ROLLOFFS_12000 = 1,
|
|
715
|
+
OMX_DRIVE_HF_ROLLOFFS_16000 = 2,
|
|
696
716
|
};
|
|
697
717
|
#define OMX_DRIVE_HF_ROLLOFFS_COUNT 3u
|
|
698
|
-
#define OMX_DRIVE_HF_ROLLOFFS_DEFAULT
|
|
718
|
+
#define OMX_DRIVE_HF_ROLLOFFS_DEFAULT OMX_DRIVE_HF_ROLLOFFS_OFF
|
|
719
|
+
#define OMX_DRIVE_HF_ROLLOFFS_UNIT "Hz"
|
|
720
|
+
#define OMX_DRIVE_HF_ROLLOFFS_12000_VALUE 12000.0f
|
|
721
|
+
#define OMX_DRIVE_HF_ROLLOFFS_16000_VALUE 16000.0f
|
|
722
|
+
static inline int omx_drive_hf_rolloffs_value(int id, float *out) {
|
|
723
|
+
switch (id) {
|
|
724
|
+
case OMX_DRIVE_HF_ROLLOFFS_12000: *out = OMX_DRIVE_HF_ROLLOFFS_12000_VALUE; return 1;
|
|
725
|
+
case OMX_DRIVE_HF_ROLLOFFS_16000: *out = OMX_DRIVE_HF_ROLLOFFS_16000_VALUE; return 1;
|
|
726
|
+
default: return 0;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
699
729
|
enum omx_drive_oversamples {
|
|
700
730
|
OMX_DRIVE_OVERSAMPLES_1 = 1,
|
|
701
731
|
OMX_DRIVE_OVERSAMPLES_2 = 2,
|
package/lib/data.mjs
CHANGED
|
@@ -203,11 +203,12 @@ export function rawValue(item) {
|
|
|
203
203
|
case 'sheet': return item.value;
|
|
204
204
|
case 'list': return item.values;
|
|
205
205
|
case 'set': return item.ids;
|
|
206
|
+
case 'codes': return item.codes;
|
|
206
207
|
default: return undefined;
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
/** The closed list of derivations
|
|
211
|
+
/** The closed list of derivations. A new one is a change here with its own test. */
|
|
211
212
|
export const DERIVATIONS = {
|
|
212
213
|
count: (d, val) => {
|
|
213
214
|
const v = val(typeof d.of === 'string' ? { ref: d.of } : d.of);
|
|
@@ -262,7 +263,7 @@ export function lagrangeReadL1(order) {
|
|
|
262
263
|
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
263
264
|
|
|
264
265
|
/**
|
|
265
|
-
* Resolve every item: `{ name, rel, kind, doc, why?, unit?, c?, shape?, default?, labels?, value }` (default and
|
|
266
|
+
* Resolve every item: `{ name, rel, kind, doc, why?, unit?, c?, shape?, default?, labels?, values?, value }` (default, labels and values: a set's), where `value` has
|
|
266
267
|
* every `{ref}` and `{derive}` evaluated. The returned map also carries `kernels`, every kernel's ordered controls
|
|
267
268
|
* (kernelControls) and, for a kernel that declares them, its ordered `readouts` `{ name, doc, unit, min, max, scale,
|
|
268
269
|
* rateHz }` with every reference resolved, which the json render publishes beside the items.
|
|
@@ -302,9 +303,11 @@ export function resolveData(data) {
|
|
|
302
303
|
if (item.why !== undefined) r.why = item.why;
|
|
303
304
|
if (item.unit !== undefined) r.unit = item.unit;
|
|
304
305
|
if (item.c !== undefined) r.c = item.c;
|
|
306
|
+
if (item.docs !== undefined) r.docs = item.docs;
|
|
305
307
|
if (item.kind === 'set') {
|
|
306
308
|
if (item.default !== undefined) r.default = setDefault(item, (v) => val(v, name), name);
|
|
307
309
|
if (item.labels !== undefined) r.labels = item.labels;
|
|
310
|
+
if (item.values !== undefined) r.values = val(item.values, name);
|
|
308
311
|
}
|
|
309
312
|
if (item.kind === 'travels') r.shape = item.fields ? 'table' : 'travel';
|
|
310
313
|
r.value = val(rawValue(item), name);
|
package/lib/fmt.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
3
|
/**
|
|
4
|
-
* The one formatting of a data file
|
|
4
|
+
* The one formatting of a data file: 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
7
|
* readouts, tables, aggregates, constants), its controls and readouts in the author's order, each one's keys in
|
|
@@ -12,7 +12,7 @@ const ORDER = {
|
|
|
12
12
|
control: ['name', 'kind', 'global', 'table', 'rearms', 'since', 'doc', 'why', 'unit', 'ids', 'default', 'labels', 'travel', 'c'],
|
|
13
13
|
sections: ['controls', 'readouts', 'tables', 'aggregates', 'constants'],
|
|
14
14
|
readout: ['name', 'doc', 'unit', 'min', 'max', 'scale', 'rateHz'],
|
|
15
|
-
item: ['kind', 'since', 'doc', 'why', 'unit', 'value', 'values', 'ids', 'default', 'labels', 'fields', 'travel', 'c'],
|
|
15
|
+
item: ['kind', 'since', 'doc', 'why', 'unit', 'value', 'docs', 'values', 'ids', 'default', 'labels', 'fields', 'travel', 'codes', 'c'],
|
|
16
16
|
travel: ['min', 'max', 'step', 'unit', 'default', 'defaultFrom', 'scale', 'byKind', 'doc', 'why'],
|
|
17
17
|
c: ['alias', 'render'],
|
|
18
18
|
alias: ['fact', 'field'],
|
|
@@ -48,6 +48,7 @@ function item(it) {
|
|
|
48
48
|
if (!isObj(it)) return it;
|
|
49
49
|
const o = ordered(it, ORDER.item);
|
|
50
50
|
if ('value' in o) o.value = o.kind === 'sheet' ? sheet(o.value) : number(o.value);
|
|
51
|
+
if (isObj(o.codes)) o.codes = Object.fromEntries(Object.entries(o.codes).map(([k, c]) => [k, ordered(c, ['code', 'doc'])]));
|
|
51
52
|
if (isObj(o.fields)) o.fields = Object.fromEntries(Object.entries(o.fields).map(([k, t]) => [k, isObj(t) ? travel(t) : t]));
|
|
52
53
|
if (isObj(o.travel)) o.travel = travel(o.travel);
|
|
53
54
|
if (isObj(o.c)) {
|
package/lib/semver.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
3
|
/**
|
|
4
|
-
* `omx-contract semver`: classify every change between two resolved declarations
|
|
4
|
+
* `omx-contract semver`: classify every change between two resolved declarations and
|
|
5
5
|
* refuse a version whose bump is shorter than the largest class found.
|
|
6
6
|
*
|
|
7
7
|
* major a field or alias removed; an item removed (MINOR in the development phase, below); a
|
|
8
8
|
* unit or kind changed; a travel narrowed; a constant a kernel computes with changed (a
|
|
9
9
|
* scalar, sheet leaf or derived value that is not a travel bound or default; a list member
|
|
10
|
-
* or set id changed or removed; a set's default added, removed
|
|
10
|
+
* or set id changed or removed; a set's default added, removed; a value an id carries changed
|
|
11
|
+
* or removed)
|
|
11
12
|
* minor an item, field, sheet leaf, list member or set id added (appended); a travel widened;
|
|
12
|
-
* a default moved inside its travel; a step changed
|
|
13
|
+
* a default moved inside its travel; a step changed; a set's values added
|
|
13
14
|
* patch doc and why text
|
|
14
15
|
*
|
|
15
16
|
* A kernel's readouts (docs/readouts.md): one removed, renamed or moved, or its unit changed, is major (a removal is
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
const RANK = { none: 0, patch: 1, minor: 2, major: 3 };
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
|
-
* The development-phase rule
|
|
24
|
+
* The development-phase rule: while every consumer of
|
|
24
25
|
* omx-contract is a FreeMixer repository named here, removing an item is MINOR. The rule ends, and
|
|
25
26
|
* this becomes null so a removal is MAJOR again, in the release that names the first consumer
|
|
26
27
|
* outside the list. Until then a MAJOR class is released as a minor bump (see refusal).
|
|
@@ -94,8 +95,9 @@ export function classify(prev, next, { developmentPhase = DEVELOPMENT_PHASE } =
|
|
|
94
95
|
continue;
|
|
95
96
|
}
|
|
96
97
|
if (a.kind !== b.kind || (a.shape ?? '') !== (b.shape ?? '')) { say('major', `kind ${a.kind} -> ${b.kind}`); continue; }
|
|
97
|
-
|
|
98
|
-
if (a.
|
|
98
|
+
// a set that gains values gains their unit with them (minor, below); any other unit change is major
|
|
99
|
+
if ((a.unit ?? '') !== (b.unit ?? '') && !(a.kind === 'set' && a.values === undefined)) say('major', `unit ${JSON.stringify(a.unit)} -> ${JSON.stringify(b.unit)}`);
|
|
100
|
+
if (a.doc !== b.doc || (a.why ?? '') !== (b.why ?? '') || !same(a.docs, b.docs)) say('patch', 'doc');
|
|
99
101
|
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');
|
|
100
102
|
if (!same(a.c?.render, b.c?.render)) say(b.c?.render === false ? 'major' : 'minor', 'C render of the list changed');
|
|
101
103
|
switch (a.kind) {
|
|
@@ -123,11 +125,26 @@ export function classify(prev, next, { developmentPhase = DEVELOPMENT_PHASE } =
|
|
|
123
125
|
case 'set':
|
|
124
126
|
a.value.forEach((v, i) => {
|
|
125
127
|
if (i >= b.value.length) say('major', `${name}[${i}] removed`);
|
|
126
|
-
else if (b.value[i] !== v) say('major', `${name}[${i}]: ${v} -> ${b.value[i]}`);
|
|
128
|
+
else if (b.value[i] !== v) say('major', `${name}[${i}]: ${JSON.stringify(v)} -> ${JSON.stringify(b.value[i])}`);
|
|
127
129
|
});
|
|
128
130
|
if (b.value.length > a.value.length) say('minor', `${name}: ${b.value.length - a.value.length} id(s) appended`);
|
|
129
131
|
if (a.default !== b.default) say(a.default === undefined || b.default === undefined ? 'major' : 'minor', `${name}: default ${a.default} -> ${b.default}`);
|
|
130
132
|
if (!same(a.labels, b.labels)) say('patch', `${name}: labels`);
|
|
133
|
+
if (a.values === undefined && b.values !== undefined) say('minor', `${name}: values added`);
|
|
134
|
+
else if (a.values !== undefined && b.values === undefined) say('major', `${name}: values removed`);
|
|
135
|
+
else if (a.values !== undefined) {
|
|
136
|
+
a.values.forEach((v, i) => {
|
|
137
|
+
if (i < b.values.length && b.values[i] !== v) say('major', `${name}: the value of ${a.value[i]}: ${v} -> ${b.values[i]} (a constant a kernel computes with)`);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case 'codes':
|
|
142
|
+
for (const k of Object.keys(a.value)) {
|
|
143
|
+
if (!(k in b.value)) say('major', `${name}.${k} removed`);
|
|
144
|
+
else if (a.value[k].code !== b.value[k].code) say('major', `${name}.${k}: ${JSON.stringify(a.value[k].code)} -> ${JSON.stringify(b.value[k].code)} (a code is read by verdicts, never reworded)`);
|
|
145
|
+
else if ((a.value[k].doc ?? '') !== (b.value[k].doc ?? '')) say('patch', `${name}.${k}: doc`);
|
|
146
|
+
}
|
|
147
|
+
for (const k of Object.keys(b.value)) if (!(k in a.value)) say('minor', `${name}.${k} added`);
|
|
131
148
|
break;
|
|
132
149
|
default:
|
|
133
150
|
break;
|
package/lib/validate.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
3
|
/**
|
|
4
|
-
* `omx-contract validate`: the schema, then the checks a schema cannot express
|
|
4
|
+
* `omx-contract validate`: the schema, then the checks a schema cannot express: every
|
|
5
5
|
* reference resolves with no cycle; every default inside its travel; every C name the render spells
|
|
6
6
|
* is unique; an item's `c.alias` names no other item. Each failure names file, item and rule.
|
|
7
7
|
*/
|
|
@@ -112,6 +112,14 @@ function semanticRules(resolved, say) {
|
|
|
112
112
|
if (r.kind === 'set') {
|
|
113
113
|
if (r.default !== undefined && !r.value.includes(r.default)) say(r.rel, r.name, 'set-default', `default ${JSON.stringify(r.default)} is not one of ${JSON.stringify(r.value)}`);
|
|
114
114
|
if (r.labels !== undefined && r.labels.length !== r.value.length) say(r.rel, r.name, 'set-labels', `${r.labels.length} labels for ${r.value.length} ids`);
|
|
115
|
+
if (r.values !== undefined) {
|
|
116
|
+
if (r.values.length !== r.value.length) say(r.rel, r.name, 'set-values', `${r.values.length} values for ${r.value.length} ids`);
|
|
117
|
+
if (r.value.some((id) => typeof id !== 'string')) say(r.rel, r.name, 'set-values', 'a set of numeric ids carries no values: the id is its own value');
|
|
118
|
+
r.values.forEach((v, i) => {
|
|
119
|
+
if (v !== null && (typeof v !== 'number' || !Number.isFinite(v))) say(r.rel, r.name, 'set-values', `the value of ${JSON.stringify(r.value[i])} resolves to ${JSON.stringify(v)}`);
|
|
120
|
+
});
|
|
121
|
+
if (r.values.every((v) => v === null)) say(r.rel, r.name, 'set-values', 'no id carries a value');
|
|
122
|
+
}
|
|
115
123
|
}
|
|
116
124
|
const travels = r.kind === 'travels' ? (r.shape === 'table' ? Object.entries(r.value) : [['', r.value]]) : [];
|
|
117
125
|
for (const [field, t] of travels) {
|
|
@@ -119,6 +127,26 @@ function semanticRules(resolved, say) {
|
|
|
119
127
|
}
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
// A sheet's prose names fields the sheet has; a code is declared once.
|
|
131
|
+
for (const r of resolved.values()) {
|
|
132
|
+
if (r.kind === 'sheet' && r.docs !== undefined) {
|
|
133
|
+
for (const f of Object.keys(r.docs)) if (!(f in r.value)) say(r.rel, r.name, 'sheet-docs', `docs.${f} describes no field of ${r.name}`);
|
|
134
|
+
}
|
|
135
|
+
if (r.kind === 'codes') {
|
|
136
|
+
const seenCodes = new Map();
|
|
137
|
+
for (const [k, c] of Object.entries(r.value)) {
|
|
138
|
+
if (seenCodes.has(c.code)) say(r.rel, r.name, 'code-unique', `${k} and ${seenCodes.get(c.code)} both carry ${JSON.stringify(c.code)}`);
|
|
139
|
+
seenCodes.set(c.code, k);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The hosted stage's limits are finite numbers, the header of the CLAP core reads each as a define.
|
|
145
|
+
const stage = resolved.get('HOSTED_STAGE_LIMITS');
|
|
146
|
+
if (stage) {
|
|
147
|
+
for (const [f, v] of Object.entries(stage.value)) if (typeof v !== 'number' || !Number.isFinite(v)) say(stage.rel, stage.name, 'finite', `HOSTED_STAGE_LIMITS.${f} resolves to ${JSON.stringify(v)}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
122
150
|
// A readout's numbers are finite, its range runs upward, its rate is positive and its scale is a PARAM_SCALES id.
|
|
123
151
|
for (const [k, { readouts }] of Object.entries(resolved.kernels ?? {})) {
|
|
124
152
|
for (const ro of readouts ?? []) {
|
|
@@ -130,6 +158,9 @@ function semanticRules(resolved, say) {
|
|
|
130
158
|
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
159
|
}
|
|
132
160
|
}
|
|
161
|
+
for (const r of resolved.values()) {
|
|
162
|
+
if (r.kind === 'set' && r.values && resolved.has(`${r.name}_VALUES`)) say(r.rel, `${r.name}_VALUES`, 'set-values', `${r.name}_VALUES is the ts render's export of ${r.name}'s values: no item may take the name`);
|
|
163
|
+
}
|
|
133
164
|
if (resolved.has('READOUTS') && Object.values(resolved.kernels ?? {}).some((k) => k.readouts)) {
|
|
134
165
|
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
166
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmixer/omx-contract",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* `clap_host_limits.h`: the header the CLAP core (omx-clap-core) builds against and installs, rendered from
|
|
5
|
+
* `HOSTED_STAGE_LIMITS` and `HOSTING_CODES`. It is the library's API documentation too, so every define carries
|
|
6
|
+
* the prose declared beside its number or code as a trailing `///<` comment; the frame below is the only prose
|
|
7
|
+
* here. The fields named `worker*` are the LV2 adapter's and are not rendered.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const constCase = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
11
|
+
|
|
12
|
+
/** The licence block every source file of omx-clap-core opens with: the SPDX line, the GPL notice, the copyright line, a blank line. */
|
|
13
|
+
const LICENCE = `/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
14
|
+
/*
|
|
15
|
+
* This file is part of omx-clap-host.
|
|
16
|
+
*
|
|
17
|
+
* omx-clap-host is free software: you can redistribute it and/or modify
|
|
18
|
+
* it under the terms of the GNU General Public License as published by
|
|
19
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
20
|
+
* (at your option) any later version.
|
|
21
|
+
*
|
|
22
|
+
* omx-clap-host is distributed in the hope that it will be useful,
|
|
23
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
24
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
25
|
+
* GNU General Public License for more details.
|
|
26
|
+
*
|
|
27
|
+
* You should have received a copy of the GNU General Public License
|
|
28
|
+
* along with omx-clap-host. If not, see <http://www.gnu.org/licenses/>.
|
|
29
|
+
*
|
|
30
|
+
* Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const isLevel = (field) => field.endsWith('Dbfs');
|
|
36
|
+
|
|
37
|
+
/** A define with its prose as a trailing member comment; a one-line prose only, or the rest would be read as C. */
|
|
38
|
+
function documented(define, prose, where) {
|
|
39
|
+
if (prose === undefined) return define;
|
|
40
|
+
if (typeof prose !== 'string' || prose.length === 0 || /[\n\r]/.test(prose)) throw new Error(`clap-host-limits: ${where} is no one-line description`);
|
|
41
|
+
return `${define} ///< ${prose}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The header of the resolved items `HOSTED_STAGE_LIMITS` and `HOSTING_CODES`. */
|
|
45
|
+
export function renderClapHostLimits(stage, codes) {
|
|
46
|
+
const numbers = Object.entries(stage.value)
|
|
47
|
+
.filter(([field]) => !field.startsWith('worker'))
|
|
48
|
+
.map(([field, v]) => {
|
|
49
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) throw new Error(`clap-host-limits: HOSTED_STAGE_LIMITS.${field} is not a finite number`);
|
|
50
|
+
const literal = isLevel(field) ? `${Number.isInteger(v) ? `${v}.0` : v}f` : Number.isInteger(v) && v >= 0 ? `${v}u` : null;
|
|
51
|
+
if (literal === null) throw new Error(`clap-host-limits: HOSTED_STAGE_LIMITS.${field} is neither a level (Dbfs) nor a non-negative integer`);
|
|
52
|
+
return documented(`#define CLAP_HOST_${constCase(field)} ${literal}`, stage.docs?.[field], `HOSTED_STAGE_LIMITS.docs.${field}`);
|
|
53
|
+
});
|
|
54
|
+
const refusals = Object.entries(codes.value)
|
|
55
|
+
.map(([name, c]) => documented(`#define CLAP_HOST_CODE_${constCase(name)} ${JSON.stringify(c.code)}`, c.doc, `HOSTING_CODES.${name}.doc`));
|
|
56
|
+
return `${LICENCE}#ifndef CLAP_HOST_LIMITS_H
|
|
57
|
+
#define CLAP_HOST_LIMITS_H
|
|
58
|
+
/*
|
|
59
|
+
* GENERATED — DO NOT EDIT BY HAND.
|
|
60
|
+
* Rendered by \`omx-contract render --target c\` from FreeMixer/omx-contract's HOSTED_STAGE_LIMITS and
|
|
61
|
+
* HOSTING_CODES. The rendered file is kept in this repository so the library builds on its own; the numbers
|
|
62
|
+
* and codes are changed where they are declared and the file is re-rendered, never edited here.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/** @file
|
|
66
|
+
* @brief The limits and refusal codes of the hosting code. */
|
|
67
|
+
|
|
68
|
+
#include <stddef.h>
|
|
69
|
+
|
|
70
|
+
/** the stage and the host: every number the core reads */
|
|
71
|
+
${numbers.join('\n')}
|
|
72
|
+
|
|
73
|
+
/** the refusals the core returns, as the hosting codes the console's verdicts use */
|
|
74
|
+
${refusals.join('\n')}
|
|
75
|
+
|
|
76
|
+
#endif /* CLAP_HOST_LIMITS_H */
|
|
77
|
+
`;
|
|
78
|
+
}
|
package/render/c.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
3
|
/**
|
|
4
|
-
* The `c` target: `omxcontract/omx_contract_limits.h
|
|
4
|
+
* The `c` target: `omxcontract/omx_contract_limits.h`, and `omxcontract/clap_host_limits.h` where the data declares the hosting limits and codes (docs/hosting.md). Every define is spelled by the console engine's
|
|
5
5
|
* renderer, moved unchanged (c/contract-limits-render.mjs); only the frame (banner, section
|
|
6
6
|
* comments) is this repository's. A plugin's parameter header is rendered by omx-plugins from its
|
|
7
7
|
* declaration, by reference to the items here.
|
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
cStringLiteral, genCoreLimitBlock, genDeclaredListsBlock, genDeclaredRates, genDeclaredScalarsBlock, genLimitBlock, renderContractLimits,
|
|
11
11
|
scalarLiteral,
|
|
12
12
|
} from './c/contract-limits-render.mjs';
|
|
13
|
+
import { renderClapHostLimits } from './c/clap-host-limits.mjs';
|
|
13
14
|
import { doors } from './doors.mjs';
|
|
14
15
|
|
|
15
16
|
const defined = (text) => new Set([...text.matchAll(/^#define (OMX_\w+)/gm)].map((m) => m[1]));
|
|
16
17
|
|
|
17
|
-
/** The rate tail of
|
|
18
|
+
/** The rate tail of the console engine's header, outside the include guard, byte for byte. */
|
|
18
19
|
function ratesTail(rates) {
|
|
19
20
|
return `
|
|
20
21
|
/* The sample rates the console declares (core's STANDARD_SAMPLE_RATES, row-codecs.ts): the set a
|
|
@@ -43,8 +44,26 @@ export const setEnumerators = (set) => set.ids.map((id) => `OMX_${set.name}_${co
|
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Every set as a C enum: the id's position is its value for named ids, the number itself for numeric
|
|
46
|
-
* ids; `OMX_<SET>_COUNT`, and `OMX_<SET>_DEFAULT` when the set names a default.
|
|
47
|
+
* ids; `OMX_<SET>_COUNT`, and `OMX_<SET>_DEFAULT` when the set names a default. A set whose ids carry values adds
|
|
48
|
+
* `OMX_<SET>_UNIT`, `OMX_<SET>_<ID>_VALUE` for each id that carries one, and `omx_<set>_value(id, &out)`, which writes
|
|
49
|
+
* the id's value and returns 1, or returns 0 for an id that carries none: a kernel reads the number from the id
|
|
50
|
+
* (the drive's roll-off corner), never a sentinel.
|
|
47
51
|
*/
|
|
52
|
+
function genSetValues(set, names) {
|
|
53
|
+
const fn = `omx_${set.name.toLowerCase()}_value`;
|
|
54
|
+
const carried = set.ids.map((id, i) => [names[i], set.values[i]]).filter(([, v]) => v !== null);
|
|
55
|
+
return [
|
|
56
|
+
`#define OMX_${set.name}_UNIT ${cStringLiteral(set.unit)}`,
|
|
57
|
+
...carried.map(([n, v]) => `#define ${n}_VALUE ${floatLiteral(v)}`),
|
|
58
|
+
`static inline int ${fn}(int id, float *out) {`,
|
|
59
|
+
' switch (id) {',
|
|
60
|
+
...carried.map(([n]) => ` case ${n}: *out = ${n}_VALUE; return 1;`),
|
|
61
|
+
' default: return 0;',
|
|
62
|
+
' }',
|
|
63
|
+
'}',
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
function genDeclaredSetsEnums(sets) {
|
|
49
68
|
return sets.map((set) => {
|
|
50
69
|
const names = setEnumerators(set);
|
|
@@ -52,6 +71,7 @@ function genDeclaredSetsEnums(sets) {
|
|
|
52
71
|
const members = set.ids.map((id, i) => ` ${names[i]} = ${numeric ? id : i},`);
|
|
53
72
|
const lines = [`enum omx_${set.name.toLowerCase()} {`, ...members, '};', `#define OMX_${set.name}_COUNT ${set.ids.length}u`];
|
|
54
73
|
if (set.default !== undefined) lines.push(`#define OMX_${set.name}_DEFAULT ${names[set.ids.indexOf(set.default)]}`);
|
|
74
|
+
if (set.values) lines.push(...genSetValues(set, names));
|
|
55
75
|
return lines.join('\n');
|
|
56
76
|
}).join('\n');
|
|
57
77
|
}
|
|
@@ -174,12 +194,16 @@ ${d.STANDARD_SAMPLE_RATES ? ratesTail(d.STANDARD_SAMPLE_RATES) : ''}`;
|
|
|
174
194
|
|
|
175
195
|
/** Every file of the `c` target: `[{ path, text }]`, paths relative to the output directory. */
|
|
176
196
|
export function renderC(resolved) {
|
|
177
|
-
|
|
197
|
+
const files = [{ path: 'omxcontract/omx_contract_limits.h', text: renderLimitsHeader(resolved) }];
|
|
198
|
+
const stage = resolved.get('HOSTED_STAGE_LIMITS');
|
|
199
|
+
const codes = resolved.get('HOSTING_CODES');
|
|
200
|
+
if (stage && codes) files.push({ path: 'omxcontract/clap_host_limits.h', text: renderClapHostLimits(stage, codes) });
|
|
201
|
+
return files;
|
|
178
202
|
}
|
|
179
203
|
|
|
180
204
|
/**
|
|
181
|
-
* The `c` target of a frozen
|
|
182
|
-
* renderer, for the round-trip proof
|
|
205
|
+
* The `c` target of a frozen sheet (test/fixtures/frozen-limits-sheet.json): `{ sheet }` rendered by the moved
|
|
206
|
+
* renderer, for the round-trip proof.
|
|
183
207
|
*/
|
|
184
208
|
export function renderSheetC(fixture) {
|
|
185
209
|
const s = fixture.sheet;
|
package/render/doors.mjs
CHANGED
|
@@ -52,7 +52,7 @@ export function doors(resolved) {
|
|
|
52
52
|
.map((r) => ({ name: r.name, values: r.value }))
|
|
53
53
|
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
54
54
|
const DECLARED_SET_ITEMS = items.filter((r) => r.kind === 'set')
|
|
55
|
-
.map((r) => ({ name: r.name, ids: r.value, default: r.default }))
|
|
55
|
+
.map((r) => ({ name: r.name, ids: r.value, default: r.default, ...(r.values ? { values: r.values, unit: r.unit } : {}) }))
|
|
56
56
|
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
57
57
|
const ALIASES = new Map();
|
|
58
58
|
for (const r of items) {
|
package/render/ts.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { eqDefaultBands, eqDefaultCentres, eqDefaultParams, eqDefaultTypes } fro
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The `ts` target: one ESM module and its type declarations, every item exported under its own
|
|
7
|
-
* name with its literal type (`as const`), so a re-export
|
|
7
|
+
* name with its literal type (`as const`), so a consumer's re-export keeps names, values and types.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/** The value a TypeScript reader sees for one resolved item. */
|
|
@@ -14,7 +14,7 @@ export function tsValue(r) {
|
|
|
14
14
|
|
|
15
15
|
/** The literal type of a JSON value, every member readonly. */
|
|
16
16
|
export function tsType(v, indent = '') {
|
|
17
|
-
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
17
|
+
if (v === null || typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
18
18
|
if (typeof v === 'string') return JSON.stringify(v);
|
|
19
19
|
const inner = `${indent} `;
|
|
20
20
|
if (Array.isArray(v)) return v.length ? `readonly [${v.map((x) => tsType(x, indent)).join(', ')}]` : 'readonly []';
|
|
@@ -83,12 +83,30 @@ function readoutsExport(resolved) {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The value each id of a set carries, `<SET>_VALUES`: `{ <id>: value | null }`, for every set that declares values (the
|
|
88
|
+
* drive's roll-off corners), so a reader takes the number from the id, never from its spelling. Read it by id: an
|
|
89
|
+
* object keeps no order for an id such as 12000.
|
|
90
|
+
*/
|
|
91
|
+
function setValuesExports(resolved) {
|
|
92
|
+
const sets = [...resolved.values()].filter((r) => r.kind === 'set' && r.values);
|
|
93
|
+
const one = (r, decl) => {
|
|
94
|
+
const value = Object.fromEntries(r.value.map((id, i) => [id, r.values[i]]));
|
|
95
|
+
const doc = `/**\n * The value each id of ${r.name} carries, in ${JSON.stringify(r.unit)}; null for an id that carries none.\n * @see ${r.rel}\n */`;
|
|
96
|
+
return decl
|
|
97
|
+
? `\n${doc}\nexport declare const ${r.name}_VALUES: ${tsType(value)};\n`
|
|
98
|
+
: `\n${doc}\nexport const ${r.name}_VALUES = ${JSON.stringify(value, null, 2)};\n`;
|
|
99
|
+
};
|
|
100
|
+
return { js: sets.map((r) => one(r, false)).join(''), dts: sets.map((r) => one(r, true)).join('') };
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
/** Every file of the `ts` target: `[{ path, text }]`. */
|
|
87
104
|
export function renderTs(resolved) {
|
|
88
105
|
const items = [...resolved.values()];
|
|
89
106
|
const rule = eqRule(resolved);
|
|
90
107
|
const readouts = readoutsExport(resolved);
|
|
91
|
-
const
|
|
92
|
-
const
|
|
108
|
+
const values = setValuesExports(resolved);
|
|
109
|
+
const js = BANNER + items.map((r) => `\n${docComment(r)}\nexport const ${r.name} = ${JSON.stringify(tsValue(r), null, 2)};\n`).join('') + values.js + readouts.js + rule.js;
|
|
110
|
+
const dts = BANNER + items.map((r) => `\n${docComment(r)}\nexport declare const ${r.name}: ${tsType(tsValue(r))};\n`).join('') + values.dts + readouts.dts + rule.dts;
|
|
93
111
|
return [{ path: 'omx-contract.js', text: js }, { path: 'omx-contract.d.ts', text: dts }];
|
|
94
112
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"$defs": {
|
|
13
13
|
"unit": {
|
|
14
14
|
"description": "The closed list of units the items use; \"x\" is a multiplier, \"\" is a ratio or a fraction.",
|
|
15
|
-
"enum": ["dB", "dBFS", "ms", "s", "min", "Hz", "oct", "cents", "%", "x", ""]
|
|
15
|
+
"enum": ["dB", "dBFS", "ms", "s", "min", "Hz", "oct", "cents", "st", "%", "x", ""]
|
|
16
16
|
},
|
|
17
17
|
"text": { "type": "string", "minLength": 1 },
|
|
18
18
|
"since": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$", "description": "The release that first carried an item declared outside the kernels (rates.json, primitives.json); a kernel's items record it in recipes/answers/." },
|
|
@@ -156,7 +156,14 @@
|
|
|
156
156
|
"since": { "$ref": "#/$defs/since" },
|
|
157
157
|
"doc": { "$ref": "#/$defs/text" },
|
|
158
158
|
"why": { "$ref": "#/$defs/text" },
|
|
159
|
-
"value": { "$ref": "#/$defs/sheetValue" }
|
|
159
|
+
"value": { "$ref": "#/$defs/sheetValue" },
|
|
160
|
+
"docs": {
|
|
161
|
+
"description": "One line of prose per top-level field of `value`, rendered beside the field's define in a header that documents it. A field without one has none.",
|
|
162
|
+
"type": "object",
|
|
163
|
+
"propertyNames": { "pattern": "^[a-z][A-Za-z0-9]*$" },
|
|
164
|
+
"additionalProperties": { "$ref": "#/$defs/text" },
|
|
165
|
+
"minProperties": 1
|
|
166
|
+
}
|
|
160
167
|
},
|
|
161
168
|
"required": ["kind", "doc", "value"],
|
|
162
169
|
"additionalProperties": false
|
|
@@ -179,9 +186,35 @@
|
|
|
179
186
|
"required": ["kind", "doc", "unit", "values"],
|
|
180
187
|
"additionalProperties": false
|
|
181
188
|
},
|
|
189
|
+
"codes": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"description": "Refusal codes: a lowerCamel name per code, the code itself (`hosting.<group>.<reason>`, lower case, read by verdicts and never reworded) and optionally one line of prose.",
|
|
192
|
+
"properties": {
|
|
193
|
+
"kind": { "const": "codes" },
|
|
194
|
+
"since": { "$ref": "#/$defs/since" },
|
|
195
|
+
"doc": { "$ref": "#/$defs/text" },
|
|
196
|
+
"why": { "$ref": "#/$defs/text" },
|
|
197
|
+
"codes": {
|
|
198
|
+
"type": "object",
|
|
199
|
+
"propertyNames": { "pattern": "^[a-z][A-Za-z0-9]*$" },
|
|
200
|
+
"additionalProperties": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"code": { "type": "string", "pattern": "^hosting(\\.[a-z0-9]+(-[a-z0-9]+)*){2,}$" },
|
|
204
|
+
"doc": { "$ref": "#/$defs/text" }
|
|
205
|
+
},
|
|
206
|
+
"required": ["code"],
|
|
207
|
+
"additionalProperties": false
|
|
208
|
+
},
|
|
209
|
+
"minProperties": 1
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
"required": ["kind", "doc", "codes"],
|
|
213
|
+
"additionalProperties": false
|
|
214
|
+
},
|
|
182
215
|
"set": {
|
|
183
216
|
"type": "object",
|
|
184
|
-
"description": "Ordered ids with an optional default and
|
|
217
|
+
"description": "Ordered ids with an optional default, labels and a value per id. The ids and their order are the contract: all strings of letters and digits (the id is the name, its position the value; an id may start with a digit, as 4x does) or all non-negative integers (the id is the value). Labels are face text and optional. The default may be a reference to the scalar that states it: a boolean names the second id of a two-id switch when true, the first when false.",
|
|
185
218
|
"properties": {
|
|
186
219
|
"kind": { "const": "set" },
|
|
187
220
|
"since": { "$ref": "#/$defs/since" },
|
|
@@ -194,21 +227,30 @@
|
|
|
194
227
|
]
|
|
195
228
|
},
|
|
196
229
|
"default": { "oneOf": [{ "type": "string", "pattern": "^[A-Za-z0-9]+$" }, { "type": "integer", "minimum": 0 }, { "$ref": "#/$defs/ref" }] },
|
|
197
|
-
"labels": { "type": "array", "items": { "$ref": "#/$defs/text" }, "minItems": 1 }
|
|
230
|
+
"labels": { "type": "array", "items": { "$ref": "#/$defs/text" }, "minItems": 1 },
|
|
231
|
+
"unit": { "$ref": "#/$defs/unit" },
|
|
232
|
+
"values": {
|
|
233
|
+
"description": "The value each id carries, in `unit`, one per id in id order; null for an id that carries none (the drive's `off` roll-off has no corner). Only a set of named ids carries values: a numeric id is its own value.",
|
|
234
|
+
"type": "array",
|
|
235
|
+
"items": { "oneOf": [{ "$ref": "#/$defs/number" }, { "type": "null" }] },
|
|
236
|
+
"minItems": 1
|
|
237
|
+
}
|
|
198
238
|
},
|
|
199
239
|
"required": ["kind", "doc", "ids"],
|
|
240
|
+
"dependentRequired": { "values": ["unit"], "unit": ["values"] },
|
|
200
241
|
"additionalProperties": false
|
|
201
242
|
},
|
|
202
243
|
"item": {
|
|
203
244
|
"type": "object",
|
|
204
245
|
"required": ["kind"],
|
|
205
|
-
"properties": { "kind": { "enum": ["scalar", "travels", "sheet", "list", "set"] } },
|
|
246
|
+
"properties": { "kind": { "enum": ["scalar", "travels", "sheet", "list", "set", "codes"] } },
|
|
206
247
|
"allOf": [
|
|
207
248
|
{ "if": { "properties": { "kind": { "const": "scalar" } } }, "then": { "$ref": "#/$defs/scalar" } },
|
|
208
249
|
{ "if": { "properties": { "kind": { "const": "travels" } } }, "then": { "$ref": "#/$defs/travels" } },
|
|
209
250
|
{ "if": { "properties": { "kind": { "const": "sheet" } } }, "then": { "$ref": "#/$defs/sheet" } },
|
|
210
251
|
{ "if": { "properties": { "kind": { "const": "list" } } }, "then": { "$ref": "#/$defs/list" } },
|
|
211
|
-
{ "if": { "properties": { "kind": { "const": "set" } } }, "then": { "$ref": "#/$defs/set" } }
|
|
252
|
+
{ "if": { "properties": { "kind": { "const": "set" } } }, "then": { "$ref": "#/$defs/set" } },
|
|
253
|
+
{ "if": { "properties": { "kind": { "const": "codes" } } }, "then": { "$ref": "#/$defs/codes" } }
|
|
212
254
|
]
|
|
213
255
|
}
|
|
214
256
|
}
|