supersonic-scsynth 0.4.0 → 0.6.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.
@@ -993,44 +993,6 @@ var ScsynthOSC = class {
993
993
  type: "clear"
994
994
  });
995
995
  }
996
- /**
997
- * Get statistics from all workers
998
- */
999
- async getStats() {
1000
- if (!this.initialized) {
1001
- return null;
1002
- }
1003
- const statsPromises = [
1004
- this.getWorkerStats(this.workers.oscOut, "oscOut"),
1005
- this.getWorkerStats(this.workers.oscIn, "oscIn"),
1006
- this.getWorkerStats(this.workers.debug, "debug")
1007
- ];
1008
- const results = await Promise.all(statsPromises);
1009
- return {
1010
- oscOut: results[0],
1011
- oscIn: results[1],
1012
- debug: results[2]
1013
- };
1014
- }
1015
- /**
1016
- * Get stats from a single worker
1017
- */
1018
- getWorkerStats(worker, name) {
1019
- return new Promise((resolve) => {
1020
- const timeout = setTimeout(() => {
1021
- resolve({ error: "Timeout getting stats" });
1022
- }, 1e3);
1023
- const handler = (event) => {
1024
- if (event.data.type === "stats") {
1025
- clearTimeout(timeout);
1026
- worker.removeEventListener("message", handler);
1027
- resolve(event.data.stats);
1028
- }
1029
- };
1030
- worker.addEventListener("message", handler);
1031
- worker.postMessage({ type: "getStats" });
1032
- });
1033
- }
1034
996
  /**
1035
997
  * Set callback for raw binary OSC messages received from scsynth
1036
998
  */
@@ -1601,37 +1563,145 @@ var MemPool = class {
1601
1563
  var __blockDataAddress = (blockAddress) => blockAddress > 0 ? blockAddress + SIZEOF_MEM_BLOCK : 0;
1602
1564
  var __blockSelfAddress = (dataAddress) => dataAddress > 0 ? dataAddress - SIZEOF_MEM_BLOCK : 0;
1603
1565
 
1604
- // js/timing_constants.js
1605
- var NTP_EPOCH_OFFSET = 2208988800;
1606
- var DRIFT_UPDATE_INTERVAL_MS = 15e3;
1607
-
1608
- // js/supersonic.js
1566
+ // js/lib/buffer_manager.js
1567
+ var BUFFER_POOL_ALIGNMENT = 8;
1609
1568
  var BufferManager = class {
1569
+ // Private configuration
1570
+ #sampleBaseURL;
1571
+ #audioPathMap;
1572
+ // Private implementation
1573
+ #audioContext;
1574
+ #sharedBuffer;
1575
+ #bufferPool;
1576
+ #allocatedBuffers;
1577
+ #pendingBufferOps;
1578
+ #bufferLocks;
1610
1579
  constructor(options) {
1611
1580
  const {
1612
1581
  audioContext,
1613
1582
  sharedBuffer,
1614
- bufferPool,
1615
- allocatedBuffers,
1616
- resolveAudioPath,
1617
- registerPendingOp
1583
+ bufferPoolConfig,
1584
+ sampleBaseURL,
1585
+ audioPathMap = {},
1586
+ maxBuffers = 1024
1618
1587
  } = options;
1619
- this.audioContext = audioContext;
1620
- this.sharedBuffer = sharedBuffer;
1621
- this.bufferPool = bufferPool;
1622
- this.allocatedBuffers = allocatedBuffers;
1623
- this.resolveAudioPath = resolveAudioPath;
1624
- this.registerPendingOp = registerPendingOp;
1625
- this.bufferLocks = /* @__PURE__ */ new Map();
1588
+ if (!audioContext) {
1589
+ throw new Error("BufferManager requires audioContext");
1590
+ }
1591
+ if (!sharedBuffer || !(sharedBuffer instanceof SharedArrayBuffer)) {
1592
+ throw new Error("BufferManager requires sharedBuffer (SharedArrayBuffer)");
1593
+ }
1594
+ if (!bufferPoolConfig || typeof bufferPoolConfig !== "object") {
1595
+ throw new Error("BufferManager requires bufferPoolConfig (object with start, size, align)");
1596
+ }
1597
+ if (!Number.isFinite(bufferPoolConfig.start) || bufferPoolConfig.start < 0) {
1598
+ throw new Error("bufferPoolConfig.start must be a non-negative number");
1599
+ }
1600
+ if (!Number.isFinite(bufferPoolConfig.size) || bufferPoolConfig.size <= 0) {
1601
+ throw new Error("bufferPoolConfig.size must be a positive number");
1602
+ }
1603
+ if (audioPathMap && typeof audioPathMap !== "object") {
1604
+ throw new Error("audioPathMap must be an object");
1605
+ }
1606
+ if (!Number.isInteger(maxBuffers) || maxBuffers <= 0) {
1607
+ throw new Error("maxBuffers must be a positive integer");
1608
+ }
1609
+ this.#audioContext = audioContext;
1610
+ this.#sharedBuffer = sharedBuffer;
1611
+ this.#sampleBaseURL = sampleBaseURL;
1612
+ this.#audioPathMap = audioPathMap;
1613
+ this.#bufferPool = new MemPool({
1614
+ buf: sharedBuffer,
1615
+ start: bufferPoolConfig.start,
1616
+ size: bufferPoolConfig.size,
1617
+ align: BUFFER_POOL_ALIGNMENT
1618
+ });
1619
+ this.#allocatedBuffers = /* @__PURE__ */ new Map();
1620
+ this.#pendingBufferOps = /* @__PURE__ */ new Map();
1621
+ this.#bufferLocks = /* @__PURE__ */ new Map();
1626
1622
  this.GUARD_BEFORE = 3;
1627
1623
  this.GUARD_AFTER = 1;
1628
- this.MAX_BUFFERS = 1024;
1624
+ this.MAX_BUFFERS = maxBuffers;
1625
+ const poolSizeMB = (bufferPoolConfig.size / (1024 * 1024)).toFixed(0);
1626
+ const poolOffsetMB = (bufferPoolConfig.start / (1024 * 1024)).toFixed(0);
1627
+ console.log(`[BufferManager] Initialized: ${poolSizeMB}MB pool at offset ${poolOffsetMB}MB`);
1628
+ }
1629
+ #resolveAudioPath(scPath) {
1630
+ if (typeof scPath !== "string" || scPath.length === 0) {
1631
+ throw new Error(`Invalid audio path: must be a non-empty string`);
1632
+ }
1633
+ if (scPath.includes("..")) {
1634
+ throw new Error(`Invalid audio path: path cannot contain '..' (got: ${scPath})`);
1635
+ }
1636
+ if (scPath.startsWith("/") || /^[a-zA-Z]:/.test(scPath)) {
1637
+ throw new Error(`Invalid audio path: path must be relative (got: ${scPath})`);
1638
+ }
1639
+ if (scPath.includes("%2e") || scPath.includes("%2E")) {
1640
+ throw new Error(`Invalid audio path: path cannot contain URL-encoded characters (got: ${scPath})`);
1641
+ }
1642
+ if (scPath.includes("\\")) {
1643
+ throw new Error(`Invalid audio path: use forward slashes only (got: ${scPath})`);
1644
+ }
1645
+ if (this.#audioPathMap[scPath]) {
1646
+ return this.#audioPathMap[scPath];
1647
+ }
1648
+ if (!this.#sampleBaseURL) {
1649
+ throw new Error(
1650
+ 'sampleBaseURL not configured. Please set it in SuperSonic constructor options.\nExample: new SuperSonic({ sampleBaseURL: "./dist/samples/" })\nOr use CDN: new SuperSonic({ sampleBaseURL: "https://unpkg.com/supersonic-scsynth-samples@latest/samples/" })\nOr install: npm install supersonic-scsynth-samples'
1651
+ );
1652
+ }
1653
+ return this.#sampleBaseURL + scPath;
1629
1654
  }
1630
1655
  #validateBufferNumber(bufnum) {
1631
1656
  if (!Number.isInteger(bufnum) || bufnum < 0 || bufnum >= this.MAX_BUFFERS) {
1632
1657
  throw new Error(`Invalid buffer number ${bufnum} (must be 0-${this.MAX_BUFFERS - 1})`);
1633
1658
  }
1634
1659
  }
1660
+ /**
1661
+ * Execute a buffer operation with proper locking, registration, and cleanup
1662
+ * @private
1663
+ * @param {number} bufnum - Buffer number
1664
+ * @param {number} timeoutMs - Operation timeout
1665
+ * @param {Function} operation - Async function that performs the actual buffer work
1666
+ * Should return {ptr, sizeBytes, ...extraProps}
1667
+ * @returns {Promise<Object>} Result object with ptr, uuid, allocationComplete, and extra props
1668
+ */
1669
+ async #executeBufferOperation(bufnum, timeoutMs, operation) {
1670
+ let allocatedPtr = null;
1671
+ let pendingToken = null;
1672
+ let allocationRegistered = false;
1673
+ const releaseLock = await this.#acquireBufferLock(bufnum);
1674
+ let lockReleased = false;
1675
+ try {
1676
+ await this.#awaitPendingReplacement(bufnum);
1677
+ const { ptr, sizeBytes, ...extraProps } = await operation();
1678
+ allocatedPtr = ptr;
1679
+ const { uuid, allocationComplete } = this.#registerPending(bufnum, timeoutMs);
1680
+ pendingToken = uuid;
1681
+ this.#recordAllocation(bufnum, allocatedPtr, sizeBytes, uuid, allocationComplete);
1682
+ allocationRegistered = true;
1683
+ const managedCompletion = this.#attachFinalizer(bufnum, uuid, allocationComplete);
1684
+ releaseLock();
1685
+ lockReleased = true;
1686
+ return {
1687
+ ptr: allocatedPtr,
1688
+ uuid,
1689
+ allocationComplete: managedCompletion,
1690
+ ...extraProps
1691
+ };
1692
+ } catch (error) {
1693
+ if (allocationRegistered && pendingToken) {
1694
+ this.#finalizeReplacement(bufnum, pendingToken, false);
1695
+ } else if (allocatedPtr) {
1696
+ this.#bufferPool.free(allocatedPtr);
1697
+ }
1698
+ throw error;
1699
+ } finally {
1700
+ if (!lockReleased) {
1701
+ releaseLock();
1702
+ }
1703
+ }
1704
+ }
1635
1705
  async prepareFromFile(params) {
1636
1706
  const {
1637
1707
  bufnum,
@@ -1641,20 +1711,14 @@ var BufferManager = class {
1641
1711
  channels = null
1642
1712
  } = params;
1643
1713
  this.#validateBufferNumber(bufnum);
1644
- let allocatedPtr = null;
1645
- let pendingToken = null;
1646
- let allocationRegistered = false;
1647
- const releaseLock = await this.#acquireBufferLock(bufnum);
1648
- let lockReleased = false;
1649
- try {
1650
- await this.#awaitPendingReplacement(bufnum);
1651
- const resolvedPath = this.resolveAudioPath(path);
1714
+ return this.#executeBufferOperation(bufnum, 6e4, async () => {
1715
+ const resolvedPath = this.#resolveAudioPath(path);
1652
1716
  const response = await fetch(resolvedPath);
1653
1717
  if (!response.ok) {
1654
1718
  throw new Error(`Failed to fetch ${resolvedPath}: ${response.status} ${response.statusText}`);
1655
1719
  }
1656
1720
  const arrayBuffer = await response.arrayBuffer();
1657
- const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
1721
+ const audioBuffer = await this.#audioContext.decodeAudioData(arrayBuffer);
1658
1722
  const start = Math.max(0, Math.floor(startFrame || 0));
1659
1723
  const availableFrames = audioBuffer.length - start;
1660
1724
  const framesRequested = numFrames && numFrames > 0 ? Math.min(Math.floor(numFrames), availableFrames) : availableFrames;
@@ -1664,7 +1728,7 @@ var BufferManager = class {
1664
1728
  const selectedChannels = this.#normalizeChannels(channels, audioBuffer.numberOfChannels);
1665
1729
  const numChannels = selectedChannels.length;
1666
1730
  const totalSamples = framesRequested * numChannels + (this.GUARD_BEFORE + this.GUARD_AFTER) * numChannels;
1667
- allocatedPtr = this.#malloc(totalSamples);
1731
+ const ptr = this.#malloc(totalSamples);
1668
1732
  const interleaved = new Float32Array(totalSamples);
1669
1733
  const dataOffset = this.GUARD_BEFORE * numChannels;
1670
1734
  for (let frame = 0; frame < framesRequested; frame++) {
@@ -1674,35 +1738,16 @@ var BufferManager = class {
1674
1738
  interleaved[dataOffset + frame * numChannels + ch] = channelData[start + frame];
1675
1739
  }
1676
1740
  }
1677
- this.#writeToSharedBuffer(allocatedPtr, interleaved);
1741
+ this.#writeToSharedBuffer(ptr, interleaved);
1678
1742
  const sizeBytes = interleaved.length * 4;
1679
- const { uuid, allocationComplete } = this.#registerPending(bufnum);
1680
- pendingToken = uuid;
1681
- this.#recordAllocation(bufnum, allocatedPtr, sizeBytes, uuid, allocationComplete);
1682
- allocationRegistered = true;
1683
- const managedCompletion = this.#attachFinalizer(bufnum, uuid, allocationComplete);
1684
- releaseLock();
1685
- lockReleased = true;
1686
1743
  return {
1687
- ptr: allocatedPtr,
1744
+ ptr,
1745
+ sizeBytes,
1688
1746
  numFrames: framesRequested,
1689
1747
  numChannels,
1690
- sampleRate: audioBuffer.sampleRate,
1691
- uuid,
1692
- allocationComplete: managedCompletion
1748
+ sampleRate: audioBuffer.sampleRate
1693
1749
  };
1694
- } catch (error) {
1695
- if (allocationRegistered && pendingToken) {
1696
- this.#finalizeReplacement(bufnum, pendingToken, false);
1697
- } else if (allocatedPtr) {
1698
- this.bufferPool.free(allocatedPtr);
1699
- }
1700
- throw error;
1701
- } finally {
1702
- if (!lockReleased) {
1703
- releaseLock();
1704
- }
1705
- }
1750
+ });
1706
1751
  }
