@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.
Files changed (42) hide show
  1. package/README.md +41 -4
  2. package/bin/omx-contract.mjs +14 -3
  3. package/data/kernels/balance.json +18 -14
  4. package/data/kernels/band_dyn.json +132 -0
  5. package/data/kernels/chorus.json +87 -56
  6. package/data/kernels/comp.json +67 -29
  7. package/data/kernels/deesser.json +114 -86
  8. package/data/kernels/delay.json +67 -57
  9. package/data/kernels/drive.json +118 -87
  10. package/data/kernels/eq.json +284 -113
  11. package/data/kernels/fbs.json +150 -0
  12. package/data/kernels/flanger.json +74 -44
  13. package/data/kernels/gate.json +76 -21
  14. package/data/kernels/geq.json +32 -27
  15. package/data/kernels/hrp.json +68 -0
  16. package/data/kernels/limiter.json +25 -9
  17. package/data/kernels/mixmatrix.json +11 -0
  18. package/data/kernels/phaser.json +110 -100
  19. package/data/kernels/pitch.json +35 -17
  20. package/data/kernels/reverb.json +176 -142
  21. package/data/kernels/rotary.json +189 -177
  22. package/data/kernels/rta.json +17 -0
  23. package/data/kernels/transient.json +64 -36
  24. package/data/kernels/tremolo.json +59 -48
  25. package/data/primitives.json +51 -0
  26. package/data/rates.json +29 -0
  27. package/dist/ts/omx-contract.d.ts +527 -21
  28. package/dist/ts/omx-contract.js +832 -57
  29. package/include/omxcontract/omx_contract_limits.h +233 -0
  30. package/lib/data.mjs +115 -3
  31. package/lib/eq-defaults.mjs +87 -0
  32. package/lib/fmt.mjs +24 -4
  33. package/lib/semver.mjs +12 -2
  34. package/lib/validate.mjs +99 -3
  35. package/package.json +1 -1
  36. package/render/c.mjs +76 -10
  37. package/render/doors.mjs +6 -3
  38. package/render/ts.mjs +40 -2
  39. package/schema/omx-contract.schema.json +39 -3
  40. package/share/omx-contract/omx-contract.json +759 -71
  41. package/render/c/omx_plugin_param.h +0 -25
  42. package/render/c/params-generate.mjs +0 -83
@@ -2,6 +2,7 @@
2
2
  // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
3
  #ifndef OMX_CONTRACT_LIMITS_H
4
4
  #define OMX_CONTRACT_LIMITS_H
5
+ #include <math.h>
5
6
  /*
6
7
  * GENERATED — DO NOT EDIT BY HAND.
7
8
  * Produced by `omx-contract render --target c` from FreeMixer/omx-contract's data/, the declaration
@@ -128,13 +129,43 @@
128
129
  * is an integer literal, any other a float literal AND its OMX_<macro>_DOUBLE spelling. */
129
130
  #define OMX_ALLPASS_UNITY_TOLERANCE 0.0001f
130
131
  #define OMX_ALLPASS_UNITY_TOLERANCE_DOUBLE 0.0001
132
+ #define OMX_BAND_DYN_ATTACK_RANGE_DEFAULT 5
133
+ #define OMX_BAND_DYN_ATTACK_RANGE_MAX 100
134
+ #define OMX_BAND_DYN_ATTACK_RANGE_MIN 0.5f
135
+ #define OMX_BAND_DYN_ATTACK_RANGE_MIN_DOUBLE 0.5
136
+ #define OMX_BAND_DYN_ATTACK_RANGE_STEP 0.1f
137
+ #define OMX_BAND_DYN_ATTACK_RANGE_STEP_DOUBLE 0.1
138
+ #define OMX_BAND_DYN_KNEE_DB 6
139
+ #define OMX_BAND_DYN_MAX_BANDS 9
140
+ #define OMX_BAND_DYN_RANGE_RANGE_DEFAULT -6
141
+ #define OMX_BAND_DYN_RANGE_RANGE_MAX 12
142
+ #define OMX_BAND_DYN_RANGE_RANGE_MIN -24
143
+ #define OMX_BAND_DYN_RANGE_RANGE_STEP 0.5f
144
+ #define OMX_BAND_DYN_RANGE_RANGE_STEP_DOUBLE 0.5
145
+ #define OMX_BAND_DYN_RATIO_RANGE_DEFAULT 2
146
+ #define OMX_BAND_DYN_RATIO_RANGE_MAX 20
147
+ #define OMX_BAND_DYN_RATIO_RANGE_MIN 1
148
+ #define OMX_BAND_DYN_RATIO_RANGE_STEP 0.1f
149
+ #define OMX_BAND_DYN_RATIO_RANGE_STEP_DOUBLE 0.1
150
+ #define OMX_BAND_DYN_RELEASE_RANGE_DEFAULT 100
151
+ #define OMX_BAND_DYN_RELEASE_RANGE_MAX 3000
152
+ #define OMX_BAND_DYN_RELEASE_RANGE_MIN 5
153
+ #define OMX_BAND_DYN_RELEASE_RANGE_STEP 1
154
+ #define OMX_BAND_DYN_THRESHOLD_RANGE_DEFAULT -30
155
+ #define OMX_BAND_DYN_THRESHOLD_RANGE_MAX 0
156
+ #define OMX_BAND_DYN_THRESHOLD_RANGE_MIN -60
157
+ #define OMX_BAND_DYN_THRESHOLD_RANGE_STEP 0.5f
158
+ #define OMX_BAND_DYN_THRESHOLD_RANGE_STEP_DOUBLE 0.5
159
+ #define OMX_BUS_INPUT_CAP 128
131
160
  #define OMX_BUTTERWORTH_Q 0.7071067811865476f
132
161
  #define OMX_BUTTERWORTH_Q_DOUBLE 0.7071067811865476
162
+ #define OMX_CHORUS_BASE_MS 10
133
163
  #define OMX_CHORUS_DEPTH_RANGE_DEFAULT 4
134
164
  #define OMX_CHORUS_DEPTH_RANGE_MAX 12
135
165
  #define OMX_CHORUS_DEPTH_RANGE_MIN 0
136
166
  #define OMX_CHORUS_DEPTH_RANGE_STEP 0.1f
137
167
  #define OMX_CHORUS_DEPTH_RANGE_STEP_DOUBLE 0.1
