@openmixer/omx-contract 2.10.0 → 2.11.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 CHANGED
@@ -49,7 +49,7 @@ pkg-config --cflags omx-contract
49
49
  ```
50
50
 
51
51
  Pin one version and build against its renders instead of keeping a copy: the installed package at exactly
52
- that version (`pkg-config --exact-version=2.10.0 omx-contract`), else the npm tarball, unpacked, and
52
+ that version (`pkg-config --exact-version=2.11.0 omx-contract`), else the npm tarball, unpacked, and
53
53
  `omx-contract render --target c --out build/omx-contract/include`. A kernel's controls and readouts, in their
54
54
  order, are in `share/omx-contract/omx-contract.json`; how a declaration is written is in
55
55
  [docs/kernel-files.md](docs/kernel-files.md), and what a readout is in [docs/readouts.md](docs/readouts.md).
@@ -149,17 +149,23 @@
149
149
  {
150
150
  "name": "hfRolloff",
151
151
  "kind": "choice",
152
- "doc": "The drive's post-curve HIGH-FREQUENCY ROLL-OFF corner, in Hz: the id is the corner, and 0 names no roll-off (the section does not run: `omx_drive.h`'s `struct omx_drive.hf_on` is 0). The console's drive row offers off, 12000 and 16000 (`DriveState.hfRolloff`, `DRIVE_HF_ROLLOFFS`) and comes up off, as omx-dsp's drive face does.",
152
+ "doc": "The drive's post-curve HIGH-FREQUENCY ROLL-OFF: `off` (the section does not run: `omx_drive.h`'s `struct omx_drive.hf_on` is 0), `12000` or `16000`, each carrying its corner in Hz as its value and `off` none (operator ruling, 2026-10-13: the choice is an enumeration and the frequency is data of each member, so no 0 Hz doubles as a sentinel). The ids are the console's drive-row wire vocabulary (`DriveState.hfRolloff`, what the drive row's OPTIONS publishes and a session stores), so the console re-exports this set; a kernel reads the corner from the id's value. It comes up off, as omx-dsp's drive face does.",
153
+ "unit": "Hz",
153
154
  "ids": [
154
- 0,
155
- 12000,
156
- 16000
155
+ "off",
156
+ "12000",
157
+ "16000"
157
158
  ],
158
- "default": 0,
159
+ "default": "off",
159
160
  "labels": [
160
161
  "Off",
161
162
  "12 kHz",
162
163
  "16 kHz"
164
+ ],
165
+ "values": [
166
+ null,
167
+ 12000,
168
+ 16000
163
169
  ]
164
170
  },
165
171
  {
@@ -543,10 +543,10 @@ export declare const DRIVE_AUTO_GAINS: readonly ["off", "on"];
543
543
  export declare const DRIVE_STEREO_LINKS: readonly ["off", "on"];
544
544
 
545
545
  /**
546
- * The drive's post-curve HIGH-FREQUENCY ROLL-OFF corner, in Hz: the id is the corner, and 0 names no roll-off (the section does not run: `omx_drive.h`'s `struct omx_drive.hf_on` is 0). The console's drive row offers off, 12000 and 16000 (`DriveState.hfRolloff`, `DRIVE_HF_ROLLOFFS`) and comes up off, as omx-dsp's drive face does.
546
+ * The drive's post-curve HIGH-FREQUENCY ROLL-OFF: `off` (the section does not run: `omx_drive.h`'s `struct omx_drive.hf_on` is 0), `12000` or `16000`, each carrying its corner in Hz as its value and `off` none (operator ruling, 2026-10-13: the choice is an enumeration and the frequency is data of each member, so no 0 Hz doubles as a sentinel). The ids are the console's drive-row wire vocabulary (`DriveState.hfRolloff`, what the drive row's OPTIONS publishes and a session stores), so the console re-exports this set; a kernel reads the corner from the id's value. It comes up off, as omx-dsp's drive face does.
547
547
  * @see data/kernels/drive.json
548
548
  */
549
- export declare const DRIVE_HF_ROLLOFFS: readonly [0, 12000, 16000];
549
+ export declare const DRIVE_HF_ROLLOFFS: readonly ["off", "12000", "16000"];
550
550
 
551
551
  /**
552
552
  * The drive's OVERSAMPLING factor: the shaper runs inside the console's one oversampler at 1x, 2x or 4x, and the id is the factor (`omx_drive.h`'s `omx_drive_factor_of` member set, `struct omx_drive.os_factor`). A field of the console's drive row (`DriveState.oversample`, the console's DRIVE_OVERSAMPLE_FACTORS; the set is named for its control), 4 at come-up there and in omx-dsp's drive face (OMX_DRIVE_INSTANCE_FACTOR, the oversampler's largest).
@@ -2132,6 +2132,16 @@ export declare const ORACLE_FLOOR_RATES: readonly [44100, 48000, 96000, 192000];
2132
2132
  */
2133
2133
  export declare const RME_RATES: readonly [32000, 44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000];
2134
2134
 
2135
+ /**
2136
+ * The value each id of DRIVE_HF_ROLLOFFS carries, in "Hz"; null for an id that carries none.
2137
+ * @see data/kernels/drive.json
2138
+ */
2139
+ export declare const DRIVE_HF_ROLLOFFS_VALUES: {
2140
+ readonly "12000": 12000;
2141
+ readonly "16000": 16000;
2142
+ readonly off: null;
2143
+ };
2144
+
2135
2145
  /**
2136
2146
  * Every kernel's readouts, the meters a host draws, each kernel's in its declared order: the position is the
2137
2147
  * readout's id. Every reference is resolved.
@@ -578,13 +578,13 @@ export const DRIVE_STEREO_LINKS = [
578
578
  ];
579
579
 
580
580
  /**
581
- * The drive's post-curve HIGH-FREQUENCY ROLL-OFF corner, in Hz: the id is the corner, and 0 names no roll-off (the section does not run: `omx_drive.h`'s `struct omx_drive.hf_on` is 0). The console's drive row offers off, 12000 and 16000 (`DriveState.hfRolloff`, `DRIVE_HF_ROLLOFFS`) and comes up off, as omx-dsp's drive face does.
581
+ * The drive's post-curve HIGH-FREQUENCY ROLL-OFF: `off` (the section does not run: `omx_drive.h`'s `struct omx_drive.hf_on` is 0), `12000` or `16000`, each carrying its corner in Hz as its value and `off` none (operator ruling, 2026-10-13: the choice is an enumeration and the frequency is data of each member, so no 0 Hz doubles as a sentinel). The ids are the console's drive-row wire vocabulary (`DriveState.hfRolloff`, what the drive row's OPTIONS publishes and a session stores), so the console re-exports this set; a kernel reads the corner from the id's value. It comes up off, as omx-dsp's drive face does.
582
582
  * @see data/kernels/drive.json
583
583
  */
584
584
  export const DRIVE_HF_ROLLOFFS = [
585
- 0,
586
- 12000,
587
- 16000
585
+ "off",
586
+ "12000",
587
+ "16000"
588
588
  ];
589
589
 
590
590
  /**
@@ -2351,6 +2351,16 @@ export const RME_RATES = [
2351
2351
  192000
2352
2352
  ];
2353
2353
 
2354
+ /**
2355
+ * The value each id of DRIVE_HF_ROLLOFFS carries, in "Hz"; null for an id that carries none.
2356
+ * @see data/kernels/drive.json
2357
+ */
2358
+ export const DRIVE_HF_ROLLOFFS_VALUES = {
2359
+ "12000": 12000,
2360
+ "16000": 16000,
2361
+ "off": null
2362
+ };
2363
+
2354
2364
  /**
2355
2365
  * Every kernel's readouts, the meters a host draws, each kernel's in its declared order: the position is the
2356
2366
  * readout's id. Every reference is resolved.
@@ -690,12 +690,22 @@ enum omx_drive_curves {
690
690
  };
691
691
  #define OMX_DRIVE_CURVES_COUNT 4u
692
692
  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,
693
+ OMX_DRIVE_HF_ROLLOFFS_OFF = 0,
694
+ OMX_DRIVE_HF_ROLLOFFS_12000 = 1,
695
+ OMX_DRIVE_HF_ROLLOFFS_16000 = 2,
696
696
  };
697
697
  #define OMX_DRIVE_HF_ROLLOFFS_COUNT 3u
698
- #define OMX_DRIVE_HF_ROLLOFFS_DEFAULT OMX_DRIVE_HF_ROLLOFFS_0
698
+ #define OMX_DRIVE_HF_ROLLOFFS_DEFAULT OMX_DRIVE_HF_ROLLOFFS_OFF
699
+ #define OMX_DRIVE_HF_ROLLOFFS_UNIT "Hz"
700
+ #define OMX_DRIVE_HF_ROLLOFFS_12000_VALUE 12000.0f
701
+ #define OMX_DRIVE_HF_ROLLOFFS_16000_VALUE 16000.0f
702
+ static inline int omx_drive_hf_rolloffs_value(int id, float *out) {
703
+ switch (id) {
704
+ case OMX_DRIVE_HF_ROLLOFFS_12000: *out = OMX_DRIVE_HF_ROLLOFFS_12000_VALUE; return 1;
705
+ case OMX_DRIVE_HF_ROLLOFFS_16000: *out = OMX_DRIVE_HF_ROLLOFFS_16000_VALUE; return 1;
706
+ default: return 0;
707
+ }
708
+ }
699
709
  enum omx_drive_oversamples {
700
710
  OMX_DRIVE_OVERSAMPLES_1 = 1,
701
711
  OMX_DRIVE_OVERSAMPLES_2 = 2,
package/lib/data.mjs CHANGED
@@ -262,7 +262,7 @@ export function lagrangeReadL1(order) {
262
262
  const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
263
263
 
264
264
  /**
265
- * Resolve every item: `{ name, rel, kind, doc, why?, unit?, c?, shape?, default?, labels?, value }` (default and labels: a set's), where `value` has
265
+ * 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
266
  * every `{ref}` and `{derive}` evaluated. The returned map also carries `kernels`, every kernel's ordered controls
267
267
  * (kernelControls) and, for a kernel that declares them, its ordered `readouts` `{ name, doc, unit, min, max, scale,
268
268
  * rateHz }` with every reference resolved, which the json render publishes beside the items.
@@ -305,6 +305,7 @@ export function resolveData(data) {
305
305
  if (item.kind === 'set') {
306
306
  if (item.default !== undefined) r.default = setDefault(item, (v) => val(v, name), name);
307
307
  if (item.labels !== undefined) r.labels = item.labels;
308
+ if (item.values !== undefined) r.values = val(item.values, name);
308
309
  }
309
310
  if (item.kind === 'travels') r.shape = item.fields ? 'table' : 'travel';
310
311
  r.value = val(rawValue(item), name);
package/lib/semver.mjs CHANGED
@@ -7,9 +7,10 @@
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
@@ -23,7 +24,7 @@ const RANK = { none: 0, patch: 1, minor: 2, major: 3 };
23
24
  * The development-phase rule (omx-contract spec §3.6): 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
- * outside the list.
27
+ * outside the list. Until then a MAJOR class is released as a minor bump (see refusal).
27
28
  */
28
29
  export const DEVELOPMENT_PHASE = Object.freeze({ consumers: Object.freeze(['omx-dsp', 'omx-plugins', 'openmixer']) });
29
30
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
@@ -94,7 +95,8 @@ 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
+ // 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)}`);
98
100
  if (a.doc !== b.doc || (a.why ?? '') !== (b.why ?? '')) 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');
