@weasel-js/audio 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,524 @@ 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 closed = false;
429
+ let cutDone = false;
430
+ const dones = /* @__PURE__ */ new Set();
431
+ const cut = () => {
432
+ cancel?.();
433
+ cancel = null;
434
+ if (cutDone) return;
435
+ cutDone = true;
436
+ from.disconnect(gain);
437
+ gain.disconnect();
438
+ };
439
+ return {
440
+ gain,
441
+ open(ms) {
442
+ writeParam(ctx, gain.gain, 1, ms);
443
+ },
444
+ close(ms, done) {
445
+ if (closed) done?.();
446
+ if (closed || cutDone) return;
447
+ if (done) dones.add(done);
448
+ if (closing) return;
449
+ closing = true;
450
+ const length = ctx.state === "running" ? ms : 0;
451
+ writeParam(ctx, gain.gain, 0, length);
452
+ cancel = atAudioTime(ctx, timers, ctx.currentTime + length / 1e3, () => {
453
+ cancel = null;
454
+ cut();
455
+ closed = true;
456
+ const all = [...dones];
457
+ dones.clear();
458
+ for (const d of all) d();
459
+ });
460
+ },
461
+ cut
462
+ };
463
+ }
464
+
465
+ // src/graph/wrapper.ts
466
+ function createWrapper(ctx, timers, id, def) {
467
+ const ins = {};
468
+ const inFaders = {};
469
+ for (const port of Object.keys(def.inputs)) {
470
+ ins[port] = ctx.createGain();
471
+ inFaders[port] = createFader(ctx, timers, ins[port], def.inputs[port]);
472
+ inFaders[port].open(0);
473
+ }
474
+ const out = ctx.createGain();
475
+ const outFader = createFader(ctx, timers, def.output, out);
476
+ outFader.open(0);
477
+ return { id, def, ins, inFaders, out, outFader, taps: /* @__PURE__ */ new Set(), running: false, outgoing: /* @__PURE__ */ new Set() };
478
+ }
479
+ function tapWrapper(ctx, w, opts) {
480
+ const inner = createAnalyserTap(ctx, w.out, opts);
481
+ const t = {
482
+ ...inner,
483
+ dispose() {
484
+ w.taps.delete(t);
485
+ inner.dispose();
486
+ }
487
+ };
488
+ w.taps.add(t);
489
+ return t;
490
+ }
491
+ function startWrapper(w, when) {
492
+ const { def } = w;
493
+ if (w.running || !def.start) return;
494
+ w.running = true;
495
+ def.start(when, () => {
496
+ if (w.def === def) w.running = false;
497
+ });
498
+ }
499
+ function stopWrapper(w, when) {
500
+ for (const o of w.outgoing) {
501
+ if (o.running) o.def.stop?.(when);
502
+ o.running = false;
503
+ }
504
+ if (!w.running) return;
505
+ w.running = false;
506
+ w.def.stop?.(when);
507
+ }
508
+ function releaseOutgoing(w, o, when) {
509
+ w.outgoing.delete(o);
510
+ if (o.running) o.def.stop?.(when);
511
+ o.running = false;
512
+ for (const f of o.faders) f.cut();
513
+ o.def.dispose?.();
514
+ }
515
+ function releaseWrapper(w, when) {
516
+ for (const o of [...w.outgoing]) releaseOutgoing(w, o, when);
517
+ stopWrapper(w, when);
518
+ for (const f of Object.values(w.inFaders)) f.cut();
519
+ w.outFader.cut();
520
+ for (const t of w.taps) t.dispose();
521
+ w.taps.clear();
522
+ w.def.dispose?.();
523
+ }
524
+
525
+ // src/graph/replace.ts
526
+ function replaceDef(ctx, timers, w, next, ms, playing, hooks) {
527
+ const old = {
528
+ def: w.def,
529
+ faders: [...Object.values(w.inFaders), w.outFader],
530
+ running: w.running
531
+ };
532
+ w.outgoing.add(old);
533
+ for (const port of Object.keys(w.ins)) {
534
+ if (Object.prototype.hasOwnProperty.call(next.inputs, port)) continue;
535
+ hooks.closePort(port);
536
+ delete w.ins[port];
537
+ delete w.inFaders[port];
538
+ }
539
+ for (const port of Object.keys(next.inputs)) {
540
+ w.ins[port] ??= ctx.createGain();
541
+ w.inFaders[port] = openFader(ctx, timers, w.ins[port], next.inputs[port], ms);
542
+ }
543
+ w.outFader = openFader(ctx, timers, next.output, w.out, ms);
544
+ w.def = next;
545
+ w.running = false;
546
+ if (playing) startWrapper(w, ctx.currentTime);
547
+ let open = old.faders.length;
548
+ for (const f of old.faders) {
549
+ f.close(ms, () => {
550
+ open -= 1;
551
+ if (open > 0 || !w.outgoing.has(old)) return;
552
+ releaseOutgoing(w, old, ctx.currentTime);
553
+ hooks.onSettle();
554
+ });
555
+ }
556
+ }
557
+ function openFader(ctx, timers, from, to, ms) {
558
+ const f = createFader(ctx, timers, from, to);
559
+ f.open(ms);
560
+ return f;
561
+ }
562
+
563
+ // src/graph/errors.ts
564
+ var graphError = (msg) => new Error(`@weasel-js/audio: ${msg}`);
565
+ var owns = (rec, key) => Object.prototype.hasOwnProperty.call(rec, key);
566
+ var noNode = (id) => graphError(`no node "${id}"`);
567
+ var duplicateNode = (id) => graphError(`a node "${id}" already exists`);
568
+ var noPort = (id, port) => graphError(`node "${id}" has no input port "${port}"`);
569
+ var noParam = (id, param) => graphError(`node "${id}" has no param "${param}"`);
570
+ var closesCycle = (from, to) => graphError(`connecting "${from}" to "${to}" would close a cycle`);
571
+
572
+ // src/graph/edits.ts
573
+ function target(live, to) {
574
+ if ("output" in to) return [null, ""];
575
+ const w = live(to.node);
576
+ if (!owns(w.ins, to.port)) throw noPort(to.node, to.port);
577
+ return [w, to.port];
578
+ }
579
+ function createEdits(deps) {
580
+ const { ctx, timers, fade, output } = deps;
581
+ const nodes = /* @__PURE__ */ new Map();
582
+ const leaving = /* @__PURE__ */ new Set();
583
+ const edges = [];
584
+ const live = (id) => {
585
+ const w = nodes.get(id);
586
+ if (!w) throw noNode(id);
587
+ return w;
588
+ };
589
+ const wire = (e) => {
590
+ e.state = "open";
591
+ e.fader = createFader(ctx, timers, e.from.out, e.to ? e.to.ins[e.port] : output);
592
+ e.fader.open(fade());
593
+ };
594
+ const sweep = () => {
595
+ for (const e of edges) if (e.state === "deferred" && !blocked(edges, e)) wire(e);
596
+ for (const w of [...leaving]) {
597
+ if (edges.some((e) => touches(e, w))) continue;
598
+ leaving.delete(w);
599
+ releaseWrapper(w, ctx.currentTime);
600
+ }
601
+ deps.onSettle();
602
+ };
603
+ const close = (e) => {
604
+ if (e.state === "closing") return;
605
+ if (e.state === "deferred") {
606
+ drop(edges, e);
607
+ return;
608
+ }
609
+ e.state = "closing";
610
+ e.fader.close(fade(), () => {
611
+ drop(edges, e);
612
+ sweep();
613
+ });
614
+ };
615
+ return {
616
+ live,
617
+ has: (id) => nodes.has(id),
618
+ ids: () => [...nodes.keys()],
619
+ liveNodes: () => [...nodes.values()],
620
+ /** Live and leaving: every wrapper whose def has not been released. */
621
+ allNodes: () => [...nodes.values(), ...leaving],
622
+ /** The live nodes and the edges a caller can see, by id, for `validatePatches`. */
623
+ view: () => ({
624
+ nodes: new Map([...nodes].map(([id, w]) => [id, { ports: new Set(Object.keys(w.ins)), params: w.def.params }])),
625
+ edges: edges.filter(logical).map((e) => ({ from: e.from.id, to: e.to?.id ?? null, port: e.port }))
626
+ }),
627
+ /** An edge is fading out or waiting to be wired, or a replaced def is fading out. */
628
+ busy: () => edges.some((e) => e.state !== "open") || [...nodes.values(), ...leaving].some((w) => w.outgoing.size > 0),
629
+ add(id, def) {
630
+ if (nodes.has(id)) throw duplicateNode(id);
631
+ const w = createWrapper(ctx, timers, id, def);
632
+ nodes.set(id, w);
633
+ deps.onAdd(w);
634
+ },
635
+ remove(id) {
636
+ const w = nodes.get(id);
637
+ if (!w) return;
638
+ nodes.delete(id);
639
+ leaving.add(w);
640
+ for (const e of edges.filter((x) => touches(x, w))) close(e);
641
+ sweep();
642
+ },
643
+ /** Crossfade `id` into `def`; edges into a port `def` lacks close with it. */
644
+ replace(id, def, playing) {
645
+ const w = live(id);
646
+ replaceDef(ctx, timers, w, def, fade(), playing, {
647
+ closePort: (port) => {
648
+ for (const e of edges.filter((x) => x.to === w && x.port === port)) close(e);
649
+ },
650
+ onSettle: deps.onSettle
651
+ });
652
+ deps.onSettle();
653
+ },
654
+ connect(from, to) {
655
+ const w = live(from);
656
+ const [tw, port] = target(live, to);
657
+ if (findLive(edges, w, tw, port)) return;
658
+ if (tw && reaches(edges, tw, w, logical)) {
659
+ throw closesCycle(from, tw.id);
660
+ }
661
+ const e = { from: w, to: tw, port, state: "deferred", fader: null };
662
+ edges.push(e);
663
+ if (!blocked(edges, e)) wire(e);
664
+ },
665
+ disconnect(from, to) {
666
+ const w = nodes.get(from);
667
+ const tw = "output" in to ? null : nodes.get(to.node);
668
+ if (!w || tw === void 0) return;
669
+ const e = findLive(edges, w, tw, "output" in to ? "" : to.port);
670
+ if (!e) return;
671
+ close(e);
672
+ deps.onSettle();
673
+ },
674
+ set(id, param, value, rampMs) {
675
+ const w = live(id);
676
+ if (!owns(w.def.params, param)) throw noParam(id, param);
677
+ w.def.params[param](value, rampMs ?? fade());
678
+ },
679
+ /** Cut every edge, canceling its close, then release every def. */
680
+ dispose() {
681
+ for (const e of edges.splice(0)) e.fader?.cut();
682
+ const all = [...nodes.values(), ...leaving];
683
+ nodes.clear();
684
+ leaving.clear();
685
+ for (const w of all) releaseWrapper(w, ctx.currentTime);
686
+ }
687
+ };
688
+ }
689
+
690
+ // src/graph/transport.ts
691
+ function createTransport(deps) {
692
+ const { ctx, timers, output } = deps;
693
+ let playing = false;
694
+ let cancel = null;
695
+ const fadeOutput = (value, then) => {
696
+ cancel?.();
697
+ cancel = null;
698
+ const ms = deps.fade();
699
+ writeParam(ctx, output.gain, value, ms);
700
+ let pending = true;
701
+ const c = atAudioTime(ctx, timers, ctx.currentTime + ms / 1e3, () => {
702
+ pending = false;
703
+ cancel = null;
704
+ then();
705
+ deps.onSettle();
706
+ });
707
+ if (pending) cancel = c;
708
+ };
709
+ return {
710
+ playing: () => playing,
711
+ /** An output fade is in flight. */
712
+ busy: () => cancel !== null,
713
+ start() {
714
+ if (playing) return;
715
+ playing = true;
716
+ fadeOutput(1, () => {
717
+ });
718
+ for (const w of deps.live()) startWrapper(w, ctx.currentTime);
719
+ },
720
+ stop() {
721
+ if (!playing) return;
722
+ playing = false;
723
+ fadeOutput(0, () => {
724
+ for (const w of deps.all()) stopWrapper(w, ctx.currentTime);
725
+ });
726
+ },
727
+ /** Playing, or an output fade is still in flight: the output may not be silent. */
728
+ audible: () => playing || cancel !== null,
729
+ /** Fade the output to silence, then run `then` instead of stopping the sources. */
730
+ fadeOut(then) {
731
+ playing = false;
732
+ fadeOutput(0, then);
733
+ },
734
+ /** Cancel the fade in flight and forget playing; the caller stops the sources. */
735
+ dispose() {
736
+ cancel?.();
737
+ cancel = null;
738
+ playing = false;
739
+ }
740
+ };
741
+ }
742
+
743
+ // src/graph/validate.ts
744
+ var viewNode = (def) => ({
745
+ ports: new Set(Object.keys(def.inputs)),
746
+ params: def.params
747
+ });
748
+ var toId = (to) => "output" in to ? null : to.node;
749
+ var toPort = (to) => "output" in to ? "" : to.port;
750
+ function validatePatches(view, patches) {
751
+ const nodes = new Map(view.nodes);
752
+ let edges = [...view.edges];
753
+ const live = (id) => {
754
+ const n = nodes.get(id);
755
+ if (!n) throw noNode(id);
756
+ return n;
757
+ };
758
+ const same = (e, from, to) => e.from === from && e.to === toId(to) && e.port === toPort(to);
759
+ for (const p of patches) {
760
+ switch (p.kind) {
761
+ case "add":
762
+ if (nodes.has(p.id)) throw duplicateNode(p.id);
763
+ nodes.set(p.id, viewNode(p.def));
764
+ break;
765
+ case "remove":
766
+ if (nodes.delete(p.id)) edges = edges.filter((e) => e.from !== p.id && e.to !== p.id);
767
+ break;
768
+ case "replace": {
769
+ live(p.id);
770
+ const n = viewNode(p.def);
771
+ nodes.set(p.id, n);
772
+ edges = edges.filter((e) => e.to !== p.id || n.ports.has(e.port));
773
+ break;
774
+ }
775
+ case "connect": {
776
+ live(p.from);
777
+ const to = toId(p.to);
778
+ if (to !== null && !live(to).ports.has(toPort(p.to))) throw noPort(to, toPort(p.to));
779
+ if (edges.some((e) => same(e, p.from, p.to))) break;
780
+ if (to !== null && reaches(edges, to, p.from)) throw closesCycle(p.from, to);
781
+ edges.push({ from: p.from, to, port: toPort(p.to) });
782
+ break;
783
+ }
784
+ case "disconnect":
785
+ edges = edges.filter((e) => !same(e, p.from, p.to));
786
+ break;
787
+ case "set":
788
+ if (!owns(live(p.id).params, p.param)) throw noParam(p.id, p.param);
789
+ break;
790
+ }
791
+ }
792
+ }
793
+
794
+ // src/graph/createAudioGraph.ts
795
+ var teardowns = /* @__PURE__ */ new WeakMap();
796
+ function disposeNow(graph) {
797
+ teardowns.get(graph)?.();
798
+ }
799
+ function createAudioGraph(ctx, destination, opts = {}) {
800
+ const fadeMs = opts.fadeMs ?? 15;
801
+ const timers = {
802
+ setTimer: opts.setTimer ?? defaultTimers.setTimer,
803
+ clearTimer: opts.clearTimer ?? defaultTimers.clearTimer
804
+ };
805
+ const fade = () => ctx.state === "running" ? fadeMs : 0;
806
+ const output = ctx.createGain();
807
+ initParam(ctx, output.gain, 0);
808
+ output.connect(destination);
809
+ let waiters = [];
810
+ let disposed = false;
811
+ let torn = false;
812
+ const onSettle = () => {
813
+ if (!torn && (edits.busy() || transport.busy())) return;
814
+ const done = waiters;
815
+ waiters = [];
816
+ for (const w of done) w();
817
+ };
818
+ const edits = createEdits({
819
+ ctx,
820
+ timers,
821
+ fade,
822
+ output,
823
+ onSettle,
824
+ onAdd: (w) => {
825
+ if (transport.playing()) startWrapper(w, ctx.currentTime);
826
+ }
827
+ });
828
+ const transport = createTransport({
829
+ ctx,
830
+ timers,
831
+ fade,
832
+ output,
833
+ onSettle,
834
+ live: edits.liveNodes,
835
+ all: edits.allNodes
836
+ });
837
+ const guard = (f) => (...args) => {
838
+ if (disposed) throw graphError("graph is disposed");
839
+ return f(...args);
840
+ };
841
+ const graph = {
842
+ add: guard(edits.add),
843
+ remove: guard(edits.remove),
844
+ replace: guard((id, def) => {
845
+ edits.replace(id, def, transport.playing());
846
+ }),
847
+ connect: guard(edits.connect),
848
+ disconnect: guard(edits.disconnect),
849
+ set: guard(edits.set),
850
+ apply: guard((patches) => {
851
+ validatePatches(edits.view(), patches);
852
+ let failed = null;
853
+ for (const p of patches) {
854
+ switch (p.kind) {
855
+ case "add":
856
+ graph.add(p.id, p.def);
857
+ break;
858
+ case "remove":
859
+ graph.remove(p.id);
860
+ break;
861
+ case "replace":
862
+ graph.replace(p.id, p.def);
863
+ break;
864
+ case "connect":
865
+ graph.connect(p.from, p.to);
866
+ break;
867
+ case "disconnect":
868
+ graph.disconnect(p.from, p.to);
869
+ break;
870
+ case "set":
871
+ try {
872
+ graph.set(p.id, p.param, p.value, p.rampMs);
873
+ } catch (error) {
874
+ failed ??= { error };
875
+ }
876
+ break;
877
+ }
878
+ }
879
+ if (failed) throw failed.error;
880
+ }),
881
+ start: guard(transport.start),
882
+ stop: guard(transport.stop),
883
+ playing: transport.playing,
884
+ has: edits.has,
885
+ ids: edits.ids,
886
+ tap: guard((id, tapOpts) => tapWrapper(ctx, edits.live(id), tapOpts)),
887
+ settled() {
888
+ return new Promise((resolve) => {
889
+ waiters.push(resolve);
890
+ onSettle();
891
+ });
892
+ },
893
+ dispose() {
894
+ if (disposed) return;
895
+ disposed = true;
896
+ if (ctx.state === "running" && transport.audible()) transport.fadeOut(teardown);
897
+ else teardown();
898
+ }
899
+ };
900
+ const teardown = () => {
901
+ if (torn) return;
902
+ disposed = true;
903
+ torn = true;
904
+ transport.dispose();
905
+ edits.dispose();
906
+ output.disconnect();
907
+ onSettle();
908
+ };
909
+ teardowns.set(graph, teardown);
910
+ return graph;
911
+ }
912
+
391
913
  // src/resumeWatch.ts