168
+ #define OMX_CHORUS_MAX_MS 22
138
169
  #define OMX_CHORUS_MIX_RANGE_DEFAULT 35
139
170
  #define OMX_CHORUS_MIX_RANGE_MAX 100
140
171
  #define OMX_CHORUS_MIX_RANGE_MIN 0
@@ -157,6 +188,7 @@
157
188
  #define OMX_CHORUS_VOICES_RANGE_MAX 4
158
189
  #define OMX_CHORUS_VOICES_RANGE_MIN 1
159
190
  #define OMX_CHORUS_VOICES_RANGE_STEP 1
191
+ #define OMX_DEEP_PLATEAU_FRAMES 20
160
192
  #define OMX_DEESS_ATTACK_RANGE_DEFAULT 1
161
193
  #define OMX_DEESS_ATTACK_RANGE_MAX 50
162
194
  #define OMX_DEESS_ATTACK_RANGE_MIN 0.1f
@@ -193,6 +225,20 @@
193
225
  #define OMX_DEESS_WIDTH_RANGE_MIN_DOUBLE 0.25
194
226
  #define OMX_DEESS_WIDTH_RANGE_STEP 0.05f
195
227
  #define OMX_DEESS_WIDTH_RANGE_STEP_DOUBLE 0.05
228
+ #define OMX_DEFAULT_THRESHOLDS_GROWTH_CONFIRM_FRAMES 2
229
+ #define OMX_DEFAULT_THRESHOLDS_GROWTH_MAX_DIP_DB 0.5f
230
+ #define OMX_DEFAULT_THRESHOLDS_GROWTH_MAX_DIP_DB_DOUBLE 0.5
231
+ #define OMX_DEFAULT_THRESHOLDS_GROWTH_MAX_SINGLE_FRAME_SHARE 0.5f
232
+ #define OMX_DEFAULT_THRESHOLDS_GROWTH_MAX_SINGLE_FRAME_SHARE_DOUBLE 0.5
233
+ #define OMX_DEFAULT_THRESHOLDS_GROWTH_MIN_DB_PER_FRAME 0.8f
234
+ #define OMX_DEFAULT_THRESHOLDS_GROWTH_MIN_DB_PER_FRAME_DOUBLE 0.8
235
+ #define OMX_DEFAULT_THRESHOLDS_GROWTH_MIN_TOTAL_RISE_DB 6
236
+ #define OMX_DEFAULT_THRESHOLDS_PAPR_DB 18
237
+ #define OMX_DEFAULT_THRESHOLDS_PERSIST_FRAMES 5
238
+ #define OMX_DEFAULT_THRESHOLDS_PHPR_DB 9
239
+ #define OMX_DEFAULT_THRESHOLDS_PNPR_DB 12
240
+ #define OMX_DEFAULT_THRESHOLDS_PSHR_MIN_DB 10
241
+ #define OMX_DEFAULT_THRESHOLDS_SUB_CLEAN_FRAMES 5
196
242
  #define OMX_DELAY_MIX_RANGE_BY_KIND_FX_RETURN 1
197
243
  #define OMX_DELAY_MIX_RANGE_DEFAULT 0.3f
198
244
  #define OMX_DELAY_MIX_RANGE_DEFAULT_DOUBLE 0.3
@@ -204,6 +250,8 @@
204
250
  #define OMX_DELAY_TONE_RANGE_MAX 1
205
251
  #define OMX_DELAY_TONE_RANGE_MIN 0
206
252
  #define OMX_DELAY_TONE_RANGE_STEP 0
253
+ #define OMX_DETECTOR_OVERSAMPLE_AUTO_MS 0.5f
254
+ #define OMX_DETECTOR_OVERSAMPLE_AUTO_MS_DOUBLE 0.5
207
255
  #define OMX_DRIVE_AMOUNT_RANGE_DEFAULT 0
208
256
  #define OMX_DRIVE_AMOUNT_RANGE_MAX 36
209
257
  #define OMX_DRIVE_AMOUNT_RANGE_MIN 0
@@ -231,6 +279,18 @@
231
279
  #define OMX_DRIVE_TRIM_RANGE_STEP 0.1f
232
280
  #define OMX_DRIVE_TRIM_RANGE_STEP_DOUBLE 0.1
233
281
  #define OMX_DSP_KNOB_REFERENCE_RATE 96000
282
+ #define OMX_EQ_BAND_COUNTS_CHANNEL_DEFAULT 4
283
+ #define OMX_EQ_BAND_COUNTS_CHANNEL_MAX 24
284
+ #define OMX_EQ_BAND_COUNTS_EQ16_DEFAULT 16
285
+ #define OMX_EQ_BAND_COUNTS_EQ16_MAX 16
286
+ #define OMX_EQ_BAND_COUNTS_EQ32_DEFAULT 32
287
+ #define OMX_EQ_BAND_COUNTS_EQ32_MAX 32
288
+ #define OMX_EQ_BAND_COUNTS_EQ8_DEFAULT 8
289
+ #define OMX_EQ_BAND_COUNTS_EQ8_MAX 8
290
+ #define OMX_EQ_BAND_COUNTS_STRIP_DEFAULT 4
291
+ #define OMX_EQ_BAND_COUNTS_STRIP_MAX 4
292
+ #define OMX_EQ_BAND_DEFAULTS_GAIN_DB 0
293
+ #define OMX_EQ_BAND_DEFAULTS_Q 1
234
294
  #define OMX_EQ_FREQ_RANGE_MAX 20000
235
295
  #define OMX_EQ_FREQ_RANGE_MIN 20
236
296
  #define OMX_EQ_FREQ_RANGE_STEP 10
@@ -243,14 +303,29 @@
243
303
  #define OMX_EQ_NOTCH_Q_RANGE_MIN 0.3f
244
304
  #define OMX_EQ_NOTCH_Q_RANGE_MIN_DOUBLE 0.3
245
305
  #define OMX_EQ_NOTCH_Q_RANGE_STEP 1
306
+ #define OMX_EQ_PASS_FILTER_DEFAULTS_HPF_FREQ_HZ 80
307
+ #define OMX_EQ_PASS_FILTER_DEFAULTS_LPF_FREQ_HZ 18000
246
308
  #define OMX_EQ_Q_RANGE_MAX 8
247
309
  #define OMX_EQ_Q_RANGE_MIN 0.3f
