@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/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 horizon = opts.now() + lookahead;
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
- queue.push({ when, fire, key });
426
- if (running && !flushQueued && when <= opts.now() + lookahead) {
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
- let warnedLocked = false;
928
- const warnLocked = () => {
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, { once: true, passive: true });
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
- warnedLocked = false;
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
- writeParam(ctx, voice.chain.gain.gain, voice.baseGain * s.gain);
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
- warnDisposed();
1069
+ warn.disposed();
1070
+ reportDrop(common.onDrop, "disposed");
1035
1071
  return dropped(id);
1036
1072
  }
1037
1073
  if (ctx.state !== "running") {
1038
- warnLocked();
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
- const source = starter.start(voice, scheduledWhen);
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
- warnUnknownSound(sound.id);
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
- engine.stopAll();
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 list = byStep.get(e.step);
1586
+ const step = Math.floor(e.step);
1587
+ const list = byStep.get(step);
1534
1588
  if (list) list.push(e);
1535
- else byStep.set(e.step, [e]);
1536
- last = Math.max(last, e.step);
1589
+ else byStep.set(step, [e]);
1537
1590
  }
1538
- length = len ?? Math.max(1, Math.ceil((last + 1) / stepsPerBeat)) * stepsPerBeat;
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
- opts.onStep?.(step, when);
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 + stepMs();
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
- export { createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createPatternPlayer, createReverbEffect, createScheduler, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, resolveEnvelope, spatialize, toFrequency };
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