@waveform-playlist/playout 12.2.0 → 12.4.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.mjs CHANGED
@@ -1443,7 +1443,7 @@ function extractLoopAndEnvelope(params) {
1443
1443
  releaseVolEnv
1444
1444
  };
1445
1445
  }
1446
- var SoundFontCache = class {
1446
+ var SoundFontCache = class _SoundFontCache {
1447
1447
  /**
1448
1448
  * @param context Optional AudioContext for createBuffer(). If omitted, uses
1449
1449
  * an OfflineAudioContext which doesn't require user gesture — safe to
@@ -1454,6 +1454,17 @@ var SoundFontCache = class {
1454
1454
  this.audioBufferCache = /* @__PURE__ */ new Map();
1455
1455
  this.context = context ?? new OfflineAudioContext(1, 1, 44100);
1456
1456
  }
1457
+ /**
1458
+ * Fetch and parse an SF2 file, resolving only once it's ready to play.
1459
+ * Prefer this over `new SoundFontCache()` + `load()` — the returned cache
1460
+ * is always loaded, so it can't hit the "unloaded cache → PolySynth
1461
+ * fallback" path in createToneAdapter / setSoundFontCache.
1462
+ */
1463
+ static async fromUrl(url, options) {
1464
+ const cache = new _SoundFontCache(options?.context);
1465
+ await cache.load(url, options?.signal);
1466
+ return cache;
1467
+ }
1457
1468
  /**
1458
1469
  * Load and parse an SF2 file from a URL.
1459
1470
  */
@@ -1632,6 +1643,9 @@ import {
1632
1643
  trackChannelCount
1633
1644
  } from "@waveform-playlist/core";
1634
1645
  import { now as now2 } from "tone";
1646
+ function isToneAdapter(adapter) {
1647
+ return typeof adapter?.setSoundFontCache === "function";
1648
+ }
1635
1649
  function createToneAdapter(options) {
1636
1650
  getGlobalContext();
1637
1651
  let _playoutGeneration = 1;
@@ -1649,85 +1663,98 @@ function createToneAdapter(options) {
1649
1663
  let _pendingInit = null;
1650
1664
  const _ppqn = options?.ppqn ?? 192;
1651
1665
  let _bpm = 120;
1652
- function addTrackToPlayout(p, track) {
1666
+ let _soundFontCache = options?.soundFontCache;
1667
+ const _currentTracks = /* @__PURE__ */ new Map();
1668
+ const _midiTrackBuild = /* @__PURE__ */ new Map();
1669
+ function midiPlayoutTrackId(track) {
1670
+ const hasAudio = track.clips.some((c) => c.audioBuffer && !c.midiNotes);
1671
+ return hasAudio ? `${track.id}:midi` : track.id;
1672
+ }
1673
+ function addAudioTrackToPlayout(p, track) {
1653
1674
  const audioClips = track.clips.filter((c) => c.audioBuffer && !c.midiNotes);
1675
+ if (audioClips.length === 0) return;
1676
+ const startTime = Math.min(...audioClips.map(clipStartTime));
1677
+ const endTime = Math.max(...audioClips.map(clipEndTime));
1678
+ const trackObj = {
1679
+ id: track.id,
1680
+ name: track.name,
1681
+ gain: track.volume,
1682
+ muted: track.muted,
1683
+ soloed: track.soloed,
1684
+ stereoPan: track.pan,
1685
+ startTime,
1686
+ endTime
1687
+ };
1688
+ const clipInfos = audioClips.map((clip) => ({
1689
+ buffer: clip.audioBuffer,
1690
+ startTime: clipStartTime(clip) - startTime,
1691
+ duration: clipDurationTime(clip),
1692
+ offset: clipOffsetTime(clip),
1693
+ fadeIn: clip.fadeIn,
1694
+ fadeOut: clip.fadeOut,
1695
+ gain: clip.gain
1696
+ }));
1697
+ p.addTrack({
1698
+ clips: clipInfos,
1699
+ track: trackObj,
1700
+ effects: track.effects,
1701
+ channelCount: trackChannelCount(track)
1702
+ });
1703
+ }
1704
+ function addMidiTrackToPlayout(p, track) {
1654
1705
  const midiClips = track.clips.filter((c) => c.midiNotes && c.midiNotes.length > 0);
1655
- if (audioClips.length > 0) {
1656
- const startTime = Math.min(...audioClips.map(clipStartTime));
1657
- const endTime = Math.max(...audioClips.map(clipEndTime));
1658
- const trackObj = {
1659
- id: track.id,
1660
- name: track.name,
1661
- gain: track.volume,
1662
- muted: track.muted,
1663
- soloed: track.soloed,
1664
- stereoPan: track.pan,
1665
- startTime,
1666
- endTime
1667
- };
1668
- const clipInfos = audioClips.map((clip) => ({
1669
- buffer: clip.audioBuffer,
1670
- startTime: clipStartTime(clip) - startTime,
1671
- duration: clipDurationTime(clip),
1672
- offset: clipOffsetTime(clip),
1673
- fadeIn: clip.fadeIn,
1674
- fadeOut: clip.fadeOut,
1675
- gain: clip.gain
1676
- }));
1677
- p.addTrack({
1678
- clips: clipInfos,
1706
+ if (midiClips.length === 0) return;
1707
+ const startTime = Math.min(...midiClips.map(clipStartTime));
1708
+ const endTime = Math.max(...midiClips.map(clipEndTime));
1709
+ const trackId = midiPlayoutTrackId(track);
1710
+ const trackObj = {
1711
+ id: trackId,
1712
+ name: track.name,
1713
+ gain: track.volume,
1714
+ muted: track.muted,
1715
+ soloed: track.soloed,
1716
+ stereoPan: track.pan,
1717
+ startTime,
1718
+ endTime
1719
+ };
1720
+ const midiClipInfos = midiClips.map((clip) => ({
1721
+ notes: clip.midiNotes,
1722
+ startTime: clipStartTime(clip) - startTime,
1723
+ duration: clipDurationTime(clip),
1724
+ offset: clipOffsetTime(clip)
1725
+ }));
1726
+ if (_soundFontCache?.isLoaded) {
1727
+ const firstClip = midiClips[0];
1728
+ const midiChannel = firstClip.midiChannel;
1729
+ const isPercussion = midiChannel === 9;
1730
+ const programNumber = firstClip.midiProgram ?? 0;
1731
+ p.addSoundFontTrack({
1732
+ clips: midiClipInfos,
1679
1733
  track: trackObj,
1680
- effects: track.effects,
1681
- channelCount: trackChannelCount(track)
1734
+ soundFontCache: _soundFontCache,
1735
+ programNumber,
1736
+ isPercussion,
1737
+ effects: track.effects
1682
1738
  });
1683
- }
1684
- if (midiClips.length > 0) {
1685
- const startTime = Math.min(...midiClips.map(clipStartTime));
1686
- const endTime = Math.max(...midiClips.map(clipEndTime));
1687
- const trackId = audioClips.length > 0 ? `${track.id}:midi` : track.id;
1688
- const trackObj = {
1689
- id: trackId,
1690
- name: track.name,
1691
- gain: track.volume,
1692
- muted: track.muted,
1693
- soloed: track.soloed,
1694
- stereoPan: track.pan,
1695
- startTime,
1696
- endTime
1697
- };
1698
- const midiClipInfos = midiClips.map((clip) => ({
1699
- notes: clip.midiNotes,
1700
- startTime: clipStartTime(clip) - startTime,
1701
- duration: clipDurationTime(clip),
1702
- offset: clipOffsetTime(clip)
1703
- }));
1704
- if (options?.soundFontCache?.isLoaded) {
1705
- const firstClip = midiClips[0];
1706
- const midiChannel = firstClip.midiChannel;
1707
- const isPercussion = midiChannel === 9;
1708
- const programNumber = firstClip.midiProgram ?? 0;
1709
- p.addSoundFontTrack({
1710
- clips: midiClipInfos,
1711
- track: trackObj,
1712
- soundFontCache: options.soundFontCache,
1713
- programNumber,
1714
- isPercussion,
1715
- effects: track.effects
1716
- });
1717
- } else {
1718
- if (options?.soundFontCache) {
1719
- console.warn(
1720
- `[waveform-playlist] SoundFont not loaded for track "${track.name}" \u2014 falling back to PolySynth.`
1721
- );
1722
- }
1723
- p.addMidiTrack({
1724
- clips: midiClipInfos,
1725
- track: trackObj,
1726
- effects: track.effects
1727
- });
1739
+ _midiTrackBuild.set(trackId, _soundFontCache);
1740
+ } else {
1741
+ if (_soundFontCache) {
1742
+ console.warn(
1743
+ `[waveform-playlist] SoundFont not loaded for track "${track.name}" \u2014 falling back to PolySynth.`
1744
+ );
1728
1745
  }
1746
+ p.addMidiTrack({
1747
+ clips: midiClipInfos,
1748
+ track: trackObj,
1749
+ effects: track.effects
1750
+ });
1751
+ _midiTrackBuild.set(trackId, null);
1729
1752
  }
1730
1753
  }
1754
+ function addTrackToPlayout(p, track) {
1755
+ addAudioTrackToPlayout(p, track);
1756
+ addMidiTrackToPlayout(p, track);
1757
+ }
1731
1758
  function buildPlayout(tracks) {
1732
1759
  if (playout) {
1733
1760
  try {
@@ -1775,6 +1802,11 @@ function createToneAdapter(options) {
1775
1802
  }
1776
1803
  },
1777
1804
  setTracks(tracks) {
1805
+ _currentTracks.clear();
1806
+ _midiTrackBuild.clear();
1807
+ for (const track of tracks) {
1808
+ _currentTracks.set(track.id, track);
1809
+ }
1778
1810
  if (!playout) {
1779
1811
  buildPlayout(tracks);
1780
1812
  return;
@@ -1802,6 +1834,7 @@ function createToneAdapter(options) {
1802
1834
  }
1803
1835
  },
1804
1836
  updateTrack(trackId, track) {
1837
+ _currentTracks.set(trackId, track);
1805
1838
  if (!playout) return;
1806
1839
  const audioClips = track.clips.filter((c) => c.audioBuffer && !c.midiNotes);
1807
1840
  if (audioClips.length > 0) {
@@ -1820,7 +1853,7 @@ function createToneAdapter(options) {
1820
1853
  if (midiClips.length > 0) {
1821
1854
  const midiTrackId = trackId + ":midi";
1822
1855
  playout.removeTrack(midiTrackId);
1823
- addTrackToPlayout(playout, track);
1856
+ addMidiTrackToPlayout(playout, track);
1824
1857
  if (_isPlaying) {
1825
1858
  playout.resumeTrackMidPlayback(midiTrackId);
1826
1859
  }
@@ -1832,6 +1865,8 @@ function createToneAdapter(options) {
1832
1865
  }
1833
1866
  playout.removeTrack(trackId);
1834
1867
  playout.removeTrack(trackId + ":midi");
1868
+ _midiTrackBuild.delete(trackId);
1869
+ _midiTrackBuild.delete(trackId + ":midi");
1835
1870
  addTrackToPlayout(playout, track);
1836
1871
  playout.applyInitialSoloState();
1837
1872
  if (_isPlaying) {
@@ -1840,6 +1875,7 @@ function createToneAdapter(options) {
1840
1875
  }
1841
1876
  },
1842
1877
  addTrack(track) {
1878
+ _currentTracks.set(track.id, track);
1843
1879
  if (!playout) {
1844
1880
  console.warn(
1845
1881
  "[waveform-playlist] adapter.addTrack() called but playout is not available (adapter may have been disposed)."
@@ -1850,8 +1886,12 @@ function createToneAdapter(options) {
1850
1886
  playout.applyInitialSoloState();
1851
1887
  },
1852
1888
  removeTrack(trackId) {
1889
+ _currentTracks.delete(trackId);
1890
+ _midiTrackBuild.delete(trackId);
1891
+ _midiTrackBuild.delete(trackId + ":midi");
1853
1892
  if (!playout) return;
1854
1893
  playout.removeTrack(trackId);
1894
+ playout.removeTrack(trackId + ":midi");
1855
1895
  playout.applyInitialSoloState();
1856
1896
  },
1857
1897
  play(startTime, endTime) {
@@ -1886,15 +1926,23 @@ function createToneAdapter(options) {
1886
1926
  playout?.setMasterGain(volume);
1887
1927
  },
1888
1928
  setTrackVolume(trackId, volume) {
1929
+ const existing = _currentTracks.get(trackId);
1930
+ if (existing) _currentTracks.set(trackId, { ...existing, volume });
1889
1931
  playout?.getTrack(trackId)?.setVolume(volume);
1890
1932
  },
1891
1933
  setTrackMute(trackId, muted) {
1934
+ const existing = _currentTracks.get(trackId);
1935
+ if (existing) _currentTracks.set(trackId, { ...existing, muted });
1892
1936
  playout?.setMute(trackId, muted);
1893
1937
  },
1894
1938
  setTrackSolo(trackId, soloed) {
1939
+ const existing = _currentTracks.get(trackId);
1940
+ if (existing) _currentTracks.set(trackId, { ...existing, soloed });
1895
1941
  playout?.setSolo(trackId, soloed);
1896
1942
  },
1897
1943
  setTrackPan(trackId, pan) {
1944
+ const existing = _currentTracks.get(trackId);
1945
+ if (existing) _currentTracks.set(trackId, { ...existing, pan });
1898
1946
  playout?.getTrack(trackId)?.setPan(pan);
1899
1947
  },
1900
1948
  setLoop(enabled, start2, end) {
@@ -1906,6 +1954,9 @@ function createToneAdapter(options) {
1906
1954
  get audioContext() {
1907
1955
  return getGlobalAudioContext();
1908
1956
  },
1957
+ get lookAhead() {
1958
+ return getGlobalContext().lookAhead ?? 0;
1959
+ },
1909
1960
  get ppqn() {
1910
1961
  return _ppqn;
1911
1962
  },
@@ -1950,6 +2001,38 @@ function createToneAdapter(options) {
1950
2001
  }
1951
2002
  return playout.masterOutputNode;
1952
2003
  },
2004
+ setSoundFontCache(cache) {
2005
+ _soundFontCache = cache;
2006
+ if (cache && !cache.isLoaded) {
2007
+ console.warn(
2008
+ "[waveform-playlist] setSoundFontCache called with a SoundFontCache that is not loaded \u2014 MIDI tracks remain on PolySynth. Await cache.load() and call again."
2009
+ );
2010
+ }
2011
+ if (!playout) return;
2012
+ const effective = cache?.isLoaded ? cache : null;
2013
+ let changed = false;
2014
+ for (const track of _currentTracks.values()) {
2015
+ const hasMidi = track.clips.some((c) => c.midiNotes && c.midiNotes.length > 0);
2016
+ if (!hasMidi) continue;
2017
+ const midiTrackId = midiPlayoutTrackId(track);
2018
+ if (_midiTrackBuild.get(midiTrackId) === effective) continue;
2019
+ try {
2020
+ playout.removeTrack(midiTrackId);
2021
+ addMidiTrackToPlayout(playout, track);
2022
+ if (_isPlaying) {
2023
+ playout.resumeTrackMidPlayback(midiTrackId);
2024
+ }
2025
+ changed = true;
2026
+ } catch (err) {
2027
+ console.warn(
2028
+ `[waveform-playlist] SoundFont swap failed for track "${track.name}": ` + String(err)
2029
+ );
2030
+ }
2031
+ }
2032
+ if (changed) {
2033
+ playout.applyInitialSoloState();
2034
+ }
2035
+ },
1953
2036
  dispose() {
1954
2037
  try {
1955
2038
  playout?.dispose();
@@ -1958,6 +2041,8 @@ function createToneAdapter(options) {
1958
2041
  }
1959
2042
  playout = null;
1960
2043
  _isPlaying = false;
2044
+ _currentTracks.clear();
2045
+ _midiTrackBuild.clear();
1961
2046
  }
1962
2047
  };
1963
2048
  }
@@ -1981,6 +2066,7 @@ export {
1981
2066
  getUnderlyingAudioParam,
1982
2067
  hasMediaStreamSource,
1983
2068
  int16ToFloat32,
2069
+ isToneAdapter,
1984
2070
  releaseMediaStreamSource,
1985
2071
  resumeGlobalAudioContext,
1986
2072
  timecentsToSeconds