@hyperframes/core 0.7.107 → 0.7.109

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 (89) hide show
  1. package/dist/audio/audioFxAutomation.d.ts +63 -0
  2. package/dist/audio/audioFxAutomation.d.ts.map +1 -0
  3. package/dist/audio/audioFxAutomation.js +219 -0
  4. package/dist/audio/audioFxAutomation.js.map +1 -0
  5. package/dist/audio/audioFxGraph.d.ts +73 -0
  6. package/dist/audio/audioFxGraph.d.ts.map +1 -0
  7. package/dist/audio/audioFxGraph.js +649 -0
  8. package/dist/audio/audioFxGraph.js.map +1 -0
  9. package/dist/audio/audioFxTail.d.ts +28 -0
  10. package/dist/audio/audioFxTail.d.ts.map +1 -0
  11. package/dist/audio/audioFxTail.js +95 -0
  12. package/dist/audio/audioFxTail.js.map +1 -0
  13. package/dist/audio/audioFxWorklets.d.ts +8 -0
  14. package/dist/audio/audioFxWorklets.d.ts.map +1 -0
  15. package/dist/audio/audioFxWorklets.js +263 -0
  16. package/dist/audio/audioFxWorklets.js.map +1 -0
  17. package/dist/audioAutomation.d.ts +191 -0
  18. package/dist/audioAutomation.d.ts.map +1 -0
  19. package/dist/audioAutomation.js +488 -0
  20. package/dist/audioAutomation.js.map +1 -0
  21. package/dist/audioCarve.d.ts +216 -0
  22. package/dist/audioCarve.d.ts.map +1 -0
  23. package/dist/audioCarve.js +608 -0
  24. package/dist/audioCarve.js.map +1 -0
  25. package/dist/audioFx.d.ts +182 -0
  26. package/dist/audioFx.d.ts.map +1 -0
  27. package/dist/audioFx.js +813 -0
  28. package/dist/audioFx.js.map +1 -0
  29. package/dist/audioFxCopy.d.ts +89 -0
  30. package/dist/audioFxCopy.d.ts.map +1 -0
  31. package/dist/audioFxCopy.js +340 -0
  32. package/dist/audioFxCopy.js.map +1 -0
  33. package/dist/audioFxEq.d.ts +75 -0
  34. package/dist/audioFxEq.d.ts.map +1 -0
  35. package/dist/audioFxEq.js +153 -0
  36. package/dist/audioFxEq.js.map +1 -0
  37. package/dist/audioFxJobs.d.ts +52 -0
  38. package/dist/audioFxJobs.d.ts.map +1 -0
  39. package/dist/audioFxJobs.js +92 -0
  40. package/dist/audioFxJobs.js.map +1 -0
  41. package/dist/audioFxPresets.d.ts +91 -0
  42. package/dist/audioFxPresets.d.ts.map +1 -0
  43. package/dist/audioFxPresets.js +320 -0
  44. package/dist/audioFxPresets.js.map +1 -0
  45. package/dist/audioFxProfiles.d.ts +65 -0
  46. package/dist/audioFxProfiles.d.ts.map +1 -0
  47. package/dist/audioFxProfiles.js +209 -0
  48. package/dist/audioFxProfiles.js.map +1 -0
  49. package/dist/audioLeveller.d.ts +62 -0
  50. package/dist/audioLeveller.d.ts.map +1 -0
  51. package/dist/audioLeveller.js +208 -0
  52. package/dist/audioLeveller.js.map +1 -0
  53. package/dist/canaryRegistry.d.ts.map +1 -1
  54. package/dist/canaryRegistry.js +8 -0
  55. package/dist/canaryRegistry.js.map +1 -1
  56. package/dist/compiler/compositionScoping.d.ts +3 -0
  57. package/dist/compiler/compositionScoping.d.ts.map +1 -1
  58. package/dist/compiler/compositionScoping.js +34 -11
  59. package/dist/compiler/compositionScoping.js.map +1 -1
  60. package/dist/compiler/htmlBundler.d.ts.map +1 -1
  61. package/dist/compiler/htmlBundler.js +42 -2
  62. package/dist/compiler/htmlBundler.js.map +1 -1
  63. package/dist/editing/affordances.d.ts +3 -0
  64. package/dist/editing/affordances.d.ts.map +1 -1
  65. package/dist/editing/affordances.js +1 -0
  66. package/dist/editing/affordances.js.map +1 -1
  67. package/dist/generated/audio-fx-runtime-inline.d.ts +3 -0
  68. package/dist/generated/audio-fx-runtime-inline.d.ts.map +1 -0
  69. package/dist/generated/audio-fx-runtime-inline.js +7 -0
  70. package/dist/generated/audio-fx-runtime-inline.js.map +1 -0
  71. package/dist/generated/runtime-inline.js +1 -1
  72. package/dist/generated/runtime-inline.js.map +1 -1
  73. package/dist/hyperframe.runtime.iife.js +238 -28
  74. package/dist/runtime/applyVariableBindings.d.ts +45 -0
  75. package/dist/runtime/applyVariableBindings.d.ts.map +1 -0
  76. package/dist/runtime/applyVariableBindings.js +194 -0
  77. package/dist/runtime/applyVariableBindings.js.map +1 -0
  78. package/dist/runtime/audioAutomationVolume.d.ts +17 -0
  79. package/dist/runtime/audioAutomationVolume.d.ts.map +1 -0
  80. package/dist/runtime/audioAutomationVolume.js +49 -0
  81. package/dist/runtime/audioAutomationVolume.js.map +1 -0
  82. package/dist/runtime/media.d.ts.map +1 -1
  83. package/dist/runtime/media.js +17 -1
  84. package/dist/runtime/media.js.map +1 -1
  85. package/dist/runtime/variableScope.d.ts +9 -0
  86. package/dist/runtime/variableScope.d.ts.map +1 -0
  87. package/dist/runtime/variableScope.js +21 -0
  88. package/dist/runtime/variableScope.js.map +1 -0
  89. package/package.json +51 -6
