@weasel-js/audio 1.9.3 → 1.10.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 +49 -1
- package/dist/index.d.ts +220 -3
- package/dist/index.js +336 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -121,6 +121,7 @@ function createInsertChain(ctx, head, tail, opts = {}) {
|
|
|
121
121
|
const live = [];
|
|
122
122
|
const links = [];
|
|
123
123
|
let busy = false;
|
|
124
|
+
let leaving = null;
|
|
124
125
|
let cancel = null;
|
|
125
126
|
let disposed = false;
|
|
126
127
|
let waiters = [];
|
|
@@ -219,9 +220,11 @@ function createInsertChain(ctx, head, tail, opts = {}) {
|
|
|
219
220
|
const from = find(outOf(s), inOf(next));
|
|
220
221
|
const direct = link(outOf(prev), inOf(next), 0);
|
|
221
222
|
live.splice(i, 1);
|
|
223
|
+
leaving = s;
|
|
222
224
|
writeParam(ctx, from.gain.gain, 0, ms);
|
|
223
225
|
writeParam(ctx, direct.gain.gain, 1, ms);
|
|
224
226
|
return () => {
|
|
227
|
+
leaving = null;
|
|
225
228
|
unlink(into);
|
|
226
229
|
unlink(from);
|
|
227
230
|
if (s.removed) {
|
|
@@ -277,6 +280,10 @@ function createInsertChain(ctx, head, tail, opts = {}) {
|
|
|
277
280
|
if (at < 0) return;
|
|
278
281
|
order.splice(at, 1);
|
|
279
282
|
s.removed = true;
|
|
283
|
+
if (!live.includes(s) && leaving !== s) {
|
|
284
|
+
destroy(s);
|
|
285
|
+
s.effect.dispose?.();
|
|
286
|
+
}
|
|
280
287
|
pump();
|
|
281
288
|
},
|
|
282
289
|
index: () => order.indexOf(s)
|
|
@@ -312,6 +319,9 @@ function createInsertChain(ctx, head, tail, opts = {}) {
|
|
|
312
319
|
disposed = true;
|
|
313
320
|
cancel?.();
|
|
314
321
|
cancel = null;
|
|
322
|
+
const done = waiters;
|
|
323
|
+
waiters = [];
|
|
324
|
+
for (const w of done) w();
|
|
315
325
|
}
|
|
316
326
|
};
|
|
317
327
|
}
|
|
@@ -387,13 +397,15 @@ function createScheduler(opts) {
|
|
|
387
397
|
let handle = null;
|
|
388
398
|
let flushQueued = false;
|
|
389
399
|
const fireDue = () => {
|
|
390
|
-
const
|
|
400
|
+
const now = opts.now();
|
|
401
|
+
const horizon = now + lookahead;
|
|
391
402
|
const due = [];
|
|
392
403
|
const rest = [];
|
|
393
404
|
for (const entry of queue) (entry.when <= horizon ? due : rest).push(entry);
|
|
394
405
|
queue = rest;
|
|
395
406
|
due.sort((a, b) => a.when - b.when);
|
|
396
407
|
for (const entry of due) {
|
|
408
|
+
if (entry.waited && entry.when < now) opts.onLate?.(now - entry.when, entry.when);
|
|
397
409
|
try {
|
|
398
410
|
entry.fire(entry.when);
|
|
399
411
|
} catch (err) {
|
|
@@ -422,8 +434,9 @@ function createScheduler(opts) {
|
|
|
422
434
|
handle = null;
|
|
423
435
|
},
|
|
424
436
|
schedule(when, fire, key) {
|
|
425
|
-
|
|
426
|
-
|
|
437
|
+
const waited = when > opts.now() + lookahead;
|
|
438
|
+
queue.push({ when, fire, key, waited });
|
|
439
|
+
if (running && !flushQueued && !waited) {
|
|
427
440
|
flushQueued = true;
|
|
428
441
|
queueMicrotask(flush);
|
|
429
442
|
}
|
|
@@ -795,6 +808,40 @@ var partialPresets = {
|
|
|
795
808
|
}
|
|
796
809
|
};
|
|
797
810
|
|
|
811
|
+
// src/voiceDrops.ts
|
|
812
|
+
var reportDrop = (onDrop, reason) => {
|
|
813
|
+
if (onDrop) queueMicrotask(() => onDrop(reason));
|
|
814
|
+
};
|
|
815
|
+
function createDropWarnings() {
|
|
816
|
+
let warnedLocked = false;
|
|
817
|
+
let warnedDisposed = false;
|
|
818
|
+
let warnedUnknownSound = false;
|
|
819
|
+
return {
|
|
820
|
+
locked() {
|
|
821
|
+
if (warnedLocked) return;
|
|
822
|
+
warnedLocked = true;
|
|
823
|
+
console.warn(
|
|
824
|
+
"@weasel-js/audio: play() before the AudioContext was unlocked \u2014 the voice was dropped. Browsers require a user gesture; call engine.unlock() from one, or wait for the automatic gesture listener."
|
|
825
|
+
);
|
|
826
|
+
},
|
|
827
|
+
disposed() {
|
|
828
|
+
if (warnedDisposed) return;
|
|
829
|
+
warnedDisposed = true;
|
|
830
|
+
console.warn("@weasel-js/audio: play() after dispose() \u2014 the voice was dropped.");
|
|
831
|
+
},
|
|
832
|
+
unknownSound(id) {
|
|
833
|
+
if (warnedUnknownSound) return;
|
|
834
|
+
warnedUnknownSound = true;
|
|
835
|
+
console.warn(
|
|
836
|
+
`@weasel-js/audio: play() was handed sound "${id}", which this engine never loaded \u2014 the voice was dropped. A handle belongs to the engine that produced it.`
|
|
837
|
+
);
|
|
838
|
+
},
|
|
839
|
+
rearmLocked() {
|
|
840
|
+
warnedLocked = false;
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
798
845
|
// src/voicePool.ts
|
|
799
846
|
function createVoicePool(opts) {
|
|
800
847
|
if (!(opts.limit >= 1)) {
|
|
@@ -899,7 +946,8 @@ function createAudioEngine(opts = {}) {
|
|
|
899
946
|
setTimer: timers.setTimer,
|
|
900
947
|
clearTimer: timers.clearTimer,
|
|
901
948
|
lookahead: opts.lookahead,
|
|
902
|
-
interval: opts.tickInterval
|
|
949
|
+
interval: opts.tickInterval,
|
|
950
|
+
onLate: opts.onLate
|
|
903
951
|
});
|
|
904
952
|
scheduler.start();
|
|
905
953
|
const graph = createBusGraph(ctx, busNames, { ...timers, fadeMs: opts.insertFadeMs });
|
|
@@ -924,50 +972,30 @@ function createAudioEngine(opts = {}) {
|
|
|
924
972
|
chain.gain.connect(bus);
|
|
925
973
|
return chain;
|
|
926
974
|
};
|
|
927
|
-
|
|
928
|
-
const
|
|
929
|
-
if (warnedLocked) return;
|
|
930
|
-
warnedLocked = true;
|
|
931
|
-
console.warn(
|
|
932
|
-
"@weasel-js/audio: play() before the AudioContext was unlocked \u2014 the voice was dropped. Browsers require a user gesture; call engine.unlock() from one, or wait for the automatic gesture listener."
|
|
933
|
-
);
|
|
934
|
-
};
|
|
935
|
-
let warnedDisposed = false;
|
|
936
|
-
const warnDisposed = () => {
|
|
937
|
-
if (warnedDisposed) return;
|
|
938
|
-
warnedDisposed = true;
|
|
939
|
-
console.warn("@weasel-js/audio: play() after dispose() \u2014 the voice was dropped.");
|
|
940
|
-
};
|
|
941
|
-
let warnedUnknownSound = false;
|
|
942
|
-
const warnUnknownSound = (id) => {
|
|
943
|
-
if (warnedUnknownSound) return;
|
|
944
|
-
warnedUnknownSound = true;
|
|
945
|
-
console.warn(
|
|
946
|
-
`@weasel-js/audio: play() was handed sound "${id}", which this engine never loaded \u2014 the voice was dropped. A handle belongs to the engine that produced it.`
|
|
947
|
-
);
|
|
948
|
-
};
|
|
949
|
-
const gestures = ["pointerdown", "keydown", "touchstart"];
|
|
975
|
+
const warn = createDropWarnings();
|
|
976
|
+
const gestures = ["pointerdown", "pointerup", "touchend", "click", "keydown"];
|
|
950
977
|
const onGesture = () => {
|
|
951
|
-
void engine.unlock();
|
|
978
|
+
void engine.unlock().catch(() => void 0);
|
|
952
979
|
};
|
|
953
980
|
const armGestures = () => {
|
|
954
981
|
if (typeof window === "undefined") return;
|
|
955
|
-
for (const g of gestures) window.addEventListener(g, onGesture, {
|
|
982
|
+
for (const g of gestures) window.addEventListener(g, onGesture, { passive: true });
|
|
956
983
|
};
|
|
957
984
|
const disarmGestures = () => {
|
|
958
985
|
if (typeof window === "undefined") return;
|
|
959
986
|
for (const g of gestures) window.removeEventListener(g, onGesture);
|
|
960
987
|
};
|
|
961
|
-
armGestures();
|
|
988
|
+
if (ctx.state !== "running") armGestures();
|
|
962
989
|
const onStateChange = () => {
|
|
963
990
|
if (disposed) return;
|
|
964
991
|
if (ctx.state === "running") {
|
|
992
|
+
disarmGestures();
|
|
965
993
|
if (suspendedSince) staleFloor = now();
|
|
966
994
|
suspendedSince = false;
|
|
967
995
|
return;
|
|
968
996
|
}
|
|
969
997
|
suspendedSince = true;
|
|
970
|
-
|
|
998
|
+
warn.rearmLocked();
|
|
971
999
|
armGestures();
|
|
972
1000
|
};
|
|
973
1001
|
ctx.addEventListener("statechange", onStateChange);
|
|
@@ -980,13 +1008,17 @@ function createAudioEngine(opts = {}) {
|
|
|
980
1008
|
const s = spatialize(voice.position, listener, spatialOpts);
|
|
981
1009
|
voice.spatialGain = s.gain;
|
|
982
1010
|
voice.chain.panner.pan.value = s.pan;
|
|
983
|
-
|
|
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
|
+
}
|
|
984
1015
|
poolGain(voice);
|
|
985
1016
|
};
|
|
986
|
-
const teardown = (voice) => {
|
|
1017
|
+
const teardown = (voice, reason = "stopped") => {
|
|
987
1018
|
voice.cancelled = true;
|
|
988
1019
|
if (!voice.playing && voice.source === null) return;
|
|
989
1020
|
voice.playing = false;
|
|
1021
|
+
if (voice.source === null) reportDrop(voice.onDrop, reason);
|
|
990
1022
|
if (voice.source) {
|
|
991
1023
|
try {
|
|
992
1024
|
voice.source.stop();
|
|
@@ -1010,6 +1042,9 @@ function createAudioEngine(opts = {}) {
|
|
|
1010
1042
|
voice.slots.pool.release(voice.slot, voice.token);
|
|
1011
1043
|
}
|
|
1012
1044
|
};
|
|
1045
|
+
const stopVoices = (reason) => {
|
|
1046
|
+
for (const voice of [...live.values()]) teardown(voice, reason);
|
|
1047
|
+
};
|
|
1013
1048
|
const dropped = (id) => ({
|
|
1014
1049
|
id,
|
|
1015
1050
|
stop: () => {
|
|
@@ -1031,11 +1066,13 @@ function createAudioEngine(opts = {}) {
|
|
|
1031
1066
|
const launch = (common, starter) => {
|
|
1032
1067
|
const id = nextVoiceId++;
|
|
1033
1068
|
if (disposed) {
|
|
1034
|
-
|
|
1069
|
+
warn.disposed();
|
|
1070
|
+
reportDrop(common.onDrop, "disposed");
|
|
1035
1071
|
return dropped(id);
|
|
1036
1072
|
}
|
|
1037
1073
|
if (ctx.state !== "running") {
|
|
1038
|
-
|
|
1074
|
+
warn.locked();
|
|
1075
|
+
reportDrop(common.onDrop, "locked");
|
|
1039
1076
|
return dropped(id);
|
|
1040
1077
|
}
|
|
1041
1078
|
const busName = common.bus ?? busNames[0];
|
|
@@ -1050,6 +1087,7 @@ function createAudioEngine(opts = {}) {
|
|
|
1050
1087
|
slot: null,
|
|
1051
1088
|
token: 0,
|
|
1052
1089
|
key: common.cancelKey,
|
|
1090
|
+
onDrop: common.onDrop,
|
|
1053
1091
|
source: null,
|
|
1054
1092
|
env: null,
|
|
1055
1093
|
retune: null,
|
|
@@ -1062,17 +1100,19 @@ function createAudioEngine(opts = {}) {
|
|
|
1062
1100
|
detune: common.detune ?? 0,
|
|
1063
1101
|
position: common.position,
|
|
1064
1102
|
playing: true,
|
|
1065
|
-
cancelled: false
|
|
1103
|
+
cancelled: false,
|
|
1104
|
+
fading: false,
|
|
1105
|
+
rampEnd: 0
|
|
1066
1106
|
};
|
|
1067
1107
|
live.set(id, voice);
|
|
1068
1108
|
scheduler.schedule(when, (scheduledWhen) => {
|
|
1069
1109
|
if (voice.cancelled) return;
|
|
1070
1110
|
if (bookedAt < staleFloor && scheduledWhen < staleFloor) {
|
|
1071
|
-
teardown(voice);
|
|
1111
|
+
teardown(voice, "stale");
|
|
1072
1112
|
return;
|
|
1073
1113
|
}
|
|
1074
1114
|
if (!starter.ready()) {
|
|
1075
|
-
teardown(voice);
|
|
1115
|
+
teardown(voice, "unready");
|
|
1076
1116
|
return;
|
|
1077
1117
|
}
|
|
1078
1118
|
const acquired = slots.pool.acquire({
|
|
@@ -1086,7 +1126,15 @@ function createAudioEngine(opts = {}) {
|
|
|
1086
1126
|
const victim = slots.byToken.get(acquired.stolen);
|
|
1087
1127
|
if (victim) teardown(victim);
|
|
1088
1128
|
}
|
|
1089
|
-
|
|
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
|
+
}
|
|
1090
1138
|
source.onended = () => {
|
|
1091
1139
|
teardown(voice);
|
|
1092
1140
|
common.onDone?.();
|
|
@@ -1098,6 +1146,7 @@ function createAudioEngine(opts = {}) {
|
|
|
1098
1146
|
stop(fadeMs) {
|
|
1099
1147
|
if (fadeMs && fadeMs > 0 && voice.source && voice.chain && !voice.cancelled) {
|
|
1100
1148
|
voice.cancelled = true;
|
|
1149
|
+
voice.fading = true;
|
|
1101
1150
|
writeParam(ctx, voice.chain.gain.gain, 0, fadeMs);
|
|
1102
1151
|
try {
|
|
1103
1152
|
voice.source.stop(ctx.currentTime + fadeMs / 1e3);
|
|
@@ -1119,7 +1168,8 @@ function createAudioEngine(opts = {}) {
|
|
|
1119
1168
|
},
|
|
1120
1169
|
setGain(value, rampMs) {
|
|
1121
1170
|
voice.baseGain = value;
|
|
1122
|
-
if (!voice.chain) return;
|
|
1171
|
+
if (!voice.chain || voice.fading) return;
|
|
1172
|
+
voice.rampEnd = rampMs && rampMs > 0 ? ctx.currentTime + rampMs / 1e3 : 0;
|
|
1123
1173
|
writeParam(ctx, voice.chain.gain.gain, value * voice.spatialGain, rampMs);
|
|
1124
1174
|
poolGain(voice);
|
|
1125
1175
|
},
|
|
@@ -1245,7 +1295,7 @@ function createAudioEngine(opts = {}) {
|
|
|
1245
1295
|
return launch(playOpts, {
|
|
1246
1296
|
ready() {
|
|
1247
1297
|
if (sounds.buffer(sound)) return true;
|
|
1248
|
-
|
|
1298
|
+
warn.unknownSound(sound.id);
|
|
1249
1299
|
return false;
|
|
1250
1300
|
},
|
|
1251
1301
|
start(voice, when) {
|
|
@@ -1362,9 +1412,7 @@ function createAudioEngine(opts = {}) {
|
|
|
1362
1412
|
if (voice.key === key) teardown(voice);
|
|
1363
1413
|
}
|
|
1364
1414
|
},
|
|
1365
|
-
stopAll()
|
|
1366
|
-
for (const voice of [...live.values()]) teardown(voice);
|
|
1367
|
-
},
|
|
1415
|
+
stopAll: () => stopVoices("stopped"),
|
|
1368
1416
|
bus: graph.bus,
|
|
1369
1417
|
busNames: graph.names,
|
|
1370
1418
|
activeVoices(bus) {
|
|
@@ -1392,7 +1440,7 @@ function createAudioEngine(opts = {}) {
|
|
|
1392
1440
|
dispose() {
|
|
1393
1441
|
if (disposed) return;
|
|
1394
1442
|
disposed = true;
|
|
1395
|
-
|
|
1443
|
+
stopVoices("disposed");
|
|
1396
1444
|
idleChains.length = 0;
|
|
1397
1445
|
scheduler.stop();
|
|
1398
1446
|
scheduler.clear();
|
|
@@ -1513,6 +1561,11 @@ function createCompressorEffect(ctx, opts = {}) {
|
|
|
1513
1561
|
|
|
1514
1562
|
// src/patternPlayer.ts
|
|
1515
1563
|
var nextPlayer = 1;
|
|
1564
|
+
function patternLength(events, stepsPerBeat = 4) {
|
|
1565
|
+
let last = -1;
|
|
1566
|
+
for (const e of events) last = Math.max(last, Math.floor(e.step));
|
|
1567
|
+
return Math.max(1, Math.ceil((last + 1) / stepsPerBeat)) * stepsPerBeat;
|
|
1568
|
+
}
|
|
1516
1569
|
var checkTempo = (bpm) => {
|
|
1517
1570
|
if (!(bpm > 0) || !Number.isFinite(bpm)) {
|
|
1518
1571
|
throw new RangeError(`@weasel-js/audio: tempo must be a positive number of bpm, got ${bpm}`);
|
|
@@ -1524,21 +1577,22 @@ function createPatternPlayer(engine, opts) {
|
|
|
1524
1577
|
const loop = opts.loop ?? true;
|
|
1525
1578
|
const key = `\0pattern:${nextPlayer++}`;
|
|
1526
1579
|
let bpm = opts.tempo;
|
|
1580
|
+
let drift = opts.detune ?? 0;
|
|
1527
1581
|
let length = 1;
|
|
1528
1582
|
let byStep = /* @__PURE__ */ new Map();
|
|
1529
1583
|
const setEvents = (events, len) => {
|
|
1530
1584
|
byStep = /* @__PURE__ */ new Map();
|
|
1531
|
-
let last = -1;
|
|
1532
1585
|
for (const e of events) {
|
|
1533
|
-
const
|
|
1586
|
+
const step = Math.floor(e.step);
|
|
1587
|
+
const list = byStep.get(step);
|
|
1534
1588
|
if (list) list.push(e);
|
|
1535
|
-
else byStep.set(
|
|
1536
|
-
last = Math.max(last, e.step);
|
|
1589
|
+
else byStep.set(step, [e]);
|
|
1537
1590
|
}
|
|
1538
|
-
length = len ??
|
|
1591
|
+
length = len ?? patternLength(events, stepsPerBeat);
|
|
1539
1592
|
};
|
|
1540
1593
|
setEvents(opts.events ?? [], opts.length);
|
|
1541
1594
|
let running = false;
|
|
1595
|
+
let inStep = false;
|
|
1542
1596
|
let count = 0;
|
|
1543
1597
|
let lastAt = null;
|
|
1544
1598
|
let nextAt = 0;
|
|
@@ -1563,21 +1617,30 @@ function createPatternPlayer(engine, opts) {
|
|
|
1563
1617
|
const step = count % length;
|
|
1564
1618
|
for (const v of voices) if (!v.isPlaying()) voices.delete(v);
|
|
1565
1619
|
for (const e of byStep.get(step) ?? []) {
|
|
1620
|
+
const at = when + (e.step - step) * stepMs();
|
|
1621
|
+
const detune = (e.detune ?? 0) + (typeof drift === "function" ? drift(e.step) : drift);
|
|
1566
1622
|
if ("sound" in e) {
|
|
1567
1623
|
const { step: _s, sound, ...play } = e;
|
|
1568
|
-
voices.add(engine.play(sound, { ...play, when }));
|
|
1624
|
+
voices.add(engine.play(sound, { ...play, detune, when: at }));
|
|
1569
1625
|
} else if ("noise" in e) {
|
|
1570
1626
|
const { step: _s, length: steps = 1, ...noise } = e;
|
|
1571
|
-
voices.add(engine.playNoise({ ...noise, when, duration: steps * stepMs() }));
|
|
1627
|
+
voices.add(engine.playNoise({ ...noise, detune, when: at, duration: steps * stepMs() }));
|
|
1572
1628
|
} else {
|
|
1573
1629
|
const { step: _s, length: steps = 1, ...note } = e;
|
|
1574
|
-
voices.add(engine.playNote({ ...note, when, duration: steps * stepMs() }));
|
|
1630
|
+
voices.add(engine.playNote({ ...note, detune, when: at, duration: steps * stepMs() }));
|
|
1575
1631
|
}
|
|
1576
1632
|
}
|
|
1577
|
-
|
|
1633
|
+
const ms = stepMs();
|
|
1634
|
+
inStep = true;
|
|
1635
|
+
try {
|
|
1636
|
+
opts.onStep?.(step, when);
|
|
1637
|
+
} finally {
|
|
1638
|
+
inStep = false;
|
|
1639
|
+
}
|
|
1640
|
+
if (!running) return;
|
|
1578
1641
|
count += 1;
|
|
1579
1642
|
lastAt = when;
|
|
1580
|
-
nextAt = when +
|
|
1643
|
+
nextAt = when + ms;
|
|
1581
1644
|
book();
|
|
1582
1645
|
};
|
|
1583
1646
|
return {
|
|
@@ -1601,15 +1664,231 @@ function createPatternPlayer(engine, opts) {
|
|
|
1601
1664
|
setTempo(next) {
|
|
1602
1665
|
checkTempo(next);
|
|
1603
1666
|
bpm = next;
|
|
1604
|
-
if (!running || lastAt === null) return;
|
|
1667
|
+
if (!running || lastAt === null || inStep) return;
|
|
1605
1668
|
engine.stopKey(key);
|
|
1606
1669
|
nextAt = Math.max(lastAt + stepMs(), engine.now());
|
|
1607
1670
|
book();
|
|
1608
1671
|
},
|
|
1672
|
+
setDetune(next) {
|
|
1673
|
+
drift = next;
|
|
1674
|
+
},
|
|
1609
1675
|
setEvents
|
|
1610
1676
|
};
|
|
1611
1677
|
}
|
|
1612
1678
|
|
|
1613
|
-
|
|
1679
|
+
// src/patternLoad.ts
|
|
1680
|
+
function patternVoicePeaks(events, opts) {
|
|
1681
|
+
const stepsPerBeat = opts.stepsPerBeat ?? 4;
|
|
1682
|
+
const stepMs = 6e4 / opts.tempo / stepsPerBeat;
|
|
1683
|
+
const passMs = (opts.length ?? patternLength(events, stepsPerBeat)) * stepMs;
|
|
1684
|
+
const lead = opts.lookahead ?? 100;
|
|
1685
|
+
const spans = events.map((e) => {
|
|
1686
|
+
const sounds = "sound" in e ? opts.hitMs?.(e) ?? stepMs : (e.length ?? 1) * stepMs + resolveEnvelope(e.envelope).release;
|
|
1687
|
+
const at = e.step * stepMs;
|
|
1688
|
+
return { bus: e.bus ?? opts.bus ?? "sfx", from: at - lead, until: at + sounds };
|
|
1689
|
+
});
|
|
1690
|
+
const longest = Math.max(0, ...spans.map((s) => s.until - s.from));
|
|
1691
|
+
const passes = opts.loop === false ? 0 : Math.ceil(longest / passMs);
|
|
1692
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1693
|
+
for (const span of spans) {
|
|
1694
|
+
const list = edges.get(span.bus) ?? [];
|
|
1695
|
+
edges.set(span.bus, list);
|
|
1696
|
+
for (let pass = 0; pass <= passes; pass++) {
|
|
1697
|
+
list.push({ at: span.from + pass * passMs, by: 1 }, { at: span.until + pass * passMs, by: -1 });
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
const peaks = {};
|
|
1701
|
+
for (const [bus, list] of edges) {
|
|
1702
|
+
list.sort((a, b) => a.at - b.at || a.by - b.by);
|
|
1703
|
+
let held = 0;
|
|
1704
|
+
let peak = 0;
|
|
1705
|
+
for (const edge of list) {
|
|
1706
|
+
held += edge.by;
|
|
1707
|
+
peak = Math.max(peak, held);
|
|
1708
|
+
}
|
|
1709
|
+
peaks[bus] = peak;
|
|
1710
|
+
}
|
|
1711
|
+
return peaks;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/scoreMood.ts
|
|
1715
|
+
var scoreCues = [
|
|
1716
|
+
{
|
|
1717
|
+
name: "dread",
|
|
1718
|
+
mood: { valence: -1, tension: 1, urgency: 0 },
|
|
1719
|
+
device: "Sustained low tones a minor second and a tritone apart, with no pulse."
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
name: "investigation",
|
|
1723
|
+
mood: { valence: -0.4, tension: 0.5, urgency: 0.5 },
|
|
1724
|
+
device: "A plucked minor ostinato that adds a layer every two bars."
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
name: "wonder",
|
|
1728
|
+
mood: { valence: 1, tension: 0.1, urgency: 0.1 },
|
|
1729
|
+
device: "Lydian chords in open voicings, swelling in with a soft attack."
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
name: "urgency",
|
|
1733
|
+
mood: { valence: 0, tension: 0.7, urgency: 1 },
|
|
1734
|
+
device: "Driving sixteenths on stacked fourths and fifths, a new chord every bar."
|
|
1735
|
+
}
|
|
1736
|
+
];
|
|
1737
|
+
var clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
|
|
1738
|
+
var inRange = (mood) => ({
|
|
1739
|
+
valence: clamp(mood.valence, -1, 1),
|
|
1740
|
+
tension: clamp(mood.tension, 0, 1),
|
|
1741
|
+
urgency: clamp(mood.urgency, 0, 1)
|
|
1742
|
+
});
|
|
1743
|
+
function readScoreCue(mood) {
|
|
1744
|
+
const m = inRange(mood);
|
|
1745
|
+
const distance = scoreCues.map((cue) => ((m.valence - cue.mood.valence) / 2) ** 2 + (m.tension - cue.mood.tension) ** 2 + (m.urgency - cue.mood.urgency) ** 2);
|
|
1746
|
+
const exact = distance.indexOf(0);
|
|
1747
|
+
const raw = distance.map((d, i) => exact >= 0 ? Number(i === exact) : 1 / d);
|
|
1748
|
+
const total = raw.reduce((sum, w) => sum + w, 0);
|
|
1749
|
+
const weights = {};
|
|
1750
|
+
scoreCues.forEach((cue, i) => {
|
|
1751
|
+
weights[cue.name] = raw[i] / total;
|
|
1752
|
+
});
|
|
1753
|
+
return { name: scoreCues[raw.indexOf(Math.max(...raw))].name, weights };
|
|
1754
|
+
}
|
|
1755
|
+
var scoreModes = {
|
|
1756
|
+
lydian: [0, 2, 4, 6, 7, 9, 11],
|
|
1757
|
+
ionian: [0, 2, 4, 5, 7, 9, 11],
|
|
1758
|
+
dorian: [0, 2, 3, 5, 7, 9, 10],
|
|
1759
|
+
aeolian: [0, 2, 3, 5, 7, 8, 10],
|
|
1760
|
+
phrygian: [0, 1, 3, 5, 7, 8, 10]
|
|
1761
|
+
};
|
|
1762
|
+
function scoreParams(mood) {
|
|
1763
|
+
const { valence, tension, urgency } = inRange(mood);
|
|
1764
|
+
const sustained = urgency < 0.35;
|
|
1765
|
+
return {
|
|
1766
|
+
mode: valence > 0.6 ? "lydian" : valence > 0.2 ? "ionian" : valence > -0.2 ? "dorian" : valence > -0.6 ? "aeolian" : "phrygian",
|
|
1767
|
+
register: 36 + 12 * Math.round(valence + 1),
|
|
1768
|
+
spread: 1 + Math.round(2 * Math.max(0, valence)),
|
|
1769
|
+
harmony: tension >= 0.8 ? "cluster" : tension >= 0.55 ? "quartal" : "triadic",
|
|
1770
|
+
layers: 1 + Math.round(4 * tension),
|
|
1771
|
+
tempo: Math.round(60 + 80 * urgency),
|
|
1772
|
+
density: urgency < 0.15 ? 0 : urgency < 0.4 ? 1 : urgency < 0.75 ? 2 : 4,
|
|
1773
|
+
articulation: sustained ? "sustained" : "plucked",
|
|
1774
|
+
harmonicRhythm: urgency < 0.33 ? 4 : urgency < 0.66 ? 2 : 1,
|
|
1775
|
+
attack: sustained ? Math.round(800 * (1 - urgency)) : 5
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// src/scoreBar.ts
|
|
1780
|
+
var SCORE_BAR_STEPS = 16;
|
|
1781
|
+
var PROGRESSION = [0, 5, 3, 4];
|
|
1782
|
+
function scoreChord(params, chord, tonic = 0) {
|
|
1783
|
+
const scale = scoreModes[params.mode];
|
|
1784
|
+
const tone = (degree2) => params.register + tonic + scale[degree2 % 7] + 12 * Math.floor(degree2 / 7);
|
|
1785
|
+
if (params.harmony === "cluster") {
|
|
1786
|
+
const root2 = tone(0);
|
|
1787
|
+
return [root2, root2 + 1, root2 + 6];
|
|
1788
|
+
}
|
|
1789
|
+
const degree = PROGRESSION[chord % PROGRESSION.length];
|
|
1790
|
+
const root = tone(degree);
|
|
1791
|
+
if (params.harmony === "quartal") return [root, root + 7, root + 12, root + 17];
|
|
1792
|
+
if (params.spread < 2) return [root, tone(degree + 2), tone(degree + 4)];
|
|
1793
|
+
const open = [root, tone(degree + 4), tone(degree + 2) + 12];
|
|
1794
|
+
if (params.spread >= 3) open.push(tone(degree + 3) + 24);
|
|
1795
|
+
return open;
|
|
1796
|
+
}
|
|
1797
|
+
function figure(count, seed) {
|
|
1798
|
+
const order = Array.from({ length: count }, (_, i) => i);
|
|
1799
|
+
if (seed === void 0) return order;
|
|
1800
|
+
let state = seed >>> 0 || 1;
|
|
1801
|
+
for (let i = count - 1; i > 0; i--) {
|
|
1802
|
+
state = Math.imul(state, 1664525) + 1013904223 >>> 0;
|
|
1803
|
+
const j = state % (i + 1);
|
|
1804
|
+
[order[i], order[j]] = [order[j], order[i]];
|
|
1805
|
+
}
|
|
1806
|
+
return order;
|
|
1807
|
+
}
|
|
1808
|
+
var PLUCK = { attack: 5, decay: 140, sustain: 0.2, release: 80 };
|
|
1809
|
+
function scoreBar(params, bar, opts = {}) {
|
|
1810
|
+
const gain = opts.gain ?? 0.5;
|
|
1811
|
+
const chord = scoreChord(params, Math.floor(bar / params.harmonicRhythm), opts.tonic);
|
|
1812
|
+
const swell = { attack: params.attack, release: params.attack };
|
|
1813
|
+
const held = params.articulation === "sustained";
|
|
1814
|
+
const note = (step, length, midi, level, envelope2) => ({
|
|
1815
|
+
step,
|
|
1816
|
+
length,
|
|
1817
|
+
pitch: { midi },
|
|
1818
|
+
gain: gain * level,
|
|
1819
|
+
envelope: envelope2,
|
|
1820
|
+
wave: held ? "sine" : "triangle",
|
|
1821
|
+
...opts.bus === void 0 ? {} : { bus: opts.bus }
|
|
1822
|
+
});
|
|
1823
|
+
const pad = (level, envelope2) => chord.map((midi) => note(0, SCORE_BAR_STEPS, midi, level / chord.length, envelope2));
|
|
1824
|
+
if (params.density === 0) return pad(1, swell);
|
|
1825
|
+
const layers = Math.min(params.layers, 1 + Math.floor(bar / 2));
|
|
1826
|
+
const every = 4 / params.density;
|
|
1827
|
+
const order = figure(chord.length, opts.seed);
|
|
1828
|
+
const envelope = held ? swell : PLUCK;
|
|
1829
|
+
const events = [];
|
|
1830
|
+
for (let pulse = 0; pulse * every < SCORE_BAR_STEPS; pulse++) {
|
|
1831
|
+
const midi = chord[order[pulse % order.length]];
|
|
1832
|
+
events.push(note(pulse * every, every, midi, 0.4, envelope));
|
|
1833
|
+
if (layers >= 3 && pulse % 2 === 1) events.push(note(pulse * every, every, midi + 12, 0.15, envelope));
|
|
1834
|
+
}
|
|
1835
|
+
if (layers >= 2) {
|
|
1836
|
+
for (const step of [0, 8]) events.push(note(step, 8, chord[0] - 12, 0.3, { attack: 5, release: 120 }));
|
|
1837
|
+
}
|
|
1838
|
+
if (layers >= 4) events.push(...pad(0.15, { attack: 120, release: 200 }));
|
|
1839
|
+
return events;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/score.ts
|
|
1843
|
+
function createScore(engine, opts) {
|
|
1844
|
+
const { mood: initial, onBar, ...barOpts } = opts;
|
|
1845
|
+
let target = initial;
|
|
1846
|
+
const stateAt = (bar, before) => {
|
|
1847
|
+
const cue = readScoreCue(target);
|
|
1848
|
+
return {
|
|
1849
|
+
bar,
|
|
1850
|
+
cueBar: before && before.cue.name === cue.name ? before.cueBar + 1 : 0,
|
|
1851
|
+
mood: target,
|
|
1852
|
+
cue,
|
|
1853
|
+
params: scoreParams(target)
|
|
1854
|
+
};
|
|
1855
|
+
};
|
|
1856
|
+
const eventsOf = (state) => scoreBar(state.params, state.cueBar, barOpts);
|
|
1857
|
+
let current = stateAt(0, null);
|
|
1858
|
+
let next = current;
|
|
1859
|
+
const player = createPatternPlayer(engine, {
|
|
1860
|
+
tempo: current.params.tempo,
|
|
1861
|
+
length: SCORE_BAR_STEPS,
|
|
1862
|
+
events: eventsOf(current),
|
|
1863
|
+
onStep(step, when) {
|
|
1864
|
+
if (step === 0) {
|
|
1865
|
+
current = next;
|
|
1866
|
+
onBar?.(current, when);
|
|
1867
|
+
}
|
|
1868
|
+
if (step !== SCORE_BAR_STEPS - 1) return;
|
|
1869
|
+
next = stateAt(current.bar + 1, current);
|
|
1870
|
+
player.setTempo(next.params.tempo);
|
|
1871
|
+
player.setEvents(eventsOf(next), SCORE_BAR_STEPS);
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
return {
|
|
1875
|
+
start(when) {
|
|
1876
|
+
if (player.playing()) return;
|
|
1877
|
+
current = next = stateAt(0, null);
|
|
1878
|
+
player.setTempo(current.params.tempo);
|
|
1879
|
+
player.setEvents(eventsOf(current), SCORE_BAR_STEPS);
|
|
1880
|
+
player.start(when);
|
|
1881
|
+
},
|
|
1882
|
+
stop: () => player.stop(),
|
|
1883
|
+
playing: () => player.playing(),
|
|
1884
|
+
mood: () => target,
|
|
1885
|
+
setMood(mood) {
|
|
1886
|
+
target = mood;
|
|
1887
|
+
},
|
|
1888
|
+
state: () => current
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
export { SCORE_BAR_STEPS, createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createPatternPlayer, createReverbEffect, createScheduler, createScore, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, patternLength, patternVoicePeaks, readScoreCue, resolveEnvelope, scoreBar, scoreChord, scoreCues, scoreModes, scoreParams, spatialize, toFrequency };
|
|
1614
1893
|
//# sourceMappingURL=index.js.map
|
|
1615
1894
|
//# sourceMappingURL=index.js.map
|