@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/README.md +58 -0
- package/dist/index.d.ts +235 -3
- package/dist/index.js +486 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
436
|
-
|
|
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
|
|
1586
|
+
const step = Math.floor(e.step);
|
|
1587
|
+
const list = byStep.get(step);
|
|
1578
1588
|
if (list) list.push(e);
|
|
1579
|
-
else byStep.set(
|
|
1580
|
-
last = Math.max(last, e.step);
|
|
1589
|
+
else byStep.set(step, [e]);
|
|
1581
1590
|
}
|
|
1582
|
-
length = len ??
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
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
|