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