@weasel-js/audio 1.11.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19,14 +19,14 @@ function createAnalyserTap(ctx, source, opts = {}) {
19
19
  return {
20
20
  node,
21
21
  frequencies(out) {
22
- const target = out ?? new Uint8Array(node.frequencyBinCount);
23
- node.getByteFrequencyData(target);
24
- return target;
22
+ const target2 = out ?? new Uint8Array(node.frequencyBinCount);
23
+ node.getByteFrequencyData(target2);
24
+ return target2;
25
25
  },
26
26
  waveform(out) {
27
- const target = out ?? new Uint8Array(node.fftSize);
28
- node.getByteTimeDomainData(target);
29
- return target;
27
+ const target2 = out ?? new Uint8Array(node.fftSize);
28
+ node.getByteTimeDomainData(target2);
29
+ return target2;
30
30
  },
31
31
  level() {
32
32
  const buf = timeBuf();
@@ -49,16 +49,16 @@ function createAnalyserTap(ctx, source, opts = {}) {
49
49
  }
50
50
  const buf = freqBuf();
51
51
  node.getByteFrequencyData(buf);
52
- const target = out ?? new Float32Array(n);
52
+ const target2 = out ?? new Float32Array(n);
53
53
  const per = buf.length / n;
54
54
  for (let b = 0; b < n; b += 1) {
55
55
  const lo = Math.floor(b * per);
56
56
  const hi = Math.max(lo + 1, Math.floor((b + 1) * per));
57
57
  let sum = 0;
58
58
  for (let i = lo; i < hi; i += 1) sum += buf[i];
59
- target[b] = sum / (hi - lo) / 255;
59
+ target2[b] = sum / (hi - lo) / 255;
60
60
  }
61
- return target;
61
+ return target2;
62
62
  },
63
63
  // `node.disconnect()` would cut the analyser's outgoing edges, and it has
64
64
  // none: the edge that keeps it alive is the source's.
@@ -108,6 +108,10 @@ function writeParam(ctx, param, value, rampMs) {
108
108
  param.setValueAtTime(value, now);
109
109
  }
110
110
  }
111
+ function initParam(ctx, param, value) {
112
+ param.value = value;
113
+ writeParam(ctx, param, value);
114
+ }
111
115
 
112
116
  // src/inserts.ts
113
117
  function createInsertChain(ctx, head, tail, opts = {}) {
@@ -388,6 +392,512 @@ function createBusGraph(ctx, names, opts = {}) {
388
392
  };
389
393
  }
390
394
 
395
+ // src/graph/edges.ts
396
+ function findLive(edges, from, to, port) {
397
+ return edges.find((e) => e.state !== "closing" && e.from === from && e.to === to && e.port === port);
398
+ }
399
+ var touches = (e, w) => e.from === w || e.to === w;
400
+ var logical = (e) => e.state !== "closing";
401
+ var wired = (e) => e.state !== "deferred";
402
+ function reaches(edges, start, goal, admit = () => true) {
403
+ const seen = /* @__PURE__ */ new Set();
404
+ const stack = [start];
405
+ while (stack.length > 0) {
406
+ const n = stack.pop();
407
+ if (n === goal) return true;
408
+ if (seen.has(n)) continue;
409
+ seen.add(n);
410
+ for (const e of edges) if (admit(e) && e.from === n && e.to !== null) stack.push(e.to);
411
+ }
412
+ return false;
413
+ }
414
+ var blocked = (edges, e) => e.to !== null && reaches(edges, e.to, e.from, wired);
415
+ function drop(edges, e) {
416
+ const i = edges.indexOf(e);
417
+ if (i >= 0) edges.splice(i, 1);
418
+ }
419
+
420
+ // src/graph/fader.ts
421
+ function createFader(ctx, timers, from, to) {
422
+ const gain = ctx.createGain();
423
+ initParam(ctx, gain.gain, 0);
424
+ from.connect(gain);
425
+ gain.connect(to);
426
+ let cancel = null;
427
+ let closing = false;
428
+ let cutDone = false;
429
+ const cut = () => {
430
+ cancel?.();
431
+ cancel = null;
432
+ if (cutDone) return;
433
+ cutDone = true;
434
+ from.disconnect(gain);
435
+ gain.disconnect();
436
+ };
437
+ return {
438
+ gain,
439
+ open(ms) {
440
+ writeParam(ctx, gain.gain, 1, ms);
441
+ },
442
+ close(ms, done) {
443
+ if (closing || cutDone) return;
444
+ closing = true;
445
+ const length = ctx.state === "running" ? ms : 0;
446
+ writeParam(ctx, gain.gain, 0, length);
447
+ cancel = atAudioTime(ctx, timers, ctx.currentTime + length / 1e3, () => {
448
+ cancel = null;
449
+ cut();
450
+ done?.();
451
+ });
452
+ },
453
+ cut
454
+ };
455
+ }
456
+
457
+ // src/graph/wrapper.ts
458
+ function createWrapper(ctx, timers, id, def) {
459
+ const ins = {};
460
+ const inFaders = {};
461
+ for (const port of Object.keys(def.inputs)) {
462
+ ins[port] = ctx.createGain();
463
+ inFaders[port] = createFader(ctx, timers, ins[port], def.inputs[port]);
464
+ inFaders[port].open(0);
465
+ }
466
+ const out = ctx.createGain();
467
+ const outFader = createFader(ctx, timers, def.output, out);
468
+ outFader.open(0);
469
+ return { id, def, ins, inFaders, out, outFader, taps: /* @__PURE__ */ new Set(), running: false, outgoing: /* @__PURE__ */ new Set() };
470
+ }
471
+ function tapWrapper(ctx, w, opts) {
472
+ const inner = createAnalyserTap(ctx, w.out, opts);
473
+ const t = {
474
+ ...inner,
475
+ dispose() {
476
+ w.taps.delete(t);
477
+ inner.dispose();
478
+ }
479
+ };
480
+ w.taps.add(t);
481
+ return t;
482
+ }
483
+ function startWrapper(w, when) {
484
+ if (w.running || !w.def.start) return;
485
+ w.running = true;
486
+ w.def.start(when);
487
+ }
488
+ function stopWrapper(w, when) {
489
+ if (!w.running) return;
490
+ w.running = false;
491
+ w.def.stop?.(when);
492
+ }
493
+ function releaseOutgoing(w, o, when) {
494
+ w.outgoing.delete(o);
495
+ if (o.running) o.def.stop?.(when);
496
+ o.running = false;
497
+ for (const f of o.faders) f.cut();
498
+ o.def.dispose?.();
499
+ }
500
+ function releaseWrapper(w, when) {
501
+ for (const o of [...w.outgoing]) releaseOutgoing(w, o, when);
502
+ stopWrapper(w, when);
503
+ for (const f of Object.values(w.inFaders)) f.cut();
504
+ w.outFader.cut();
505
+ for (const t of w.taps) t.dispose();
506
+ w.taps.clear();
507
+ w.def.dispose?.();
508
+ }
509
+
510
+ // src/graph/replace.ts
511
+ function replaceDef(ctx, timers, w, next, ms, playing, hooks) {
512
+ const old = {
513
+ def: w.def,
514
+ faders: [...Object.values(w.inFaders), w.outFader],
515
+ running: w.running
516
+ };
517
+ w.outgoing.add(old);
518
+ for (const port of Object.keys(w.ins)) {
519
+ if (Object.prototype.hasOwnProperty.call(next.inputs, port)) continue;
520
+ hooks.closePort(port);
521
+ delete w.ins[port];
522
+ delete w.inFaders[port];
523
+ }
524
+ for (const port of Object.keys(next.inputs)) {
525
+ w.ins[port] ??= ctx.createGain();
526
+ w.inFaders[port] = openFader(ctx, timers, w.ins[port], next.inputs[port], ms);
527
+ }
528
+ w.outFader = openFader(ctx, timers, next.output, w.out, ms);
529
+ w.def = next;
530
+ w.running = false;
531
+ if (playing && next.start) {
532
+ w.running = true;
533
+ next.start(ctx.currentTime);
534
+ }
535
+ let open = old.faders.length;
536
+ for (const f of old.faders) {
537
+ f.close(ms, () => {
538
+ open -= 1;
539
+ if (open > 0 || !w.outgoing.has(old)) return;
540
+ releaseOutgoing(w, old, ctx.currentTime);
541
+ hooks.onSettle();
542
+ });
543
+ }
544
+ }
545
+ function openFader(ctx, timers, from, to, ms) {
546
+ const f = createFader(ctx, timers, from, to);
547
+ f.open(ms);
548
+ return f;
549
+ }
550
+
551
+ // src/graph/errors.ts
552
+ var graphError = (msg) => new Error(`@weasel-js/audio: ${msg}`);
553
+ var owns = (rec, key) => Object.prototype.hasOwnProperty.call(rec, key);
554
+ var noNode = (id) => graphError(`no node "${id}"`);
555
+ var duplicateNode = (id) => graphError(`a node "${id}" already exists`);
556
+ var noPort = (id, port) => graphError(`node "${id}" has no input port "${port}"`);
557
+ var noParam = (id, param) => graphError(`node "${id}" has no param "${param}"`);
558
+ var closesCycle = (from, to) => graphError(`connecting "${from}" to "${to}" would close a cycle`);
559
+
560
+ // src/graph/edits.ts
561
+ function target(live, to) {
562
+ if ("output" in to) return [null, ""];
563
+ const w = live(to.node);
564
+ if (!owns(w.ins, to.port)) throw noPort(to.node, to.port);
565
+ return [w, to.port];
566
+ }
567
+ function createEdits(deps) {
568
+ const { ctx, timers, fade, output } = deps;
569
+ const nodes = /* @__PURE__ */ new Map();
570
+ const leaving = /* @__PURE__ */ new Set();
571
+ const edges = [];
572
+ const live = (id) => {
573
+ const w = nodes.get(id);
574
+ if (!w) throw noNode(id);
575
+ return w;
576
+ };
577
+ const wire = (e) => {
578
+ e.state = "open";
579
+ e.fader = createFader(ctx, timers, e.from.out, e.to ? e.to.ins[e.port] : output);
580
+ e.fader.open(fade());
581
+ };
582
+ const sweep = () => {
583
+ for (const e of edges) if (e.state === "deferred" && !blocked(edges, e)) wire(e);
584
+ for (const w of [...leaving]) {
585
+ if (edges.some((e) => touches(e, w))) continue;
586
+ leaving.delete(w);
587
+ releaseWrapper(w, ctx.currentTime);
588
+ }
589
+ deps.onSettle();
590
+ };
591
+ const close = (e) => {
592
+ if (e.state === "closing") return;
593
+ if (e.state === "deferred") {
594
+ drop(edges, e);
595
+ return;
596
+ }
597
+ e.state = "closing";
598
+ e.fader.close(fade(), () => {
599
+ drop(edges, e);
600
+ sweep();
601
+ });
602
+ };
603
+ return {
604
+ live,
605
+ has: (id) => nodes.has(id),
606
+ ids: () => [...nodes.keys()],
607
+ liveNodes: () => [...nodes.values()],
608
+ /** Live and leaving: every wrapper whose def has not been released. */
609
+ allNodes: () => [...nodes.values(), ...leaving],
610
+ /** The live nodes and the edges a caller can see, by id, for `validatePatches`. */
611
+ view: () => ({
612
+ nodes: new Map([...nodes].map(([id, w]) => [id, { ports: new Set(Object.keys(w.ins)), params: w.def.params }])),
613
+ edges: edges.filter(logical).map((e) => ({ from: e.from.id, to: e.to?.id ?? null, port: e.port }))
614
+ }),
615
+ /** An edge is fading out or waiting to be wired, or a replaced def is fading out. */
616
+ busy: () => edges.some((e) => e.state !== "open") || [...nodes.values(), ...leaving].some((w) => w.outgoing.size > 0),
617
+ add(id, def) {
618
+ if (nodes.has(id)) throw duplicateNode(id);
619
+ const w = createWrapper(ctx, timers, id, def);
620
+ nodes.set(id, w);
621
+ deps.onAdd(w);
622
+ },
623
+ remove(id) {
624
+ const w = nodes.get(id);
625
+ if (!w) return;
626
+ nodes.delete(id);
627
+ leaving.add(w);
628
+ for (const e of edges.filter((x) => touches(x, w))) close(e);
629
+ sweep();
630
+ },
631
+ /** Crossfade `id` into `def`; edges into a port `def` lacks close with it. */
632
+ replace(id, def, playing) {
633
+ const w = live(id);
634
+ replaceDef(ctx, timers, w, def, fade(), playing, {
635
+ closePort: (port) => {
636
+ for (const e of edges.filter((x) => x.to === w && x.port === port)) close(e);
637
+ },
638
+ onSettle: deps.onSettle
639
+ });
640
+ deps.onSettle();
641
+ },
642
+ connect(from, to) {
643
+ const w = live(from);
644
+ const [tw, port] = target(live, to);
645
+ if (findLive(edges, w, tw, port)) return;
646
+ if (tw && reaches(edges, tw, w, logical)) {
647
+ throw closesCycle(from, tw.id);
648
+ }
649
+ const e = { from: w, to: tw, port, state: "deferred", fader: null };
650
+ edges.push(e);
651
+ if (!blocked(edges, e)) wire(e);
652
+ },
653
+ disconnect(from, to) {
654
+ const w = nodes.get(from);
655
+ const tw = "output" in to ? null : nodes.get(to.node);
656
+ if (!w || tw === void 0) return;
657
+ const e = findLive(edges, w, tw, "output" in to ? "" : to.port);
658
+ if (!e) return;
659
+ close(e);
660
+ deps.onSettle();
661
+ },
662
+ set(id, param, value, rampMs) {
663
+ const w = live(id);
664
+ if (!owns(w.def.params, param)) throw noParam(id, param);
665
+ w.def.params[param](value, rampMs ?? fade());
666
+ },
667
+ /** Cut every edge, canceling its close, then release every def. */
668
+ dispose() {
669
+ for (const e of edges.splice(0)) e.fader?.cut();
670
+ const all = [...nodes.values(), ...leaving];
671
+ nodes.clear();
672
+ leaving.clear();
673
+ for (const w of all) releaseWrapper(w, ctx.currentTime);
674
+ }
675
+ };
676
+ }
677
+
678
+ // src/graph/transport.ts
679
+ function createTransport(deps) {
680
+ const { ctx, timers, output } = deps;
681
+ let playing = false;
682
+ let cancel = null;
683
+ const fadeOutput = (value, then) => {
684
+ cancel?.();
685
+ cancel = null;
686
+ const ms = deps.fade();
687
+ writeParam(ctx, output.gain, value, ms);
688
+ let pending = true;
689
+ const c = atAudioTime(ctx, timers, ctx.currentTime + ms / 1e3, () => {
690
+ pending = false;
691
+ cancel = null;
692
+ then();
693
+ deps.onSettle();
694
+ });
695
+ if (pending) cancel = c;
696
+ };
697
+ return {
698
+ playing: () => playing,
699
+ /** An output fade is in flight. */
700
+ busy: () => cancel !== null,
701
+ start() {
702
+ if (playing) return;
703
+ playing = true;
704
+ fadeOutput(1, () => {
705
+ });
706
+ for (const w of deps.live()) startWrapper(w, ctx.currentTime);
707
+ },
708
+ stop() {
709
+ if (!playing) return;
710
+ playing = false;
711
+ fadeOutput(0, () => {
712
+ for (const w of deps.all()) stopWrapper(w, ctx.currentTime);
713
+ });
714
+ },
715
+ /** Playing, or an output fade is still in flight: the output may not be silent. */
716
+ audible: () => playing || cancel !== null,
717
+ /** Fade the output to silence, then run `then` instead of stopping the sources. */
718
+ fadeOut(then) {
719
+ playing = false;
720
+ fadeOutput(0, then);
721
+ },
722
+ /** Cancel the fade in flight and forget playing; the caller stops the sources. */
723
+ dispose() {
724
+ cancel?.();
725
+ cancel = null;
726
+ playing = false;
727
+ }
728
+ };
729
+ }
730
+
731
+ // src/graph/validate.ts
732
+ var viewNode = (def) => ({
733
+ ports: new Set(Object.keys(def.inputs)),
734
+ params: def.params
735
+ });
736
+ var toId = (to) => "output" in to ? null : to.node;
737
+ var toPort = (to) => "output" in to ? "" : to.port;
738
+ function validatePatches(view, patches) {
739
+ const nodes = new Map(view.nodes);
740
+ let edges = [...view.edges];
741
+ const live = (id) => {
742
+ const n = nodes.get(id);
743
+ if (!n) throw noNode(id);
744
+ return n;
745
+ };
746
+ const same = (e, from, to) => e.from === from && e.to === toId(to) && e.port === toPort(to);
747
+ for (const p of patches) {
748
+ switch (p.kind) {
749
+ case "add":
750
+ if (nodes.has(p.id)) throw duplicateNode(p.id);
751
+ nodes.set(p.id, viewNode(p.def));
752
+ break;
753
+ case "remove":
754
+ if (nodes.delete(p.id)) edges = edges.filter((e) => e.from !== p.id && e.to !== p.id);
755
+ break;
756
+ case "replace": {
757
+ live(p.id);
758
+ const n = viewNode(p.def);
759
+ nodes.set(p.id, n);
760
+ edges = edges.filter((e) => e.to !== p.id || n.ports.has(e.port));
761
+ break;
762
+ }
763
+ case "connect": {
764
+ live(p.from);
765
+ const to = toId(p.to);
766
+ if (to !== null && !live(to).ports.has(toPort(p.to))) throw noPort(to, toPort(p.to));
767
+ if (edges.some((e) => same(e, p.from, p.to))) break;
768
+ if (to !== null && reaches(edges, to, p.from)) throw closesCycle(p.from, to);
769
+ edges.push({ from: p.from, to, port: toPort(p.to) });
770
+ break;
771
+ }
772
+ case "disconnect":
773
+ edges = edges.filter((e) => !same(e, p.from, p.to));
774
+ break;
775
+ case "set":
776
+ if (!owns(live(p.id).params, p.param)) throw noParam(p.id, p.param);
777
+ break;
778
+ }
779
+ }
780
+ }
781
+
782
+ // src/graph/createAudioGraph.ts
783
+ var teardowns = /* @__PURE__ */ new WeakMap();
784
+ function disposeNow(graph) {
785
+ teardowns.get(graph)?.();
786
+ }
787
+ function createAudioGraph(ctx, destination, opts = {}) {
788
+ const fadeMs = opts.fadeMs ?? 15;
789
+ const timers = {
790
+ setTimer: opts.setTimer ?? defaultTimers.setTimer,
791
+ clearTimer: opts.clearTimer ?? defaultTimers.clearTimer
792
+ };
793
+ const fade = () => ctx.state === "running" ? fadeMs : 0;
794
+ const output = ctx.createGain();
795
+ initParam(ctx, output.gain, 0);
796
+ output.connect(destination);
797
+ let waiters = [];
798
+ let disposed = false;
799
+ let torn = false;
800
+ const onSettle = () => {
801
+ if (!torn && (edits.busy() || transport.busy())) return;
802
+ const done = waiters;
803
+ waiters = [];
804
+ for (const w of done) w();
805
+ };
806
+ const edits = createEdits({
807
+ ctx,
808
+ timers,
809
+ fade,
810
+ output,
811
+ onSettle,
812
+ onAdd: (w) => {
813
+ if (transport.playing()) startWrapper(w, ctx.currentTime);
814
+ }
815
+ });
816
+ const transport = createTransport({
817
+ ctx,
818
+ timers,
819
+ fade,
820
+ output,
821
+ onSettle,
822
+ live: edits.liveNodes,
823
+ all: edits.allNodes
824
+ });
825
+ const guard = (f) => (...args) => {
826
+ if (disposed) throw graphError("graph is disposed");
827
+ return f(...args);
828
+ };
829
+ const graph = {
830
+ add: guard(edits.add),
831
+ remove: guard(edits.remove),
832
+ replace: guard((id, def) => {
833
+ edits.replace(id, def, transport.playing());
834
+ }),
835
+ connect: guard(edits.connect),
836
+ disconnect: guard(edits.disconnect),
837
+ set: guard(edits.set),
838
+ apply: guard((patches) => {
839
+ validatePatches(edits.view(), patches);
840
+ let failed = null;
841
+ for (const p of patches) {
842
+ switch (p.kind) {
843
+ case "add":
844
+ graph.add(p.id, p.def);
845
+ break;
846
+ case "remove":
847
+ graph.remove(p.id);
848
+ break;
849
+ case "replace":
850
+ graph.replace(p.id, p.def);
851
+ break;
852
+ case "connect":
853
+ graph.connect(p.from, p.to);
854
+ break;
855
+ case "disconnect":
856
+ graph.disconnect(p.from, p.to);
857
+ break;
858
+ case "set":
859
+ try {
860
+ graph.set(p.id, p.param, p.value, p.rampMs);
861
+ } catch (error) {
862
+ failed ??= { error };
863
+ }
864
+ break;
865
+ }
866
+ }
867
+ if (failed) throw failed.error;
868
+ }),
869
+ start: guard(transport.start),
870
+ stop: guard(transport.stop),
871
+ playing: transport.playing,
872
+ has: edits.has,
873
+ ids: edits.ids,
874
+ tap: guard((id, tapOpts) => tapWrapper(ctx, edits.live(id), tapOpts)),
875
+ settled() {
876
+ return new Promise((resolve) => {
877
+ waiters.push(resolve);
878
+ onSettle();
879
+ });
880
+ },
881
+ dispose() {
882
+ if (disposed) return;
883
+ disposed = true;
884
+ if (ctx.state === "running" && transport.audible()) transport.fadeOut(teardown);
885
+ else teardown();
886
+ }
887
+ };
888
+ const teardown = () => {
889
+ if (torn) return;
890
+ disposed = true;
891
+ torn = true;
892
+ transport.dispose();
893
+ edits.dispose();
894
+ output.disconnect();
895
+ onSettle();
896
+ };
897
+ teardowns.set(graph, teardown);
898
+ return graph;
899
+ }
900
+
391
901
  // src/resumeWatch.ts
392
902
  function createResumeWatch(ctx, hooks) {
393
903
  let disposed = false;
@@ -503,7 +1013,7 @@ function createSoundCache(ctx, fetchFn = fetch) {
503
1013
  buffers.set(id, buffer);
504
1014
  return { id };
505
1015
  };
506
- const cache = {
1016
+ const cache2 = {
507
1017
  async load(url) {
508
1018
  const existing = byUrl.get(url);
509
1019
  if (existing) return existing;
@@ -527,7 +1037,7 @@ function createSoundCache(ctx, fetchFn = fetch) {
527
1037
  },
528
1038
  async loadAll(urls) {
529
1039
  const names = Object.keys(urls);
530
- const handles = await Promise.all(names.map((n) => cache.load(urls[n])));
1040
+ const handles = await Promise.all(names.map((n) => cache2.load(urls[n])));
531
1041
  return Object.fromEntries(names.map((n, i) => [n, handles[i]]));
532
1042
  },
533
1043
  async decode(bytes) {
@@ -536,7 +1046,7 @@ function createSoundCache(ctx, fetchFn = fetch) {
536
1046
  register: (buffer) => store(buffer),
537
1047
  buffer: (handle) => buffers.get(handle.id) ?? null
538
1048
  };
539
- return cache;
1049
+ return cache2;
540
1050
  }
541
1051
 
542
1052
  // src/streamStarter.ts
@@ -645,6 +1155,25 @@ function envelopePoints(e, gate) {
645
1155
  return points;
646
1156
  }
647
1157
 
1158
+ // src/periodicWave.ts
1159
+ var cache = /* @__PURE__ */ new WeakMap();
1160
+ function periodicWave(ctx, partials) {
1161
+ let byKey = cache.get(ctx);
1162
+ if (!byKey) {
1163
+ byKey = /* @__PURE__ */ new Map();
1164
+ cache.set(ctx, byKey);
1165
+ }
1166
+ const key = partials.join(",");
1167
+ let wave = byKey.get(key);
1168
+ if (!wave) {
1169
+ const imag = new Float32Array(partials.length + 1);
1170
+ imag.set(partials, 1);
1171
+ wave = ctx.createPeriodicWave(new Float32Array(partials.length + 1), imag);
1172
+ byKey.set(key, wave);
1173
+ }
1174
+ return wave;
1175
+ }
1176
+
648
1177
  // src/liveVoice.ts
649
1178
  var sourceGroup = (nodes) => ({
650
1179
  get onended() {
@@ -907,21 +1436,12 @@ function noteOscillators(ctx, note, { from, glideTo, partials }, applyWave, t0,
907
1436
  return oscs;
908
1437
  }
909
1438
  function createSynthStarters(ctx, random) {
910
- const waves = /* @__PURE__ */ new Map();
911
1439
  const applyWave = (osc, wave) => {
912
1440
  if (typeof wave === "string") {
913
1441
  osc.type = wave;
914
1442
  return;
915
1443
  }
916
- const key = wave.join(",");
917
- let periodic = waves.get(key);
918
- if (!periodic) {
919
- const imag = new Float32Array(wave.length + 1);
920
- imag.set(wave, 1);
921
- periodic = ctx.createPeriodicWave(new Float32Array(wave.length + 1), imag);
922
- waves.set(key, periodic);
923
- }
924
- osc.setPeriodicWave(periodic);
1444
+ osc.setPeriodicWave(periodicWave(ctx, wave));
925
1445
  };
926
1446
  return {
927
1447
  note(note) {
@@ -1672,6 +2192,27 @@ function engineSynth(ctx, opts, busInput) {
1672
2192
  dispose: () => worklet?.dispose()
1673
2193
  };
1674
2194
  }
2195
+ function patchGraphs(ctx, busInput, opts) {
2196
+ const live = /* @__PURE__ */ new Set();
2197
+ return {
2198
+ create(bus) {
2199
+ const g = createAudioGraph(ctx, busInput(bus), opts);
2200
+ live.add(g);
2201
+ return {
2202
+ ...g,
2203
+ dispose() {
2204
+ g.dispose();
2205
+ void g.settled().then(() => live.delete(g));
2206
+ }
2207
+ };
2208
+ },
2209
+ /** Tear every graph down now: the engine is closing, so nothing fades. */
2210
+ dispose() {
2211
+ for (const g of live) disposeNow(g);
2212
+ live.clear();
2213
+ }
2214
+ };
2215
+ }
1675
2216
  function engineClock(ctx, opts) {
1676
2217
  const now = () => ctx.currentTime * 1e3;
1677
2218
  const tickTimer = opts.setTimer === void 0 && opts.clearTimer === void 0 ? createTickTimer() : null;
@@ -1729,6 +2270,7 @@ function createAudioEngine(opts = {}) {
1729
2270
  });
1730
2271
  const synth = engineSynth(ctx, opts, graph.input);
1731
2272
  const streams = createStreamStarters(ctx, timers);
2273
+ const patches = patchGraphs(ctx, graph.input, { ...timers, fadeMs: opts.insertFadeMs });
1732
2274
  const engine = {
1733
2275
  context: ctx,
1734
2276
  state: () => ctx.state,
@@ -1775,6 +2317,10 @@ function createAudioEngine(opts = {}) {
1775
2317
  }
1776
2318
  };
1777
2319
  },
2320
+ graph(bus) {
2321
+ if (disposed) throw new Error("@weasel-js/audio: engine is disposed");
2322
+ return patches.create(bus ?? busNames[0]);
2323
+ },
1778
2324
  setListener(p, o) {
1779
2325
  listener = p;
1780
2326
  if (o) spatialOpts = o;
@@ -1792,6 +2338,7 @@ function createAudioEngine(opts = {}) {
1792
2338
  taps.clear();
1793
2339
  watch.dispose();
1794
2340
  synth.dispose();
2341
+ patches.dispose();
1795
2342
  graph.dispose();
1796
2343
  graph.master.disconnect();
1797
2344
  if (ownsContext) void ctx.close().catch(() => void 0);
@@ -1804,9 +2351,9 @@ function createAudioEngine(opts = {}) {
1804
2351
  function createFilterEffect(ctx, opts = {}) {
1805
2352
  const filter = ctx.createBiquadFilter();
1806
2353
  filter.type = opts.type ?? "lowpass";
1807
- if (opts.frequency !== void 0) writeParam(ctx, filter.frequency, opts.frequency);
1808
- if (opts.Q !== void 0) writeParam(ctx, filter.Q, opts.Q);
1809
- if (opts.gain !== void 0) writeParam(ctx, filter.gain, opts.gain);
2354
+ if (opts.frequency !== void 0) initParam(ctx, filter.frequency, opts.frequency);
2355
+ if (opts.Q !== void 0) initParam(ctx, filter.Q, opts.Q);
2356
+ if (opts.gain !== void 0) initParam(ctx, filter.gain, opts.gain);
1810
2357
  return { input: filter, output: filter, filter };
1811
2358
  }
1812
2359
  var checkMix = (mix) => {
@@ -1828,7 +2375,8 @@ function wetDry(ctx, wetIn, wetOut, initial) {
1828
2375
  writeParam(ctx, dry.gain, 1 - mix, rampMs);
1829
2376
  writeParam(ctx, wet.gain, mix, rampMs);
1830
2377
  };
1831
- write();
2378
+ initParam(ctx, dry.gain, 1 - mix);
2379
+ initParam(ctx, wet.gain, mix);
1832
2380
  return {
1833
2381
  input,
1834
2382
  output,
@@ -1861,8 +2409,8 @@ function createDelayEffect(ctx, opts = {}) {
1861
2409
  const m = wetDry(ctx, delay, delay, opts.mix ?? 0.35);
1862
2410
  delay.connect(loop);
1863
2411
  loop.connect(delay);
1864
- writeParam(ctx, delay.delayTime, time / 1e3);
1865
- writeParam(ctx, loop.gain, fb);
2412
+ initParam(ctx, delay.delayTime, time / 1e3);
2413
+ initParam(ctx, loop.gain, fb);
1866
2414
  return {
1867
2415
  input: m.input,
1868
2416
  output: m.output,
@@ -1891,7 +2439,7 @@ function createCompressorEffect(ctx, opts = {}) {
1891
2439
  const makeup = ctx.createGain();
1892
2440
  compressor.connect(makeup);
1893
2441
  const set = (param, value) => {
1894
- if (value !== void 0) writeParam(ctx, param, value);
2442
+ if (value !== void 0) initParam(ctx, param, value);
1895
2443
  };
1896
2444
  set(compressor.threshold, opts.threshold);
1897
2445
  set(compressor.knee, opts.knee);
@@ -2185,15 +2733,15 @@ function scoreBar(params, bar, opts = {}) {
2185
2733
  // src/score.ts
2186
2734
  function createScore(engine, opts) {
2187
2735
  const { mood: initial, onBar, ...barOpts } = opts;
2188
- let target = initial;
2736
+ let target2 = initial;
2189
2737
  const stateAt = (bar, before) => {
2190
- const cue = readScoreCue(target);
2738
+ const cue = readScoreCue(target2);
2191
2739
  return {
2192
2740
  bar,
2193
2741
  cueBar: before && before.cue.name === cue.name ? before.cueBar + 1 : 0,
2194
- mood: target,
2742
+ mood: target2,
2195
2743
  cue,
2196
- params: scoreParams(target)
2744
+ params: scoreParams(target2)
2197
2745
  };
2198
2746
  };
2199
2747
  const eventsOf = (state) => scoreBar(state.params, state.cueBar, barOpts);
@@ -2224,9 +2772,9 @@ function createScore(engine, opts) {
2224
2772
  },
2225
2773
  stop: () => player.stop(),
2226
2774
  playing: () => player.playing(),
2227
- mood: () => target,
2775
+ mood: () => target2,
2228
2776
  setMood(mood) {
2229
- target = mood;
2777
+ target2 = mood;
2230
2778
  },
2231
2779
  state: () => current
2232
2780
  };
@@ -2466,6 +3014,252 @@ function renderNoise(noise, sampleRate, random = Math.random) {
2466
3014
  return renderWhole(createNoiseKernel(noise, sampleRate, random), noise, sampleRate, "renderNoise");
2467
3015
  }
2468
3016
 
2469
- export { SCORE_BAR_STEPS, biquadCoefficients, biquadResponseDb, createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createNoiseKernel, createNoteKernel, createPatternPlayer, createReverbEffect, createScheduler, createScore, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, patternLength, patternVoicePeaks, readScoreCue, renderNoise, renderNote, resolveEnvelope, scoreBar, scoreChord, scoreCues, scoreModes, scoreParams, spatialize, toFrequency };
3017
+ // src/impulse.ts
3018
+ function mulberry32(seed) {
3019
+ let a = seed >>> 0;
3020
+ return () => {
3021
+ a = a + 1831565813 >>> 0;
3022
+ let t = a;
3023
+ t = Math.imul(t ^ t >>> 15, t | 1);
3024
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
3025
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
3026
+ };
3027
+ }
3028
+ function impulseResponse(ctx, seconds, opts = {}) {
3029
+ if (!(seconds > 0)) throw new RangeError(`@weasel-js/audio: impulse length ${seconds} s must be above 0`);
3030
+ const decayDb = opts.decayDb ?? 60;
3031
+ const channels = opts.channels ?? 2;
3032
+ const seed = opts.seed ?? 1;
3033
+ const length = Math.max(1, Math.round(seconds * ctx.sampleRate));
3034
+ const buffer = ctx.createBuffer(channels, length, ctx.sampleRate);
3035
+ const rate = decayDb / 20 * Math.LN10;
3036
+ for (let c = 0; c < channels; c += 1) {
3037
+ const data = buffer.getChannelData(c);
3038
+ const rand = mulberry32(seed + c * 2654435761);
3039
+ for (let i = 0; i < length; i += 1) {
3040
+ data[i] = (rand() * 2 - 1) * Math.exp(-rate * i / length);
3041
+ }
3042
+ }
3043
+ return buffer;
3044
+ }
3045
+
3046
+ // src/graph/sources.ts
3047
+ function sourceShell(ctx, make) {
3048
+ const output = ctx.createGain();
3049
+ let live = null;
3050
+ return {
3051
+ output,
3052
+ live: () => live,
3053
+ /** `onEnd` runs if the node ends while it is still the live one: it played out. */
3054
+ begin(when, setup, offset, onEnd) {
3055
+ const node = make();
3056
+ setup(node);
3057
+ node.connect(output);
3058
+ node.onended = () => {
3059
+ if (live !== node) return;
3060
+ live = null;
3061
+ node.disconnect();
3062
+ onEnd?.();
3063
+ };
3064
+ if (offset === void 0) node.start(when);
3065
+ else node.start(when, offset);
3066
+ live = node;
3067
+ return node;
3068
+ },
3069
+ end(when) {
3070
+ if (!live) return;
3071
+ const node = live;
3072
+ live = null;
3073
+ node.onended = () => node.disconnect();
3074
+ node.stop(when);
3075
+ },
3076
+ dispose() {
3077
+ if (live) {
3078
+ live.stop();
3079
+ live.disconnect();
3080
+ live = null;
3081
+ }
3082
+ output.disconnect();
3083
+ },
3084
+ level: (value, rampMs) => writeParam(ctx, output.gain, value, rampMs)
3085
+ };
3086
+ }
3087
+ function createFileSourceNode(ctx, opts) {
3088
+ const { buffer } = opts;
3089
+ const loop = opts.loop ?? true;
3090
+ let rate = 1;
3091
+ let offset = 0;
3092
+ let startedAt = 0;
3093
+ const shell = sourceShell(ctx, () => ctx.createBufferSource());
3094
+ const advance = (to) => {
3095
+ const next = offset + Math.max(0, to - startedAt) * rate;
3096
+ offset = loop ? next % buffer.duration : Math.min(next, buffer.duration);
3097
+ startedAt = to;
3098
+ };
3099
+ return {
3100
+ inputs: {},
3101
+ output: shell.output,
3102
+ params: {
3103
+ rate(value, rampMs) {
3104
+ const src = shell.live();
3105
+ if (src) {
3106
+ advance(ctx.currentTime);
3107
+ writeParam(ctx, src.playbackRate, value, rampMs);
3108
+ }
3109
+ rate = value;
3110
+ },
3111
+ level: shell.level
3112
+ },
3113
+ start(when) {
3114
+ if (shell.live()) return;
3115
+ startedAt = when;
3116
+ shell.begin(when, (s) => {
3117
+ s.buffer = buffer;
3118
+ s.loop = loop;
3119
+ initParam(ctx, s.playbackRate, rate);
3120
+ }, offset, () => {
3121
+ offset = 0;
3122
+ });
3123
+ },
3124
+ stop(when) {
3125
+ if (!shell.live()) return;
3126
+ advance(when);
3127
+ shell.end(when);
3128
+ },
3129
+ dispose: shell.dispose
3130
+ };
3131
+ }
3132
+ function createOscillatorNode(ctx, opts) {
3133
+ const wave = opts.wave ?? "sine";
3134
+ let frequency = opts.frequency ?? 440;
3135
+ let detune = 0;
3136
+ const shell = sourceShell(ctx, () => ctx.createOscillator());
3137
+ return {
3138
+ inputs: {},
3139
+ output: shell.output,
3140
+ params: {
3141
+ frequency(value, rampMs) {
3142
+ const osc = shell.live();
3143
+ if (osc) writeParam(ctx, osc.frequency, value, rampMs);
3144
+ frequency = value;
3145
+ },
3146
+ detune(value, rampMs) {
3147
+ const osc = shell.live();
3148
+ if (osc) writeParam(ctx, osc.detune, value, rampMs);
3149
+ detune = value;
3150
+ },
3151
+ level: shell.level
3152
+ },
3153
+ start(when) {
3154
+ if (shell.live()) return;
3155
+ shell.begin(when, (osc) => {
3156
+ if (typeof wave === "string") osc.type = wave;
3157
+ else osc.setPeriodicWave(periodicWave(ctx, wave));
3158
+ initParam(ctx, osc.frequency, frequency);
3159
+ initParam(ctx, osc.detune, detune);
3160
+ });
3161
+ },
3162
+ stop: shell.end,
3163
+ dispose: shell.dispose
3164
+ };
3165
+ }
3166
+ function createNoiseNode(ctx, opts) {
3167
+ const shell = sourceShell(ctx, () => ctx.createBufferSource());
3168
+ return {
3169
+ inputs: {},
3170
+ output: shell.output,
3171
+ params: { level: shell.level },
3172
+ start(when) {
3173
+ if (shell.live()) return;
3174
+ shell.begin(when, (src) => {
3175
+ src.buffer = noiseBuffer(ctx, opts.color);
3176
+ src.loop = true;
3177
+ });
3178
+ },
3179
+ stop: shell.end,
3180
+ dispose: shell.dispose
3181
+ };
3182
+ }
3183
+
3184
+ // src/graph/processors.ts
3185
+ function createGainNode(ctx, opts = {}) {
3186
+ const node = ctx.createGain();
3187
+ initParam(ctx, node.gain, opts.gain ?? 1);
3188
+ return {
3189
+ inputs: { in: node },
3190
+ output: node,
3191
+ params: { gain: (value, rampMs) => writeParam(ctx, node.gain, value, rampMs) }
3192
+ };
3193
+ }
3194
+ function createPanNode(ctx, opts = {}) {
3195
+ const node = ctx.createStereoPanner();
3196
+ initParam(ctx, node.pan, opts.pan ?? 0);
3197
+ return {
3198
+ inputs: { in: node },
3199
+ output: node,
3200
+ params: { pan: (value, rampMs) => writeParam(ctx, node.pan, value, rampMs) }
3201
+ };
3202
+ }
3203
+ function createMixNode(ctx, opts) {
3204
+ if (opts.inputs.length === 0) throw new RangeError("@weasel-js/audio: a mix needs at least one input");
3205
+ const output = ctx.createGain();
3206
+ const inputs = {};
3207
+ const params = {};
3208
+ for (const name of opts.inputs) {
3209
+ if (Object.prototype.hasOwnProperty.call(inputs, name)) {
3210
+ throw new RangeError(`@weasel-js/audio: a mix has two inputs named "${name}"`);
3211
+ }
3212
+ const level = ctx.createGain();
3213
+ level.connect(output);
3214
+ inputs[name] = level;
3215
+ params[name] = (value, rampMs) => writeParam(ctx, level.gain, value, rampMs);
3216
+ }
3217
+ return { inputs, output, params };
3218
+ }
3219
+ function insertNode(effect, params) {
3220
+ return {
3221
+ inputs: { in: effect.input },
3222
+ output: effect.output,
3223
+ params,
3224
+ ...effect.dispose ? { dispose: () => effect.dispose() } : {}
3225
+ };
3226
+ }
3227
+ function filterNode(ctx, opts = {}) {
3228
+ const { filter } = createFilterEffect(ctx, opts);
3229
+ const write = (p) => (value, rampMs) => writeParam(ctx, p, value, rampMs);
3230
+ return insertNode({ input: filter, output: filter }, {
3231
+ frequency: write(filter.frequency),
3232
+ Q: write(filter.Q),
3233
+ gain: write(filter.gain)
3234
+ });
3235
+ }
3236
+ function delayNode(ctx, opts = {}) {
3237
+ const fx = createDelayEffect(ctx, { ...opts, maxTime: opts.maxTime ?? Math.max(2e3, opts.time ?? 0) });
3238
+ return insertNode(fx, {
3239
+ time: (value, rampMs) => fx.setTime(value, rampMs),
3240
+ feedback: (value, rampMs) => fx.setFeedback(value, rampMs),
3241
+ mix: (value, rampMs) => fx.setMix(value, rampMs)
3242
+ });
3243
+ }
3244
+ function reverbNode(ctx, opts = {}) {
3245
+ const { seconds, impulse, ...rest } = opts;
3246
+ const fx = createReverbEffect(ctx, { ...rest, impulse: impulse ?? impulseResponse(ctx, seconds ?? 2) });
3247
+ return insertNode(fx, { mix: (value, rampMs) => fx.setMix(value, rampMs) });
3248
+ }
3249
+ function compressorNode(ctx, opts = {}) {
3250
+ const fx = createCompressorEffect(ctx, opts);
3251
+ const { compressor, makeup } = fx;
3252
+ const write = (p, scale = 1) => (value, rampMs) => writeParam(ctx, p, value * scale, rampMs);
3253
+ return insertNode(fx, {
3254
+ threshold: write(compressor.threshold),
3255
+ knee: write(compressor.knee),
3256
+ ratio: write(compressor.ratio),
3257
+ attack: write(compressor.attack, 1e-3),
3258
+ release: write(compressor.release, 1e-3),
3259
+ makeup: write(makeup.gain)
3260
+ });
3261
+ }
3262
+
3263
+ export { SCORE_BAR_STEPS, biquadCoefficients, biquadResponseDb, compressorNode, createAnalyserTap, createAudioEngine, createAudioGraph, createBusGraph, createCompressorEffect, createDelayEffect, createFileSourceNode, createFilterEffect, createGainNode, createMixNode, createNoiseKernel, createNoiseNode, createNoteKernel, createOscillatorNode, createPanNode, createPatternPlayer, createReverbEffect, createScheduler, createScore, createSoundCache, createTickTimer, createVoicePool, delayNode, envelopeLevel, envelopePoints, filterNode, impulseResponse, insertNode, midiToFrequency, noiseSamples, noteToMidi, partialPresets, patternLength, patternVoicePeaks, readScoreCue, renderNoise, renderNote, resolveEnvelope, reverbNode, scoreBar, scoreChord, scoreCues, scoreModes, scoreParams, spatialize, toFrequency };
2470
3264
  //# sourceMappingURL=index.js.map
2471
3265
  //# sourceMappingURL=index.js.map