@@ -123,11 +125,18 @@ 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
+ }
131
140
  break;
132
141
  default:
133
142
  break;
@@ -155,9 +164,16 @@ export function bumpOf(from, to) {
155
164
  return 'none';
156
165
  }
157
166
 
158
- /** null when bumping `from` -> `to` covers `level`, else the refusal. */
159
- export function refusal(level, from, to) {
167
+ /**
168
+ * null when bumping `from` -> `to` covers `level`, else the refusal. In the development phase no
169
+ * release takes a major bump (operator ruling 2026-10-13: majors wait for the first public
170
+ * release), so a MAJOR class is covered by a minor bump; the classes are still reported as found.
171
+ */
172
+ export function refusal(level, from, to, { developmentPhase = DEVELOPMENT_PHASE } = {}) {
160
173
  let bump;
161
174
  try { bump = bumpOf(from, to); } catch (e) { return e.message; }
162
- return RANK[bump] >= RANK[level] ? null : `the changes since ${from} are ${level.toUpperCase()}, and ${from} -> ${to} is a ${bump} bump`;
175
+ const need = developmentPhase && level === 'major' ? 'minor' : level;
176
+ if (RANK[bump] >= RANK[need]) return null;
177
+ const phase = need === level ? '' : ' (at least a minor in the development phase)';
178
+ return `the changes since ${from} are ${level.toUpperCase()}, and ${from} -> ${to} is a ${bump} bump${phase}`;
163
179
  }
package/lib/validate.mjs CHANGED
@@ -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) {
@@ -130,6 +138,9 @@ function semanticRules(resolved, say) {
130
138
  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
139
  }
132
140
  }
