@openmixer/omx-contract 1.2.1 → 2.0.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 +41 -4
- package/bin/omx-contract.mjs +14 -3
- package/data/kernels/balance.json +18 -14
- package/data/kernels/band_dyn.json +132 -0
- package/data/kernels/chorus.json +87 -56
- package/data/kernels/comp.json +67 -29
- package/data/kernels/deesser.json +114 -86
- package/data/kernels/delay.json +67 -57
- package/data/kernels/drive.json +118 -87
- package/data/kernels/eq.json +284 -113
- package/data/kernels/fbs.json +150 -0
- package/data/kernels/flanger.json +74 -44
- package/data/kernels/gate.json +76 -21
- package/data/kernels/geq.json +32 -27
- package/data/kernels/hrp.json +68 -0
- package/data/kernels/limiter.json +25 -9
- package/data/kernels/mixmatrix.json +11 -0
- package/data/kernels/phaser.json +110 -100
- package/data/kernels/pitch.json +35 -17
- package/data/kernels/reverb.json +176 -142
- package/data/kernels/rotary.json +189 -177
- package/data/kernels/rta.json +17 -0
- package/data/kernels/transient.json +64 -36
- package/data/kernels/tremolo.json +59 -48
- package/data/primitives.json +51 -0
- package/data/rates.json +29 -0
- package/dist/ts/omx-contract.d.ts +527 -21
- package/dist/ts/omx-contract.js +832 -57
- package/include/omxcontract/omx_contract_limits.h +233 -0
- package/lib/data.mjs +115 -3
- package/lib/eq-defaults.mjs +87 -0
- package/lib/fmt.mjs +24 -4
- package/lib/semver.mjs +12 -2
- package/lib/validate.mjs +99 -3
- package/package.json +1 -1
- package/render/c.mjs +76 -10
- package/render/doors.mjs +6 -3
- package/render/ts.mjs +40 -2
- package/schema/omx-contract.schema.json +39 -3
- package/share/omx-contract/omx-contract.json +759 -71
- package/render/c/omx_plugin_param.h +0 -25
- package/render/c/params-generate.mjs +0 -83
package/lib/semver.mjs
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
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
|
-
* changed or removed)
|
|
11
|
-
* minor an item, field, sheet leaf
|
|
10
|
+
* or set id changed or removed; a set's default added, removed)
|
|
11
|
+
* minor an item, field, sheet leaf, list member or set id added (appended); a travel widened;
|
|
12
12
|
* a default moved inside its travel; a step changed
|
|
13
13
|
* patch doc and why text
|
|
14
14
|
*/
|
|
@@ -32,6 +32,7 @@ function travelChanges(at, a, b, say) {
|
|
|
32
32
|
if (a.default !== b.default) say(a.default === undefined || b.default === undefined ? 'major' : 'minor', `${at}: default ${a.default} -> ${b.default}`);
|
|
33
33
|
if (a.step !== b.step) say('minor', `${at}: step ${a.step} -> ${b.step}`);
|
|
34
34
|
if (a.defaultFrom !== b.defaultFrom) say('minor', `${at}: defaultFrom ${a.defaultFrom} -> ${b.defaultFrom}`);
|
|
35
|
+
if (a.scale !== b.scale) say('minor', `${at}: scale ${a.scale} -> ${b.scale}`);
|
|
35
36
|
const ka = a.byKind ?? {};
|
|
36
37
|
const kb = b.byKind ?? {};
|
|
37
38
|
for (const k of Object.keys(ka)) if (!(k in kb)) say('major', `${at}.byKind.${k} removed`);
|
|
@@ -92,6 +93,15 @@ export function classify(prev, next, { developmentPhase = DEVELOPMENT_PHASE } =
|
|
|
92
93
|
});
|
|
93
94
|
if (b.value.length > a.value.length) say('minor', `${name}: ${b.value.length - a.value.length} member(s) appended`);
|
|
94
95
|
break;
|
|
96
|
+
case 'set':
|
|
97
|
+
a.value.forEach((v, i) => {
|
|
98
|
+
if (i >= b.value.length) say('major', `${name}[${i}] removed`);
|
|
99
|
+
else if (b.value[i] !== v) say('major', `${name}[${i}]: ${v} -> ${b.value[i]}`);
|
|
100
|
+
});
|
|
101
|
+
if (b.value.length > a.value.length) say('minor', `${name}: ${b.value.length - a.value.length} id(s) appended`);
|
|
102
|
+
if (a.default !== b.default) say(a.default === undefined || b.default === undefined ? 'major' : 'minor', `${name}: default ${a.default} -> ${b.default}`);
|
|
103
|
+
if (!same(a.labels, b.labels)) say('patch', `${name}: labels`);
|
|
104
|
+
break;
|
|
95
105
|
default:
|
|
96
106
|
break;
|
|
97
107
|
}
|
package/lib/validate.mjs
CHANGED
|
@@ -8,12 +8,47 @@
|
|
|
8
8
|
import { readFileSync } from 'node:fs';
|
|
9
9
|
import { posix } from 'node:path';
|
|
10
10
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
11
|
-
import { resolveData } from './data.mjs';
|
|
11
|
+
import { KERNEL_SECTIONS, entriesOf, isKernelFile, resolveData } from './data.mjs';
|
|
12
|
+
import { eqDefaultBands, eqDefaultParams } from './eq-defaults.mjs';
|
|
12
13
|
import { renderC } from '../render/c.mjs';
|
|
13
14
|
import { renderTs } from '../render/ts.mjs';
|
|
14
15
|
|
|
15
16
|
const SCHEMA = JSON.parse(readFileSync(new URL('../schema/omx-contract.schema.json', import.meta.url), 'utf8'));
|
|
16
17
|
let compiled;
|
|
18
|
+
/**
|
|
19
|
+
* The kernel-file shape (2.0.0), which the item schema cannot see: only the four sections at the top; every control
|
|
20
|
+
* named, of kind travel or choice, unique by name, a travel carrying a `travel` and a choice its `ids`; a control's
|
|
21
|
+
* `table` declared under `tables`; an aggregate's fields naming controls; a constant a scalar, a list or a sheet.
|
|
22
|
+
* A kernel file written as a flat item map (the 1.x shape) is refused as a whole. Any other data file returns [].
|
|
23
|
+
*/
|
|
24
|
+
export function shapeProblems(rel, doc) {
|
|
25
|
+
if (!isKernelFile(rel)) return [];
|
|
26
|
+
const sections = KERNEL_SECTIONS;
|
|
27
|
+
const keys = Object.keys(doc).filter((k) => k !== '$schema');
|
|
28
|
+
if (keys.length && !keys.some((k) => sections.includes(k))) {
|
|
29
|
+
return [`${rel} is a flat item map (the 1.x shape): a kernel file declares its items under ${sections.join(', ')}`];
|
|
30
|
+
}
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const k of keys) if (!sections.includes(k)) out.push(`unknown top-level key ${k}: a kernel file holds ${sections.join(', ')}`);
|
|
33
|
+
const controls = doc.controls ?? [];
|
|
34
|
+
if (!Array.isArray(controls)) return [...out, 'controls must be a list'];
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
for (const [i, c] of controls.entries()) {
|
|
37
|
+
if (typeof c.name !== 'string' || !/^[a-z][A-Za-z0-9]*$/.test(c.name)) out.push(`controls[${i}]: name ${JSON.stringify(c.name)} is not a lowerCamel identifier`);
|
|
38
|
+
if (seen.has(c.name)) out.push(`controls[${i}]: name ${c.name} declared twice`);
|
|
39
|
+
seen.add(c.name);
|
|
40
|
+
if (c.kind === 'travel' && !c.travel) out.push(`controls[${i}] ${c.name}: a travel without "travel"`);
|
|
41
|
+
else if (c.kind === 'choice' && !Array.isArray(c.ids)) out.push(`controls[${i}] ${c.name}: a choice without "ids"`);
|
|
42
|
+
else if (c.kind !== 'travel' && c.kind !== 'choice') out.push(`controls[${i}] ${c.name}: kind ${JSON.stringify(c.kind)} is neither travel nor choice`);
|
|
43
|
+
if (c.table && !(doc.tables && c.table in doc.tables)) out.push(`controls[${i}] ${c.name}: table ${c.table} is not declared`);
|
|
44
|
+
}
|
|
45
|
+
for (const [n, a] of Object.entries(doc.aggregates ?? {})) for (const f of a.fields ?? []) if (!seen.has(f)) out.push(`aggregate ${n}: field ${f} names no control`);
|
|
46
|
+
for (const [n, it] of Object.entries(doc.constants ?? {})) {
|
|
47
|
+
if (!['scalar', 'list', 'sheet'].includes(it?.kind)) out.push(`constant ${n}: kind ${JSON.stringify(it?.kind)} is not scalar, list or sheet (a travel or a set is a control)`);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
17
52
|
/** The compiled schema validator. */
|
|
18
53
|
export function schemaValidator() {
|
|
19
54
|
if (!compiled) compiled = new Ajv2020({ allErrors: true, strict: true, strictRequired: false, allowUnionTypes: true }).compile(SCHEMA);
|
|
@@ -22,6 +57,62 @@ export function schemaValidator() {
|
|
|
22
57
|
|
|
23
58
|
const constCase = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
|
|
24
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The rules about the meaning of the numbers, not their shape (they live with the data they judge):
|
|
62
|
+
* a set's default and labels; a travel's scale is a PARAM_SCALES id; the EQ band counts and budget; the
|
|
63
|
+
* default EQ rule's inputs.
|
|
64
|
+
*/
|
|
65
|
+
function semanticRules(resolved, say) {
|
|
66
|
+
const val = (name) => resolved.get(name)?.value;
|
|
67
|
+
for (const r of resolved.values()) {
|
|
68
|
+
if (r.kind === 'set') {
|
|
69
|
+
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)}`);
|
|
70
|
+
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`);
|
|
71
|
+
}
|
|
72
|
+
const travels = r.kind === 'travels' ? (r.shape === 'table' ? Object.entries(r.value) : [['', r.value]]) : [];
|
|
73
|
+
for (const [field, t] of travels) {
|
|
74
|
+
if (t.scale !== undefined && !(val('PARAM_SCALES') ?? []).includes(t.scale)) say(r.rel, r.name, 'scale', `${field ? `${r.name}.${field}` : r.name}: scale ${JSON.stringify(t.scale)} is not an id of PARAM_SCALES`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The EQ band budget (ruling 2026-10-08): what the operator keeps, what the feedback suppressor plants
|
|
79
|
+
// and what the ring correction plants all fit in the band maximum.
|
|
80
|
+
const reserve = val('OPERATOR_EQ_BANDS_RESERVE');
|
|
81
|
+
if (reserve !== undefined) {
|
|
82
|
+
const need = reserve + val('FBS_DEFAULT_MAX_AUTO_BANDS') + val('HRP_DEFAULT_MAX_AUTO_BANDS');
|
|
83
|
+
const cap = val('EQ_MAX_BANDS');
|
|
84
|
+
if (!(need <= cap)) say(resolved.get('OPERATOR_EQ_BANDS_RESERVE').rel, 'OPERATOR_EQ_BANDS_RESERVE', 'eq-band-budget',
|
|
85
|
+
`OPERATOR_EQ_BANDS_RESERVE ${reserve} + FBS_DEFAULT_MAX_AUTO_BANDS ${val('FBS_DEFAULT_MAX_AUTO_BANDS')} + HRP_DEFAULT_MAX_AUTO_BANDS ${val('HRP_DEFAULT_MAX_AUTO_BANDS')} = ${need} exceeds EQ_MAX_BANDS ${cap}`);
|
|
86
|
+
}
|
|
87
|
+
const counts = val('EQ_BAND_COUNTS');
|
|
88
|
+
if (counts) {
|
|
89
|
+
const rel = resolved.get('EQ_BAND_COUNTS').rel;
|
|
90
|
+
for (const [strip, c] of Object.entries(counts)) {
|
|
91
|
+
for (const k of ['default', 'max']) if (!Number.isInteger(c[k]) || c[k] < 1) say(rel, 'EQ_BAND_COUNTS', 'eq-band-counts', `${strip}.${k} ${c[k]} is not a positive integer`);
|
|
92
|
+
if (c.default > c.max) say(rel, 'EQ_BAND_COUNTS', 'eq-band-counts', `${strip}: default ${c.default} exceeds max ${c.max}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The default EQ rule: its inputs are consistent and it answers for every declared default count.
|
|
97
|
+
if (resolved.has('EQ_DEFAULT_CENTRES_FOUR_BAND_HZ')) {
|
|
98
|
+
const rel = resolved.get('EQ_DEFAULT_CENTRES_FOUR_BAND_HZ').rel;
|
|
99
|
+
const bad = (rule, message) => say(rel, 'EQ_DEFAULT_CENTRES_FOUR_BAND_HZ', rule, message);
|
|
100
|
+
let p;
|
|
101
|
+
try { p = eqDefaultParams(resolved); } catch (e) { bad('eq-default-rule', e.message); return; }
|
|
102
|
+
const [r10, r20] = p.series;
|
|
103
|
+
if (p.four.length !== 4) bad('eq-default-rule', `the four-band set holds ${p.four.length} centres`);
|
|
104
|
+
for (const [name, list] of [['four-band set', p.four], ['R10', r10], ['R20', r20]]) {
|
|
105
|
+
if (!list.every((f, i) => f >= p.lo && f <= p.hi && (i === 0 || f > list[i - 1]))) bad('eq-default-rule', `the ${name} is not ascending inside [${p.lo}, ${p.hi}] Hz`);
|
|
106
|
+
}
|
|
107
|
+
for (const f of [...p.four, ...r10]) if (!r20.includes(f)) bad('eq-default-rule', `${f} Hz is not in the R20 series: R10 and the four-band set are preferred values of it`);
|
|
108
|
+
const types = val('EQ_BAND_TYPES') ?? [];
|
|
109
|
+
for (const id of ['bell', 'lowShelf', 'highShelf']) if (!types.includes(id)) bad('eq-default-rule', `EQ_BAND_TYPES lacks ${id}, which the default rule uses`);
|
|
110
|
+
for (const [strip, c] of Object.entries(counts ?? {})) {
|
|
111
|
+
try { eqDefaultBands(c.default, p); } catch (e) { bad('eq-default-rule', `${strip}: ${e.message}`); }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
25
116
|
/** Every problem of the loaded data: `[{ file, item, rule, message }]`; empty is valid. */
|
|
26
117
|
export function validateData(data) {
|
|
27
118
|
const problems = [];
|
|
@@ -31,7 +122,10 @@ export function validateData(data) {
|
|
|
31
122
|
const depth = f.rel.split('/').length - 1;
|
|
32
123
|
const want = posix.join(...Array(depth).fill('..'), 'schema/omx-contract.schema.json');
|
|
33
124
|
if (f.doc.$schema !== want) say(f.rel, '-', 'schema-path', `"$schema" must be "${want}"`);
|
|
34
|
-
|
|
125
|
+
for (const m of shapeProblems(f.rel, f.doc)) say(f.rel, '-', 'shape', m);
|
|
126
|
+
// The schema reads items: a kernel file is checked as the items it expands to, so no rule is lost.
|
|
127
|
+
const items = { $schema: f.doc.$schema, ...Object.fromEntries(entriesOf(f.rel, f.doc)) };
|
|
128
|
+
if (!validate(items)) {
|
|
35
129
|
for (const e of validate.errors) {
|
|
36
130
|
const item = e.instancePath.split('/')[1] || '-';
|
|
37
131
|
say(f.rel, item, 'schema', `${e.instancePath || '/'} ${e.message}${e.params?.allowedValues ? ` (${e.params.allowedValues.join(', ')})` : ''}`);
|
|
@@ -71,6 +165,8 @@ export function validateData(data) {
|
|
|
71
165
|
if (typeof r.doc !== 'string' || !r.doc.trim()) say(r.rel, r.name, 'doc', 'every item carries its doc');
|
|
72
166
|
}
|
|
73
167
|
|
|
168
|
+
semanticRules(resolved, say);
|
|
169
|
+
|
|
74
170
|
for (const [target, files] of [['c', () => renderC(resolved)], ['ts', () => renderTs(resolved)]]) {
|
|
75
171
|
let out;
|
|
76
172
|
try {
|
|
@@ -82,7 +178,7 @@ export function validateData(data) {
|
|
|
82
178
|
if (target !== 'c') continue;
|
|
83
179
|
for (const f of out) {
|
|
84
180
|
const seen = new Map();
|
|
85
|
-
for (const m of f.text.matchAll(
|
|
181
|
+
for (const m of f.text.matchAll(/^(?:#define | )(OMX_\w+)(?= = |\s)/gm)) {
|
|
86
182
|
if (seen.has(m[1])) say('-', m[1], 'c-name-unique', `${f.path} defines ${m[1]} twice`);
|
|
87
183
|
seen.set(m[1], true);
|
|
88
184
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmixer/omx-contract",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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
|
@@ -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
|
|
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
|
|
86
|
-
*
|
|
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
|
-
|
|
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
|
|
39
|
-
const
|
|
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
|
|
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
|
}
|