@@ -0,0 +1,813 @@
1
+ /**
2
+ * Audio FX chain: the one description of every effect that can be applied to an
3
+ * audio track.
4
+ *
5
+ * Preview and render both run the same Web Audio graph — the studio in a live
6
+ * AudioContext, the engine in an OfflineAudioContext inside the headless
7
+ * browser it already drives. There is only one implementation of each effect,
8
+ * so preview predicting the render is a property of the architecture rather
9
+ * than something to measure and defend.
10
+ *
11
+ * This file holds what both ends need to agree on: the parameter set for each
12
+ * effect with its usable range, and the id of the graph builder that realises
13
+ * it. Parameters are declared in the units a person thinks in (dB, ms, Hz);
14
+ * the graph builders convert where the Web Audio node wants something else.
15
+ */
16
+ export const HF_AUDIO_FX_ATTR = "data-fx-chain";
17
+ /**
18
+ * The same attribute as a `dataset` / `dataAttributes` key — the `data-` prefix
19
+ * is not part of that spelling.
20
+ *
21
+ * Derived rather than restated: the studio writes through
22
+ * `HF_AUDIO_FX_ATTR` and reads through the key, so a hardcoded `"fx-chain"`
23
+ * on the read side is a rename waiting to half-land.
24
+ */
25
+ export const HF_AUDIO_FX_DATA_KEY = HF_AUDIO_FX_ATTR.slice("data-".length);
26
+ /** Chain files are versioned; a reader must refuse a version it doesn't know. */
27
+ export const HF_AUDIO_FX_CHAIN_VERSION = 1;
28
+ const freq = (key = "frequency", label = "Frequency", def = 1000, min = 20, max = 20000) => ({
29
+ kind: "number",
30
+ key,
31
+ label,
32
+ unit: "Hz",
33
+ min,
34
+ max,
35
+ step: 1,
36
+ default: def,
37
+ scale: "log",
38
+ automatable: true,
39
+ });
40
+ const qParam = (def = 0.707, hint = "Bandwidth — higher is narrower.") => ({
41
+ kind: "number",
42
+ key: "q",
43
+ label: "Q",
44
+ unit: "",
45
+ min: 0.1,
46
+ max: 20,
47
+ step: 0.01,
48
+ default: def,
49
+ scale: "log",
50
+ automatable: true,
51
+ hint,
52
+ });
53
+ const gainDb = (min = -40, max = 40, def = 0) => ({
54
+ kind: "number",
55
+ key: "gain",
56
+ label: "Gain",
57
+ unit: "dB",
58
+ min,
59
+ max,
60
+ step: 0.1,
61
+ default: def,
62
+ automatable: true,
63
+ });
64
+ const poles = {
65
+ kind: "enum",
66
+ key: "poles",
67
+ label: "Slope",
68
+ options: [
69
+ { value: "1", label: "6 dB/oct" },
70
+ { value: "2", label: "12 dB/oct" },
71
+ ],
72
+ default: "2",
73
+ hint: "Two poles is the usual biquad; one pole is gentler.",
74
+ };
75
+ /**
76
+ * Every effect, in panel order. Ranges are the usable span for each control;
77
+ * a value that survives `normalizeAudioFxParams` is always safe to realise.
78
+ */
79
+ export const HF_AUDIO_FX = [
80
+ {
81
+ id: "gain",
82
+ label: "Gain",
83
+ group: "dynamics",
84
+ description: "Raise or lower the whole signal. Automate it to duck under something else.",
85
+ // Cuts go deep, boosts stay modest: this is a level stage for making room,
86
+ // and a chain that can add 40 dB clips long before it is useful.
87
+ params: [gainDb(-60, 12, 0)],
88
+ web: "gain-node",
89
+ },
90
+ {
91
+ id: "peaking",
92
+ label: "Peaking EQ",
93
+ group: "filter",
94
+ description: "Boost or cut a band, leaving everything either side alone.",
95
+ params: [freq("frequency", "Frequency", 1000), gainDb(-40, 40, 0), qParam(1)],
96
+ web: "biquad-peaking",
97
+ },
98
+ {
99
+ id: "lowshelf",
100
+ label: "Low Shelf",
101
+ group: "filter",
102
+ description: "Lift or drop everything below the corner frequency.",
103
+ // No Q: the Web Audio spec leaves it unused for shelving filters, so the
104
+ // control moved nothing — and being automatable, a lane drawn on it would
105
+ // have been silently inert.
106
+ params: [freq("frequency", "Frequency", 200, 20, 2000), gainDb(-40, 40, 0)],
107
+ web: "biquad-lowshelf",
108
+ },
109
+ {
110
+ id: "highshelf",
111
+ label: "High Shelf",
112
+ group: "filter",
113
+ description: "Lift or drop everything above the corner frequency.",
114
+ params: [freq("frequency", "Frequency", 4000, 500, 20000), gainDb(-40, 40, 0)],
115
+ web: "biquad-highshelf",
116
+ },
117
+ {
118
+ id: "highpass",
119
+ label: "High-pass",
120
+ group: "filter",
121
+ description: "Remove low frequencies — the usual fix for rumble on a voice.",
122
+ params: [freq("frequency", "Cutoff", 300, 20, 20000), qParam(0.707), poles],
123
+ web: "biquad-highpass",
124
+ },
125
+ {
126
+ id: "lowpass",
127
+ label: "Low-pass",
128
+ group: "filter",
129
+ description: "Remove high frequencies — darkens or muffles a track.",
130
+ params: [freq("frequency", "Cutoff", 8000, 100, 20000), qParam(0.707), poles],
131
+ web: "biquad-lowpass",
132
+ },
133
+ {
134
+ id: "compressor",
135
+ label: "Compressor",
136
+ group: "dynamics",
137
+ description: "Pull loud parts down so the quiet ones can come up.",
138
+ params: [
139
+ {
140
+ kind: "number",
141
+ key: "threshold",
142
+ label: "Threshold",
143
+ unit: "dB",
144
+ min: -60,
145
+ max: 0,
146
+ step: 0.5,
147
+ default: -24,
148
+ hint: "Level above which the compressor starts working.",
149
+ },
150
+ {
151
+ kind: "number",
152
+ key: "ratio",
153
+ label: "Ratio",
154
+ unit: ":1",
155
+ min: 1,
156
+ max: 20,
157
+ step: 0.1,
158
+ default: 4,
159
+ },
160
+ {
161
+ kind: "number",
162
+ key: "attack",
163
+ label: "Attack",
164
+ unit: "ms",
165
+ min: 0.01,
166
+ max: 2000,
167
+ step: 0.1,
168
+ default: 20,
169
+ scale: "log",
170
+ },
171
+ {
172
+ kind: "number",
173
+ key: "release",
174
+ label: "Release",
175
+ unit: "ms",
176
+ min: 0.01,
177
+ max: 9000,
178
+ step: 1,
179
+ default: 250,
180
+ scale: "log",
181
+ },
182
+ {
183
+ kind: "number",
184
+ key: "knee",
185
+ label: "Knee",
186
+ unit: "",
187
+ min: 1,
188
+ max: 8,
189
+ step: 0.01,
190
+ default: 2.83,
191
+ hint: "1 is a hard corner; higher eases into it.",
192
+ },
193
+ {
194
+ kind: "number",
195
+ key: "makeup",
196
+ label: "Makeup",
197
+ unit: "dB",
198
+ min: 0,
199
+ max: 36,
200
+ step: 0.1,
201
+ default: 0,
202
+ },
203
+ {
204
+ kind: "number",
205
+ key: "mix",
206
+ label: "Mix",
207
+ unit: "",
208
+ min: 0,
209
+ max: 1,
210
+ step: 0.01,
211
+ default: 1,
212
+ hint: "Below 1 blends the dry signal back in.",
213
+ },
214
+ ],
215
+ web: "worklet-compressor",
216
+ },
217
+ {
218
+ id: "limiter",
219
+ label: "Limiter",
220
+ group: "dynamics",
221
+ description: "Hard ceiling — nothing gets past the limit.",
222
+ params: [
223
+ {
224
+ kind: "number",
225
+ key: "limit",
226
+ label: "Ceiling",
227
+ unit: "dB",
228
+ min: -24,
229
+ max: 0,
230
+ step: 0.1,
231
+ default: -1,
232
+ },
233
+ {
234
+ kind: "number",
235
+ key: "attack",
236
+ label: "Attack",
237
+ unit: "ms",
238
+ min: 0.1,
239
+ max: 80,
240
+ step: 0.1,
241
+ default: 5,
242
+ },
243
+ {
244
+ kind: "number",
245
+ key: "release",
246
+ label: "Release",
247
+ unit: "ms",
248
+ min: 1,
249
+ max: 8000,
250
+ step: 1,
251
+ default: 50,
252
+ scale: "log",
253
+ },
254
+ {
255
+ kind: "number",
256
+ key: "level_out",
257
+ label: "Output",
258
+ unit: "dB",
259
+ min: -24,
260
+ max: 24,
261
+ step: 0.1,
262
+ default: 0,
263
+ },
264
+ ],
265
+ web: "worklet-limiter",
266
+ },
267
+ {
268
+ id: "gate",
269
+ label: "Noise Gate",
270
+ group: "dynamics",
271
+ description: "Silence the track when it drops below the threshold.",
272
+ params: [
273
+ {
274
+ kind: "number",
275
+ key: "threshold",
276
+ label: "Threshold",
277
+ unit: "dB",
278
+ min: -80,
279
+ max: 0,
280
+ step: 0.5,
281
+ default: -35,
282
+ },
283
+ {
284
+ kind: "number",
285
+ key: "range",
286
+ label: "Range",
287
+ unit: "dB",
288
+ min: -80,
289
+ max: 0,
290
+ step: 0.5,
291
+ default: -24,
292
+ hint: "How far down the gate pulls when closed.",
293
+ },
294
+ {
295
+ kind: "number",
296
+ key: "ratio",
297
+ label: "Ratio",
298
+ unit: ":1",
299
+ min: 1,
300
+ max: 20,
301
+ step: 0.1,
302
+ default: 10,
303
+ },
304
+ {
305
+ kind: "number",
306
+ key: "attack",
307
+ label: "Attack",
308
+ unit: "ms",
309
+ min: 0.01,
310
+ max: 9000,
311
+ step: 0.1,
312
+ default: 1,
313
+ scale: "log",
314
+ },
315
+ {
316
+ kind: "number",
317
+ key: "release",
318
+ label: "Release",
319
+ unit: "ms",
320
+ min: 0.01,
321
+ max: 9000,
322
+ step: 1,
323
+ default: 100,
324
+ scale: "log",
325
+ },
326
+ {
327
+ kind: "number",
328
+ key: "knee",
329
+ label: "Knee",
330
+ unit: "",
331
+ min: 1,
332
+ max: 8,
333
+ step: 0.01,
334
+ default: 2.83,
335
+ },
336
+ ],
337
+ web: "worklet-gate",
338
+ },
339
+ {
340
+ id: "saturate",
341
+ label: "Saturation",
342
+ group: "nonlinear",
343
+ description: "Soft-clip the waveform for warmth or outright distortion.",
344
+ params: [
345
+ {
346
+ kind: "enum",
347
+ key: "type",
348
+ label: "Curve",
349
+ options: [
350
+ { value: "tanh", label: "Tanh" },
351
+ { value: "atan", label: "Arctan" },
352
+ { value: "cubic", label: "Cubic" },
353
+ { value: "exp", label: "Exponential" },
354
+ { value: "alg", label: "Algebraic" },
355
+ { value: "quintic", label: "Quintic" },
356
+ { value: "sin", label: "Sine" },
357
+ { value: "erf", label: "Error function" },
358
+ { value: "hard", label: "Hard clip" },
359
+ ],
360
+ default: "tanh",
361
+ },
362
+ {
363
+ kind: "number",
364
+ key: "threshold",
365
+ label: "Threshold",
366
+ unit: "dB",
367
+ min: -40,
368
+ max: 0,
369
+ step: 0.1,
370
+ default: -6,
371
+ },
372
+ {
373
+ kind: "number",
374
+ key: "output",
375
+ automatable: true,
376
+ label: "Output",
377
+ unit: "dB",
378
+ min: -24,
379
+ max: 24,
380
+ step: 0.1,
381
+ default: 0,
382
+ },
383
+ {
384
+ kind: "number",
385
+ key: "oversample",
386
+ label: "Oversample",
387
+ unit: "x",
388
+ min: 1,
389
+ max: 8,
390
+ step: 1,
391
+ default: 4,
392
+ hint: "Higher costs more but keeps aliasing down.",
393
+ },
394
+ ],
395
+ web: "waveshaper",
396
+ },
397
+ {
398
+ id: "bitcrush",
399
+ label: "Bitcrush",
400
+ group: "nonlinear",
401
+ description: "Drop bit depth and sample rate for a lo-fi, digital sound.",
402
+ params: [
403
+ {
404
+ kind: "number",
405
+ key: "bits",
406
+ label: "Bit depth",
407
+ unit: "bit",
408
+ min: 1,
409
+ max: 32,
410
+ step: 0.1,
411
+ default: 8,
412
+ },
413
+ {
414
+ kind: "number",
415
+ key: "samples",
416
+ label: "Sample hold",
417
+ unit: "x",
418
+ min: 1,
419
+ max: 250,
420
+ step: 1,
421
+ default: 1,
422
+ hint: "Repeats each sample N times — a crude downsample.",
423
+ },
424
+ {
425
+ kind: "number",
426
+ key: "mix",
427
+ label: "Mix",
428
+ unit: "",
429
+ min: 0,
430
+ max: 1,
431
+ step: 0.01,
432
+ default: 1,
433
+ },
434
+ ],
435
+ web: "worklet-bitcrush",
436
+ },
437
+ {
438
+ id: "delay",
439
+ label: "Delay",
440
+ group: "time",
441
+ description: "Repeating echoes behind the dry signal.",
442
+ params: [
443
+ {
444
+ kind: "number",
445
+ key: "time",
446
+ automatable: true,
447
+ label: "Time",
448
+ unit: "ms",
449
+ min: 1,
450
+ max: 5000,
451
+ step: 1,
452
+ default: 250,
453
+ scale: "log",
454
+ },
455
+ // aecho rejects a decay of exactly 0 ("out of allowed range: (0, 1]"),
456
+ // so the floor is a hair above silence rather than at it.
457
+ {
458
+ kind: "number",
459
+ key: "feedback",
460
+ automatable: true,
461
+ label: "Feedback",
462
+ unit: "",
463
+ min: 0.01,
464
+ max: 0.95,
465
+ step: 0.01,
466
+ default: 0.35,
467
+ },
468
+ {
469
+ kind: "number",
470
+ key: "mix",
471
+ automatable: true,
472
+ label: "Mix",
473
+ unit: "",
474
+ min: 0,
475
+ max: 1,
476
+ step: 0.01,
477
+ default: 0.4,
478
+ },
479
+ ],
480
+ web: "delay-feedback",
481
+ },
482
+ {
483
+ id: "chorus",
484
+ label: "Chorus",
485
+ group: "time",
486
+ description: "Detuned copies of the signal for width and thickness.",
487
+ params: [
488
+ {
489
+ kind: "number",
490
+ key: "delay",
491
+ automatable: true,
492
+ label: "Delay",
493
+ unit: "ms",
494
+ min: 1,
495
+ max: 100,
496
+ step: 0.1,
497
+ default: 7,
498
+ },
499
+ {
500
+ kind: "number",
501
+ key: "depth",
502
+ automatable: true,
503
+ label: "Depth",
504
+ unit: "ms",
505
+ min: 0,
506
+ max: 10,
507
+ step: 0.01,
508
+ default: 2,
509
+ },
510
+ {
511
+ kind: "number",
512
+ key: "speed",
513
+ automatable: true,
514
+ label: "Rate",
515
+ unit: "Hz",
516
+ min: 0.01,
517
+ max: 10,
518
+ step: 0.01,
519
+ default: 1,
520
+ },
521
+ {
522
+ kind: "number",
523
+ key: "mix",
524
+ automatable: true,
525
+ label: "Mix",
526
+ unit: "",
527
+ min: 0,
528
+ max: 1,
529
+ step: 0.01,
530
+ default: 0.5,
531
+ },
532
+ ],
533
+ web: "chorus-lfo",
534
+ },
535
+ {
536
+ id: "phaser",
537
+ label: "Phaser",
538
+ group: "time",
539
+ description: "Sweeping notches moving through the spectrum.",
540
+ params: [
541
+ {
542
+ kind: "number",
543
+ key: "in_gain",
544
+ automatable: true,
545
+ label: "Input",
546
+ unit: "",
547
+ min: 0,
548
+ max: 1,
549
+ step: 0.01,
550
+ default: 0.4,
551
+ },
552
+ {
553
+ kind: "number",
554
+ key: "out_gain",
555
+ automatable: true,
556
+ label: "Output",
557
+ unit: "",
558
+ min: 0,
559
+ max: 2,
560
+ step: 0.01,
561
+ default: 0.74,
562
+ },
563
+ // aphaser advertises a delay range starting at 0 but rejects it at
564
+ // runtime with "delay is too small"; 0.1 ms is the real floor.
565
+ {
566
+ kind: "number",
567
+ key: "delay",
568
+ label: "Delay",
569
+ unit: "ms",
570
+ min: 0.1,
571
+ max: 5,
572
+ step: 0.1,
573
+ default: 3,
574
+ },
575
+ {
576
+ kind: "number",
577
+ key: "decay",
578
+ label: "Decay",
579
+ unit: "",
580
+ min: 0,
581
+ max: 0.99,
582
+ step: 0.01,
583
+ default: 0.4,
584
+ },
585
+ {
586
+ kind: "number",
587
+ key: "speed",
588
+ automatable: true,
589
+ label: "Rate",
590
+ unit: "Hz",
591
+ min: 0.1,
592
+ max: 2,
593
+ step: 0.01,
594
+ default: 0.5,
595
+ },
596
+ {
597
+ kind: "enum",
598
+ key: "type",
599
+ label: "Waveform",
600
+ options: [
601
+ { value: "0", label: "Triangular" },
602
+ { value: "1", label: "Sinusoidal" },
603
+ ],
604
+ default: "0",
605
+ },
606
+ ],
607
+ web: "allpass-phaser",
608
+ },
609
+ {
610
+ id: "reverb",
611
+ label: "Reverb",
612
+ group: "time",
613
+ description: "Room tail. Both ends convolve the same generated impulse, so preview matches render.",
614
+ params: [
615
+ {
616
+ kind: "number",
617
+ key: "size",
618
+ label: "Room size",
619
+ unit: "",
620
+ min: 0.05,
621
+ max: 1,
622
+ step: 0.01,
623
+ default: 0.7,
624
+ },
625
+ {
626
+ kind: "number",
627
+ key: "damping",
628
+ label: "Damping",
629
+ unit: "",
630
+ min: 0,
631
+ max: 1,
632
+ step: 0.01,
633
+ default: 0.5,
634
+ hint: "Higher rolls the top off the tail faster.",
635
+ },
636
+ {
637
+ kind: "number",
638
+ key: "wet",
639
+ automatable: true,
640
+ label: "Wet",
641
+ unit: "",
642
+ min: 0,
643
+ max: 1,
644
+ step: 0.01,
645
+ default: 0.35,
646
+ },
647
+ {
648
+ kind: "number",
649
+ key: "dry",
650
+ automatable: true,
651
+ label: "Dry",
652
+ unit: "",
653
+ min: 0,
654
+ max: 1,
655
+ step: 0.01,
656
+ default: 0.7,
657
+ },
658
+ ],
659
+ web: "convolver",
660
+ },
661
+ ];
662
+ const BY_ID = new Map(HF_AUDIO_FX.map((d) => [d.id, d]));
663
+ export function getAudioFxDef(id) {
664
+ return BY_ID.get(id);
665
+ }
666
+ export const HF_AUDIO_FX_IDS = HF_AUDIO_FX.map((d) => d.id);
667
+ /** Every parameter at its declared default, ready to seed a freshly added effect. */
668
+ export function defaultAudioFxParams(id) {
669
+ const def = BY_ID.get(id);
670
+ if (!def)
671
+ return {};
672
+ const out = {};
673
+ for (const p of def.params)
674
+ out[p.key] = p.default;
675
+ return out;
676
+ }
677
+ /**
678
+ * Clamp and fill a parameter set so it is always renderable: unknown keys are
679
+ * dropped, missing keys take their default, numbers are clamped into their
680
+ * declared range, and an unrecognised enum value falls back to its default.
681
+ * A non-finite number is treated as missing rather than passed through, since
682
+ * NaN reaching an AudioParam silences the node for the rest of the render.
683
+ */
684
+ export function normalizeAudioFxParams(id, values) {
685
+ const def = BY_ID.get(id);
686
+ if (!def)
687
+ return {};
688
+ const out = {};
689
+ for (const p of def.params) {
690
+ const raw = values?.[p.key];
691
+ if (p.kind === "enum") {
692
+ const ok = p.options.some((o) => o.value === raw);
693
+ out[p.key] = ok ? raw : p.default;
694
+ continue;
695
+ }
696
+ // Only a number, or a string that actually spells one. `Number(null)`,
697
+ // `Number("")`, `Number(false)` and `Number([])` are all 0 and all pass
698
+ // Number.isFinite, so a missing or blanked value used to clamp to 0 rather
699
+ // than fall back to the declared default — and 0 is a legal value for most
700
+ // of these knobs, so nothing downstream could tell. A compressor whose
701
+ // threshold arrived as null sat at 0 dB and never engaged, silently,
702
+ // instead of at its -24 dB default. `numberOrNull` in audioAutomation.ts
703
+ // already guards exactly this.
704
+ const n = typeof raw === "number"
705
+ ? raw
706
+ : typeof raw === "string" && raw.trim() !== ""
707
+ ? Number(raw)
708
+ : Number.NaN;
709
+ out[p.key] = Number.isFinite(n) ? Math.min(p.max, Math.max(p.min, n)) : p.default;
710
+ }
711
+ return out;
712
+ }
713
+ export class AudioFxChainError extends Error {
714
+ constructor(message) {
715
+ super(message);
716
+ this.name = "AudioFxChainError";
717
+ }
718
+ }
719
+ /**
720
+ * Parse a chain file. Unknown effect ids are rejected rather than skipped: a
721
+ * chain that silently loses a node would render differently from the project
722
+ * the author saved, which is worse than refusing to render at all.
723
+ */
724
+ export function parseAudioFxChain(json) {
725
+ let raw;
726
+ try {
727
+ raw = JSON.parse(json);
728
+ }
729
+ catch (err) {
730
+ throw new AudioFxChainError(`Chain file is not valid JSON: ${err.message}`);
731
+ }
732
+ if (typeof raw !== "object" || raw === null) {
733
+ throw new AudioFxChainError("Chain file must be a JSON object.");
734
+ }
735
+ const obj = raw;
736
+ if (obj.version !== HF_AUDIO_FX_CHAIN_VERSION) {
737
+ throw new AudioFxChainError(`Unsupported chain version: ${String(obj.version)}`);
738
+ }
739
+ if (!Array.isArray(obj.nodes)) {
740
+ throw new AudioFxChainError("Chain file is missing a `nodes` array.");
741
+ }
742
+ const nodes = obj.nodes.map((n, i) => {
743
+ if (typeof n !== "object" || n === null) {
744
+ throw new AudioFxChainError(`Node ${i} is not an object.`);
745
+ }
746
+ const node = n;
747
+ if (typeof node.type !== "string" || !BY_ID.has(node.type)) {
748
+ throw new AudioFxChainError(`Node ${i} has unknown effect type: ${String(node.type)}`);
749
+ }
750
+ return {
751
+ type: node.type,
752
+ ...(typeof node.id === "string" && node.id ? { id: node.id } : {}),
753
+ ...(node.fromCarve === true ? { fromCarve: true } : {}),
754
+ // Both survive the round trip or a preset stops being able to find its
755
+ // own nodes after a reload: re-applying would stack a second copy and
756
+ // the rack would lose the grouping it braces them with.
757
+ ...(typeof node.fromPreset === "string" && node.fromPreset
758
+ ? { fromPreset: node.fromPreset }
759
+ : {}),
760
+ ...(typeof node.label === "string" && node.label ? { label: node.label } : {}),
761
+ ...(typeof node.fromEq === "string" && node.fromEq ? { fromEq: node.fromEq } : {}),
762
+ ...(node.fromLeveller === true ? { fromLeveller: true } : {}),
763
+ // Clamped on the way in: the blend is two gains in opposition, and a value
764
+ // outside 0..1 makes the dry leg negative rather than simply loud.
765
+ ...(typeof node.presetAmount === "number" && Number.isFinite(node.presetAmount)
766
+ ? { presetAmount: Math.min(1, Math.max(0, node.presetAmount)) }
767
+ : {}),
768
+ enabled: node.enabled !== false,
769
+ params: normalizeAudioFxParams(node.type, (node.params ?? undefined)),
770
+ };
771
+ });
772
+ return { version: HF_AUDIO_FX_CHAIN_VERSION, nodes };
773
+ }
774
+ /** The nodes that should process audio, in order. */
775
+ export function enabledAudioFxNodes(chain) {
776
+ return chain.nodes.filter((n) => n.enabled !== false);
777
+ }
778
+ /** Serialise a chain for the `data-fx-chain` attribute. */
779
+ export function serializeAudioFxChain(chain) {
780
+ return JSON.stringify({
781
+ version: HF_AUDIO_FX_CHAIN_VERSION,
782
+ nodes: chain.nodes.map((node) => ({
783
+ type: node.type,
784
+ ...(node.id ? { id: node.id } : {}),
785
+ ...(node.fromCarve === true ? { fromCarve: true } : {}),
786
+ ...(node.fromPreset ? { fromPreset: node.fromPreset } : {}),
787
+ ...(node.label ? { label: node.label } : {}),
788
+ ...(node.fromEq ? { fromEq: node.fromEq } : {}),
789
+ ...(node.fromLeveller === true ? { fromLeveller: true } : {}),
790
+ // Omitted when fully applied, so an untouched preset does not grow a field
791
+ // in every chain that carries one.
792
+ ...(typeof node.presetAmount === "number" && node.presetAmount !== 1
793
+ ? { presetAmount: node.presetAmount }
794
+ : {}),
795
+ ...(node.enabled === false ? { enabled: false } : {}),
796
+ params: normalizeAudioFxParams(node.type, node.params),
797
+ })),
798
+ });
799
+ }
800
+ /**
801
+ * Next free node id for a chain, as `n1`, `n2`, … — counted rather than random
802
+ * so that adding an effect produces the same document on every machine, which
803
+ * compositions require.
804
+ */
805
+ export function mintAudioFxNodeId(chain) {
806
+ const taken = new Set(chain.nodes.map((n) => n.id).filter(Boolean));
807
+ for (let i = 1;; i += 1) {
808
+ const id = `n${i}`;
809
+ if (!taken.has(id))
810
+ return id;
811
+ }
812
+ }
813
+ //# sourceMappingURL=audioFx.js.map