141
+ for (const r of resolved.values()) {
142
+ 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`);
143
+ }
133
144
  if (resolved.has('READOUTS') && Object.values(resolved.kernels ?? {}).some((k) => k.readouts)) {
134
145
  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
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmixer/omx-contract",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "The FreeMixer declarations as data: a JSON Schema, the data, and one CLI that renders them into C, TypeScript and JSON",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
package/render/c.mjs CHANGED
@@ -43,8 +43,26 @@ export const setEnumerators = (set) => set.ids.map((id) => `OMX_${set.name}_${co
43
43
 
44
44
  /**
45
45
  * 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.
46
+ * ids; `OMX_<SET>_COUNT`, and `OMX_<SET>_DEFAULT` when the set names a default. A set whose ids carry values adds
47
+ * `OMX_<SET>_UNIT`, `OMX_<SET>_<ID>_VALUE` for each id that carries one, and `omx_<set>_value(id, &out)`, which writes
48
+ * the id's value and returns 1, or returns 0 for an id that carries none: a kernel reads the number from the id
49
+ * (the drive's roll-off corner), never a sentinel.
47
50
  */
51
+ function genSetValues(set, names) {
52
+ const fn = `omx_${set.name.toLowerCase()}_value`;
53
+ const carried = set.ids.map((id, i) => [names[i], set.values[i]]).filter(([, v]) => v !== null);
54
+ return [
55
+ `#define OMX_${set.name}_UNIT ${cStringLiteral(set.unit)}`,
56
+ ...carried.map(([n, v]) => `#define ${n}_VALUE ${floatLiteral(v)}`),
57
+ `static inline int ${fn}(int id, float *out) {`,
58
+ ' switch (id) {',
59
+ ...carried.map(([n]) => ` case ${n}: *out = ${n}_VALUE; return 1;`),
60
+ ' default: return 0;',
61
+ ' }',
62
+ '}',
63
+ ];
64
+ }
65
+
48
66
  function genDeclaredSetsEnums(sets) {
49
67
  return sets.map((set) => {
50
68
  const names = setEnumerators(set);
@@ -52,6 +70,7 @@ function genDeclaredSetsEnums(sets) {
52
70
  const members = set.ids.map((id, i) => ` ${names[i]} = ${numeric ? id : i},`);
53
71
  const lines = [`enum omx_${set.name.toLowerCase()} {`, ...members, '};', `#define OMX_${set.name}_COUNT ${set.ids.length}u`];
54
72
  if (set.default !== undefined) lines.push(`#define OMX_${set.name}_DEFAULT ${names[set.ids.indexOf(set.default)]}`);
73
+ if (set.values) lines.push(...genSetValues(set, names));
55
74
  return lines.join('\n');
56
75
  }).join('\n');
57
76
  }
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
@@ -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
  }
@@ -181,7 +181,7 @@
181
181
  },
182
182
  "set": {
183
183
  "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.",
184
+ "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
185
  "properties": {
186
186
  "kind": { "const": "set" },
187
187
  "since": { "$ref": "#/$defs/since" },
@@ -194,9 +194,17 @@
194
194
  ]
195
195
  },
196
196
  "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 }
197
+ "labels": { "type": "array", "items": { "$ref": "#/$defs/text" }, "minItems": 1 },
198
+ "unit": { "$ref": "#/$defs/unit" },
199
+ "values": {
200
+ "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.",
201
+ "type": "array",
202
+ "items": { "oneOf": [{ "$ref": "#/$defs/number" }, { "type": "null" }] },
203
+ "minItems": 1
204
+ }
198
205
  },
