@openmixer/omx-contract 1.2.1 → 1.3.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/render/c.mjs CHANGED
@@ -4,17 +4,13 @@
4
4
  * The `c` target: `omxcontract/omx_contract_limits.h`. Every define is spelled by openmixer'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
- * declaration; c/params-generate.mjs stays here only for renderSheetC, the frozen fixture of the
8
- * round-trip proof.
7
+ * declaration, by reference to the items here.
9
8
  */
10
- import { readFileSync } from 'node:fs';
11
9
  import {
12
10
  genCoreLimitBlock, genDeclaredListsBlock, genDeclaredRates, genDeclaredScalarsBlock, genLimitBlock, renderContractLimits,
13
11
  } from './c/contract-limits-render.mjs';
14
- import { generate } from './c/params-generate.mjs';
15
12
  import { doors } from './doors.mjs';
16
13
 
17
- const PLUGIN_PARAM_H = readFileSync(new URL('./c/omx_plugin_param.h', import.meta.url), 'utf8');
18
14
  const defined = (text) => new Set([...text.matchAll(/^#define (OMX_\w+)/gm)].map((m) => m[1]));
19
15
 
20
16
  /** The rate tail of openmixer's header, outside the include guard, byte for byte. */
@@ -39,6 +35,71 @@ static inline int omx_declared_rates_multiple_of(unsigned base, int *out, int ca
39
35
  `;
40
36
  }
41
37
 
38
+ const constCase = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
39
+
40
+ /** The enumerator names of one set, in order: `OMX_<SET>_<ID>`. */
41
+ export const setEnumerators = (set) => set.ids.map((id) => `OMX_${set.name}_${constCase(id)}`);
42
+
43
+ /**
44
+ * Every set as a C enum: the id's position is its value for named ids, the number itself for numeric
45
+ * ids; `OMX_<SET>_COUNT`, and `OMX_<SET>_DEFAULT` when the set names a default.
46
+ */
47
+ function genDeclaredSetsEnums(sets) {
48
+ return sets.map((set) => {
49
+ const names = setEnumerators(set);
50
+ const numeric = typeof set.ids[0] === 'number';
51
+ const members = set.ids.map((id, i) => ` ${names[i]} = ${numeric ? id : i},`);
52
+ const lines = [`enum omx_${set.name.toLowerCase()} {`, ...members, '};', `#define OMX_${set.name}_COUNT ${set.ids.length}u`];
53
+ if (set.default !== undefined) lines.push(`#define OMX_${set.name}_DEFAULT ${names[set.ids.indexOf(set.default)]}`);
54
+ return lines.join('\n');
55
+ }).join('\n');
56
+ }
57
+
58
+ /**
59
+ * The default band centres of an EQ (lib/eq-defaults.mjs is the rule; this is its C spelling, held
60
+ * equal to it for every count by test/eq-defaults.test.mjs). Writes \`count\` centres to \`out\` and returns
61
+ * \`count\`, or returns 0 for a count that is 0 or has no distinct preferred centres.
62
+ */
63
+ function eqCentresFunction() {
64
+ return `static inline unsigned omx_eq_default_centres(unsigned count, float *out) {
65
+ static const double r10[] = OMX_ISO_THIRD_OCTAVE_CENTRES_HZ_INIT;
66
+ static const double r20[] = OMX_EQ_CENTRE_SERIES_R20_HZ_INIT;
67
+ static const double four[] = OMX_EQ_DEFAULT_CENTRES_FOUR_BAND_HZ_INIT;
68
+ if (count == 0) return 0;
69
+ if (count == 4) {
70
+ for (unsigned i = 0; i < 4; i++) out[i] = (float)four[i];
71
+ return 4;
72
+ }
73
+ const double lo = log((double)OMX_EQ_FREQ_RANGE_MIN);
74
+ const double span = log((double)OMX_EQ_FREQ_RANGE_MAX / (double)OMX_EQ_FREQ_RANGE_MIN);
75
+ for (unsigned s = 0; s < 2; s++) {
76
+ const double *series = s ? r20 : r10;
77
+ const unsigned n = s ? OMX_EQ_CENTRE_SERIES_R20_HZ_COUNT : OMX_ISO_THIRD_OCTAVE_CENTRES_HZ_COUNT;
78
+ int distinct = count <= n;
79
+ for (unsigned i = 0; distinct && i < count; i++) {
80
+ const double t = lo + span * ((double)i + 0.5) / (double)count;
81
+ unsigned best = 0;
82
+ double best_d = 1e300;
83
+ for (unsigned k = 0; k < n; k++) {
84
+ const double d = fabs(log(series[k]) - t);
85
+ if (d < best_d) { best_d = d; best = k; }
86
+ }
87
+ out[i] = (float)series[best];
88
+ if (i > 0 && out[i] == out[i - 1]) distinct = 0;
89
+ }
90
+ if (distinct) return count;
91
+ }
92
+ return 0;
93
+ }
94
+
95
+ /* The default type of band \`index\` of an EQ with \`count\` bands: a low shelf first, a high shelf last, bells
96
+ * between (a single band is a bell). An OMX_EQ_BAND_TYPES_* member. */
97
+ static inline int omx_eq_default_type(unsigned index, unsigned count) {
98
+ if (count < 2) return OMX_EQ_BAND_TYPES_BELL;
99
+ return index == 0 ? OMX_EQ_BAND_TYPES_LOW_SHELF : index == count - 1 ? OMX_EQ_BAND_TYPES_HIGH_SHELF : OMX_EQ_BAND_TYPES_BELL;
100
+ }`;
101
+ }
102
+
42
103
  /** The limits header of the resolved data. */
43
104
  export function renderLimitsHeader(resolved) {
44
105
  const d = doors(resolved);
@@ -48,6 +109,8 @@ export function renderLimitsHeader(resolved) {
48
109
  const above = defined([tables, aliases].join('\n'));
49
110
  const scalars = d.DECLARED_SCALARS.length ? genDeclaredScalarsBlock(d.DECLARED_SCALARS, above) : '';
50
111
  const lists = d.DECLARED_LISTS.length ? genDeclaredListsBlock(d.DECLARED_LISTS, defined([tables, aliases, scalars].join('\n'))) : '';
112
+ const sets = d.DECLARED_SET_ITEMS.length ? genDeclaredSetsEnums(d.DECLARED_SET_ITEMS) : '';
113
+ const centres = resolved.has('EQ_DEFAULT_CENTRES_FOUR_BAND_HZ') ? eqCentresFunction() : '';
51
114
  const sections = [
52
115
  [tables, `/* Per-parameter numeric travel, one block per \`travels\` table named <PREFIX>_LIMITS:
53
116
  * OMX_<PREFIX>_<FIELD>_MIN/_MAX/_DEFAULT, float literals. */`],
@@ -56,11 +119,16 @@ export function renderLimitsHeader(resolved) {
56
119
  [scalars, `/* Every scalar, sheet leaf and single travel's numbers, one block sorted by macro. An integer value
57
120
  * is an integer literal, any other a float literal AND its OMX_<macro>_DOUBLE spelling. */`],
58
121
  [lists, `/* Every list rendered to C, sorted by list: OMX_<LIST>_COUNT and OMX_<LIST>_INIT { a, b, … }. */`],
122
+ [sets, `/* Every set rendered to C, sorted by set: an enum of the ids (a named id's position, or a numeric id's number), \`OMX_<SET>_COUNT\` and,
123
+ * where the set names a default, \`OMX_<SET>_DEFAULT\`. */`],
124
+ [centres, `/* The default bands of an EQ with \`count\` bands: THE one rule, for every strip, host and count (omx-contract's
125
+ * lib/eq-defaults.mjs; the ts render exports the same as eqDefaultCentres and eqDefaultTypes). Needs -lm. */`],
59
126
  ].filter(([body]) => body);
60
127
  return `// SPDX-License-Identifier: GPL-3.0-or-later
61
128
  // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
62
129
  #ifndef OMX_CONTRACT_LIMITS_H
63
130
  #define OMX_CONTRACT_LIMITS_H
131
+ #include <math.h>
64
132
  /*
65
133
  * GENERATED — DO NOT EDIT BY HAND.
66
134
  * Produced by \`omx-contract render --target c\` from FreeMixer/omx-contract's data/, the declaration
@@ -82,13 +150,11 @@ export function renderC(resolved) {
82
150
  }
83
151
 
84
152
  /**
85
- * The `c` target of a frozen openmixer sheet (test/fixtures/): `{ sheet, plugins: [{ header, prefix,
86
- * source, params }] }` rendered by openmixer's own renderer, for the round-trip proof (spec §4.4 step 1).
153
+ * The `c` target of a frozen openmixer sheet (test/fixtures/): `{ sheet }` rendered by openmixer's own
154
+ * renderer, for the round-trip proof (spec §4.4 step 1).
87
155
  */
88
156
  export function renderSheetC(fixture) {
89
157
  const s = fixture.sheet;
90
158
  const sheet = { ...s, CORE_LIMITS: new Map(Object.entries(s.CORE_LIMITS)) };
91
- const files = [{ path: 'omxcontract/omx_contract_limits.h', text: renderContractLimits(sheet) }];
92
- for (const p of fixture.plugins) files.push({ path: `omxcontract/params/${p.header}`, text: generate(p, p.params) });
93
- return files;
159
+ return [{ path: 'omxcontract/omx_contract_limits.h', text: renderContractLimits(sheet) }];
94
160
  }
package/render/doors.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  * (packages/core/src/native-limit-tables.ts, declared-scalars.ts, declared-lists.ts): a `travels`
6
6
  * table named <PREFIX>_LIMITS is a native table; every scalar, sheet and single travel is scalars
7
7
  * (its finite-number leaves, a travel's `unit`/`defaultFrom` labels skipped); a list is a declared
8
- * list unless its item says `"c": { "render": false }`; a `c.alias` is the core-limit spelling.
8
+ * list unless its item says `"c": { "render": false }`; a set is an enum; a `c.alias` is the core-limit spelling.
9
9
  */
10
10
 
11
11
  const constantCase = (key) => key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
@@ -38,7 +38,7 @@ export function doors(resolved) {
38
38
  .sort(([a], [b]) => a.localeCompare(b));
39
39
  const byMacro = new Map();
40
40
  for (const r of items) {
41
- if (isNativeTable(r) || r.kind === 'list') continue;
41
+ if (isNativeTable(r) || r.kind === 'list' || r.kind === 'set') continue;
42
42
  const leaves = numberLeaves(r.value, r.name, r.name.replace(/_(KERNEL|LIMITS)$/, ''));
43
43
  if (leaves === undefined) continue;
44
44
  for (const s of leaves) {
@@ -51,6 +51,9 @@ export function doors(resolved) {
51
51
  const DECLARED_LISTS = items.filter((r) => r.kind === 'list' && r.c?.render !== false)
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
+ const DECLARED_SET_ITEMS = items.filter((r) => r.kind === 'set')
55
+ .map((r) => ({ name: r.name, ids: r.value, default: r.default }))
56
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
54
57
  const ALIASES = new Map();
55
58
  for (const r of items) {
56
59
  if (!r.c?.alias) continue;
@@ -59,5 +62,5 @@ export function doors(resolved) {
59
62
  ALIASES.get(fact)[field] = r.value;
60
63
  }
61
64
  const STANDARD_SAMPLE_RATES = resolved.get('STANDARD_SAMPLE_RATES')?.value;
62
- return { NATIVE_LIMIT_TABLES, DECLARED_SCALARS, DECLARED_LISTS, ALIASES, STANDARD_SAMPLE_RATES };
65
+ return { NATIVE_LIMIT_TABLES, DECLARED_SCALARS, DECLARED_LISTS, DECLARED_SET_ITEMS, ALIASES, STANDARD_SAMPLE_RATES };
63
66
  }
package/render/ts.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-or-later
2
2
  // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ import { eqDefaultBands, eqDefaultCentres, eqDefaultParams, eqDefaultTypes } from '../lib/eq-defaults.mjs';
4
+
3
5
  /**
4
6
  * The `ts` target: one ESM module and its type declarations, every item exported under its own
5
7
  * name with its literal type (`as const`), so a re-export in openmixer keeps names, values and types.
@@ -32,10 +34,46 @@ const BANNER = `// SPDX-License-Identifier: GPL-3.0-or-later
32
34
  // GENERATED by \`omx-contract render --target ts\` from FreeMixer/omx-contract's data/ — do not edit.
33
35
  `;
34
36
 
37
+ /**
38
+ * The default EQ rule (lib/eq-defaults.mjs) as functions of the module: the library's own source,
39
+ * with the declared parameters bound, so the ts reader runs the code the tests hold equal to the c one.
40
+ */
41
+ function eqRule(resolved) {
42
+ if (!resolved.has('EQ_DEFAULT_CENTRES_FOUR_BAND_HZ')) return { js: '', dts: '' };
43
+ const bound = JSON.stringify(eqDefaultParams(resolved), null, 2);
44
+ const fn = (f, as) => f.toString().replace(`function ${f.name}(`, `function ${as}(`);
45
+ const js = `
46
+ /** The declared parameters of the default EQ rule: the four-band set, the preferred series, the band frequency travel, the band gain and Q. */
47
+ const EQ_DEFAULT_RULE = ${bound};
48
+ ${fn(eqDefaultCentres, 'centres')}
49
+ ${fn(eqDefaultTypes, 'types')}
50
+ ${fn(eqDefaultBands, 'bands')}
51
+
52
+ /** The default band centres (Hz) of an EQ with \`count\` bands: THE one rule, for every strip, host and count. */
53
+ export function eqDefaultCentres(count) { return centres(count, EQ_DEFAULT_RULE); }
54
+
55
+ /** The default band types, by EQ_BAND_TYPES id: a low shelf, bells, a high shelf (a single band is a bell). */
56
+ export function eqDefaultTypes(count) { return types(count); }
57
+
58
+ /** Every default band of an EQ with \`count\` bands: type, centre, gain and Q. */
59
+ export function eqDefaultBands(count) { return bands(count, EQ_DEFAULT_RULE); }
60
+ `;
61
+ const dts = `
62
+ /** The default band centres (Hz) of an EQ with \`count\` bands: THE one rule, for every strip, host and count. */
63
+ export declare function eqDefaultCentres(count: number): number[];
64
+ /** The default band types, by EQ_BAND_TYPES id: a low shelf, bells, a high shelf (a single band is a bell). */
65
+ export declare function eqDefaultTypes(count: number): string[];
66
+ /** Every default band of an EQ with \`count\` bands: type, centre, gain and Q. */
67
+ export declare function eqDefaultBands(count: number): { type: string; freqHz: number; gainDb: number; q: number }[];
68
+ `;
69
+ return { js, dts };
70
+ }
71
+
35
72
  /** Every file of the `ts` target: `[{ path, text }]`. */
36
73
  export function renderTs(resolved) {
37
74
  const items = [...resolved.values()];
38
- const js = BANNER + items.map((r) => `\n${docComment(r)}\nexport const ${r.name} = ${JSON.stringify(tsValue(r), null, 2)};\n`).join('');
39
- const dts = BANNER + items.map((r) => `\n${docComment(r)}\nexport declare const ${r.name}: ${tsType(tsValue(r))};\n`).join('');
75
+ const rule = eqRule(resolved);
76
+ const js = BANNER + items.map((r) => `\n${docComment(r)}\nexport const ${r.name} = ${JSON.stringify(tsValue(r), null, 2)};\n`).join('') + rule.js;
77
+ const dts = BANNER + items.map((r) => `\n${docComment(r)}\nexport declare const ${r.name}: ${tsType(tsValue(r))};\n`).join('') + rule.dts;
40
78
  return [{ path: 'omx-contract.js', text: js }, { path: 'omx-contract.d.ts', text: dts }];
41
79
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/FreeMixer/omx-contract/raw/main/schema/omx-contract.schema.json",
4
4
  "title": "omx-contract data file",
5
- "description": "A map from an export name to one item: a scalar, a travels table or travel, a sheet or a list. GPL-3.0-or-later.",
5
+ "description": "A map from an export name to one item: a scalar, a travels table or travel, a sheet, a list or a set. GPL-3.0-or-later.",
6
6
  "type": "object",
7
7
  "properties": {
8
8
  "$schema": { "type": "string" }
@@ -15,6 +15,7 @@
15
15
  "enum": ["dB", "dBFS", "ms", "s", "min", "Hz", "oct", "cents", "%", "x", ""]
16
16
  },
17
17
  "text": { "type": "string", "minLength": 1 },
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/." },
18
19
  "name": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" },
19
20
  "ref": {
20
21
  "type": "object",
@@ -37,6 +38,15 @@
37
38
  "required": ["derive", "of"],
38
39
  "additionalProperties": false
39
40
  },
41
+ {
42
+ "type": "object",
43
+ "properties": {
44
+ "derive": { "const": "ceilSum" },
45
+ "of": { "type": "array", "items": { "$ref": "#/$defs/number" }, "minItems": 2 }
46
+ },
47
+ "required": ["derive", "of"],
48
+ "additionalProperties": false
49
+ },
40
50
  {
41
51
  "type": "object",
42
52
  "properties": {
@@ -58,6 +68,7 @@
58
68
  "unit": { "$ref": "#/$defs/unit" },
59
69
  "default": { "$ref": "#/$defs/number" },
60
70
  "defaultFrom": { "enum": ["desk"] },
71
+ "scale": { "type": "string", "pattern": "^[a-z]+$", "description": "How a host draws the travel: an id of PARAM_SCALES." },
61
72
  "byKind": {
62
73
  "type": "object",
63
74
  "propertyNames": { "pattern": "^[a-z][A-Za-z0-9]*$" },
@@ -80,6 +91,7 @@
80
91
  "type": "object",
81
92
  "properties": {
82
93
  "kind": { "const": "scalar" },
94
+ "since": { "$ref": "#/$defs/since" },
83
95
  "doc": { "$ref": "#/$defs/text" },
84
96
  "why": { "$ref": "#/$defs/text" },
85
97
  "unit": { "$ref": "#/$defs/unit" },
@@ -92,6 +104,7 @@
92
104
  "type": "object",
93
105
  "properties": {
94
106
  "kind": { "const": "travels" },
107
+ "since": { "$ref": "#/$defs/since" },
95
108
  "doc": { "$ref": "#/$defs/text" },
96
109
  "why": { "$ref": "#/$defs/text" },
97
110
  "fields": {
@@ -125,6 +138,7 @@
125
138
  "type": "object",
126
139
  "properties": {
127
140
  "kind": { "const": "sheet" },
141
+ "since": { "$ref": "#/$defs/since" },
128
142
  "doc": { "$ref": "#/$defs/text" },
129
143
  "why": { "$ref": "#/$defs/text" },
130
144
  "value": { "$ref": "#/$defs/sheetValue" }
@@ -136,6 +150,7 @@
136
150
  "type": "object",
137
151
  "properties": {
138
152
  "kind": { "const": "list" },
153
+ "since": { "$ref": "#/$defs/since" },
139
154
  "doc": { "$ref": "#/$defs/text" },
140
155
  "why": { "$ref": "#/$defs/text" },
141
156
  "unit": { "$ref": "#/$defs/unit" },
@@ -149,15 +164,36 @@
149
164
  "required": ["kind", "doc", "unit", "values"],
150
165
  "additionalProperties": false
151
166
  },
167
+ "set": {
168
+ "type": "object",
169
+ "description": "Ordered ids with an optional default and labels. The ids and their order are the contract: all strings (the id is the name, its position the value) or all non-negative integers (the id is the value). Labels are face text and optional.",
170
+ "properties": {
171
+ "kind": { "const": "set" },
172
+ "since": { "$ref": "#/$defs/since" },
173
+ "doc": { "$ref": "#/$defs/text" },
174
+ "why": { "$ref": "#/$defs/text" },
175
+ "ids": {
176
+ "oneOf": [
177
+ { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9]*$" }, "minItems": 1, "uniqueItems": true },
178
+ { "type": "array", "items": { "type": "integer", "minimum": 0 }, "minItems": 1, "uniqueItems": true }
179
+ ]
180
+ },
181
+ "default": { "oneOf": [{ "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9]*$" }, { "type": "integer", "minimum": 0 }] },
182
+ "labels": { "type": "array", "items": { "$ref": "#/$defs/text" }, "minItems": 1 }
183
+ },
184
+ "required": ["kind", "doc", "ids"],
185
+ "additionalProperties": false
186
+ },
152
187
  "item": {
153
188
  "type": "object",
154
189
  "required": ["kind"],
155
- "properties": { "kind": { "enum": ["scalar", "travels", "sheet", "list"] } },
190
+ "properties": { "kind": { "enum": ["scalar", "travels", "sheet", "list", "set"] } },
156
191
  "allOf": [
157
192
  { "if": { "properties": { "kind": { "const": "scalar" } } }, "then": { "$ref": "#/$defs/scalar" } },
158
193
  { "if": { "properties": { "kind": { "const": "travels" } } }, "then": { "$ref": "#/$defs/travels" } },
159
194
  { "if": { "properties": { "kind": { "const": "sheet" } } }, "then": { "$ref": "#/$defs/sheet" } },
160
- { "if": { "properties": { "kind": { "const": "list" } } }, "then": { "$ref": "#/$defs/list" } }
195
+ { "if": { "properties": { "kind": { "const": "list" } } }, "then": { "$ref": "#/$defs/list" } },
196
+ { "if": { "properties": { "kind": { "const": "set" } } }, "then": { "$ref": "#/$defs/set" } }
161
197
  ]
162
198
  }
163
199
  }