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