@weasel-js/audio 1.9.4 → 1.10.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/dist/index.js CHANGED
@@ -397,13 +397,15 @@ function createScheduler(opts) {
397
397
  let handle = null;
398
398
  let flushQueued = false;
399
399
  const fireDue = () => {
400
- const horizon = opts.now() + lookahead;
400
+ const now = opts.now();
401
+ const horizon = now + lookahead;
401
402
  const due = [];
402
403
  const rest = [];
403
404
  for (const entry of queue) (entry.when <= horizon ? due : rest).push(entry);
404
405
  queue = rest;
405
406
  due.sort((a, b) => a.when - b.when);
406
407
  for (const entry of due) {
408
+ if (entry.waited && entry.when < now) opts.onLate?.(now - entry.when, entry.when);
407
409
  try {
408
410
  entry.fire(entry.when);
409
411
  } catch (err) {
@@ -432,8 +434,9 @@ function createScheduler(opts) {
432
434
  handle = null;
433
435
  },
434
436
  schedule(when, fire, key) {
435
- queue.push({ when, fire, key });
436
- 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) {
437
440
  flushQueued = true;
438
441
  queueMicrotask(flush);
439
442
  }
@@ -943,7 +946,8 @@ function createAudioEngine(opts = {}) {
943
946
  setTimer: timers.setTimer,
944
947
  clearTimer: timers.clearTimer,
945
948
  lookahead: opts.lookahead,
946
- interval: opts.tickInterval
949
+ interval: opts.tickInterval,
950
+ onLate: opts.onLate
947
951
  });
948
952
  scheduler.start();
949
953
  const graph = createBusGraph(ctx, busNames, { ...timers, fadeMs: opts.insertFadeMs });
@@ -1557,6 +1561,11 @@ function createCompressorEffect(ctx, opts = {}) {
1557
1561
 
1558
1562
  // src/patternPlayer.ts
1559
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
+ }
1560
1569
  var checkTempo = (bpm) => {
1561
1570
  if (!(bpm > 0) || !Number.isFinite(bpm)) {
1562
1571
  throw new RangeError(`@weasel-js/audio: tempo must be a positive number of bpm, got ${bpm}`);
@@ -1568,21 +1577,22 @@ function createPatternPlayer(engine, opts) {
1568
1577
  const loop = opts.loop ?? true;
1569
1578
  const key = `\0pattern:${nextPlayer++}`;
1570
1579
  let bpm = opts.tempo;
1580
+ let drift = opts.detune ?? 0;
1571
1581
  let length = 1;
1572
1582
  let byStep = /* @__PURE__ */ new Map();
1573
1583
  const setEvents = (events, len) => {
1574
1584
  byStep = /* @__PURE__ */ new Map();
1575
- let last = -1;
1576
1585
  for (const e of events) {
1577
- const list = byStep.get(e.step);
1586
+ const step = Math.floor(e.step);
1587
+ const list = byStep.get(step);
1578
1588
  if (list) list.push(e);
1579
- else byStep.set(e.step, [e]);
1580
- last = Math.max(last, e.step);
1589
+ else byStep.set(step, [e]);
1581
1590
  }
1582
- length = len ?? Math.max(1, Math.ceil((last + 1) / stepsPerBeat)) * stepsPerBeat;
1591
+ length = len ?? patternLength(events, stepsPerBeat);
1583
1592
  };
1584
1593
  setEvents(opts.events ?? [], opts.length);
1585
1594
  let running = false;
1595
+ let inStep = false;
1586
1596
  let count = 0;
1587
1597
  let lastAt = null;
1588
1598
  let nextAt = 0;
@@ -1607,21 +1617,30 @@ function createPatternPlayer(engine, opts) {
1607
1617
  const step = count % length;
1608
1618
  for (const v of voices) if (!v.isPlaying()) voices.delete(v);
1609
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);
1610
1622
  if ("sound" in e) {
1611
1623
  const { step: _s, sound, ...play } = e;
1612
- voices.add(engine.play(sound, { ...play, when }));
1624
+ voices.add(engine.play(sound, { ...play, detune, when: at }));
1613
1625
  } else if ("noise" in e) {
1614
1626
  const { step: _s, length: steps = 1, ...noise } = e;
1615
- voices.add(engine.playNoise({ ...noise, when, duration: steps * stepMs() }));
1627
+ voices.add(engine.playNoise({ ...noise, detune, when: at, duration: steps * stepMs() }));
1616
1628
  } else {
1617
1629
  const { step: _s, length: steps = 1, ...note } = e;
1618
- voices.add(engine.playNote({ ...note, when, duration: steps * stepMs() }));
1630
+ voices.add(engine.playNote({ ...note, detune, when: at, duration: steps * stepMs() }));
1619
1631
  }
1620
1632
  }
1621
- 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;
1622
1641
  count += 1;
1623
1642
  lastAt = when;
1624
- nextAt = when + stepMs();
1643
+ nextAt = when + ms;
1625
1644
  book();
1626
1645
  };
1627
1646
  return {
@@ -1645,15 +1664,466 @@ function createPatternPlayer(engine, opts) {
1645
1664
  setTempo(next) {
1646
1665
  checkTempo(next);
1647
1666
  bpm = next;
1648
- if (!running || lastAt === null) return;
1667
+ if (!running || lastAt === null || inStep) return;
1649
1668
  engine.stopKey(key);
1650
1669
  nextAt = Math.max(lastAt + stepMs(), engine.now());
1651
1670
  book();
1652
1671
  },
1672
+ setDetune(next) {
1673
+ drift = next;
1674
+ },
1653
1675
  setEvents
1654
1676
  };
1655
1677
  }
1656
1678
 
1657
- 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
+ // src/biquad.ts
1893
+ function biquadCoefficients(type, frequency, sampleRate, Q = 1, gain = 0, into = { b0: 0, b1: 0, b2: 0, a1: 0, a2: 0 }) {
1894
+ const f = Math.min(Math.max(frequency, 1e-3), sampleRate * 0.4999);
1895
+ const w0 = 2 * Math.PI * f / sampleRate;
1896
+ const cos = Math.cos(w0);
1897
+ const sin = Math.sin(w0);
1898
+ const A = 10 ** (gain / 40);
1899
+ const alphaQ = sin / (2 * Math.max(Q, 1e-4));
1900
+ const alphaDb = sin / (2 * 10 ** (Q / 20));
1901
+ const shelf = sin * Math.SQRT2 * Math.sqrt(A);
1902
+ let b0 = (1 - cos) / 2;
1903
+ let b1 = 1 - cos;
1904
+ let b2 = b0;
1905
+ let a0 = 1 + alphaDb;
1906
+ let a1 = -2 * cos;
1907
+ let a2 = 1 - alphaDb;
1908
+ if (type === "highpass") {
1909
+ b0 = b2 = (1 + cos) / 2;
1910
+ b1 = -(1 + cos);
1911
+ } else if (type === "peaking") {
1912
+ b0 = 1 + alphaQ * A;
1913
+ b1 = -2 * cos;
1914
+ b2 = 1 - alphaQ * A;
1915
+ a0 = 1 + alphaQ / A;
1916
+ a2 = 1 - alphaQ / A;
1917
+ } else if (type === "lowshelf") {
1918
+ b0 = A * (A + 1 - (A - 1) * cos + shelf);
1919
+ b1 = 2 * A * (A - 1 - (A + 1) * cos);
1920
+ b2 = A * (A + 1 - (A - 1) * cos - shelf);
1921
+ a0 = A + 1 + (A - 1) * cos + shelf;
1922
+ a1 = -2 * (A - 1 + (A + 1) * cos);
1923
+ a2 = A + 1 + (A - 1) * cos - shelf;
1924
+ } else if (type === "highshelf") {
1925
+ b0 = A * (A + 1 + (A - 1) * cos + shelf);
1926
+ b1 = -2 * A * (A - 1 + (A + 1) * cos);
1927
+ b2 = A * (A + 1 + (A - 1) * cos - shelf);
1928
+ a0 = A + 1 - (A - 1) * cos + shelf;
1929
+ a1 = 2 * (A - 1 - (A + 1) * cos);
1930
+ a2 = A + 1 - (A - 1) * cos - shelf;
1931
+ } else if (type !== "lowpass") {
1932
+ a0 = 1 + alphaQ;
1933
+ a2 = 1 - alphaQ;
1934
+ b0 = type === "bandpass" ? alphaQ : type === "notch" ? 1 : 1 - alphaQ;
1935
+ b1 = type === "bandpass" ? 0 : -2 * cos;
1936
+ b2 = type === "bandpass" ? -alphaQ : type === "notch" ? 1 : 1 + alphaQ;
1937
+ }
1938
+ into.b0 = b0 / a0;
1939
+ into.b1 = b1 / a0;
1940
+ into.b2 = b2 / a0;
1941
+ into.a1 = a1 / a0;
1942
+ into.a2 = a2 / a0;
1943
+ return into;
1944
+ }
1945
+ function biquadResponseDb(c, hz, sampleRate) {
1946
+ const w = 2 * Math.PI * hz / sampleRate;
1947
+ const c1 = Math.cos(w);
1948
+ const s1 = Math.sin(w);
1949
+ const c2 = Math.cos(2 * w);
1950
+ const s2 = Math.sin(2 * w);
1951
+ const num = (c.b0 + c.b1 * c1 + c.b2 * c2) ** 2 + (c.b1 * s1 + c.b2 * s2) ** 2;
1952
+ const den = (1 + c.a1 * c1 + c.a2 * c2) ** 2 + (c.a1 * s1 + c.a2 * s2) ** 2;
1953
+ return 10 * Math.log10(num / den);
1954
+ }
1955
+
1956
+ // src/oscillator.ts
1957
+ var TAU = 2 * Math.PI;
1958
+ var PEAK_POINTS = 4096;
1959
+ var TRIANGLE_SUMMED_FROM = 1 / 128;
1960
+ function polyBlep(t, dt) {
1961
+ const d = Math.min(dt, 0.5);
1962
+ if (t < d) {
1963
+ const x = t / d;
1964
+ return x + x - x * x - 1;
1965
+ }
1966
+ if (t > 1 - d) {
1967
+ const x = (t - 1) / d;
1968
+ return x * x + x + x + 1;
1969
+ }
1970
+ return 0;
1971
+ }
1972
+ var sineShape = (phase) => Math.sin(TAU * phase);
1973
+ var STEPPED_LEVEL = 1 / 1.17898;
1974
+ var BUILT_IN = {
1975
+ sine: sineShape,
1976
+ square: (phase, dt) => STEPPED_LEVEL * ((phase < 0.5 ? 1 : -1) + polyBlep(phase, dt) - polyBlep((phase + 0.5) % 1, dt)),
1977
+ sawtooth: (phase, dt) => {
1978
+ const t = (phase + 0.5) % 1;
1979
+ return STEPPED_LEVEL * (2 * t - 1 - polyBlep(t, dt));
1980
+ },
1981
+ triangle: (phase, dt) => {
1982
+ if (dt < TRIANGLE_SUMMED_FROM) return phase < 0.25 ? 4 * phase : phase < 0.75 ? 2 - 4 * phase : 4 * phase - 4;
1983
+ let v = 0;
1984
+ let sign = 1;
1985
+ for (let k = 1; k * dt < 0.5; k += 2) {
1986
+ v += sign * Math.sin(TAU * k * phase) / (k * k);
1987
+ sign = -sign;
1988
+ }
1989
+ return v * 8 / (Math.PI * Math.PI);
1990
+ }
1991
+ };
1992
+ function harmonicShape(levels) {
1993
+ const sum = (phase, count) => {
1994
+ let v = 0;
1995
+ for (let k = 1; k <= count; k += 1) v += levels[k - 1] * Math.sin(TAU * k * phase);
1996
+ return v;
1997
+ };
1998
+ let peak = 0;
1999
+ for (let i = 0; i < PEAK_POINTS; i += 1) peak = Math.max(peak, Math.abs(sum(i / PEAK_POINTS, levels.length)));
2000
+ const scale = peak > 0 ? 1 / peak : 0;
2001
+ return (phase, dt) => scale * sum(phase, Math.min(levels.length, Math.ceil(0.5 / dt) - 1));
2002
+ }
2003
+ function waveShape(wave) {
2004
+ return typeof wave === "string" ? BUILT_IN[wave] : harmonicShape(wave);
2005
+ }
2006
+
2007
+ // src/voiceKernel.ts
2008
+ var tuning = (rate, detune) => rate * 2 ** (detune / 1200);
2009
+ function filterStage(spec, sampleRate) {
2010
+ const type = spec.type ?? "lowpass";
2011
+ const amount = spec.amount ?? 0;
2012
+ const env = spec.envelope && amount !== 0 ? resolveEnvelope(spec.envelope) : null;
2013
+ const coefficients = biquadCoefficients(type, spec.frequency, sampleRate, spec.Q, spec.gain);
2014
+ let cutoff = spec.frequency;
2015
+ let z1 = 0;
2016
+ let z2 = 0;
2017
+ return (x, t, gate) => {
2018
+ if (env) {
2019
+ const hz = spec.frequency + amount * envelopeLevel(env, t, gate);
2020
+ if (hz !== cutoff) {
2021
+ cutoff = hz;
2022
+ biquadCoefficients(type, hz, sampleRate, spec.Q, spec.gain, coefficients);
2023
+ }
2024
+ }
2025
+ const y = coefficients.b0 * x + z1;
2026
+ z1 = coefficients.b1 * x - coefficients.a1 * y + z2;
2027
+ z2 = coefficients.b2 * x - coefficients.a2 * y;
2028
+ return y;
2029
+ };
2030
+ }
2031
+ function createKernel(patch, sampleRate, source) {
2032
+ const env = resolveEnvelope(patch.envelope);
2033
+ const gain = patch.gain ?? 1;
2034
+ const filter = patch.filter ? filterStage(patch.filter, sampleRate) : null;
2035
+ const msPerSample = 1e3 / sampleRate;
2036
+ let gate = patch.duration;
2037
+ let tune = tuning(patch.rate ?? 1, patch.detune ?? 0);
2038
+ let n = 0;
2039
+ let ended = false;
2040
+ return {
2041
+ render(out, from = 0, to = out.length) {
2042
+ if (ended) return false;
2043
+ for (let i = from; i < to; i += 1) {
2044
+ const t = n * msPerSample;
2045
+ if (gate !== void 0 && t >= gate + env.release) {
2046
+ ended = true;
2047
+ return false;
2048
+ }
2049
+ const raw = source(t, tune);
2050
+ out[i] += gain * envelopeLevel(env, t, gate) * (filter ? filter(raw, t, gate) : raw);
2051
+ n += 1;
2052
+ }
2053
+ return true;
2054
+ },
2055
+ release() {
2056
+ const t = n * msPerSample;
2057
+ if (gate === void 0 || t < gate) gate = t;
2058
+ },
2059
+ retune(rate, detune) {
2060
+ tune = tuning(rate, detune);
2061
+ }
2062
+ };
2063
+ }
2064
+ function createNoteKernel(note, sampleRate) {
2065
+ const from = toFrequency(note.pitch);
2066
+ const glideTo = note.glide ? toFrequency(note.glide.to) : void 0;
2067
+ const glideMs = note.glide?.ms ?? note.duration ?? 100;
2068
+ const linear = note.glide?.curve === "linear";
2069
+ const wave = note.wave ?? "sine";
2070
+ const shape = isInharmonic(wave) ? sineShape : waveShape(wave);
2071
+ const tones = isInharmonic(wave) ? resolvePartials(wave).map((p) => ({ ratio: p.ratio, gain: p.gain, decay: p.decay, phase: 0 })) : [{ ratio: 1, gain: 1, phase: 0 }];
2072
+ return createKernel(note, sampleRate, (t, tune) => {
2073
+ let hz = from;
2074
+ if (glideTo !== void 0) {
2075
+ const k = glideMs > 0 ? Math.min(1, t / glideMs) : 1;
2076
+ hz = linear ? from + (glideTo - from) * k : from * (glideTo / from) ** k;
2077
+ }
2078
+ const cycles = hz * tune / sampleRate;
2079
+ let sum = 0;
2080
+ for (const tone of tones) {
2081
+ const dt = cycles * tone.ratio;
2082
+ if (dt >= 0.5) continue;
2083
+ const level = tone.decay === void 0 ? tone.gain : tone.gain * Math.exp(-t / tone.decay);
2084
+ sum += level * shape(tone.phase, dt);
2085
+ tone.phase = (tone.phase + dt) % 1;
2086
+ }
2087
+ return sum;
2088
+ });
2089
+ }
2090
+ var loops = /* @__PURE__ */ new Map();
2091
+ function noiseLoop(color, sampleRate) {
2092
+ const key = `${color}:${sampleRate}`;
2093
+ let loop = loops.get(key);
2094
+ if (!loop) {
2095
+ loop = noiseSamples(color, Math.round(sampleRate * NOISE_SECONDS));
2096
+ loops.set(key, loop);
2097
+ }
2098
+ return loop;
2099
+ }
2100
+ function createNoiseKernel(noise, sampleRate, random = Math.random) {
2101
+ const loop = noiseLoop(noise.noise, sampleRate);
2102
+ let at = Math.floor(random() * loop.length);
2103
+ return createKernel(noise, sampleRate, (_t, tune) => {
2104
+ const i = Math.floor(at);
2105
+ const a = loop[i];
2106
+ const sample = a + (loop[(i + 1) % loop.length] - a) * (at - i);
2107
+ at = (at + tune) % loop.length;
2108
+ return sample;
2109
+ });
2110
+ }
2111
+ function renderWhole(kernel, patch, sampleRate, what) {
2112
+ if (patch.duration === void 0) {
2113
+ throw new RangeError(`@weasel-js/audio: ${what} needs a duration to know where the voice ends`);
2114
+ }
2115
+ const total = patch.duration + resolveEnvelope(patch.envelope).release;
2116
+ const out = new Float32Array(Math.round(total * sampleRate / 1e3));
2117
+ kernel.render(out);
2118
+ return out;
2119
+ }
2120
+ function renderNote(note, sampleRate) {
2121
+ return renderWhole(createNoteKernel(note, sampleRate), note, sampleRate, "renderNote");
2122
+ }
2123
+ function renderNoise(noise, sampleRate, random = Math.random) {
2124
+ return renderWhole(createNoiseKernel(noise, sampleRate, random), noise, sampleRate, "renderNoise");
2125
+ }
2126
+
2127
+ export { SCORE_BAR_STEPS, biquadCoefficients, biquadResponseDb, createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createNoiseKernel, createNoteKernel, createPatternPlayer, createReverbEffect, createScheduler, createScore, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, patternLength, patternVoicePeaks, readScoreCue, renderNoise, renderNote, resolveEnvelope, scoreBar, scoreChord, scoreCues, scoreModes, scoreParams, spatialize, toFrequency };
1658
2128
  //# sourceMappingURL=index.js.map
1659
2129
  //# sourceMappingURL=index.js.map