@openmixer/omx-contract 2.1.0 → 2.2.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 +17 -2
- package/data/kernels/comp.json +31 -0
- package/data/kernels/delay.json +16 -0
- package/data/kernels/drive.json +44 -0
- package/data/kernels/eq.json +105 -49
- package/data/kernels/geq.json +2 -1
- package/dist/ts/omx-contract.d.ts +84 -30
- package/dist/ts/omx-contract.js +120 -37
- package/include/omxcontract/omx_contract_limits.h +55 -0
- package/lib/data.mjs +46 -12
- package/lib/validate.mjs +37 -4
- package/package.json +1 -1
- package/schema/omx-contract.schema.json +2 -2
- package/share/omx-contract/omx-contract.json +257 -70
package/dist/ts/omx-contract.js
CHANGED
|
@@ -245,6 +245,25 @@ export const COMP_LIMITS = {
|
|
|
245
245
|
}
|
|
246
246
|
};
|
|
247
247
|
|
|
248
|
+
/**
|
|
249
|
+
* The compressor's character, which picks its detector: `comp` runs the RMS detector, `limiter` the peak detector. The console's comp row carries it (`CompState.kind`, `COMP_KINDS`) and comes up as `comp`.
|
|
250
|
+
* @see data/kernels/comp.json
|
|
251
|
+
*/
|
|
252
|
+
export const COMP_KINDS = [
|
|
253
|
+
"comp",
|
|
254
|
+
"limiter"
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The compressor's detector oversampling: `auto` runs the control path at 4x only while the attack is faster than DETECTOR_OVERSAMPLE_AUTO_MS, `off` never, `x4` always (omx-dsp's OMX_DYN_OVS_AUTO, _OFF and _X4, in that order). The console's comp row carries it as the operator's choice (`CompState.detectorOversampling`, spelled auto, off, 4x) and comes up `auto`.
|
|
259
|
+
* @see data/kernels/comp.json
|
|
260
|
+
*/
|
|
261
|
+
export const DETECTOR_OVERSAMPLINGS = [
|
|
262
|
+
"auto",
|
|
263
|
+
"off",
|
|
264
|
+
"x4"
|
|
265
|
+
];
|
|
266
|
+
|
|
248
267
|
/**
|
|
249
268
|
* The program-dependent release's model constants — the LA-2A's opto cell as DATA (`docs/design/specs/2026-09-26-compressor-models.md` §3b). `chargeMs`/`dischargeMs` are the word's memory constants, shared by every profile; each profile's `fastMs`, `slowMinMs`, `slowMaxMs` and `share` fit the unit's published two-stage recovery. Not operator controls: generated into `omx_contract_limits.h` as `OMX_PROGRAM_RELEASE_*`, which libomxdsp's `omx_env_program_release` reads.
|
|
250
269
|
* @see data/kernels/comp.json
|
|
@@ -423,6 +442,15 @@ export const DELAY_MIX_RANGE = {
|
|
|
423
442
|
}
|
|
424
443
|
};
|
|
425
444
|
|
|
445
|
+
/**
|
|
446
|
+
* The FX delay's PING-PONG switch: on cross-feeds the two legs' feedback, so each leg's repeats come from the other leg's damped tap and the echoes bounce L<->R (`omx_delay.h`'s `struct omx_fx_delay.pingpong`, read where the feedback is formed). A live control on the console's delay row (`DelayState.pingpong`) and on the omx-delay plugin. Its come-up value is FX_DELAY_PINGPONG_DEFAULT, by reference, so the default keeps one home.
|
|
447
|
+
* @see data/kernels/delay.json
|
|
448
|
+
*/
|
|
449
|
+
export const DELAY_PINGPONGS = [
|
|
450
|
+
"off",
|
|
451
|
+
"on"
|
|
452
|
+
];
|
|
453
|
+
|
|
426
454
|
/**
|
|
427
455
|
* The FX delay's PING-PONG come-up value — a toggle, so it has no travel, only this. Declared here rather than as a literal in the server's `DEFAULT_DELAY_STATE` (which now reads it) so the DPF plugin generator (`packages/omx-plugins/tools/params-gen.mjs`, `2026-09-25-omx-plugins-dpf.md` §3a) reads the same fact the desk seeds a fresh strip with.
|
|
428
456
|
* @see data/kernels/delay.json
|
|
@@ -517,54 +545,47 @@ export const DRIVE_BANDS = [
|
|
|
517
545
|
];
|
|
518
546
|
|
|
519
547
|
/**
|
|
520
|
-
* The
|
|
548
|
+
* The drive's AUTO-GAIN switch: on, the stage measures its own level change and matches it out (`omx_drive.h`'s `struct omx_drive.auto_gain`, drive-stage spec §4d), so turning the drive up adds colour rather than loudness. A field of the console's drive row (`DriveState.autoGain`), on at come-up there and in omx-dsp's drive face.
|
|
521
549
|
* @see data/kernels/drive.json
|
|
522
550
|
*/
|
|
523
|
-
export const
|
|
551
|
+
export const DRIVE_AUTO_GAINS = [
|
|
552
|
+
"off",
|
|
553
|
+
"on"
|
|
554
|
+
];
|
|
524
555
|
|
|
525
556
|
/**
|
|
526
|
-
* The
|
|
527
|
-
* @see data/kernels/
|
|
557
|
+
* The drive's STEREO LINK switch: on, ONE auto-gain path is driven by both legs' summed energy, so the image cannot shift under the level match; off, each leg matches its own (`omx_drive.h`'s `struct omx_drive.stereo_link`, spec §4e). A field of the console's drive row (`DriveState.stereoLink`), on at come-up there and in omx-dsp's drive face.
|
|
558
|
+
* @see data/kernels/drive.json
|
|
528
559
|
*/
|
|
529
|
-
export const
|
|
530
|
-
"
|
|
531
|
-
"
|
|
532
|
-
|
|
533
|
-
"unit": "Hz"
|
|
534
|
-
};
|
|
560
|
+
export const DRIVE_STEREO_LINKS = [
|
|
561
|
+
"off",
|
|
562
|
+
"on"
|
|
563
|
+
];
|
|
535
564
|
|
|
536
565
|
/**
|
|
537
|
-
* The
|
|
538
|
-
* @see data/kernels/
|
|
566
|
+
* 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.
|
|
567
|
+
* @see data/kernels/drive.json
|
|
539
568
|
*/
|
|
540
|
-
export const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
};
|
|
569
|
+
export const DRIVE_HF_ROLLOFFS = [
|
|
570
|
+
0,
|
|
571
|
+
12000,
|
|
572
|
+
16000
|
|
573
|
+
];
|
|
546
574
|
|
|
547
575
|
/**
|
|
548
|
-
* The
|
|
549
|
-
* @see data/kernels/
|
|
576
|
+
* The bias at `character = +1`, in the SHAPER's own input units — `OMX_DRIVE_BIAS_MAX`. DERIVED, not chosen (§4b). In the saturating limit every knee tends to `sgn`, the output is a rectangular wave of duty `d = ½ + asin(b)/π`, and its `n`th harmonic goes as `sin(nπd)/n` — so `|H2|` is maximal at `d = ¾`, which is `b = sin(π/4) = 1/√2` exactly. Measured too: the H2-maximising bias for a full-scale sine at unity drive is 0.712 (soft), 0.658 (tape), 0.645 (tube), and this one value costs 0.000 / 0.035 / 0.088 dB of H2 against each curve's own optimum. ONE normalised bias for all three knees, not three.
|
|
577
|
+
* @see data/kernels/drive.json
|
|
550
578
|
*/
|
|
551
|
-
export const
|
|
552
|
-
"min": 0.3,
|
|
553
|
-
"max": 8,
|
|
554
|
-
"step": 0.1,
|
|
555
|
-
"unit": ""
|
|
556
|
-
};
|
|
579
|
+
export const DRIVE_BIAS_MAX = 0.7071067811865476;
|
|
557
580
|
|
|
558
581
|
/**
|
|
559
|
-
* The
|
|
582
|
+
* The HPF's switch: off, the filter is not in the EQ's bank at all; on, its one or two Butterworth sections run after the bands. It comes up off, on the console's channel EQ and in omx-dsp's EQ face (see EQ_PASS_FILTER_DEFAULTS).
|
|
560
583
|
* @see data/kernels/eq.json
|
|
561
584
|
*/
|
|
562
|
-
export const
|
|
563
|
-
"
|
|
564
|
-
"
|
|
565
|
-
|
|
566
|
-
"unit": ""
|
|
567
|
-
};
|
|
585
|
+
export const EQ_HPF_ONS = [
|
|
586
|
+
"off",
|
|
587
|
+
"on"
|
|
588
|
+
];
|
|
568
589
|
|
|
569
590
|
/**
|
|
570
591
|
* Where the HPF cutoff can sit (low end); mirrors the drawn low-frequency handle.
|
|
@@ -577,6 +598,24 @@ export const HPF_FREQ_RANGE = {
|
|
|
577
598
|
"unit": "Hz"
|
|
578
599
|
};
|
|
579
600
|
|
|
601
|
+
/**
|
|
602
|
+
* The pass-filter slopes the EQ offers, in dB per octave: the HPF's slope, and by use the LPF's. The id is the slope: 12 is one maximally flat section, 24 the two-section Butterworth.
|
|
603
|
+
* @see data/kernels/eq.json
|
|
604
|
+
*/
|
|
605
|
+
export const FILTER_SLOPES = [
|
|
606
|
+
12,
|
|
607
|
+
24
|
|
608
|
+
];
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* The LPF's switch: off, the filter is not in the EQ's bank at all; on, its one or two Butterworth sections run after the bands and the HPF. It comes up off, on the console's channel EQ and in omx-dsp's EQ face (see EQ_PASS_FILTER_DEFAULTS).
|
|
612
|
+
* @see data/kernels/eq.json
|
|
613
|
+
*/
|
|
614
|
+
export const EQ_LPF_ONS = [
|
|
615
|
+
"off",
|
|
616
|
+
"on"
|
|
617
|
+
];
|
|
618
|
+
|
|
580
619
|
/**
|
|
581
620
|
* Where the LPF cutoff can sit (high end).
|
|
582
621
|
* @see data/kernels/eq.json
|
|
@@ -602,12 +641,56 @@ export const EQ_BAND_TYPES = [
|
|
|
602
641
|
];
|
|
603
642
|
|
|
604
643
|
/**
|
|
605
|
-
* The
|
|
644
|
+
* The frequency band a parametric band can be dragged across.
|
|
606
645
|
* @see data/kernels/eq.json
|
|
607
646
|
*/
|
|
608
|
-
export const
|
|
609
|
-
|
|
610
|
-
|
|
647
|
+
export const EQ_FREQ_RANGE = {
|
|
648
|
+
"min": 20,
|
|
649
|
+
"max": 20000,
|
|
650
|
+
"step": 10,
|
|
651
|
+
"unit": "Hz"
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* The parametric-band gain travel, in dB. The ± scalar `clampEqGain` enforces is READ off this (`core/src/eq.ts` derives `EQ_GAIN_LIMIT_DB` from `max`), so the bound is stated once and a surface cannot draw a wider knob than the clamp will honour.
|
|
656
|
+
* @see data/kernels/eq.json
|
|
657
|
+
*/
|
|
658
|
+
export const EQ_GAIN_RANGE = {
|
|
659
|
+
"min": -15,
|
|
660
|
+
"max": 15,
|
|
661
|
+
"step": 0.5,
|
|
662
|
+
"unit": "dB"
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* The Q (resonance) extent of a parametric band.
|
|
667
|
+
* @see data/kernels/eq.json
|
|
668
|
+
*/
|
|
669
|
+
export const EQ_Q_RANGE = {
|
|
670
|
+
"min": 0.3,
|
|
671
|
+
"max": 8,
|
|
672
|
+
"step": 0.1,
|
|
673
|
+
"unit": ""
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* The Q extent of a NOTCH band. Wider than EQ_Q_RANGE on purpose (#116, operator decision): an ultra-narrow surgical cut is the notch shape's whole point — the FBS ring-out pool plants dbx-AFS-style ring killers at Q ≈ 116 (~1/80 octave), inaudible on program yet fatal to a single ringing frequency. Gain-carrying shapes stay bounded at EQ_Q_RANGE — a Q-116 bell is a resonator, not an EQ move.
|
|
678
|
+
* @see data/kernels/eq.json
|
|
679
|
+
*/
|
|
680
|
+
export const EQ_NOTCH_Q_RANGE = {
|
|
681
|
+
"min": 0.3,
|
|
682
|
+
"max": 116,
|
|
683
|
+
"step": 1,
|
|
684
|
+
"unit": ""
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* A band's switch. Off, the band is not in the EQ's bank (the slots close up); on, it runs, parked when it is a gain-carrying shape at 0 dB. No come-up value is declared, because the strips differ: the console's channel EQ brings its default bands up on, an omx-eq plugin brings every band up off so a racked instance is a wire.
|
|
689
|
+
* @see data/kernels/eq.json
|
|
690
|
+
*/
|
|
691
|
+
export const EQ_BAND_ONS = [
|
|
692
|
+
"off",
|
|
693
|
+
"on"
|
|
611
694
|
];
|
|
612
695
|
|
|
613
696
|
/**
|
|
@@ -622,11 +622,36 @@ enum omx_band_dyn_modes {
|
|
|
622
622
|
};
|
|
623
623
|
#define OMX_BAND_DYN_MODES_COUNT 2u
|
|
624
624
|
#define OMX_BAND_DYN_MODES_DEFAULT OMX_BAND_DYN_MODES_ABOVE
|
|
625
|
+
enum omx_comp_kinds {
|
|
626
|
+
OMX_COMP_KINDS_COMP = 0,
|
|
627
|
+
OMX_COMP_KINDS_LIMITER = 1,
|
|
628
|
+
};
|
|
629
|
+
#define OMX_COMP_KINDS_COUNT 2u
|
|
630
|
+
#define OMX_COMP_KINDS_DEFAULT OMX_COMP_KINDS_COMP
|
|
625
631
|
enum omx_deess_modes {
|
|
626
632
|
OMX_DEESS_MODES_SPLIT = 0,
|
|
627
633
|
OMX_DEESS_MODES_WIDEBAND = 1,
|
|
628
634
|
};
|
|
629
635
|
#define OMX_DEESS_MODES_COUNT 2u
|
|
636
|
+
enum omx_delay_pingpongs {
|
|
637
|
+
OMX_DELAY_PINGPONGS_OFF = 0,
|
|
638
|
+
OMX_DELAY_PINGPONGS_ON = 1,
|
|
639
|
+
};
|
|
640
|
+
#define OMX_DELAY_PINGPONGS_COUNT 2u
|
|
641
|
+
#define OMX_DELAY_PINGPONGS_DEFAULT OMX_DELAY_PINGPONGS_OFF
|
|
642
|
+
enum omx_detector_oversamplings {
|
|
643
|
+
OMX_DETECTOR_OVERSAMPLINGS_AUTO = 0,
|
|
644
|
+
OMX_DETECTOR_OVERSAMPLINGS_OFF = 1,
|
|
645
|
+
OMX_DETECTOR_OVERSAMPLINGS_X4 = 2,
|
|
646
|
+
};
|
|
647
|
+
#define OMX_DETECTOR_OVERSAMPLINGS_COUNT 3u
|
|
648
|
+
#define OMX_DETECTOR_OVERSAMPLINGS_DEFAULT OMX_DETECTOR_OVERSAMPLINGS_AUTO
|
|
649
|
+
enum omx_drive_auto_gains {
|
|
650
|
+
OMX_DRIVE_AUTO_GAINS_OFF = 0,
|
|
651
|
+
OMX_DRIVE_AUTO_GAINS_ON = 1,
|
|
652
|
+
};
|
|
653
|
+
#define OMX_DRIVE_AUTO_GAINS_COUNT 2u
|
|
654
|
+
#define OMX_DRIVE_AUTO_GAINS_DEFAULT OMX_DRIVE_AUTO_GAINS_ON
|
|
630
655
|
enum omx_drive_bands {
|
|
631
656
|
OMX_DRIVE_BANDS_FULL = 0,
|
|
632
657
|
OMX_DRIVE_BANDS_LOW = 1,
|
|
@@ -641,6 +666,24 @@ enum omx_drive_curves {
|
|
|
641
666
|
OMX_DRIVE_CURVES_EXCITER = 3,
|
|
642
667
|
};
|
|
643
668
|
#define OMX_DRIVE_CURVES_COUNT 4u
|
|
669
|
+
enum omx_drive_hf_rolloffs {
|
|
670
|
+
OMX_DRIVE_HF_ROLLOFFS_0 = 0,
|
|
671
|
+
OMX_DRIVE_HF_ROLLOFFS_12000 = 12000,
|
|
672
|
+
OMX_DRIVE_HF_ROLLOFFS_16000 = 16000,
|
|
673
|
+
};
|
|
674
|
+
#define OMX_DRIVE_HF_ROLLOFFS_COUNT 3u
|
|
675
|
+
#define OMX_DRIVE_HF_ROLLOFFS_DEFAULT OMX_DRIVE_HF_ROLLOFFS_0
|
|
676
|
+
enum omx_drive_stereo_links {
|
|
677
|
+
OMX_DRIVE_STEREO_LINKS_OFF = 0,
|
|
678
|
+
OMX_DRIVE_STEREO_LINKS_ON = 1,
|
|
679
|
+
};
|
|
680
|
+
#define OMX_DRIVE_STEREO_LINKS_COUNT 2u
|
|
681
|
+
#define OMX_DRIVE_STEREO_LINKS_DEFAULT OMX_DRIVE_STEREO_LINKS_ON
|
|
682
|
+
enum omx_eq_band_ons {
|
|
683
|
+
OMX_EQ_BAND_ONS_OFF = 0,
|
|
684
|
+
OMX_EQ_BAND_ONS_ON = 1,
|
|
685
|
+
};
|
|
686
|
+
#define OMX_EQ_BAND_ONS_COUNT 2u
|
|
644
687
|
enum omx_eq_band_types {
|
|
645
688
|
OMX_EQ_BAND_TYPES_BELL = 0,
|
|
646
689
|
OMX_EQ_BAND_TYPES_LOW_SHELF = 1,
|
|
@@ -651,6 +694,18 @@ enum omx_eq_band_types {
|
|
|
651
694
|
};
|
|
652
695
|
#define OMX_EQ_BAND_TYPES_COUNT 6u
|
|
653
696
|
#define OMX_EQ_BAND_TYPES_DEFAULT OMX_EQ_BAND_TYPES_BELL
|
|
697
|
+
enum omx_eq_hpf_ons {
|
|
698
|
+
OMX_EQ_HPF_ONS_OFF = 0,
|
|
699
|
+
OMX_EQ_HPF_ONS_ON = 1,
|
|
700
|
+
};
|
|
701
|
+
#define OMX_EQ_HPF_ONS_COUNT 2u
|
|
702
|
+
#define OMX_EQ_HPF_ONS_DEFAULT OMX_EQ_HPF_ONS_OFF
|
|
703
|
+
enum omx_eq_lpf_ons {
|
|
704
|
+
OMX_EQ_LPF_ONS_OFF = 0,
|
|
705
|
+
OMX_EQ_LPF_ONS_ON = 1,
|
|
706
|
+
};
|
|
707
|
+
#define OMX_EQ_LPF_ONS_COUNT 2u
|
|
708
|
+
#define OMX_EQ_LPF_ONS_DEFAULT OMX_EQ_LPF_ONS_OFF
|
|
654
709
|
enum omx_filter_slopes {
|
|
655
710
|
OMX_FILTER_SLOPES_12 = 12,
|
|
656
711
|
OMX_FILTER_SLOPES_24 = 24,
|
package/lib/data.mjs
CHANGED
|
@@ -54,6 +54,13 @@ export const isKernelFile = (rel) => /(^|\/)kernels\/[^/]+\.json$/.test(rel);
|
|
|
54
54
|
/** The kernel's upper-case prefix, read off its file name: `kernels/band_dyn.json` is BAND_DYN. */
|
|
55
55
|
export const kernelPrefix = (rel) => rel.replace(/^.*\//, '').replace(/\.json$/, '').toUpperCase().replace(/-/g, '_');
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Whether a control is a USE: it declares no travel and no ids of its own and takes its values from the item its
|
|
59
|
+
* `global` names, which another control of the same kernel declares (the EQ's lpfSlope takes FILTER_SLOPES, which
|
|
60
|
+
* hpfSlope declares). A use adds no item; it is a control of the kernel all the same.
|
|
61
|
+
*/
|
|
62
|
+
export const isUse = (c) => c && !c.table && c.travel === undefined && c.ids === undefined && typeof c.global === 'string';
|
|
63
|
+
|
|
57
64
|
/** A control's item name: its `global`, else <KERNEL>_<NAME>_RANGE for a travel and <KERNEL>_<NAME>S for a choice. */
|
|
58
65
|
export const controlGlobal = (rel, c) => c.global ?? `${kernelPrefix(rel)}_${snakeOf(c.name)}${c.kind === 'choice' ? 'S' : '_RANGE'}`;
|
|
59
66
|
|
|
@@ -69,16 +76,20 @@ export function entriesOf(rel, doc) {
|
|
|
69
76
|
const out = [];
|
|
70
77
|
const controls = Array.isArray(doc.controls) ? doc.controls : [];
|
|
71
78
|
const tables = new Map(Object.entries(doc.tables ?? {}).map(([n, meta]) => [n, { kind: 'travels', ...meta, fields: {} }]));
|
|
79
|
+
const placed = new Set();
|
|
72
80
|
for (const c of controls) {
|
|
73
|
-
const { name, kind, global, table, rearms, ...rest } = c;
|
|
81
|
+
const { name, kind, global, table, rearms, count, of, when, ...rest } = c;
|
|
82
|
+
if (isUse(c)) continue;
|
|
74
83
|
if (table) {
|
|
75
84
|
if (!tables.has(table)) throw new Error(`${rel}: control ${name} names table ${table}, which is not declared`);
|
|
85
|
+
// a table is an item where its first control stands, so a control after a table keeps its place
|
|
86
|
+
if (!placed.has(table)) { placed.add(table); out.push([table, tables.get(table)]); }
|
|
76
87
|
tables.get(table).fields[name] = rest.travel;
|
|
77
88
|
continue;
|
|
78
89
|
}
|
|
79
90
|
out.push([controlGlobal(rel, c), kind === 'choice' ? { kind: 'set', ...rest } : { kind: 'travels', ...rest }]);
|
|
80
91
|
}
|
|
81
|
-
for (const [n, t] of tables) out.push([n, t]);
|
|
92
|
+
for (const [n, t] of tables) if (!placed.has(n)) out.push([n, t]);
|
|
82
93
|
const byName = new Map(controls.map((c) => [c.name, c]));
|
|
83
94
|
for (const [n, a] of Object.entries(doc.aggregates ?? {})) {
|
|
84
95
|
const { fields, ...meta } = a;
|
|
@@ -95,24 +106,33 @@ export function entriesOf(rel, doc) {
|
|
|
95
106
|
export const kernelName = (rel) => rel.replace(/^.*\//, '').replace(/\.json$/, '');
|
|
96
107
|
|
|
97
108
|
/**
|
|
98
|
-
* Every kernel's controls, in its file's order: `{ <kernel>: { controls: [{ name, kind, global, table?, rearms? }] } }`,
|
|
109
|
+
* Every kernel's controls, in its file's order: `{ <kernel>: { controls: [{ name, kind, global, table?, rearms?, count?, when? }] } }`,
|
|
99
110
|
* kernels in file order. `global` is the item the control's value lives in: its own travel or set, or, for a field of
|
|
100
111
|
* a table, the table (`table` names it too, and the field is the control's name). `rearms` is carried only when true:
|
|
101
|
-
* changing the control re-arms the kernel's state, it is not a smooth parameter.
|
|
112
|
+
* changing the control re-arms the kernel's state, it is not a smooth parameter. `count` names the item that counts a
|
|
113
|
+
* control the kernel takes once per band (a scalar, or a sheet whose every variant gives its count as `max`). A
|
|
114
|
+
* control that declares `of` is no control of its own but the travel the control it names reaches while the choice
|
|
115
|
+
* `when` names holds the id it names: it is listed under that control's `when` as `{ control, is, global }`.
|
|
102
116
|
*/
|
|
103
117
|
export function kernelControls(data) {
|
|
104
118
|
const out = {};
|
|
105
119
|
for (const f of data.files) {
|
|
106
120
|
if (!isKernelFile(f.rel)) continue;
|
|
107
121
|
const controls = Array.isArray(f.doc.controls) ? f.doc.controls : [];
|
|
122
|
+
const global = (c) => c.table ?? controlGlobal(f.rel, c);
|
|
108
123
|
out[kernelName(f.rel)] = {
|
|
109
|
-
controls: controls.map((c) =>
|
|
110
|
-
name:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
controls: controls.filter((c) => c.of === undefined).map((c) => {
|
|
125
|
+
const when = controls.filter((x) => x.of === c.name).map((x) => ({ control: x.when.control, is: x.when.is, global: global(x) }));
|
|
126
|
+
return {
|
|
127
|
+
name: c.name,
|
|
128
|
+
kind: c.kind,
|
|
129
|
+
global: global(c),
|
|
130
|
+
...(c.table ? { table: c.table } : {}),
|
|
131
|
+
...(c.rearms === true ? { rearms: true } : {}),
|
|
132
|
+
...(c.count !== undefined ? { count: c.count } : {}),
|
|
133
|
+
...(when.length ? { when } : {}),
|
|
134
|
+
};
|
|
135
|
+
}),
|
|
116
136
|
};
|
|
117
137
|
}
|
|
118
138
|
return out;
|
|
@@ -279,7 +299,7 @@ export function resolveData(data) {
|
|
|
279
299
|
if (item.unit !== undefined) r.unit = item.unit;
|
|
280
300
|
if (item.c !== undefined) r.c = item.c;
|
|
281
301
|
if (item.kind === 'set') {
|
|
282
|
-
if (item.default !== undefined) r.default = item
|
|
302
|
+
if (item.default !== undefined) r.default = setDefault(item, (v) => val(v, name), name);
|
|
283
303
|
if (item.labels !== undefined) r.labels = item.labels;
|
|
284
304
|
}
|
|
285
305
|
if (item.kind === 'travels') r.shape = item.fields ? 'table' : 'travel';
|
|
@@ -294,6 +314,20 @@ export function resolveData(data) {
|
|
|
294
314
|
return out;
|
|
295
315
|
}
|
|
296
316
|
|
|
317
|
+
/**
|
|
318
|
+
* A set's default, resolved: an id as written, or `{ ref }` to the scalar that states it. A boolean scalar names the
|
|
319
|
+
* second id of a two-id switch when true and the first when false (FX_DELAY_PINGPONG_DEFAULT is the delay's
|
|
320
|
+
* DELAY_PINGPONG_SWITCH default, so the come-up value keeps one home).
|
|
321
|
+
* @throws naming the set: a boolean default of a set that is not two ids.
|
|
322
|
+
*/
|
|
323
|
+
export function setDefault(item, val, name) {
|
|
324
|
+
if (!isObj(item.default)) return item.default;
|
|
325
|
+
const v = val(item.default);
|
|
326
|
+
if (typeof v !== 'boolean') return v;
|
|
327
|
+
if (item.ids.length !== 2) throw new Error(`${name}: a boolean default needs a two-id switch, not ${item.ids.length} ids`);
|
|
328
|
+
return item.ids[v ? 1 : 0];
|
|
329
|
+
}
|
|
330
|
+
|
|
297
331
|
/** The package version (package.json), which names the tag, the release and the npm version. */
|
|
298
332
|
export function packageVersion(root) {
|
|
299
333
|
return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
|
package/lib/validate.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
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 { KERNEL_SECTIONS, entriesOf, isKernelFile, resolveData } from './data.mjs';
|
|
11
|
+
import { KERNEL_SECTIONS, controlGlobal, entriesOf, isKernelFile, isUse, resolveData } from './data.mjs';
|
|
12
12
|
import { eqDefaultBands, eqDefaultParams } from './eq-defaults.mjs';
|
|
13
13
|
import { renderC } from '../render/c.mjs';
|
|
14
14
|
import { renderTs } from '../render/ts.mjs';
|
|
@@ -18,7 +18,10 @@ let compiled;
|
|
|
18
18
|
/**
|
|
19
19
|
* The kernel-file shape (2.0.0), which the item schema cannot see: only the four sections at the top; every control
|
|
20
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`; `rearms` a boolean, and only on a control;
|
|
21
|
+
* `table` declared under `tables`; `rearms` a boolean, and only on a control; a use (a control with a `global` and no
|
|
22
|
+
* travel or ids) naming an item another control of the kernel declares, of its kind; `count` an item name; `of` naming
|
|
23
|
+
* another control of its kind with `when` a { control, is } of a choice of the kernel; an aggregate's fields naming
|
|
24
|
+
* controls; a constant a scalar, a list or a sheet.
|
|
22
25
|
* A kernel file written as a flat item map (the 1.x shape) is refused as a whole. Any other data file returns [].
|
|
23
26
|
*/
|
|
24
27
|
export function shapeProblems(rel, doc) {
|
|
@@ -37,9 +40,26 @@ export function shapeProblems(rel, doc) {
|
|
|
37
40
|
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
41
|
if (seen.has(c.name)) out.push(`controls[${i}]: name ${c.name} declared twice`);
|
|
39
42
|
seen.add(c.name);
|
|
40
|
-
if (c.kind
|
|
43
|
+
if (c.kind !== 'travel' && c.kind !== 'choice') out.push(`controls[${i}] ${c.name}: kind ${JSON.stringify(c.kind)} is neither travel nor choice`);
|
|
44
|
+
else if (isUse(c)) {
|
|
45
|
+
const owner = controls.find((x) => x !== c && !isUse(x) && !x.table && controlGlobal(rel, x) === c.global);
|
|
46
|
+
if (!owner) out.push(`controls[${i}] ${c.name}: uses ${c.global}, which no other control of this kernel declares`);
|
|
47
|
+
else if (owner.kind !== c.kind) out.push(`controls[${i}] ${c.name}: a ${c.kind} that uses ${c.global}, which is a ${owner.kind}`);
|
|
48
|
+
for (const k of ['doc', 'default', 'labels', 'c']) if (k in c) out.push(`controls[${i}] ${c.name}: a use carries no ${k}; ${c.global} holds it`);
|
|
49
|
+
} else if (c.kind === 'travel' && !c.travel) out.push(`controls[${i}] ${c.name}: a travel without "travel"`);
|
|
41
50
|
else if (c.kind === 'choice' && !Array.isArray(c.ids)) out.push(`controls[${i}] ${c.name}: a choice without "ids"`);
|
|
42
|
-
|
|
51
|
+
if ('count' in c && (typeof c.count !== 'string' || !/^[A-Z][A-Z0-9_]*$/.test(c.count))) out.push(`controls[${i}] ${c.name}: count ${JSON.stringify(c.count)} is not an item name`);
|
|
52
|
+
if ('of' in c || 'when' in c) {
|
|
53
|
+
const target = controls.find((x) => x.name === c.of);
|
|
54
|
+
const w = c.when;
|
|
55
|
+
const chooser = w && controls.find((x) => x.name === w.control);
|
|
56
|
+
const ids = chooser && (isUse(chooser) ? controls.find((x) => !isUse(x) && controlGlobal(rel, x) === chooser.global)?.ids : chooser.ids);
|
|
57
|
+
if (!target || target === c || target.of !== undefined || target.kind !== c.kind) out.push(`controls[${i}] ${c.name}: of ${JSON.stringify(c.of)} names no other control of its kind`);
|
|
58
|
+
else if (!w || typeof w !== 'object' || Object.keys(w).sort().join() !== 'control,is') out.push(`controls[${i}] ${c.name}: when must be { control, is }`);
|
|
59
|
+
else if (!chooser || chooser.kind !== 'choice') out.push(`controls[${i}] ${c.name}: when.control ${JSON.stringify(w.control)} names no choice of this kernel`);
|
|
60
|
+
else if (!(ids ?? []).includes(w.is)) out.push(`controls[${i}] ${c.name}: when.is ${JSON.stringify(w.is)} is not an id of ${w.control}`);
|
|
61
|
+
if (c.count !== undefined) out.push(`controls[${i}] ${c.name}: a control with of is counted as the control it names`);
|
|
62
|
+
}
|
|
43
63
|
if (c.table && !(doc.tables && c.table in doc.tables)) out.push(`controls[${i}] ${c.name}: table ${c.table} is not declared`);
|
|
44
64
|
if ('rearms' in c && typeof c.rearms !== 'boolean') out.push(`controls[${i}] ${c.name}: rearms ${JSON.stringify(c.rearms)} is not a boolean`);
|
|
45
65
|
}
|
|
@@ -79,6 +99,19 @@ function semanticRules(resolved, say) {
|
|
|
79
99
|
}
|
|
80
100
|
}
|
|
81
101
|
|
|
102
|
+
// A per-band control's count: a positive integer scalar, or a sheet whose every variant has a positive integer max.
|
|
103
|
+
for (const [k, { controls }] of Object.entries(resolved.kernels ?? {})) {
|
|
104
|
+
for (const c of controls) {
|
|
105
|
+
if (c.count === undefined) continue;
|
|
106
|
+
const it = resolved.get(c.count);
|
|
107
|
+
const rel = `data/kernels/${k}.json`;
|
|
108
|
+
if (!it) { say(rel, c.global, 'count', `${k}.${c.name}: count ${c.count} is no item`); continue; }
|
|
109
|
+
const ok = (n) => Number.isInteger(n) && n >= 1;
|
|
110
|
+
const good = it.kind === 'scalar' ? ok(it.value) : it.kind === 'sheet' && Object.values(it.value).length > 0 && Object.values(it.value).every((v) => ok(v?.max));
|
|
111
|
+
if (!good) say(rel, c.global, 'count', `${k}.${c.name}: count ${c.count} is neither a positive integer scalar nor a sheet of variants with a positive integer max`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
82
115
|
// The EQ band budget (ruling 2026-10-08): what the operator keeps, what the feedback suppressor plants
|
|
83
116
|
// and what the ring correction plants all fit in the band maximum.
|
|
84
117
|
const reserve = val('OPERATOR_EQ_BANDS_RESERVE');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmixer/omx-contract",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
},
|
|
167
167
|
"set": {
|
|
168
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.",
|
|
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. 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.",
|
|
170
170
|
"properties": {
|
|
171
171
|
"kind": { "const": "set" },
|
|
172
172
|
"since": { "$ref": "#/$defs/since" },
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
{ "type": "array", "items": { "type": "integer", "minimum": 0 }, "minItems": 1, "uniqueItems": true }
|
|
179
179
|
]
|
|
180
180
|
},
|
|
181
|
-
"default": { "oneOf": [{ "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9]*$" }, { "type": "integer", "minimum": 0 }] },
|
|
181
|
+
"default": { "oneOf": [{ "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9]*$" }, { "type": "integer", "minimum": 0 }, { "$ref": "#/$defs/ref" }] },
|
|
182
182
|
"labels": { "type": "array", "items": { "$ref": "#/$defs/text" }, "minItems": 1 }
|
|
183
183
|
},
|
|
184
184
|
"required": ["kind", "doc", "ids"],
|