392
914
  function createResumeWatch(ctx, hooks) {
393
915
  let disposed = false;
@@ -503,7 +1025,7 @@ function createSoundCache(ctx, fetchFn = fetch) {
503
1025
  buffers.set(id, buffer);
504
1026
  return { id };
505
1027
  };
506
- const cache = {
1028
+ const cache2 = {
507
1029
  async load(url) {
508
1030
  const existing = byUrl.get(url);
509
1031
  if (existing) return existing;
@@ -527,7 +1049,7 @@ function createSoundCache(ctx, fetchFn = fetch) {
527
1049
  },
528
1050
  async loadAll(urls) {
529
1051
  const names = Object.keys(urls);
530
- const handles = await Promise.all(names.map((n) => cache.load(urls[n])));
1052
+ const handles = await Promise.all(names.map((n) => cache2.load(urls[n])));
531
1053
  return Object.fromEntries(names.map((n, i) => [n, handles[i]]));
532
1054
  },
533
1055
  async decode(bytes) {
@@ -536,7 +1058,7 @@ function createSoundCache(ctx, fetchFn = fetch) {
536
1058
  register: (buffer) => store(buffer),
537
1059
  buffer: (handle) => buffers.get(handle.id) ?? null
538
1060
  };
539
- return cache;
1061
+ return cache2;
540
1062
  }
541
1063
 
542
1064
  // src/streamStarter.ts
@@ -645,6 +1167,25 @@ function envelopePoints(e, gate) {
645
1167
  return points;
646
1168
  }
647
1169
 
1170
+ // src/periodicWave.ts
1171
+ var cache = /* @__PURE__ */ new WeakMap();
1172
+ function periodicWave(ctx, partials) {
1173
+ let byKey = cache.get(ctx);
1174
+ if (!byKey) {
1175
+ byKey = /* @__PURE__ */ new Map();
1176
+ cache.set(ctx, byKey);
1177
+ }
1178
+ const key = partials.join(",");
1179
+ let wave = byKey.get(key);
1180
+ if (!wave) {
1181
+ const imag = new Float32Array(partials.length + 1);
1182
+ imag.set(partials, 1);
1183
+ wave = ctx.createPeriodicWave(new Float32Array(partials.length + 1), imag);
1184
+ byKey.set(key, wave);
1185
+ }
1186
+ return wave;
1187
+ }
1188
+
648
1189
  // src/liveVoice.ts
649
1190
  var sourceGroup = (nodes) => ({
650
1191
  get onended() {
@@ -907,21 +1448,12 @@ function noteOscillators(ctx, note, { from, glideTo, partials }, applyWave, t0,
907
1448
  return oscs;
908
1449
  }
909
1450
  function createSynthStarters(ctx, random) {
910
- const waves = /* @__PURE__ */ new Map();
911
1451
  const applyWave = (osc, wave) => {
912
1452
  if (typeof wave === "string") {
913
1453
  osc.type = wave;
914
1454
  return;
915
1455
  }
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);
1456
+ osc.setPeriodicWave(periodicWave(ctx, wave));
925
1457
  };
926
1458
  return {
927
1459
  note(note) {
@@ -1672,6 +2204,27 @@ function engineSynth(ctx, opts, busInput) {
1672
2204
  dispose: () => worklet?.dispose()
1673
2205
  };
1674
2206
  }
2207
+ function patchGraphs(ctx, busInput, opts) {
2208
+ const live = /* @__PURE__ */ new Set();
2209
+ return {
2210
+ create(bus) {
2211
+ const g = createAudioGraph(ctx, busInput(bus), opts);
2212
+ live.add(g);
2213
+ return {
2214
+ ...g,
2215
+ dispose() {
2216
+ g.dispose();
2217
+ void g.settled().then(() => live.delete(g));
2218
+ }
2219
+ };
2220
+ },
2221
+ /** Tear every graph down now: the engine is closing, so nothing fades. */
2222
+ dispose() {
2223
+ for (const g of live) disposeNow(g);
2224
+ live.clear();
2225
+ }
2226
+ };
2227
+ }
1675
2228
  function engineClock(ctx, opts) {
1676
2229
  const now = () => ctx.currentTime * 1e3;
1677
2230
  const tickTimer = opts.setTimer === void 0 && opts.clearTimer === void 0 ? createTickTimer() : null;
@@ -1729,6 +2282,7 @@ function createAudioEngine(opts = {}) {
1729
2282
  });
1730
2283
  const synth = engineSynth(ctx, opts, graph.input);
1731
2284
  const streams = createStreamStarters(ctx, timers);
2285
+ const patches = patchGraphs(ctx, graph.input, { ...timers, fadeMs: opts.insertFadeMs });
1732
2286
  const engine = {
1733
2287
  context: ctx,
1734
2288
  state: () => ctx.state,
@@ -1775,6 +2329,10 @@ function createAudioEngine(opts = {}) {
1775
2329
  }
1776
2330
  };
1777
2331
  },
2332
+ graph(bus) {
2333
+ if (disposed) throw new Error("@weasel-js/audio: engine is disposed");
2334
+ return patches.create(bus ?? busNames[0]);
2335
+ },
1778
2336
  setListener(p, o) {
1779
2337
  listener = p;
1780
2338
  if (o) spatialOpts = o;
@@ -1792,6 +2350,7 @@ function createAudioEngine(opts = {}) {
1792
2350
  taps.clear();
1793
2351
  watch.dispose();
1794
2352
  synth.dispose();
2353
+ patches.dispose();
1795
2354
  graph.dispose();
1796
2355
  graph.master.disconnect();
1797
2356
  if (ownsContext) void ctx.close().catch(() => void 0);
@@ -1804,9 +2363,9 @@ function createAudioEngine(opts = {}) {
1804
2363
  function createFilterEffect(ctx, opts = {}) {
1805
2364
  const filter = ctx.createBiquadFilter();
1806
2365
  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);
2366
+ if (opts.frequency !== void 0) initParam(ctx, filter.frequency, opts.frequency);
2367
+ if (opts.Q !== void 0) initParam(ctx, filter.Q, opts.Q);
2368
+ if (opts.gain !== void 0) initParam(ctx, filter.gain, opts.gain);
1810
2369
  return { input: filter, output: filter, filter };
1811
2370
  }
1812
2371
  var checkMix = (mix) => {
@@ -1828,7 +2387,8 @@ function wetDry(ctx, wetIn, wetOut, initial) {
1828
2387
  writeParam(ctx, dry.gain, 1 - mix, rampMs);
1829
2388
  writeParam(ctx, wet.gain, mix, rampMs);
1830
2389
  };
1831
- write();
2390
+ initParam(ctx, dry.gain, 1 - mix);
2391
+ initParam(ctx, wet.gain, mix);
1832
2392
  return {
1833
2393
  input,
1834
2394
  output,
@@ -1861,8 +2421,8 @@ function createDelayEffect(ctx, opts = {}) {
1861
2421
  const m = wetDry(ctx, delay, delay, opts.mix ?? 0.35);
1862
2422
  delay.connect(loop);
1863
2423
  loop.connect(delay);
1864
- writeParam(ctx, delay.delayTime, time / 1e3);
1865
- writeParam(ctx, loop.gain, fb);
2424
+ initParam(ctx, delay.delayTime, time / 1e3);
2425
+ initParam(ctx, loop.gain, fb);
1866
2426
  return {
1867
2427
  input: m.input,
1868
2428
  output: m.output,
@@ -1891,7 +2451,7 @@ function createCompressorEffect(ctx, opts = {}) {
1891
2451
  const makeup = ctx.createGain();
1892
2452
  compressor.connect(makeup);
1893
2453
  const set = (param, value) => {
1894
- if (value !== void 0) writeParam(ctx, param, value);
2454
+ if (value !== void 0) initParam(ctx, param, value);
1895
2455
  };
1896
2456
  set(compressor.threshold, opts.threshold);
1897
2457
  set(compressor.knee, opts.knee);
@@ -2185,15 +2745,15 @@ function scoreBar(params, bar, opts = {}) {
2185
2745
  // src/score.ts
2186
2746
  function createScore(engine, opts) {
2187
2747
  const { mood: initial, onBar, ...barOpts } = opts;
2188
- let target = initial;
2748
+ let target2 = initial;
2189
2749
  const stateAt = (bar, before) => {
2190
- const cue = readScoreCue(target);
2750
+ const cue = readScoreCue(target2);
2191
2751
  return {
2192
2752
  bar,
2193
2753
  cueBar: before && before.cue.name === cue.name ? before.cueBar + 1 : 0,
2194
- mood: target,
2754
+ mood: target2,
2195
2755
  cue,
2196
- params: scoreParams(target)
2756
+ params: scoreParams(target2)
2197
2757
  };
2198
2758
  };
2199
2759
  const eventsOf = (state) => scoreBar(state.params, state.cueBar, barOpts);
@@ -2224,9 +2784,9 @@ function createScore(engine, opts) {
2224
2784
  },
2225
2785
  stop: () => player.stop(),
2226
2786
  playing: () => player.playing(),
2227
- mood: () => target,
2787
+ mood: () => target2,
2228
2788
  setMood(mood) {
2229
- target = mood;
2789
+ target2 = mood;
2230
2790
  },
2231
2791
  state: () => current
2232
2792
  };
@@ -2466,6 +3026,300 @@ function renderNoise(noise, sampleRate, random = Math.random) {
2466
3026
  return renderWhole(createNoiseKernel(noise, sampleRate, random), noise, sampleRate, "renderNoise");
2467
3027
  }
2468
3028
 
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 };
3029
+ // src/impulse.ts
3030
+ function mulberry32(seed) {
3031
+ let a = seed >>> 0;
3032
+ return () => {
3033
+ a = a + 1831565813 >>> 0;
3034
+ let t = a;
3035
+ t = Math.imul(t ^ t >>> 15, t | 1);
3036
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
3037
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
3038
+ };
3039
+ }
3040
+ function impulseResponse(ctx, seconds, opts = {}) {
3041
+ if (!(seconds > 0)) throw new RangeError(`@weasel-js/audio: impulse length ${seconds} s must be above 0`);
3042
+ const decayDb = opts.decayDb ?? 60;
3043
+ const channels = opts.channels ?? 2;
3044
+ const seed = opts.seed ?? 1;
3045
+ const length = Math.max(1, Math.round(seconds * ctx.sampleRate));
3046
+ const buffer = ctx.createBuffer(channels, length, ctx.sampleRate);
3047
+ const rate = decayDb / 20 * Math.LN10;
3048
+ for (let c = 0; c < channels; c += 1) {
3049
+ const data = buffer.getChannelData(c);
3050
+ const rand = mulberry32(seed + c * 2654435761);
3051
+ for (let i = 0; i < length; i += 1) {
3052
+ data[i] = (rand() * 2 - 1) * Math.exp(-rate * i / length);
3053
+ }
3054
+ }
3055
+ return buffer;
3056
+ }
3057
+
3058
+ // src/graph/playhead.ts
3059
+ function createPlayhead(duration, loop, initialRate) {
3060
+ let pos = 0;
3061
+ let from = 0;
3062
+ let until = 0;
3063
+ let t0 = 0;
3064
+ let r0 = initialRate;
3065
+ let t1 = 0;
3066
+ let r1 = initialRate;
3067
+ const rateAt = (t) => {
3068
+ if (t >= t1) return r1;
3069
+ return t <= t0 ? r0 : r0 + (r1 - r0) * (t - t0) / (t1 - t0);
3070
+ };
3071
+ const distance = (t) => t <= t1 ? (t - t0) * (r0 + rateAt(t)) / 2 : (t1 - t0) * (r0 + r1) / 2 + (t - t1) * r1;
3072
+ const wrap = (p) => loop ? (p % duration + duration) % duration : Math.min(Math.max(p, 0), duration);
3073
+ const at = (t) => {
3074
+ const x = Math.min(Math.max(t, from), until);
3075
+ return x <= from ? wrap(pos) : wrap(pos + distance(x) - distance(from));
3076
+ };
3077
+ const hold = (t, rate) => {
3078
+ t0 = t;
3079
+ t1 = t;
3080
+ r0 = rate;
3081
+ r1 = rate;
3082
+ };
3083
+ return {
3084
+ start(when, now) {
3085
+ pos = at(until);
3086
+ from = Math.max(when, now);
3087
+ until = Infinity;
3088
+ hold(now, r1);
3089
+ return pos;
3090
+ },
3091
+ stop(when, now) {
3092
+ until = Math.max(when, now, from);
3093
+ },
3094
+ rate(value, rampMs, now) {
3095
+ pos = at(now);
3096
+ from = Math.min(Math.max(now, from), until);
3097
+ r0 = rateAt(now);
3098
+ t0 = now;
3099
+ t1 = now + Math.max(0, rampMs) / 1e3;
3100
+ r1 = value;
3101
+ },
3102
+ rewind(now) {
3103
+ pos = 0;
3104
+ from = now;
3105
+ until = now;
3106
+ }
3107
+ };
3108
+ }
3109
+
3110
+ // src/graph/sources.ts
3111
+ function sourceShell(ctx, make) {
3112
+ const output = ctx.createGain();
3113
+ let live = null;
3114
+ const ending = /* @__PURE__ */ new Set();
3115
+ return {
3116
+ output,
3117
+ live: () => live,
3118
+ sounding: () => live ? [live, ...ending] : [...ending],
3119
+ /** `onEnd` runs if the node ends while it is still the live one: it played out. */
3120
+ begin(when, setup, offset, onEnd) {
3121
+ const node = make();
3122
+ setup(node);
3123
+ node.connect(output);
3124
+ node.onended = () => {
3125
+ if (live !== node) return;
3126
+ live = null;
3127
+ node.disconnect();
3128
+ onEnd?.();
3129
+ };
3130
+ if (offset === void 0) node.start(when);
3131
+ else node.start(when, offset);
3132
+ live = node;
3133
+ return node;
3134
+ },
3135
+ end(when) {
3136
+ if (!live) return;
3137
+ const node = live;
3138
+ live = null;
3139
+ ending.add(node);
3140
+ node.onended = () => {
3141
+ ending.delete(node);
3142
+ node.disconnect();
3143
+ };
3144
+ node.stop(when);
3145
+ },
3146
+ dispose() {
3147
+ if (live) {
3148
+ live.stop();
3149
+ live.disconnect();
3150
+ live = null;
3151
+ }
3152
+ output.disconnect();
3153
+ },
3154
+ level: (value, rampMs) => writeParam(ctx, output.gain, value, rampMs)
3155
+ };
3156
+ }
3157
+ function createFileSourceNode(ctx, opts) {
3158
+ const { buffer } = opts;
3159
+ const loop = opts.loop ?? true;
3160
+ let rate = 1;
3161
+ const head = createPlayhead(buffer.duration, loop, rate);
3162
+ const shell = sourceShell(ctx, () => ctx.createBufferSource());
3163
+ return {
3164
+ inputs: {},
3165
+ output: shell.output,
3166
+ params: {
3167
+ rate(value, rampMs) {
3168
+ for (const src of shell.sounding()) writeParam(ctx, src.playbackRate, value, rampMs);
3169
+ head.rate(value, rampMs, ctx.currentTime);
3170
+ rate = value;
3171
+ },
3172
+ level: shell.level
3173
+ },
3174
+ start(when, ended) {
3175
+ if (shell.live()) return;
3176
+ const offset = head.start(when, ctx.currentTime);
3177
+ shell.begin(when, (s) => {
3178
+ s.buffer = buffer;
3179
+ s.loop = loop;
3180
+ initParam(ctx, s.playbackRate, rate);
3181
+ }, offset, () => {
3182
+ head.rewind(ctx.currentTime);
3183
+ ended?.();
3184
+ });
3185
+ },
3186
+ stop(when) {
3187
+ if (!shell.live()) return;
3188
+ head.stop(when, ctx.currentTime);
3189
+ shell.end(when);
3190
+ },
3191
+ dispose: shell.dispose
3192
+ };
3193
+ }
3194
+ function createOscillatorNode(ctx, opts) {
3195
+ const wave = opts.wave ?? "sine";
3196
+ let frequency = opts.frequency ?? 440;
3197
+ let detune = 0;
3198
+ const shell = sourceShell(ctx, () => ctx.createOscillator());
3199
+ return {
3200
+ inputs: {},
3201
+ output: shell.output,
3202
+ params: {
3203
+ frequency(value, rampMs) {
3204
+ for (const osc of shell.sounding()) writeParam(ctx, osc.frequency, value, rampMs);
3205
+ frequency = value;
3206
+ },
3207
+ detune(value, rampMs) {
3208
+ for (const osc of shell.sounding()) writeParam(ctx, osc.detune, value, rampMs);
3209
+ detune = value;
3210
+ },
3211
+ level: shell.level
3212
+ },
3213
+ start(when, ended) {
3214
+ if (shell.live()) return;
3215
+ shell.begin(when, (osc) => {
3216
+ if (typeof wave === "string") osc.type = wave;
3217
+ else osc.setPeriodicWave(periodicWave(ctx, wave));
3218
+ initParam(ctx, osc.frequency, frequency);
3219
+ initParam(ctx, osc.detune, detune);
3220
+ }, void 0, ended);
3221
+ },
3222
+ stop: shell.end,
3223
+ dispose: shell.dispose
3224
+ };
3225
+ }
3226
+ function createNoiseNode(ctx, opts) {
3227
+ const shell = sourceShell(ctx, () => ctx.createBufferSource());
3228
+ return {
3229
+ inputs: {},
3230
+ output: shell.output,
3231
+ params: { level: shell.level },
3232
+ start(when, ended) {
3233
+ if (shell.live()) return;
3234
+ shell.begin(when, (src) => {
3235
+ src.buffer = noiseBuffer(ctx, opts.color);
3236
+ src.loop = true;
3237
+ }, void 0, ended);
3238
+ },
3239
+ stop: shell.end,
3240
+ dispose: shell.dispose
3241
+ };
3242
+ }
3243
+
3244
+ // src/graph/processors.ts
3245
+ function createGainNode(ctx, opts = {}) {
3246
+ const node = ctx.createGain();
3247
+ initParam(ctx, node.gain, opts.gain ?? 1);
3248
+ return {
3249
+ inputs: { in: node },
3250
+ output: node,
3251
+ params: { gain: (value, rampMs) => writeParam(ctx, node.gain, value, rampMs) }
3252
+ };
3253
+ }
3254
+ function createPanNode(ctx, opts = {}) {
3255
+ const node = ctx.createStereoPanner();
3256
+ initParam(ctx, node.pan, opts.pan ?? 0);
3257
+ return {
3258
+ inputs: { in: node },
3259
+ output: node,
3260
+ params: { pan: (value, rampMs) => writeParam(ctx, node.pan, value, rampMs) }
3261
+ };
3262
+ }
3263
+ function createMixNode(ctx, opts) {
3264
+ if (opts.inputs.length === 0) throw new RangeError("@weasel-js/audio: a mix needs at least one input");
3265
+ const output = ctx.createGain();
3266
+ const inputs = {};
3267
+ const params = {};
3268
+ for (const name of opts.inputs) {
3269
+ if (Object.prototype.hasOwnProperty.call(inputs, name)) {
3270
+ throw new RangeError(`@weasel-js/audio: a mix has two inputs named "${name}"`);
3271
+ }
3272
+ const level = ctx.createGain();
3273
+ level.connect(output);
3274
+ inputs[name] = level;
3275
+ params[name] = (value, rampMs) => writeParam(ctx, level.gain, value, rampMs);
3276
+ }
3277
+ return { inputs, output, params };
3278
+ }
3279
+ function insertNode(effect, params) {
3280
+ return {
3281
+ inputs: { in: effect.input },
3282
+ output: effect.output,
3283
+ params,
3284
+ ...effect.dispose ? { dispose: () => effect.dispose() } : {}
3285
+ };
3286
+ }
3287
+ function filterNode(ctx, opts = {}) {
3288
+ const { filter } = createFilterEffect(ctx, opts);
3289
+ const write = (p) => (value, rampMs) => writeParam(ctx, p, value, rampMs);
3290
+ return insertNode({ input: filter, output: filter }, {
3291
+ frequency: write(filter.frequency),
3292
+ Q: write(filter.Q),
3293
+ gain: write(filter.gain)
3294
+ });
3295
+ }
3296
+ function delayNode(ctx, opts = {}) {
3297
+ const fx = createDelayEffect(ctx, { ...opts, maxTime: opts.maxTime ?? Math.max(2e3, opts.time ?? 0) });
3298
+ return insertNode(fx, {
3299
+ time: (value, rampMs) => fx.setTime(value, rampMs),
3300
+ feedback: (value, rampMs) => fx.setFeedback(value, rampMs),
3301
+ mix: (value, rampMs) => fx.setMix(value, rampMs)
3302
+ });
3303
+ }
3304
+ function reverbNode(ctx, opts = {}) {
3305
+ const { seconds, impulse, ...rest } = opts;
3306
+ const fx = createReverbEffect(ctx, { ...rest, impulse: impulse ?? impulseResponse(ctx, seconds ?? 2) });
3307
+ return insertNode(fx, { mix: (value, rampMs) => fx.setMix(value, rampMs) });
3308
+ }
3309
+ function compressorNode(ctx, opts = {}) {
3310
+ const fx = createCompressorEffect(ctx, opts);
3311
+ const { compressor, makeup } = fx;
3312
+ const write = (p, scale = 1) => (value, rampMs) => writeParam(ctx, p, value * scale, rampMs);
3313
+ return insertNode(fx, {
3314
+ threshold: write(compressor.threshold),
3315
+ knee: write(compressor.knee),
3316
+ ratio: write(compressor.ratio),
3317
+ attack: write(compressor.attack, 1e-3),
3318
+ release: write(compressor.release, 1e-3),
3319
+ makeup: write(makeup.gain)
3320
+ });
3321
+ }
3322
+
3323
+ 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
3324
  //# sourceMappingURL=index.js.map
2471
3325
  //# sourceMappingURL=index.js.map