1707
1752
  async prepareEmpty(params) {
1708
1753
  const {
@@ -1712,9 +1757,6 @@ var BufferManager = class {
1712
1757
  sampleRate = null
1713
1758
  } = params;
1714
1759
  this.#validateBufferNumber(bufnum);
1715
- let allocationRegistered = false;
1716
- let pendingToken = null;
1717
- let allocatedPtr = null;
1718
1760
  if (!Number.isFinite(numFrames) || numFrames <= 0) {
1719
1761
  throw new Error(`/b_alloc requires a positive number of frames (got ${numFrames})`);
1720
1762
  }
@@ -1723,42 +1765,20 @@ var BufferManager = class {
1723
1765
  }
1724
1766
  const roundedFrames = Math.floor(numFrames);
1725
1767
  const roundedChannels = Math.floor(numChannels);
1726
- const totalSamples = roundedFrames * roundedChannels + (this.GUARD_BEFORE + this.GUARD_AFTER) * roundedChannels;
1727
- const releaseLock = await this.#acquireBufferLock(bufnum);
1728
- let lockReleased = false;
1729
- try {
1730
- await this.#awaitPendingReplacement(bufnum);
1731
- allocatedPtr = this.#malloc(totalSamples);
1768
+ return this.#executeBufferOperation(bufnum, 5e3, async () => {
1769
+ const totalSamples = roundedFrames * roundedChannels + (this.GUARD_BEFORE + this.GUARD_AFTER) * roundedChannels;
1770
+ const ptr = this.#malloc(totalSamples);
1732
1771
  const interleaved = new Float32Array(totalSamples);
1733
- this.#writeToSharedBuffer(allocatedPtr, interleaved);
1772
+ this.#writeToSharedBuffer(ptr, interleaved);
1734
1773
  const sizeBytes = interleaved.length * 4;
1735
- const { uuid, allocationComplete } = this.#registerPending(bufnum);
1736
- pendingToken = uuid;
1737
- this.#recordAllocation(bufnum, allocatedPtr, sizeBytes, uuid, allocationComplete);
1738
- allocationRegistered = true;
1739
- const managedCompletion = this.#attachFinalizer(bufnum, uuid, allocationComplete);
1740
- releaseLock();
1741
- lockReleased = true;
1742
1774
  return {
1743
- ptr: allocatedPtr,
1775
+ ptr,
1776
+ sizeBytes,
1744
1777
  numFrames: roundedFrames,
1745
1778
  numChannels: roundedChannels,
1746
- sampleRate: sampleRate || this.audioContext.sampleRate,
1747
- uuid,
1748
- allocationComplete: managedCompletion
1779
+ sampleRate: sampleRate || this.#audioContext.sampleRate
1749
1780
  };
1750
- } catch (error) {
1751
- if (allocationRegistered && pendingToken) {
1752
- this.#finalizeReplacement(bufnum, pendingToken, false);
1753
- } else if (allocatedPtr) {
1754
- this.bufferPool.free(allocatedPtr);
1755
- }
1756
- throw error;
1757
- } finally {
1758
- if (!lockReleased) {
1759
- releaseLock();
1760
- }
1761
- }
1781
+ });
1762
1782
  }
1763
1783
  #normalizeChannels(requestedChannels, fileChannels) {
1764
1784
  if (!requestedChannels || requestedChannels.length === 0) {
@@ -1773,9 +1793,9 @@ var BufferManager = class {
1773
1793
  }
1774
1794
  #malloc(totalSamples) {
1775
1795
  const bytesNeeded = totalSamples * 4;
1776
- const ptr = this.bufferPool.malloc(bytesNeeded);
1796
+ const ptr = this.#bufferPool.malloc(bytesNeeded);
1777
1797
  if (ptr === 0) {
1778
- const stats = this.bufferPool.stats();
1798
+ const stats = this.#bufferPool.stats();
1779
1799
  const availableMB = ((stats.available || 0) / (1024 * 1024)).toFixed(2);
1780
1800
  const totalMB = ((stats.total || 0) / (1024 * 1024)).toFixed(2);
1781
1801
  const requestedMB = (bytesNeeded / (1024 * 1024)).toFixed(2);
@@ -1786,40 +1806,43 @@ var BufferManager = class {
1786
1806
  return ptr;
1787
1807
  }
1788
1808
  #writeToSharedBuffer(ptr, data) {
1789
- const heap = new Float32Array(this.sharedBuffer, ptr, data.length);
1809
+ const heap = new Float32Array(this.#sharedBuffer, ptr, data.length);
1790
1810
  heap.set(data);
1791
1811
  }
1792
- #registerPending(bufnum) {
1793
- if (!this.registerPendingOp) {
1794
- return {
1795
- uuid: crypto.randomUUID(),
1796
- allocationComplete: Promise.resolve()
1797
- };
1798
- }
1812
+ #createPendingOperation(uuid, bufnum, timeoutMs) {
1813
+ return new Promise((resolve, reject) => {
1814
+ const timeout = setTimeout(() => {
1815
+ this.#pendingBufferOps.delete(uuid);
1816
+ reject(new Error(`Buffer ${bufnum} allocation timeout (${timeoutMs}ms)`));
1817
+ }, timeoutMs);
1818
+ this.#pendingBufferOps.set(uuid, { resolve, reject, timeout });
1819
+ });
1820
+ }
1821
+ #registerPending(bufnum, timeoutMs) {
1799
1822
  const uuid = crypto.randomUUID();
1800
- const allocationComplete = this.registerPendingOp(uuid, bufnum);
1823
+ const allocationComplete = this.#createPendingOperation(uuid, bufnum, timeoutMs);
1801
1824
  return { uuid, allocationComplete };
1802
1825
  }
1803
1826
  async #acquireBufferLock(bufnum) {
1804
- const prev = this.bufferLocks.get(bufnum) || Promise.resolve();
1827
+ const prev = this.#bufferLocks.get(bufnum) || Promise.resolve();
1805
1828
  let releaseLock;
1806
1829
  const current = new Promise((resolve) => {
1807
1830
  releaseLock = resolve;
1808
1831
  });
1809
- this.bufferLocks.set(bufnum, prev.then(() => current));
1832
+ this.#bufferLocks.set(bufnum, prev.then(() => current));
1810
1833
  await prev;
1811
1834
  return () => {
1812
1835
  if (releaseLock) {
1813
1836
  releaseLock();
1814
1837
  releaseLock = null;
1815
1838
  }
1816
- if (this.bufferLocks.get(bufnum) === current) {
1817
- this.bufferLocks.delete(bufnum);
1839
+ if (this.#bufferLocks.get(bufnum) === current) {
1840
+ this.#bufferLocks.delete(bufnum);
1818
1841
  }
1819
1842
  };
1820
1843
  }
1821
1844
  #recordAllocation(bufnum, ptr, sizeBytes, pendingToken, pendingPromise) {
1822
- const previousEntry = this.allocatedBuffers.get(bufnum);
1845
+ const previousEntry = this.#allocatedBuffers.get(bufnum);
1823
1846
  const entry = {
1824
1847
  ptr,
1825
1848
  size: sizeBytes,
@@ -1827,11 +1850,11 @@ var BufferManager = class {
1827
1850
  pendingPromise,
1828
1851
  previousAllocation: previousEntry ? { ptr: previousEntry.ptr, size: previousEntry.size } : null
1829
1852
  };
1830
- this.allocatedBuffers.set(bufnum, entry);
1853
+ this.#allocatedBuffers.set(bufnum, entry);
1831
1854
  return entry;
1832
1855
  }
1833
1856
  async #awaitPendingReplacement(bufnum) {
1834
- const existing = this.allocatedBuffers.get(bufnum);
1857
+ const existing = this.#allocatedBuffers.get(bufnum);
1835
1858
  if (existing && existing.pendingToken && existing.pendingPromise) {
1836
1859
  try {
1837
1860
  await existing.pendingPromise;
@@ -1853,7 +1876,7 @@ var BufferManager = class {
1853
1876
  });
1854
1877
  }
1855
1878
  #finalizeReplacement(bufnum, pendingToken, success) {
1856
- const entry = this.allocatedBuffers.get(bufnum);
1879
+ const entry = this.#allocatedBuffers.get(bufnum);
1857
1880
  if (!entry || entry.pendingToken !== pendingToken) {
1858
1881
  return;
1859
1882
  }
@@ -1863,57 +1886,382 @@ var BufferManager = class {
1863
1886
  entry.pendingPromise = null;
1864
1887
  entry.previousAllocation = null;
1865
1888
  if (previous?.ptr) {
1866
- this.bufferPool.free(previous.ptr);
1889
+ this.#bufferPool.free(previous.ptr);
1867
1890
  }
1868
1891
  return;
1869
1892
  }
1870
1893
  if (entry.ptr) {
1871
- this.bufferPool.free(entry.ptr);
1894
+ this.#bufferPool.free(entry.ptr);
1872
1895
  }
1873
1896
  entry.pendingPromise = null;
