@jpdutoit/squelchy 0.0.4 → 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 +5 -1
  2. package/{chunk-N7J25N5J.js → chunk-PJDNZ52Q.js} +21 -5
  3. package/chunk-PJDNZ52Q.js.map +7 -0
  4. package/docs/GUIDE.md +87 -9
  5. package/docs/MODULES.md +47 -37
  6. package/docs/modules.json +114 -32
  7. package/index.d.ts +99 -30
  8. package/index.js +557 -362
  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" }));
@@ -180,26 +184,29 @@ function createTransport(ctx, opts = {}) {
180
184
  if (at === "now") return now2;
181
185
  if (at === "beat") return Math.ceil(now2 - EPS);
182
186
  if (at === "bar") return Math.ceil(now2 / beatsPerBar - EPS) * beatsPerBar;
183
- if ("bar" in at) return at.bar * beatsPerBar;
187
+ if ("bar" in at) return at.bar * beatsPerBar + (at.beat ?? 0);
184
188
  return at.beat;
185
189
  }
186
190
  const latency = () => ctx.outputLatency || ctx.baseLatency || 0;
187
- function emitTap(hit, local) {
188
- if (!local && taps.size === 0) return;
189
- const fire = () => {
190
- local?.(hit);
191
- for (const cb of taps) cb(hit);
192
- };
191
+ function whenAudible(time, fire) {
193
192
  if (offline) {
194
193
  fire();
195
- return;
194
+ return void 0;
196
195
  }
197
- const delay = Math.max(0, (hit.time - ctx.currentTime + latency()) * 1e3);
196
+ const delay = Math.max(0, (time - ctx.currentTime + latency()) * 1e3);
198
197
  const id = setTimeout(() => {
199
198
  tapTimers.delete(id);
200
199
  fire();
201
200
  }, delay);
202
201
  tapTimers.add(id);
202
+ return id;
203
+ }
204
+ function emitTap(hit, local) {
205
+ if (!local && taps.size === 0) return;
206
+ whenAudible(hit.time, () => {
207
+ local?.(hit);
208
+ for (const cb of taps) cb(hit);
209
+ });
203
210
  }
204
211
  function horizonBeat() {
205
212
  if (offline) return beatAt(ctx.length / ctx.sampleRate);
@@ -320,16 +327,20 @@ function createTransport(ctx, opts = {}) {
320
327
  }
321
328
  };
322
329
  },