199
206
  "required": ["kind", "doc", "ids"],
207
+ "dependentRequired": { "values": ["unit"], "unit": ["values"] },
200
208
  "additionalProperties": false
201
209
  },
202
210
  "item": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmixer/omx-contract",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "items": {
5
5
  "PAN_RANGE": {
6
6
  "rel": "data/kernels/balance.json",
@@ -686,17 +686,23 @@
686
686
  "DRIVE_HF_ROLLOFFS": {
687
687
  "rel": "data/kernels/drive.json",
688
688
  "kind": "set",
689
- "doc": "The drive's post-curve HIGH-FREQUENCY ROLL-OFF corner, in Hz: the id is the corner, and 0 names no roll-off (the section does not run: `omx_drive.h`'s `struct omx_drive.hf_on` is 0). The console's drive row offers off, 12000 and 16000 (`DriveState.hfRolloff`, `DRIVE_HF_ROLLOFFS`) and comes up off, as omx-dsp's drive face does.",
690
- "default": 0,
689
+ "doc": "The drive's post-curve HIGH-FREQUENCY ROLL-OFF: `off` (the section does not run: `omx_drive.h`'s `struct omx_drive.hf_on` is 0), `12000` or `16000`, each carrying its corner in Hz as its value and `off` none (operator ruling, 2026-10-13: the choice is an enumeration and the frequency is data of each member, so no 0 Hz doubles as a sentinel). The ids are the console's drive-row wire vocabulary (`DriveState.hfRolloff`, what the drive row's OPTIONS publishes and a session stores), so the console re-exports this set; a kernel reads the corner from the id's value. It comes up off, as omx-dsp's drive face does.",
690
+ "unit": "Hz",
691
+ "default": "off",
691
692
  "labels": [
692
693
  "Off",
693
694
  "12 kHz",
694
695
  "16 kHz"
695
696
  ],
696
- "value": [
697
- 0,
697
+ "values": [
698
+ null,
698
699
  12000,
699
700
  16000
701
+ ],
702
+ "value": [
703
+ "off",
704
+ "12000",
705
+ "16000"
700
706
  ]
701
707
  },
702
708
  "DRIVE_OVERSAMPLES": {