1874
1897
  if (previous?.ptr) {
1875
- this.allocatedBuffers.set(bufnum, {
1898
+ this.#allocatedBuffers.set(bufnum, {
1876
1899
  ptr: previous.ptr,
1877
1900
  size: previous.size,
1878
1901
  pendingToken: null,
1879
1902
  previousAllocation: null
1880
1903
  });
1881
1904
  } else {
1882
- this.allocatedBuffers.delete(bufnum);
1905
+ this.#allocatedBuffers.delete(bufnum);
1906
+ }
1907
+ }
1908
+ /**
1909
+ * Handle /buffer/freed notification from scsynth
1910
+ * Called by SuperSonic when /buffer/freed OSC message is received
1911
+ * @param {Array} args - [bufnum, freedPtr]
1912
+ */
1913
+ handleBufferFreed(args) {
1914
+ const bufnum = args[0];
1915
+ const freedPtr = args[1];
1916
+ const bufferInfo = this.#allocatedBuffers.get(bufnum);
1917
+ if (!bufferInfo) {
1918
+ if (typeof freedPtr === "number" && freedPtr !== 0) {
1919
+ this.#bufferPool.free(freedPtr);
1920
+ }
1921
+ return;
1922
+ }
1923
+ if (typeof freedPtr === "number" && freedPtr === bufferInfo.ptr) {
1924
+ this.#bufferPool.free(bufferInfo.ptr);
1925
+ this.#allocatedBuffers.delete(bufnum);
1926
+ return;
1927
+ }
1928
+ if (typeof freedPtr === "number" && bufferInfo.previousAllocation && bufferInfo.previousAllocation.ptr === freedPtr) {
1929
+ this.#bufferPool.free(freedPtr);
1930
+ bufferInfo.previousAllocation = null;
1931
+ return;
1932
+ }
1933
+ this.#bufferPool.free(bufferInfo.ptr);
1934
+ this.#allocatedBuffers.delete(bufnum);
1935
+ }
1936
+ /**
1937
+ * Handle /buffer/allocated notification from scsynth
1938
+ * Called by SuperSonic when /buffer/allocated OSC message is received
1939
+ * @param {Array} args - [uuid, bufnum]
1940
+ */
1941
+ handleBufferAllocated(args) {
1942
+ const uuid = args[0];
1943
+ const bufnum = args[1];
1944
+ const pending = this.#pendingBufferOps.get(uuid);
1945
+ if (pending) {
1946
+ clearTimeout(pending.timeout);
1947
+ pending.resolve({ bufnum });
1948
+ this.#pendingBufferOps.delete(uuid);
1949
+ }
1950
+ }
1951
+ /**
1952
+ * Allocate raw buffer memory
1953
+ * @param {number} numSamples - Number of Float32 samples
1954
+ * @returns {number} Byte offset, or 0 if failed
1955
+ */
1956
+ allocate(numSamples) {
1957
+ const sizeBytes = numSamples * 4;
1958
+ const addr = this.#bufferPool.malloc(sizeBytes);
1959
+ if (addr === 0) {
1960
+ const stats = this.#bufferPool.stats();
1961
+ const availableMB = ((stats.available || 0) / (1024 * 1024)).toFixed(2);
1962
+ const totalMB = ((stats.total || 0) / (1024 * 1024)).toFixed(2);
1963
+ const requestedMB = (sizeBytes / (1024 * 1024)).toFixed(2);
1964
+ console.error(
1965
+ `[BufferManager] Allocation failed: requested ${requestedMB}MB, available ${availableMB}MB of ${totalMB}MB total`
1966
+ );
1883
1967
  }
1968
+ return addr;
1969
+ }
1970
+ /**
1971
+ * Free previously allocated buffer
1972
+ * @param {number} addr - Buffer address
1973
+ * @returns {boolean} true if freed successfully
1974
+ */
1975
+ free(addr) {
1976
+ return this.#bufferPool.free(addr);
1977
+ }
1978
+ /**
1979
+ * Get Float32Array view of buffer
1980
+ * @param {number} addr - Buffer address
1981
+ * @param {number} numSamples - Number of samples
1982
+ * @returns {Float32Array} Typed array view
1983
+ */
1984
+ getView(addr, numSamples) {
1985
+ return new Float32Array(this.#sharedBuffer, addr, numSamples);
1986
+ }
1987
+ /**
1988
+ * Get buffer pool statistics
1989
+ * @returns {Object} Stats including total, available, used
1990
+ */
1991
+ getStats() {
1992
+ return this.#bufferPool.stats();
1993
+ }
1994
+ /**
1995
+ * Get buffer diagnostics
1996
+ * @returns {Object} Buffer state and pool statistics
1997
+ */
1998
+ getDiagnostics() {
1999
+ const poolStats = this.#bufferPool.stats();
2000
+ let bytesActive = 0;
2001
+ let pendingCount = 0;
2002
+ for (const entry of this.#allocatedBuffers.values()) {
2003
+ if (!entry) continue;
2004
+ bytesActive += entry.size || 0;
2005
+ if (entry.pendingToken) {
2006
+ pendingCount++;
2007
+ }
2008
+ }
2009
+ return {
2010
+ active: this.#allocatedBuffers.size,
2011
+ pending: pendingCount,
2012
+ bytesActive,
2013
+ pool: {
2014
+ total: poolStats.total || 0,
2015
+ available: poolStats.available || 0,
2016
+ freeBytes: poolStats.free?.size || 0,
2017
+ freeBlocks: poolStats.free?.count || 0,
2018
+ usedBytes: poolStats.used?.size || 0,
2019
+ usedBlocks: poolStats.used?.count || 0
2020
+ }
2021
+ };
2022
+ }
2023
+ /**
2024
+ * Clean up resources
2025
+ */
2026
+ destroy() {
2027
+ for (const [uuid, pending] of this.#pendingBufferOps.entries()) {
2028
+ clearTimeout(pending.timeout);
2029
+ pending.reject(new Error("BufferManager destroyed"));
2030
+ }
2031
+ this.#pendingBufferOps.clear();
2032
+ for (const [bufnum, entry] of this.#allocatedBuffers.entries()) {
2033
+ if (entry.ptr) {
2034
+ this.#bufferPool.free(entry.ptr);
2035
+ }
2036
+ }
2037
+ this.#allocatedBuffers.clear();
2038
+ this.#bufferLocks.clear();
2039
+ console.log("[BufferManager] Destroyed");
1884
2040
  }
1885
2041
  };
