@weasel-js/audio 1.10.1 → 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,553 @@ 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
+
901
+ // src/resumeWatch.ts
902
+ function createResumeWatch(ctx, hooks) {
903
+ let disposed = false;
904
+ let suspendedSince = false;
905
+ let staleFloor = -Infinity;
906
+ const gestures = ["pointerdown", "pointerup", "touchend", "click", "keydown"];
907
+ const onGesture = () => {
908
+ void hooks.unlock().catch(() => void 0);
909
+ };
910
+ const arm = () => {
911
+ if (typeof window === "undefined") return;
912
+ for (const g of gestures) window.addEventListener(g, onGesture, { passive: true });
913
+ };
914
+ const disarm = () => {
915
+ if (typeof window === "undefined") return;
916
+ for (const g of gestures) window.removeEventListener(g, onGesture);
917
+ };
918
+ if (ctx.state !== "running") arm();
919
+ const onStateChange = () => {
920
+ if (disposed) return;
921
+ if (ctx.state === "running") {
922
+ disarm();
923
+ if (suspendedSince) staleFloor = hooks.now();
924
+ suspendedSince = false;
925
+ return;
926
+ }
927
+ suspendedSince = true;
928
+ hooks.onSuspend();
929
+ arm();
930
+ };
931
+ ctx.addEventListener("statechange", onStateChange);
932
+ return {
933
+ staleFloor: () => staleFloor,
934
+ dispose() {
935
+ disposed = true;
936
+ disarm();
937
+ ctx.removeEventListener("statechange", onStateChange);
938
+ }
939
+ };
940
+ }
941
+
391
942
  // src/scheduler.ts