248
310
  #define OMX_EQ_Q_RANGE_MIN_DOUBLE 0.3
249
311
  #define OMX_EQ_Q_RANGE_STEP 0.1f
250
312
  #define OMX_EQ_Q_RANGE_STEP_DOUBLE 0.1
313
+ #define OMX_FBS_DEFAULT_MAX_AUTO_BANDS 6
314
+ #define OMX_FBS_DEFAULT_NOTCH_Q 116
315
+ #define OMX_FBS_FLOOR_LIN 1e-12f
316
+ #define OMX_FBS_FLOOR_LIN_DOUBLE 1e-12
317
+ #define OMX_FBS_MAX_NOTCH_DEPTH_DB 18
318
+ #define OMX_FBS_PAPR_GUARD_BINS 2
319
+ #define OMX_FBS_PHPR_HARMONICS 3
320
+ #define OMX_FBS_PNPR_GUARD_BINS 1
321
+ #define OMX_FBS_PNPR_RADIUS_BINS 3
322
+ #define OMX_FBS_PSHR_SELF_SKIP_BINS 3
323
+ #define OMX_FBS_TRACK_SLOTS 512
251
324
  #define OMX_FDELAY_MOD_READ_ORDER 3
252
325
  #define OMX_FDELAY_READ_L1_NORM 1.25f
253
326
  #define OMX_FDELAY_READ_L1_NORM_DOUBLE 1.25
327
+ #define OMX_FLANGER_BASE_MS 0.5f
328
+ #define OMX_FLANGER_BASE_MS_DOUBLE 0.5
254
329
  #define OMX_FLANGER_DEPTH_RANGE_DEFAULT 2
255
330
  #define OMX_FLANGER_DEPTH_RANGE_MAX 5
256
331
  #define OMX_FLANGER_DEPTH_RANGE_MIN 0
@@ -264,6 +339,7 @@
264
339
  #define OMX_FLANGER_FEEDBACK_RANGE_MIN_DOUBLE -0.95
265
340
  #define OMX_FLANGER_FEEDBACK_RANGE_STEP 0.01f
266
341
  #define OMX_FLANGER_FEEDBACK_RANGE_STEP_DOUBLE 0.01
342
+ #define OMX_FLANGER_MAX_MS 6
267
343
  #define OMX_FLANGER_MIX_RANGE_DEFAULT 50
268
344
  #define OMX_FLANGER_MIX_RANGE_MAX 100
269
345
  #define OMX_FLANGER_MIX_RANGE_MIN 0
@@ -296,12 +372,35 @@
296
372
  #define OMX_GEQ_BAND_RANGE_STEP_DOUBLE 0.5
297
373
  #define OMX_GEQ_DESIGN_GAIN_MAX_DB 30
298
374
  #define OMX_GEQ_PROTO_DB 12
375
+ #define OMX_GROWTH_CONFIRM_FRAMES 2
376
+ #define OMX_GROWTH_MAX_DIP_DB 0.5f
377
+ #define OMX_GROWTH_MAX_DIP_DB_DOUBLE 0.5
378
+ #define OMX_GROWTH_MAX_SINGLE_FRAME_SHARE 0.5f
379
+ #define OMX_GROWTH_MAX_SINGLE_FRAME_SHARE_DOUBLE 0.5
380
+ #define OMX_GROWTH_MIN_DB_PER_FRAME 0.8f
381
+ #define OMX_GROWTH_MIN_DB_PER_FRAME_DOUBLE 0.8
382
+ #define OMX_GROWTH_MIN_TOTAL_RISE_DB 6
383
+ #define OMX_GROWTH_WINDOW_FRAMES 5
299
384
  #define OMX_HPF_FREQ_RANGE_MAX 1000
300
385
  #define OMX_HPF_FREQ_RANGE_MIN 20
301
386
  #define OMX_HPF_FREQ_RANGE_STEP 10
387
+ #define OMX_HRP_AMOUNT_RANGE_DEFAULT 0.5f
388
+ #define OMX_HRP_AMOUNT_RANGE_DEFAULT_DOUBLE 0.5
389
+ #define OMX_HRP_AMOUNT_RANGE_MAX 1
390
+ #define OMX_HRP_AMOUNT_RANGE_MIN 0
391
+ #define OMX_HRP_AMOUNT_RANGE_STEP 0
392
+ #define OMX_HRP_ATTRIBUTION_CENTS 60
393
+ #define OMX_HRP_BAND_Q 5
394
+ #define OMX_HRP_COLLISION_CENTS 100
395
+ #define OMX_HRP_DEFAULT_MAX_AUTO_BANDS 8
396
+ #define OMX_HRP_EXCESS_THRESHOLD_DB 3
397
+ #define OMX_HRP_GAIN_STEP_DB 2
398
+ #define OMX_HRP_MAX_CUT_DB 6
399
+ #define OMX_HRP_MAX_VOICES 8
302
400
  #define OMX_LPF_FREQ_RANGE_MAX 20000
303
401
  #define OMX_LPF_FREQ_RANGE_MIN 1000
304
402
  #define OMX_LPF_FREQ_RANGE_STEP 10
403
+ #define OMX_OPERATOR_EQ_BANDS_RESERVE 9
305
404
  #define OMX_PAN_RANGE_MAX 1
306
405
  #define OMX_PAN_RANGE_MIN -1
307
406
  #define OMX_PAN_RANGE_STEP 0
@@ -322,6 +421,7 @@
322
421
  #define OMX_PHASER_FEEDBACK_RANGE_MIN_DOUBLE -0.9
323
422
  #define OMX_PHASER_FEEDBACK_RANGE_STEP 0.01f
324
423
  #define OMX_PHASER_FEEDBACK_RANGE_STEP_DOUBLE 0.01
424
+ #define OMX_PHASER_F_TOP_HZ 16000
325
425
  #define OMX_PHASER_MIX_RANGE_DEFAULT 50
326
426
  #define OMX_PHASER_MIX_RANGE_MAX 100
327
427
  #define OMX_PHASER_MIX_RANGE_MIN 0
@@ -349,11 +449,14 @@
349
449
  #define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SHARE_DOUBLE 0.5
350
450
  #define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SLOW_MAX_MS 1100
351
451
  #define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SLOW_MIN_MS 110
