@jpdutoit/squelchy 0.0.5 → 0.0.6

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 (68) hide show
  1. package/README.md +2 -0
  2. package/{chunk-N7J25N5J.js → chunk-PJDNZ52Q.js} +21 -5
  3. package/chunk-PJDNZ52Q.js.map +7 -0
  4. package/docs/GUIDE.md +27 -4
  5. package/docs/MODULES.md +46 -36
  6. package/docs/modules.json +113 -31
  7. package/index.d.ts +75 -25
  8. package/index.js +526 -350
  9. package/index.js.map +3 -3
  10. package/kits/compu-78/bd.flac +0 -0
  11. package/kits/compu-78/ch.flac +0 -0
  12. package/kits/compu-78/clap.flac +0 -0
  13. package/kits/compu-78/cym.flac +0 -0
  14. package/kits/compu-78/ht.flac +0 -0
  15. package/kits/compu-78/lt.flac +0 -0
  16. package/kits/compu-78/mt.flac +0 -0
  17. package/kits/compu-78/oh.flac +0 -0
  18. package/kits/compu-78/perc.flac +0 -0
  19. package/kits/compu-78/rim.flac +0 -0
  20. package/kits/compu-78/sd.flac +0 -0
  21. package/kits/deep-analogue/bd.flac +0 -0
  22. package/kits/deep-analogue/ch.flac +0 -0
  23. package/kits/deep-analogue/clap.flac +0 -0
  24. package/kits/deep-analogue/cym.flac +0 -0
  25. package/kits/deep-analogue/ht.flac +0 -0
  26. package/kits/deep-analogue/lt.flac +0 -0
  27. package/kits/deep-analogue/mt.flac +0 -0
  28. package/kits/deep-analogue/oh.flac +0 -0
  29. package/kits/deep-analogue/perc.flac +0 -0
  30. package/kits/deep-analogue/rim.flac +0 -0
  31. package/kits/deep-analogue/sd.flac +0 -0
  32. package/kits/hard-cell/bd.flac +0 -0
  33. package/kits/hard-cell/ch.flac +0 -0
  34. package/kits/hard-cell/clap.flac +0 -0
  35. package/kits/hard-cell/cym.flac +0 -0
  36. package/kits/hard-cell/ht.flac +0 -0
  37. package/kits/hard-cell/lt.flac +0 -0
  38. package/kits/hard-cell/mt.flac +0 -0
  39. package/kits/hard-cell/oh.flac +0 -0
  40. package/kits/hard-cell/perc.flac +0 -0
  41. package/kits/hard-cell/rim.flac +0 -0
  42. package/kits/hard-cell/sd.flac +0 -0
  43. package/kits/micro-click/bd.flac +0 -0
  44. package/kits/micro-click/ch.flac +0 -0
  45. package/kits/micro-click/clap.flac +0 -0
  46. package/kits/micro-click/cym.flac +0 -0
  47. package/kits/micro-click/ht.flac +0 -0
  48. package/kits/micro-click/lt.flac +0 -0
  49. package/kits/micro-click/mt.flac +0 -0
  50. package/kits/micro-click/oh.flac +0 -0
  51. package/kits/micro-click/perc.flac +0 -0
  52. package/kits/micro-click/rim.flac +0 -0
  53. package/kits/micro-click/sd.flac +0 -0
  54. package/kits/tape-dust/bd.flac +0 -0
  55. package/kits/tape-dust/ch.flac +0 -0
  56. package/kits/tape-dust/clap.flac +0 -0
  57. package/kits/tape-dust/cym.flac +0 -0
  58. package/kits/tape-dust/ht.flac +0 -0
  59. package/kits/tape-dust/lt.flac +0 -0
  60. package/kits/tape-dust/mt.flac +0 -0
  61. package/kits/tape-dust/oh.flac +0 -0
  62. package/kits/tape-dust/perc.flac +0 -0
  63. package/kits/tape-dust/rim.flac +0 -0
  64. package/kits/tape-dust/sd.flac +0 -0
  65. package/package.json +1 -1
  66. package/{worklets-inline-EF3OTQ4D.js → worklets-inline-NWG5PWND.js} +10 -2
  67. package/{worklets-inline-EF3OTQ4D.js.map → worklets-inline-NWG5PWND.js.map} +2 -2
  68. package/chunk-N7J25N5J.js.map +0 -7
package/index.js CHANGED
@@ -2,12 +2,14 @@ import {
2
2
  adsr_envelope_default,
3
3
  bitcrusher_default,
4
4
  comb_filter_default,
5
+ compressor_default,
5
6
  env_follower_default,
6
7
  fdn_reverb_default,
7
8
  flanger_default,
8
9
  formant_table_default,
9
10
  freq_divider_default,
10
11
  freq_shifter_default,
12
+ gate_default,
11
13
  glide_default,
12
14
  ladder_filter_default,
13
15
  map_res_default,
@@ -15,12 +17,14 @@ import {
15
17
  map_voct_to_period_default,
16
18
  ping_pong_default,
17
19
  pitch_shifter_default,
20
+ shaper_default,
18
21
  spring_reverb_default,
19
22
  tape_delay_default,
20
23
  tape_stop_default,
21
24
  vca_default,
25
+ vinyl_noise_default,
22
26
  waveshaper_default
23
- } from "./chunk-N7J25N5J.js";
27
+ } from "./chunk-PJDNZ52Q.js";
24
28
 
25
29
  // src/lib-runtime/core.ts
26
30
  var pendingSources = {};