392
943
  function createScheduler(opts) {
393
944
  const lookahead = opts.lookahead ?? 100;
@@ -433,9 +984,9 @@ function createScheduler(opts) {
433
984
  opts.clearTimer(handle);
434
985
  handle = null;
435
986
  },
436
- schedule(when, fire, key) {
987
+ schedule(when, fire, key, entryOpts) {
437
988
  const waited = when > opts.now() + lookahead;
438
- queue.push({ when, fire, key, waited });
989
+ queue.push({ when, fire, key, waited: waited && entryOpts?.reportLate !== false });
439
990
  if (running && !flushQueued && !waited) {
440
991
  flushQueued = true;
441
992
  queueMicrotask(flush);
@@ -462,7 +1013,7 @@ function createSoundCache(ctx, fetchFn = fetch) {
462
1013
  buffers.set(id, buffer);
463
1014
  return { id };
464
1015
  };
465
- const cache = {
1016
+ const cache2 = {
466
1017
  async load(url) {
467
1018
  const existing = byUrl.get(url);
468
1019
  if (existing) return existing;
@@ -486,7 +1037,7 @@ function createSoundCache(ctx, fetchFn = fetch) {
486
1037
  },
487
1038
  async loadAll(urls) {
488
1039
  const names = Object.keys(urls);
489
- 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])));
490
1041
  return Object.fromEntries(names.map((n, i) => [n, handles[i]]));
491
1042
  },
492
1043
  async decode(bytes) {
@@ -495,114 +1046,83 @@ function createSoundCache(ctx, fetchFn = fetch) {
495
1046
  register: (buffer) => store(buffer),
496
1047
  buffer: (handle) => buffers.get(handle.id) ?? null
497
1048
  };
498
- return cache;
499
- }
500
-
501
- // src/spatialize.ts
502
- function spatialize(source, listener, opts = {}) {
503
- const refDistance = opts.refDistance ?? 1;
504
- const maxDistance = opts.maxDistance ?? 1e4;
505
- const rolloff = opts.rolloff ?? "inverse";
506
- const rolloffFactor = opts.rolloffFactor ?? 1;
507
- const panWidth = opts.panWidth ?? 500;
508
- const dx = source.x - listener.x;
509
- const dy = source.y - listener.y;
510
- const distance = Math.hypot(dx, dy);
511
- let gain;
512
- if (distance <= refDistance) {
513
- gain = 1;
514
- } else if (rolloff === "linear") {
515
- const span = maxDistance - refDistance;
516
- gain = span <= 0 ? 0 : 1 - rolloffFactor * ((distance - refDistance) / span);
517
- } else {
518
- gain = refDistance / (refDistance + rolloffFactor * (distance - refDistance));
519
- }
520
- gain = Math.min(1, Math.max(0, gain));
521
- const pan = panWidth <= 0 ? 0 : Math.min(1, Math.max(-1, dx / panWidth));
522
- return { gain, pan };
1049
+ return cache2;
523
1050
  }
524
1051
 
525
- // src/tickTimer.ts
526
- var WORKER_SOURCE = `const timers = new Map();
527
- self.onmessage = (e) => {
528
- const id = e.data.id;
529
- clearTimeout(timers.get(id));
530
- timers.delete(id);
531
- if (e.data.ms === undefined) return;
532
- timers.set(id, setTimeout(() => {
533
- timers.delete(id);
534
- self.postMessage(id);
535
- }, e.data.ms));
536
- };
537
- `;
538
- function spawn() {
539
- if (typeof Worker !== "function" || typeof Blob !== "function" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function") {
540
- return null;
541
- }
542
- let url;
543
- try {
544
- url = URL.createObjectURL(new Blob([WORKER_SOURCE], { type: "text/javascript" }));
545
- return { worker: new Worker(url), url };
546
- } catch {
547
- if (url !== void 0) URL.revokeObjectURL(url);
548
- return null;
549
- }
550
- }
551
- function createTickTimer() {
552
- let nextId = 1;
553
- const pending = /* @__PURE__ */ new Map();
554
- let spawned = spawn();
555
- const fire = (id) => {
556
- const p = pending.get(id);
557
- if (!p) return;
558
- pending.delete(id);
559
- p.cb();
560
- };
561
- const arm = (id, p) => {
562
- p.fallback = setTimeout(() => fire(id), p.ms);
563
- };
564
- const release = () => {
565
- if (!spawned) return;
566
- spawned.worker.terminate();
567
- URL.revokeObjectURL(spawned.url);
568
- spawned = null;
569
- };
570
- if (spawned) {
571
- spawned.worker.addEventListener("message", (e) => fire(e.data));
572
- spawned.worker.addEventListener("error", () => {
573
- if (!spawned) return;
574
- release();
575
- for (const [id, p] of pending) arm(id, p);
576
- });
577
- }
578
- return {
579
- setTimer(cb, ms) {
580
- const id = nextId++;
581
- const p = { cb, ms };
582
- pending.set(id, p);
583
- if (spawned) spawned.worker.postMessage({ id, ms });
584
- else arm(id, p);
585
- return id;
586
- },
587
- clearTimer(handle) {
588
- const id = handle;
589
- const p = pending.get(id);
590
- if (!p) return;
591
- pending.delete(id);
592
- if (p.fallback !== void 0) clearTimeout(p.fallback);
593
- else spawned?.worker.postMessage({ id });
1052
+ // src/streamStarter.ts
1053
+ function mediaSource(ctx, timers, el, node, into) {
1054
+ let ended = false;
1055
+ let cancelWait = null;
1056
+ const finish = () => {
1057
+ if (ended) return;
1058
+ ended = true;
1059
+ cancelWait?.();
1060
+ el.pause();
1061
+ el.removeEventListener("ended", finish);
1062
+ el.removeEventListener("error", finish);
1063
+ queueMicrotask(() => shim.onended?.());
1064
+ };
1065
+ const shim = {
1066
+ onended: null,
1067
+ stop(when) {
1068
+ if (ended) return;
1069
+ cancelWait?.();
1070
+ cancelWait = when !== void 0 && when > ctx.currentTime ? atAudioTime(ctx, timers, when, finish) : null;
1071
+ if (!cancelWait) finish();
594
1072
  },
595
- dispose() {
596
- for (const p of pending.values()) {
597
- if (p.fallback !== void 0) clearTimeout(p.fallback);
1073
+ disconnect() {
1074
+ try {
1075
+ node.disconnect(into);
1076
+ } catch {
598
1077
  }
599
- pending.clear();
600
- release();
601
1078
  }
602
1079
  };
1080
+ el.addEventListener("ended", finish);
1081
+ el.addEventListener("error", finish);
1082
+ el.play().catch((err) => {
1083
+ if (ended) return;
1084
+ console.warn(`@weasel-js/audio: a stream would not start \u2014 ${String(err)}`);
1085
+ finish();
1086
+ });
1087
+ return shim;
603
1088
  }
604
-
605
- // src/envelope.ts
1089
+ function createStreamStarters(ctx, timers) {
1090
+ const mediaSources = /* @__PURE__ */ new WeakMap();
1091
+ return {
1092
+ element(media, opts) {
1093
+ if (typeof media !== "string") return media;
1094
+ const el = new Audio();
1095
+ if (opts.crossOrigin !== void 0) el.crossOrigin = opts.crossOrigin;
1096
+ el.preload = "auto";
1097
+ el.src = media;
1098
+ return el;
1099
+ },
1100
+ starter(el, opts) {
1101
+ return {
1102
+ ready: () => true,
1103
+ start(voice) {
1104
+ let node = mediaSources.get(el);
1105
+ if (!node) {
1106
+ node = ctx.createMediaElementSource(el);
1107
+ mediaSources.set(el, node);
1108
+ }
1109
+ el.loop = opts.loop ?? false;
1110
+ if (opts.offset !== void 0) el.currentTime = opts.offset / 1e3;
1111
+ voice.retune = () => {
1112
+ el.playbackRate = voice.rate;
1113
+ };
1114
+ voice.retune();
1115
+ const into = voice.out.input;
1116
+ node.connect(into);
1117
+ return mediaSource(ctx, timers, el, node, into);
1118
+ }
1119
+ };
1120
+ },
1121
+ streaming: /* @__PURE__ */ new WeakMap()
1122
+ };
1123
+ }
1124
+
1125
+ // src/envelope.ts
606
1126
  function resolveEnvelope(env = {}) {
607
1127
  return {
608
1128
  attack: Math.max(0, env.attack ?? 5),
@@ -635,6 +1155,46 @@ function envelopePoints(e, gate) {
635
1155
  return points;
636
1156
  }
637
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
+
1177
+ // src/liveVoice.ts
1178
+ var sourceGroup = (nodes) => ({
1179
+ get onended() {
1180
+ return nodes[0].onended;
1181
+ },
1182
+ set onended(fn) {
1183
+ nodes[0].onended = fn;
1184
+ },
1185
+ stop(when) {
1186
+ for (const n of nodes) {
1187
+ try {
1188
+ n.stop(when);
1189
+ } catch {
1190
+ }
1191
+ }
1192
+ },
1193
+ disconnect() {
1194
+ for (const n of nodes) n.disconnect();
1195
+ }
1196
+ });
1197
+
638
1198
  // src/pitch.ts
639
1199
  function midiToFrequency(midi) {
640
1200
  return 440 * 2 ** ((midi - 69) / 12);
@@ -740,6 +1300,14 @@ function resolvePartials(wave) {
740
1300
  decay: p.decay
741
1301
  }));
742
1302
  }
1303
+ function resolveNote(note) {
1304
+ const wave = note.wave ?? "sine";
1305
+ return {
1306
+ from: toFrequency(note.pitch),
1307
+ glideTo: note.glide ? toFrequency(note.glide.to) : void 0,
1308
+ partials: isInharmonic(wave) ? resolvePartials(wave) : null
1309
+ };
1310
+ }
743
1311
  function envelopeOnParam(param, env, gate, t0, map) {
744
1312
  const [first, ...rest] = envelopePoints(env, gate);
745
1313
  param.setValueAtTime(map(first.value), t0);
@@ -808,6 +1376,409 @@ var partialPresets = {
808
1376
  }
809
1377
  };
810
1378
 
1379
+ // src/synthStarters.ts
1380
+ function synthStart(ctx, voice, whenMs, patch, make) {
1381
+ const t0 = whenMs / 1e3;
1382
+ const env = resolveEnvelope(patch.envelope);
1383
+ const gate = patch.duration;
1384
+ const shaper = ctx.createGain();
1385
+ const amp = envelopeOnParam(shaper.gain, env, gate, t0, (v) => v);
1386
+ const filter = patch.filter ? voiceFilter(ctx, patch.filter, gate, t0) : null;
1387
+ filter?.node.connect(shaper);
1388
+ shaper.connect(voice.out.input);
1389
+ voice.env = shaper;
1390
+ const { nodes, retune } = make(t0, filter?.node ?? shaper);
1391
+ voice.retune = retune;
1392
+ retune();
1393
+ const stopAll = (at) => {
1394
+ for (const n of nodes) n.stop(at);
1395
+ };
1396
+ voice.release = () => {
1397
+ const at = ctx.currentTime;
1398
+ if (gate !== void 0 && at * 1e3 - whenMs >= gate) return;
1399
+ amp.release(at);
1400
+ filter?.release(at);
1401
+ stopAll(at + env.release / 1e3);
1402
+ };
1403
+ if (gate !== void 0) stopAll(t0 + (gate + env.release) / 1e3);
1404
+ return nodes.length === 1 ? nodes[0] : sourceGroup(nodes);
1405
+ }
1406
+ function noteOscillators(ctx, note, { from, glideTo, partials }, applyWave, t0, into) {
1407
+ const wave = note.wave ?? "sine";
1408
+ const tone = (ratio) => {
1409
+ const osc = ctx.createOscillator();
1410
+ osc.frequency.setValueAtTime(from * ratio, t0);
1411
+ if (glideTo !== void 0) {
1412
+ const end = t0 + (note.glide.ms ?? note.duration ?? 100) / 1e3;
1413
+ if (note.glide.curve === "linear") osc.frequency.linearRampToValueAtTime(glideTo * ratio, end);
1414
+ else osc.frequency.exponentialRampToValueAtTime(glideTo * ratio, end);
1415
+ }
1416
+ return osc;
1417
+ };
1418
+ let oscs;
1419
+ if (partials) {
1420
+ oscs = partials.map((p) => {
1421
+ const osc = tone(p.ratio);
1422
+ const level = ctx.createGain();
1423
+ level.gain.setValueAtTime(p.gain, t0);
1424
+ if (p.decay !== void 0) level.gain.setTargetAtTime(0, t0, p.decay / 1e3);
1425
+ osc.connect(level);
1426
+ level.connect(into);
1427
+ return osc;
1428
+ });
1429
+ } else {
1430
+ const osc = tone(1);
1431
+ applyWave(osc, wave);
1432
+ osc.connect(into);
1433
+ oscs = [osc];
1434
+ }
1435
+ for (const osc of oscs) osc.start(t0);
1436
+ return oscs;
1437
+ }
1438
+ function createSynthStarters(ctx, random) {
1439
+ const applyWave = (osc, wave) => {
1440
+ if (typeof wave === "string") {
1441
+ osc.type = wave;
1442
+ return;
1443
+ }
1444
+ osc.setPeriodicWave(periodicWave(ctx, wave));
1445
+ };
1446
+ return {
1447
+ note(note) {
1448
+ const resolved = resolveNote(note);
1449
+ return {
1450
+ ready: () => true,
1451
+ start: (voice, whenMs) => synthStart(ctx, voice, whenMs, note, (t0, into) => {
1452
+ const oscs = noteOscillators(ctx, note, resolved, applyWave, t0, into);
1453
+ return {
1454
+ nodes: oscs,
1455
+ retune: () => {
1456
+ for (const osc of oscs) osc.detune.value = voice.detune + 1200 * Math.log2(voice.rate);
1457
+ }
1458
+ };
1459
+ })
1460
+ };
1461
+ },
1462
+ noise(noise) {
1463
+ return {
1464
+ ready: () => true,
1465
+ start: (voice, whenMs) => synthStart(ctx, voice, whenMs, noise, (t0, into) => {
1466
+ const source = ctx.createBufferSource();
1467
+ source.buffer = noiseBuffer(ctx, noise.noise);
1468
+ source.loop = true;
1469
+ source.connect(into);
1470
+ source.start(t0, random() * source.buffer.duration);
1471
+ return {
1472
+ nodes: [source],
1473
+ retune: () => {
1474
+ source.playbackRate.value = voice.rate;
1475
+ source.detune.value = voice.detune;
1476
+ }
1477
+ };
1478
+ })
1479
+ };
1480
+ }
1481
+ };
1482
+ }
1483
+
1484
+ // src/worklet/loadVoiceProcessor.ts
1485
+ var loads = /* @__PURE__ */ new WeakMap();
1486
+ function loadVoiceProcessor(ctx) {
1487
+ let load = loads.get(ctx);
1488
+ if (!load) {
1489
+ load = (async () => {
1490
+ if (!ctx.audioWorklet) throw new Error("@weasel-js/audio: this context has no audioWorklet");
1491
+ const { voiceProcessorSource } = await import('./voiceProcessor.gen-KWFGBRBP.js');
1492
+ const url = URL.createObjectURL(new Blob([voiceProcessorSource], { type: "text/javascript" }));
1493
+ try {
1494
+ await ctx.audioWorklet.addModule(url);
1495
+ } finally {
1496
+ URL.revokeObjectURL(url);
1497
+ }
1498
+ })();
1499
+ loads.set(ctx, load);
1500
+ }
1501
+ return load;
1502
+ }
1503
+
1504
+ // src/worklet/protocol.ts
1505
+ var VOICE_PROCESSOR = "weasel-voices";
1506
+
1507
+ // src/worklet/workletVoices.ts
1508
+ function workletSource(post, id) {
1509
+ let onended = null;
1510
+ let ended = false;
1511
+ const fire = () => {
1512
+ onended?.();
1513
+ };
1514
+ return {
1515
+ get onended() {
1516
+ return onended;
1517
+ },
1518
+ set onended(fn) {
1519
+ onended = fn;
1520
+ if (ended) queueMicrotask(fire);
1521
+ },
1522
+ stop(when) {
1523
+ post({ kind: "stop", id, at: when ?? 0 });
1524
+ },
1525
+ disconnect() {
1526
+ },
1527
+ end() {
1528
+ if (ended) return;
1529
+ ended = true;
1530
+ fire();
1531
+ }
1532
+ };
1533
+ }
1534
+ function workletOut(ctx, post, id) {
1535
+ return {
1536
+ input: null,
1537
+ level(value, rampMs) {
1538
+ post({ kind: "level", id, value, end: rampMs && rampMs > 0 ? ctx.currentTime + rampMs / 1e3 : 0 });
1539
+ },
1540
+ pan(value) {
1541
+ post({ kind: "pan", id, value });
1542
+ },
1543
+ free() {
1544
+ },
1545
+ abandon() {
1546
+ }
1547
+ };
1548
+ }
1549
+ function createWorkletVoices(ctx, busInput, random) {
1550
+ let ready = false;
1551
+ const loaded = loadVoiceProcessor(ctx).then(
1552
+ () => {
1553
+ ready = true;
1554
+ },
1555
+ (err) => {
1556
+ console.warn(`@weasel-js/audio: the voice worklet did not load, so synth voices stay on nodes \u2014 ${String(err)}`);
1557
+ }
1558
+ );
1559
+ const buses = /* @__PURE__ */ new Map();
1560
+ const busFor = (name) => {
1561
+ let bus = buses.get(name);
1562
+ if (!bus) {
1563
+ const node = new AudioWorkletNode(ctx, VOICE_PROCESSOR, {
1564
+ numberOfInputs: 0,
1565
+ numberOfOutputs: 1,
1566
+ outputChannelCount: [2]
1567
+ });
1568
+ const sources = /* @__PURE__ */ new Map();
1569
+ node.port.onmessage = (e) => {
1570
+ const source = sources.get(e.data.id);
1571
+ sources.delete(e.data.id);
1572
+ source?.end();
1573
+ };
1574
+ node.connect(busInput(name));
1575
+ bus = { node, sources };
1576
+ buses.set(name, bus);
1577
+ }
1578
+ return bus;
1579
+ };
1580
+ const starter = (event) => {
1581
+ let source = null;
1582
+ let post = () => {
1583
+ };
1584
+ return {
1585
+ ready: () => true,
1586
+ book(voice, at) {
1587
+ const bus = busFor(at.bus);
1588
+ post = (e) => {
1589
+ bus.node.port.postMessage(e);
1590
+ };
1591
+ post(event(voice, at.when / 1e3, at.level, at.pan));
1592
+ source = workletSource(post, voice.id);
1593
+ bus.sources.set(voice.id, source);
1594
+ voice.retune = () => {
1595
+ post({ kind: "retune", id: voice.id, rate: voice.rate, detune: voice.detune });
1596
+ };
1597
+ return {
1598
+ out: workletOut(ctx, post, voice.id),
1599
+ cancel: () => {
1600
+ post({ kind: "stop", id: voice.id, at: 0 });
1601
+ }
1602
+ };
1603
+ },
1604
+ start(voice) {
1605
+ voice.release = () => {
1606
+ post({ kind: "release", id: voice.id });
1607
+ };
1608
+ return source;
1609
+ }
1610
+ };
1611
+ };
1612
+ return {
1613
+ ready: () => ready,
1614
+ loaded,
1615
+ note(note) {
1616
+ resolveNote(note);
1617
+ const patch = {
1618
+ pitch: note.pitch,
1619
+ wave: note.wave,
1620
+ envelope: note.envelope,
1621
+ glide: note.glide,
1622
+ filter: note.filter,
1623
+ duration: note.duration
1624
+ };
1625
+ return starter((voice, at, level, pan) => ({
1626
+ kind: "note",
1627
+ id: voice.id,
1628
+ at,
1629
+ level,
1630
+ pan,
1631
+ rate: voice.rate,
1632
+ detune: voice.detune,
1633
+ patch
1634
+ }));
1635
+ },
1636
+ noise(noise) {
1637
+ const patch = { noise: noise.noise, envelope: noise.envelope, filter: noise.filter, duration: noise.duration };
1638
+ return starter((voice, at, level, pan) => ({
1639
+ kind: "noise",
1640
+ id: voice.id,
1641
+ at,
1642
+ level,
1643
+ pan,
1644
+ rate: voice.rate,
1645
+ detune: voice.detune,
1646
+ patch,
1647
+ // A random point in the loop, so two hits in a row are not the same hit.
1648
+ seed: random()
1649
+ }));
1650
+ },
1651
+ dispose() {
1652
+ for (const bus of buses.values()) {
1653
+ bus.node.port.postMessage({ kind: "dispose" });
1654
+ bus.node.port.onmessage = null;
1655
+ bus.node.disconnect();
1656
+ }
1657
+ buses.clear();
1658
+ }
1659
+ };
1660
+ }
1661
+
1662
+ // src/tickTimer.ts
1663
+ var WORKER_SOURCE = `const timers = new Map();
1664
+ self.onmessage = (e) => {
1665
+ const id = e.data.id;
1666
+ clearTimeout(timers.get(id));
1667
+ timers.delete(id);
1668
+ if (e.data.ms === undefined) return;
1669
+ timers.set(id, setTimeout(() => {
1670
+ timers.delete(id);
1671
+ self.postMessage(id);
1672
+ }, e.data.ms));
1673
+ };
1674
+ `;
1675
+ function spawn() {
1676
+ if (typeof Worker !== "function" || typeof Blob !== "function" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function") {
1677
+ return null;
1678
+ }
1679
+ let url;
1680
+ try {
1681
+ url = URL.createObjectURL(new Blob([WORKER_SOURCE], { type: "text/javascript" }));
1682
+ return { worker: new Worker(url), url };
1683
+ } catch {
1684
+ if (url !== void 0) URL.revokeObjectURL(url);
1685
+ return null;
1686
+ }
1687
+ }
1688
+ function createTickTimer() {
1689
+ let nextId = 1;
1690
+ const pending = /* @__PURE__ */ new Map();
1691
+ let spawned = spawn();
1692
+ const fire = (id) => {
1693
+ const p = pending.get(id);
1694
+ if (!p) return;
1695
+ pending.delete(id);
1696
+ p.cb();
1697
+ };
1698
+ const arm = (id, p) => {
1699
+ p.fallback = setTimeout(() => fire(id), p.ms);
1700
+ };
1701
+ const release = () => {
1702
+ if (!spawned) return;
1703
+ spawned.worker.terminate();
1704
+ URL.revokeObjectURL(spawned.url);
1705
+ spawned = null;
1706
+ };
1707
+ if (spawned) {
1708
+ spawned.worker.addEventListener("message", (e) => fire(e.data));
1709
+ spawned.worker.addEventListener("error", () => {
1710
+ if (!spawned) return;
1711
+ release();
1712
+ for (const [id, p] of pending) arm(id, p);
1713
+ });
1714
+ }
1715
+ return {
1716
+ setTimer(cb, ms) {
1717
+ const id = nextId++;
1718
+ const p = { cb, ms };
1719
+ pending.set(id, p);
1720
+ if (spawned) spawned.worker.postMessage({ id, ms });
1721
+ else arm(id, p);
1722
+ return id;
1723
+ },
1724
+ clearTimer(handle) {
1725
+ const id = handle;
1726
+ const p = pending.get(id);
1727
+ if (!p) return;
1728
+ pending.delete(id);
1729
+ if (p.fallback !== void 0) clearTimeout(p.fallback);
1730
+ else spawned?.worker.postMessage({ id });
1731
+ },
1732
+ dispose() {
1733
+ for (const p of pending.values()) {
1734
+ if (p.fallback !== void 0) clearTimeout(p.fallback);
1735
+ }
1736
+ pending.clear();
1737
+ release();
1738
+ }
1739
+ };
1740
+ }
1741
+
1742
+ // src/voiceChains.ts
1743
+ function createChainPool(ctx, limit) {
1744
+ const idle = [];
1745
+ const newChain = () => {
1746
+ const chain = { gain: ctx.createGain(), panner: ctx.createStereoPanner() };
1747
+ chain.panner.connect(chain.gain);
1748
+ return chain;
1749
+ };
1750
+ return {
1751
+ take(bus, gain, pan) {
1752
+ const chain = idle.pop() ?? newChain();
1753
+ const { gain: amp, panner } = chain;
1754
+ writeParam(ctx, amp.gain, gain);
1755
+ panner.pan.value = pan;
1756
+ amp.connect(bus);
1757
+ let held = true;
1758
+ return {
1759
+ input: panner,
1760
+ level: (value, rampMs) => writeParam(ctx, amp.gain, value, rampMs),
1761
+ pan: (value) => {
1762
+ panner.pan.value = value;
1763
+ },
1764
+ free() {
1765
+ if (!held) return;
1766
+ held = false;
1767
+ amp.disconnect();
1768
+ if (idle.length < limit) idle.push(chain);
1769
+ },
1770
+ abandon() {
1771
+ held = false;
1772
+ amp.disconnect();
1773
+ }
1774
+ };
1775
+ },
1776
+ clear() {
1777
+ idle.length = 0;
1778
+ }
1779
+ };
1780
+ }
1781
+
811
1782
  // src/voiceDrops.ts
812
1783
  var reportDrop = (onDrop, reason) => {
813
1784
  if (onDrop) queueMicrotask(() => onDrop(reason));
@@ -891,35 +1862,304 @@ function createVoicePool(opts) {
891
1862
  };
892
1863
  }
893
1864
 
894
- // src/createAudioEngine.ts
895
- var sourceGroup = (nodes) => ({
896
- get onended() {
897
- return nodes[0].onended;
1865
+ // src/spatialize.ts
1866
+ function spatialize(source, listener, opts = {}) {
1867
+ const refDistance = opts.refDistance ?? 1;
1868
+ const maxDistance = opts.maxDistance ?? 1e4;
1869
+ const rolloff = opts.rolloff ?? "inverse";
1870
+ const rolloffFactor = opts.rolloffFactor ?? 1;
1871
+ const panWidth = opts.panWidth ?? 500;
1872
+ const dx = source.x - listener.x;
1873
+ const dy = source.y - listener.y;
1874
+ const distance = Math.hypot(dx, dy);
1875
+ let gain;
1876
+ if (distance <= refDistance) {
1877
+ gain = 1;
1878
+ } else if (rolloff === "linear") {
1879
+ const span = maxDistance - refDistance;
1880
+ gain = span <= 0 ? 0 : 1 - rolloffFactor * ((distance - refDistance) / span);
1881
+ } else {
1882
+ gain = refDistance / (refDistance + rolloffFactor * (distance - refDistance));
1883
+ }
1884
+ gain = Math.min(1, Math.max(0, gain));
1885
+ const pan = panWidth <= 0 ? 0 : Math.min(1, Math.max(-1, dx / panWidth));
1886
+ return { gain, pan };
1887
+ }
1888
+
1889
+ // src/voices.ts
1890
+ var dropped = (id) => ({
1891
+ id,
1892
+ stop: () => {
898
1893
  },
899
- set onended(fn) {
900
- nodes[0].onended = fn;
1894
+ release: () => {
901
1895
  },
902
- stop(when) {
903
- for (const n of nodes) {
904
- try {
905
- n.stop(when);
906
- } catch {
907
- }
908
- }
1896
+ setGain: () => {
909
1897
  },
910
- disconnect() {
911
- for (const n of nodes) n.disconnect();
912
- }
1898
+ setRate: () => {
1899
+ },
1900
+ setDetune: () => {
1901
+ },
1902
+ setPan: () => {
1903
+ },
1904
+ setPosition: () => {
1905
+ },
1906
+ isPlaying: () => false
913
1907
  });
914
- function createAudioEngine(opts = {}) {
915
- const ctx = opts.context ?? new AudioContext();
916
- const ownsContext = opts.context === void 0;
917
- const random = opts.random ?? Math.random;
918
- const busNames = opts.buses ?? ["sfx", "music", "ui"];
919
- if (busNames.length === 0) {
920
- throw new Error("@weasel-js/audio: an engine needs at least one bus");
1908
+ function teardownVoice(voice, reason, live) {
1909
+ voice.canceled = true;
1910
+ if (!voice.playing && voice.source === null) return;
1911
+ voice.playing = false;
1912
+ if (voice.source === null) {
1913
+ voice.unbook?.();
1914
+ reportDrop(voice.onDrop, reason);
921
1915
  }
922
- const sounds = createSoundCache(ctx, opts.fetchFn);
1916
+ voice.unbook = null;
1917
+ if (voice.source) {
1918
+ try {
1919
+ voice.source.stop();
1920
+ } catch {
1921
+ }
1922
+ voice.source.disconnect();
1923
+ }
1924
+ voice.source = null;
1925
+ voice.retune = null;
1926
+ voice.release = null;
1927
+ voice.env?.disconnect();
1928
+ voice.env = null;
1929
+ voice.out?.free();
1930
+ voice.out = null;
1931
+ live.delete(voice.id);
1932
+ if (voice.slot !== null) {
1933
+ voice.slots.byToken.delete(voice.token);
1934
+ voice.slots.pool.release(voice.slot, voice.token);
1935
+ }
1936
+ }
1937
+ function startVoice(voice, starter, bookedAt, scheduledWhen, onDone, staleFloor, teardown) {
1938
+ if (voice.canceled || voice.source !== null) return;
1939
+ const booked = voice.unbook !== null;
1940
+ if (!booked && bookedAt < staleFloor && scheduledWhen < staleFloor) {
1941
+ teardown(voice, "stale");
1942
+ return;
1943
+ }
1944
+ if (!starter.ready()) {
1945
+ teardown(voice, "unready");
1946
+ return;
1947
+ }
1948
+ const { slots } = voice;
1949
+ const acquired = slots.pool.acquire({
1950
+ startedAt: scheduledWhen,
1951
+ gain: voice.baseGain * voice.spatialGain
1952
+ });
1953
+ voice.slot = acquired.slot;
1954
+ voice.token = acquired.token;
1955
+ slots.byToken.set(acquired.token, voice);
1956
+ if (acquired.stolen !== null) {
1957
+ const victim = slots.byToken.get(acquired.stolen);
1958
+ if (victim) teardown(victim);
1959
+ }
1960
+ let source;
1961
+ try {
1962
+ source = starter.start(voice, scheduledWhen);
1963
+ } catch (err) {
1964
+ voice.out?.abandon();
1965
+ voice.out = null;
1966
+ teardown(voice, "failed");
1967
+ throw err;
1968
+ }
1969
+ voice.unbook = null;
1970
+ voice.catchUp = null;
1971
+ source.onended = () => {
1972
+ teardown(voice);
1973
+ onDone?.();
1974
+ };
1975
+ voice.source = source;
1976
+ }
1977
+ function voiceHandle(ctx, voice, ops) {
1978
+ const { teardown, applySpatial, poolGain } = ops;
1979
+ return {
1980
+ id: voice.id,
1981
+ stop(fadeMs) {
1982
+ voice.catchUp?.();
1983
+ if (fadeMs && fadeMs > 0 && voice.source && voice.out && !voice.canceled) {
1984
+ voice.canceled = true;
1985
+ voice.fading = true;
1986
+ voice.out.level(0, fadeMs);
1987
+ try {
1988
+ voice.source.stop(ctx.currentTime + fadeMs / 1e3);
1989
+ } catch {
1990
+ }
1991
+ return;
1992
+ }
1993
+ teardown(voice);
1994
+ },
1995
+ release() {
1996
+ if (voice.releasing) return;
1997
+ voice.catchUp?.();
1998
+ if (voice.release && voice.source && !voice.canceled) {
1999
+ voice.releasing = true;
2000
+ voice.canceled = true;
2001
+ voice.release();
2002
+ return;
2003
+ }
2004
+ teardown(voice);
2005
+ },
2006
+ setGain(value, rampMs) {
2007
+ voice.baseGain = value;
2008
+ if (!voice.out || voice.fading) return;
2009
+ voice.rampEnd = rampMs && rampMs > 0 ? ctx.currentTime + rampMs / 1e3 : 0;
2010
+ voice.out.level(value * voice.spatialGain, rampMs);
2011
+ poolGain(voice);
2012
+ },
2013
+ setRate(value) {
2014
+ voice.rate = value;
2015
+ voice.retune?.();
2016
+ },
2017
+ setDetune(cents) {
2018
+ voice.detune = cents;
2019
+ voice.retune?.();
2020
+ },
2021
+ setPan(value) {
2022
+ voice.position = void 0;
2023
+ voice.out?.pan(value);
2024
+ },
2025
+ setPosition(p) {
2026
+ voice.position = p;
2027
+ applySpatial(voice);
2028
+ },
2029
+ isPlaying: () => voice.playing
2030
+ };
2031
+ }
2032
+ function createVoices(deps) {
2033
+ const { ctx, chains, warn } = deps;
2034
+ const live = /* @__PURE__ */ new Map();
2035
+ let nextVoiceId = 1;
2036
+ const spatialOf = (p) => {
2037
+ const l = deps.listener();
2038
+ return spatialize(p, l.at, l.opts);
2039
+ };
2040
+ const poolGain = (voice) => {
2041
+ if (voice.slot === null) return;
2042
+ voice.slots.pool.setGain(voice.slot, voice.token, voice.baseGain * voice.spatialGain);
2043
+ };
2044
+ const applySpatial = (voice) => {
2045
+ if (!voice.position || !voice.out) return;
2046
+ const s = spatialOf(voice.position);
2047
+ voice.spatialGain = s.gain;
2048
+ voice.out.pan(s.pan);
2049
+ if (!voice.fading) {
2050
+ const left = (voice.rampEnd - ctx.currentTime) * 1e3;
2051
+ voice.out.level(voice.baseGain * s.gain, left > 0 ? left : void 0);
2052
+ }
2053
+ poolGain(voice);
2054
+ };
2055
+ const teardown = (voice, reason = "stopped") => {
2056
+ teardownVoice(voice, reason, live);
2057
+ };
2058
+ const ops = { teardown, applySpatial, poolGain };
2059
+ return {
2060
+ launch(common, starter) {
2061
+ const id = nextVoiceId++;
2062
+ if (deps.disposed()) {
2063
+ warn.disposed();
2064
+ reportDrop(common.onDrop, "disposed");
2065
+ return dropped(id);
2066
+ }
2067
+ if (ctx.state !== "running") {
2068
+ warn.locked();
2069
+ reportDrop(common.onDrop, "locked");
2070
+ return dropped(id);
2071
+ }
2072
+ const busName = common.bus ?? deps.defaultBus;
2073
+ const slots = deps.slotsFor(busName);
2074
+ const bookedAt = ctx.currentTime * 1e3;
2075
+ const when = common.when ?? bookedAt;
2076
+ const baseGain = common.gain ?? 1;
2077
+ const spatial = common.position ? spatialOf(common.position) : { gain: 1, pan: common.pan ?? 0 };
2078
+ const level = baseGain * spatial.gain;
2079
+ const voice = {
2080
+ id,
2081
+ slots,
2082
+ slot: null,
2083
+ token: 0,
2084
+ key: common.cancelKey,
2085
+ onDrop: common.onDrop,
2086
+ source: null,
2087
+ env: null,
2088
+ retune: null,
2089
+ release: null,
2090
+ releasing: false,
2091
+ out: null,
2092
+ unbook: null,
2093
+ catchUp: null,
2094
+ baseGain,
2095
+ spatialGain: spatial.gain,
2096
+ rate: common.rate ?? 1,
2097
+ detune: common.detune ?? 0,
2098
+ position: common.position,
2099
+ playing: true,
2100
+ canceled: false,
2101
+ fading: false,
2102
+ rampEnd: 0
2103
+ };
2104
+ if (starter.book) {
2105
+ const booking = starter.book(voice, { bus: busName, when, level, pan: spatial.pan });
2106
+ voice.out = booking.out;
2107
+ voice.unbook = booking.cancel;
2108
+ } else {
2109
+ voice.out = chains.take(deps.busInput(busName), level, spatial.pan);
2110
+ }
2111
+ live.set(id, voice);
2112
+ const begin = () => {
2113
+ startVoice(voice, starter, bookedAt, when, common.onDone, deps.staleFloor(), teardown);
2114
+ };
2115
+ if (starter.book) {
2116
+ voice.catchUp = () => {
2117
+ if (ctx.currentTime * 1e3 >= when) begin();
2118
+ };
2119
+ }
2120
+ deps.scheduler.schedule(when, begin, common.cancelKey, { reportLate: !starter.book });
2121
+ return voiceHandle(ctx, voice, ops);
2122
+ },
2123
+ stopAll(reason) {
2124
+ for (const voice of [...live.values()]) teardown(voice, reason);
2125
+ },
2126
+ stopKey(key) {
2127
+ for (const voice of [...live.values()]) {
2128
+ if (voice.key !== key) continue;
2129
+ voice.catchUp?.();
2130
+ teardown(voice);
2131
+ }
2132
+ },
2133
+ respatialize() {
2134
+ for (const voice of live.values()) applySpatial(voice);
2135
+ }
2136
+ };
2137
+ }
2138
+
2139
+ // src/createAudioEngine.ts
2140
+ function bufferStarter(ctx, sounds, warn, sound, loop) {
2141
+ return {
2142
+ ready() {
2143
+ if (sounds.buffer(sound)) return true;
2144
+ warn.unknownSound(sound.id);
2145
+ return false;
2146
+ },
2147
+ start(voice, when) {
2148
+ const source = ctx.createBufferSource();
2149
+ source.buffer = sounds.buffer(sound);
2150
+ source.loop = loop;
2151
+ voice.retune = () => {
2152
+ source.playbackRate.value = voice.rate;
2153
+ source.detune.value = voice.detune;
2154
+ };
2155
+ voice.retune();
2156
+ source.connect(voice.out.input);
2157
+ source.start(when / 1e3);
2158
+ return source;
2159
+ }
2160
+ };
2161
+ }
2162
+ function busSlots(busNames, opts) {
923
2163
  const slotsByBus = /* @__PURE__ */ new Map();
924
2164
  for (const name of busNames) {
925
2165
  slotsByBus.set(name, {
@@ -932,6 +2172,48 @@ function createAudioEngine(opts = {}) {
932
2172
  if (!s) throw new Error(`@weasel-js/audio: unknown bus "${bus}"`);
933
2173
  return s;
934
2174
  };
2175
+ const active = (bus) => {
2176
+ if (bus !== void 0) return slotsFor(bus).pool.active();
2177
+ let total = 0;
2178
+ for (const s of slotsByBus.values()) total += s.pool.active();
2179
+ return total;
2180
+ };
2181
+ return { slotsFor, active };
2182
+ }
2183
+ function engineSynth(ctx, opts, busInput) {
2184
+ const random = opts.random ?? Math.random;
2185
+ const nodes = createSynthStarters(ctx, random);
2186
+ const worklet = opts.synthVoices === "worklet" ? createWorkletVoices(ctx, busInput, random) : null;
2187
+ const pick = () => worklet?.ready() ? worklet : nodes;
2188
+ return {
2189
+ note: (note) => pick().note(note),
2190
+ noise: (noise) => pick().noise(noise),
2191
+ loaded: worklet?.loaded,
2192
+ dispose: () => worklet?.dispose()
2193
+ };
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
+ }
2216
+ function engineClock(ctx, opts) {
935
2217
  const now = () => ctx.currentTime * 1e3;
936
2218
  const tickTimer = opts.setTimer === void 0 && opts.clearTimer === void 0 ? createTickTimer() : null;
937
2219
  const timers = {
@@ -950,456 +2232,66 @@ function createAudioEngine(opts = {}) {
950
2232
  onLate: opts.onLate
951
2233
  });
952
2234
  scheduler.start();
2235
+ return { now, tickTimer, timers, scheduler };
2236
+ }
2237
+ function createAudioEngine(opts = {}) {
2238
+ const ctx = opts.context ?? new AudioContext();
2239
+ const ownsContext = opts.context === void 0;
2240
+ const busNames = opts.buses ?? ["sfx", "music", "ui"];
2241
+ if (busNames.length === 0) {
2242
+ throw new Error("@weasel-js/audio: an engine needs at least one bus");
2243
+ }
2244
+ const sounds = createSoundCache(ctx, opts.fetchFn);
2245
+ const { slotsFor, active } = busSlots(busNames, opts);
2246
+ const { now, tickTimer, timers, scheduler } = engineClock(ctx, opts);
953
2247
  const graph = createBusGraph(ctx, busNames, { ...timers, fadeMs: opts.insertFadeMs });
954
2248
  let listener = { x: 0, y: 0 };
955
2249
  let spatialOpts = {};
956
- let nextVoiceId = 1;
957
2250
  let disposed = false;
958
- let suspendedSince = false;
959
- let staleFloor = -Infinity;
960
- const live = /* @__PURE__ */ new Map();
961
2251
  const taps = /* @__PURE__ */ new Set();
962
- const idleChains = [];
963
- const idleChainLimit = (opts.voiceLimit ?? 32) * busNames.length;
964
- const takeChain = (bus, gain, pan) => {
965
- let chain = idleChains.pop();
966
- if (!chain) {
967
- chain = { gain: ctx.createGain(), panner: ctx.createStereoPanner() };
968
- chain.panner.connect(chain.gain);
969
- }
970
- writeParam(ctx, chain.gain.gain, gain);
971
- chain.panner.pan.value = pan;
972
- chain.gain.connect(bus);
973
- return chain;
974
- };
2252
+ const chains = createChainPool(ctx, (opts.voiceLimit ?? 32) * busNames.length);
975
2253
  const warn = createDropWarnings();
976
- const gestures = ["pointerdown", "pointerup", "touchend", "click", "keydown"];
977
- const onGesture = () => {
978
- void engine.unlock().catch(() => void 0);
979
- };
980
- const armGestures = () => {
981
- if (typeof window === "undefined") return;
982
- for (const g of gestures) window.addEventListener(g, onGesture, { passive: true });
983
- };
984
- const disarmGestures = () => {
985
- if (typeof window === "undefined") return;
986
- for (const g of gestures) window.removeEventListener(g, onGesture);
987
- };
988
- if (ctx.state !== "running") armGestures();
989
- const onStateChange = () => {
990
- if (disposed) return;
991
- if (ctx.state === "running") {
992
- disarmGestures();
993
- if (suspendedSince) staleFloor = now();
994
- suspendedSince = false;
995
- return;
996
- }
997
- suspendedSince = true;
998
- warn.rearmLocked();
999
- armGestures();
1000
- };
1001
- ctx.addEventListener("statechange", onStateChange);
1002
- const poolGain = (voice) => {
1003
- if (voice.slot === null) return;
1004
- voice.slots.pool.setGain(voice.slot, voice.token, voice.baseGain * voice.spatialGain);
1005
- };
1006
- const applySpatial = (voice) => {
1007
- if (!voice.position || !voice.chain) return;
1008
- const s = spatialize(voice.position, listener, spatialOpts);
1009
- voice.spatialGain = s.gain;
1010
- voice.chain.panner.pan.value = s.pan;
1011
- if (!voice.fading) {
1012
- const left = (voice.rampEnd - ctx.currentTime) * 1e3;
1013
- writeParam(ctx, voice.chain.gain.gain, voice.baseGain * s.gain, left > 0 ? left : void 0);
1014
- }
1015
- poolGain(voice);
1016
- };
1017
- const teardown = (voice, reason = "stopped") => {
1018
- voice.cancelled = true;
1019
- if (!voice.playing && voice.source === null) return;
1020
- voice.playing = false;
1021
- if (voice.source === null) reportDrop(voice.onDrop, reason);
1022
- if (voice.source) {
1023
- try {
1024
- voice.source.stop();
1025
- } catch {
1026
- }
1027
- voice.source.disconnect();
1028
- }
1029
- voice.source = null;
1030
- voice.retune = null;
1031
- voice.release = null;
1032
- voice.env?.disconnect();
1033
- voice.env = null;
1034
- if (voice.chain) {
1035
- voice.chain.gain.disconnect();
1036
- if (idleChains.length < idleChainLimit) idleChains.push(voice.chain);
1037
- voice.chain = null;
1038
- }
1039
- live.delete(voice.id);
1040
- if (voice.slot !== null) {
1041
- voice.slots.byToken.delete(voice.token);
1042
- voice.slots.pool.release(voice.slot, voice.token);
1043
- }
1044
- };
1045
- const stopVoices = (reason) => {
1046
- for (const voice of [...live.values()]) teardown(voice, reason);
1047
- };
1048
- const dropped = (id) => ({
1049
- id,
1050
- stop: () => {
1051
- },
1052
- release: () => {
1053
- },
1054
- setGain: () => {
1055
- },
1056
- setRate: () => {
1057
- },
1058
- setDetune: () => {
1059
- },
1060
- setPan: () => {
1061
- },
1062
- setPosition: () => {
1063
- },
1064
- isPlaying: () => false
2254
+ const watch = createResumeWatch(ctx, {
2255
+ now,
2256
+ unlock: () => engine.unlock(),
2257
+ onSuspend: () => warn.rearmLocked()
1065
2258
  });
1066
- const launch = (common, starter) => {
1067
- const id = nextVoiceId++;
1068
- if (disposed) {
1069
- warn.disposed();
1070
- reportDrop(common.onDrop, "disposed");
1071
- return dropped(id);
1072
- }
1073
- if (ctx.state !== "running") {
1074
- warn.locked();
1075
- reportDrop(common.onDrop, "locked");
1076
- return dropped(id);
1077
- }
1078
- const busName = common.bus ?? busNames[0];
1079
- const slots = slotsFor(busName);
1080
- const bookedAt = now();
1081
- const when = common.when ?? bookedAt;
1082
- const baseGain = common.gain ?? 1;
1083
- const spatial = common.position ? spatialize(common.position, listener, spatialOpts) : { gain: 1, pan: common.pan ?? 0 };
1084
- const voice = {
1085
- id,
1086
- slots,
1087
- slot: null,
1088
- token: 0,
1089
- key: common.cancelKey,
1090
- onDrop: common.onDrop,
1091
- source: null,
1092
- env: null,
1093
- retune: null,
1094
- release: null,
1095
- releasing: false,
1096
- chain: takeChain(graph.input(busName), baseGain * spatial.gain, spatial.pan),
1097
- baseGain,
1098
- spatialGain: spatial.gain,
1099
- rate: common.rate ?? 1,
1100
- detune: common.detune ?? 0,
1101
- position: common.position,
1102
- playing: true,
1103
- cancelled: false,
1104
- fading: false,
1105
- rampEnd: 0
1106
- };
1107
- live.set(id, voice);
1108
- scheduler.schedule(when, (scheduledWhen) => {
1109
- if (voice.cancelled) return;
1110
- if (bookedAt < staleFloor && scheduledWhen < staleFloor) {
1111
- teardown(voice, "stale");
1112
- return;
1113
- }
1114
- if (!starter.ready()) {
1115
- teardown(voice, "unready");
1116
- return;
1117
- }
1118
- const acquired = slots.pool.acquire({
1119
- startedAt: scheduledWhen,
1120
- gain: voice.baseGain * voice.spatialGain
1121
- });
1122
- voice.slot = acquired.slot;
1123
- voice.token = acquired.token;
1124
- slots.byToken.set(acquired.token, voice);
1125
- if (acquired.stolen !== null) {
1126
- const victim = slots.byToken.get(acquired.stolen);
1127
- if (victim) teardown(victim);
1128
- }
1129
- let source;
1130
- try {
1131
- source = starter.start(voice, scheduledWhen);
1132
- } catch (err) {
1133
- voice.chain?.gain.disconnect();
1134
- voice.chain = null;
1135
- teardown(voice, "failed");
1136
- throw err;
1137
- }
1138
- source.onended = () => {
1139
- teardown(voice);
1140
- common.onDone?.();
1141
- };
1142
- voice.source = source;
1143
- }, common.cancelKey);
1144
- return {
1145
- id,
1146
- stop(fadeMs) {
1147
- if (fadeMs && fadeMs > 0 && voice.source && voice.chain && !voice.cancelled) {
1148
- voice.cancelled = true;
1149
- voice.fading = true;
1150
- writeParam(ctx, voice.chain.gain.gain, 0, fadeMs);
1151
- try {
1152
- voice.source.stop(ctx.currentTime + fadeMs / 1e3);
1153
- } catch {
1154
- }
1155
- return;
1156
- }
1157
- teardown(voice);
1158
- },
1159
- release() {
1160
- if (voice.releasing) return;
1161
- if (voice.release && voice.source && !voice.cancelled) {
1162
- voice.releasing = true;
1163
- voice.cancelled = true;
1164
- voice.release();
1165
- return;
1166
- }
1167
- teardown(voice);
1168
- },
1169
- setGain(value, rampMs) {
1170
- voice.baseGain = value;
1171
- if (!voice.chain || voice.fading) return;
1172
- voice.rampEnd = rampMs && rampMs > 0 ? ctx.currentTime + rampMs / 1e3 : 0;
1173
- writeParam(ctx, voice.chain.gain.gain, value * voice.spatialGain, rampMs);
1174
- poolGain(voice);
1175
- },
1176
- setRate(value) {
1177
- voice.rate = value;
1178
- voice.retune?.();
1179
- },
1180
- setDetune(cents) {
1181
- voice.detune = cents;
1182
- voice.retune?.();
1183
- },
1184
- setPan(value) {
1185
- voice.position = void 0;
1186
- if (voice.chain) voice.chain.panner.pan.value = value;
1187
- },
1188
- setPosition(p) {
1189
- voice.position = p;
1190
- applySpatial(voice);
1191
- },
1192
- isPlaying: () => voice.playing
1193
- };
1194
- };
1195
- const synthStart = (voice, whenMs, patch, make) => {
1196
- const t0 = whenMs / 1e3;
1197
- const env = resolveEnvelope(patch.envelope);
1198
- const gate = patch.duration;
1199
- const shaper = ctx.createGain();
1200
- const amp = envelopeOnParam(shaper.gain, env, gate, t0, (v) => v);
1201
- const filter = patch.filter ? voiceFilter(ctx, patch.filter, gate, t0) : null;
1202
- filter?.node.connect(shaper);
1203
- shaper.connect(voice.chain.panner);
1204
- voice.env = shaper;
1205
- const { nodes, retune } = make(t0, filter?.node ?? shaper);
1206
- voice.retune = retune;
1207
- retune();
1208
- const stopAll = (at) => {
1209
- for (const n of nodes) n.stop(at);
1210
- };
1211
- voice.release = () => {
1212
- const at = ctx.currentTime;
1213
- if (gate !== void 0 && at * 1e3 - whenMs >= gate) return;
1214
- amp.release(at);
1215
- filter?.release(at);
1216
- stopAll(at + env.release / 1e3);
1217
- };
1218
- if (gate !== void 0) stopAll(t0 + (gate + env.release) / 1e3);
1219
- return nodes.length === 1 ? nodes[0] : sourceGroup(nodes);
1220
- };
1221
- const waves = /* @__PURE__ */ new Map();
1222
- const applyWave = (osc, wave) => {
1223
- if (typeof wave === "string") {
1224
- osc.type = wave;
1225
- return;
1226
- }
1227
- const key = wave.join(",");
1228
- let periodic = waves.get(key);
1229
- if (!periodic) {
1230
- const imag = new Float32Array(wave.length + 1);
1231
- imag.set(wave, 1);
1232
- periodic = ctx.createPeriodicWave(new Float32Array(wave.length + 1), imag);
1233
- waves.set(key, periodic);
1234
- }
1235
- osc.setPeriodicWave(periodic);
1236
- };
1237
- const mediaSources = /* @__PURE__ */ new WeakMap();
1238
- const streaming = /* @__PURE__ */ new WeakMap();
1239
- const openMedia = (url, crossOrigin) => {
1240
- const el = new Audio();
1241
- if (crossOrigin !== void 0) el.crossOrigin = crossOrigin;
1242
- el.preload = "auto";
1243
- el.src = url;
1244
- return el;
1245
- };
1246
- const mediaSource = (el, node, into) => {
1247
- let ended = false;
1248
- let cancelWait = null;
1249
- const finish = () => {
1250
- if (ended) return;
1251
- ended = true;
1252
- cancelWait?.();
1253
- el.pause();
1254
- el.removeEventListener("ended", finish);
1255
- el.removeEventListener("error", finish);
1256
- queueMicrotask(() => shim.onended?.());
1257
- };
1258
- const shim = {
1259
- onended: null,
1260
- stop(when) {
1261
- if (ended) return;
1262
- cancelWait?.();
1263
- cancelWait = when !== void 0 && when > ctx.currentTime ? atAudioTime(ctx, timers, when, finish) : null;
1264
- if (!cancelWait) finish();
1265
- },
1266
- disconnect() {
1267
- try {
1268
- node.disconnect(into);
1269
- } catch {
1270
- }
1271
- }
1272
- };
1273
- el.addEventListener("ended", finish);
1274
- el.addEventListener("error", finish);
1275
- el.play().catch((err) => {
1276
- if (ended) return;
1277
- console.warn(`@weasel-js/audio: a stream would not start \u2014 ${String(err)}`);
1278
- finish();
1279
- });
1280
- return shim;
1281
- };
2259
+ const voices = createVoices({
2260
+ ctx,
2261
+ scheduler,
2262
+ chains,
2263
+ slotsFor,
2264
+ warn,
2265
+ busInput: graph.input,
2266
+ defaultBus: busNames[0],
2267
+ staleFloor: watch.staleFloor,
2268
+ disposed: () => disposed,
2269
+ listener: () => ({ at: listener, opts: spatialOpts })
2270
+ });
2271
+ const synth = engineSynth(ctx, opts, graph.input);
2272
+ const streams = createStreamStarters(ctx, timers);
2273
+ const patches = patchGraphs(ctx, graph.input, { ...timers, fadeMs: opts.insertFadeMs });
1282
2274
  const engine = {
1283
2275
  context: ctx,
1284
2276
  state: () => ctx.state,
1285
2277
  async unlock() {
1286
2278
  if (disposed) return;
1287
2279
  if (ctx.state !== "running") await ctx.resume();
2280
+ await synth.loaded;
1288
2281
  },
1289
2282
  now,
1290
2283
  load: sounds.load,
1291
2284
  loadAll: sounds.loadAll,
1292
2285
  decode: sounds.decode,
1293
2286
  register: sounds.register,
1294
- play(sound, playOpts = {}) {
1295
- return launch(playOpts, {
1296
- ready() {
1297
- if (sounds.buffer(sound)) return true;
1298
- warn.unknownSound(sound.id);
1299
- return false;
1300
- },
1301
- start(voice, when) {
1302
- const source = ctx.createBufferSource();
1303
- source.buffer = sounds.buffer(sound);
1304
- source.loop = playOpts.loop ?? false;
1305
- voice.retune = () => {
1306
- source.playbackRate.value = voice.rate;
1307
- source.detune.value = voice.detune;
1308
- };
1309
- voice.retune();
1310
- source.connect(voice.chain.panner);
1311
- source.start(when / 1e3);
1312
- return source;
1313
- }
1314
- });
1315
- },
1316
- playNote(note) {
1317
- const from = toFrequency(note.pitch);
1318
- const glideTo = note.glide ? toFrequency(note.glide.to) : void 0;
1319
- const wave = note.wave ?? "sine";
1320
- const partials = isInharmonic(wave) ? resolvePartials(wave) : null;
1321
- return launch(note, {
1322
- ready: () => true,
1323
- start: (voice, whenMs) => synthStart(voice, whenMs, note, (t0, into) => {
1324
- const tone = (ratio) => {
1325
- const osc = ctx.createOscillator();
1326
- osc.frequency.setValueAtTime(from * ratio, t0);
1327
- if (glideTo !== void 0) {
1328
- const end = t0 + (note.glide.ms ?? note.duration ?? 100) / 1e3;
1329
- if (note.glide.curve === "linear") osc.frequency.linearRampToValueAtTime(glideTo * ratio, end);
1330
- else osc.frequency.exponentialRampToValueAtTime(glideTo * ratio, end);
1331
- }
1332
- return osc;
1333
- };
1334
- let oscs;
1335
- if (partials) {
1336
- oscs = partials.map((p) => {
1337
- const osc = tone(p.ratio);
1338
- const level = ctx.createGain();
1339
- level.gain.setValueAtTime(p.gain, t0);
1340
- if (p.decay !== void 0) level.gain.setTargetAtTime(0, t0, p.decay / 1e3);
1341
- osc.connect(level);
1342
- level.connect(into);
1343
- return osc;
1344
- });
1345
- } else {
1346
- const osc = tone(1);
1347
- applyWave(osc, wave);
1348
- osc.connect(into);
1349
- oscs = [osc];
1350
- }
1351
- for (const osc of oscs) osc.start(t0);
1352
- return {
1353
- nodes: oscs,
1354
- retune: () => {
1355
- for (const osc of oscs) osc.detune.value = voice.detune + 1200 * Math.log2(voice.rate);
1356
- }
1357
- };
1358
- })
1359
- });
1360
- },
1361
- playNoise(noise) {
1362
- return launch(noise, {
1363
- ready: () => true,
1364
- start: (voice, whenMs) => synthStart(voice, whenMs, noise, (t0, into) => {
1365
- const source = ctx.createBufferSource();
1366
- source.buffer = noiseBuffer(ctx, noise.noise);
1367
- source.loop = true;
1368
- source.connect(into);
1369
- source.start(t0, random() * source.buffer.duration);
1370
- return {
1371
- nodes: [source],
1372
- retune: () => {
1373
- source.playbackRate.value = voice.rate;
1374
- source.detune.value = voice.detune;
1375
- }
1376
- };
1377
- })
1378
- });
1379
- },
2287
+ play: (sound, playOpts = {}) => voices.launch(playOpts, bufferStarter(ctx, sounds, warn, sound, playOpts.loop ?? false)),
2288
+ playNote: (note) => voices.launch(note, synth.note(note)),
2289
+ playNoise: (noise) => voices.launch(noise, synth.noise(noise)),
1380
2290
  stream(media, streamOpts = {}) {
1381
- const el = typeof media === "string" ? openMedia(media, streamOpts.crossOrigin) : media;
1382
- streaming.get(el)?.stop();
1383
- const handle = launch(streamOpts, {
1384
- ready: () => true,
1385
- start(voice) {
1386
- let node = mediaSources.get(el);
1387
- if (!node) {
1388
- node = ctx.createMediaElementSource(el);
1389
- mediaSources.set(el, node);
1390
- }
1391
- el.loop = streamOpts.loop ?? false;
1392
- if (streamOpts.offset !== void 0) el.currentTime = streamOpts.offset / 1e3;
1393
- voice.retune = () => {
1394
- el.playbackRate = voice.rate;
1395
- };
1396
- voice.retune();
1397
- const into = voice.chain.panner;
1398
- node.connect(into);
1399
- return mediaSource(el, node, into);
1400
- }
1401
- });
1402
- streaming.set(el, handle);
2291
+ const el = streams.element(media, streamOpts);
2292
+ streams.streaming.get(el)?.stop();
2293
+ const handle = voices.launch(streamOpts, streams.starter(el, streamOpts));
2294
+ streams.streaming.set(el, handle);
1403
2295
  return handle;
1404
2296
  },
1405
2297
  schedule(when, fire, key) {
@@ -1408,19 +2300,12 @@ function createAudioEngine(opts = {}) {
1408
2300
  },
1409
2301
  stopKey(key) {
1410
2302
  scheduler.cancelKey(key);
1411
- for (const voice of [...live.values()]) {
1412
- if (voice.key === key) teardown(voice);
1413
- }
2303
+ voices.stopKey(key);
1414
2304
  },
1415
- stopAll: () => stopVoices("stopped"),
2305
+ stopAll: () => voices.stopAll("stopped"),
1416
2306
  bus: graph.bus,
1417
2307
  busNames: graph.names,
1418
- activeVoices(bus) {
1419
- if (bus !== void 0) return slotsFor(bus).pool.active();
1420
- let total = 0;
1421
- for (const s of slotsByBus.values()) total += s.pool.active();
1422
- return total;
1423
- },
2308
+ activeVoices: active,
1424
2309
  analyser(busName, tapOpts) {
1425
2310
  const tap = createAnalyserTap(ctx, busName ? graph.node(busName) : graph.master, tapOpts);
1426
2311
  taps.add(tap);
@@ -1432,23 +2317,28 @@ function createAudioEngine(opts = {}) {
1432
2317
  }
1433
2318
  };
1434
2319
  },
2320
+ graph(bus) {
2321
+ if (disposed) throw new Error("@weasel-js/audio: engine is disposed");
2322
+ return patches.create(bus ?? busNames[0]);
2323
+ },
1435
2324
  setListener(p, o) {
1436
2325
  listener = p;
1437
2326
  if (o) spatialOpts = o;
1438
- for (const voice of live.values()) applySpatial(voice);
2327
+ voices.respatialize();
1439
2328
  },
1440
2329
  dispose() {
1441
2330
  if (disposed) return;
1442
2331
  disposed = true;
1443
- stopVoices("disposed");
1444
- idleChains.length = 0;
2332
+ voices.stopAll("disposed");
2333
+ chains.clear();
1445
2334
  scheduler.stop();
1446
2335
  scheduler.clear();
1447
2336
  tickTimer?.dispose();
1448
2337
  for (const tap of [...taps]) tap.dispose();
1449
2338
  taps.clear();
1450
- disarmGestures();
1451
- ctx.removeEventListener("statechange", onStateChange);
2339
+ watch.dispose();
2340
+ synth.dispose();
2341
+ patches.dispose();
1452
2342
  graph.dispose();
1453
2343
  graph.master.disconnect();
1454
2344
  if (ownsContext) void ctx.close().catch(() => void 0);
@@ -1461,9 +2351,9 @@ function createAudioEngine(opts = {}) {
1461
2351
  function createFilterEffect(ctx, opts = {}) {
1462
2352
  const filter = ctx.createBiquadFilter();
1463
2353
  filter.type = opts.type ?? "lowpass";
1464
- if (opts.frequency !== void 0) writeParam(ctx, filter.frequency, opts.frequency);
1465
- if (opts.Q !== void 0) writeParam(ctx, filter.Q, opts.Q);
1466
- 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);
1467
2357
  return { input: filter, output: filter, filter };
1468
2358
  }
1469
2359
  var checkMix = (mix) => {
@@ -1485,7 +2375,8 @@ function wetDry(ctx, wetIn, wetOut, initial) {
1485
2375
  writeParam(ctx, dry.gain, 1 - mix, rampMs);
1486
2376
  writeParam(ctx, wet.gain, mix, rampMs);
1487
2377
  };
1488
- write();
2378
+ initParam(ctx, dry.gain, 1 - mix);
2379
+ initParam(ctx, wet.gain, mix);
1489
2380
  return {
1490
2381
  input,
1491
2382
  output,
@@ -1518,8 +2409,8 @@ function createDelayEffect(ctx, opts = {}) {
1518
2409
  const m = wetDry(ctx, delay, delay, opts.mix ?? 0.35);
1519
2410
  delay.connect(loop);
1520
2411
  loop.connect(delay);
1521
- writeParam(ctx, delay.delayTime, time / 1e3);
1522
- writeParam(ctx, loop.gain, fb);
2412
+ initParam(ctx, delay.delayTime, time / 1e3);
2413
+ initParam(ctx, loop.gain, fb);
1523
2414
  return {
1524
2415
  input: m.input,
1525
2416
  output: m.output,
@@ -1548,7 +2439,7 @@ function createCompressorEffect(ctx, opts = {}) {
1548
2439
  const makeup = ctx.createGain();
1549
2440
  compressor.connect(makeup);
1550
2441
  const set = (param, value) => {
1551
- if (value !== void 0) writeParam(ctx, param, value);
2442
+ if (value !== void 0) initParam(ctx, param, value);
1552
2443
  };
1553
2444
  set(compressor.threshold, opts.threshold);
1554
2445
  set(compressor.knee, opts.knee);
@@ -1842,15 +2733,15 @@ function scoreBar(params, bar, opts = {}) {
1842
2733
  // src/score.ts
1843
2734
  function createScore(engine, opts) {
1844
2735
  const { mood: initial, onBar, ...barOpts } = opts;
1845
- let target = initial;
2736
+ let target2 = initial;
1846
2737
  const stateAt = (bar, before) => {
1847
- const cue = readScoreCue(target);
2738
+ const cue = readScoreCue(target2);
1848
2739
  return {
1849
2740
  bar,
1850
2741
  cueBar: before && before.cue.name === cue.name ? before.cueBar + 1 : 0,
1851
- mood: target,
2742
+ mood: target2,
1852
2743
  cue,
1853
- params: scoreParams(target)
2744
+ params: scoreParams(target2)
1854
2745
  };
1855
2746
  };
1856
2747
  const eventsOf = (state) => scoreBar(state.params, state.cueBar, barOpts);
@@ -1881,9 +2772,9 @@ function createScore(engine, opts) {
1881
2772
  },
1882
2773
  stop: () => player.stop(),
1883
2774
  playing: () => player.playing(),
1884
- mood: () => target,
2775
+ mood: () => target2,
1885
2776
  setMood(mood) {
1886
- target = mood;
2777
+ target2 = mood;
1887
2778
  },
1888
2779
  state: () => current
1889
2780
  };
@@ -2062,13 +2953,12 @@ function createKernel(patch, sampleRate, source) {
2062
2953
  };
2063
2954
  }
2064
2955
  function createNoteKernel(note, sampleRate) {
2065
- const from = toFrequency(note.pitch);
2066
- const glideTo = note.glide ? toFrequency(note.glide.to) : void 0;
2956
+ const { from, glideTo, partials } = resolveNote(note);
2067
2957
  const glideMs = note.glide?.ms ?? note.duration ?? 100;
2068
2958
  const linear = note.glide?.curve === "linear";
2069
2959
  const wave = note.wave ?? "sine";
2070
2960
  const shape = isInharmonic(wave) ? sineShape : waveShape(wave);
2071
- const tones = isInharmonic(wave) ? resolvePartials(wave).map((p) => ({ ratio: p.ratio, gain: p.gain, decay: p.decay, phase: 0 })) : [{ ratio: 1, gain: 1, phase: 0 }];
2961
+ const tones = partials ? partials.map((p) => ({ ratio: p.ratio, gain: p.gain, decay: p.decay, phase: 0 })) : [{ ratio: 1, gain: 1, phase: 0 }];
2072
2962
  return createKernel(note, sampleRate, (t, tune) => {
2073
2963
  let hz = from;
2074
2964
  if (glideTo !== void 0) {
@@ -2124,6 +3014,252 @@ function renderNoise(noise, sampleRate, random = Math.random) {
2124
3014
  return renderWhole(createNoiseKernel(noise, sampleRate, random), noise, sampleRate, "renderNoise");
2125
3015
  }
2126
3016
 
2127
- 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 };
2128
3264
  //# sourceMappingURL=index.js.map
2129
3265
  //# sourceMappingURL=index.js.map