2042
+
2043
+ // js/timing_constants.js
2044
+ var NTP_EPOCH_OFFSET = 2208988800;
2045
+ var DRIFT_UPDATE_INTERVAL_MS = 15e3;
2046
+
2047
+ // js/memory_layout.js
2048
+ var MemoryLayout = {
2049
+ /**
2050
+ * Total WebAssembly memory in pages (1 page = 64KB)
2051
+ * Current: 1280 pages = 80MB
2052
+ *
2053
+ * This value is used by build.sh to set -sINITIAL_MEMORY
2054
+ * Must match: totalPages * 65536 = bufferPoolOffset + bufferPoolSize
2055
+ */
2056
+ totalPages: 1280,
2057
+ /**
2058
+ * WASM heap size (implicit, first section of memory)
2059
+ * Not directly configurable here - defined by bufferPoolOffset - ringBufferReserved
2060
+ * Current: 0-16MB (16 * 1024 * 1024 = 16777216 bytes)
2061
+ */
2062
+ // wasmHeapSize is implicitly: bufferPoolOffset - ringBufferReserved
2063
+ /**
2064
+ * Ring buffer reserved space (between WASM heap and buffer pool)
2065
+ * Actual ring buffer usage: IN: 768KB, OUT: 128KB, DEBUG: 64KB = 960KB
2066
+ * Plus control structures: CONTROL_SIZE (40B) + METRICS_SIZE (48B) + NTP_START_TIME_SIZE (8B) ≈ 96B
2067
+ * Total actual usage: ~960KB
2068
+ * Reserved: 1MB (provides ~64KB headroom for alignment and future expansion)
2069
+ * Current: 1MB reserved (starts where WASM heap ends at 16MB)
2070
+ */
2071
+ ringBufferReserved: 1 * 1024 * 1024,
2072
+ // 1MB reserved
2073
+ /**
2074
+ * Buffer pool byte offset from start of SharedArrayBuffer
2075
+ * Audio samples are allocated from this pool using @thi.ng/malloc
2076
+ * Must be after WASM heap + ring buffer area
2077
+ * Current: 17MB offset = after 16MB heap + 1MB ring buffers
2078
+ */
2079
+ bufferPoolOffset: 17 * 1024 * 1024,
2080
+ // 17825792 bytes
2081
+ /**
2082
+ * Buffer pool size in bytes
2083
+ * Used for audio sample storage (loaded files + allocated buffers)
2084
+ * Current: 63MB (enough for ~3.5 minutes of stereo at 48kHz uncompressed)
2085
+ */
2086
+ bufferPoolSize: 63 * 1024 * 1024,
2087
+ // 66060288 bytes
2088
+ /**
2089
+ * Total memory calculation (should equal totalPages * 65536)
2090
+ * wasmHeap (16MB) + ringReserve (1MB) + bufferPool (63MB) = 80MB
2091
+ */
2092
+ get totalMemory() {
2093
+ return this.bufferPoolOffset + this.bufferPoolSize;
2094
+ },
2095
+ /**
2096
+ * Effective WASM heap size (derived)
2097
+ * This is the space available for scsynth C++ allocations
2098
+ */
2099
+ get wasmHeapSize() {
2100
+ return this.bufferPoolOffset - this.ringBufferReserved;
2101
+ }
2102
+ };
2103
+
2104
+ // js/scsynth_options.js
2105
+ var defaultWorldOptions = {
2106
+ /**
2107
+ * Maximum number of audio buffers (SndBuf slots)
2108
+ * Each buffer slot: 104 bytes overhead (2x SndBuf + SndBufUpdates structs)
2109
+ * Actual audio data is stored in buffer pool (separate from heap)
2110
+ * Default: 1024 (matching SuperCollider default)
2111
+ * Range: 1-65535 (limited by practical memory constraints)
2112
+ */
2113
+ numBuffers: 1024,
2114
+ /**
2115
+ * Maximum number of synthesis nodes (synths + groups)
2116
+ * Each node: ~200-500 bytes depending on synth complexity
2117
+ * Default: 1024 (matching SuperCollider default)
2118
+ */
2119
+ maxNodes: 1024,
2120
+ /**
2121
+ * Maximum number of synth definitions (SynthDef count)
2122
+ * Each definition: variable size (typically 1-10KB)
2123
+ * Default: 1024 (matching SuperCollider default)
2124
+ */
2125
+ maxGraphDefs: 1024,
2126
+ /**
2127
+ * Maximum wire buffers for internal audio routing
2128
+ * Wire buffers: temporary buffers for UGen connections
2129
+ * Each: bufLength * 8 bytes (128 samples * 8 = 1024 bytes)
2130
+ * Default: 64 (matching SuperCollider default)
2131
+ */
2132
+ maxWireBufs: 64,
2133
+ /**
2134
+ * Number of audio bus channels
2135
+ * Audio buses: real-time audio routing between synths
2136
+ * Memory: bufLength * numChannels * 4 bytes (128 * 128 * 4 = 64KB)
2137
+ * Default: 128 (SuperSonic default, SC uses 1024)
2138
+ */
2139
+ numAudioBusChannels: 128,
2140
+ /**
2141
+ * Number of input bus channels (hardware audio input)
2142
+ * AudioWorklet can support input, but SuperSonic doesn't currently route it
2143
+ * Default: 0 (audio input not implemented)
2144
+ */
2145
+ numInputBusChannels: 0,
2146
+ /**
2147
+ * Number of output bus channels (hardware audio output)
2148
+ * WebAudio/AudioWorklet output
2149
+ * Default: 2 (stereo)
2150
+ */
2151
+ numOutputBusChannels: 2,
2152
+ /**
2153
+ * Number of control bus channels
2154
+ * Control buses: control-rate data sharing between synths
2155
+ * Memory: numChannels * 4 bytes (4096 * 4 = 16KB)
2156
+ * Default: 4096 (SuperSonic default, SC uses 16384)
2157
+ */
2158
+ numControlBusChannels: 4096,
2159
+ /**
2160
+ * Audio buffer length in samples (AudioWorklet quantum)
2161
+ *
2162
+ * FIXED at 128 (WebAudio API spec - cannot be changed)
2163
+ * Unlike SuperCollider (configurable 32/64/128), AudioWorklet has a fixed quantum.
2164
+ * Overriding this value will cause initialization to fail.
2165
+ *
2166
+ * Default: 128
2167
+ */
2168
+ bufLength: 128,
2169
+ /**
2170
+ * Real-time memory pool size in kilobytes
2171
+ * AllocPool for synthesis-time allocations (UGen memory, etc.)
2172
+ * This is the largest single allocation from WASM heap
2173
+ * Memory: realTimeMemorySize * 1024 bytes (8192 * 1024 = 8MB)
2174
+ * Default: 8192 KB (8MB, matching Sonic Pi and SuperCollider defaults)
2175
+ */
2176
+ realTimeMemorySize: 8192,
2177
+ /**
2178
+ * Number of random number generators
2179
+ * Each synth can have its own RNG for reproducible randomness
2180
+ * Default: 64 (matching SuperCollider default)
2181
+ */
2182
+ numRGens: 64,
2183
+ /**
2184
+ * Clock source mode
2185
+ * false = Externally clocked (driven by AudioWorklet process() callback)
2186
+ * true = Internally clocked (not applicable in WebAudio context)
2187
+ * Note: In SC terminology, this is "NRT mode" but we're still doing real-time audio
2188
+ * Default: false (SuperSonic is always externally clocked by AudioWorklet)
2189
+ */
2190
+ realTime: false,
2191
+ /**
2192
+ * Memory locking (mlock)
2193
+ * Not applicable in WebAssembly/browser environment
2194
+ * Default: false
2195
+ */
2196
+ memoryLocking: false,
2197
+ /**
2198
+ * Auto-load SynthDefs from disk
2199
+ * 0 = don't auto-load (synths sent via /d_recv)
2200
+ * 1 = auto-load from plugin path
2201
+ * Default: 0 (SuperSonic loads synthdefs via network)
2202
+ */
2203
+ loadGraphDefs: 0,
2204
+ /**
2205
+ * Preferred sample rate (if not specified, uses AudioContext.sampleRate)
2206
+ * Common values: 44100, 48000, 96000
2207
+ * Default: 0 (use AudioContext default, typically 48000)
2208
+ */
2209
+ preferredSampleRate: 0,
2210
+ /**
2211
+ * Debug verbosity level
2212
+ * 0 = quiet, 1 = errors, 2 = warnings, 3 = info, 4 = debug
2213
+ * Default: 0
2214
+ */
2215
+ verbosity: 0
2216
+ };
2217
+
2218
+ // js/supersonic.js
1886
2219
  var SuperSonic = class _SuperSonic {
1887
2220
  // Expose OSC utilities as static methods
1888
2221
  static osc = {
1889
2222
  encode: (message) => osc_default.writePacket(message),
1890
2223
  decode: (data, options = { metadata: false }) => osc_default.readPacket(data, options)
1891
2224
  };
2225
+ // Private implementation
2226
+ #audioContext;
2227
+ #workletNode;
2228
+ #osc;
2229
+ #wasmMemory;
2230
+ #sharedBuffer;
2231
+ #ringBufferBase;
2232
+ #bufferConstants;
2233
+ #bufferManager;
2234
+ #driftOffsetTimer;
2235
+ #syncListeners;
2236
+ #initialNTPStartTime;
2237
+ #sampleBaseURL;
2238
+ #synthdefBaseURL;
2239
+ #audioPathMap;
2240
+ #initialized;
2241
+ #initializing;
2242
+ #capabilities;
2243
+ // Runtime metrics (private counters)
2244
+ #metrics_messagesSent = 0;
2245
+ #metrics_messagesReceived = 0;
2246
+ #metrics_errors = 0;
2247
+ #metricsIntervalId = null;
2248
+ #metricsGatherInProgress = false;
1892
2249
  constructor(options = {}) {
1893
- this.initialized = false;
1894
- this.initializing = false;
1895
- this.capabilities = {};
1896
- this.sharedBuffer = null;
1897
- this.ringBufferBase = null;
1898
- this.bufferConstants = null;
1899
- this.audioContext = null;
1900
- this.workletNode = null;
1901
- this.osc = null;
1902
- this.wasmModule = null;
1903
- this.wasmInstance = null;
1904
- this.bufferPool = null;
1905
- this.bufferManager = null;
2250
+ this.#initialized = false;
2251
+ this.#initializing = false;
2252
+ this.#capabilities = {};
2253
+ this.#sharedBuffer = null;
2254
+ this.#ringBufferBase = null;
2255
+ this.#bufferConstants = null;
2256
+ this.#audioContext = null;
2257
+ this.#workletNode = null;
2258
+ this.#osc = null;
2259
+ this.#bufferManager = null;
1906
2260
  this.loadedSynthDefs = /* @__PURE__ */ new Set();
1907
- this.pendingBufferOps = /* @__PURE__ */ new Map();
1908
- this._timeOffsetPromise = null;
1909
- this._resolveTimeOffset = null;
1910
- this._localClockOffsetTimer = null;
1911
2261
  this.onOSC = null;
1912
2262
  this.onMessage = null;
1913
2263
  this.onMessageSent = null;
1914
2264
  this.onMetricsUpdate = null;
1915
- this.onStatusUpdate = null;
1916
- this.onSendError = null;
1917
2265
  this.onDebugMessage = null;
1918
2266
  this.onInitialized = null;
1919
2267
  this.onError = null;
@@ -1922,38 +2270,58 @@ var SuperSonic = class _SuperSonic {
1922
2270
  }
1923
2271
  const workerBaseURL = options.workerBaseURL;
1924
2272
  const wasmBaseURL = options.wasmBaseURL;
2273
+ const worldOptions = { ...defaultWorldOptions, ...options.scsynthOptions };
1925
2274
  this.config = {
1926
2275
  wasmUrl: options.wasmUrl || wasmBaseURL + "scsynth-nrt.wasm",
2276
+ wasmBaseURL,
1927
2277
  workletUrl: options.workletUrl || workerBaseURL + "scsynth_audio_worklet.js",
1928
2278
  workerBaseURL,
1929
- // Store for worker creation
1930
2279
  development: false,
1931
2280
  audioContextOptions: {
1932
2281
  latencyHint: "interactive",
2282
+ // hint to push for lowest latency possible
1933
2283
  sampleRate: 48e3
1934
- }
2284
+ // only requested rate - actual rate is determined by hardware
2285
+ },
2286
+ // Build-time memory layout (constant)
2287
+ memory: MemoryLayout,
2288
+ // Runtime world options (merged defaults + user overrides)
2289
+ worldOptions
1935
2290
  };
1936
- this.sampleBaseURL = options.sampleBaseURL || null;
1937
- this.synthdefBaseURL = options.synthdefBaseURL || null;
1938
- this.audioPathMap = options.audioPathMap || {};
1939
- this.allocatedBuffers = /* @__PURE__ */ new Map();
1940
- this.stats = {
2291
+ this.#sampleBaseURL = options.sampleBaseURL || null;
2292
+ this.#synthdefBaseURL = options.synthdefBaseURL || null;
2293
+ this.#audioPathMap = options.audioPathMap || {};
2294
+ this.bootStats = {
1941
2295
  initStartTime: null,
1942
- initDuration: null,
1943
- messagesSent: 0,
1944
- messagesReceived: 0,
1945
- errors: 0
2296
+ initDuration: null
1946
2297
  };
1947
2298
  }
1948
2299
  /**
1949
- * Check browser capabilities for required features
2300
+ * Get initialization status (read-only)
2301
+ */
2302
+ get initialized() {
2303
+ return this.#initialized;
2304
+ }
2305
+ /**
2306
+ * Get initialization in-progress status (read-only)
2307
+ */
2308
+ get initializing() {
2309
+ return this.#initializing;
2310
+ }
2311
+ /**
2312
+ * Get browser capabilities (read-only)
1950
2313
  */
1951
- checkCapabilities() {
1952
- this.capabilities = {
1953
- audioWorklet: "AudioWorklet" in window,
2314
+ get capabilities() {
2315
+ return this.#capabilities;
2316
+ }
2317
+ /**
2318
+ * Set and validate browser capabilities for required features
2319
+ */
2320
+ setAndValidateCapabilities() {
2321
+ this.#capabilities = {
2322
+ audioWorklet: typeof AudioWorklet !== "undefined",
1954
2323
  sharedArrayBuffer: typeof SharedArrayBuffer !== "undefined",
1955
2324
  crossOriginIsolated: window.crossOriginIsolated === true,
1956
- wasmThreads: typeof WebAssembly !== "undefined" && typeof WebAssembly.Memory !== "undefined" && WebAssembly.Memory.prototype.hasOwnProperty("shared"),
1957
2325
  atomics: typeof Atomics !== "undefined",
1958
2326
  webWorker: typeof Worker !== "undefined"
1959
2327
  };
@@ -1964,11 +2332,11 @@ var SuperSonic = class _SuperSonic {
1964
2332
  "atomics",
1965
2333
  "webWorker"
1966
2334
  ];
1967
- const missing = required.filter((f) => !this.capabilities[f]);
2335
+ const missing = required.filter((f) => !this.#capabilities[f]);
1968
2336
  if (missing.length > 0) {
1969
2337
  const error = new Error(`Missing required features: ${missing.join(", ")}`);
1970
- if (!this.capabilities.crossOriginIsolated) {
1971
- if (this.capabilities.sharedArrayBuffer) {
2338
+ if (!this.#capabilities.crossOriginIsolated) {
2339
+ if (this.#capabilities.sharedArrayBuffer) {
1972
2340
  error.message += "\n\nSharedArrayBuffer is available but cross-origin isolation is not enabled. Please ensure COOP and COEP headers are set correctly:\n Cross-Origin-Opener-Policy: same-origin\n Cross-Origin-Embedder-Policy: require-corp";
1973
2341
  } else {
1974
2342
  error.message += "\n\nSharedArrayBuffer is not available. This may be due to:\n1. Missing COOP/COEP headers\n2. Browser doesn't support SharedArrayBuffer\n3. Browser security settings";
@@ -1976,78 +2344,54 @@ var SuperSonic = class _SuperSonic {
1976
2344
  }
1977
2345
  throw error;
1978
2346
  }
1979
- return this.capabilities;
2347
+ return this.#capabilities;
1980
2348
  }
2349
+ /**
2350
+ * Merge user-provided world options with defaults
2351
+ * @private
2352
+ */
1981
2353
  /**
1982
2354
  * Initialize shared WebAssembly memory
1983
2355
  */
1984
2356
  #initializeSharedMemory() {
1985
- const TOTAL_PAGES = 3072;
1986
- this.wasmMemory = new WebAssembly.Memory({
1987
- initial: TOTAL_PAGES,
1988
- maximum: TOTAL_PAGES,
2357
+ const memConfig = this.config.memory;
2358
+ this.#wasmMemory = new WebAssembly.Memory({
2359
+ initial: memConfig.totalPages,
2360
+ maximum: memConfig.totalPages,
1989
2361
  shared: true
1990
2362
  });
1991
- this.sharedBuffer = this.wasmMemory.buffer;
1992
- const BUFFER_POOL_OFFSET = 64 * 1024 * 1024;
1993
- const BUFFER_POOL_SIZE = 128 * 1024 * 1024;
1994
- this.bufferPool = new MemPool({
1995
- buf: this.sharedBuffer,
1996
- start: BUFFER_POOL_OFFSET,
1997
- size: BUFFER_POOL_SIZE,
1998
- align: 8
1999
- // 8-byte alignment (minimum required by MemPool)
2000
- });
2001
- console.log("[SuperSonic] Buffer pool initialized: 128MB at offset 64MB");
2363
+ this.#sharedBuffer = this.#wasmMemory.buffer;
2002
2364
  }
2003
- /**
2004
- * Initialize AudioContext and set up time offset calculation
2005
- */
2006
2365
  #initializeAudioContext() {
2007
- this.audioContext = new (window.AudioContext || window.webkitAudioContext)(
2008
- this.config.audioContextOptions
2009
- );
2010
- this._timeOffsetPromise = new Promise((resolve) => {
2011
- this._resolveTimeOffset = resolve;
2012
- });
2013
- if (this.audioContext.state === "suspended") {
2014
- const resumeContext = async () => {
2015
- if (this.audioContext.state === "suspended") {
2016
- await this.audioContext.resume();
2017
- }
2018
- };
2019
- document.addEventListener("click", resumeContext, { once: true });
2020
- document.addEventListener("touchstart", resumeContext, { once: true });
2021
- }
2022
- return this.audioContext;
2366
+ this.#audioContext = new AudioContext(this.config.audioContextOptions);
2367
+ return this.#audioContext;
2023
2368
  }
2024
2369
  #initializeBufferManager() {
2025
- this.bufferManager = new BufferManager({
2026
- audioContext: this.audioContext,
2027
- sharedBuffer: this.sharedBuffer,
2028
- bufferPool: this.bufferPool,
2029
- allocatedBuffers: this.allocatedBuffers,
2030
- resolveAudioPath: (path) => this._resolveAudioPath(path),
2031
- registerPendingOp: (uuid, bufnum, timeoutMs) => this.#createPendingBufferOperation(uuid, bufnum, timeoutMs)
2370
+ this.#bufferManager = new BufferManager({
2371
+ audioContext: this.#audioContext,
2372
+ sharedBuffer: this.#sharedBuffer,
2373
+ bufferPoolConfig: {
2374
+ start: this.config.memory.bufferPoolOffset,
2375
+ size: this.config.memory.bufferPoolSize
2376
+ },
2377
+ sampleBaseURL: this.#sampleBaseURL,
2378
+ audioPathMap: this.#audioPathMap,
2379
+ maxBuffers: this.config.worldOptions.numBuffers
2032
2380
  });
2033
2381
  }
2034
- /**
2035
- * Load WASM manifest to get the current hashed filename
2036
- */
2037
2382
  async #loadWasmManifest() {
2383
+ const manifestUrl = this.config.wasmBaseURL + "manifest.json";
2038
2384
  try {
2039
- const wasmBaseURL = this.config.workerBaseURL.replace("/workers/", "/wasm/");
2040
- const manifestUrl = wasmBaseURL + "manifest.json";
2041
2385
  const response = await fetch(manifestUrl);
2042
- if (response.ok) {
2043
- const manifest = await response.json();
2044
- const wasmFile = manifest.wasmFile;
2045
- this.config.wasmUrl = wasmBaseURL + wasmFile;
2046
- console.log(`[SuperSonic] Using WASM build: ${wasmFile}`);
2047
- console.log(`[SuperSonic] Build: ${manifest.buildId} (git: ${manifest.gitHash})`);
2386
+ if (!response.ok) {
2387
+ console.warn(`[SuperSonic] WASM manifest not found (${response.status}), using default`);
2388
+ return;
2048
2389
  }
2390
+ const manifest = await response.json();
2391
+ this.config.wasmUrl = this.config.wasmBaseURL + manifest.wasmFile;
2392
+ console.log(`[SuperSonic] WASM: ${manifest.wasmFile} (${manifest.buildId}, git: ${manifest.gitHash})`);
2049
2393
  } catch (error) {
2050
- console.warn("[SuperSonic] WASM manifest not found, using default filename");
2394
+ console.warn("[SuperSonic] Failed to load WASM manifest, using default");
2051
2395
  }
2052
2396
  }
2053
2397
  /**
@@ -2065,21 +2409,24 @@ var SuperSonic = class _SuperSonic {
2065
2409
  * Initialize AudioWorklet with WASM
2066
2410
  */
2067
2411
  async #initializeAudioWorklet(wasmBytes) {
2068
- await this.audioContext.audioWorklet.addModule(this.config.workletUrl);
2069
- this.workletNode = new AudioWorkletNode(this.audioContext, "scsynth-processor", {
2412
+ await this.#audioContext.audioWorklet.addModule(this.config.workletUrl);
2413
+ this.#workletNode = new AudioWorkletNode(this.#audioContext, "scsynth-processor", {
2070
2414
  numberOfInputs: 0,
2071
2415
  numberOfOutputs: 1,
2072
2416
  outputChannelCount: [2]
2073
2417
  });
2074
- this.workletNode.connect(this.audioContext.destination);
2075
- this.workletNode.port.postMessage({
2418
+ this.#workletNode.connect(this.#audioContext.destination);
2419
+ this.#workletNode.port.postMessage({
2076
2420
  type: "init",
2077
- sharedBuffer: this.sharedBuffer
2421
+ sharedBuffer: this.#sharedBuffer
2078
2422
  });
2079
- this.workletNode.port.postMessage({
2423
+ this.#workletNode.port.postMessage({
2080
2424
  type: "loadWasm",
2081
2425
  wasmBytes,
2082
- wasmMemory: this.wasmMemory
2426
+ wasmMemory: this.#wasmMemory,
2427
+ worldOptions: this.config.worldOptions,
2428
+ sampleRate: this.#audioContext.sampleRate
2429
+ // Pass actual AudioContext sample rate
2083
2430
  });
2084
2431
  await this.#waitForWorkletInit();
2085
2432
  }
@@ -2087,55 +2434,55 @@ var SuperSonic = class _SuperSonic {
2087
2434
  * Initialize OSC communication layer
2088
2435
  */
2089
2436
  async #initializeOSC() {
2090
- this.osc = new ScsynthOSC(this.config.workerBaseURL);
2091
- this.osc.onRawOSC((msg) => {
2437
+ this.#osc = new ScsynthOSC(this.config.workerBaseURL);
2438
+ this.#osc.onRawOSC((msg) => {
2092
2439
  if (this.onOSC) {
2093
2440
  this.onOSC(msg);
2094
2441
  }
2095
2442
  });
2096
- this.osc.onParsedOSC((msg) => {
2443
+ this.#osc.onParsedOSC((msg) => {
2097
2444
  if (msg.address === "/buffer/freed") {
2098
- this._handleBufferFreed(msg.args);
2445
+ this.#bufferManager?.handleBufferFreed(msg.args);
2099
2446
  } else if (msg.address === "/buffer/allocated") {
2100
- this._handleBufferAllocated(msg.args);
2447
+ this.#bufferManager?.handleBufferAllocated(msg.args);
2101
2448
  } else if (msg.address === "/synced" && msg.args.length > 0) {
2102
2449
  const syncId = msg.args[0];
2103
- if (this._syncListeners && this._syncListeners.has(syncId)) {
2104
- const listener = this._syncListeners.get(syncId);
2450
+ if (this.#syncListeners && this.#syncListeners.has(syncId)) {
2451
+ const listener = this.#syncListeners.get(syncId);
2105
2452
  listener(msg);
2106
2453
  }
2107
2454
  }
2108
2455
  if (this.onMessage) {
2109
- this.stats.messagesReceived++;
2456
+ this.#metrics_messagesReceived++;
2110
2457
  this.onMessage(msg);
2111
2458
  }
2112
2459
  });
2113
- this.osc.onDebugMessage((msg) => {
2460
+ this.#osc.onDebugMessage((msg) => {
2114
2461
  if (this.onDebugMessage) {
2115
2462
  this.onDebugMessage(msg);
2116
2463
  }
2117
2464
  });
2118
- this.osc.onError((error, workerName) => {
2465
+ this.#osc.onError((error, workerName) => {
2119
2466
  console.error(`[SuperSonic] ${workerName} error:`, error);
2120
- this.stats.errors++;
2467
+ this.#metrics_errors++;
2121
2468
  if (this.onError) {
2122
2469
  this.onError(new Error(`${workerName}: ${error}`));
2123
2470
  }
2124
2471
  });
2125
- await this.osc.init(this.sharedBuffer, this.ringBufferBase, this.bufferConstants);
2472
+ await this.#osc.init(this.#sharedBuffer, this.#ringBufferBase, this.#bufferConstants);
2126
2473
  }
2127
2474
  /**
2128
2475
  * Complete initialization and trigger callbacks
2129
2476
  */
2130
2477
  #finishInitialization() {
2131
- this.initialized = true;
2132
- this.initializing = false;
2133
- this.stats.initDuration = performance.now() - this.stats.initStartTime;
2134
- console.log(`[SuperSonic] Initialization complete in ${this.stats.initDuration.toFixed(2)}ms`);
2478
+ this.#initialized = true;
2479
+ this.#initializing = false;
2480
+ this.bootStats.initDuration = performance.now() - this.bootStats.initStartTime;
2481
+ console.log(`[SuperSonic] Initialization complete in ${this.bootStats.initDuration.toFixed(2)}ms`);
2135
2482
  if (this.onInitialized) {
2136
2483
  this.onInitialized({
2137
- capabilities: this.capabilities,
2138
- stats: this.stats
2484
+ capabilities: this.#capabilities,
2485
+ bootStats: this.bootStats
2139
2486
  });
2140
2487
  }
2141
2488
  }
@@ -2148,11 +2495,11 @@ var SuperSonic = class _SuperSonic {
2148
2495
  * @param {Object} config.audioContextOptions - AudioContext options
2149
2496
  */
2150
2497
  async init(config = {}) {
2151
- if (this.initialized) {
2498
+ if (this.#initialized) {
2152
2499
  console.warn("[SuperSonic] Already initialized");
2153
2500
  return;
2154
2501
  }
2155
- if (this.initializing) {
2502
+ if (this.#initializing) {
2156
2503
  console.warn("[SuperSonic] Initialization already in progress");
2157
2504
  return;
2158
2505
  }
@@ -2164,10 +2511,10 @@ var SuperSonic = class _SuperSonic {
2164
2511
  ...config.audioContextOptions || {}
2165
2512
  }
2166
2513
  };
2167
- this.initializing = true;
2168
- this.stats.initStartTime = performance.now();
2514
+ this.#initializing = true;
2515
+ this.bootStats.initStartTime = performance.now();
2169
2516
  try {
2170
- this.checkCapabilities();
2517
+ this.setAndValidateCapabilities();
2171
2518
  this.#initializeSharedMemory();
2172
2519
  this.#initializeAudioContext();
2173
2520
  this.#initializeBufferManager();
@@ -2178,7 +2525,7 @@ var SuperSonic = class _SuperSonic {
2178
2525
  this.#startPerformanceMonitoring();
2179
2526
  this.#finishInitialization();
2180
2527
  } catch (error) {
2181
- this.initializing = false;
2528
+ this.#initializing = false;
2182
2529
  console.error("[SuperSonic] Initialization failed:", error);
2183
2530
  if (this.onError) {
2184
2531
  this.onError(error);
@@ -2201,30 +2548,25 @@ var SuperSonic = class _SuperSonic {
2201
2548
  if (event.data.type === "error") {
2202
2549
  console.error("[AudioWorklet] Error:", event.data.error);
2203
2550
  clearTimeout(timeout);
2204
- this.workletNode.port.removeEventListener("message", messageHandler);
2551
+ this.#workletNode.port.removeEventListener("message", messageHandler);
2205
2552
  reject(new Error(event.data.error || "AudioWorklet error"));
2206
2553
  return;
2207
2554
  }
2208
2555
  if (event.data.type === "initialized") {
2209
2556
  clearTimeout(timeout);
2210
- this.workletNode.port.removeEventListener("message", messageHandler);
2557
+ this.#workletNode.port.removeEventListener("message", messageHandler);
2211
2558
  if (event.data.success) {
2212
2559
  if (event.data.ringBufferBase !== void 0) {
2213
- this.ringBufferBase = event.data.ringBufferBase;
2560
+ this.#ringBufferBase = event.data.ringBufferBase;
2214
2561
  } else {
2215
2562
  console.warn("[SuperSonic] Warning: ringBufferBase not provided by worklet");
2216
2563
  }
2217
2564
  if (event.data.bufferConstants !== void 0) {
2218
2565
  console.log("[SuperSonic] Received bufferConstants from worklet");
2219
- this.bufferConstants = event.data.bufferConstants;
2566
+ this.#bufferConstants = event.data.bufferConstants;
2220
2567
  console.log("[SuperSonic] Initializing NTP timing");
2221
2568
  this.initializeNTPTiming();
2222
2569
  this.#startDriftOffsetTimer();
2223
- console.log("[SuperSonic] Resolving time offset promise, _resolveTimeOffset=", this._resolveTimeOffset);
2224
- if (this._resolveTimeOffset) {
2225
- this._resolveTimeOffset();
2226
- this._resolveTimeOffset = null;
2227
- }
2228
2570
  } else {
2229
2571
  console.warn("[SuperSonic] Warning: bufferConstants not provided by worklet");
2230
2572
  }
@@ -2235,34 +2577,24 @@ var SuperSonic = class _SuperSonic {
2235
2577
  }
2236
2578
  }
2237
2579
  };
2238
- this.workletNode.port.addEventListener("message", messageHandler);
2239
- this.workletNode.port.start();
2580
+ this.#workletNode.port.addEventListener("message", messageHandler);
2581
+ this.#workletNode.port.start();
2240
2582
  });
2241
2583
  }
2242
2584
  /**
2243
2585
  * Set up message handlers for worklet
2244
2586
  */
2245
2587
  #setupMessageHandlers() {
2246
- this.workletNode.port.onmessage = (event) => {
2588
+ this.#workletNode.port.onmessage = (event) => {
2247
2589
  const { data } = event;
2248
2590
  switch (data.type) {
2249
- case "status":
2250
- if (this.onStatusUpdate) {
2251
- this.onStatusUpdate(data);
2252
- }
2253
- break;
2254
- case "metrics":
2255
- if (this.onMetricsUpdate) {
2256
- this.onMetricsUpdate(data.metrics);
2257
- }
2258
- break;
2259
2591
  case "error":
2260
2592
  console.error("[Worklet] Error:", data.error);
2261
2593
  if (data.diagnostics) {
2262
2594
  console.error("[Worklet] Diagnostics:", data.diagnostics);
2263
2595
  console.table(data.diagnostics);
2264
2596
  }
2265
- this.stats.errors++;
2597
+ this.#metrics_errors++;
2266
2598
  if (this.onError) {
2267
2599
  this.onError(new Error(data.error));
2268
2600
  }
@@ -2285,21 +2617,173 @@ var SuperSonic = class _SuperSonic {
2285
2617
  };
2286
2618
  }
2287
2619
  /**
2288
- * Start performance monitoring
2620
+ * Get metrics from SharedArrayBuffer (worklet metrics written by WASM)
2621
+ * @returns {Object|null}
2622
+ * @private
2623
+ */
2624
+ #getWorkletMetrics() {
2625
+ if (!this.#sharedBuffer || !this.#bufferConstants || !this.#ringBufferBase) {
2626
+ return null;
2627
+ }
2628
+ const metricsBase = this.#ringBufferBase + this.#bufferConstants.METRICS_START;
2629
+ const metricsCount = this.#bufferConstants.METRICS_SIZE / 4;
2630
+ const metricsView = new Uint32Array(this.#sharedBuffer, metricsBase, metricsCount);
2631
+ return {
2632
+ processCount: Atomics.load(metricsView, 0),
2633
+ // PROCESS_COUNT offset / 4
2634
+ bufferOverruns: Atomics.load(metricsView, 1),
2635
+ // BUFFER_OVERRUNS offset / 4
2636
+ messagesProcessed: Atomics.load(metricsView, 2),
2637
+ // MESSAGES_PROCESSED offset / 4
2638
+ messagesDropped: Atomics.load(metricsView, 3),
2639
+ // MESSAGES_DROPPED offset / 4
2640
+ schedulerQueueDepth: Atomics.load(metricsView, 4),
2641
+ // SCHEDULER_QUEUE_DEPTH offset / 4
2642
+ schedulerQueueMax: Atomics.load(metricsView, 5),
2643
+ // SCHEDULER_QUEUE_MAX offset / 4
2644
+ schedulerQueueDropped: Atomics.load(metricsView, 6)
2645
+ // SCHEDULER_QUEUE_DROPPED offset / 4
2646
+ };
2647
+ }
2648
+ /**
2649
+ * Get buffer usage statistics from SAB head/tail pointers
2650
+ * @returns {Object|null}
2651
+ * @private
2652
+ */
2653
+ #getBufferUsage() {
2654
+ if (!this.#sharedBuffer || !this.#bufferConstants || !this.#ringBufferBase) {
2655
+ return null;
2656
+ }
2657
+ const atomicView = new Int32Array(this.#sharedBuffer);
2658
+ const controlBase = this.#ringBufferBase + this.#bufferConstants.CONTROL_START;
2659
+ const inHead = Atomics.load(atomicView, (controlBase + 0) / 4);
2660
+ const inTail = Atomics.load(atomicView, (controlBase + 4) / 4);
2661
+ const outHead = Atomics.load(atomicView, (controlBase + 8) / 4);
2662
+ const outTail = Atomics.load(atomicView, (controlBase + 12) / 4);
2663
+ const debugHead = Atomics.load(atomicView, (controlBase + 16) / 4);
2664
+ const debugTail = Atomics.load(atomicView, (controlBase + 20) / 4);
2665
+ const inUsed = (inHead - inTail + this.#bufferConstants.IN_BUFFER_SIZE) % this.#bufferConstants.IN_BUFFER_SIZE;
2666
+ const outUsed = (outHead - outTail + this.#bufferConstants.OUT_BUFFER_SIZE) % this.#bufferConstants.OUT_BUFFER_SIZE;
2667
+ const debugUsed = (debugHead - debugTail + this.#bufferConstants.DEBUG_BUFFER_SIZE) % this.#bufferConstants.DEBUG_BUFFER_SIZE;
2668
+ return {
2669
+ inBufferUsed: {
2670
+ bytes: inUsed,
2671
+ percentage: Math.round(inUsed / this.#bufferConstants.IN_BUFFER_SIZE * 100)
2672
+ },
2673
+ outBufferUsed: {
2674
+ bytes: outUsed,
2675
+ percentage: Math.round(outUsed / this.#bufferConstants.OUT_BUFFER_SIZE * 100)
2676
+ },
2677
+ debugBufferUsed: {
2678
+ bytes: debugUsed,
2679
+ percentage: Math.round(debugUsed / this.#bufferConstants.DEBUG_BUFFER_SIZE * 100)
2680
+ }
2681
+ };
2682
+ }
2683
+ /**
2684
+ * Get OSC worker metrics from SharedArrayBuffer (written by OSC workers)
2685
+ * @returns {Object|null}
2686
+ * @private
2687
+ */
2688
+ #getOSCMetrics() {
2689
+ if (!this.#sharedBuffer || !this.#bufferConstants || !this.#ringBufferBase) {
2690
+ return null;
2691
+ }
2692
+ const metricsBase = this.#ringBufferBase + this.#bufferConstants.METRICS_START;
2693
+ const metricsCount = this.#bufferConstants.METRICS_SIZE / 4;
2694
+ const metricsView = new Uint32Array(this.#sharedBuffer, metricsBase, metricsCount);
2695
+ return {
2696
+ // OSC Out (prescheduler) - offsets 7-18
2697
+ preschedulerPending: metricsView[7],
2698
+ preschedulerPeak: metricsView[8],
2699
+ preschedulerSent: metricsView[9],
2700
+ bundlesDropped: metricsView[10],
2701
+ retriesSucceeded: metricsView[11],
2702
+ retriesFailed: metricsView[12],
2703
+ bundlesScheduled: metricsView[13],
2704
+ eventsCancelled: metricsView[14],
2705
+ totalDispatches: metricsView[15],
2706
+ messagesRetried: metricsView[16],
2707
+ retryQueueSize: metricsView[17],
2708
+ retryQueueMax: metricsView[18],
2709
+ // OSC In - offsets 19-22
2710
+ oscInMessagesReceived: metricsView[19],
2711
+ oscInDroppedMessages: metricsView[20],
2712
+ oscInWakeups: metricsView[21],
2713
+ oscInTimeouts: metricsView[22],
2714
+ // Debug - offsets 23-26
2715
+ debugMessagesReceived: metricsView[23],
2716
+ debugWakeups: metricsView[24],
2717
+ debugTimeouts: metricsView[25],
2718
+ debugBytesRead: metricsView[26]
2719
+ };
2720
+ }
2721
+ /**
2722
+ * Gather metrics from all sources (worklet, OSC, internal counters)
2723
+ * All metrics are read synchronously from SAB
2724
+ * @returns {SuperSonicMetrics}
2725
+ * @private
2726
+ */
2727
+ #gatherMetrics() {
2728
+ const startTime = performance.now();
2729
+ const metrics = {
2730
+ // SuperSonic counters (in-memory, fast)
2731
+ messagesSent: this.#metrics_messagesSent,
2732
+ messagesReceived: this.#metrics_messagesReceived,
2733
+ errors: this.#metrics_errors
2734
+ };
2735
+ const workletMetrics = this.#getWorkletMetrics();
2736
+ if (workletMetrics) {
2737
+ Object.assign(metrics, workletMetrics);
2738
+ }
2739
+ const bufferUsage = this.#getBufferUsage();
2740
+ if (bufferUsage) {
2741
+ Object.assign(metrics, bufferUsage);
2742
+ }
2743
+ const oscMetrics = this.#getOSCMetrics();
2744
+ if (oscMetrics) {
2745
+ Object.assign(metrics, oscMetrics);
2746
+ }
2747
+ const totalDuration = performance.now() - startTime;
2748
+ if (totalDuration > 1) {
2749
+ console.warn(`[SuperSonic] Slow metrics gathering: ${totalDuration.toFixed(2)}ms`);
2750
+ }
2751
+ return metrics;
2752
+ }
2753
+ /**
2754
+ * Start performance monitoring - gathers metrics from all sources
2755
+ * and calls onMetricsUpdate with consolidated snapshot
2289
2756
  */
2290
2757
  #startPerformanceMonitoring() {
2291
- setInterval(() => {
2292
- if (this.osc) {
2293
- this.osc.getStats().then((stats) => {
2294
- if (stats && this.onMetricsUpdate) {
2295
- this.onMetricsUpdate(stats);
2296
- }
2297
- });
2758
+ if (this.#metricsIntervalId) {
2759
+ clearInterval(this.#metricsIntervalId);
2760
+ }
2761
+ this.#metricsIntervalId = setInterval(() => {
2762
+ if (!this.onMetricsUpdate) return;
2763
+ if (this.#metricsGatherInProgress) {
2764
+ console.warn("[SuperSonic] Metrics gathering took >100ms, skipping this interval");
2765
+ return;
2298
2766
  }
2299
- if (this.workletNode) {
2300
- this.workletNode.port.postMessage({ type: "getMetrics" });
2767
+ this.#metricsGatherInProgress = true;
2768
+ try {
2769
+ const metrics = this.#gatherMetrics();
2770
+ this.onMetricsUpdate(metrics);
2771
+ } catch (error) {
2772
+ console.error("[SuperSonic] Metrics gathering failed:", error);
2773
+ } finally {
2774
+ this.#metricsGatherInProgress = false;
2301
2775
  }
2302
- }, 50);
2776
+ }, 100);
2777
+ }
2778
+ /**
2779
+ * Stop performance monitoring
2780
+ * @private
2781
+ */
2782
+ #stopPerformanceMonitoring() {
2783
+ if (this.#metricsIntervalId) {
2784
+ clearInterval(this.#metricsIntervalId);
2785
+ this.#metricsIntervalId = null;
2786
+ }
2303
2787
  }
2304
2788
  /**
2305
2789
  * Send OSC message with simplified syntax (auto-detects types)
@@ -2327,73 +2811,11 @@ var SuperSonic = class _SuperSonic {
2327
2811
  const oscData = _SuperSonic.osc.encode(message);
2328
2812
  return this.sendOSC(oscData);
2329
2813
  }
2330
- /**
2331
- * Resolve audio file path to full URL
2332
- */
2333
- _resolveAudioPath(scPath) {
2334
- if (this.audioPathMap[scPath]) {
2335
- return this.audioPathMap[scPath];
2336
- }
2337
- if (!this.sampleBaseURL) {
2338
- throw new Error(
2339
- 'sampleBaseURL not configured. Please set it in SuperSonic constructor options.\nExample: new SuperSonic({ sampleBaseURL: "./dist/samples/" })\nOr use CDN: new SuperSonic({ sampleBaseURL: "https://unpkg.com/supersonic-scsynth-samples@latest/samples/" })\nOr install: npm install supersonic-scsynth-samples'
2340
- );
2341
- }
2342
- return this.sampleBaseURL + scPath;
2343
- }
2344
2814
  #ensureInitialized(actionDescription = "perform this operation") {
2345
- if (!this.initialized) {
2815
+ if (!this.#initialized) {
2346
2816
  throw new Error(`SuperSonic not initialized. Call init() before attempting to ${actionDescription}.`);
2347
2817
  }
2348
2818
  }
2349
- #createPendingBufferOperation(uuid, bufnum, timeoutMs = 3e4) {
2350
- return new Promise((resolve, reject) => {
2351
- const timeout = setTimeout(() => {
2352
- this.pendingBufferOps.delete(uuid);
2353
- reject(new Error(`Buffer ${bufnum} allocation timeout (${timeoutMs}ms)`));
2354
- }, timeoutMs);
2355
- this.pendingBufferOps.set(uuid, { resolve, reject, timeout });
2356
- });
2357
- }
2358
- /**
2359
- * Handle /buffer/freed message from WASM
2360
- */
2361
- _handleBufferFreed(args) {
2362
- const bufnum = args[0];
2363
- const freedPtr = args[1];
2364
- const bufferInfo = this.allocatedBuffers.get(bufnum);
2365
- if (!bufferInfo) {
2366
- if (typeof freedPtr === "number" && freedPtr !== 0) {
2367
- this.bufferPool.free(freedPtr);
2368
- }
2369
- return;
2370
- }
2371
- if (typeof freedPtr === "number" && freedPtr === bufferInfo.ptr) {
2372
- this.bufferPool.free(bufferInfo.ptr);
2373
- this.allocatedBuffers.delete(bufnum);
2374
- return;
2375
- }
2376
- if (typeof freedPtr === "number" && bufferInfo.previousAllocation && bufferInfo.previousAllocation.ptr === freedPtr) {
2377
- this.bufferPool.free(freedPtr);
2378
- bufferInfo.previousAllocation = null;
2379
- return;
2380
- }
2381
- this.bufferPool.free(bufferInfo.ptr);
2382
- this.allocatedBuffers.delete(bufnum);
2383
- }
2384
- /**
2385
- * Handle /buffer/allocated message with UUID correlation
2386
- */
2387
- _handleBufferAllocated(args) {
2388
- const uuid = args[0];
2389
- const bufnum = args[1];
2390
- const pending = this.pendingBufferOps.get(uuid);
2391
- if (pending) {
2392
- clearTimeout(pending.timeout);
2393
- pending.resolve({ bufnum });
2394
- this.pendingBufferOps.delete(uuid);
2395
- }
2396
- }
2397
2819
  /**
2398
2820
  * Send pre-encoded OSC bytes to scsynth
2399
2821
  * @param {ArrayBuffer|Uint8Array} oscData - Pre-encoded OSC data
@@ -2403,7 +2825,7 @@ var SuperSonic = class _SuperSonic {
2403
2825
  this.#ensureInitialized("send OSC data");
2404
2826
  const uint8Data = this.#toUint8Array(oscData);
2405
2827
  const preparedData = await this.#prepareOutboundPacket(uint8Data);
2406
- this.stats.messagesSent++;
2828
+ this.#metrics_messagesSent++;
2407
2829
  if (this.onMessageSent) {
2408
2830
  this.onMessageSent(preparedData);
2409
2831
  }
@@ -2413,17 +2835,56 @@ var SuperSonic = class _SuperSonic {
2413
2835
  sendOptions.audioTimeS = timing.audioTimeS;
2414
2836
  sendOptions.currentTimeS = timing.currentTimeS;
2415
2837
  }
2416
- this.osc.send(preparedData, sendOptions);
2838
+ this.#osc.send(preparedData, sendOptions);
2839
+ }
2840
+ /**
2841
+ * Get AudioContext instance (read-only)
2842
+ * @returns {AudioContext} The AudioContext instance
2843
+ */
2844
+ get audioContext() {
2845
+ return this.#audioContext;
2846
+ }
2847
+ /**
2848
+ * Get AudioWorkletNode instance (read-only)
2849
+ * @returns {AudioWorkletNode} The AudioWorkletNode instance
2850
+ */
2851
+ get workletNode() {
2852
+ return this.#workletNode;
2853
+ }
2854
+ /**
2855
+ * Get ScsynthOSC instance (read-only)
2856
+ * @returns {ScsynthOSC} The OSC communication layer instance
2857
+ */
2858
+ get osc() {
2859
+ return this.#osc;
2417
2860
  }
2418
2861
  /**
2419
2862
  * Get current status
2420
2863
  */
2421
2864
  getStatus() {
2422
2865
  return {
2423
- initialized: this.initialized,
2424
- capabilities: this.capabilities,
2425
- stats: this.stats,
2426
- audioContextState: this.audioContext?.state
2866
+ initialized: this.#initialized,
2867
+ capabilities: this.#capabilities,
2868
+ bootStats: this.bootStats,
2869
+ audioContextState: this.#audioContext?.state
2870
+ };
2871
+ }
2872
+ /**
2873
+ * Get current configuration (merged defaults + user overrides)
2874
+ * Useful for debugging and displaying in UI
2875
+ * @returns {Object} Current scsynth configuration
2876
+ * @example
2877
+ * const config = sonic.getConfig();
2878
+ * console.log('Buffer limit:', config.worldOptions.numBuffers);
2879
+ * console.log('Memory layout:', config.memory);
2880
+ */
2881
+ getConfig() {
2882
+ if (!this.config) {
2883
+ return null;
2884
+ }
2885
+ return {
2886
+ memory: { ...this.config.memory },
2887
+ worldOptions: { ...this.config.worldOptions }
2427
2888
  };
2428
2889
  }
2429
2890
  /**
@@ -2432,42 +2893,36 @@ var SuperSonic = class _SuperSonic {
2432
2893
  async destroy() {
2433
2894
  console.log("[SuperSonic] Destroying...");
2434
2895
  this.#stopDriftOffsetTimer();
2435
- if (this.osc) {
2436
- this.osc.terminate();
2437
- this.osc = null;
2896
+ this.#stopPerformanceMonitoring();
2897
+ if (this.#osc) {
2898
+ this.#osc.terminate();
2899
+ this.#osc = null;
2438
2900
  }
2439
- if (this.workletNode) {
2440
- this.workletNode.disconnect();
2441
- this.workletNode = null;
2901
+ if (this.#workletNode) {
2902
+ this.#workletNode.disconnect();
2903
+ this.#workletNode = null;
2442
2904
  }
2443
- if (this.audioContext) {
2444
- await this.audioContext.close();
2445
- this.audioContext = null;
2905
+ if (this.#audioContext) {
2906
+ await this.#audioContext.close();
2907
+ this.#audioContext = null;
2446
2908
  }
2447
- for (const [uuid, pending] of this.pendingBufferOps.entries()) {
2448
- clearTimeout(pending.timeout);
2449
- pending.reject(new Error("SuperSonic instance destroyed"));
2909
+ if (this.#bufferManager) {
2910
+ this.#bufferManager.destroy();
2911
+ this.#bufferManager = null;
2450
2912
  }
2451
- this.pendingBufferOps.clear();
2452
- this.sharedBuffer = null;
2453
- this.initialized = false;
2454
- this.bufferManager = null;
2455
- this.allocatedBuffers.clear();
2913
+ this.#sharedBuffer = null;
2914
+ this.#initialized = false;
2456
2915
  this.loadedSynthDefs.clear();
2457
2916
  console.log("[SuperSonic] Destroyed");
2458
2917
  }
2459
2918
  /**
2460
- * Wait until NTP timing has been established.
2461
- * Note: NTP calculation is now done internally in C++ process_audio().
2462
- * Returns 0 for backward compatibility.
2919
+ * Get NTP start time for bundle creation.
2920
+ * This is the NTP timestamp when AudioContext.currentTime was 0.
2921
+ * Bundles should have timestamp = audioContextTime + ntpStartTime
2463
2922
  */
2464
- async waitForTimeSync() {
2465
- if (!this.bufferConstants) {
2466
- if (this._timeOffsetPromise) {
2467
- await this._timeOffsetPromise;
2468
- }
2469
- }
2470
- const ntpStartView = new Float64Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.NTP_START_TIME_START, 1);
2923
+ waitForTimeSync() {
2924
+ this.#ensureInitialized("wait for time sync");
2925
+ const ntpStartView = new Float64Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.NTP_START_TIME_START, 1);
2471
2926
  return ntpStartView[0];
2472
2927
  }
2473
2928
  /**
@@ -2503,7 +2958,7 @@ var SuperSonic = class _SuperSonic {
2503
2958
  * await sonic.loadSynthDef('./extra/synthdefs/sonic-pi-beep.scsyndef');
2504
2959
  */
2505
2960
  async loadSynthDef(path) {
2506
- if (!this.initialized) {
2961
+ if (!this.#initialized) {
2507
2962
  throw new Error("SuperSonic not initialized. Call init() first.");
2508
2963
  }
2509
2964
  try {
@@ -2532,10 +2987,10 @@ var SuperSonic = class _SuperSonic {
2532
2987
  * const results = await sonic.loadSynthDefs(['sonic-pi-beep', 'sonic-pi-tb303']);
2533
2988
  */
2534
2989
  async loadSynthDefs(names) {
2535
- if (!this.initialized) {
2990
+ if (!this.#initialized) {
2536
2991
  throw new Error("SuperSonic not initialized. Call init() first.");
2537
2992
  }
2538
- if (!this.synthdefBaseURL) {
2993
+ if (!this.#synthdefBaseURL) {
2539
2994
  throw new Error(
2540
2995
  'synthdefBaseURL not configured. Please set it in SuperSonic constructor options.\nExample: new SuperSonic({ synthdefBaseURL: "./dist/synthdefs/" })\nOr use CDN: new SuperSonic({ synthdefBaseURL: "https://unpkg.com/supersonic-scsynth-synthdefs@latest/synthdefs/" })\nOr install: npm install supersonic-scsynth-synthdefs'
2541
2996
  );
@@ -2544,7 +2999,7 @@ var SuperSonic = class _SuperSonic {
2544
2999
  await Promise.all(
2545
3000
  names.map(async (name) => {
2546
3001
  try {
2547
- const path = `${this.synthdefBaseURL}${name}.scsyndef`;
3002
+ const path = `${this.#synthdefBaseURL}${name}.scsyndef`;
2548
3003
  await this.loadSynthDef(path);
2549
3004
  results[name] = { success: true };
2550
3005
  } catch (error) {
@@ -2567,7 +3022,7 @@ var SuperSonic = class _SuperSonic {
2567
3022
  * await sonic.sync(12345); // Wait for all synthdefs to be processed
2568
3023
  */
2569
3024
  async sync(syncId) {
2570
- if (!this.initialized) {
3025
+ if (!this.#initialized) {
2571
3026
  throw new Error("SuperSonic not initialized. Call init() first.");
2572
3027
  }
2573
3028
  if (!Number.isInteger(syncId)) {
@@ -2575,20 +3030,20 @@ var SuperSonic = class _SuperSonic {
2575
3030
  }
2576
3031
  const syncPromise = new Promise((resolve, reject) => {
2577
3032
  const timeout = setTimeout(() => {
2578
- if (this._syncListeners) {
2579
- this._syncListeners.delete(syncId);
3033
+ if (this.#syncListeners) {
3034
+ this.#syncListeners.delete(syncId);
2580
3035
  }
2581
3036
  reject(new Error("Timeout waiting for /synced response"));
2582
3037
  }, 1e4);
2583
3038
  const messageHandler = (msg) => {
2584
3039
  clearTimeout(timeout);
2585
- this._syncListeners.delete(syncId);
3040
+ this.#syncListeners.delete(syncId);
2586
3041
  resolve();
2587
3042
  };
2588
- if (!this._syncListeners) {
2589
- this._syncListeners = /* @__PURE__ */ new Map();
3043
+ if (!this.#syncListeners) {
3044
+ this.#syncListeners = /* @__PURE__ */ new Map();
2590
3045
  }
2591
- this._syncListeners.set(syncId, messageHandler);
3046
+ this.#syncListeners.set(syncId, messageHandler);
2592
3047
  });
2593
3048
  await this.send("/sync", syncId);
2594
3049
  await syncPromise;
@@ -2601,15 +3056,8 @@ var SuperSonic = class _SuperSonic {
2601
3056
  * const bufferAddr = sonic.allocBuffer(44100); // Allocate 1 second at 44.1kHz
2602
3057
  */
2603
3058
  allocBuffer(numSamples) {
2604
- if (!this.initialized) {
2605
- throw new Error("SuperSonic not initialized. Call init() first.");
2606
- }
2607
- const sizeBytes = numSamples * 4;
2608
- const addr = this.bufferPool.malloc(sizeBytes);
2609
- if (addr === 0) {
2610
- console.error(`[SuperSonic] Buffer allocation failed: ${numSamples} samples (${sizeBytes} bytes)`);
2611
- }
2612
- return addr;
3059
+ this.#ensureInitialized("allocate buffers");
3060
+ return this.#bufferManager.allocate(numSamples);
2613
3061
  }
2614
3062
  /**
2615
3063
  * Free a previously allocated buffer
@@ -2619,10 +3067,8 @@ var SuperSonic = class _SuperSonic {
2619
3067
  * sonic.freeBuffer(bufferAddr);
2620
3068
  */
2621
3069
  freeBuffer(addr) {
2622
- if (!this.initialized) {
2623
- throw new Error("SuperSonic not initialized. Call init() first.");
2624
- }
2625
- return this.bufferPool.free(addr);
3070
+ this.#ensureInitialized("free buffers");
3071
+ return this.#bufferManager.free(addr);
2626
3072
  }
2627
3073
  /**
2628
3074
  * Get a Float32Array view of an allocated buffer
@@ -2634,10 +3080,8 @@ var SuperSonic = class _SuperSonic {
2634
3080
  * view[0] = 1.0; // Write to buffer
2635
3081
  */
2636
3082
  getBufferView(addr, numSamples) {
2637
- if (!this.initialized) {
2638
- throw new Error("SuperSonic not initialized. Call init() first.");
2639
- }
2640
- return new Float32Array(this.sharedBuffer, addr, numSamples);
3083
+ this.#ensureInitialized("get buffer views");
3084
+ return this.#bufferManager.getView(addr, numSamples);
2641
3085
  }
2642
3086
  /**
2643
3087
  * Get buffer pool statistics
@@ -2647,37 +3091,13 @@ var SuperSonic = class _SuperSonic {
2647
3091
  * console.log(`Available: ${stats.available} bytes`);
2648
3092
  */
2649
3093
  getBufferPoolStats() {
2650
- if (!this.initialized) {
2651
- throw new Error("SuperSonic not initialized. Call init() first.");
2652
- }
2653
- return this.bufferPool.stats();
3094
+ this.#ensureInitialized("get buffer pool stats");
3095
+ return this.#bufferManager.getStats();
2654
3096
  }
2655
3097
  getDiagnostics() {
2656
3098
  this.#ensureInitialized("get diagnostics");
2657
- const poolStats = this.bufferPool?.stats ? this.bufferPool.stats() : null;
2658
- let bytesActive = 0;
2659
- let pendingCount = 0;
2660
- for (const entry of this.allocatedBuffers.values()) {
2661
- if (!entry) continue;
2662
- bytesActive += entry.size || 0;
2663
- if (entry.pendingToken) {
2664
- pendingCount++;
2665
- }
2666
- }
2667
3099
  return {
2668
- buffers: {
2669
- active: this.allocatedBuffers.size,
2670
- pending: pendingCount,
2671
- bytesActive,
2672
- pool: poolStats ? {
2673
- total: poolStats.total || 0,
2674
- available: poolStats.available || 0,
2675
- freeBytes: poolStats.free?.size || 0,
2676
- freeBlocks: poolStats.free?.count || 0,
2677
- usedBytes: poolStats.used?.size || 0,
2678
- usedBlocks: poolStats.used?.count || 0
2679
- } : null
2680
- },
3100
+ buffers: this.#bufferManager.getDiagnostics(),
2681
3101
  synthdefs: {
2682
3102
  count: this.loadedSynthDefs.size
2683
3103
  }
@@ -2689,21 +3109,21 @@ var SuperSonic = class _SuperSonic {
2689
3109
  * @private
2690
3110
  */
2691
3111
  initializeNTPTiming() {
2692
- if (!this.bufferConstants || !this.audioContext) {
3112
+ if (!this.#bufferConstants || !this.#audioContext) {
2693
3113
  return;
2694
3114
  }
2695
- const perfTimeMs = performance.timeOrigin + performance.now();
3115
+ const timestamp = this.#audioContext.getOutputTimestamp();
3116
+ const perfTimeMs = performance.timeOrigin + timestamp.performanceTime;
2696
3117
  const currentNTP = perfTimeMs / 1e3 + NTP_EPOCH_OFFSET;
2697
- const currentAudioCtx = this.audioContext.currentTime;
2698
- const ntpStartTime = currentNTP - currentAudioCtx;
3118
+ const ntpStartTime = currentNTP - timestamp.contextTime;
2699
3119
  const ntpStartView = new Float64Array(
2700
- this.sharedBuffer,
2701
- this.ringBufferBase + this.bufferConstants.NTP_START_TIME_START,
3120
+ this.#sharedBuffer,
3121
+ this.#ringBufferBase + this.#bufferConstants.NTP_START_TIME_START,
2702
3122
  1
2703
3123
  );
2704
3124
  ntpStartView[0] = ntpStartTime;
2705
- this._initialNTPStartTime = ntpStartTime;
2706
- console.log(`[SuperSonic] NTP timing initialized: start=${ntpStartTime.toFixed(6)}s (current NTP=${currentNTP.toFixed(3)}, AudioCtx=${currentAudioCtx.toFixed(3)}), ringBufferBase=${this.ringBufferBase}`);
3125
+ this.#initialNTPStartTime = ntpStartTime;
3126
+ console.log(`[SuperSonic] NTP timing initialized: start=${ntpStartTime.toFixed(6)}s (NTP=${currentNTP.toFixed(3)}s, contextTime=${timestamp.contextTime.toFixed(3)}s)`);
2707
3127
  }
2708
3128
  /**
2709
3129
  * Update drift offset (AudioContext → NTP drift correction)
@@ -2711,34 +3131,34 @@ var SuperSonic = class _SuperSonic {
2711
3131
  * @private
2712
3132
  */
2713
3133
  updateDriftOffset() {
2714
- if (!this.bufferConstants || !this.audioContext || this._initialNTPStartTime === void 0) {
3134
+ if (!this.#bufferConstants || !this.#audioContext || this.#initialNTPStartTime === void 0) {
2715
3135
  return;
2716
3136
  }
2717
- const perfTimeMs = performance.timeOrigin + performance.now();
3137
+ const timestamp = this.#audioContext.getOutputTimestamp();
3138
+ const perfTimeMs = performance.timeOrigin + timestamp.performanceTime;
2718
3139
  const currentNTP = perfTimeMs / 1e3 + NTP_EPOCH_OFFSET;
2719
- const currentAudioCtx = this.audioContext.currentTime;
2720
- const currentNTPStartTime = currentNTP - currentAudioCtx;
2721
- const driftSeconds = currentNTPStartTime - this._initialNTPStartTime;
3140
+ const expectedContextTime = currentNTP - this.#initialNTPStartTime;
3141
+ const driftSeconds = expectedContextTime - timestamp.contextTime;
2722
3142
  const driftMs = Math.round(driftSeconds * 1e3);
2723
3143
  const driftView = new Int32Array(
2724
- this.sharedBuffer,
2725
- this.ringBufferBase + this.bufferConstants.DRIFT_OFFSET_START,
3144
+ this.#sharedBuffer,
3145
+ this.#ringBufferBase + this.#bufferConstants.DRIFT_OFFSET_START,
2726
3146
  1
2727
3147
  );
2728
3148
  Atomics.store(driftView, 0, driftMs);
2729
- console.log(`[SuperSonic] Drift offset updated: ${driftMs}ms (current NTP start=${currentNTPStartTime.toFixed(6)}, initial=${this._initialNTPStartTime.toFixed(6)})`);
3149
+ console.log(`[SuperSonic] Drift offset: ${driftMs}ms (expected=${expectedContextTime.toFixed(3)}s, actual=${timestamp.contextTime.toFixed(3)}s, NTP=${currentNTP.toFixed(3)}s)`);
2730
3150
  }
2731
3151
  /**
2732
3152
  * Get current drift offset in milliseconds
2733
3153
  * @returns {number} Current drift in milliseconds
2734
3154
  */
2735
3155
  getDriftOffset() {
2736
- if (!this.bufferConstants) {
3156
+ if (!this.#bufferConstants) {
2737
3157
  return 0;
2738
3158
  }
2739
3159
  const driftView = new Int32Array(
2740
- this.sharedBuffer,
2741
- this.ringBufferBase + this.bufferConstants.DRIFT_OFFSET_START,
3160
+ this.#sharedBuffer,
3161
+ this.#ringBufferBase + this.#bufferConstants.DRIFT_OFFSET_START,
2742
3162
  1
2743
3163
  );
2744
3164
  return Atomics.load(driftView, 0);
@@ -2749,7 +3169,7 @@ var SuperSonic = class _SuperSonic {
2749
3169
  */
2750
3170
  #startDriftOffsetTimer() {
2751
3171
  this.#stopDriftOffsetTimer();
2752
- this._driftOffsetTimer = setInterval(() => {
3172
+ this.#driftOffsetTimer = setInterval(() => {
2753
3173
  this.updateDriftOffset();
2754
3174
  }, DRIFT_UPDATE_INTERVAL_MS);
2755
3175
  console.log(`[SuperSonic] Started drift offset correction (every ${DRIFT_UPDATE_INTERVAL_MS}ms)`);
@@ -2759,9 +3179,9 @@ var SuperSonic = class _SuperSonic {
2759
3179
  * @private
2760
3180
  */
2761
3181
  #stopDriftOffsetTimer() {
2762
- if (this._driftOffsetTimer) {
2763
- clearInterval(this._driftOffsetTimer);
2764
- this._driftOffsetTimer = null;
3182
+ if (this.#driftOffsetTimer) {
3183
+ clearInterval(this.#driftOffsetTimer);
3184
+ this.#driftOffsetTimer = null;
2765
3185
  }
2766
3186
  }
2767
3187
  #extractSynthDefName(path) {
@@ -2883,7 +3303,7 @@ var SuperSonic = class _SuperSonic {
2883
3303
  const numFrames = this.#requireIntArg(message.args, 1, "/b_alloc requires a frame count");
2884
3304
  let argIndex = 2;
2885
3305
  let numChannels = 1;
2886
- let sampleRate = this.audioContext?.sampleRate || 44100;
3306
+ let sampleRate = this.#audioContext?.sampleRate || 44100;
2887
3307
  if (this.#isNumericArg(this.#argAt(message.args, argIndex))) {
2888
3308
  numChannels = Math.max(1, this.#optionalIntArg(message.args, argIndex, 1));
2889
3309
  argIndex++;
@@ -2974,10 +3394,10 @@ var SuperSonic = class _SuperSonic {
2974
3394
  });
2975
3395
  }
2976
3396
  #requireBufferManager() {
2977
- if (!this.bufferManager) {
3397
+ if (!this.#bufferManager) {
2978
3398
  throw new Error("Buffer manager not ready. Call init() before issuing buffer commands.");
2979
3399
  }
2980
- return this.bufferManager;
3400
+ return this.#bufferManager;
2981
3401
  }
2982
3402
  #isBundle(packet) {
2983
3403
  return packet && packet.timeTag !== void 0 && Array.isArray(packet.packets);
@@ -2990,16 +3410,16 @@ var SuperSonic = class _SuperSonic {
2990
3410
  if (header !== "#bundle\0") {
2991
3411
  return null;
2992
3412
  }
2993
- const ntpStartView = new Float64Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.NTP_START_TIME_START, 1);
3413
+ const ntpStartView = new Float64Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.NTP_START_TIME_START, 1);
2994
3414
  const ntpStartTime = ntpStartView[0];
2995
3415
  if (ntpStartTime === 0) {
2996
3416
  console.warn("[SuperSonic] NTP start time not yet initialized");
2997
3417
  return null;
2998
3418
  }
2999
- const driftView = new Int32Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.DRIFT_OFFSET_START, 1);
3419
+ const driftView = new Int32Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.DRIFT_OFFSET_START, 1);
3000
3420
  const driftMs = Atomics.load(driftView, 0);
3001
3421
  const driftSeconds = driftMs / 1e3;
3002
- const globalView = new Int32Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.GLOBAL_OFFSET_START, 1);
3422
+ const globalView = new Int32Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.GLOBAL_OFFSET_START, 1);
3003
3423
  const globalMs = Atomics.load(globalView, 0);
3004
3424
  const globalSeconds = globalMs / 1e3;
3005
3425
  const totalOffset = ntpStartTime + driftSeconds + globalSeconds;
@@ -3011,7 +3431,7 @@ var SuperSonic = class _SuperSonic {
3011
3431
  }
3012
3432
  const ntpTimeS = ntpSeconds + ntpFraction / 4294967296;
3013
3433
  const audioTimeS = ntpTimeS - totalOffset;
3014
- const currentTimeS = this.audioContext.currentTime;
3434
+ const currentTimeS = this.#audioContext.currentTime;
3015
3435
  return { audioTimeS, currentTimeS };
3016
3436
  }
3017
3437
  };