452
+ #define OMX_PSHR_MIN_DB 10
352
453
  #define OMX_REVERB_DAMPING_RANGE_DEFAULT 0.5f
353
454
  #define OMX_REVERB_DAMPING_RANGE_DEFAULT_DOUBLE 0.5
354
455
  #define OMX_REVERB_DAMPING_RANGE_MAX 1
355
456
  #define OMX_REVERB_DAMPING_RANGE_MIN 0
356
457
  #define OMX_REVERB_DAMPING_RANGE_STEP 0
458
+ #define OMX_REVERB_GATE_ATTACK_MS 1.5f
459
+ #define OMX_REVERB_GATE_ATTACK_MS_DOUBLE 1.5
357
460
  #define OMX_REVERB_GATE_THRESHOLD_RANGE_DEFAULT -40
358
461
  #define OMX_REVERB_GATE_THRESHOLD_RANGE_MAX 0
359
462
  #define OMX_REVERB_GATE_THRESHOLD_RANGE_MIN -80
@@ -460,8 +563,12 @@
460
563
  #define OMX_ROTARY_RING_FLOATS 256
461
564
  #define OMX_ROTARY_STOP_EPS 0.0001f
462
565
  #define OMX_ROTARY_STOP_EPS_DOUBLE 0.0001
566
+ #define OMX_RTA_FFT_SIZE_MAX 32768
567
+ #define OMX_RTA_TARGET_BIN_HZ 5.9f
568
+ #define OMX_RTA_TARGET_BIN_HZ_DOUBLE 5.9
463
569
  #define OMX_RT_HARD_TARGET_QUANTUM 128
464
570
  #define OMX_RT_HARD_TARGET_RATE 192000
571
+ #define OMX_SUBHARMONIC_RADIUS_BINS 2
465
572
  #define OMX_TRANSIENT_FAST_ATTACK_MS 0.5f
466
573
  #define OMX_TRANSIENT_FAST_ATTACK_MS_DOUBLE 0.5
467
574
  #define OMX_TRANSIENT_FAST_RELEASE_MS 20
@@ -492,11 +599,137 @@
492
599
  #define OMX_XOVER_PARTITION_TOLERANCE_DOUBLE 0.00001
493
600
 
494
601
  /* Every list rendered to C, sorted by list: OMX_<LIST>_COUNT and OMX_<LIST>_INIT { a, b, … }. */
602
+ #define OMX_EQ_CENTRE_SERIES_R20_HZ_COUNT 61u
603
+ #define OMX_EQ_CENTRE_SERIES_R20_HZ_INIT { 20, 22.4f, 25, 28, 31.5f, 35.5f, 40, 45, 50, 56, 63, 71, 80, 90, 100, 112, 125, 140, 160, 180, 200, 224, 250, 280, 315, 355, 400, 450, 500, 560, 630, 710, 800, 900, 1000, 1120, 1250, 1400, 1600, 1800, 2000, 2240, 2500, 2800, 3150, 3550, 4000, 4500, 5000, 5600, 6300, 7100, 8000, 9000, 10000, 11200, 12500, 14000, 16000, 18000, 20000 }
604
+ #define OMX_EQ_DEFAULT_CENTRES_FOUR_BAND_HZ_COUNT 4u
605
+ #define OMX_EQ_DEFAULT_CENTRES_FOUR_BAND_HZ_INIT { 100, 400, 2000, 8000 }
606
+ #define OMX_ISO_THIRD_OCTAVE_CENTRES_HZ_COUNT 31u
607
+ #define OMX_ISO_THIRD_OCTAVE_CENTRES_HZ_INIT { 20, 25, 31.5f, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500, 16000, 20000 }
608
+ #define OMX_ORACLE_FLOOR_RATES_COUNT 4u
609
+ #define OMX_ORACLE_FLOOR_RATES_INIT { 44100, 48000, 96000, 192000 }
495
610
  #define OMX_PHASER_STAGE_COUNTS_COUNT 6u
496
611
  #define OMX_PHASER_STAGE_COUNTS_INIT { 2, 4, 6, 8, 10, 12 }
612
+ #define OMX_RME_RATES_COUNT 9u
613
+ #define OMX_RME_RATES_INIT { 32000, 44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000 }
497
614
  #define OMX_STANDARD_SAMPLE_RATES_COUNT 6u
498
615
  #define OMX_STANDARD_SAMPLE_RATES_INIT { 44100, 48000, 88200, 96000, 176400, 192000 }
499
616
 