323
- at(when, fn) {
330
+ at(when, fn, o = {}) {
324
331
  const b = typeof when === "number" ? when : resolve(when);
325
332
  let fired = false;
333
+ let audibleTimer;
326
334
  const job = {
327
335
  nextBeat: () => b,
328
336
  run(until) {
329
337
  if (fired) return false;
330
338
  if (b < until) {
331
339
  fired = true;
332
- fn(timeAt(b));
340
+ const time = timeAt(b);
341
+ fn(time);
342
+ const onAudible = o.onAudible;
343
+ if (onAudible) audibleTimer = whenAudible(time, () => onAudible(time));
333
344
  return false;
334
345
  }
335
346
  return true;
@@ -340,6 +351,10 @@ function createTransport(ctx, opts = {}) {
340
351
  return () => {
341
352
  fired = true;
342
353
  jobs.delete(job);
354
+ if (audibleTimer !== void 0) {
355
+ clearTimeout(audibleTimer);
356
+ tapTimers.delete(audibleTimer);
357
+ }
343
358
  };
344
359
  },
345
360
  onHit(cb) {
@@ -491,20 +506,27 @@ var NoteCv = {
491
506
  const pitch = ctx.createConstantSource();
492
507
  pitch.offset.value = 60;
493
508
  pitch.start();
509
+ const velocity = ctx.createConstantSource();
510
+ velocity.offset.value = 0;
511
+ velocity.start();
494
512
  return {
495
513
  gate,
496
514
  pitch,
515
+ velocity,
497
516
  trigger(time, h) {
498
517
  if (typeof h?.value === "number") pitch.offset.setValueAtTime(h.value, time);
499
- gate.offset.setValueAtTime(0, Math.max(0, time - 1 / ctx.sampleRate));
500
- 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);
501
523
  gate.offset.setValueAtTime(0, time + (h?.duration ?? 0.1));
502
524
  },
503
525
  dispose() {
504
- gate.stop();
505
- pitch.stop();
506
- gate.disconnect();
507
- pitch.disconnect();
526
+ for (const n of [gate, pitch, velocity]) {
527
+ n.stop();
528
+ n.disconnect();
529
+ }
508
530
  }
509
531
  };
510
532
  }
@@ -604,8 +626,9 @@ async function loadSampleSet(ctx, set, opts = {}) {
604
626
  var SampleVoice = {
605
627
  create(ctx, kit, key, opts = {}) {
606
628
  const out = ctx.createGain();
607
- const buf = kit.buffer(key);
608
629
  const root = opts.root ?? 60;
630
+ let current = kit;
631
+ let buf = kit.buffer(key);
609
632
  const trigger = (when, h) => {
610
633
  if (!buf) return;
611
634
  const src = ctx.createBufferSource();
@@ -619,6 +642,13 @@ var SampleVoice = {
619
642
  return {
620
643
  output: out,
621
644
  trigger,
645
+ get kit() {
646
+ return current;
647
+ },
648
+ setKit(next) {
649
+ current = next;
650
+ buf = next.buffer(key);
651
+ },
622
652
  connect: (d) => {
623
653
  out.connect(d);
624
654
  return d;
@@ -679,14 +709,33 @@ function createFormantNode(ctx) {
679
709
  return new AudioWorkletNode(ctx, "formant-table", {
680
710
  numberOfInputs: 1,
681
711
  numberOfOutputs: 1,
682
- 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 }
683
731
  });
684
732
  }
685
733
  function createBitcrusherNode(ctx) {
686
734
  return new AudioWorkletNode(ctx, "bitcrusher", {
687
735
  numberOfInputs: 1,
688
736
  numberOfOutputs: 1,
689
- outputChannelCount: [1]
737
+ channelCount: 2,
738
+ channelCountMode: "clamped-max"
690
739
  });
691
740
  }
692
741
  function createSubOctaveNode(ctx) {
@@ -703,6 +752,27 @@ function createEnvFollowerNode(ctx) {
703
752
  outputChannelCount: [1]
704
753
  });
705
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
+ }
706
776
  function createWaveshaperNode(ctx) {
707
777
  return new AudioWorkletNode(ctx, "waveshaper", {
708
778
  numberOfInputs: 1,
@@ -908,7 +978,7 @@ var filter303Descriptor = /* @__PURE__ */ defineDescriptor({
908
978
  const envDepth = ctx.createGain();
909
979
  envDepth.gain.value = module.envMod * (1 + module.accent) * 36;
910
980
  const accentEnvGain = ctx.createGain();
911
- accentEnvGain.gain.value = module.accent * 0.5;
981
+ accentEnvGain.gain.value = module.accent * 1;
912
982
  input.connect(filter1);
913
983
  filter1.connect(filter2);
914
984
  filter2.connect(accentVCA);
@@ -938,7 +1008,7 @@ var filter303Descriptor = /* @__PURE__ */ defineDescriptor({
938
1008
  case "accent": {
939
1009
  const acc = value;
940
1010
  nodes.envDepth.gain.value = module.envMod * (1 + acc) * 36;
941
- nodes.accentEnvGain.gain.value = acc * 0.5;
1011
+ nodes.accentEnvGain.gain.value = acc * 1;
942
1012
  break;
943
1013
  }
944
1014
  case "decay": {
@@ -964,8 +1034,9 @@ var AcidFilter = /* @__PURE__ */ defineModule(filter303Descriptor, {
964
1034
 
965
1035
  // src/lib-runtime/generated/modules/auto-pan.tsx
966
1036
  var shapes = ["sine", "triangle", "square"];
1037
+ var SHAPE_VALUE = { square: -1, triangle: 0, sine: 1 };
967
1038
  var params2 = {
968
- 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 },
969
1040
  depth: { node: "lfoGain", field: "gain", unit: "factor", default: 0.8, min: 0, max: 1 },
970
1041
  shape: { kind: "enum", options: shapes, default: "sine" }
971
1042
  };
@@ -975,7 +1046,7 @@ var autoPanDescriptor = /* @__PURE__ */ defineDescriptor({
975
1046
  ports: {
976
1047
  "in": { type: "input", signal: "audio", node: "input" },
977
1048
  "out": { type: "output", signal: "audio", node: "output" },
978
- "modRate": { type: "modulation", signal: "modulation", node: "lfo", field: "frequency" },
1049
+ "modRate": { type: "modulation", signal: "modulation", node: "_freqSrc", field: "offset" },
979
1050
  "modDepth": { type: "modulation", signal: "modulation", node: "lfoGain", field: "gain" }
980
1051
  },
981
1052
  createModule(id, position) {
@@ -985,34 +1056,41 @@ var autoPanDescriptor = /* @__PURE__ */ defineDescriptor({
985
1056
  const input = ctx.createGain();
986
1057
  const panner = ctx.createStereoPanner();
987
1058
  const output = ctx.createGain();
988
- 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];
989
1066
  const lfoGain = ctx.createGain();
990
- lfo.type = module.shape;
991
- lfo.frequency.value = module.rate;
992
1067
  lfoGain.gain.value = module.depth;
993
- lfo.connect(lfoGain);
1068
+ _ws.connect(lfoGain);
994
1069
  lfoGain.connect(panner.pan);
995
- lfo.start();
996
1070
  input.connect(panner);
997
1071
  panner.connect(output);
998
- return { input, output, lfo, lfoGain };
1072
+ return { input, output, _ws, _freqSrc, lfoGain };
999
1073
  },
1000
1074
  updateParam(nodes, _m, param, value) {
1001
1075
  if (param === "rate") {
1002
- nodes.lfo.frequency.value = value;
1076
+ nodes._freqSrc.offset.value = value;
1003
1077
  } else if (param === "depth") {
1004
1078
  nodes.lfoGain.gain.value = value;
1005
1079
  } else if (param === "shape") {
1006
- nodes.lfo.type = value;
1080
+ nodes._ws.parameters.get("shape").value = SHAPE_VALUE[value];
1007
1081
  }
1008
1082
  },
1009
1083
  dispose(nodes) {
1010
- nodes.lfo.stop();
1084
+ nodes._freqSrc.stop();
1011
1085
  }
1012
1086
  });
1013
1087
 
1014
1088
  // src/lib-runtime/generated/modules/auto-pan.wrapper.ts
1015
- var AutoPan = /* @__PURE__ */ defineModule(autoPanDescriptor);
1089
+ var AutoPan = /* @__PURE__ */ defineModule(autoPanDescriptor, {
1090
+ worklets: {
1091
+ "waveshaper": waveshaper_default
1092
+ }
1093
+ });
1016
1094
 
1017
1095
  // src/lib/dsp/mix.ts
1018
1096
  var CURVE_POINTS = 2049;
@@ -1064,7 +1142,10 @@ var bitcrusherDescriptor = /* @__PURE__ */ defineDescriptor({
1064
1142
  params: params3,
1065
1143
  ports: {
1066
1144
  "in": { type: "input", signal: "audio", node: "input" },
1067
- "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" }
1068
1149
  },
1069
1150
  createModule(id, position) {
1070
1151
  return { id, type: "bitcrusher", position, ...paramDefaults(params3) };
@@ -1105,6 +1186,8 @@ var Bitcrusher = /* @__PURE__ */ defineModule(bitcrusherDescriptor, {
1105
1186
  });
1106
1187
 
1107
1188
  // src/lib-runtime/generated/modules/chorus.tsx
1189
+ var CENTRE_S = 566e-5;
1190
+ var SWING_S = 4e-3;
1108
1191
  var params4 = {
1109
1192
  rate: { node: "lfo", field: "frequency", unit: "Hz", default: 1.5, min: 0.1, max: 5 },
1110
1193
  depth: { unit: "factor", default: 0.5, min: 0, max: 1 },
@@ -1137,8 +1220,8 @@ var chorusDescriptor = /* @__PURE__ */ defineDescriptor({
1137
1220
  const lfoGain = ctx.createGain();
1138
1221
  const lfoInv = ctx.createGain();
1139
1222
  const merger = ctx.createChannelMerger(2);
1140
- delayL.delayTime.value = 0.02;
1141
- delayR.delayTime.value = 0.02;
1223
+ delayL.delayTime.value = CENTRE_S;
1224
+ delayR.delayTime.value = CENTRE_S;
1142
1225
  const _mix = ctx.createConstantSource();
1143
1226
  _mix.offset.value = module.mix;
1144
1227
  dryGain.gain.value = 1;
@@ -1150,7 +1233,7 @@ var chorusDescriptor = /* @__PURE__ */ defineDescriptor({
1150
1233
  _mix.start();
1151
1234
  lfo.type = "sine";
1152
1235
  lfo.frequency.value = module.rate;
1153
- lfoGain.gain.value = module.depth * 0.01;
1236
+ lfoGain.gain.value = module.depth * SWING_S;
1154
1237
  lfoInv.gain.value = -1;
1155
1238
  lfo.connect(lfoGain);
1156
1239
  lfoGain.connect(delayL.delayTime);
@@ -1174,7 +1257,7 @@ var chorusDescriptor = /* @__PURE__ */ defineDescriptor({
1174
1257
  nodes.lfo.frequency.value = value;
1175
1258
  break;
1176
1259
  case "depth":
1177
- nodes.lfoGain.gain.value = value * 0.01;
1260
+ nodes.lfoGain.gain.value = value * SWING_S;
1178
1261
  break;
1179
1262
  case "mix":
1180
1263
  nodes._mix.offset.value = value;
@@ -1287,62 +1370,59 @@ var CombFilter = /* @__PURE__ */ defineModule(combFilterDescriptor, {
1287
1370
  var params6 = {
1288
1371
  threshold: { node: "comp", field: "threshold", unit: "dB", default: -24, min: -60, max: 0 },
1289
1372
  ratio: { node: "comp", field: "ratio", unit: "factor", default: 4, min: 1, max: 20, scale: "log" },
1290
- 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" },
1291
1374
  release: { node: "comp", field: "release", unit: "s", default: 0.25, min: 0.01, max: 1, scale: "log" },
1292
1375
  knee: { node: "comp", field: "knee", unit: "dB", default: 10, min: 0, max: 40 },
1293
- // makeup is stored in dB but drives a linear gain node (Math.pow(10, v/20)) —
1294
- // not a 1:1 AudioParam mapping, so no node/field.
1295
- 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" }
1296
1380
  };
1297
1381
  var compressorDescriptor = /* @__PURE__ */ defineDescriptor({
1298
1382
  type: "compressor",
1299
1383
  params: params6,
1300
1384
  ports: {
1301
1385
  "in": { type: "input", signal: "audio", node: "comp" },
1302
- "out": { type: "output", signal: "audio", node: "makeup" }
1386
+ "key": { type: "input", signal: "audio", node: "_keyIn" },
1387
+ "out": { type: "output", signal: "audio", node: "comp" }
1303
1388
  },
1304
1389
  createModule(id, position) {
1305
- return { id, type: "compressor", position, ...paramDefaults(params6) };
1390
+ return { id, type: "compressor", position, timeUnit: "tau", ...paramDefaults(params6) };
1306
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.
1307
1394
  createNodes(ctx, module) {
1308
- const comp = ctx.createDynamicsCompressor();
1309
- comp.threshold.value = module.threshold;
1310
- comp.ratio.value = module.ratio;
1311
- comp.attack.value = module.attack;
1312
- comp.release.value = module.release;
1313
- comp.knee.value = module.knee;
1314
- const makeup = ctx.createGain();
1315
- makeup.gain.value = Math.pow(10, (module.makeup || 0) / 20);
1316
- comp.connect(makeup);
1317
- 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 };
1318
1409
  },
1319
1410
  updateParam(nodes, _m, param, value) {
1320
- const v = value;
1321
- switch (param) {
1322
- case "threshold":
1323
- nodes.comp.threshold.value = v;
1324
- break;
1325
- case "ratio":
1326
- nodes.comp.ratio.value = v;
1327
- break;
1328
- case "attack":
1329
- nodes.comp.attack.value = v;
1330
- break;
1331
- case "release":
1332
- nodes.comp.release.value = v;
1333
- break;
1334
- case "knee":
1335
- nodes.comp.knee.value = v;
1336
- break;
1337
- case "makeup":
1338
- nodes.makeup.gain.value = Math.pow(10, v / 20);
1339
- break;
1411
+ if (param === "keySource") {
1412
+ nodes.comp.parameters.get("keyExt").value = value === "external" ? 1 : 0;
1413
+ return;
1340
1414
  }
1415
+ const p = nodes.comp.parameters.get(param);
1416
+ if (p) p.value = value;
1341
1417
  }
1342
1418
  });
1343
1419
 
1344
1420
  // src/lib-runtime/generated/modules/comp.wrapper.ts
1345
- var Compressor = /* @__PURE__ */ defineModule(compressorDescriptor);
1421
+ var Compressor = /* @__PURE__ */ defineModule(compressorDescriptor, {
1422
+ worklets: {
1423
+ "compressor": compressor_default
1424
+ }
1425
+ });
1346
1426
 
1347
1427
  // src/lib-runtime/generated/modules/crossfade.tsx
1348
1428
  var params7 = {
@@ -1469,7 +1549,19 @@ var Delay = /* @__PURE__ */ defineModule(delayDescriptor, {
1469
1549
  });
1470
1550
 
1471
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
+ }
1472
1563
  var params9 = {
1564
+ width: { unit: "factor", default: 1.5, min: 0, max: 2 },
1473
1565
  rate: { node: "lfo", field: "frequency", unit: "Hz", default: 0.5, min: 0.1, max: 5 },
1474
1566
  depth: { unit: "factor", default: 0.3, min: 0, max: 1 }
1475
1567
  };
@@ -1484,6 +1576,24 @@ var dimensionDescriptor = /* @__PURE__ */ defineDescriptor({
1484
1576
  createModule(id, position) {
1485
1577
  return { id, type: "dimension", position, ...paramDefaults(params9) };
1486
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.
1487
1597
  createNodes(ctx, module) {
1488
1598
  const input = ctx.createGain();
1489
1599
  input.channelCount = 2;
@@ -1497,51 +1607,62 @@ var dimensionDescriptor = /* @__PURE__ */ defineDescriptor({
1497
1607
  const lfo = ctx.createOscillator();
1498
1608
  const lfoGainL = ctx.createGain();
1499
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();
1500
1627
  const llGain = ctx.createGain();
1501
1628
  const lrGain = ctx.createGain();
1502
1629
  const rlGain = ctx.createGain();
1503
1630
  const rrGain = ctx.createGain();
1504
- const w = module.width;
1505
- llGain.gain.value = (1 + w) / 2;
1506
- rrGain.gain.value = (1 + w) / 2;
1507
- rlGain.gain.value = (1 - w) / 2;
1508
- lrGain.gain.value = (1 - w) / 2;
1509
- leftDelay.delayTime.value = 0.01;
1510
- rightDelay.delayTime.value = 0.012;
1511
- 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";
1512
1635
  lfo.frequency.value = module.rate;
1513
- lfoGainL.gain.value = module.depth * 2e-3;
1514
- lfoGainR.gain.value = -module.depth * 2e-3;
1636
+ lfoGainL.gain.value = module.depth * SWING_S2;
1637
+ lfoGainR.gain.value = -module.depth * SWING_S2;
1515
1638
  lfo.connect(lfoGainL);
1516
1639
  lfo.connect(lfoGainR);
1517
1640
  lfoGainL.connect(leftDelay.delayTime);
1518
1641
  lfoGainR.connect(rightDelay.delayTime);
1519
1642
  lfo.start();
1520
1643
  input.connect(splitter);
1644
+ splitter.connect(aSum, 0);
1645
+ splitter.connect(bSum, 1);
1521
1646
  splitter.connect(leftDelay, 0);
1522
1647
  splitter.connect(rightDelay, 1);
1523
- leftDelay.connect(llGain);
1524
- leftDelay.connect(lrGain);
1525
- rightDelay.connect(rrGain);
1526
- rightDelay.connect(rlGain);
1527
- llGain.connect(merger, 0, 0);
1528
- rlGain.connect(merger, 0, 0);
1529
- lrGain.connect(merger, 0, 1);
1530
- 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);
1531
1657
  merger.connect(output);
1532
- 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 };
1533
1659
  },
1534
1660
  updateParam(nodes, _m, param, value) {
1535
- if (param === "width") {
1536
- const w = value;
1537
- nodes.llGain.gain.value = (1 + w) / 2;
1538
- nodes.rrGain.gain.value = (1 + w) / 2;
1539
- nodes.rlGain.gain.value = (1 - w) / 2;
1540
- nodes.lrGain.gain.value = (1 - w) / 2;
1541
- } 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;
1542
1663
  else if (param === "depth") {
1543
- nodes.lfoGainL.gain.value = value * 2e-3;
1544
- nodes.lfoGainR.gain.value = -value * 2e-3;
1664
+ nodes.lfoGainL.gain.value = value * SWING_S2;
1665
+ nodes.lfoGainR.gain.value = -value * SWING_S2;
1545
1666
  }
1546
1667
  },
1547
1668
  dispose(nodes) {
@@ -1613,12 +1734,10 @@ var distortionDescriptor = /* @__PURE__ */ defineDescriptor({
1613
1734
  const output = ctx.createGain();
1614
1735
  const dryGain = ctx.createGain();
1615
1736
  const wetGain = ctx.createGain();
1616
- const waveshaper = ctx.createWaveShaper();
1737
+ const waveshaper = createShaperNode(ctx, makeDistortionCurve(module.distortionType, module.drive));
1617
1738
  const makeup = ctx.createGain();
1618
1739
  const dcBlock = ctx.createBiquadFilter();
1619
1740
  const toneFilter = ctx.createBiquadFilter();
1620
- waveshaper.curve = makeDistortionCurve(module.distortionType, module.drive);
1621
- waveshaper.oversample = "4x";
1622
1741
  makeup.gain.value = computeMakeup(module.distortionType, module.drive);
1623
1742
  dcBlock.type = "highpass";
1624
1743
  dcBlock.frequency.value = 10;
@@ -1627,9 +1746,9 @@ var distortionDescriptor = /* @__PURE__ */ defineDescriptor({
1627
1746
  toneFilter.frequency.value = module.frequency;
1628
1747
  toneFilter.Q.value = 0.707;
1629
1748
  const _mix = createMix(ctx, dryGain, wetGain, module.mix, "linear");
1630
- input.connect(dryGain);
1631
1749
  input.connect(waveshaper);
1632
- waveshaper.connect(makeup);
1750
+ waveshaper.connect(dryGain, 1);
1751
+ waveshaper.connect(makeup, 0);
1633
1752
  makeup.connect(dcBlock);
1634
1753
  dcBlock.connect(toneFilter);
1635
1754
  toneFilter.connect(wetGain);
@@ -1639,7 +1758,7 @@ var distortionDescriptor = /* @__PURE__ */ defineDescriptor({
1639
1758
  },
1640
1759
  updateParam(nodes, module, param, value) {
1641
1760
  if (param === "drive" || param === "distortionType") {
1642
- nodes.waveshaper.curve = makeDistortionCurve(module.distortionType, module.drive);
1761
+ nodes.waveshaper.port.postMessage({ curve: makeDistortionCurve(module.distortionType, module.drive) });
1643
1762
  nodes.makeup.gain.value = computeMakeup(module.distortionType, module.drive);
1644
1763
  }
1645
1764
  if (param === "frequency") {
@@ -1652,7 +1771,11 @@ var distortionDescriptor = /* @__PURE__ */ defineDescriptor({
1652
1771
  });
1653
1772
 
1654
1773
  // src/lib-runtime/generated/modules/distortion.wrapper.ts
1655
- var Distortion = /* @__PURE__ */ defineModule(distortionDescriptor);
1774
+ var Distortion = /* @__PURE__ */ defineModule(distortionDescriptor, {
1775
+ worklets: {
1776
+ "shaper": shaper_default
1777
+ }
1778
+ });
1656
1779
 
1657
1780
  // src/lib-runtime/generated/modules/ducker.tsx
1658
1781
  var params11 = {
@@ -1954,22 +2077,21 @@ var exciterDescriptor = /* @__PURE__ */ defineDescriptor({
1954
2077
  const dryGain = ctx.createGain();
1955
2078
  const wetGain = ctx.createGain();
1956
2079
  const highpass = ctx.createBiquadFilter();
1957
- const saturation = ctx.createWaveShaper();
2080
+ const saturation = createShaperNode(ctx, makeExciterCurve(module.drive));
1958
2081
  const highshelf = ctx.createBiquadFilter();
1959
2082
  highpass.type = "highpass";
1960
2083
  highpass.frequency.value = module.frequency;
1961
2084
  highpass.Q.value = 0.7;
1962
- saturation.curve = makeExciterCurve(module.drive);
1963
- saturation.oversample = "2x";
1964
2085
  highshelf.type = "highshelf";
1965
2086
  highshelf.frequency.value = module.frequency;
1966
2087
  highshelf.gain.value = 6 + module.drive * 6;
1967
2088
  dryGain.gain.value = 1;
1968
2089
  wetGain.gain.value = module.mix;
1969
- input.connect(dryGain);
2090
+ input.connect(saturation, 0, 1);
2091
+ saturation.connect(dryGain, 1);
1970
2092
  input.connect(highpass);
1971
- highpass.connect(saturation);
1972
- saturation.connect(highshelf);
2093
+ highpass.connect(saturation, 0, 0);
2094
+ saturation.connect(highshelf, 0);
1973
2095
  highshelf.connect(wetGain);
1974
2096
  dryGain.connect(output);
1975
2097
  wetGain.connect(output);
@@ -1977,7 +2099,7 @@ var exciterDescriptor = /* @__PURE__ */ defineDescriptor({
1977
2099
  },
1978
2100
  updateParam(nodes, _m, param, value) {
1979
2101
  if (param === "drive") {
1980
- nodes.saturation.curve = makeExciterCurve(value);
2102
+ nodes.saturation.port.postMessage({ curve: makeExciterCurve(value) });
1981
2103
  nodes.highshelf.gain.value = 6 + value * 6;
1982
2104
  } else if (param === "frequency") {
1983
2105
  nodes.highpass.frequency.value = value;
@@ -1989,7 +2111,11 @@ var exciterDescriptor = /* @__PURE__ */ defineDescriptor({
1989
2111
  });
1990
2112
 
1991
2113
  // src/lib-runtime/generated/modules/exciter.wrapper.ts
1992
- var Exciter = /* @__PURE__ */ defineModule(exciterDescriptor);
2114
+ var Exciter = /* @__PURE__ */ defineModule(exciterDescriptor, {
2115
+ worklets: {
2116
+ "shaper": shaper_default
2117
+ }
2118
+ });
1993
2119
 
1994
2120
  // src/lib-runtime/generated/modules/flanger.tsx
1995
2121
  var params16 = {
@@ -2057,7 +2183,7 @@ var Flanger = /* @__PURE__ */ defineModule(flangerDescriptor, {
2057
2183
  // src/lib-runtime/generated/modules/formant.tsx
2058
2184
  var params17 = {
2059
2185
  morph: { unit: "factor", default: 0, min: 0, max: 5 },
2060
- 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" },
2061
2187
  mix: { unit: "factor", default: 1, min: 0, max: 1 }
2062
2188
  };
2063
2189
  var formantDescriptor = /* @__PURE__ */ defineDescriptor({
@@ -2076,75 +2202,54 @@ var formantDescriptor = /* @__PURE__ */ defineDescriptor({
2076
2202
  const output = ctx.createGain();
2077
2203
  const dryGain = ctx.createGain();
2078
2204
  const wetGain = ctx.createGain();
2079
- const f1 = ctx.createBiquadFilter();
2080
- const f2 = ctx.createBiquadFilter();
2081
- const f3 = ctx.createBiquadFilter();
2082
- const f4 = ctx.createBiquadFilter();
2083
- const f5 = ctx.createBiquadFilter();
2084
- 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
+ });
2085
2212
  const formantWorklet = createFormantNode(ctx);
2086
- 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;
2087
2216
  const vowelCV = ctx.createGain();
2088
2217
  vowelCV.gain.value = 1;
2089
2218
  vowelCV.connect(formantWorklet);
2090
- const splitter = ctx.createChannelSplitter(5);
2219
+ const splitter = ctx.createChannelSplitter(11);
2091
2220
  formantWorklet.connect(splitter);
2092
- splitter.connect(f1.frequency, 0);
2093
- splitter.connect(f2.frequency, 1);
2094
- splitter.connect(f3.frequency, 2);
2095
- splitter.connect(f4.frequency, 3);
2096
- splitter.connect(f5.frequency, 4);
2097
- f1.frequency.value = 0;
2098
- f2.frequency.value = 0;
2099
- f3.frequency.value = 0;
2100
- f4.frequency.value = 0;
2101
- f5.frequency.value = 0;
2102
- f1.type = "bandpass";
2103
- f2.type = "bandpass";
2104
- f3.type = "bandpass";
2105
- f4.type = "bandpass";
2106
- f5.type = "bandpass";
2107
- f1.Q.value = module.resonance * 0.5;
2108
- f2.Q.value = module.resonance * 0.4;
2109
- f3.Q.value = module.resonance * 0.3;
2110
- f4.Q.value = module.resonance * 0.25;
2111
- 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));
2112
2237
  dryGain.gain.value = 1 - module.mix;
2113
2238
  wetGain.gain.value = module.mix;
2114
- formantMix.gain.value = 2;
2115
- const f4Gain = ctx.createGain();
2116
- const f5Gain = ctx.createGain();
2117
- f4Gain.gain.value = 0.5;
2118
- f5Gain.gain.value = 0.35;
2119
2239
  input.connect(dryGain);
2120
- input.connect(f1);
2121
- input.connect(f2);
2122
- input.connect(f3);
2123
- input.connect(f4);
2124
- input.connect(f5);
2125
- f1.connect(formantMix);
2126
- f2.connect(formantMix);
2127
- f3.connect(formantMix);
2128
- f4.connect(f4Gain);
2129
- f5.connect(f5Gain);
2130
- f4Gain.connect(formantMix);
2131
- f5Gain.connect(formantMix);
2132
2240
  formantMix.connect(wetGain);
2133
2241
  dryGain.connect(output);
2134
2242
  wetGain.connect(output);
2243
+ const [f1, f2, f3, f4, f5] = bands;
2135
2244
  const _alive = createKeepAlive(ctx, f1, f2, f3, f4, f5);
2136
- 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 };
2137
2246
  },
2138
2247
  updateParam(nodes, _module, param, value) {
2248
+ const wp = nodes.formantWorklet.parameters;
2139
2249
  if (param === "morph") {
2140
- nodes.formantWorklet.parameters.get("baseMorph").value = value;
2250
+ wp.get("baseMorph").value = value;
2141
2251
  } else if (param === "resonance") {
2142
- const r = value;
2143
- nodes.f1.Q.value = r * 0.5;
2144
- nodes.f2.Q.value = r * 0.4;
2145
- nodes.f3.Q.value = r * 0.3;
2146
- nodes.f4.Q.value = r * 0.25;
2147
- nodes.f5.Q.value = r * 0.2;
2252
+ wp.get("resonance").value = value;
2148
2253
  } else if (param === "mix") {
2149
2254
  nodes.dryGain.gain.value = 1 - value;
2150
2255
  nodes.wetGain.gain.value = value;
@@ -2211,35 +2316,22 @@ var FrequencyShifter = /* @__PURE__ */ defineModule(freqShifterDescriptor, {
2211
2316
 
2212
2317
  // src/lib-runtime/generated/modules/gate.tsx
2213
2318
  var params19 = {
2214
- // threshold/range rebuild the gateShaper curve — not a 1:1 AudioParam
2215
- // write — so no node/field.
2216
- threshold: { unit: "dB", default: -40, min: -60, max: 0 },
2217
- range: { unit: "factor", default: 0.8, min: 0, max: 1 },
2218
- attack: { node: "envFollower", field: "attack", unit: "s", default: 5e-3, min: 1e-3, max: 0.05, scale: "log" },
2219
- 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" }
2220
2328
  };
2221
- function makeGateCurve(threshold, range) {
2222
- const threshLinear = Math.pow(10, threshold / 20);
2223
- const kneeLow = Math.pow(10, (threshold - 2) / 20);
2224
- const closed = 1 - range;
2225
- const gsLen = 32768;
2226
- const gsCurve = new Float32Array(gsLen);
2227
- for (let i = 0; i < gsLen; i++) {
2228
- const x = i / (gsLen - 1) * 2 - 1;
2229
- const env = Math.max(0, x);
2230
- let g;
2231
- if (env >= threshLinear) g = 1;
2232
- else if (env <= kneeLow) g = closed;
2233
- else g = closed + (1 - closed) * ((env - kneeLow) / (threshLinear - kneeLow));
2234
- gsCurve[i] = g;
2235
- }
2236
- return gsCurve;
2237
- }
2238
2329
  var gateDescriptor = /* @__PURE__ */ defineDescriptor({
2239
2330
  type: "gate",
2240
2331
  params: params19,
2241
2332
  ports: {
2242
2333
  "in": { type: "input", signal: "audio", node: "input" },
2334
+ "key": { type: "input", signal: "audio", node: "_keyIn" },
2243
2335
  "out": { type: "output", signal: "audio", node: "output" }
2244
2336
  },
2245
2337
  createModule(id, position) {
@@ -2248,37 +2340,33 @@ var gateDescriptor = /* @__PURE__ */ defineDescriptor({
2248
2340
  createNodes(ctx, module) {
2249
2341
  const input = ctx.createGain();
2250
2342
  const output = ctx.createGain();
2251
- const gateGain = ctx.createGain();
2252
- const envFollower = createEnvFollowerNode(ctx);
2253
- envFollower.parameters.get("attack").value = module.attack;
2254
- envFollower.parameters.get("release").value = module.release;
2255
- const gateShaper = ctx.createWaveShaper();
2256
- gateShaper.curve = makeGateCurve(module.threshold, module.range);
2257
- gateGain.gain.value = 0;
2258
- input.connect(envFollower);
2259
- envFollower.connect(gateShaper);
2260
- gateShaper.connect(gateGain.gain);
2261
- input.connect(gateGain);
2262
- gateGain.connect(output);
2263
- 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 };
2264
2356
  },
2265
2357
  updateParam(nodes, module, param, value) {
2266
- if (param === "threshold" || param === "range") {
2267
- const thresh = param === "threshold" ? value : module.threshold;
2268
- const range = param === "range" ? value : module.range;
2269
- nodes.gateShaper.curve = makeGateCurve(thresh, range);
2270
- } else if (param === "attack") {
2271
- nodes.envFollower.parameters.get("attack").value = value;
2272
- } else if (param === "release") {
2273
- nodes.envFollower.parameters.get("release").value = value;
2358
+ if (param === "keySource") {
2359
+ nodes.gate.parameters.get("keyExt").value = value === "external" ? 1 : 0;
2360
+ return;
2274
2361
  }
2362
+ nodes.gate.parameters.get(param).value = value;
2275
2363
  }
2276
2364
  });
2277
2365
 
2278
2366
  // src/lib-runtime/generated/modules/gate.wrapper.ts
2279
2367
  var Gate = /* @__PURE__ */ defineModule(gateDescriptor, {
2280
2368
  worklets: {
2281
- "env-follower": env_follower_default
2369
+ "gate": gate_default
2282
2370
  }
2283
2371
  });
2284
2372
 
@@ -2403,7 +2491,7 @@ var HaasWidener = /* @__PURE__ */ defineModule(haasDescriptor);
2403
2491
  var params22 = {
2404
2492
  rate: { node: "lfo", field: "frequency", unit: "Hz", default: 4, min: 1, max: 10 },
2405
2493
  depth: { unit: "factor", default: 0.7, min: 0, max: 1 },
2406
- 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 }
2407
2495
  };
2408
2496
  var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2409
2497
  type: "harmonic-trem",
@@ -2412,7 +2500,7 @@ var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2412
2500
  "in": { type: "input", signal: "audio", node: "input" },
2413
2501
  "out": { type: "output", signal: "audio", node: "output" },
2414
2502
  "modRate": { type: "modulation", signal: "modulation", node: "lfo", field: "frequency" },
2415
- "modCrossover": { type: "modulation", signal: "modulation", node: "lowpass", field: "frequency" }
2503
+ "modCrossover": { type: "modulation", signal: "modulation", node: "_xover", field: "offset" }
2416
2504
  },
2417
2505
  createModule(id, position) {
2418
2506
  return { id, type: "harmonic-trem", position, ...paramDefaults(params22) };
@@ -2421,8 +2509,9 @@ var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2421
2509
  const input = ctx.createGain();
2422
2510
  const output = ctx.createGain();
2423
2511
  const lowpass = ctx.createBiquadFilter();
2424
- const lpInvert = ctx.createGain();
2425
- const highBand = ctx.createGain();
2512
+ const highpass = ctx.createBiquadFilter();
2513
+ const hpInvert = ctx.createGain();
2514
+ const _xover = ctx.createConstantSource();
2426
2515
  const lowGain = ctx.createGain();
2427
2516
  const highGain = ctx.createGain();
2428
2517
  const lfo = ctx.createOscillator();
@@ -2430,10 +2519,14 @@ var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2430
2519
  const lfoGainHigh = ctx.createGain();
2431
2520
  const lfoInvert = ctx.createGain();
2432
2521
  lowpass.type = "lowpass";
2433
- lowpass.frequency.value = module.crossover;
2434
- lowpass.Q.value = -6.0206;
2435
- lpInvert.gain.value = -1;
2436
- 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;
2437
2530
  lfo.type = "sine";
2438
2531
  lfo.frequency.value = module.rate;
2439
2532
  lfoGainLow.gain.value = module.depth * 0.5;
@@ -2442,20 +2535,20 @@ var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2442
2535
  lowGain.gain.value = 1;
2443
2536
  highGain.gain.value = 1;
2444
2537
  input.connect(lowpass);
2445
- input.connect(highBand);
2446
- lowpass.connect(lpInvert);
2447
- lpInvert.connect(highBand);
2538
+ input.connect(highpass);
2539
+ highpass.connect(hpInvert);
2448
2540
  lfo.connect(lfoGainLow);
2449
2541
  lfo.connect(lfoInvert);
2450
2542
  lfoInvert.connect(lfoGainHigh);
2451
2543
  lfoGainLow.connect(lowGain.gain);
2452
2544
  lfoGainHigh.connect(highGain.gain);
2453
2545
  lowpass.connect(lowGain);
2454
- highBand.connect(highGain);
2546
+ hpInvert.connect(highGain);
2455
2547
  lowGain.connect(output);
2456
2548
  highGain.connect(output);
2457
2549
  lfo.start();
2458
- return { input, output, lfo, lfoGainLow, lfoGainHigh, lowpass, lpInvert, highBand };
2550
+ _xover.start();
2551
+ return { input, output, lfo, lfoGainLow, lfoGainHigh, lowpass, highpass, hpInvert, _xover };
2459
2552
  },
2460
2553
  updateParam(nodes, _m, param, value) {
2461
2554
  if (param === "rate") nodes.lfo.frequency.value = value;
@@ -2463,10 +2556,11 @@ var harmonicTremDescriptor = /* @__PURE__ */ defineDescriptor({
2463
2556
  nodes.lfoGainLow.gain.value = value * 0.5;
2464
2557
  nodes.lfoGainHigh.gain.value = value * 0.5;
2465
2558
  } else if (param === "crossover") {
2466
- nodes.lowpass.frequency.value = value;
2559
+ nodes._xover.offset.value = value;
2467
2560
  }
2468
2561
  },
2469
2562
  dispose(nodes) {
2563
+ nodes._xover.stop();
2470
2564
  nodes.lfo.stop();
2471
2565
  }
2472
2566
  });
@@ -2604,21 +2698,27 @@ var Lfo = /* @__PURE__ */ defineModule(lfoDescriptor, {
2604
2698
  });
2605
2699
 
2606
2700
  // src/lib-runtime/generated/modules/lo-fi.tsx
2607
- function makeLoFiSaturationCurve(amount) {
2608
- const samples = 44100;
2609
- const curve = new Float32Array(samples);
2610
- const k = 1 + amount * 5;
2611
- for (let i = 0; i < samples; i++) {
2612
- const x = i * 2 / samples - 1;
2613
- 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);
2614
2714
  }
2615
- return curve;
2715
+ return c;
2616
2716
  }
2617
2717
  var params25 = {
2618
2718
  drive: { unit: "factor", default: 0.3, min: 0, max: 1 },
2619
2719
  cutoff: { node: "lowpass", field: "frequency", unit: "Hz", default: 4e3, min: 500, max: 8e3 },
2620
- noise: { unit: "factor", default: 0.2, min: 0, max: 1 },
2621
- 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 }
2622
2722
  };
2623
2723
  var loFiDescriptor = /* @__PURE__ */ defineDescriptor({
2624
2724
  type: "lo-fi",
@@ -2634,73 +2734,78 @@ var loFiDescriptor = /* @__PURE__ */ defineDescriptor({
2634
2734
  createNodes(ctx, module) {
2635
2735
  const input = ctx.createGain();
2636
2736
  const output = ctx.createGain();
2637
- 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;
2638
2744
  const lowpass = ctx.createBiquadFilter();
2639
- const noiseGain = ctx.createGain();
2640
- const wobbleLfo = ctx.createOscillator();
2641
- const wobbleGain = ctx.createGain();
2642
- saturation.curve = makeLoFiSaturationCurve(module.drive);
2643
- saturation.oversample = "2x";
2644
2745
  lowpass.type = "lowpass";
2645
2746
  lowpass.frequency.value = module.cutoff;
2646
2747
  lowpass.Q.value = 0.7;
2647
- const bufferSize = 2 * ctx.sampleRate;
2648
- const noiseBuffer2 = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
2649
- const data = noiseBuffer2.getChannelData(0);
2650
- const rand = noiseSource(NOISE_SEEDS.loFiCrackle);
2651
- let b0 = 0, b1 = 0, b2 = 0;
2652
- for (let i = 0; i < bufferSize; i++) {
2653
- const white = rand();
2654
- b0 = 0.99765 * b0 + white * 0.099046;
2655
- b1 = 0.963 * b1 + white * 0.2965164;
2656
- b2 = 0.57 * b2 + white * 1.0526913;
2657
- data[i] = (b0 + b1 + b2) * 0.1;
2658
- }
2659
- const noiseSrc = ctx.createBufferSource();
2660
- noiseSrc.buffer = noiseBuffer2;
2661
- noiseSrc.loop = true;
2662
- noiseGain.gain.value = module.noise * 0.05;
2663
- noiseSrc.connect(noiseGain);
2664
- noiseSrc.start();
2665
- const wobbleDelay = ctx.createDelay(0.05);
2666
- 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();
2667
2753
  wobbleLfo.type = "sine";
2668
2754
  wobbleLfo.frequency.value = 0.73;
2669
- wobbleGain.gain.value = module.wobble * 2e-3;
2670
- wobbleLfo.connect(wobbleGain);
2671
- 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();
2672
2765
  wobbleLfo.start();
2673
- input.connect(saturation);
2674
- 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);
2675
2774
  lowpass.connect(wobbleDelay);
2676
2775
  wobbleDelay.connect(output);
2677
2776
  noiseGain.connect(output);
2678
- 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 };
2679
2778
  },
2680
2779
  updateParam(nodes, _m, param, value) {
2681
2780
  if (param === "drive") {
2682
- nodes.saturation.curve = makeLoFiSaturationCurve(value);
2781
+ nodes.saturation.port.postMessage({ curve: loFiCurve(value) });
2683
2782
  } else if (param === "cutoff") {
2684
2783
  nodes.lowpass.frequency.value = value;
2685
2784
  } else if (param === "noise") {
2686
- nodes.noiseGain.gain.value = value * 0.05;
2785
+ nodes.noiseGain.gain.value = value;
2687
2786
  } else if (param === "wobble") {
2688
- nodes.wobbleGain.gain.value = value * 2e-3;
2787
+ nodes.wobbleAmt.offset.value = value;
2689
2788
  }
2690
2789
  },
2691
2790
  dispose(nodes) {
2692
2791
  nodes.wobbleLfo.stop();
2693
- nodes.noiseSrc.stop();
2792
+ nodes.wobbleAmt.stop();
2694
2793
  }
2695
2794
  });
2696
2795
 
2697
2796
  // src/lib-runtime/generated/modules/lo-fi.wrapper.ts
2698
- 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
+ });
2699
2803
 
2700
2804
  // src/lib-runtime/generated/modules/mid-side.tsx
2701
2805
  var params26 = {
2702
2806
  midGain: { node: "midGainNode", field: "gain", unit: "factor", default: 1, min: 0, max: 2 },
2703
- 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 }
2704
2809
  };
2705
2810
  var midSideDescriptor = /* @__PURE__ */ defineDescriptor({
2706
2811
  type: "mid-side",
@@ -3143,9 +3248,58 @@ function dampingToQ(damping) {
3143
3248
  const d = Math.min(1, Math.max(0, Number.isFinite(damping) ? damping : 0.5));
3144
3249
  return MODAL_Q_MAX * Math.pow(MODAL_Q_MIN / MODAL_Q_MAX, d);
3145
3250
  }
3251
+ function modeT60(q, hz2) {
3252
+ return T60_PER_Q_HZ * q / hz2;
3253
+ }
3146
3254
  function modalMakeup(q) {
3147
3255
  return Math.sqrt(Math.max(1, q));
3148
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));
3149
3303
 
3150
3304
  // src/lib-runtime/generated/modules/resonator.tsx
3151
3305
  var params31 = {
@@ -3153,12 +3307,13 @@ var params31 = {
3153
3307
  offset: { node: "_root", field: "offset", default: 57, min: -48, max: 108, unit: "semitone" },
3154
3308
  structure: { unit: "factor", default: 0, min: 0, max: 1 },
3155
3309
  damping: { unit: "factor", default: 0.5, min: 0, max: 1 },
3156
- 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 }
3157
3311
  };
3158
3312
  function partialOffset(partial, structure) {
3159
3313
  return 12 * (1 + structure) * Math.log2(partial);
3160
3314
  }
3161
3315
  var patchCount2 = /* @__PURE__ */ new WeakMap();
3316
+ var shape = /* @__PURE__ */ new WeakMap();
3162
3317
  var resonatorDescriptor = /* @__PURE__ */ defineDescriptor({
3163
3318
  type: "resonator",
3164
3319
  params: params31,
@@ -3186,7 +3341,17 @@ var resonatorDescriptor = /* @__PURE__ */ defineDescriptor({
3186
3341
  const partialGains = [];
3187
3342
  const voct = [];
3188
3343
  const q = dampingToQ(module.damping);
3189
- 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);
3190
3355
  for (let i = 1; i <= 5; i++) {
3191
3356
  const v = createVoctFreqNode(ctx);
3192
3357
  v.parameters.get("transpose").value = partialOffset(i, module.structure);
@@ -3200,9 +3365,9 @@ var resonatorDescriptor = /* @__PURE__ */ defineDescriptor({
3200
3365
  voct.push(v);
3201
3366
  input.connect(filter);
3202
3367
  const partialGain = ctx.createGain();
3203
- partialGain.gain.value = makeup / Math.sqrt(i);
3368
+ partialGain.gain.value = modalPartialGain(i, q, module.structure);
3204
3369
  filter.connect(partialGain);
3205
- partialGain.connect(wetGain);
3370
+ partialGain.connect(_track);
3206
3371
  partialGains.push(partialGain);
3207
3372
  }
3208
3373
  const _alive = createKeepAlive(ctx, ...filters);
@@ -3219,6 +3384,9 @@ var resonatorDescriptor = /* @__PURE__ */ defineDescriptor({
3219
3384
  _root,
3220
3385
  _pitch,
3221
3386
  _alive,
3387
+ _halfPitch,
3388
+ _trackGain,
3389
+ _track,
3222
3390
  v1: voct[0],
3223
3391
  v2: voct[1],
3224
3392
  v3: voct[2],
@@ -3244,19 +3412,20 @@ var resonatorDescriptor = /* @__PURE__ */ defineDescriptor({
3244
3412
  if (!patchCount2.get(nodes)) nodes._root.offset.value = value;
3245
3413
  } else if (param === "transpose") {
3246
3414
  if (patchCount2.get(nodes)) nodes._root.offset.value = value;
3247
- } else if (param === "structure") {
3248
- const voct = [nodes.v1, nodes.v2, nodes.v3, nodes.v4, nodes.v5];
3249
- for (let i = 1; i < 5; i++) {
3250
- 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;
3251
3426
  }
3252
- } else if (param === "damping") {
3253
- const q = dampingToQ(value);
3254
- const makeup = modalMakeup(q);
3255
3427
  const gains = [nodes.g1, nodes.g2, nodes.g3, nodes.g4, nodes.g5];
3256
- for (let i = 0; i < 5; i++) {
3257
- filters[i].Q.value = q;
3258
- gains[i].gain.value = makeup / Math.sqrt(i + 1);
3259
- }
3428
+ for (let i = 0; i < 5; i++) gains[i].gain.value = modalPartialGain(i + 1, st.q, st.structure);
3260
3429
  } else if (param === "mix") {
3261
3430
  nodes._mix.offset.value = value;
3262
3431
  }
@@ -3295,13 +3464,30 @@ var Resonator = /* @__PURE__ */ defineModule(resonatorDescriptor, {
3295
3464
  });
3296
3465
 
3297
3466
  // src/lib/dsp/fdnTank.ts
3298
- 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
+ ];
3299
3485
  function tankEnergyGain500(rt60, size = 1) {
3300
3486
  let g2 = 0;
3301
- for (const ms2 of COMB_TIMES_MS) g2 += Math.pow(10, -6 * (ms2 / 1e3) * size / Math.max(rt60, 0.05));
3302
- 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;
3303
3489
  const loss = 1 - g2;
3304
- 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);
3305
3491
  }
3306
3492
  function createFdnTank(ctx, opts) {
3307
3493
  const input = ctx.createGain();
@@ -3481,10 +3667,10 @@ var RingModulator = /* @__PURE__ */ defineModule(ringModDescriptor, {
3481
3667
 
3482
3668
  // src/lib-runtime/generated/modules/rotary.tsx
3483
3669
  var CROSSOVER_HZ = 800;
3484
- var BUTTERWORTH_Q_DB = -3.0103;
3670
+ var BUTTERWORTH_Q_DB2 = -3.0103;
3485
3671
  var DRUM_RATIO = 0.85;
3486
- var HORN = { baseDelay: 3e-3, dopplerSec: 44e-5, ampBase: 0.7, ampDepth: 0.3, phaseDeg: 0, tauUp: 0.35, tauDown: 0.55 };
3487
- 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 };
3488
3674
  var params34 = {
3489
3675
  speed: { kind: "enum", options: ["slow", "fast"], default: "slow" },
3490
3676
  slowRate: { unit: "Hz", default: 0.8, min: 0.5, max: 2 },
@@ -3517,7 +3703,7 @@ var rotaryDescriptor = /* @__PURE__ */ defineDescriptor({
3517
3703
  const f = ctx.createBiquadFilter();
3518
3704
  f.type = type;
3519
3705
  f.frequency.value = CROSSOVER_HZ;
3520
- f.Q.value = BUTTERWORTH_Q_DB;
3706
+ f.Q.value = BUTTERWORTH_Q_DB2;
3521
3707
  return f;
3522
3708
  };
3523
3709
  const lp1 = biquad("lowpass"), lp2 = biquad("lowpass");
@@ -3610,22 +3796,24 @@ var Rotary = /* @__PURE__ */ defineModule(rotaryDescriptor);
3610
3796
 
3611
3797
  // src/lib-runtime/generated/modules/saturator.tsx
3612
3798
  var params35 = {
3613
- // drive feeds both driveGain.gain (1 + drive*4) and the waveshaper curve —
3614
- // 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.
3615
3801
  drive: { unit: "factor", default: 0.5, min: 0, max: 1 },
3616
3802
  // warmthFilter.gain = warmth * 6 — a scaled conversion, so no node/field.
3617
3803
  warmth: { unit: "factor", default: 0.5, min: 0, max: 1 },
3618
3804
  mix: { node: "_mix", field: "offset", unit: "factor", default: 1, min: 0, max: 1 }
3619
3805
  };
3620
- function makeSaturationCurve(amount) {
3621
- const samples = 44100;
3622
- const curve = new Float32Array(samples);
3623
- const k = 1 + amount * 10;
3624
- for (let i = 0; i < samples; i++) {
3625
- const x = i * 2 / samples - 1;
3626
- 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;
3627
3815
  }
3628
- return curve;
3816
+ return c;
3629
3817
  }
3630
3818
  var saturatorDescriptor = /* @__PURE__ */ defineDescriptor({
3631
3819
  type: "saturator",
@@ -3642,29 +3830,29 @@ var saturatorDescriptor = /* @__PURE__ */ defineDescriptor({
3642
3830
  const output = ctx.createGain();
3643
3831
  const dryGain = ctx.createGain();
3644
3832
  const wetGain = ctx.createGain();
3645
- const driveGain = ctx.createGain();
3646
- 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;
3647
3838
  const warmthFilter = ctx.createBiquadFilter();
3648
- driveGain.gain.value = 1 + module.drive * 4;
3649
- waveshaper.curve = makeSaturationCurve(module.drive);
3650
- waveshaper.oversample = "2x";
3651
3839
  warmthFilter.type = "lowshelf";
3652
3840
  warmthFilter.frequency.value = 300;
3653
3841
  warmthFilter.gain.value = module.warmth * 6;
3654
3842
  const _mix = createMix(ctx, dryGain, wetGain, module.mix, "linear");
3655
- input.connect(dryGain);
3656
- input.connect(driveGain);
3657
- driveGain.connect(waveshaper);
3658
- waveshaper.connect(warmthFilter);
3843
+ input.connect(preGain);
3844
+ preGain.connect(shaper);
3845
+ shaper.connect(warmthFilter, 0);
3659
3846
  warmthFilter.connect(wetGain);
3847
+ shaper.connect(dryTrim, 1);
3848
+ dryTrim.connect(dryGain);
3660
3849
  dryGain.connect(output);
3661
3850
  wetGain.connect(output);
3662
- return { input, output, driveGain, waveshaper, warmthFilter, dryGain, wetGain, _mix };
3851
+ return { input, output, preGain, shaper, dryTrim, warmthFilter, dryGain, wetGain, _mix };
3663
3852
  },
3664
3853
  updateParam(nodes, _m, param, value) {
3665
3854
  if (param === "drive") {
3666
- nodes.driveGain.gain.value = 1 + value * 4;
3667
- nodes.waveshaper.curve = makeSaturationCurve(value);
3855
+ nodes.shaper.port.postMessage({ curve: saturationCurve(value) });
3668
3856
  } else if (param === "warmth") {
3669
3857
  nodes.warmthFilter.gain.value = value * 6;
3670
3858
  } else if (param === "mix") {
@@ -3674,7 +3862,11 @@ var saturatorDescriptor = /* @__PURE__ */ defineDescriptor({
3674
3862
  });
3675
3863
 
3676
3864
  // src/lib-runtime/generated/modules/saturator.wrapper.ts
3677
- var Saturator = /* @__PURE__ */ defineModule(saturatorDescriptor);
3865
+ var Saturator = /* @__PURE__ */ defineModule(saturatorDescriptor, {
3866
+ worklets: {
3867
+ "shaper": shaper_default
3868
+ }
3869
+ });
3678
3870
 
3679
3871
  // src/lib/dsp/shimmerRegen.ts
3680
3872
  var REGEN_HP_HZ = 300;
@@ -3684,13 +3876,13 @@ var SHIFTER_ENERGY = 0.75;
3684
3876
  var REGEN_DELAY_S = 0.02;
3685
3877
  var SHIFTER_SEEDS = [5370206, 739982445];
3686
3878
  var UNISON_FLOOR = [
3687
- [0.5, 0.68],
3688
- [1, 0.75],
3689
- [2, 0.76],
3690
- [3, 0.8],
3691
- [5, 0.84],
3692
- [7, 0.75],
3693
- [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]
3694
3886
  ];
3695
3887
  var UNISON_PITCH_SPAN = 0.75;
3696
3888
  function unisonFloor(decay) {
@@ -3832,9 +4024,9 @@ function makeSoftLimitCurve(knee, drive) {
3832
4024
  const k = 0.1 + knee * 0.9;
3833
4025
  const driveMul = 1 + drive * 3;
3834
4026
  for (let i = 0; i < samples; i++) {
3835
- const x = (i * 2 / samples - 1) * driveMul;
3836
- if (Math.abs(x) < k) {
3837
- 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));
3838
4030
  } else {
3839
4031
  const sign = x > 0 ? 1 : -1;
3840
4032
  curve[i] = sign * (k + (1 - k) * Math.tanh((Math.abs(x) - k) / (1 - k)));
@@ -3855,25 +4047,28 @@ var softLimitDescriptor = /* @__PURE__ */ defineDescriptor({
3855
4047
  createNodes(ctx, module) {
3856
4048
  const input = ctx.createGain();
3857
4049
  const output = ctx.createGain();
3858
- const waveshaper = ctx.createWaveShaper();
4050
+ const waveshaper = createShaperNode(ctx, makeSoftLimitCurve(module.knee, module.drive), { dry: false });
3859
4051
  const ceiling = ctx.createGain();
3860
- waveshaper.curve = makeSoftLimitCurve(module.knee, module.drive);
3861
- waveshaper.oversample = "2x";
3862
4052
  ceiling.gain.value = Math.pow(10, module.ceiling / 20);
3863
4053
  input.connect(waveshaper);
3864
- waveshaper.connect(ceiling);
4054
+ waveshaper.connect(ceiling, 0);
3865
4055
  ceiling.connect(output);
3866
4056
  return { input, output, waveshaper, ceiling };
3867
4057
  },
3868
4058
  updateParam(nodes, m, param, value) {
3869
- if (param === "drive") nodes.waveshaper.curve = makeSoftLimitCurve(m.knee, value);
3870
- 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) });
3871
4062
  else if (param === "ceiling") nodes.ceiling.gain.value = Math.pow(10, value / 20);
3872
4063
  }
3873
4064
  });
3874
4065
 
3875
4066
  // src/lib-runtime/generated/modules/soft-limit.wrapper.ts
3876
- var SoftLimiter = /* @__PURE__ */ defineModule(softLimitDescriptor);
4067
+ var SoftLimiter = /* @__PURE__ */ defineModule(softLimitDescriptor, {
4068
+ worklets: {
4069
+ "shaper": shaper_default
4070
+ }
4071
+ });
3877
4072
 
3878
4073
  // src/lib-runtime/generated/modules/spring-reverb.tsx
3879
4074
  var params38 = {
@@ -3938,6 +4133,7 @@ var SpringReverb = /* @__PURE__ */ defineModule(springReverbDescriptor, {
3938
4133
  // src/lib-runtime/generated/modules/stereo-spread.tsx
3939
4134
  var params39 = {
3940
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 },
3941
4137
  center: { unit: "Hz", default: 800, min: 200, max: 2e3, scale: "log" }
3942
4138
  };
3943
4139
  var stereoSpreadDescriptor = /* @__PURE__ */ defineDescriptor({
@@ -3953,12 +4149,11 @@ var stereoSpreadDescriptor = /* @__PURE__ */ defineDescriptor({
3953
4149
  createNodes(ctx, module) {
3954
4150
  const input = ctx.createGain();
3955
4151
  const output = ctx.createGain();
3956
- const lowpass = ctx.createBiquadFilter();
4152
+ input.connect(output);
3957
4153
  const highpass = ctx.createBiquadFilter();
3958
- lowpass.type = "lowpass";
3959
- lowpass.frequency.value = module.center;
3960
4154
  highpass.type = "highpass";
3961
4155
  highpass.frequency.value = module.center;
4156
+ highpass.Q.value = -3.0102999566;
3962
4157
  const panLeft = ctx.createStereoPanner();
3963
4158
  const panRight = ctx.createStereoPanner();
3964
4159
  const haasDelay = ctx.createDelay(0.02);
@@ -3967,8 +4162,6 @@ var stereoSpreadDescriptor = /* @__PURE__ */ defineDescriptor({
3967
4162
  haasDelay.delayTime.value = module.spread * 5e-3;
3968
4163
  const wetGain = ctx.createGain();
3969
4164
  wetGain.gain.value = module.width;
3970
- input.connect(lowpass);
3971
- lowpass.connect(output);
3972
4165
  input.connect(highpass);
3973
4166
  highpass.connect(panLeft);
3974
4167
  highpass.connect(haasDelay);
@@ -3976,7 +4169,7 @@ var stereoSpreadDescriptor = /* @__PURE__ */ defineDescriptor({
3976
4169
  panLeft.connect(wetGain);
3977
4170
  panRight.connect(wetGain);
3978
4171
  wetGain.connect(output);
3979
- return { input, output, lowpass, highpass, panLeft, panRight, haasDelay, wetGain };
4172
+ return { input, output, highpass, panLeft, panRight, haasDelay, wetGain };
3980
4173
  },
3981
4174
  updateParam(nodes, _m, param, value) {
3982
4175
  if (param === "spread") {
@@ -3986,7 +4179,6 @@ var stereoSpreadDescriptor = /* @__PURE__ */ defineDescriptor({
3986
4179
  } else if (param === "width") {
3987
4180
  nodes.wetGain.gain.value = value;
3988
4181
  } else if (param === "center") {
3989
- nodes.lowpass.frequency.value = value;
3990
4182
  nodes.highpass.frequency.value = value;
3991
4183
  }
3992
4184
  }
@@ -4483,12 +4675,13 @@ var Vco = /* @__PURE__ */ defineModule(oscillatorDescriptor, {
4483
4675
  });
4484
4676
 
4485
4677
  // src/lib-runtime/generated/modules/wah.tsx
4678
+ var DEPTH_ST = 18;
4486
4679
  var params48 = {
4487
4680
  note: { node: "_cutoffVoct", field: "transpose", unit: "note", default: 79, min: 55, max: 95 },
4488
4681
  // Resonance drives both the filter Q and a separate makeup gain — a split, so it stays set-only.
4489
4682
  resonance: { unit: "factor", default: 8, min: 1, max: 20, scale: "log" },
4490
4683
  rate: { node: "lfo", field: "frequency", unit: "Hz", default: 1, min: 0.1, max: 10, scale: "log" },
4491
- // 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.
4492
4685
  depth: { unit: "factor", default: 0.6, min: 0, max: 1 }
4493
4686
  };
4494
4687
  var wahDescriptor = /* @__PURE__ */ defineDescriptor({
@@ -4522,7 +4715,7 @@ var wahDescriptor = /* @__PURE__ */ defineDescriptor({
4522
4715
  _cutoffVoct.connect(filter.frequency);
4523
4716
  lfo.type = "sine";
4524
4717
  lfo.frequency.value = module.rate;
4525
- lfoGain.gain.value = module.depth * 36;
4718
+ lfoGain.gain.value = module.depth * DEPTH_ST;
4526
4719
  lfo.connect(lfoGain);
4527
4720
  lfoGain.connect(_cutoffVoct);
4528
4721
  lfo.start();
@@ -4541,7 +4734,7 @@ var wahDescriptor = /* @__PURE__ */ defineDescriptor({
4541
4734
  } else if (param === "rate") {
4542
4735
  nodes.lfo.frequency.value = value;
4543
4736
  } else if (param === "depth") {
4544
- nodes.lfoGain.gain.value = value * 36;
4737
+ nodes.lfoGain.gain.value = value * DEPTH_ST;
4545
4738
  }
4546
4739
  },
4547
4740
  dispose(nodes) {
@@ -4599,20 +4792,18 @@ var wavefolderDescriptor = /* @__PURE__ */ defineDescriptor({
4599
4792
  const output = ctx.createGain();
4600
4793
  const dryGain = ctx.createGain();
4601
4794
  const wetGain = ctx.createGain();
4602
- const waveshaper = ctx.createWaveShaper();
4603
- waveshaper.curve = makeWavefoldCurve(module.folds, module.symmetry);
4604
- waveshaper.oversample = "4x";
4795
+ const waveshaper = createShaperNode(ctx, makeWavefoldCurve(module.folds, module.symmetry));
4605
4796
  const _mix = createMix(ctx, dryGain, wetGain, module.mix, "linear");
4606
- input.connect(dryGain);
4607
4797
  input.connect(waveshaper);
4608
- waveshaper.connect(wetGain);
4798
+ waveshaper.connect(dryGain, 1);
4799
+ waveshaper.connect(wetGain, 0);
4609
4800
  dryGain.connect(output);
4610
4801
  wetGain.connect(output);
4611
4802
  return { input, output, waveshaper, dryGain, wetGain, _mix };
4612
4803
  },
4613
4804
  updateParam(nodes, module, param, value) {
4614
4805
  if (param === "folds" || param === "symmetry") {
4615
- nodes.waveshaper.curve = makeWavefoldCurve(module.folds, module.symmetry);
4806
+ nodes.waveshaper.port.postMessage({ curve: makeWavefoldCurve(module.folds, module.symmetry) });
4616
4807
  } else if (param === "mix") {
4617
4808
  nodes._mix.offset.value = value;
4618
4809
  }
@@ -4620,7 +4811,11 @@ var wavefolderDescriptor = /* @__PURE__ */ defineDescriptor({
4620
4811
  });
4621
4812
 
4622
4813
  // src/lib-runtime/generated/modules/wavefolder.wrapper.ts
4623
- var Wavefolder = /* @__PURE__ */ defineModule(wavefolderDescriptor);
4814
+ var Wavefolder = /* @__PURE__ */ defineModule(wavefolderDescriptor, {
4815
+ worklets: {
4816
+ "shaper": shaper_default
4817
+ }
4818
+ });
4624
4819
  export {
4625
4820
  AcidFilter,
4626
4821
  AutoPan,