@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.
@@ -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
- OMX_DRIVE_HF_ROLLOFFS_0 = 0,
694
- OMX_DRIVE_HF_ROLLOFFS_12000 = 12000,
695
- OMX_DRIVE_HF_ROLLOFFS_16000 = 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 OMX_DRIVE_HF_ROLLOFFS_0
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 (spec §3.3). A new one is a change here with its own test. */
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 labels: a set's), where `value` has
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 (spec §3.2): two-space indent, one key per line, keys in schema
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 (spec §3.6) and
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 (omx-contract spec §3.6): while every consumer of
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
- if ((a.unit ?? '') !== (b.unit ?? '')) say('major', `unit ${JSON.stringify(a.unit)} -> ${JSON.stringify(b.unit)}`);
98
- if (a.doc !== b.doc || (a.why ?? '') !== (b.why ?? '')) say('patch', 'doc');
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 (spec §3.3): every
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.10.1",
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`. Every define is spelled by openmixer's
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 openmixer's header, outside the include guard, byte for byte. */
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
- return [{ path: 'omxcontract/omx_contract_limits.h', text: renderLimitsHeader(resolved) }];
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 openmixer sheet (test/fixtures/): `{ sheet }` rendered by openmixer's own
182
- * renderer, for the round-trip proof (spec §4.4 step 1).
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 in openmixer keeps names, values and types.
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 js = BANNER + items.map((r) => `\n${docComment(r)}\nexport const ${r.name} = ${JSON.stringify(tsValue(r), null, 2)};\n`).join('') + readouts.js + rule.js;
92
- const dts = BANNER + items.map((r) => `\n${docComment(r)}\nexport declare const ${r.name}: ${tsType(tsValue(r))};\n`).join('') + readouts.dts + rule.dts;
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 labels. 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.",
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
  }