617
+ /* 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,
618
+ * where the set names a default, `OMX_<SET>_DEFAULT`. */
619
+ enum omx_band_dyn_modes {
620
+ OMX_BAND_DYN_MODES_ABOVE = 0,
621
+ OMX_BAND_DYN_MODES_BELOW = 1,
622
+ };
623
+ #define OMX_BAND_DYN_MODES_COUNT 2u
624
+ #define OMX_BAND_DYN_MODES_DEFAULT OMX_BAND_DYN_MODES_ABOVE
625
+ enum omx_deess_modes {
626
+ OMX_DEESS_MODES_SPLIT = 0,
627
+ OMX_DEESS_MODES_WIDEBAND = 1,
628
+ };
629
+ #define OMX_DEESS_MODES_COUNT 2u
630
+ enum omx_drive_bands {
631
+ OMX_DRIVE_BANDS_FULL = 0,
632
+ OMX_DRIVE_BANDS_LOW = 1,
633
+ OMX_DRIVE_BANDS_HIGH = 2,
634
+ OMX_DRIVE_BANDS_TILT = 3,
635
+ };
636
+ #define OMX_DRIVE_BANDS_COUNT 4u
637
+ enum omx_drive_curves {
638
+ OMX_DRIVE_CURVES_SOFT = 0,
639
+ OMX_DRIVE_CURVES_TAPE = 1,
640
+ OMX_DRIVE_CURVES_TUBE = 2,
641
+ OMX_DRIVE_CURVES_EXCITER = 3,
642
+ };
643
+ #define OMX_DRIVE_CURVES_COUNT 4u
644
+ enum omx_eq_band_types {
645
+ OMX_EQ_BAND_TYPES_BELL = 0,
646
+ OMX_EQ_BAND_TYPES_LOW_SHELF = 1,
647
+ OMX_EQ_BAND_TYPES_HIGH_SHELF = 2,
648
+ OMX_EQ_BAND_TYPES_NOTCH = 3,
649
+ OMX_EQ_BAND_TYPES_ALLPASS1 = 4,
650
+ OMX_EQ_BAND_TYPES_ALLPASS2 = 5,
651
+ };
652
+ #define OMX_EQ_BAND_TYPES_COUNT 6u
653
+ #define OMX_EQ_BAND_TYPES_DEFAULT OMX_EQ_BAND_TYPES_BELL
654
+ enum omx_filter_slopes {
655
+ OMX_FILTER_SLOPES_12 = 12,
656
+ OMX_FILTER_SLOPES_24 = 24,
657
+ };
658
+ #define OMX_FILTER_SLOPES_COUNT 2u
659
+ #define OMX_FILTER_SLOPES_DEFAULT OMX_FILTER_SLOPES_12
660
+ enum omx_gate_key_sources {
661
+ OMX_GATE_KEY_SOURCES_SELF = 0,
662
+ OMX_GATE_KEY_SOURCES_SIDECHAIN = 1,
663
+ };
664
+ #define OMX_GATE_KEY_SOURCES_COUNT 2u
665
+ enum omx_param_scales {
666
+ OMX_PARAM_SCALES_LINEAR = 0,
667
+ OMX_PARAM_SCALES_LOG = 1,
668
+ OMX_PARAM_SCALES_STEPPED = 2,
669
+ };
670
+ #define OMX_PARAM_SCALES_COUNT 3u
671
+ #define OMX_PARAM_SCALES_DEFAULT OMX_PARAM_SCALES_LINEAR
672
+ enum omx_reverb_algorithms {
673
+ OMX_REVERB_ALGORITHMS_ROOM = 0,
674
+ OMX_REVERB_ALGORITHMS_PLATE = 1,
675
+ OMX_REVERB_ALGORITHMS_HALL = 2,
676
+ OMX_REVERB_ALGORITHMS_REVERSE = 3,
677
+ OMX_REVERB_ALGORITHMS_GATED = 4,
678
+ };
679
+ #define OMX_REVERB_ALGORITHMS_COUNT 5u
680
+ enum omx_rotary_speeds {
681
+ OMX_ROTARY_SPEEDS_STOP = 0,
682
+ OMX_ROTARY_SPEEDS_SLOW = 1,
683
+ OMX_ROTARY_SPEEDS_FAST = 2,
684
+ };
685
+ #define OMX_ROTARY_SPEEDS_COUNT 3u
686
+ enum omx_tremolo_modes {
687
+ OMX_TREMOLO_MODES_TREMOLO = 0,
688
+ OMX_TREMOLO_MODES_PAN = 1,
689
+ };
690
+ #define OMX_TREMOLO_MODES_COUNT 2u
691
+ #define OMX_TREMOLO_MODES_DEFAULT OMX_TREMOLO_MODES_TREMOLO
692
+
693
+ /* The default bands of an EQ with `count` bands: THE one rule, for every strip, host and count (omx-contract's
694
+ * lib/eq-defaults.mjs; the ts render exports the same as eqDefaultCentres and eqDefaultTypes). Needs -lm. */
695
+ static inline unsigned omx_eq_default_centres(unsigned count, float *out) {
696
+ static const double r10[] = OMX_ISO_THIRD_OCTAVE_CENTRES_HZ_INIT;
697
+ static const double r20[] = OMX_EQ_CENTRE_SERIES_R20_HZ_INIT;
698
+ static const double four[] = OMX_EQ_DEFAULT_CENTRES_FOUR_BAND_HZ_INIT;
699
+ if (count == 0) return 0;
700
+ if (count == 4) {
701
+ for (unsigned i = 0; i < 4; i++) out[i] = (float)four[i];
702
+ return 4;
703
+ }
704
+ const double lo = log((double)OMX_EQ_FREQ_RANGE_MIN);
705
+ const double span = log((double)OMX_EQ_FREQ_RANGE_MAX / (double)OMX_EQ_FREQ_RANGE_MIN);
706
+ for (unsigned s = 0; s < 2; s++) {
707
+ const double *series = s ? r20 : r10;
708
+ const unsigned n = s ? OMX_EQ_CENTRE_SERIES_R20_HZ_COUNT : OMX_ISO_THIRD_OCTAVE_CENTRES_HZ_COUNT;
709
+ int distinct = count <= n;
710
+ for (unsigned i = 0; distinct && i < count; i++) {
711
+ const double t = lo + span * ((double)i + 0.5) / (double)count;
712
+ unsigned best = 0;
713
+ double best_d = 1e300;
714
+ for (unsigned k = 0; k < n; k++) {
715
+ const double d = fabs(log(series[k]) - t);
716
+ if (d < best_d) { best_d = d; best = k; }
717
+ }
718
+ out[i] = (float)series[best];
719
+ if (i > 0 && out[i] == out[i - 1]) distinct = 0;
720
+ }
721
+ if (distinct) return count;
722
+ }
723
+ return 0;
724
+ }
725
+
726
+ /* The default type of band `index` of an EQ with `count` bands: a low shelf first, a high shelf last, bells
727
+ * between (a single band is a bell). An OMX_EQ_BAND_TYPES_* member. */
728
+ static inline int omx_eq_default_type(unsigned index, unsigned count) {
729
+ if (count < 2) return OMX_EQ_BAND_TYPES_BELL;
730
+ return index == 0 ? OMX_EQ_BAND_TYPES_LOW_SHELF : index == count - 1 ? OMX_EQ_BAND_TYPES_HIGH_SHELF : OMX_EQ_BAND_TYPES_BELL;
731
+ }
732
+
500
733
  #endif /* OMX_CONTRACT_LIMITS_H */
501
734
 
