@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/README.md +26 -0
- package/dist/index.d.ts +180 -5
- package/dist/index.js +1736 -600
- package/dist/index.js.map +1 -1
- package/dist/voiceProcessor.gen-KWFGBRBP.js +6 -0
- package/dist/voiceProcessor.gen-KWFGBRBP.js.map +1 -0
- package/package.json +4 -1
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
|
|
23
|
-
node.getByteFrequencyData(
|
|
24
|
-
return
|
|
22
|
+
const target2 = out ?? new Uint8Array(node.frequencyBinCount);
|
|
23
|
+
node.getByteFrequencyData(target2);
|
|
24
|
+
return target2;
|
|
25
25
|
},
|
|
26
26
|
waveform(out) {
|
|
27
|
-
const
|
|
28
|
-
node.getByteTimeDomainData(
|
|
29
|
-
return
|
|
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
|
|
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
|
-
|
|
59
|
+
target2[b] = sum / (hi - lo) / 255;
|
|
60
60
|
}
|
|
61
|
-
return
|
|
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
|
|
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) =>
|
|
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
|
|
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/
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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/
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
900
|
-
nodes[0].onended = fn;
|
|
1894
|
+
release: () => {
|
|
901
1895
|
},
|
|
902
|
-
|
|
903
|
-
for (const n of nodes) {
|
|
904
|
-
try {
|
|
905
|
-
n.stop(when);
|
|
906
|
-
} catch {
|
|
907
|
-
}
|
|
908
|
-
}
|
|
1896
|
+
setGain: () => {
|
|
909
1897
|
},
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1898
|
+
setRate: () => {
|
|
1899
|
+
},
|
|
1900
|
+
setDetune: () => {
|
|
1901
|
+
},
|
|
1902
|
+
setPan: () => {
|
|
1903
|
+
},
|
|
1904
|
+
setPosition: () => {
|
|
1905
|
+
},
|
|
1906
|
+
isPlaying: () => false
|
|
913
1907
|
});
|
|
914
|
-
function
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
977
|
-
|
|
978
|
-
|
|
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
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
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 =
|
|
1382
|
-
streaming.get(el)?.stop();
|
|
1383
|
-
const handle = launch(streamOpts,
|
|
1384
|
-
|
|
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
|
-
|
|
1412
|
-
if (voice.key === key) teardown(voice);
|
|
1413
|
-
}
|
|
2303
|
+
voices.stopKey(key);
|
|
1414
2304
|
},
|
|
1415
|
-
stopAll: () =>
|
|
2305
|
+
stopAll: () => voices.stopAll("stopped"),
|
|
1416
2306
|
bus: graph.bus,
|
|
1417
2307
|
busNames: graph.names,
|
|
1418
|
-
activeVoices
|
|
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
|
-
|
|
2327
|
+
voices.respatialize();
|
|
1439
2328
|
},
|
|
1440
2329
|
dispose() {
|
|
1441
2330
|
if (disposed) return;
|
|
1442
2331
|
disposed = true;
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
1451
|
-
|
|
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)
|
|
1465
|
-
if (opts.Q !== void 0)
|
|
1466
|
-
if (opts.gain !== void 0)
|
|
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
|
-
|
|
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
|
-
|
|
1522
|
-
|
|
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)
|
|
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
|
|
2736
|
+
let target2 = initial;
|
|
1846
2737
|
const stateAt = (bar, before) => {
|
|
1847
|
-
const cue = readScoreCue(
|
|
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:
|
|
2742
|
+
mood: target2,
|
|
1852
2743
|
cue,
|
|
1853
|
-
params: scoreParams(
|
|
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: () =>
|
|
2775
|
+
mood: () => target2,
|
|
1885
2776
|
setMood(mood) {
|
|
1886
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|