@@ -28,7 +32,7 @@ var registrations = /* @__PURE__ */ new WeakMap();
28
32
  function preload(ctx, opts) {
29
33
  const existing = registrations.get(ctx);
30
34
  if (existing) return existing;
31
- const sources = opts?.allWorklets ? import("./worklets-inline-EF3OTQ4D.js").then((m) => m.WORKLET_SOURCES) : Promise.resolve(pendingSources);
35
+ const sources = opts?.allWorklets ? import("./worklets-inline-NWG5PWND.js").then((m) => m.WORKLET_SOURCES) : Promise.resolve(pendingSources);
32
36
  const promise = sources.then(
33
37
  (map) => Promise.all(Object.entries(map).map(async ([name, src]) => {
34
38
  const url = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
@@ -502,20 +506,27 @@ var NoteCv = {
502
506
  const pitch = ctx.createConstantSource();
503
507
  pitch.offset.value = 60;
504
508
  pitch.start();
509
+ const velocity = ctx.createConstantSource();
510
+ velocity.offset.value = 0;
511
+ velocity.start();
505
512
  return {
506
513
  gate,
507
514
  pitch,
515
+ velocity,
508
516
  trigger(time, h) {
509
517
  if (typeof h?.value === "number") pitch.offset.setValueAtTime(h.value, time);
510
- gate.offset.setValueAtTime(0, Math.max(0, time - 1 / ctx.sampleRate));
511
- gate.offset.setValueAtTime(vel(h), time);
518
+ velocity.offset.setValueAtTime(vel(h), time);
519
+ const edge = Math.max(0, time - 1 / ctx.sampleRate);
520
+ gate.offset.cancelScheduledValues(edge);
521
+ gate.offset.setValueAtTime(0, edge);
522
+ gate.offset.setValueAtTime(1, time);
512
523
  gate.offset.setValueAtTime(0, time + (h?.duration ?? 0.1));
513
524
  },
514
525
  dispose() {
515
- gate.stop();
516
- pitch.stop();
517
- gate.disconnect();
518
- pitch.disconnect();
526
+ for (const n of [gate, pitch, velocity]) {
527
+ n.stop();
528
+ n.disconnect();
529
+ }
519
530
  }
520
531
  };
521
532
  }
@@ -698,14 +709,33 @@ function createFormantNode(ctx) {
698
709
  return new AudioWorkletNode(ctx, "formant-table", {
699
710
  numberOfInputs: 1,
700
711
  numberOfOutputs: 1,
701
- outputChannelCount: [5]
712
+ outputChannelCount: [11]
713
+ });
714
+ }
715
+ function createShaperNode(ctx, curve, opts = {}) {
716
+ return new AudioWorkletNode(ctx, "shaper", {
717
+ numberOfInputs: 2,
718
+ numberOfOutputs: 2,
719
+ channelCount: 2,
720
+ channelCountMode: "clamped-max",
721
+ outputChannelCount: [2, 2],
722
+ processorOptions: { curve, dry: opts.dry !== false }
723
+ });
724
+ }
725
+ function createVinylNoiseNode(ctx, seed) {
726
+ return new AudioWorkletNode(ctx, "vinyl-noise", {
727
+ numberOfInputs: 0,
728
+ numberOfOutputs: 1,
729
+ outputChannelCount: [1],
730
+ processorOptions: { seed }
702
731
  });
703
732
  }
704
733
  function createBitcrusherNode(ctx) {
705
734
  return new AudioWorkletNode(ctx, "bitcrusher", {
706
735
  numberOfInputs: 1,
707
736
  numberOfOutputs: 1,
708
- outputChannelCount: [1]
737
+ channelCount: 2,
738
+ channelCountMode: "clamped-max"
709
739
  });
710
740
  }
711
741
  function createSubOctaveNode(ctx) {
@@ -722,6 +752,27 @@ function createEnvFollowerNode(ctx) {
722
752
  outputChannelCount: [1]
723
753
  });
724
754
  }
755
+ function createGateNode(ctx) {
756
+ return new AudioWorkletNode(ctx, "gate", {
757
+ numberOfInputs: 2,
758
+ numberOfOutputs: 1,
759
+ outputChannelCount: [2],
760
+ channelCount: 2,
761
+ channelCountMode: "clamped-max",
762
+ channelInterpretation: "speakers"
763
+ });
764
+ }
765
+ function createCompressorNode(ctx, init) {
766
+ return new AudioWorkletNode(ctx, "compressor", {
767
+ numberOfInputs: 2,
768
+ numberOfOutputs: 1,
769
+ outputChannelCount: [2],
770
+ channelCount: 2,
771
+ channelCountMode: "clamped-max",
772
+ channelInterpretation: "speakers",
773
+ parameterData: init
774
+ });
775
+ }
725
776
  function createWaveshaperNode(ctx) {
726
777
  return new AudioWorkletNode(ctx, "waveshaper", {
727
778
  numberOfInputs: 1,
@@ -927,7 +978,7 @@ var filter303Descriptor = /* @__PURE__ */ defineDescriptor({
927
978
  const envDepth = ctx.createGain();
928
979
  envDepth.gain.value = module.envMod * (1 + module.accent) * 36;
929
980
  const accentEnvGain = ctx.createGain();
930
- accentEnvGain.gain.value = module.accent * 0.5;
981
+ accentEnvGain.gain.value = module.accent * 1;
931
982
  input.connect(filter1);
932
983
  filter1.connect(filter2);
933
984
  filter2.connect(accentVCA);
@@ -957,7 +1008,7 @@ var filter303Descriptor = /* @__PURE__ */ defineDescriptor({
957
1008
  case "accent": {
958
1009
  const acc = value;
959
1010
  nodes.envDepth.gain.value = module.envMod * (1 + acc) * 36;
960
- nodes.accentEnvGain.gain.value = acc * 0.5;
1011
+ nodes.accentEnvGain.gain.value = acc * 1;
961
1012
  break;
962
1013
  }
963
1014
  case "decay": {
@@ -983,8 +1034,9 @@ var AcidFilter = /* @__PURE__ */ defineModule(filter303Descriptor, {
983
1034
 
984
1035
  // src/lib-runtime/generated/modules/auto-pan.tsx
985
1036
  var shapes = ["sine", "triangle", "square"];
1037
+ var SHAPE_VALUE = { square: -1, triangle: 0, sine: 1 };
986
1038
  var params2 = {
987
- rate: { node: "lfo", field: "frequency", unit: "Hz", default: 2, min: 0.1, max: 10 },
1039
+ rate: { node: "_freqSrc", field: "offset", unit: "Hz", default: 2, min: 0.1, max: 10 },
988
1040
  depth: { node: "lfoGain", field: "gain", unit: "factor", default: 0.8, min: 0, max: 1 },
989
1041
  shape: { kind: "enum", options: shapes, default: "sine" }
990
1042
  };
@@ -994,7 +1046,7 @@ var autoPanDescriptor = /* @__PURE__ */ defineDescriptor({
994
1046
  ports: {
995
1047
  "in": { type: "input", signal: "audio", node: "input" },
996
1048
  "out": { type: "output", signal: "audio", node: "output" },
997
- "modRate": { type: "modulation", signal: "modulation", node: "lfo", field: "frequency" },
1049
+ "modRate": { type: "modulation", signal: "modulation", node: "_freqSrc", field: "offset" },
998
1050
  "modDepth": { type: "modulation", signal: "modulation", node: "lfoGain", field: "gain" }
999
1051
  },
1000
1052
  createModule(id, position) {
@@ -1004,34 +1056,41 @@ var autoPanDescriptor = /* @__PURE__ */ defineDescriptor({
1004
1056
  const input = ctx.createGain();
1005
1057
  const panner = ctx.createStereoPanner();
1006
1058
  const output = ctx.createGain();
1007
- const lfo = ctx.createOscillator();
1059
+ const _ws = createWaveshaperNode(ctx);
1060
+ const _freqSrc = ctx.createConstantSource();
1061
+ _freqSrc.offset.value = module.rate;
1062
+ _freqSrc.start();
1063
+ _freqSrc.connect(_ws);
1064
+ _ws.parameters.get("skew").value = 0;
1065
+ _ws.parameters.get("shape").value = SHAPE_VALUE[module.shape];
1008
1066
  const lfoGain = ctx.createGain();
1009
- lfo.type = module.shape;
1010
- lfo.frequency.value = module.rate;
1011
1067
  lfoGain.gain.value = module.depth;
1012
- lfo.connect(lfoGain);
1068
+ _ws.connect(lfoGain);
1013
1069
  lfoGain.connect(panner.pan);
1014
- lfo.start();
1015
1070
  input.connect(panner);
1016
1071
  panner.connect(output);
1017
- return { input, output, lfo, lfoGain };
1072
+ return { input, output, _ws, _freqSrc, lfoGain };
1018
1073
  },
1019
1074
  updateParam(nodes, _m, param, value) {
1020
1075
  if (param === "rate") {
1021
- nodes.lfo.frequency.value = value;
1076
+ nodes._freqSrc.offset.value = value;
1022
1077
  } else if (param === "depth") {
1023
1078
  nodes.lfoGain.gain.value = value;
1024
1079
  } else if (param === "shape") {
1025
- nodes.lfo.type = value;
1080
+ nodes._ws.parameters.get("shape").value = SHAPE_VALUE[value];
1026
1081
  }
1027
1082
  },
1028
1083
  dispose(nodes) {
1029
- nodes.lfo.stop();
1084
+ nodes._freqSrc.stop();
1030
1085
  }
1031
1086
  });
1032
1087
 
1033
1088
  // src/lib-runtime/generated/modules/auto-pan.wrapper.ts
1034
- var AutoPan = /* @__PURE__ */ defineModule(autoPanDescriptor);
1089
+ var AutoPan = /* @__PURE__ */ defineModule(autoPanDescriptor, {
1090
+ worklets: {
1091
+ "waveshaper": waveshaper_default
1092
+ }
1093
+ });
1035
1094
 
1036
1095
  // src/lib/dsp/mix.ts
1037
1096
  var CURVE_POINTS = 2049;
@@ -1083,7 +1142,10 @@ var bitcrusherDescriptor = /* @__PURE__ */ defineDescriptor({
1083
1142
  params: params3,
1084
1143
  ports: {
1085
1144
  "in": { type: "input", signal: "audio", node: "input" },
1086
- "out": { type: "output", signal: "audio", node: "output" }
1145
+ "out": { type: "output", signal: "audio", node: "output" },
1146
+ "modBits": { type: "modulation", signal: "modulation", node: "crusher", field: "bits" },
1147
+ "modRate": { type: "modulation", signal: "modulation", node: "crusher", field: "rate" },
1148
+ "modMix": { type: "modulation", signal: "modulation", node: "_mix", field: "offset" }
1087
1149
  },
1088
1150
  createModule(id, position) {
1089
1151
  return { id, type: "bitcrusher", position, ...paramDefaults(params3) };
@@ -1124,6 +1186,8 @@ var Bitcrusher = /* @__PURE__ */ defineModule(bitcrusherDescriptor, {
1124
1186
  });
1125
1187
 
1126
1188
  // src/lib-runtime/generated/modules/chorus.tsx
1189
+ var CENTRE_S = 566e-5;
1190
+ var SWING_S = 4e-3;
1127
1191
  var params4 = {
1128
1192
  rate: { node: "lfo", field: "frequency", unit: "Hz", default: 1.5, min: 0.1, max: 5 },
1129
1193
  depth: { unit: "factor", default: 0.5, min: 0, max: 1 },
@@ -1156,8 +1220,8 @@ var chorusDescriptor = /* @__PURE__ */ defineDescriptor({
1156
1220
  const lfoGain = ctx.createGain();
1157
1221
  const lfoInv = ctx.createGain();
1158
1222
  const merger = ctx.createChannelMerger(2);
1159
- delayL.delayTime.value = 0.02;
1160
- delayR.delayTime.value = 0.02;
1223
+ delayL.delayTime.value = CENTRE_S;
1224
+ delayR.delayTime.value = CENTRE_S;
1161
1225
  const _mix = ctx.createConstantSource();
1162
1226
  _mix.offset.value = module.mix;
1163
1227
  dryGain.gain.value = 1;
@@ -1169,7 +1233,7 @@ var chorusDescriptor = /* @__PURE__ */ defineDescriptor({
1169
1233
  _mix.start();
1170
1234
  lfo.type = "sine";
1171
1235
  lfo.frequency.value = module.rate;
1172
- lfoGain.gain.value = module.depth * 0.01;
1236
+ lfoGain.gain.value = module.depth * SWING_S;
1173
1237
  lfoInv.gain.value = -1;
1174
1238
  lfo.connect(lfoGain);
1175
1239
  lfoGain.connect(delayL.delayTime);
@@ -1193,7 +1257,7 @@ var chorusDescriptor = /* @__PURE__ */ defineDescriptor({
1193
1257
  nodes.lfo.frequency.value = value;
1194
1258
  break;
1195
1259
  case "depth":
1196
- nodes.lfoGain.gain.value = value * 0.01;
1260
+ nodes.lfoGain.gain.value = value * SWING_S;
1197
1261
  break;
1198
1262
  case "mix":
1199
1263
  nodes._mix.offset.value = value;
@@ -1306,62 +1370,59 @@ var CombFilter = /* @__PURE__ */ defineModule(combFilterDescriptor, {
1306
1370
  var params6 = {
1307
1371
  threshold: { node: "comp", field: "threshold", unit: "dB", default: -24, min: -60, max: 0 },
1308
1372
  ratio: { node: "comp", field: "ratio", unit: "factor", default: 4, min: 1, max: 20, scale: "log" },
1309
- attack: { node: "comp", field: "attack", unit: "s", default: 3e-3, min: 1e-3, max: 1, scale: "log" },
1373
+ attack: { node: "comp", field: "attack", unit: "s", default: 3e-3, min: 1e-4, max: 1, scale: "log" },
1310
1374
  release: { node: "comp", field: "release", unit: "s", default: 0.25, min: 0.01, max: 1, scale: "log" },
1311
1375
  knee: { node: "comp", field: "knee", unit: "dB", default: 10, min: 0, max: 40 },
1312
- // makeup is stored in dB but drives a linear gain node (Math.pow(10, v/20)) —
1313
- // not a 1:1 AudioParam mapping, so no node/field.
1314
- makeup: { unit: "dB", default: 0, min: 0, max: 30 }
1376
+ makeup: { node: "comp", field: "makeup", unit: "dB", default: 0, min: 0, max: 30 },
1377
+ // A switch, not jack sensing: the browser reports an input as empty
1378
+ // whenever its source stops (e.g. between drum samples). See compressor.ts.
1379
+ keySource: { kind: "enum", options: ["internal", "external"], default: "internal" }
1315
1380
  };
1316
1381
  var compressorDescriptor = /* @__PURE__ */ defineDescriptor({
1317
1382
  type: "compressor",
1318
1383
  params: params6,
1319
1384
  ports: {
1320
1385
  "in": { type: "input", signal: "audio", node: "comp" },
1321
- "out": { type: "output", signal: "audio", node: "makeup" }
1386
+ "key": { type: "input", signal: "audio", node: "_keyIn" },
1387
+ "out": { type: "output", signal: "audio", node: "comp" }
1322
1388
  },
1323
1389
  createModule(id, position) {
1324
- return { id, type: "compressor", position, ...paramDefaults(params6) };
1390
+ return { id, type: "compressor", position, timeUnit: "tau", ...paramDefaults(params6) };
1325
1391
  },
1392
+ // One worklet does detection, gain computing, smoothing and makeup — every
1393
+ // knob is one of its AudioParams, 1:1. See src/worklets/compressor.ts.
1326
1394
  createNodes(ctx, module) {
1327
- const comp = ctx.createDynamicsCompressor();
1328
- comp.threshold.value = module.threshold;
1329
- comp.ratio.value = module.ratio;
1330
- comp.attack.value = module.attack;
1331
- comp.release.value = module.release;
1332
- comp.knee.value = module.knee;
1333
- const makeup = ctx.createGain();
1334
- makeup.gain.value = Math.pow(10, (module.makeup || 0) / 20);
1335
- comp.connect(makeup);
1336
- return { comp, makeup };
1395
+ const comp = createCompressorNode(ctx, {
1396
+ threshold: module.threshold,
1397
+ ratio: module.ratio,
1398
+ attack: module.attack,
1399
+ release: module.release,
1400
+ knee: module.knee,
1401
+ makeup: module.makeup ?? 0,
1402
+ keyExt: module.keySource === "external" ? 1 : 0
1403
+ });
1404
+ const _keyIn = ctx.createGain();
1405
+ _keyIn.channelCount = 2;
1406
+ _keyIn.channelCountMode = "clamped-max";
1407
+ _keyIn.connect(comp, 0, 1);
1408
+ return { comp, _keyIn };
1337
1409
  },
1338
1410
  updateParam(nodes, _m, param, value) {
1339
- const v = value;
1340
- switch (param) {
1341
- case "threshold":
1342
- nodes.comp.threshold.value = v;
1343
- break;
1344
- case "ratio":
1345
- nodes.comp.ratio.value = v;
1346
- break;
1347
- case "attack":
1348
- nodes.comp.attack.value = v;
1349
- break;
1350
- case "release":
1351
- nodes.comp.release.value = v;
1352
- break;
1353
- case "knee":
1354
- nodes.comp.knee.value = v;
1355
- break;
1356
- case "makeup":
1357
- nodes.makeup.gain.value = Math.pow(10, v / 20);
1358
- break;
1411
+ if (param === "keySource") {
1412
+ nodes.comp.parameters.get("keyExt").value = value === "external" ? 1 : 0;
1413
+ return;
1359
1414
  }
1415
+ const p = nodes.comp.parameters.get(param);
1416
+ if (p) p.value = value;
1360
1417
  }
1361
1418
  });
1362
1419
 
1363
1420
  // src/lib-runtime/generated/modules/comp.wrapper.ts
1364
- var Compressor = /* @__PURE__ */ defineModule(compressorDescriptor);
1421
+ var Compressor = /* @__PURE__ */ defineModule(compressorDescriptor, {
1422
+ worklets: {
1423
+ "compressor": compressor_default
1424
+ }
1425
+ });
1365
1426
 
1366
1427
  // src/lib-runtime/generated/modules/crossfade.tsx
1367
1428
  var params7 = {
@@ -1488,7 +1549,19 @@ var Delay = /* @__PURE__ */ defineModule(delayDescriptor, {
1488
1549
  });
1489
1550
 
1490
1551
  // src/lib-runtime/generated/modules/dimension.tsx
1552
+ var CENTER_S = 35e-4;
1553
+ var SWING_S2 = 185e-5;
1554
+ var WET = 0.5;
1555
+ var CROSS_HPF_HZ = 200;
1556
+ var BUTTERWORTH_Q_DB = -3.0102999566;
1557
+ function setWidth(n, w) {
1558
+ n.llGain.gain.value = (1 + w) / 2;
1559
+ n.rrGain.gain.value = (1 + w) / 2;
1560
+ n.rlGain.gain.value = (1 - w) / 2;
1561
+ n.lrGain.gain.value = (1 - w) / 2;
1562
+ }
1491
1563
  var params9 = {
1564
+ width: { unit: "factor", default: 1.5, min: 0, max: 2 },
1492
1565
  rate: { node: "lfo", field: "frequency", unit: "Hz", default: 0.5, min: 0.1, max: 5 },
1493
1566
  depth: { unit: "factor", default: 0.3, min: 0, max: 1 }
1494
1567
  };
@@ -1503,6 +1576,24 @@ var dimensionDescriptor = /* @__PURE__ */ defineDescriptor({
1503
1576
  createModule(id, position) {
1504
1577
  return { id, type: "dimension", position, ...paramDefaults(params9) };
1505
1578
  },
1579
+ // Roland SDD-320 topology (Haible's service-schematic description, HSR 1984
1580
+ // review, pendragon-andyh's Juno-60 BBD analysis — see
1581
+ // reports/expectations/discussion/dimension.md): the dry signal anchors
1582
+ // both outputs; each BBD line is mixed in phase into its own channel and
1583
+ // inverted into the other, with the low end kept out of the cancellation.
1584
+ // Two short BBD-range lines (1.65–5.35 ms at full DEPTH) swept by ONE
1585
+ // triangle LFO in antiphase. Written out, that matrix is
1586
+ // A = dry_L + WET·HP(bbdA − bbdB), B = dry_R − WET·HP(bbdA − bbdB)
1587
+ // i.e. the moving DIFFERENCE of the two lines becomes side signal. Nothing
1588
+ // reaches the mid, so there is no vibrato and the mono sum is exactly the
1589
+ // dry input (the hardware's famed mono compatibility); the bass stays dry
1590
+ // and centred. HP is a Linkwitz-Riley 4th-order low-cut at 200 Hz; since
1591
+ // the side enters A and B in opposite polarity, the mono sum cancels it
1592
+ // whatever its phase, so a steep filter costs no mono compatibility.
1593
+ // Previously the module was 100% wet — two 10/12 ms modulated delays, i.e.
1594
+ // a stereo vibrato with 10 ms of latency and nothing anchoring pitch.
1595
+ // WIDTH then scales the side of (A, B): L = M + w·S, R = M − w·S, so
1596
+ // 0 = mono, 1 = the hardware matrix, 2 = exaggerated.
1506
1597
  createNodes(ctx, module) {
1507
1598
  const input = ctx.createGain();
1508
1599
  input.channelCount = 2;
@@ -1516,51 +1607,62 @@ var dimensionDescriptor = /* @__PURE__ */ defineDescriptor({
1516
1607
  const lfo = ctx.createOscillator();
1517
1608
  const lfoGainL = ctx.createGain();
1518
1609
  const lfoGainR = ctx.createGain();
1610
+ const invB = ctx.createGain();
1611
+ invB.gain.value = -1;
1612
+ const diff = ctx.createGain();
1613
+ const hp1 = ctx.createBiquadFilter();
1614
+ hp1.type = "highpass";
1615
+ hp1.frequency.value = CROSS_HPF_HZ;
1616
+ hp1.Q.value = BUTTERWORTH_Q_DB;
1617
+ const hp2 = ctx.createBiquadFilter();
1618
+ hp2.type = "highpass";
1619
+ hp2.frequency.value = CROSS_HPF_HZ;
1620
+ hp2.Q.value = BUTTERWORTH_Q_DB;
1621
+ const side = ctx.createGain();
1622
+ side.gain.value = WET;
1623
+ const sideInv = ctx.createGain();
1624
+ sideInv.gain.value = -1;
1625
+ const aSum = ctx.createGain();
1626
+ const bSum = ctx.createGain();
1519
1627
  const llGain = ctx.createGain();
1520
1628
  const lrGain = ctx.createGain();
1521
1629
  const rlGain = ctx.createGain();
1522
1630
  const rrGain = ctx.createGain();
1523
- const w = module.width;
1524
- llGain.gain.value = (1 + w) / 2;
1525
- rrGain.gain.value = (1 + w) / 2;
1526
- rlGain.gain.value = (1 - w) / 2;
1527
- lrGain.gain.value = (1 - w) / 2;
1528
- leftDelay.delayTime.value = 0.01;
1529
- rightDelay.delayTime.value = 0.012;
1530
- lfo.type = "sine";
1631
+ setWidth({ llGain, lrGain, rlGain, rrGain }, module.width);
1632
+ leftDelay.delayTime.value = CENTER_S;
1633
+ rightDelay.delayTime.value = CENTER_S;
1634
+ lfo.type = "triangle";
1531
1635
  lfo.frequency.value = module.rate;
1532
- lfoGainL.gain.value = module.depth * 2e-3;
1533
- lfoGainR.gain.value = -module.depth * 2e-3;
1636
+ lfoGainL.gain.value = module.depth * SWING_S2;
1637
+ lfoGainR.gain.value = -module.depth * SWING_S2;
1534
1638
  lfo.connect(lfoGainL);
1535
1639
  lfo.connect(lfoGainR);
1536
1640
  lfoGainL.connect(leftDelay.delayTime);
1537
1641
  lfoGainR.connect(rightDelay.delayTime);
1538
1642
  lfo.start();
1539
1643
  input.connect(splitter);
1644
+ splitter.connect(aSum, 0);
1645
+ splitter.connect(bSum, 1);
1540
1646
  splitter.connect(leftDelay, 0);
1541
1647
  splitter.connect(rightDelay, 1);
1542
- leftDelay.connect(llGain);
1543
- leftDelay.connect(lrGain);
1544
- rightDelay.connect(rrGain);
1545
- rightDelay.connect(rlGain);
1546
- llGain.connect(merger, 0, 0);
1547
- rlGain.connect(merger, 0, 0);
1548
- lrGain.connect(merger, 0, 1);
1549
- rrGain.connect(merger, 0, 1);
1648
+ leftDelay.connect(diff);
1649
+ rightDelay.connect(invB).connect(diff);
1650
+ diff.connect(hp1).connect(hp2).connect(side);
1651
+ side.connect(aSum);
1652
+ side.connect(sideInv).connect(bSum);
1653
+ aSum.connect(llGain).connect(merger, 0, 0);
1654
+ aSum.connect(lrGain).connect(merger, 0, 1);
1655
+ bSum.connect(rlGain).connect(merger, 0, 0);
1656
+ bSum.connect(rrGain).connect(merger, 0, 1);
1550
1657
  merger.connect(output);
1551
- return { input, output, splitter, merger, leftDelay, rightDelay, lfo, lfoGainL, lfoGainR, llGain, rrGain, lrGain, rlGain };
1658
+ return { input, output, splitter, merger, leftDelay, rightDelay, lfo, lfoGainL, lfoGainR, invB, diff, hp1, hp2, side, sideInv, aSum, bSum, llGain, rrGain, lrGain, rlGain };
1552
1659
  },
1553
1660
  updateParam(nodes, _m, param, value) {
1554
- if (param === "width") {
1555
- const w = value;
1556
- nodes.llGain.gain.value = (1 + w) / 2;
1557
- nodes.rrGain.gain.value = (1 + w) / 2;
1558
- nodes.rlGain.gain.value = (1 - w) / 2;
1559
- nodes.lrGain.gain.value = (1 - w) / 2;
1560
- } else if (param === "rate") nodes.lfo.frequency.value = value;
1661
+ if (param === "width") setWidth(nodes, value);
1662
+ else if (param === "rate") nodes.lfo.frequency.value = value;
1561
1663
  else if (param === "depth") {
1562
- nodes.lfoGainL.gain.value = value * 2e-3;
1563
- nodes.lfoGainR.gain.value = -value * 2e-3;
1664
+ nodes.lfoGainL.gain.value = value * SWING_S2;
1665
+ nodes.lfoGainR.gain.value = -value * SWING_S2;
1564
1666
  }
1565
1667
  },
1566
1668
  dispose(nodes) {
@@ -1632,12 +1734,10 @@ var distortionDescriptor = /* @__PURE__ */ defineDescriptor({
1632
1734
  const output = ctx.createGain();
1633
1735
  const dryGain = ctx.createGain();
1634
1736
  const wetGain = ctx.createGain();
1635
- const waveshaper = ctx.createWaveShaper();
1737
+ const waveshaper = createShaperNode(ctx, makeDistortionCurve(module.distortionType, module.drive));
1636
1738
  const makeup = ctx.createGain();
1637
1739
  const dcBlock = ctx.createBiquadFilter();
1638
1740
  const toneFilter = ctx.createBiquadFilter();
1639
- waveshaper.curve = makeDistortionCurve(module.distortionType, module.drive);
1640
- waveshaper.oversample = "4x";
1641
1741
  makeup.gain.value = computeMakeup(module.distortionType, module.drive);
1642
1742
  dcBlock.type = "highpass";
1643
1743
  dcBlock.frequency.value = 10;
@@ -1646,9 +1746,9 @@ var distortionDescriptor = /* @__PURE__ */ defineDescriptor({
1646
1746
  toneFilter.frequency.value = module.frequency;
1647
1747
  toneFilter.Q.value = 0.707;
1648
1748
  const _mix = createMix(ctx, dryGain, wetGain, module.mix, "linear");
1649
- input.connect(dryGain);
1650
1749
  input.connect(waveshaper);
1651
- waveshaper.connect(makeup);
1750
+ waveshaper.connect(dryGain, 1);
1751
+ waveshaper.connect(makeup, 0);
1652
1752
  makeup.connect(dcBlock);
1653
1753
  dcBlock.connect(toneFilter);
1654
1754
  toneFilter.connect(wetGain);
@@ -1658,7 +1758,7 @@ var distortionDescriptor = /* @__PURE__ */ defineDescriptor({
1658
1758
  },
1659
1759
  updateParam(nodes, module, param, value) {
1660
1760
  if (param === "drive" || param === "distortionType") {
1661
- nodes.waveshaper.curve = makeDistortionCurve(module.distortionType, module.drive);
1761
+ nodes.waveshaper.port.postMessage({ curve: makeDistortionCurve(module.distortionType, module.drive) });
1662
1762
  nodes.makeup.gain.value = computeMakeup(module.distortionType, module.drive);
1663
1763
  }
1664
1764
  if (param === "frequency") {
@@ -1671,7 +1771,11 @@ var distortionDescriptor = /* @__PURE__ */ defineDescriptor({
1671
1771
  });
1672
1772
 
1673
1773
  // src/lib-runtime/generated/modules/distortion.wrapper.ts
1674
- var Distortion = /* @__PURE__ */ defineModule(distortionDescriptor);
1774
+ var Distortion = /* @__PURE__ */ defineModule(distortionDescriptor, {
1775
+ worklets: {
1776
+ "shaper": shaper_default
1777
+ }
1778
+ });
1675
1779
 
1676
1780
  // src/lib-runtime/generated/modules/ducker.tsx
1677
1781
  var params11 = {
@@ -1973,22 +2077,21 @@ var exciterDescriptor = /* @__PURE__ */ defineDescriptor({
1973
2077
  const dryGain = ctx.createGain();
1974
2078
  const wetGain = ctx.createGain();
1975
2079
  const highpass = ctx.createBiquadFilter();
1976
- const saturation = ctx.createWaveShaper();
2080
+ const saturation = createShaperNode(ctx, makeExciterCurve(module.drive));
1977
2081
  const highshelf = ctx.createBiquadFilter();
1978
2082
  highpass.type = "highpass";
1979
2083
  highpass.frequency.value = module.frequency;
1980
2084
  highpass.Q.value = 0.7;
1981
- saturation.curve = makeExciterCurve(module.drive);
1982
- saturation.oversample = "2x";
1983
2085
  highshelf.type = "highshelf";
1984
2086
  highshelf.frequency.value = module.frequency;
1985
2087
  highshelf.gain.value = 6 + module.drive * 6;
1986
2088
  dryGain.gain.value = 1;
1987
2089
  wetGain.gain.value = module.mix;
1988
- input.connect(dryGain);
2090
+ input.connect(saturation, 0, 1);
2091
+ saturation.connect(dryGain, 1);
1989
2092
  input.connect(highpass);
1990
- highpass.connect(saturation);
1991
- saturation.connect(highshelf);
2093
+ highpass.connect(saturation, 0, 0);
2094
+ saturation.connect(highshelf, 0);
1992
2095
  highshelf.connect(wetGain);
1993
2096
  dryGain.connect(output);
1994
2097
  wetGain.connect(output);
@@ -1996,7 +2099,7 @@ var exciterDescriptor = /* @__PURE__ */ defineDescriptor({
1996
2099
  },
1997
2100
  updateParam(nodes, _m, param, value) {
1998
2101
  if (param === "drive") {
1999
- nodes.saturation.curve = makeExciterCurve(value);
2102
+ nodes.saturation.port.postMessage({ curve: makeExciterCurve(value) });
2000
2103
  nodes.highshelf.gain.value = 6 + value * 6;
2001
2104
  } else if (param === "frequency") {
2002
2105
  nodes.highpass.frequency.value = value;
@@ -2008,7 +2111,11 @@ var exciterDescriptor = /* @__PURE__ */ defineDescriptor({
2008
2111
  });
2009
2112
 
2010
2113
  // src/lib-runtime/generated/modules/exciter.wrapper.ts
2011
- var Exciter = /* @__PURE__ */ defineModule(exciterDescriptor);
2114
+ var Exciter = /* @__PURE__ */ defineModule(exciterDescriptor, {
2115
+ worklets: {
2116
+ "shaper": shaper_default
2117
+ }
2118
+ });
2012
2119
 
2013
2120
  // src/lib-runtime/generated/modules/flanger.tsx
2014
2121
  var params16 = {
@@ -2076,7 +2183,7 @@ var Flanger = /* @__PURE__ */ defineModule(flangerDescriptor, {
2076
2183
  // src/lib-runtime/generated/modules/formant.tsx
2077
2184
  var params17 = {
2078
2185
  morph: { unit: "factor", default: 0, min: 0, max: 5 },
2079
- resonance: { unit: "factor", default: 6, min: 1, max: 20, scale: "sqrt" },
2186
+ resonance: { node: "formantWorklet", field: "resonance", unit: "factor", default: 6, min: 1, max: 20, scale: "sqrt" },
2080
2187
  mix: { unit: "factor", default: 1, min: 0, max: 1 }
2081
2188
  };
2082
2189
  var formantDescriptor = /* @__PURE__ */ defineDescriptor({
@@ -2095,75 +2202,54 @@ var formantDescriptor = /* @__PURE__ */ defineDescriptor({
2095
2202
  const output = ctx.createGain();
2096
2203
  const dryGain = ctx.createGain();
2097
2204
  const wetGain = ctx.createGain();
2098
- const f1 = ctx.createBiquadFilter();
2099
- const f2 = ctx.createBiquadFilter();
2100
- const f3 = ctx.createBiquadFilter();
2101
- const f4 = ctx.createBiquadFilter();
2102
- const f5 = ctx.createBiquadFilter();
2103
- const formantMix = ctx.createGain();
2205
+ const bands = [0, 1, 2, 3, 4].map(() => {
2206
+ const f = ctx.createBiquadFilter();
2207
+ f.type = "bandpass";
2208
+ f.frequency.value = 0;
2209
+ f.Q.value = 0;
2210
+ return f;
2211
+ });
2104
2212
  const formantWorklet = createFormantNode(ctx);
2105
- formantWorklet.parameters.get("baseMorph").value = module.morph;
2213
+ const wp = formantWorklet.parameters;
2214
+ wp.get("baseMorph").value = module.morph;
2215
+ wp.get("resonance").value = module.resonance;
2106
2216
  const vowelCV = ctx.createGain();
2107
2217
  vowelCV.gain.value = 1;
2108
2218
  vowelCV.connect(formantWorklet);
2109
- const splitter = ctx.createChannelSplitter(5);
2219
+ const splitter = ctx.createChannelSplitter(11);
2110
2220
  formantWorklet.connect(splitter);
2111
- splitter.connect(f1.frequency, 0);
2112
- splitter.connect(f2.frequency, 1);
2113
- splitter.connect(f3.frequency, 2);
2114
- splitter.connect(f4.frequency, 3);
2115
- splitter.connect(f5.frequency, 4);
2116
- f1.frequency.value = 0;
2117
- f2.frequency.value = 0;
2118
- f3.frequency.value = 0;
2119
- f4.frequency.value = 0;
2120
- f5.frequency.value = 0;
2121
- f1.type = "bandpass";
2122
- f2.type = "bandpass";
2123
- f3.type = "bandpass";
2124
- f4.type = "bandpass";
2125
- f5.type = "bandpass";
2126
- f1.Q.value = module.resonance * 0.5;
2127
- f2.Q.value = module.resonance * 0.4;
2128
- f3.Q.value = module.resonance * 0.3;
2129
- f4.Q.value = module.resonance * 0.25;
2130
- f5.Q.value = module.resonance * 0.2;
2221
+ bands.forEach((f, k) => {
2222
+ splitter.connect(f.frequency, k);
2223
+ splitter.connect(f.Q, 5 + k);
2224
+ });
2225
+ const BAND_GAIN = [1, -1, 1, -0.5, 0.35];
2226
+ const bandGains = bands.map((f, k) => {
2227
+ const g = ctx.createGain();
2228
+ g.gain.value = BAND_GAIN[k];
2229
+ input.connect(f);
2230
+ f.connect(g);
2231
+ return g;
2232
+ });
2233
+ const formantMix = ctx.createGain();
2234
+ formantMix.gain.value = 0;
2235
+ splitter.connect(formantMix.gain, 10);
2236
+ bandGains.forEach((g) => g.connect(formantMix));
2131
2237
  dryGain.gain.value = 1 - module.mix;
2132
2238
  wetGain.gain.value = module.mix;
2133
- formantMix.gain.value = 2;
2134
- const f4Gain = ctx.createGain();
2135
- const f5Gain = ctx.createGain();
2136
- f4Gain.gain.value = 0.5;
2137
- f5Gain.gain.value = 0.35;
2138
2239
  input.connect(dryGain);
2139
- input.connect(f1);
2140
- input.connect(f2);
2141
- input.connect(f3);
2142
- input.connect(f4);
2143
- input.connect(f5);
2144
- f1.connect(formantMix);
2145
- f2.connect(formantMix);
2146
- f3.connect(formantMix);
2147
- f4.connect(f4Gain);
2148
- f5.connect(f5Gain);
2149
- f4Gain.connect(formantMix);
2150
- f5Gain.connect(formantMix);
2151
2240
  formantMix.connect(wetGain);
2152
2241
  dryGain.connect(output);
2153
2242
  wetGain.connect(output);
2243
+ const [f1, f2, f3, f4, f5] = bands;
2154
2244
  const _alive = createKeepAlive(ctx, f1, f2, f3, f4, f5);
2155
- return { input, output, vowelCV, f1, f2, f3, f4, f5, f4Gain, f5Gain, dryGain, wetGain, formantWorklet, _alive };
2245
+ return { input, output, vowelCV, f1, f2, f3, f4, f5, formantMix, dryGain, wetGain, formantWorklet, _alive };
2156
2246
  },
2157
2247
  updateParam(nodes, _module, param, value) {
2248
+ const wp = nodes.formantWorklet.parameters;
2158
2249
  if (param === "morph") {
2159
- nodes.formantWorklet.parameters.get("baseMorph").value = value;
2250
+ wp.get("baseMorph").value = value;
2160
2251
  } else if (param === "resonance") {
2161
- const r = value;
2162
- nodes.f1.Q.value = r * 0.5;
2163
- nodes.f2.Q.value = r * 0.4;
2164
- nodes.f3.Q.value = r * 0.3;
2165
- nodes.f4.Q.value = r * 0.25;
2166
- nodes.f5.Q.value = r * 0.2;
2252
+ wp.get("resonance").value = value;
2167
2253
  } else if (param === "mix") {
2168
2254
  nodes.dryGain.gain.value = 1 - value;
2169
2255
  nodes.wetGain.gain.value = value;
@@ -2230,35 +2316,22 @@ var FrequencyShifter = /* @__PURE__ */ defineModule(freqShifterDescriptor, {
2230
2316
 
2231
2317
  // src/lib-runtime/generated/modules/gate.tsx
2232
2318
  var params19 = {
2233
- // threshold/range rebuild the gateShaper curve — not a 1:1 AudioParam
2234
- // write — so no node/field.
2235
- threshold: { unit: "dB", default: -40, min: -60, max: 0 },
2236
- range: { unit: "factor", default: 0.8, min: 0, max: 1 },
2237
- attack: { node: "envFollower", field: "attack", unit: "s", default: 5e-3, min: 1e-3, max: 0.05, scale: "log" },
2238
- release: { node: "envFollower", field: "release", unit: "s", default: 0.1, min: 0.01, max: 0.5, scale: "log" }
2319
+ threshold: { node: "gate", field: "threshold", unit: "dB", default: -40, min: -60, max: 0 },
2320
+ range: { node: "gate", field: "range", unit: "factor", default: 0.8, min: 0, max: 1 },
2321
+ // ATK/REL shape the VCA gain's own attack/release ramp once the (fixed,
2322
+ // fast) detector has decided open/closed — same separation as a real
2323
+ // hardware gate (e.g. Drawmer DS201). See src/worklets/gate.ts.
2324
+ attack: { node: "gate", field: "attack", unit: "s", default: 5e-3, min: 1e-3, max: 0.05, scale: "log" },
2325
+ release: { node: "gate", field: "release", unit: "s", default: 0.1, min: 0.01, max: 0.5, scale: "log" },
2326
+ // A switch, not jack sensing (see src/worklets/gate.ts).
2327
+ keySource: { kind: "enum", options: ["internal", "external"], default: "internal" }
2239
2328
  };
2240
- function makeGateCurve(threshold, range) {
2241
- const threshLinear = Math.pow(10, threshold / 20);
2242
- const kneeLow = Math.pow(10, (threshold - 2) / 20);
2243
- const closed = 1 - range;
2244
- const gsLen = 32768;
2245
- const gsCurve = new Float32Array(gsLen);
2246
- for (let i = 0; i < gsLen; i++) {
2247
- const x = i / (gsLen - 1) * 2 - 1;
2248
- const env = Math.max(0, x);
2249
- let g;
2250
- if (env >= threshLinear) g = 1;
2251
- else if (env <= kneeLow) g = closed;
2252
- else g = closed + (1 - closed) * ((env - kneeLow) / (threshLinear - kneeLow));
2253
- gsCurve[i] = g;
2254
- }
2255
- return gsCurve;
2256
- }
2257
2329
  var gateDescriptor = /* @__PURE__ */ defineDescriptor({
2258
2330
  type: "gate",
2259
2331
  params: params19,
2260
2332
  ports: {
2261
2333
  "in": { type: "input", signal: "audio", node: "input" },
2334
+ "key": { type: "input", signal: "audio", node: "_keyIn" },
2262
2335
  "out": { type: "output", signal: "audio", node: "output" }
2263
2336
  },
2264
2337
  createModule(id, position) {
@@ -2267,37 +2340,33 @@ var gateDescriptor = /* @__PURE__ */ defineDescriptor({
2267
2340
  createNodes(ctx, module) {
2268
2341
  const input = ctx.createGain();
2269
2342
  const output = ctx.createGain();
2270
- const gateGain = ctx.createGain();
2271
- const envFollower = createEnvFollowerNode(ctx);
2272
- envFollower.parameters.get("attack").value = module.attack;
2273
- envFollower.parameters.get("release").value = module.release;
2274
- const gateShaper = ctx.createWaveShaper();
2275
- gateShaper.curve = makeGateCurve(module.threshold, module.range);
2276
- gateGain.gain.value = 0;
2277
- input.connect(envFollower);
2278
- envFollower.connect(gateShaper);
2279
- gateShaper.connect(gateGain.gain);
2280
- input.connect(gateGain);
2281
- gateGain.connect(output);
2282
- return { input, output, envFollower, gateShaper };
2343
+ const gate = createGateNode(ctx);
2344
+ gate.parameters.get("threshold").value = module.threshold;
2345
+ gate.parameters.get("range").value = module.range;
2346
+ gate.parameters.get("attack").value = module.attack;
2347
+ gate.parameters.get("release").value = module.release;
2348
+ gate.parameters.get("keyExt").value = module.keySource === "external" ? 1 : 0;
2349
+ const _keyIn = ctx.createGain();
2350
+ _keyIn.channelCount = 2;
2351
+ _keyIn.channelCountMode = "clamped-max";
2352
+ input.connect(gate);
2353
+ _keyIn.connect(gate, 0, 1);
2354
+ gate.connect(output);
2355
+ return { input, output, gate, _keyIn };
2283
2356
  },
2284
2357
  updateParam(nodes, module, param, value) {
2285
- if (param === "threshold" || param === "range") {
2286
- const thresh = param === "threshold" ? value : module.threshold;
2287
- const range = param === "range" ? value : module.range;
2288
- nodes.gateShaper.curve = makeGateCurve(thresh, range);
2289
- } else if (param === "attack") {
2290
- nodes.envFollower.parameters.get("attack").value = value;
2291
- } else if (param === "release") {
2292
- nodes.envFollower.parameters.get("release").value = value;
2358
+ if (param === "keySource") {
2359
+ nodes.gate.parameters.get("keyExt").value = value === "external" ? 1 : 0;
2360
+ return;
2293
2361
  }
2362
+ nodes.gate.parameters.get(param).value = value;
2294
2363
  }
2295
2364
  });
2296
2365
 
2297
2366
  // src/lib-runtime/generated/modules/gate.wrapper.ts
2298
2367
  var Gate = /* @__PURE__ */ defineModule(gateDescriptor, {
2299
2368
  worklets: {
2300
- "env-follower": env_follower_default
2369
+ "gate": gate_default
2301
2370
  }
2302
2371
  });
2303
2372
 
@@ -2422,7 +2491,7 @@ var HaasWidener = /* @__PURE__ */ defineModule(haasDescriptor);
2422
2491
  var params22 = {
2423
2492
  rate: { node: "lfo", field: "frequency", unit: "Hz", default: 4, min: 1, max: 10 },
2424
2493
  depth: { unit: "factor", default: 0.7, min: 0, max: 1 },
2425
- crossover: { node: "lowpass", field: "frequency", unit: "Hz", default: 800, min: 200, max: 2e3 }
2494
+ crossover: { node: "_xover", field: "offset", unit: "Hz", default: 800, min: 200, max: 2e3 }
2426
2495
  };
2427
2496
  var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2428
2497
  type: "harmonic-trem",
@@ -2431,7 +2500,7 @@ var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2431
2500
  "in": { type: "input", signal: "audio", node: "input" },
2432
2501
  "out": { type: "output", signal: "audio", node: "output" },
2433
2502
  "modRate": { type: "modulation", signal: "modulation", node: "lfo", field: "frequency" },
2434
- "modCrossover": { type: "modulation", signal: "modulation", node: "lowpass", field: "frequency" }
2503
+ "modCrossover": { type: "modulation", signal: "modulation", node: "_xover", field: "offset" }
2435
2504
  },
2436
2505
  createModule(id, position) {
2437
2506
  return { id, type: "harmonic-trem", position, ...paramDefaults(params22) };
@@ -2440,8 +2509,9 @@ var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2440
2509
  const input = ctx.createGain();
2441
2510
  const output = ctx.createGain();
2442
2511
  const lowpass = ctx.createBiquadFilter();
2443
- const lpInvert = ctx.createGain();
2444
- const highBand = ctx.createGain();
2512
+ const highpass = ctx.createBiquadFilter();
2513
+ const hpInvert = ctx.createGain();
2514
+ const _xover = ctx.createConstantSource();
2445
2515
  const lowGain = ctx.createGain();
2446
2516
  const highGain = ctx.createGain();
2447
2517
  const lfo = ctx.createOscillator();
@@ -2449,10 +2519,14 @@ var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2449
2519
  const lfoGainHigh = ctx.createGain();
2450
2520
  const lfoInvert = ctx.createGain();
2451
2521
  lowpass.type = "lowpass";
2452
- lowpass.frequency.value = module.crossover;
2453
- lowpass.Q.value = -6.0206;
2454
- lpInvert.gain.value = -1;
2455
- highBand.gain.value = 1;
2522
+ highpass.type = "highpass";
2523
+ for (const f of [lowpass, highpass]) {
2524
+ f.Q.value = -6.0206;
2525
+ f.frequency.value = 0;
2526
+ _xover.connect(f.frequency);
2527
+ }
2528
+ _xover.offset.value = module.crossover;
2529
+ hpInvert.gain.value = -1;
2456
2530
  lfo.type = "sine";
2457
2531
  lfo.frequency.value = module.rate;
2458
2532
  lfoGainLow.gain.value = module.depth * 0.5;
@@ -2461,20 +2535,20 @@ var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2461
2535
  lowGain.gain.value = 1;
2462
2536
  highGain.gain.value = 1;
2463
2537
  input.connect(lowpass);
2464
- input.connect(highBand);
2465
- lowpass.connect(lpInvert);
2466
- lpInvert.connect(highBand);
2538
+ input.connect(highpass);
2539
+ highpass.connect(hpInvert);
2467
2540
  lfo.connect(lfoGainLow);
2468
2541
  lfo.connect(lfoInvert);
2469
2542
  lfoInvert.connect(lfoGainHigh);
2470
2543
  lfoGainLow.connect(lowGain.gain);
2471
2544
  lfoGainHigh.connect(highGain.gain);
2472
2545
  lowpass.connect(lowGain);
2473
- highBand.connect(highGain);
2546
+ hpInvert.connect(highGain);
2474
2547
  lowGain.connect(output);
2475
2548
  highGain.connect(output);
2476
2549
  lfo.start();
2477
- return { input, output, lfo, lfoGainLow, lfoGainHigh, lowpass, lpInvert, highBand };
2550
+ _xover.start();
2551
+ return { input, output, lfo, lfoGainLow, lfoGainHigh, lowpass, highpass, hpInvert, _xover };
2478
2552
  },
2479
2553
  updateParam(nodes, _m, param, value) {
2480
2554
  if (param === "rate") nodes.lfo.frequency.value = value;
@@ -2482,10 +2556,11 @@ var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2482
2556
  nodes.lfoGainLow.gain.value = value * 0.5;
2483
2557
  nodes.lfoGainHigh.gain.value = value * 0.5;
2484
2558
  } else if (param === "crossover") {
2485
- nodes.lowpass.frequency.value = value;
2559
+ nodes._xover.offset.value = value;
2486
2560
  }
2487
2561
  },
2488
2562
  dispose(nodes) {
2563
+ nodes._xover.stop();
2489
2564
  nodes.lfo.stop();
2490
2565
  }
2491
2566
  });
@@ -2623,21 +2698,27 @@ var Lfo = /* @__PURE__ */ defineModule(lfoDescriptor, {
2623
2698
  });
2624
2699
 
2625
2700
  // src/lib-runtime/generated/modules/lo-fi.tsx
2626
- function makeLoFiSaturationCurve(amount) {
2627
- const samples = 44100;
2628
- const curve = new Float32Array(samples);
2629
- const k = 1 + amount * 5;
2630
- for (let i = 0; i < samples; i++) {
2631
- const x = i * 2 / samples - 1;
2632
- curve[i] = x >= 0 ? Math.tanh(k * x * 0.8) : Math.tanh(k * x * 1.2);
2701
+ var DRIVE_BIAS = 0.3;
2702
+ var INPUT_SPAN = 16;
2703
+ var CURVE_LEN2 = 32769;
2704
+ var WOBBLE_MAX_S = 2e-3;
2705
+ var driveK = (drive) => 0.125 * Math.pow(128, Math.max(0, Math.min(1, drive)));
2706
+ function loFiCurve(drive) {
2707
+ const k = driveK(drive);
2708
+ const t0 = Math.tanh(DRIVE_BIAS);
2709
+ const slope0 = 1 - t0 * t0;
2710
+ const c = new Float32Array(CURVE_LEN2);
2711
+ for (let i = 0; i < CURVE_LEN2; i++) {
2712
+ const v = i * 2 / (CURVE_LEN2 - 1) - 1;
2713
+ c[i] = (Math.tanh(k * INPUT_SPAN * v + DRIVE_BIAS) - t0) / (k * slope0);
2633
2714
  }
2634
- return curve;
2715
+ return c;
2635
2716
  }
2636
2717
  var params25 = {
2637
2718
  drive: { unit: "factor", default: 0.3, min: 0, max: 1 },
2638
2719
  cutoff: { node: "lowpass", field: "frequency", unit: "Hz", default: 4e3, min: 500, max: 8e3 },
2639
- noise: { unit: "factor", default: 0.2, min: 0, max: 1 },
2640
- wobble: { unit: "factor", default: 0.1, min: 0, max: 1 }
2720
+ noise: { node: "noiseGain", field: "gain", unit: "factor", default: 0.2, min: 0, max: 1 },
2721
+ wobble: { node: "wobbleAmt", field: "offset", unit: "factor", default: 0.1, min: 0, max: 1 }
2641
2722
  };
2642
2723
  var loFiDescriptor = /* @__PURE__ */ defineDescriptor({
2643
2724
  type: "lo-fi",
@@ -2653,73 +2734,78 @@ var loFiDescriptor = /* @__PURE__ */ defineDescriptor({
2653
2734
  createNodes(ctx, module) {
2654
2735
  const input = ctx.createGain();
2655
2736
  const output = ctx.createGain();
2656
- const saturation = ctx.createWaveShaper();
2737
+ const preGain = ctx.createGain();
2738
+ preGain.gain.value = 1 / INPUT_SPAN;
2739
+ const saturation = createShaperNode(ctx, loFiCurve(module.drive), { dry: false });
2740
+ const dcBlock = ctx.createBiquadFilter();
2741
+ dcBlock.type = "highpass";
2742
+ dcBlock.frequency.value = 10;
2743
+ dcBlock.Q.value = Math.SQRT1_2;
2657
2744
  const lowpass = ctx.createBiquadFilter();
2658
- const noiseGain = ctx.createGain();
2659
- const wobbleLfo = ctx.createOscillator();
2660
- const wobbleGain = ctx.createGain();
2661
- saturation.curve = makeLoFiSaturationCurve(module.drive);
2662
- saturation.oversample = "2x";
2663
2745
  lowpass.type = "lowpass";
2664
2746
  lowpass.frequency.value = module.cutoff;
2665
2747
  lowpass.Q.value = 0.7;
2666
- const bufferSize = 2 * ctx.sampleRate;
2667
- const noiseBuffer2 = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
2668
- const data = noiseBuffer2.getChannelData(0);
2669
- const rand = noiseSource(NOISE_SEEDS.loFiCrackle);
2670
- let b0 = 0, b1 = 0, b2 = 0;
2671
- for (let i = 0; i < bufferSize; i++) {
2672
- const white = rand();
2673
- b0 = 0.99765 * b0 + white * 0.099046;
2674
- b1 = 0.963 * b1 + white * 0.2965164;
2675
- b2 = 0.57 * b2 + white * 1.0526913;
2676
- data[i] = (b0 + b1 + b2) * 0.1;
2677
- }
2678
- const noiseSrc = ctx.createBufferSource();
2679
- noiseSrc.buffer = noiseBuffer2;
2680
- noiseSrc.loop = true;
2681
- noiseGain.gain.value = module.noise * 0.05;
2682
- noiseSrc.connect(noiseGain);
2683
- noiseSrc.start();
2684
- const wobbleDelay = ctx.createDelay(0.05);
2685
- wobbleDelay.delayTime.value = 0.01;
2748
+ const wobbleAmt = ctx.createConstantSource();
2749
+ wobbleAmt.offset.value = module.wobble;
2750
+ const wobbleScale = ctx.createGain();
2751
+ wobbleScale.gain.value = WOBBLE_MAX_S;
2752
+ const wobbleLfo = ctx.createOscillator();
2686
2753
  wobbleLfo.type = "sine";
2687
2754
  wobbleLfo.frequency.value = 0.73;
2688
- wobbleGain.gain.value = module.wobble * 2e-3;
2689
- wobbleLfo.connect(wobbleGain);
2690
- wobbleGain.connect(wobbleDelay.delayTime);
2755
+ const wobbleDepth = ctx.createGain();
2756
+ wobbleDepth.gain.value = 0;
2757
+ const wobbleDelay = ctx.createDelay(0.05);
2758
+ wobbleDelay.delayTime.value = 0;
2759
+ wobbleAmt.connect(wobbleScale);
2760
+ wobbleScale.connect(wobbleDelay.delayTime);
2761
+ wobbleScale.connect(wobbleDepth.gain);
2762
+ wobbleLfo.connect(wobbleDepth);
2763
+ wobbleDepth.connect(wobbleDelay.delayTime);
2764
+ wobbleAmt.start();
2691
2765
  wobbleLfo.start();
2692
- input.connect(saturation);
2693
- saturation.connect(lowpass);
2766
+ const noiseSrc = createVinylNoiseNode(ctx, NOISE_SEEDS.loFiCrackle);
2767
+ const noiseGain = ctx.createGain();
2768
+ noiseGain.gain.value = module.noise;
2769
+ noiseSrc.connect(noiseGain);
2770
+ input.connect(preGain);
2771
+ preGain.connect(saturation);
2772
+ saturation.connect(dcBlock, 0);
2773
+ dcBlock.connect(lowpass);
2694
2774
  lowpass.connect(wobbleDelay);
2695
2775
  wobbleDelay.connect(output);
2696
2776
  noiseGain.connect(output);
2697
- return { input, output, saturation, lowpass, wobbleDelay, noiseGain, wobbleGain, wobbleLfo, noiseSrc };
2777
+ return { input, output, preGain, saturation, dcBlock, lowpass, wobbleAmt, wobbleScale, wobbleLfo, wobbleDepth, wobbleDelay, noiseSrc, noiseGain };
2698
2778
  },
2699
2779
  updateParam(nodes, _m, param, value) {
2700
2780
  if (param === "drive") {
2701
- nodes.saturation.curve = makeLoFiSaturationCurve(value);
2781
+ nodes.saturation.port.postMessage({ curve: loFiCurve(value) });
2702
2782
  } else if (param === "cutoff") {
2703
2783
  nodes.lowpass.frequency.value = value;
2704
2784
  } else if (param === "noise") {
2705
- nodes.noiseGain.gain.value = value * 0.05;
2785
+ nodes.noiseGain.gain.value = value;
2706
2786
  } else if (param === "wobble") {
2707
- nodes.wobbleGain.gain.value = value * 2e-3;
2787
+ nodes.wobbleAmt.offset.value = value;
2708
2788
  }
2709
2789
  },
2710
2790
  dispose(nodes) {
2711
2791
  nodes.wobbleLfo.stop();
2712
- nodes.noiseSrc.stop();
2792
+ nodes.wobbleAmt.stop();
2713
2793
  }
2714
2794
  });
2715
2795
 
2716
2796
  // src/lib-runtime/generated/modules/lo-fi.wrapper.ts
2717
- var LoFi = /* @__PURE__ */ defineModule(loFiDescriptor);
2797
+ var LoFi = /* @__PURE__ */ defineModule(loFiDescriptor, {
2798
+ worklets: {
2799
+ "shaper": shaper_default,
2800
+ "vinyl-noise": vinyl_noise_default
2801
+ }
2802
+ });
2718
2803
 
2719
2804
  // src/lib-runtime/generated/modules/mid-side.tsx
2720
2805
  var params26 = {
2721
2806
  midGain: { node: "midGainNode", field: "gain", unit: "factor", default: 1, min: 0, max: 2 },
2722
- sideGain: { node: "sideGainNode", field: "gain", unit: "factor", default: 1, min: 0, max: 2 }
2807
+ sideGain: { node: "sideGainNode", field: "gain", unit: "factor", default: 1, min: 0, max: 2 },
2808
+ width: { node: "widthNode", field: "gain", unit: "factor", default: 1, min: 0, max: 2 }
2723
2809
  };
2724
2810
  var midSideDescriptor = /* @__PURE__ */ defineDescriptor({
2725
2811
  type: "mid-side",
@@ -3162,9 +3248,58 @@ function dampingToQ(damping) {
3162
3248
  const d = Math.min(1, Math.max(0, Number.isFinite(damping) ? damping : 0.5));
3163
3249
  return MODAL_Q_MAX * Math.pow(MODAL_Q_MIN / MODAL_Q_MAX, d);
3164
3250
  }
3251
+ function modeT60(q, hz2) {
3252
+ return T60_PER_Q_HZ * q / hz2;
3253
+ }
3165
3254
  function modalMakeup(q) {
3166
3255
  return Math.sqrt(Math.max(1, q));
3167
3256
  }
3257
+ var MODAL_PARTIALS = 5;
3258
+ var MODAL_REF_NOTE = 57;
3259
+ function partialEnergySum(structure) {
3260
+ let s = 0;
3261
+ for (let n = 1; n <= MODAL_PARTIALS; n++) s += Math.pow(n, -(1 + structure));
3262
+ return s;
3263
+ }
3264
+ function modalPartialGain(n, q, structure) {
3265
+ return modalMakeup(q) * Math.pow(n, -(1 + structure)) * Math.sqrt(MODAL_PARTIALS / partialEnergySum(structure)) * modalOverlapGain(q, structure);
3266
+ }
3267
+ var OVERLAP_REF_HZ = 220;
3268
+ var OVERLAP_SR = 48e3;
3269
+ var overlapCache = /* @__PURE__ */ new Map();
3270
+ function modalOverlapGain(q, structure) {
3271
+ if (!(q < 40)) return 1;
3272
+ const key = `${q.toFixed(4)}:${structure.toFixed(4)}`;
3273
+ const hit = overlapCache.get(key);
3274
+ if (hit !== void 0) return hit;
3275
+ const n = Math.ceil(2 * modeT60(q, OVERLAP_REF_HZ) * OVERLAP_SR) + 64;
3276
+ const sum = new Float64Array(n);
3277
+ let incoherent = 0;
3278
+ for (let p = 1; p <= MODAL_PARTIALS; p++) {
3279
+ const f0 = Math.min(OVERLAP_SR * 0.45, OVERLAP_REF_HZ * Math.pow(p, 1 + structure));
3280
+ const g = Math.pow(p, -(1 + structure));
3281
+ const w0 = 2 * Math.PI * f0 / OVERLAP_SR;
3282
+ const alpha = Math.sin(w0) / (2 * q);
3283
+ const a0 = 1 + alpha;
3284
+ const b0 = alpha / a0, a1 = -2 * Math.cos(w0) / a0, a2 = (1 - alpha) / a0;
3285
+ let y2 = 0, y1 = 0;
3286
+ for (let i = 0; i < n; i++) {
3287
+ const x = i === 0 ? b0 : i === 2 ? -b0 : 0;
3288
+ const y = x - a1 * y1 - a2 * y2;
3289
+ y2 = y1;
3290
+ y1 = y;
3291
+ sum[i] += g * y;
3292
+ incoherent += g * g * y * y;
3293
+ }
3294
+ }
3295
+ let coherent = 0;
3296
+ for (let i = 0; i < n; i++) coherent += sum[i] * sum[i];
3297
+ const gain = coherent > 0 ? Math.sqrt(incoherent / coherent) : 1;
3298
+ if (overlapCache.size > 4096) overlapCache.clear();
3299
+ overlapCache.set(key, gain);
3300
+ return gain;
3301
+ }
3302
+ var MODAL_PITCH_GAIN_TRANSPOSE = 69 + 12 * (MODAL_REF_NOTE / 24 - Math.log2(440));
3168
3303
 
3169
3304
  // src/lib-runtime/generated/modules/resonator.tsx
3170
3305
  var params31 = {
@@ -3172,12 +3307,13 @@ var params31 = {
3172
3307
  offset: { node: "_root", field: "offset", default: 57, min: -48, max: 108, unit: "semitone" },
3173
3308
  structure: { unit: "factor", default: 0, min: 0, max: 1 },
3174
3309
  damping: { unit: "factor", default: 0.5, min: 0, max: 1 },
3175
- mix: { node: "_mix", field: "offset", unit: "factor", default: 0.5, min: 0, max: 1 }
3310
+ mix: { node: "_mix", field: "offset", unit: "factor", default: 1, min: 0, max: 1 }
3176
3311
  };
3177
3312
  function partialOffset(partial, structure) {
3178
3313
  return 12 * (1 + structure) * Math.log2(partial);
3179
3314
  }
3180
3315
  var patchCount2 = /* @__PURE__ */ new WeakMap();
3316
+ var shape = /* @__PURE__ */ new WeakMap();
3181
3317
  var resonatorDescriptor = /* @__PURE__ */ defineDescriptor({
3182
3318
  type: "resonator",
3183
3319
  params: params31,
@@ -3205,7 +3341,17 @@ var resonatorDescriptor = /* @__PURE__ */ defineDescriptor({
3205
3341
  const partialGains = [];
3206
3342
  const voct = [];
3207
3343
  const q = dampingToQ(module.damping);
3208
- const makeup = modalMakeup(q);
3344
+ shape.set(_pitch, { q, structure: module.structure });
3345
+ const _halfPitch = ctx.createGain();
3346
+ _halfPitch.gain.value = -0.5;
3347
+ _pitch.connect(_halfPitch);
3348
+ const _trackGain = createVoctFreqNode(ctx);
3349
+ _trackGain.parameters.get("transpose").value = MODAL_PITCH_GAIN_TRANSPOSE;
3350
+ _halfPitch.connect(_trackGain);
3351
+ const _track = ctx.createGain();
3352
+ _track.gain.value = 0;
3353
+ _trackGain.connect(_track.gain);
3354
+ _track.connect(wetGain);
3209
3355
  for (let i = 1; i <= 5; i++) {
3210
3356
  const v = createVoctFreqNode(ctx);
3211
3357
  v.parameters.get("transpose").value = partialOffset(i, module.structure);
@@ -3219,9 +3365,9 @@ var resonatorDescriptor = /* @__PURE__ */ defineDescriptor({
3219
3365
  voct.push(v);
3220
3366
  input.connect(filter);
3221
3367
  const partialGain = ctx.createGain();
3222
- partialGain.gain.value = makeup / Math.sqrt(i);
3368
+ partialGain.gain.value = modalPartialGain(i, q, module.structure);
3223
3369
  filter.connect(partialGain);
3224
- partialGain.connect(wetGain);
3370
+ partialGain.connect(_track);
3225
3371
  partialGains.push(partialGain);
3226
3372
  }
3227
3373
  const _alive = createKeepAlive(ctx, ...filters);
@@ -3238,6 +3384,9 @@ var resonatorDescriptor = /* @__PURE__ */ defineDescriptor({
3238
3384
  _root,
3239
3385
  _pitch,
3240
3386
  _alive,
3387
+ _halfPitch,
3388
+ _trackGain,
3389
+ _track,
3241
3390
  v1: voct[0],
3242
3391
  v2: voct[1],
3243
3392
  v3: voct[2],
@@ -3263,19 +3412,20 @@ var resonatorDescriptor = /* @__PURE__ */ defineDescriptor({
3263
3412
  if (!patchCount2.get(nodes)) nodes._root.offset.value = value;
3264
3413
  } else if (param === "transpose") {
3265
3414
  if (patchCount2.get(nodes)) nodes._root.offset.value = value;
3266
- } else if (param === "structure") {
3267
- const voct = [nodes.v1, nodes.v2, nodes.v3, nodes.v4, nodes.v5];
3268
- for (let i = 1; i < 5; i++) {
3269
- voct[i].parameters.get("transpose").value = partialOffset(i + 1, value);
3415
+ } else if (param === "structure" || param === "damping") {
3416
+ const st = shape.get(nodes._pitch);
3417
+ if (param === "structure") {
3418
+ st.structure = value;
3419
+ const voct = [nodes.v1, nodes.v2, nodes.v3, nodes.v4, nodes.v5];
3420
+ for (let i = 1; i < 5; i++) {
3421
+ voct[i].parameters.get("transpose").value = partialOffset(i + 1, st.structure);
3422
+ }
3423
+ } else {
3424
+ st.q = dampingToQ(value);
3425
+ for (const f of filters) f.Q.value = st.q;
3270
3426
  }
3271
- } else if (param === "damping") {
3272
- const q = dampingToQ(value);
3273
- const makeup = modalMakeup(q);
3274
3427
  const gains = [nodes.g1, nodes.g2, nodes.g3, nodes.g4, nodes.g5];
3275
- for (let i = 0; i < 5; i++) {
3276
- filters[i].Q.value = q;
3277
- gains[i].gain.value = makeup / Math.sqrt(i + 1);
3278
- }
3428
+ for (let i = 0; i < 5; i++) gains[i].gain.value = modalPartialGain(i + 1, st.q, st.structure);
3279
3429
  } else if (param === "mix") {
3280
3430
  nodes._mix.offset.value = value;
3281
3431
  }
@@ -3314,13 +3464,30 @@ var Resonator = /* @__PURE__ */ defineModule(resonatorDescriptor, {
3314
3464
  });
3315
3465
 
3316
3466
  // src/lib/dsp/fdnTank.ts
3317
- var COMB_TIMES_MS = [29.7, 37.1, 41.1, 43.7];
3467
+ var LINE_MS = [
3468
+ 32.479167,
3469
+ 32.645833,
3470
+ 39.354167,
3471
+ 37.979167,
3472
+ 44.854167,
3473
+ 46.729167,
3474
+ 54.854167,
3475
+ 54.020833,
3476
+ 64.354167,
3477
+ 65.020833,
3478
+ 77.770833,
3479
+ 76.229167,
3480
+ 90.770833,
3481
+ 91.854167,
3482
+ 108.520833,
3483
+ 108.270833
3484
+ ];
3318
3485
  function tankEnergyGain500(rt60, size = 1) {
3319
3486
  let g2 = 0;
3320
- for (const ms2 of COMB_TIMES_MS) g2 += Math.pow(10, -6 * (ms2 / 1e3) * size / Math.max(rt60, 0.05));
3321
- g2 /= COMB_TIMES_MS.length;
3487
+ for (const ms2 of LINE_MS) g2 += Math.pow(10, -6 * (ms2 / 1e3) * size / Math.max(rt60, 0.05));
3488
+ g2 /= LINE_MS.length;
3322
3489
  const loss = 1 - g2;
3323
- return Math.max(0.05, 0.6422 / loss - 0.2406 - 11e-4 * loss);
3490
+ return Math.max(0.01, 1.32117 / loss - 1.32832 - 0.01158 * loss);
3324
3491
  }
3325
3492
  function createFdnTank(ctx, opts) {
3326
3493
  const input = ctx.createGain();
@@ -3500,10 +3667,10 @@ var RingModulator = /* @__PURE__ */ defineModule(ringModDescriptor, {
3500
3667
 
3501
3668
  // src/lib-runtime/generated/modules/rotary.tsx
3502
3669
  var CROSSOVER_HZ = 800;
3503
- var BUTTERWORTH_Q_DB = -3.0103;
3670
+ var BUTTERWORTH_Q_DB2 = -3.0103;
3504
3671
  var DRUM_RATIO = 0.85;
3505
- var HORN = { baseDelay: 3e-3, dopplerSec: 44e-5, ampBase: 0.7, ampDepth: 0.3, phaseDeg: 0, tauUp: 0.35, tauDown: 0.55 };
3506
- var DRUM = { baseDelay: 3e-3, dopplerSec: 2e-4, ampBase: 0.8, ampDepth: 0.2, phaseDeg: 90, tauUp: 1.5, tauDown: 1.7 };
3672
+ var HORN = { baseDelay: 3e-3, dopplerSec: 44e-5, ampBase: 0.54, ampDepth: 0.46, phaseDeg: 0, tauUp: 0.35, tauDown: 0.55 };
3673
+ var DRUM = { baseDelay: 3e-3, dopplerSec: 2e-4, ampBase: 0.6, ampDepth: 0.4, phaseDeg: 90, tauUp: 1.5, tauDown: 1.7 };
3507
3674
  var params34 = {
3508
3675
  speed: { kind: "enum", options: ["slow", "fast"], default: "slow" },
3509
3676
  slowRate: { unit: "Hz", default: 0.8, min: 0.5, max: 2 },
@@ -3536,7 +3703,7 @@ var rotaryDescriptor = /* @__PURE__ */ defineDescriptor({
3536
3703
  const f = ctx.createBiquadFilter();
3537
3704
  f.type = type;
3538
3705
  f.frequency.value = CROSSOVER_HZ;
3539
- f.Q.value = BUTTERWORTH_Q_DB;
3706
+ f.Q.value = BUTTERWORTH_Q_DB2;
3540
3707
  return f;
3541
3708
  };
3542
3709
  const lp1 = biquad("lowpass"), lp2 = biquad("lowpass");
@@ -3629,22 +3796,24 @@ var Rotary = /* @__PURE__ */ defineModule(rotaryDescriptor);
3629
3796
 
3630
3797
  // src/lib-runtime/generated/modules/saturator.tsx
3631
3798
  var params35 = {
3632
- // drive feeds both driveGain.gain (1 + drive*4) and the waveshaper curve —
3633
- // a conversion, not a 1:1 AudioParam write, so no node/field.
3799
+ // DRIVE reshapes the curve (hotter knee, output trimmed to match) — no
3800
+ // single AudioParam carries it: set-only.
3634
3801
  drive: { unit: "factor", default: 0.5, min: 0, max: 1 },
3635
3802
  // warmthFilter.gain = warmth * 6 — a scaled conversion, so no node/field.
3636
3803
  warmth: { unit: "factor", default: 0.5, min: 0, max: 1 },
3637
3804
  mix: { node: "_mix", field: "offset", unit: "factor", default: 1, min: 0, max: 1 }
3638
3805
  };
3639
- function makeSaturationCurve(amount) {
3640
- const samples = 44100;
3641
- const curve = new Float32Array(samples);
3642
- const k = 1 + amount * 10;
3643
- for (let i = 0; i < samples; i++) {
3644
- const x = i * 2 / samples - 1;
3645
- curve[i] = Math.tanh(k * x) / Math.tanh(k);
3806
+ var INPUT_SPAN2 = 16;
3807
+ var CURVE_LEN3 = 32769;
3808
+ var driveGain = (drive) => 0.25 * Math.pow(64, Math.max(0, Math.min(1, drive)));
3809
+ function saturationCurve(drive) {
3810
+ const g = driveGain(drive);
3811
+ const c = new Float32Array(CURVE_LEN3);
3812
+ for (let i = 0; i < CURVE_LEN3; i++) {
3813
+ const v = i * 2 / (CURVE_LEN3 - 1) - 1;
3814
+ c[i] = Math.tanh(g * INPUT_SPAN2 * v) / g;
3646
3815
  }
3647
- return curve;
3816
+ return c;
3648
3817
  }
3649
3818
  var saturatorDescriptor = /* @__PURE__ */ defineDescriptor({
3650
3819
  type: "saturator",
@@ -3661,29 +3830,29 @@ var saturatorDescriptor = /* @__PURE__ */ defineDescriptor({
3661
3830
  const output = ctx.createGain();
3662
3831
  const dryGain = ctx.createGain();
3663
3832
  const wetGain = ctx.createGain();
3664
- const driveGain = ctx.createGain();
3665
- const waveshaper = ctx.createWaveShaper();
3833
+ const preGain = ctx.createGain();
3834
+ preGain.gain.value = 1 / INPUT_SPAN2;
3835
+ const shaper = createShaperNode(ctx, saturationCurve(module.drive));
3836
+ const dryTrim = ctx.createGain();
3837
+ dryTrim.gain.value = INPUT_SPAN2;
3666
3838
  const warmthFilter = ctx.createBiquadFilter();
3667
- driveGain.gain.value = 1 + module.drive * 4;
3668
- waveshaper.curve = makeSaturationCurve(module.drive);
3669
- waveshaper.oversample = "2x";
3670
3839
  warmthFilter.type = "lowshelf";
3671
3840
  warmthFilter.frequency.value = 300;
3672
3841
  warmthFilter.gain.value = module.warmth * 6;
3673
3842
  const _mix = createMix(ctx, dryGain, wetGain, module.mix, "linear");
3674
- input.connect(dryGain);
3675
- input.connect(driveGain);
3676
- driveGain.connect(waveshaper);
3677
- waveshaper.connect(warmthFilter);
3843
+ input.connect(preGain);
3844
+ preGain.connect(shaper);
3845
+ shaper.connect(warmthFilter, 0);
3678
3846
  warmthFilter.connect(wetGain);
3847
+ shaper.connect(dryTrim, 1);
3848
+ dryTrim.connect(dryGain);
3679
3849
  dryGain.connect(output);
3680
3850
  wetGain.connect(output);
3681
- return { input, output, driveGain, waveshaper, warmthFilter, dryGain, wetGain, _mix };
3851
+ return { input, output, preGain, shaper, dryTrim, warmthFilter, dryGain, wetGain, _mix };
3682
3852
  },
3683
3853
  updateParam(nodes, _m, param, value) {
3684
3854
  if (param === "drive") {
3685
- nodes.driveGain.gain.value = 1 + value * 4;
3686
- nodes.waveshaper.curve = makeSaturationCurve(value);
3855
+ nodes.shaper.port.postMessage({ curve: saturationCurve(value) });
3687
3856
  } else if (param === "warmth") {
3688
3857
  nodes.warmthFilter.gain.value = value * 6;
3689
3858
  } else if (param === "mix") {
@@ -3693,7 +3862,11 @@ var saturatorDescriptor = /* @__PURE__ */ defineDescriptor({
3693
3862
  });
3694
3863
 
3695
3864
  // src/lib-runtime/generated/modules/saturator.wrapper.ts
3696
- var Saturator = /* @__PURE__ */ defineModule(saturatorDescriptor);
3865
+ var Saturator = /* @__PURE__ */ defineModule(saturatorDescriptor, {
3866
+ worklets: {
3867
+ "shaper": shaper_default
3868
+ }
3869
+ });
3697
3870
 
3698
3871
  // src/lib/dsp/shimmerRegen.ts
3699
3872
  var REGEN_HP_HZ = 300;
@@ -3703,13 +3876,13 @@ var SHIFTER_ENERGY = 0.75;
3703
3876
  var REGEN_DELAY_S = 0.02;
3704
3877
  var SHIFTER_SEEDS = [5370206, 739982445];
3705
3878
  var UNISON_FLOOR = [
3706
- [0.5, 0.68],
3707
- [1, 0.75],
3708
- [2, 0.76],
3709
- [3, 0.8],
3710
- [5, 0.84],
3711
- [7, 0.75],
3712
- [10, 0.6]
3879
+ [0.5, 0.58],
3880
+ [1, 0.67],
3881
+ [2, 0.73],
3882
+ [3, 0.77],
3883
+ [5, 0.79],
3884
+ [7, 0.77],
3885
+ [10, 0.72]
3713
3886
  ];
3714
3887
  var UNISON_PITCH_SPAN = 0.75;
3715
3888
  function unisonFloor(decay) {
@@ -3851,9 +4024,9 @@ function makeSoftLimitCurve(knee, drive) {
3851
4024
  const k = 0.1 + knee * 0.9;
3852
4025
  const driveMul = 1 + drive * 3;
3853
4026
  for (let i = 0; i < samples; i++) {
3854
- const x = (i * 2 / samples - 1) * driveMul;
3855
- if (Math.abs(x) < k) {
3856
- curve[i] = x;
4027
+ const x = (i * 2 / (samples - 1) - 1) * driveMul;
4028
+ if (Math.abs(x) < k || 1 - k < 1e-9) {
4029
+ curve[i] = Math.max(-1, Math.min(1, x));
3857
4030
  } else {
3858
4031
  const sign = x > 0 ? 1 : -1;
3859
4032
  curve[i] = sign * (k + (1 - k) * Math.tanh((Math.abs(x) - k) / (1 - k)));
@@ -3874,25 +4047,28 @@ var softLimitDescriptor = /* @__PURE__ */ defineDescriptor({
3874
4047
  createNodes(ctx, module) {
3875
4048
  const input = ctx.createGain();
3876
4049
  const output = ctx.createGain();
3877
- const waveshaper = ctx.createWaveShaper();
4050
+ const waveshaper = createShaperNode(ctx, makeSoftLimitCurve(module.knee, module.drive), { dry: false });
3878
4051
  const ceiling = ctx.createGain();
3879
- waveshaper.curve = makeSoftLimitCurve(module.knee, module.drive);
3880
- waveshaper.oversample = "2x";
3881
4052
  ceiling.gain.value = Math.pow(10, module.ceiling / 20);
3882
4053
  input.connect(waveshaper);
3883
- waveshaper.connect(ceiling);
4054
+ waveshaper.connect(ceiling, 0);
3884
4055
  ceiling.connect(output);
3885
4056
  return { input, output, waveshaper, ceiling };
3886
4057
  },
3887
4058
  updateParam(nodes, m, param, value) {
3888
- if (param === "drive") nodes.waveshaper.curve = makeSoftLimitCurve(m.knee, value);
3889
- else if (param === "knee") nodes.waveshaper.curve = makeSoftLimitCurve(value, m.drive);
4059
+ const port = nodes.waveshaper.port;
4060
+ if (param === "drive") port.postMessage({ curve: makeSoftLimitCurve(m.knee, value) });
4061
+ else if (param === "knee") port.postMessage({ curve: makeSoftLimitCurve(value, m.drive) });
3890
4062
  else if (param === "ceiling") nodes.ceiling.gain.value = Math.pow(10, value / 20);
3891
4063
  }
3892
4064
  });
3893
4065
 
3894
4066
  // src/lib-runtime/generated/modules/soft-limit.wrapper.ts
3895
- var SoftLimiter = /* @__PURE__ */ defineModule(softLimitDescriptor);
4067
+ var SoftLimiter = /* @__PURE__ */ defineModule(softLimitDescriptor, {
4068
+ worklets: {
4069
+ "shaper": shaper_default
4070
+ }
4071
+ });
3896
4072
 
3897
4073
  // src/lib-runtime/generated/modules/spring-reverb.tsx
3898
4074
  var params38 = {
@@ -3957,6 +4133,7 @@ var SpringReverb = /* @__PURE__ */ defineModule(springReverbDescriptor, {
3957
4133
  // src/lib-runtime/generated/modules/stereo-spread.tsx
3958
4134
  var params39 = {
3959
4135
  spread: { unit: "factor", default: 0.7, min: 0, max: 1 },
4136
+ width: { node: "wetGain", field: "gain", unit: "factor", default: 1, min: 0, max: 2 },
3960
4137
  center: { unit: "Hz", default: 800, min: 200, max: 2e3, scale: "log" }
3961
4138
  };
3962
4139
  var stereoSpreadDescriptor = /* @__PURE__ */ defineDescriptor({
@@ -3972,12 +4149,11 @@ var stereoSpreadDescriptor = /* @__PURE__ */ defineDescriptor({
3972
4149
  createNodes(ctx, module) {
3973
4150
  const input = ctx.createGain();
3974
4151
  const output = ctx.createGain();
3975
- const lowpass = ctx.createBiquadFilter();
4152
+ input.connect(output);
3976
4153
  const highpass = ctx.createBiquadFilter();
3977
- lowpass.type = "lowpass";
3978
- lowpass.frequency.value = module.center;
3979
4154
  highpass.type = "highpass";
3980
4155
  highpass.frequency.value = module.center;
4156
+ highpass.Q.value = -3.0102999566;
3981
4157
  const panLeft = ctx.createStereoPanner();
3982
4158
  const panRight = ctx.createStereoPanner();
3983
4159
  const haasDelay = ctx.createDelay(0.02);
@@ -3986,8 +4162,6 @@ var stereoSpreadDescriptor = /* @__PURE__ */ defineDescriptor({
3986
4162
  haasDelay.delayTime.value = module.spread * 5e-3;
3987
4163
  const wetGain = ctx.createGain();
3988
4164
  wetGain.gain.value = module.width;
3989
- input.connect(lowpass);
3990
- lowpass.connect(output);
3991
4165
  input.connect(highpass);
3992
4166
  highpass.connect(panLeft);
3993
4167
  highpass.connect(haasDelay);
@@ -3995,7 +4169,7 @@ var stereoSpreadDescriptor = /* @__PURE__ */ defineDescriptor({
3995
4169
  panLeft.connect(wetGain);
3996
4170
  panRight.connect(wetGain);
3997
4171
  wetGain.connect(output);
3998
- return { input, output, lowpass, highpass, panLeft, panRight, haasDelay, wetGain };
4172
+ return { input, output, highpass, panLeft, panRight, haasDelay, wetGain };
3999
4173
  },
4000
4174
  updateParam(nodes, _m, param, value) {
4001
4175
  if (param === "spread") {
@@ -4005,7 +4179,6 @@ var stereoSpreadDescriptor = /* @__PURE__ */ defineDescriptor({
4005
4179
  } else if (param === "width") {
4006
4180
  nodes.wetGain.gain.value = value;
4007
4181
  } else if (param === "center") {
4008
- nodes.lowpass.frequency.value = value;
4009
4182
  nodes.highpass.frequency.value = value;
4010
4183
  }
4011
4184
  }
@@ -4502,12 +4675,13 @@ var Vco = /* @__PURE__ */ defineModule(oscillatorDescriptor, {
4502
4675
  });
4503
4676
 
4504
4677
  // src/lib-runtime/generated/modules/wah.tsx
4678
+ var DEPTH_ST = 18;
4505
4679
  var params48 = {
4506
4680
  note: { node: "_cutoffVoct", field: "transpose", unit: "note", default: 79, min: 55, max: 95 },
4507
4681
  // Resonance drives both the filter Q and a separate makeup gain — a split, so it stays set-only.
4508
4682
  resonance: { unit: "factor", default: 8, min: 1, max: 20, scale: "log" },
4509
4683
  rate: { node: "lfo", field: "frequency", unit: "Hz", default: 1, min: 0.1, max: 10, scale: "log" },
4510
- // Depth is stored 0-1 but scaled by 36 semitones onto lfoGain.gain — a conversion, so set-only.
4684
+ // Depth is stored 0-1 but scaled by DEPTH_ST semitones onto lfoGain.gain — a conversion, so set-only.
4511
4685
  depth: { unit: "factor", default: 0.6, min: 0, max: 1 }
4512
4686
  };
4513
4687
  var wahDescriptor = /* @__PURE__ */ defineDescriptor({
@@ -4541,7 +4715,7 @@ var wahDescriptor = /* @__PURE__ */ defineDescriptor({
4541
4715
  _cutoffVoct.connect(filter.frequency);
4542
4716
  lfo.type = "sine";
4543
4717
  lfo.frequency.value = module.rate;
4544
- lfoGain.gain.value = module.depth * 36;
4718
+ lfoGain.gain.value = module.depth * DEPTH_ST;
4545
4719
  lfo.connect(lfoGain);
4546
4720
  lfoGain.connect(_cutoffVoct);
4547
4721
  lfo.start();
@@ -4560,7 +4734,7 @@ var wahDescriptor = /* @__PURE__ */ defineDescriptor({
4560
4734
  } else if (param === "rate") {
4561
4735
  nodes.lfo.frequency.value = value;
4562
4736
  } else if (param === "depth") {
4563
- nodes.lfoGain.gain.value = value * 36;
4737
+ nodes.lfoGain.gain.value = value * DEPTH_ST;
4564
4738
  }
4565
4739
  },
4566
4740
  dispose(nodes) {
@@ -4618,20 +4792,18 @@ var wavefolderDescriptor = /* @__PURE__ */ defineDescriptor({
4618
4792
  const output = ctx.createGain();
4619
4793
  const dryGain = ctx.createGain();
4620
4794
  const wetGain = ctx.createGain();
4621
- const waveshaper = ctx.createWaveShaper();
4622
- waveshaper.curve = makeWavefoldCurve(module.folds, module.symmetry);
4623
- waveshaper.oversample = "4x";
4795
+ const waveshaper = createShaperNode(ctx, makeWavefoldCurve(module.folds, module.symmetry));
4624
4796
  const _mix = createMix(ctx, dryGain, wetGain, module.mix, "linear");
4625
- input.connect(dryGain);
4626
4797
  input.connect(waveshaper);
4627
- waveshaper.connect(wetGain);
4798
+ waveshaper.connect(dryGain, 1);
4799
+ waveshaper.connect(wetGain, 0);
4628
4800
  dryGain.connect(output);
4629
4801
  wetGain.connect(output);
4630
4802
  return { input, output, waveshaper, dryGain, wetGain, _mix };
4631
4803
  },
4632
4804
  updateParam(nodes, module, param, value) {
4633
4805
  if (param === "folds" || param === "symmetry") {
4634
- nodes.waveshaper.curve = makeWavefoldCurve(module.folds, module.symmetry);
4806
+ nodes.waveshaper.port.postMessage({ curve: makeWavefoldCurve(module.folds, module.symmetry) });
4635
4807
  } else if (param === "mix") {
4636
4808
  nodes._mix.offset.value = value;
4637
4809
  }
@@ -4639,7 +4811,11 @@ var wavefolderDescriptor = /* @__PURE__ */ defineDescriptor({
4639
4811
  });
4640
4812
 
4641
4813
  // src/lib-runtime/generated/modules/wavefolder.wrapper.ts
4642
- var Wavefolder = /* @__PURE__ */ defineModule(wavefolderDescriptor);
4814
+ var Wavefolder = /* @__PURE__ */ defineModule(wavefolderDescriptor, {
4815
+ worklets: {
4816
+ "shaper": shaper_default
4817
+ }
4818
+ });
4643
4819
  export {
4644
4820
  AcidFilter,
4645
4821
  AutoPan,