502
735
  /* The sample rates the console declares (core's STANDARD_SAMPLE_RATES, row-codecs.ts): the set a
package/lib/data.mjs CHANGED
@@ -34,8 +34,7 @@ export function loadData(dir, texts) {
34
34
  let doc;
35
35
  try { doc = JSON.parse(text); } catch (e) { throw new Error(`${rel}: not JSON: ${e.message}`); }
36
36
  files.push({ rel, text, doc });
37
- for (const [name, item] of Object.entries(doc)) {
38
- if (name === '$schema') continue;
37
+ for (const [name, item] of entriesOf(rel, doc)) {
39
38
  if (items.has(name)) throw new Error(`${rel}: ${name}: already declared by ${items.get(name).rel}`);
40
39
  items.set(name, { name, rel, item });
41
40
  }
@@ -43,6 +42,109 @@ export function loadData(dir, texts) {
43
42
  return { files, items };
44
43
  }
45
44
 
45
+ const snakeOf = (n) => n.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
46
+ const camelOf = (u) => u.toLowerCase().replace(/_([a-z0-9])/g, (_, x) => x.toUpperCase());
47
+
48
+ /** The four sections of a kernel file, in their order. */
49
+ export const KERNEL_SECTIONS = ['controls', 'tables', 'aggregates', 'constants'];
50
+
51
+ /** Whether a data file is a kernel file (`kernels/<kernel>.json`), which holds the four sections, never an item map. */
52
+ export const isKernelFile = (rel) => /(^|\/)kernels\/[^/]+\.json$/.test(rel);
53
+
54
+ /** The kernel's upper-case prefix, read off its file name: `kernels/band_dyn.json` is BAND_DYN. */
55
+ export const kernelPrefix = (rel) => rel.replace(/^.*\//, '').replace(/\.json$/, '').toUpperCase().replace(/-/g, '_');
56
+
57
+ /** A control's item name: its `global`, else <KERNEL>_<NAME>_RANGE for a travel and <KERNEL>_<NAME>S for a choice. */
58
+ export const controlGlobal = (rel, c) => c.global ?? `${kernelPrefix(rel)}_${snakeOf(c.name)}${c.kind === 'choice' ? 'S' : '_RANGE'}`;
59
+
60
+ /**
61
+ * A data file's items as [name, item] pairs. A kernel file's four sections are expanded into the items the renders
62
+ * read: a travel control is a `travels` item named by controlGlobal, a choice a `set`; the controls of one `table`
63
+ * regroup into that table's `travels` item with `fields`; an aggregate is regenerated as fields that reference its
64
+ * controls; constants pass through. Any other top-level key of a kernel file is not read (validation names it).
65
+ * rates.json and primitives.json are item maps, read as they are.
66
+ */
67
+ export function entriesOf(rel, doc) {
68
+ if (!isKernelFile(rel)) return Object.entries(doc).filter(([name]) => name !== '$schema');
69
+ const out = [];
70
+ const controls = Array.isArray(doc.controls) ? doc.controls : [];
71
+ const tables = new Map(Object.entries(doc.tables ?? {}).map(([n, meta]) => [n, { kind: 'travels', ...meta, fields: {} }]));
72
+ for (const c of controls) {
73
+ const { name, kind, global, table, ...rest } = c;
74
+ if (table) {
75
+ if (!tables.has(table)) throw new Error(`${rel}: control ${name} names table ${table}, which is not declared`);
76
+ tables.get(table).fields[name] = rest.travel;
77
+ continue;
78
+ }
79
+ out.push([controlGlobal(rel, c), kind === 'choice' ? { kind: 'set', ...rest } : { kind: 'travels', ...rest }]);
80
+ }
81
+ for (const [n, t] of tables) out.push([n, t]);
82
+ const byName = new Map(controls.map((c) => [c.name, c]));
83
+ for (const [n, a] of Object.entries(doc.aggregates ?? {})) {
84
+ const { fields, ...meta } = a;
85
+ out.push([n, { kind: 'travels', ...meta, fields: Object.fromEntries((fields ?? []).map((f) => {
86
+ if (!byName.has(f)) throw new Error(`${rel}: aggregate ${n} names control ${f}, which is not declared`);
87
+ return [f, { ref: controlGlobal(rel, byName.get(f)) }];
88
+ })) }]);
89
+ }
90
+ for (const [n, item] of Object.entries(doc.constants ?? {})) out.push([n, item]);
91
+ return out;
92
+ }
93
+
94
+ const isWholeRef = (v) => typeof v === 'object' && v !== null && Object.keys(v).length === 1 && typeof v.ref === 'string' && !v.ref.includes('.');
95
+
96
+ /**
97
+ * The kernel file that declares `entries` ([name, item] pairs, the items entriesOf gives back), the inverse of
98
+ * entriesOf: a single travel is a travel control and a set a choice; a fields table whose every field IS a single
99
+ * travel of the kernel (`{ ref }`) is an aggregate, and its field names name those controls; any other fields table
100
+ * is a table whose fields are its controls; every other item is a constant. A control's `global` is written only
101
+ * when its name does not derive it. `kept`, a kernel file already written, is extended: its declarations stay as
102
+ * they are and the entries it lacks are added. Empty sections are left out.
103
+ * @throws naming the item: a fields table that mixes references and travels, an aggregate field whose name is not
104
+ * its control's.
105
+ */
106
+ export function kernelDocOf(rel, entries, kept) {
107
+ const K = kernelPrefix(rel);
108
+ const doc = kept ? JSON.parse(JSON.stringify(kept)) : {};
109
+ doc.controls ??= [];
110
+ for (const s of KERNEL_SECTIONS.slice(1)) doc[s] ??= {};
111
+ const declared = new Set(entriesOf(rel, kept ?? {}).map(([n]) => n));
112
+ const singles = new Map(entries.filter(([, it]) => (it.kind === 'travels' && it.travel) || it.kind === 'set'));
113
+ for (const c of doc.controls) if (!c.table) singles.set(controlGlobal(rel, c), c);
114
+ const isAggregate = (it) => it.kind === 'travels' && it.fields && Object.values(it.fields).length > 0 && Object.values(it.fields).every((f) => isWholeRef(f) && singles.get(f.ref)?.travel);
115
+ // an aggregate's field names are its controls' names
116
+ const named = new Map();
117
+ for (const [n, it] of entries) if (isAggregate(it)) for (const [f, t] of Object.entries(it.fields)) named.set(t.ref, f);
118
+ for (const [n, it] of entries) {
119
+ if (declared.has(n)) continue;
120
+ if ((it.kind === 'travels' && it.travel) || it.kind === 'set') {
121
+ const { kind, ...rest } = it;
122
+ const ck = kind === 'set' ? 'choice' : 'travel';
123
+ const suffix = ck === 'choice' ? 'S' : '_RANGE';
124
+ const derived = n.startsWith(`${K}_`) && n.endsWith(suffix) && n.length > K.length + 1 + suffix.length ? camelOf(n.slice(K.length + 1, -suffix.length)) : undefined;
125
+ const c = { name: named.get(n) ?? derived ?? camelOf(n.endsWith(suffix) ? n.slice(0, -suffix.length) : n), kind: ck };
126
+ if (controlGlobal(rel, c) !== n) c.global = n;
127
+ doc.controls.push({ ...c, ...rest });
128
+ } else if (it.kind === 'travels' && it.fields && isAggregate(it)) {
129
+ const { kind, fields, ...meta } = it;
130
+ doc.aggregates[n] = { ...meta, fields: Object.entries(fields).map(([f, t]) => {
131
+ const c = doc.controls.find((x) => !x.table && controlGlobal(rel, x) === t.ref) ?? { name: named.get(t.ref) };
132
+ if (c.name !== f) throw new Error(`${rel}: ${n}.${f} refers to ${t.ref}, whose control is named ${c.name}: an aggregate's field is named by its control`);
133
+ return f;
134
+ }) };
135
+ } else if (it.kind === 'travels' && it.fields) {
136
+ const { kind, fields, ...meta } = it;
137
+ if (Object.values(fields).some(isWholeRef)) throw new Error(`${rel}: ${n} mixes fields that refer to single travels with travels of its own: declare it as an aggregate of controls, or as a table`);
138
+ doc.tables[n] = meta;
139
+ for (const [f, travel] of Object.entries(fields)) doc.controls.push({ name: f, kind: 'travel', table: n, travel });
140
+ } else {
141
+ doc.constants[n] = it;
142
+ }
143
+ }
144
+ for (const s of KERNEL_SECTIONS) if (s === 'controls' ? !doc[s].length : !Object.keys(doc[s]).length) delete doc[s];
145
+ return doc;
146
+ }
147
+
46
148
  /** An item's own value before resolution, by kind. */
47
149
  export function rawValue(item) {
48
150
  switch (item.kind) {
@@ -50,6 +152,7 @@ export function rawValue(item) {
50
152
  case 'travels': return item.fields ?? item.travel;
51
153
  case 'sheet': return item.value;
52
154
  case 'list': return item.values;
155
+ case 'set': return item.ids;
53
156
  default: return undefined;
54
157
  }
55
158
  }
@@ -66,6 +169,11 @@ export const DERIVATIONS = {
66
169
  if (typeof v !== 'number' || v < 0) throw new Error(`sqrt: ${JSON.stringify(d.of)} is not a non-negative number`);
67
170
  return Math.sqrt(v);
68
171
  },
172
+ ceilSum: (d, val) => {
173
+ const terms = d.of.map((x) => val(x));
174
+ if (!terms.every((t) => typeof t === 'number' && Number.isFinite(t))) throw new Error(`ceilSum: ${JSON.stringify(d.of)} holds a term that is not a finite number`);
175
+ return Math.ceil(terms.reduce((a, t) => a + t, 0));
176
+ },
69
177
  lagrangeReadL1: (d, val) => {
70
178
  const order = val(d.order);
71
179
  if (!Number.isInteger(order) || order < 1) throw new Error(`lagrangeReadL1: order ${order} is not a positive integer`);
@@ -104,7 +212,7 @@ export function lagrangeReadL1(order) {
104
212
  const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
105
213
 
106
214
  /**
107
- * Resolve every item: `{ name, rel, kind, doc, why?, unit?, c?, shape?, value }`, where `value` has
215
+ * Resolve every item: `{ name, rel, kind, doc, why?, unit?, c?, shape?, default?, labels?, value }` (default and labels: a set's), where `value` has
108
216
  * every `{ref}` and `{derive}` evaluated.
109
217
  * @throws naming the item: a reference that does not resolve, a cycle, an unknown derivation.
110
218
  */
@@ -142,6 +250,10 @@ export function resolveData(data) {
142
250
  if (item.why !== undefined) r.why = item.why;
143
251
  if (item.unit !== undefined) r.unit = item.unit;
144
252
  if (item.c !== undefined) r.c = item.c;
253
+ if (item.kind === 'set') {
254
+ if (item.default !== undefined) r.default = item.default;
255
+ if (item.labels !== undefined) r.labels = item.labels;
256
+ }
145
257
  if (item.kind === 'travels') r.shape = item.fields ? 'table' : 'travel';
146
258
  r.value = val(rawValue(item), name);
147
259
  busy.delete(name);
@@ -0,0 +1,87 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * THE default rule for the bands of a fresh EQ, whatever the strip, the host or the band count: one
5
+ * rule, no override, for the console, omx-plugins and any other host. Per strip type the contract
6
+ * declares only the band counts (EQ_BAND_COUNTS: default and maximum); the bands come from here.
7
+ *
8
+ * Types the first band is a low shelf and the last a high shelf (each switchable to a bell by the
9
+ * operator); every band between is a bell. A single band is a bell.
10
+ * Gain, Q every band is EQ_BAND_DEFAULTS.gainDb and .q.
11
+ * Centres
12
+ * 4 bands the declared four (EQ_DEFAULT_CENTRES_FOUR_BAND_HZ): 100, 400, 2000, 8000 Hz,
13
+ * ISO 266 preferred values.
14
+ * N bands N slots of equal width on a log axis across EQ_FREQ_RANGE; band i sits in the
15
+ * middle of slot i (lo * (hi / lo) ** ((i + 0.5) / N)) and takes the nearest preferred
16
+ * frequency on that axis, from the ISO 266 R10 series (ISO_THIRD_OCTAVE_CENTRES_HZ);
17
+ * when two bands would share a value in R10 the same is done on its R20 refinement
18
+ * (EQ_CENTRE_SERIES_R20_HZ). Never a repeated centre: more bands than R20 holds is an
19
+ * error.
20
+ *
21
+ * The same functions are exported by the ts render (`eqDefaultCentres`, `eqDefaultTypes`,
22
+ * `eqDefaultBands`) and the c render (`omx_eq_default_centres`, `omx_eq_default_type`); test/eq-defaults.test.mjs holds the three equal for every count.
23
+ * This file is the one JavaScript spelling: the ts render embeds its source.
24
+ */
25
+
26
+ /**
27
+ * The default band centres (Hz) of an EQ with `count` bands.
28
+ * @param {number} count how many bands the strip's EQ starts with, a positive integer.
29
+ * @param {{ four: number[], series: number[][], lo: number, hi: number }} p the declared four-band
30
+ * set, the preferred-frequency series from coarsest to finest, and the band frequency travel.
31
+ * @returns {number[]}
32
+ */
33
+ export function eqDefaultCentres(count, p) {
34
+ if (!Number.isInteger(count) || count < 1) throw new RangeError(`eqDefaultCentres: ${count} is not a band count`);
35
+ if (count === 4) return [...p.four];
36
+ const targets = [];
37
+ for (let i = 0; i < count; i++) targets.push(Math.log(p.lo) + (Math.log(p.hi / p.lo) * (i + 0.5)) / count);
38
+ for (const series of p.series) {
39
+ const out = targets.map((t) => {
40
+ let best = series[0];
41
+ let bestD = Infinity;
42
+ for (const f of series) {
43
+ const d = Math.abs(Math.log(f) - t);
44
+ if (d < bestD) { bestD = d; best = f; }
45
+ }
46
+ return best;
47
+ });
48
+ if (new Set(out).size === out.length) return out;
49
+ }
50
+ throw new RangeError(`eqDefaultCentres: ${count} bands have no distinct preferred centres across ${p.lo}..${p.hi} Hz`);
51
+ }
52
+
53
+ /**
54
+ * The band types of an EQ with `count` bands, by EQ_BAND_TYPES id: a low shelf, bells, a high shelf.
55
+ * @param {number} count
56
+ * @returns {string[]}
57
+ */
58
+ export function eqDefaultTypes(count) {
59
+ if (!Number.isInteger(count) || count < 1) throw new RangeError(`eqDefaultTypes: ${count} is not a band count`);
60
+ return Array.from({ length: count }, (_, i) => (count === 1 ? 'bell' : i === 0 ? 'lowShelf' : i === count - 1 ? 'highShelf' : 'bell'));
61
+ }
62
+
63
+ /**
64
+ * Every default band of an EQ with `count` bands.
65
+ * @param {number} count
66
+ * @param {{ four: number[], series: number[][], lo: number, hi: number, gainDb: number, q: number }} p
67
+ * @returns {{ type: string, freqHz: number, gainDb: number, q: number }[]}
68
+ */
69
+ export function eqDefaultBands(count, p) {
70
+ const types = eqDefaultTypes(count);
71
+ return eqDefaultCentres(count, p).map((freqHz, i) => ({ type: types[i], freqHz, gainDb: p.gainDb, q: p.q }));
72
+ }
73
+
74
+ /** The parameters of the rule, read off the resolved items. */
75
+ export function eqDefaultParams(resolved) {
76
+ const v = (name) => {
77
+ const r = resolved.get(name);
78
+ if (!r) throw new Error(`the default centre rule needs ${name}`);
79
+ return r.value;
80
+ };
81
+ const range = v('EQ_FREQ_RANGE');
82
+ const band = v('EQ_BAND_DEFAULTS');
83
+ return {
84
+ four: v('EQ_DEFAULT_CENTRES_FOUR_BAND_HZ'), series: [v('ISO_THIRD_OCTAVE_CENTRES_HZ'), v('EQ_CENTRE_SERIES_R20_HZ')],
85
+ lo: range.min, hi: range.max, gainDb: band.gainDb, q: band.q,
86
+ };
87
+ }
package/lib/fmt.mjs CHANGED
@@ -3,12 +3,15 @@
3
3
  /**
4
4
  * The one formatting of a data file (spec §3.2): two-space indent, one key per line, keys in schema
5
5
  * order, a trailing newline. Names (items, fields, sheet keys) keep the order the author gave them:
6
- * a travels table's field order is its C block order.
6
+ * a travels table's field order is its C block order. A kernel file's sections come in their order (controls,
7
+ * tables, aggregates, constants), its controls in the author's order, each control's keys in schema order.
7
8
  */
8
9
 
9
10
  const ORDER = {
10
- item: ['kind', 'doc', 'why', 'unit', 'value', 'values', 'fields', 'travel', 'c'],
11
- travel: ['min', 'max', 'step', 'unit', 'default', 'defaultFrom', 'byKind', 'doc', 'why'],
11
+ control: ['name', 'kind', 'global', 'table', 'since', 'doc', 'why', 'unit', 'ids', 'default', 'labels', 'travel', 'c'],
12
+ sections: ['controls', 'tables', 'aggregates', 'constants'],
13
+ item: ['kind', 'since', 'doc', 'why', 'unit', 'value', 'values', 'ids', 'default', 'labels', 'fields', 'travel', 'c'],
14
+ travel: ['min', 'max', 'step', 'unit', 'default', 'defaultFrom', 'scale', 'byKind', 'doc', 'why'],
12
15
  c: ['alias', 'render'],
13
16
  alias: ['fact', 'field'],
14
17
  number: ['ref', 'derive', 'of', 'order'],
@@ -52,10 +55,27 @@ function item(it) {
52
55
  return o;
53
56
  }
54
57
 
58
+ function control(c) {
59
+ if (!isObj(c)) return c;
60
+ const o = ordered(c, ORDER.control);
61
+ if (isObj(o.travel)) o.travel = travel(o.travel);
62
+ if (isObj(o.c)) o.c = item({ c: o.c }).c;
63
+ return o;
64
+ }
65
+
66
+ const items = (m) => (isObj(m) ? Object.fromEntries(Object.entries(m).map(([k, v]) => [k, item(v)])) : m);
67
+
55
68
  /** The canonical text of a parsed data file. */
56
69
  export function formatDoc(doc) {
57
70
  const out = {};
58
71
  if ('$schema' in doc) out.$schema = doc.$schema;
59
- for (const [k, v] of Object.entries(doc)) if (k !== '$schema') out[k] = item(v);
72
+ if (ORDER.sections.some((k) => k in doc)) {
73
+ for (const [k, v] of Object.entries(ordered(doc, ORDER.sections))) {
74
+ if (k === '$schema') continue;
75
+ out[k] = k === 'controls' && Array.isArray(v) ? v.map(control) : ORDER.sections.includes(k) ? items(v) : item(v);
76
+ }
77
+ } else {
78
+ for (const [k, v] of Object.entries(doc)) if (k !== '$schema') out[k] = item(v);
79
+ }
60
80
  return `${JSON.stringify(out, null, 2)}\n`;
61
81
  }