supersonic-scsynth 0.5.0 → 0.6.2

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,534 +1563,143 @@ 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/memory_layout.js
1609
- var MemoryLayout = {
1610
- /**
1611
- * Total WebAssembly memory in pages (1 page = 64KB)
1612
- * Current: 1536 pages = 96MB
1613
- *
1614
- * This value is used by build.sh to set -sINITIAL_MEMORY
1615
- * Must match: totalPages * 65536 = bufferPoolOffset + bufferPoolSize
1616
- */
1617
- totalPages: 1536,
1618
- /**
1619
- * WASM heap size (implicit, first section of memory)
1620
- * Not directly configurable here - defined by bufferPoolOffset - ringBufferReserved
1621
- * Current: 0-32MB (32 * 1024 * 1024 = 33554432 bytes)
1622
- */
1623
- // wasmHeapSize is implicitly: bufferPoolOffset - ringBufferReserved
1624
- /**
1625
- * Ring buffer reserved space (between WASM heap and buffer pool)
1626
- * Actual ring buffer usage: IN: 768KB, OUT: 128KB, DEBUG: 64KB = 960KB
1627
- * Plus control structures: CONTROL_SIZE (40B) + METRICS_SIZE (48B) + NTP_START_TIME_SIZE (8B) ≈ 96B
1628
- * Total actual usage: ~960KB
1629
- * Reserved: 1MB (provides ~64KB headroom for alignment and future expansion)
1630
- * Current: 1MB reserved (starts where WASM heap ends at 32MB)
1631
- */
1632
- ringBufferReserved: 1 * 1024 * 1024,
1633
- // 1MB reserved
1634
- /**
1635
- * Buffer pool byte offset from start of SharedArrayBuffer
1636
- * Audio samples are allocated from this pool using @thi.ng/malloc
1637
- * Must be after WASM heap + ring buffer area
1638
- * Current: 33MB offset = after 32MB heap + 1MB ring buffers
1639
- */
1640
- bufferPoolOffset: 33 * 1024 * 1024,
1641
- // 34603008 bytes
1642
- /**
1643
- * Buffer pool size in bytes
1644
- * Used for audio sample storage (loaded files + allocated buffers)
1645
- * Current: 63MB (enough for ~3.5 minutes of stereo at 48kHz uncompressed)
1646
- */
1647
- bufferPoolSize: 63 * 1024 * 1024,
1648
- // 66060288 bytes
1649
- /**
1650
- * Total memory calculation (should equal totalPages * 65536)
1651
- * wasmHeap (32MB) + ringReserve (1MB) + bufferPool (63MB) = 96MB
1652
- */
1653
- get totalMemory() {
1654
- return this.bufferPoolOffset + this.bufferPoolSize;
1655
- },
1656
- /**
1657
- * Effective WASM heap size (derived)
1658
- * This is the space available for scsynth C++ allocations
1659
- */
1660
- get wasmHeapSize() {
1661
- return this.bufferPoolOffset - this.ringBufferReserved;
1662
- }
1663
- };
1664
-
1665
- // js/scsynth_options.js
1666
- var worldOptions = {
1667
- /**
1668
- * Maximum number of audio buffers (SndBuf slots)
1669
- * Each buffer slot: 104 bytes overhead (2x SndBuf + SndBufUpdates structs)
1670
- * Actual audio data is stored in buffer pool (separate from heap)
1671
- * Default: 1024 (matching SuperCollider default)
1672
- * Range: 1-65535 (limited by practical memory constraints)
1673
- */
1674
- numBuffers: 1024,
1675
- /**
1676
- * Maximum number of synthesis nodes (synths + groups)
1677
- * Each node: ~200-500 bytes depending on synth complexity
1678
- * Default: 1024 (matching SuperCollider default)
1679
- */
1680
- maxNodes: 1024,
1681
- /**
1682
- * Maximum number of synth definitions (SynthDef count)
1683
- * Each definition: variable size (typically 1-10KB)
1684
- * Default: 1024 (matching SuperCollider default)
1685
- */
1686
- maxGraphDefs: 1024,
1687
- /**
1688
- * Maximum wire buffers for internal audio routing
1689
- * Wire buffers: temporary buffers for UGen connections
1690
- * Each: bufLength * 8 bytes (128 samples * 8 = 1024 bytes)
1691
- * Default: 64 (matching SuperCollider default)
1692
- */
1693
- maxWireBufs: 64,
1694
- /**
1695
- * Number of audio bus channels
1696
- * Audio buses: real-time audio routing between synths
1697
- * Memory: bufLength * numChannels * 4 bytes (128 * 128 * 4 = 64KB)
1698
- * Default: 128 (SuperSonic default, SC uses 1024)
1699
- */
1700
- numAudioBusChannels: 128,
1701
- /**
1702
- * Number of input bus channels (hardware audio input)
1703
- * WebAudio/AudioWorklet input
1704
- * Default: 0 (no input in current SuperSonic implementation)
1705
- */
1706
- numInputBusChannels: 0,
1707
- /**
1708
- * Number of output bus channels (hardware audio output)
1709
- * WebAudio/AudioWorklet output
1710
- * Default: 2 (stereo)
1711
- */
1712
- numOutputBusChannels: 2,
1713
- /**
1714
- * Number of control bus channels
1715
- * Control buses: control-rate data sharing between synths
1716
- * Memory: numChannels * 4 bytes (4096 * 4 = 16KB)
1717
- * Default: 4096 (SuperSonic default, SC uses 16384)
1718
- */
1719
- numControlBusChannels: 4096,
1720
- /**
1721
- * Audio buffer length in samples (AudioWorklet quantum)
1722
- *
1723
- * FIXED VALUE - DO NOT CHANGE
1724
- * This MUST be 128 for AudioWorklet compatibility (WebAudio API spec).
1725
- *
1726
- * Unlike SuperCollider (where bufLength can be 32, 64, 128, etc.),
1727
- * SuperSonic is locked to 128 because AudioWorklet has a fixed quantum size.
1728
- *
1729
- * This value is kept in the config for:
1730
- * 1. Documentation (shows what value SuperSonic uses)
1731
- * 2. Passing to C++ code (required by WorldOptions)
1732
- * 3. Validation (catches accidental changes)
1733
- *
1734
- * If you provide this in your config, it MUST be 128 or initialization will fail.
1735
- * It's recommended to omit this field entirely and let the default be used.
1736
- *
1737
- * Default: 128 (fixed, cannot be changed)
1738
- */
1739
- bufLength: 128,
1740
- /**
1741
- * Real-time memory pool size in kilobytes
1742
- * AllocPool for synthesis-time allocations (UGen memory, etc.)
1743
- * This is the largest single allocation from WASM heap
1744
- * Memory: realTimeMemorySize * 1024 bytes (16384 * 1024 = 16MB)
1745
- * Default: 16384 KB (16MB, SuperSonic default, SC uses 8192 = 8MB)
1746
- */
1747
- realTimeMemorySize: 16384,
1748
- /**
1749
- * Number of random number generators
1750
- * Each synth can have its own RNG for reproducible randomness
1751
- * Default: 64 (matching SuperCollider default)
1752
- */
1753
- numRGens: 64,
1754
- /**
1755
- * Real-time mode flag
1756
- * false = Non-real-time (NRT) mode, externally driven by AudioWorklet
1757
- * true = Real-time mode (not used in WebAudio context)
1758
- * Default: false (SuperSonic always uses NRT mode)
1759
- */
1760
- realTime: false,
1761
- /**
1762
- * Memory locking (mlock)
1763
- * Not applicable in WebAssembly/browser environment
1764
- * Default: false
1765
- */
1766
- memoryLocking: false,
1767
- /**
1768
- * Auto-load SynthDefs from disk
1769
- * 0 = don't auto-load (synths sent via /d_recv)
1770
- * 1 = auto-load from plugin path
1771
- * Default: 0 (SuperSonic loads synthdefs via network)
1772
- */
1773
- loadGraphDefs: 0,
1774
- /**
1775
- * Preferred sample rate (if not specified, uses AudioContext.sampleRate)
1776
- * Common values: 44100, 48000, 96000
1777
- * Default: 0 (use AudioContext default, typically 48000)
1778
- */
1779
- preferredSampleRate: 0,
1780
- /**
1781
- * Debug verbosity level
1782
- * 0 = quiet, 1 = errors, 2 = warnings, 3 = info, 4 = debug
1783
- * Default: 0
1784
- */
1785
- verbosity: 0
1786
- };
1787
- var ScsynthConfig = {
1788
- memory: MemoryLayout,
1789
- worldOptions
1790
- };
1791
-
1792
- // js/lib/config_validator.js
1793
- var ConfigValidator = class {
1794
- /**
1795
- * Validate worldOptions for common errors
1796
- * @param {Object} worldOptions - WorldOptions to validate
1797
- * @param {Object} memoryConfig - Memory layout configuration
1798
- * @throws {Error} If validation fails with helpful message
1799
- */
1800
- static validateWorldOptions(worldOptions2, memoryConfig) {
1801
- if (!worldOptions2 || typeof worldOptions2 !== "object") {
1802
- throw new Error("worldOptions must be an object");
1803
- }
1804
- const numericFields = [
1805
- "numBuffers",
1806
- "maxNodes",
1807
- "maxGraphDefs",
1808
- "maxWireBufs",
1809
- "numAudioBusChannels",
1810
- "numInputBusChannels",
1811
- "numOutputBusChannels",
1812
- "numControlBusChannels",
1813
- "bufLength",
1814
- "realTimeMemorySize",
1815
- "numRGens",
1816
- "verbosity",
1817
- "preferredSampleRate",
1818
- "loadGraphDefs"
1819
- ];
1820
- const canBeZero = /* @__PURE__ */ new Set([
1821
- "numInputBusChannels",
1822
- // 0 = no audio inputs (valid for synthesis-only)
1823
- "numOutputBusChannels",
1824
- // 0 = no audio outputs (unusual but valid)
1825
- "verbosity",
1826
- // 0 = no verbose output
1827
- "preferredSampleRate",
1828
- // 0 = auto-detect sample rate
1829
- "loadGraphDefs"
1830
- // 0 = don't auto-load SynthDefs from disk
1831
- ]);
1832
- for (const field of numericFields) {
1833
- if (field in worldOptions2) {
1834
- const value = worldOptions2[field];
1835
- if (!Number.isFinite(value)) {
1836
- throw new Error(
1837
- `worldOptions.${field} must be a finite number, got ${typeof value}: ${value}`
1838
- );
1839
- }
1840
- const minValue = canBeZero.has(field) ? 0 : 1;
1841
- if (value < minValue) {
1842
- throw new Error(
1843
- `worldOptions.${field} must be ${minValue > 0 ? "positive" : "non-negative"}, got ${value}`
1844
- );
1845
- }
1846
- const maxValue = this.#getMaxValueFor(field);
1847
- if (value > maxValue) {
1848
- throw new Error(
1849
- `worldOptions.${field} exceeds reasonable maximum.
1850
- Got: ${value}
1851
- Max: ${maxValue}
1852
- This may indicate a configuration error.`
1853
- );
1854
- }
1855
- }
1566
+ // js/lib/buffer_manager.js
1567
+ var BUFFER_POOL_ALIGNMENT = 8;
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;
1579
+ constructor(options) {
1580
+ const {
1581
+ audioContext,
1582
+ sharedBuffer,
1583
+ bufferPoolConfig,
1584
+ sampleBaseURL,
1585
+ audioPathMap = {},
1586
+ maxBuffers = 1024
1587
+ } = options;
1588
+ if (!audioContext) {
1589
+ throw new Error("BufferManager requires audioContext");
1856
1590
  }
1857
- if ("bufLength" in worldOptions2 && worldOptions2.bufLength !== 128) {
1858
- throw new Error(
1859
- `worldOptions.bufLength must be 128 (AudioWorklet quantum size).
1860
- Got: ${worldOptions2.bufLength}
1861
- This value cannot be changed as it's defined by the Web Audio API.
1862
- Remove 'bufLength' from your configuration.`
1863
- );
1591
+ if (!sharedBuffer || !(sharedBuffer instanceof SharedArrayBuffer)) {
1592
+ throw new Error("BufferManager requires sharedBuffer (SharedArrayBuffer)");
1864
1593
  }
1865
- const booleanFields = ["realTime", "memoryLocking"];
1866
- for (const field of booleanFields) {
1867
- if (field in worldOptions2 && typeof worldOptions2[field] !== "boolean") {
1868
- throw new Error(
1869
- `worldOptions.${field} must be a boolean, got ${typeof worldOptions2[field]}`
1870
- );
1871
- }
1594
+ if (!bufferPoolConfig || typeof bufferPoolConfig !== "object") {
1595
+ throw new Error("BufferManager requires bufferPoolConfig (object with start, size, align)");
1872
1596
  }
1873
- if ("preferredSampleRate" in worldOptions2) {
1874
- const rate = worldOptions2.preferredSampleRate;
1875
- if (rate !== 0 && !Number.isFinite(rate)) {
1876
- throw new Error(
1877
- `worldOptions.preferredSampleRate must be 0 (auto) or a finite number, got ${rate}`
1878
- );
1879
- }
1880
- if (rate !== 0 && (rate < 8e3 || rate > 192e3)) {
1881
- throw new Error(
1882
- `worldOptions.preferredSampleRate out of range: ${rate}
1883
- Valid range: 8000-192000 Hz, or 0 for auto`
1884
- );
1885
- }
1597
+ if (!Number.isFinite(bufferPoolConfig.start) || bufferPoolConfig.start < 0) {
1598
+ throw new Error("bufferPoolConfig.start must be a non-negative number");
1886
1599
  }
1887
- this.#validateCombinations(worldOptions2);
1888
- if (memoryConfig) {
1889
- const heapSize = memoryConfig.wasmHeapSize;
1890
- if (heapSize) {
1891
- MemoryEstimator.validateHeapFits(worldOptions2, heapSize);
1892
- }
1600
+ if (!Number.isFinite(bufferPoolConfig.size) || bufferPoolConfig.size <= 0) {
1601
+ throw new Error("bufferPoolConfig.size must be a positive number");
1893
1602
  }
1894
- }
1895
- /**
1896
- * Validate memory layout for coherence
1897
- * @param {Object} memory - Memory configuration
1898
- * @throws {Error} If layout is invalid
1899
- */
1900
- static validateMemoryLayout(memory) {
1901
- if (!memory || typeof memory !== "object") {
1902
- throw new Error("memory configuration must be an object");
1603
+ if (audioPathMap && typeof audioPathMap !== "object") {
1604
+ throw new Error("audioPathMap must be an object");
1903
1605
  }
1904
- const { totalPages, bufferPoolOffset, bufferPoolSize, ringBufferReserved } = memory;
1905
- if (!Number.isFinite(totalPages) || totalPages <= 0) {
1906
- throw new Error(
1907
- `memory.totalPages must be a positive number, got ${totalPages}`
1908
- );
1606
+ if (!Number.isInteger(maxBuffers) || maxBuffers <= 0) {
1607
+ throw new Error("maxBuffers must be a positive integer");
1909
1608
  }
1910
- if (!Number.isFinite(bufferPoolOffset) || bufferPoolOffset <= 0) {
1911
- throw new Error(
1912
- `memory.bufferPoolOffset must be a positive number, got ${bufferPoolOffset}`
1913
- );
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();
1622
+ this.GUARD_BEFORE = 3;
1623
+ this.GUARD_AFTER = 1;
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`);
1914
1632
  }
1915
- if (!Number.isFinite(bufferPoolSize) || bufferPoolSize <= 0) {
1916
- throw new Error(
1917
- `memory.bufferPoolSize must be a positive number, got ${bufferPoolSize}`
1918
- );
1633
+ if (scPath.includes("..")) {
1634
+ throw new Error(`Invalid audio path: path cannot contain '..' (got: ${scPath})`);
1919
1635
  }
1920
- const totalMemory = totalPages * 65536;
1921
- const expectedTotal = bufferPoolOffset + bufferPoolSize;
1922
- if (totalMemory !== expectedTotal) {
1923
- throw new Error(
1924
- `Memory layout mismatch:
1925
- totalPages * 65536 = ${totalMemory} bytes (${(totalMemory / 1024 / 1024).toFixed(0)}MB)
1926
- bufferPoolOffset + bufferPoolSize = ${expectedTotal} bytes (${(expectedTotal / 1024 / 1024).toFixed(0)}MB)
1927
- These must be equal. Check your memory configuration.`
1928
- );
1636
+ if (scPath.startsWith("/") || /^[a-zA-Z]:/.test(scPath)) {
1637
+ throw new Error(`Invalid audio path: path must be relative (got: ${scPath})`);
1929
1638
  }
1930
- const minBufferPoolOffset = ringBufferReserved || 32 * 1024 * 1024;
1931
- if (bufferPoolOffset < minBufferPoolOffset) {
1932
- throw new Error(
1933
- `memory.bufferPoolOffset (${bufferPoolOffset}) is too small.
1934
- Must be at least: ${minBufferPoolOffset} bytes (${(minBufferPoolOffset / 1024 / 1024).toFixed(0)}MB)
1935
- This is required to avoid collision with WASM heap and ring buffers.`
1936
- );
1639
+ if (scPath.includes("%2e") || scPath.includes("%2E")) {
1640
+ throw new Error(`Invalid audio path: path cannot contain URL-encoded characters (got: ${scPath})`);
1937
1641
  }
1938
- if (totalMemory > 512 * 1024 * 1024) {
1939
- console.warn(
1940
- `[SuperSonic] Warning: Total memory is ${(totalMemory / 1024 / 1024).toFixed(0)}MB.
1941
- Large memory allocations may cause browser performance issues on some devices.`
1942
- );
1642
+ if (scPath.includes("\\")) {
1643
+ throw new Error(`Invalid audio path: use forward slashes only (got: ${scPath})`);
1943
1644
  }
1944
- const wasmHeapSize = memory.wasmHeapSize;
1945
- if (wasmHeapSize && wasmHeapSize < 16 * 1024 * 1024) {
1946
- console.warn(
1947
- `[SuperSonic] Warning: WASM heap is only ${(wasmHeapSize / 1024 / 1024).toFixed(0)}MB.
1948
- This may be too small for typical usage. Consider increasing bufferPoolOffset.`
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'
1949
1651
  );
1950
1652
  }
1653
+ return this.#sampleBaseURL + scPath;
1951
1654
  }
1952
- /**
1953
- * Get maximum reasonable value for a field
1954
- * Based on SuperCollider defaults and reasonable WASM/browser limits
1955
- * @private
1956
- */
1957
- static #getMaxValueFor(field) {
1958
- const limits = {
1959
- // SC default: 1024, All uint32 in SC_WorldOptions.h
1960
- // Limit to prevent excessive memory allocation
1961
- numBuffers: 65535,
1962
- maxNodes: 65535,
1963
- // SC default: 1024
1964
- maxGraphDefs: 65535,
1965
- // SC default: 1024
1966
- maxWireBufs: 2048,
1967
- // SC default: 64
1968
- // SC default: 1024 (NOT 128!)
1969
- // But we use lower for browser context
1970
- numAudioBusChannels: 4096,
1971
- numInputBusChannels: 256,
1972
- // SC default: 8
1973
- numOutputBusChannels: 256,
1974
- // SC default: 8
1975
- // SC default: 16384 (NOT 4096!)
1976
- numControlBusChannels: 65535,
1977
- bufLength: 128,
1978
- // Fixed for AudioWorklet (SC default: 64)
1979
- // SC default: 8192 KB (8MB)
1980
- // Limit to 1GB for browser safety
1981
- realTimeMemorySize: 1048576,
1982
- // KB (1GB max)
1983
- numRGens: 1024,
1984
- // SC default: 64
1985
- verbosity: 10,
1986
- preferredSampleRate: 192e3,
1987
- // Maximum sample rate
1988
- loadGraphDefs: 1
1989
- // SC default: 1, 0=off 1=on
1990
- };
1991
- return limits[field] || Number.MAX_SAFE_INTEGER;
1655
+ #validateBufferNumber(bufnum) {
1656
+ if (!Number.isInteger(bufnum) || bufnum < 0 || bufnum >= this.MAX_BUFFERS) {
1657
+ throw new Error(`Invalid buffer number ${bufnum} (must be 0-${this.MAX_BUFFERS - 1})`);
1658
+ }
1992
1659
  }
1993
1660
  /**
1994
- * Validate compatible combinations of options
1661
+ * Execute a buffer operation with proper locking, registration, and cleanup
1995
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
1996
1668
  */
1997
- static #validateCombinations(worldOptions2) {
1998
- if (worldOptions2.realTimeMemorySize && worldOptions2.realTimeMemorySize < 1024) {
1999
- console.warn(
2000
- `[SuperSonic] Warning: realTimeMemorySize is only ${worldOptions2.realTimeMemorySize}KB (${(worldOptions2.realTimeMemorySize / 1024).toFixed(1)}MB).
2001
- This is very small and may cause allocation failures. Recommended minimum: 4096 KB (4MB).`
2002
- );
2003
- }
2004
- if (worldOptions2.numOutputBusChannels && worldOptions2.numOutputBusChannels > 8) {
2005
- console.warn(
2006
- `[SuperSonic] Warning: numOutputBusChannels is ${worldOptions2.numOutputBusChannels}.
2007
- Most audio hardware supports 2-8 channels. Verify your AudioContext supports this.`
2008
- );
2009
- }
2010
- }
2011
- };
2012
- var MemoryEstimator = class {
2013
- /**
2014
- * Estimate WASM heap usage for given WorldOptions
2015
- * Returns breakdown of memory usage by component
2016
- *
2017
- * Based on actual SuperCollider source code allocations (SC_World.cpp, SC_Graph.cpp)
2018
- *
2019
- * @param {Object} worldOptions - Configuration to analyze
2020
- * @returns {Object} { total, breakdown: {...} }
2021
- */
2022
- static estimateHeapUsage(worldOptions2) {
2023
- const breakdown = {
2024
- // AllocPool for real-time synthesis (SC_World.cpp:328)
2025
- // new AllocPool(..., inOptions->mRealTimeMemorySize * 1024, 0)
2026
- realTimeMemory: worldOptions2.realTimeMemorySize * 1024,
2027
- // KB → bytes
2028
- // SndBuf arrays (SC_World.cpp:383-385)
2029
- // mSndBufs: sizeof(SndBuf) = 48 bytes (SC_SndBuf.h:152-168, WASM32 with 32-bit pointers)
2030
- // mSndBufsNonRealTimeMirror: sizeof(SndBuf) = 48 bytes
2031
- // mSndBufUpdates: sizeof(SndBufUpdates) = 8 bytes (2 × int32)
2032
- // Total: (48 + 48 + 8) * mNumBuffers = 104 bytes per buffer
2033
- sndBufs: worldOptions2.numBuffers * 104,
2034
- // Audio buses (SC_World.cpp:376-377)
2035
- // mAudioBus = zalloc(mBufLength * mNumAudioBusChannels, sizeof(float))
2036
- audioBuses: worldOptions2.numAudioBusChannels * worldOptions2.bufLength * 4,
2037
- // Audio bus touched flags (SC_World.cpp:379)
2038
- // mAudioBusTouched = zalloc(mNumAudioBusChannels, sizeof(int32))
2039
- audioBusTouched: worldOptions2.numAudioBusChannels * 4,
2040
- // Control buses (SC_World.cpp:370)
2041
- // mControlBus = zalloc(mNumControlBusChannels, sizeof(float))
2042
- controlBuses: worldOptions2.numControlBusChannels * 4,
2043
- // Control bus touched flags (SC_World.cpp:380)
2044
- // mControlBusTouched = zalloc(mNumControlBusChannels, sizeof(int32))
2045
- controlBusTouched: worldOptions2.numControlBusChannels * 4,
2046
- // Wire buffers (SC_World.cpp:917)
2047
- // mWireBufSpace = malloc_alig(mMaxWireBufs * mBufLength * sizeof(float))
2048
- wireBufs: worldOptions2.maxWireBufs * worldOptions2.bufLength * 4,
2049
- // Random number generators (SC_World.cpp:398)
2050
- // mRGen = new RGen[mNumRGens]
2051
- // sizeof(RGen) = 12 bytes (3 × uint32, SC_RGen.h:57-85)
2052
- rgens: worldOptions2.numRGens * 12,
2053
- // Node hash table (SC_World.cpp:334)
2054
- // IntHashTable<Node, AllocPool> with capacity mMaxNodes
2055
- // Conservative estimate: ~64 bytes per slot for hash table overhead
2056
- nodes: worldOptions2.maxNodes * 64,
2057
- // GraphDef hash table (SC_World.cpp:333)
2058
- // HashTable<GraphDef, Malloc> with capacity mMaxGraphDefs
2059
- // Conservative estimate: ~128 bytes per slot (varies per GraphDef)
2060
- graphDefs: worldOptions2.maxGraphDefs * 128,
2061
- // World struct, HiddenWorld, and other fixed overhead
2062
- // Includes: World struct, HiddenWorld struct, locks, semaphores, etc.
2063
- overhead: 2 * 1024 * 1024
2064
- // 2MB
2065
- };
2066
- const total = Object.values(breakdown).reduce((sum, val) => sum + val, 0);
2067
- return { total, breakdown };
2068
- }
2069
- /**
2070
- * Check if worldOptions fit within available heap
2071
- * @param {Object} worldOptions
2072
- * @param {number} heapSize - Available heap in bytes
2073
- * @throws {Error} If estimated usage exceeds heap
2074
- */
2075
- static validateHeapFits(worldOptions2, heapSize) {
2076
- const { total, breakdown } = this.estimateHeapUsage(worldOptions2);
2077
- if (total > heapSize) {
2078
- const totalMB = (total / (1024 * 1024)).toFixed(2);
2079
- const heapMB = (heapSize / (1024 * 1024)).toFixed(2);
2080
- const sorted = Object.entries(breakdown).sort(([, a], [, b]) => b - a).slice(0, 3).map(([key, bytes]) => ` ${key}: ${(bytes / (1024 * 1024)).toFixed(2)}MB`).join("\n");
2081
- throw new Error(
2082
- `WorldOptions estimated to use ${totalMB}MB but WASM heap is only ${heapMB}MB.
2083
-
2084
- Largest allocations:
2085
- ${sorted}
2086
-
2087
- To fix this error:
2088
- 1. Reduce realTimeMemorySize (currently ${worldOptions2.realTimeMemorySize}KB)
2089
- 2. Reduce numBuffers (currently ${worldOptions2.numBuffers})
2090
- 3. Reduce maxNodes (currently ${worldOptions2.maxNodes})
2091
- 4. Or: Increase WASM heap size (requires rebuild - see CONFIGURATION_PHASES.md Phase 4)`
2092
- );
2093
- }
2094
- const usagePercent = total / heapSize * 100;
2095
- if (usagePercent > 80) {
2096
- console.warn(
2097
- `[SuperSonic] Warning: Estimated heap usage is ${usagePercent.toFixed(0)}% of available space.
2098
- Used: ${(total / 1024 / 1024).toFixed(1)}MB
2099
- Available: ${(heapSize / 1024 / 1024).toFixed(1)}MB
2100
- Consider reducing options or increasing heap size for safety margin.`
2101
- );
2102
- }
2103
- }
2104
- };
2105
-
2106
- // js/supersonic.js
2107
- var BufferManager = class {
2108
- constructor(options) {
2109
- const {
2110
- audioContext,
2111
- sharedBuffer,
2112
- bufferPool,
2113
- allocatedBuffers,
2114
- resolveAudioPath,
2115
- registerPendingOp,
2116
- maxBuffers = 1024
2117
- } = options;
2118
- this.audioContext = audioContext;
2119
- this.sharedBuffer = sharedBuffer;
2120
- this.bufferPool = bufferPool;
2121
- this.allocatedBuffers = allocatedBuffers;
2122
- this.resolveAudioPath = resolveAudioPath;
2123
- this.registerPendingOp = registerPendingOp;
2124
- this.bufferLocks = /* @__PURE__ */ new Map();
2125
- this.GUARD_BEFORE = 3;
2126
- this.GUARD_AFTER = 1;
2127
- this.MAX_BUFFERS = maxBuffers;
2128
- }
2129
- #validateBufferNumber(bufnum) {
2130
- if (!Number.isInteger(bufnum) || bufnum < 0 || bufnum >= this.MAX_BUFFERS) {
2131
- throw new Error(`Invalid buffer number ${bufnum} (must be 0-${this.MAX_BUFFERS - 1})`);
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
+ }
2132
1703
  }
2133
1704
  }
2134
1705
  async prepareFromFile(params) {
@@ -2140,20 +1711,14 @@ var BufferManager = class {
2140
1711
  channels = null
2141
1712
  } = params;
2142
1713
  this.#validateBufferNumber(bufnum);
2143
- let allocatedPtr = null;
2144
- let pendingToken = null;
2145
- let allocationRegistered = false;
2146
- const releaseLock = await this.#acquireBufferLock(bufnum);
2147
- let lockReleased = false;
2148
- try {
2149
- await this.#awaitPendingReplacement(bufnum);
2150
- const resolvedPath = this.resolveAudioPath(path);
1714
+ return this.#executeBufferOperation(bufnum, 6e4, async () => {
1715
+ const resolvedPath = this.#resolveAudioPath(path);
2151
1716
  const response = await fetch(resolvedPath);
2152
1717
  if (!response.ok) {
2153
1718
  throw new Error(`Failed to fetch ${resolvedPath}: ${response.status} ${response.statusText}`);
2154
1719
  }
2155
1720
  const arrayBuffer = await response.arrayBuffer();
2156
- const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
1721
+ const audioBuffer = await this.#audioContext.decodeAudioData(arrayBuffer);
2157
1722
  const start = Math.max(0, Math.floor(startFrame || 0));
2158
1723
  const availableFrames = audioBuffer.length - start;
2159
1724
  const framesRequested = numFrames && numFrames > 0 ? Math.min(Math.floor(numFrames), availableFrames) : availableFrames;
@@ -2163,7 +1728,7 @@ var BufferManager = class {
2163
1728
  const selectedChannels = this.#normalizeChannels(channels, audioBuffer.numberOfChannels);
2164
1729
  const numChannels = selectedChannels.length;
2165
1730
  const totalSamples = framesRequested * numChannels + (this.GUARD_BEFORE + this.GUARD_AFTER) * numChannels;
2166
- allocatedPtr = this.#malloc(totalSamples);
1731
+ const ptr = this.#malloc(totalSamples);
2167
1732
  const interleaved = new Float32Array(totalSamples);
2168
1733
  const dataOffset = this.GUARD_BEFORE * numChannels;
2169
1734
  for (let frame = 0; frame < framesRequested; frame++) {
@@ -2173,35 +1738,16 @@ var BufferManager = class {
2173
1738
  interleaved[dataOffset + frame * numChannels + ch] = channelData[start + frame];
2174
1739
  }
2175
1740
  }
2176
- this.#writeToSharedBuffer(allocatedPtr, interleaved);
1741
+ this.#writeToSharedBuffer(ptr, interleaved);
2177
1742
  const sizeBytes = interleaved.length * 4;
2178
- const { uuid, allocationComplete } = this.#registerPending(bufnum);
2179
- pendingToken = uuid;
2180
- this.#recordAllocation(bufnum, allocatedPtr, sizeBytes, uuid, allocationComplete);
2181
- allocationRegistered = true;
2182
- const managedCompletion = this.#attachFinalizer(bufnum, uuid, allocationComplete);
2183
- releaseLock();
2184
- lockReleased = true;
2185
1743
  return {
2186
- ptr: allocatedPtr,
1744
+ ptr,
1745
+ sizeBytes,
2187
1746
  numFrames: framesRequested,
2188
1747
  numChannels,
2189
- sampleRate: audioBuffer.sampleRate,
2190
- uuid,
2191
- allocationComplete: managedCompletion
1748
+ sampleRate: audioBuffer.sampleRate
2192
1749
  };
2193
- } catch (error) {
2194
- if (allocationRegistered && pendingToken) {
2195
- this.#finalizeReplacement(bufnum, pendingToken, false);
2196
- } else if (allocatedPtr) {
2197
- this.bufferPool.free(allocatedPtr);
2198
- }
2199
- throw error;
2200
- } finally {
2201
- if (!lockReleased) {
2202
- releaseLock();
2203
- }
2204
- }
1750
+ });
2205
1751
  }
2206
1752
  async prepareEmpty(params) {
2207
1753
  const {
@@ -2211,9 +1757,6 @@ var BufferManager = class {
2211
1757
  sampleRate = null
2212
1758
  } = params;
2213
1759
  this.#validateBufferNumber(bufnum);
2214
- let allocationRegistered = false;
2215
- let pendingToken = null;
2216
- let allocatedPtr = null;
2217
1760
  if (!Number.isFinite(numFrames) || numFrames <= 0) {
2218
1761
  throw new Error(`/b_alloc requires a positive number of frames (got ${numFrames})`);
2219
1762
  }
@@ -2222,42 +1765,20 @@ var BufferManager = class {
2222
1765
  }
2223
1766
  const roundedFrames = Math.floor(numFrames);
2224
1767
  const roundedChannels = Math.floor(numChannels);
2225
- const totalSamples = roundedFrames * roundedChannels + (this.GUARD_BEFORE + this.GUARD_AFTER) * roundedChannels;
2226
- const releaseLock = await this.#acquireBufferLock(bufnum);
2227
- let lockReleased = false;
2228
- try {
2229
- await this.#awaitPendingReplacement(bufnum);
2230
- 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);
2231
1771
  const interleaved = new Float32Array(totalSamples);
2232
- this.#writeToSharedBuffer(allocatedPtr, interleaved);
1772
+ this.#writeToSharedBuffer(ptr, interleaved);
2233
1773
  const sizeBytes = interleaved.length * 4;
2234
- const { uuid, allocationComplete } = this.#registerPending(bufnum);
2235
- pendingToken = uuid;
2236
- this.#recordAllocation(bufnum, allocatedPtr, sizeBytes, uuid, allocationComplete);
2237
- allocationRegistered = true;
2238
- const managedCompletion = this.#attachFinalizer(bufnum, uuid, allocationComplete);
2239
- releaseLock();
2240
- lockReleased = true;
2241
1774
  return {
2242
- ptr: allocatedPtr,
1775
+ ptr,
1776
+ sizeBytes,
2243
1777
  numFrames: roundedFrames,
2244
1778
  numChannels: roundedChannels,
2245
- sampleRate: sampleRate || this.audioContext.sampleRate,
2246
- uuid,
2247
- allocationComplete: managedCompletion
1779
+ sampleRate: sampleRate || this.#audioContext.sampleRate
2248
1780
  };
2249
- } catch (error) {
2250
- if (allocationRegistered && pendingToken) {
2251
- this.#finalizeReplacement(bufnum, pendingToken, false);
2252
- } else if (allocatedPtr) {
2253
- this.bufferPool.free(allocatedPtr);
2254
- }
2255
- throw error;
2256
- } finally {
2257
- if (!lockReleased) {
2258
- releaseLock();
2259
- }
2260
- }
1781
+ });
2261
1782
  }
2262
1783
  #normalizeChannels(requestedChannels, fileChannels) {
2263
1784
  if (!requestedChannels || requestedChannels.length === 0) {
@@ -2272,9 +1793,9 @@ var BufferManager = class {
2272
1793
  }
2273
1794
  #malloc(totalSamples) {
2274
1795
  const bytesNeeded = totalSamples * 4;
2275
- const ptr = this.bufferPool.malloc(bytesNeeded);
1796
+ const ptr = this.#bufferPool.malloc(bytesNeeded);
2276
1797
  if (ptr === 0) {
2277
- const stats = this.bufferPool.stats();
1798
+ const stats = this.#bufferPool.stats();
2278
1799
  const availableMB = ((stats.available || 0) / (1024 * 1024)).toFixed(2);
2279
1800
  const totalMB = ((stats.total || 0) / (1024 * 1024)).toFixed(2);
2280
1801
  const requestedMB = (bytesNeeded / (1024 * 1024)).toFixed(2);
@@ -2285,40 +1806,43 @@ var BufferManager = class {
2285
1806
  return ptr;
2286
1807
  }
2287
1808
  #writeToSharedBuffer(ptr, data) {
2288
- const heap = new Float32Array(this.sharedBuffer, ptr, data.length);
1809
+ const heap = new Float32Array(this.#sharedBuffer, ptr, data.length);
2289
1810
  heap.set(data);
2290
1811
  }
2291
- #registerPending(bufnum) {
2292
- if (!this.registerPendingOp) {
2293
- return {
2294
- uuid: crypto.randomUUID(),
2295
- allocationComplete: Promise.resolve()
2296
- };
2297
- }
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) {
2298
1822
  const uuid = crypto.randomUUID();
2299
- const allocationComplete = this.registerPendingOp(uuid, bufnum);
1823
+ const allocationComplete = this.#createPendingOperation(uuid, bufnum, timeoutMs);
2300
1824
  return { uuid, allocationComplete };
2301
1825
  }
2302
1826
  async #acquireBufferLock(bufnum) {
2303
- const prev = this.bufferLocks.get(bufnum) || Promise.resolve();
1827
+ const prev = this.#bufferLocks.get(bufnum) || Promise.resolve();
2304
1828
  let releaseLock;
2305
1829
  const current = new Promise((resolve) => {
2306
1830
  releaseLock = resolve;
2307
1831
  });
2308
- this.bufferLocks.set(bufnum, prev.then(() => current));
1832
+ this.#bufferLocks.set(bufnum, prev.then(() => current));
2309
1833
  await prev;
2310
1834
  return () => {
2311
1835
  if (releaseLock) {
2312
1836
  releaseLock();
2313
1837
  releaseLock = null;
2314
1838
  }
2315
- if (this.bufferLocks.get(bufnum) === current) {
2316
- this.bufferLocks.delete(bufnum);
1839
+ if (this.#bufferLocks.get(bufnum) === current) {
1840
+ this.#bufferLocks.delete(bufnum);
2317
1841
  }
2318
1842
  };
2319
1843
  }
2320
1844
  #recordAllocation(bufnum, ptr, sizeBytes, pendingToken, pendingPromise) {
2321
- const previousEntry = this.allocatedBuffers.get(bufnum);
1845
+ const previousEntry = this.#allocatedBuffers.get(bufnum);
2322
1846
  const entry = {
2323
1847
  ptr,
2324
1848
  size: sizeBytes,
@@ -2326,11 +1850,11 @@ var BufferManager = class {
2326
1850
  pendingPromise,
2327
1851
  previousAllocation: previousEntry ? { ptr: previousEntry.ptr, size: previousEntry.size } : null
2328
1852
  };
2329
- this.allocatedBuffers.set(bufnum, entry);
1853
+ this.#allocatedBuffers.set(bufnum, entry);
2330
1854
  return entry;
2331
1855
  }
2332
1856
  async #awaitPendingReplacement(bufnum) {
2333
- const existing = this.allocatedBuffers.get(bufnum);
1857
+ const existing = this.#allocatedBuffers.get(bufnum);
2334
1858
  if (existing && existing.pendingToken && existing.pendingPromise) {
2335
1859
  try {
2336
1860
  await existing.pendingPromise;
@@ -2352,7 +1876,7 @@ var BufferManager = class {
2352
1876
  });
2353
1877
  }
2354
1878
  #finalizeReplacement(bufnum, pendingToken, success) {
2355
- const entry = this.allocatedBuffers.get(bufnum);
1879
+ const entry = this.#allocatedBuffers.get(bufnum);
2356
1880
  if (!entry || entry.pendingToken !== pendingToken) {
2357
1881
  return;
2358
1882
  }
@@ -2362,57 +1886,382 @@ var BufferManager = class {
2362
1886
  entry.pendingPromise = null;
2363
1887
  entry.previousAllocation = null;
2364
1888
  if (previous?.ptr) {
2365
- this.bufferPool.free(previous.ptr);
1889
+ this.#bufferPool.free(previous.ptr);
2366
1890
  }
2367
1891
  return;
2368
1892
  }
2369
1893
  if (entry.ptr) {
2370
- this.bufferPool.free(entry.ptr);
1894
+ this.#bufferPool.free(entry.ptr);
2371
1895
  }
2372
1896
  entry.pendingPromise = null;
2373
1897
  if (previous?.ptr) {
2374
- this.allocatedBuffers.set(bufnum, {
1898
+ this.#allocatedBuffers.set(bufnum, {
2375
1899
  ptr: previous.ptr,
2376
1900
  size: previous.size,
2377
1901
  pendingToken: null,
2378
1902
  previousAllocation: null
2379
1903
  });
2380
1904
  } else {
2381
- this.allocatedBuffers.delete(bufnum);
1905
+ this.#allocatedBuffers.delete(bufnum);
2382
1906
  }
2383
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
+ );
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");
2040
+ }
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
+ }
2384
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
2385
2219
  var SuperSonic = class _SuperSonic {
2386
2220
  // Expose OSC utilities as static methods
2387
2221
  static osc = {
2388
2222
  encode: (message) => osc_default.writePacket(message),
2389
2223
  decode: (data, options = { metadata: false }) => osc_default.readPacket(data, options)
2390
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;
2391
2249
  constructor(options = {}) {
2392
- this.initialized = false;
2393
- this.initializing = false;
2394
- this.capabilities = {};
2395
- this.sharedBuffer = null;
2396
- this.ringBufferBase = null;
2397
- this.bufferConstants = null;
2398
- this.audioContext = null;
2399
- this.workletNode = null;
2400
- this.osc = null;
2401
- this.wasmModule = null;
2402
- this.wasmInstance = null;
2403
- this.bufferPool = null;
2404
- 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;
2405
2260
  this.loadedSynthDefs = /* @__PURE__ */ new Set();
2406
- this.pendingBufferOps = /* @__PURE__ */ new Map();
2407
- this._timeOffsetPromise = null;
2408
- this._resolveTimeOffset = null;
2409
- this._localClockOffsetTimer = null;
2410
2261
  this.onOSC = null;
2411
2262
  this.onMessage = null;
2412
2263
  this.onMessageSent = null;
2413
2264
  this.onMetricsUpdate = null;
2414
- this.onStatusUpdate = null;
2415
- this.onSendError = null;
2416
2265
  this.onDebugMessage = null;
2417
2266
  this.onInitialized = null;
2418
2267
  this.onError = null;
@@ -2421,48 +2270,58 @@ var SuperSonic = class _SuperSonic {
2421
2270
  }
2422
2271
  const workerBaseURL = options.workerBaseURL;
2423
2272
  const wasmBaseURL = options.wasmBaseURL;
2424
- const scsynthConfig = this.#mergeScsynthOptions(options.scsynthOptions || {});
2425
- try {
2426
- ConfigValidator.validateWorldOptions(scsynthConfig.worldOptions, scsynthConfig.memory);
2427
- ConfigValidator.validateMemoryLayout(scsynthConfig.memory);
2428
- } catch (error) {
2429
- throw new Error(`SuperSonic configuration validation failed:
2430
- ${error.message}`);
2431
- }
2273
+ const worldOptions = { ...defaultWorldOptions, ...options.scsynthOptions };
2432
2274
  this.config = {
2433
2275
  wasmUrl: options.wasmUrl || wasmBaseURL + "scsynth-nrt.wasm",
2276
+ wasmBaseURL,
2434
2277
  workletUrl: options.workletUrl || workerBaseURL + "scsynth_audio_worklet.js",
2435
2278
  workerBaseURL,
2436
- // Store for worker creation
2437
2279
  development: false,
2438
2280
  audioContextOptions: {
2439
2281
  latencyHint: "interactive",
2282
+ // hint to push for lowest latency possible
2440
2283
  sampleRate: 48e3
2284
+ // only requested rate - actual rate is determined by hardware
2441
2285
  },
2442
- // scsynth configuration (merged defaults + user overrides)
2443
- scsynth: scsynthConfig
2286
+ // Build-time memory layout (constant)
2287
+ memory: MemoryLayout,
2288
+ // Runtime world options (merged defaults + user overrides)
2289
+ worldOptions
2444
2290
  };
2445
- this.sampleBaseURL = options.sampleBaseURL || null;
2446
- this.synthdefBaseURL = options.synthdefBaseURL || null;
2447
- this.audioPathMap = options.audioPathMap || {};
2448
- this.allocatedBuffers = /* @__PURE__ */ new Map();
2449
- this.stats = {
2291
+ this.#sampleBaseURL = options.sampleBaseURL || null;
2292
+ this.#synthdefBaseURL = options.synthdefBaseURL || null;
2293
+ this.#audioPathMap = options.audioPathMap || {};
2294
+ this.bootStats = {
2450
2295
  initStartTime: null,
2451
- initDuration: null,
2452
- messagesSent: 0,
2453
- messagesReceived: 0,
2454
- errors: 0
2296
+ initDuration: null
2455
2297
  };
2456
2298
  }
2457
2299
  /**
2458
- * 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)
2459
2307
  */
2460
- checkCapabilities() {
2461
- this.capabilities = {
2462
- audioWorklet: "AudioWorklet" in window,
2308
+ get initializing() {
2309
+ return this.#initializing;
2310
+ }
2311
+ /**
2312
+ * Get browser capabilities (read-only)
2313
+ */
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",
2463
2323
  sharedArrayBuffer: typeof SharedArrayBuffer !== "undefined",
2464
2324
  crossOriginIsolated: window.crossOriginIsolated === true,
2465
- wasmThreads: typeof WebAssembly !== "undefined" && typeof WebAssembly.Memory !== "undefined" && WebAssembly.Memory.prototype.hasOwnProperty("shared"),
2466
2325
  atomics: typeof Atomics !== "undefined",
2467
2326
  webWorker: typeof Worker !== "undefined"
2468
2327
  };
@@ -2473,11 +2332,11 @@ ${error.message}`);
2473
2332
  "atomics",
2474
2333
  "webWorker"
2475
2334
  ];
2476
- const missing = required.filter((f) => !this.capabilities[f]);
2335
+ const missing = required.filter((f) => !this.#capabilities[f]);
2477
2336
  if (missing.length > 0) {
2478
2337
  const error = new Error(`Missing required features: ${missing.join(", ")}`);
2479
- if (!this.capabilities.crossOriginIsolated) {
2480
- if (this.capabilities.sharedArrayBuffer) {
2338
+ if (!this.#capabilities.crossOriginIsolated) {
2339
+ if (this.#capabilities.sharedArrayBuffer) {
2481
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";
2482
2341
  } else {
2483
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";
@@ -2485,113 +2344,61 @@ ${error.message}`);
2485
2344
  }
2486
2345
  throw error;
2487
2346
  }
2488
- return this.capabilities;
2347
+ return this.#capabilities;
2489
2348
  }
2490
2349
  /**
2491
- * Merge user-provided scsynth options with defaults
2350
+ * Merge user-provided world options with defaults
2492
2351
  * @private
2493
2352
  */
2494
- #mergeScsynthOptions(userOptions) {
2495
- const merged = {
2496
- memory: { ...ScsynthConfig.memory },
2497
- worldOptions: { ...ScsynthConfig.worldOptions }
2498
- };
2499
- if (userOptions.memory) {
2500
- Object.assign(merged.memory, userOptions.memory);
2501
- }
2502
- if (userOptions.worldOptions) {
2503
- Object.assign(merged.worldOptions, userOptions.worldOptions);
2504
- }
2505
- const topLevelKeys = Object.keys(userOptions).filter(
2506
- (key) => key !== "memory" && key !== "worldOptions"
2507
- );
2508
- if (topLevelKeys.length > 0) {
2509
- topLevelKeys.forEach((key) => {
2510
- if (key in merged.worldOptions) {
2511
- merged.worldOptions[key] = userOptions[key];
2512
- }
2513
- });
2514
- }
2515
- return merged;
2516
- }
2517
2353
  /**
2518
2354
  * Initialize shared WebAssembly memory
2519
2355
  */
2520
2356
  #initializeSharedMemory() {
2521
- const memConfig = this.config.scsynth.memory;
2522
- this.wasmMemory = new WebAssembly.Memory({
2357
+ const memConfig = this.config.memory;
2358
+ this.#wasmMemory = new WebAssembly.Memory({
2523
2359
  initial: memConfig.totalPages,
2524
2360
  maximum: memConfig.totalPages,
2525
2361
  shared: true
2526
2362
  });
2527
- this.sharedBuffer = this.wasmMemory.buffer;
2528
- this.bufferPool = new MemPool({
2529
- buf: this.sharedBuffer,
2530
- start: memConfig.bufferPoolOffset,
2531
- size: memConfig.bufferPoolSize,
2532
- align: 8
2533
- // 8-byte alignment (minimum required by MemPool)
2534
- });
2535
- const poolSizeMB = (memConfig.bufferPoolSize / (1024 * 1024)).toFixed(0);
2536
- const poolOffsetMB = (memConfig.bufferPoolOffset / (1024 * 1024)).toFixed(0);
2537
- console.log(`[SuperSonic] Buffer pool initialized: ${poolSizeMB}MB at offset ${poolOffsetMB}MB`);
2363
+ this.#sharedBuffer = this.#wasmMemory.buffer;
2538
2364
  }
2539
- /**
2540
- * Initialize AudioContext and set up time offset calculation
2541
- */
2542
2365
  #initializeAudioContext() {
2543
- this.audioContext = new (window.AudioContext || window.webkitAudioContext)(
2544
- this.config.audioContextOptions
2545
- );
2546
- this._timeOffsetPromise = new Promise((resolve) => {
2547
- this._resolveTimeOffset = resolve;
2548
- });
2549
- if (this.audioContext.state === "suspended") {
2550
- const resumeContext = async () => {
2551
- if (this.audioContext.state === "suspended") {
2552
- await this.audioContext.resume();
2553
- }
2554
- };
2555
- document.addEventListener("click", resumeContext, { once: true });
2556
- document.addEventListener("touchstart", resumeContext, { once: true });
2557
- }
2558
- return this.audioContext;
2366
+ this.#audioContext = new AudioContext(this.config.audioContextOptions);
2367
+ return this.#audioContext;
2559
2368
  }
2560
2369
  #initializeBufferManager() {
2561
- this.bufferManager = new BufferManager({
2562
- audioContext: this.audioContext,
2563
- sharedBuffer: this.sharedBuffer,
2564
- bufferPool: this.bufferPool,
2565
- allocatedBuffers: this.allocatedBuffers,
2566
- resolveAudioPath: (path) => this._resolveAudioPath(path),
2567
- registerPendingOp: (uuid, bufnum, timeoutMs) => this.#createPendingBufferOperation(uuid, bufnum, timeoutMs),
2568
- maxBuffers: this.config.scsynth.worldOptions.numBuffers
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
2569
2380
  });
2570
2381
  }
2571
- /**
2572
- * Load WASM manifest to get the current hashed filename
2573
- */
2574
2382
  async #loadWasmManifest() {
2383
+ const manifestUrl = this.config.wasmBaseURL + "manifest.json";
2575
2384
  try {
2576
- const wasmBaseURL = this.config.workerBaseURL.replace("/workers/", "/wasm/");
2577
- const manifestUrl = wasmBaseURL + "manifest.json";
2578
2385
  const response = await fetch(manifestUrl);
2579
- if (response.ok) {
2580
- const manifest = await response.json();
2581
- const wasmFile = manifest.wasmFile;
2582
- this.config.wasmUrl = wasmBaseURL + wasmFile;
2583
- console.log(`[SuperSonic] Using WASM build: ${wasmFile}`);
2584
- console.log(`[SuperSonic] Build: ${manifest.buildId} (git: ${manifest.gitHash})`);
2386
+ if (!response.ok) {
2387
+ return;
2585
2388
  }
2389
+ const manifest = await response.json();
2390
+ this.config.wasmUrl = this.config.wasmBaseURL + manifest.wasmFile;
2391
+ console.log(`[SuperSonic] WASM: ${manifest.wasmFile} (${manifest.buildId}, git: ${manifest.gitHash})`);
2586
2392
  } catch (error) {
2587
- console.warn("[SuperSonic] WASM manifest not found, using default filename");
2588
2393
  }
2589
2394
  }
2590
2395
  /**
2591
2396
  * Load WASM binary from network
2592
2397
  */
2593
2398
  async #loadWasm() {
2594
- await this.#loadWasmManifest();
2399
+ if (this.config.development) {
2400
+ await this.#loadWasmManifest();
2401
+ }
2595
2402
  const wasmResponse = await fetch(this.config.wasmUrl);
2596
2403
  if (!wasmResponse.ok) {
2597
2404
  throw new Error(`Failed to load WASM: ${wasmResponse.status} ${wasmResponse.statusText}`);
@@ -2602,23 +2409,23 @@ ${error.message}`);
2602
2409
  * Initialize AudioWorklet with WASM
2603
2410
  */
2604
2411
  async #initializeAudioWorklet(wasmBytes) {
2605
- await this.audioContext.audioWorklet.addModule(this.config.workletUrl);
2606
- 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", {
2607
2414
  numberOfInputs: 0,
2608
2415
  numberOfOutputs: 1,
2609
2416
  outputChannelCount: [2]
2610
2417
  });
2611
- this.workletNode.connect(this.audioContext.destination);
2612
- this.workletNode.port.postMessage({
2418
+ this.#workletNode.connect(this.#audioContext.destination);
2419
+ this.#workletNode.port.postMessage({
2613
2420
  type: "init",
2614
- sharedBuffer: this.sharedBuffer
2421
+ sharedBuffer: this.#sharedBuffer
2615
2422
  });
2616
- this.workletNode.port.postMessage({
2423
+ this.#workletNode.port.postMessage({
2617
2424
  type: "loadWasm",
2618
2425
  wasmBytes,
2619
- wasmMemory: this.wasmMemory,
2620
- worldOptions: this.config.scsynth.worldOptions,
2621
- sampleRate: this.audioContext.sampleRate
2426
+ wasmMemory: this.#wasmMemory,
2427
+ worldOptions: this.config.worldOptions,
2428
+ sampleRate: this.#audioContext.sampleRate
2622
2429
  // Pass actual AudioContext sample rate
2623
2430
  });
2624
2431
  await this.#waitForWorkletInit();
@@ -2627,55 +2434,55 @@ ${error.message}`);
2627
2434
  * Initialize OSC communication layer
2628
2435
  */
2629
2436
  async #initializeOSC() {
2630
- this.osc = new ScsynthOSC(this.config.workerBaseURL);
2631
- this.osc.onRawOSC((msg) => {
2437
+ this.#osc = new ScsynthOSC(this.config.workerBaseURL);
2438
+ this.#osc.onRawOSC((msg) => {
2632
2439
  if (this.onOSC) {
2633
2440
  this.onOSC(msg);
2634
2441
  }
2635
2442
  });
2636
- this.osc.onParsedOSC((msg) => {
2443
+ this.#osc.onParsedOSC((msg) => {
2637
2444
  if (msg.address === "/buffer/freed") {
2638
- this._handleBufferFreed(msg.args);
2445
+ this.#bufferManager?.handleBufferFreed(msg.args);
2639
2446
  } else if (msg.address === "/buffer/allocated") {
2640
- this._handleBufferAllocated(msg.args);
2447
+ this.#bufferManager?.handleBufferAllocated(msg.args);
2641
2448
  } else if (msg.address === "/synced" && msg.args.length > 0) {
2642
2449
  const syncId = msg.args[0];
2643
- if (this._syncListeners && this._syncListeners.has(syncId)) {
2644
- const listener = this._syncListeners.get(syncId);
2450
+ if (this.#syncListeners && this.#syncListeners.has(syncId)) {
2451
+ const listener = this.#syncListeners.get(syncId);
2645
2452
  listener(msg);
2646
2453
  }
2647
2454
  }
2648
2455
  if (this.onMessage) {
2649
- this.stats.messagesReceived++;
2456
+ this.#metrics_messagesReceived++;
2650
2457
  this.onMessage(msg);
2651
2458
  }
2652
2459
  });
2653
- this.osc.onDebugMessage((msg) => {
2460
+ this.#osc.onDebugMessage((msg) => {
2654
2461
  if (this.onDebugMessage) {
2655
2462
  this.onDebugMessage(msg);
2656
2463
  }
2657
2464
  });
2658
- this.osc.onError((error, workerName) => {
2465
+ this.#osc.onError((error, workerName) => {
2659
2466
  console.error(`[SuperSonic] ${workerName} error:`, error);
2660
- this.stats.errors++;
2467
+ this.#metrics_errors++;
2661
2468
  if (this.onError) {
2662
2469
  this.onError(new Error(`${workerName}: ${error}`));
2663
2470
  }
2664
2471
  });
2665
- await this.osc.init(this.sharedBuffer, this.ringBufferBase, this.bufferConstants);
2472
+ await this.#osc.init(this.#sharedBuffer, this.#ringBufferBase, this.#bufferConstants);
2666
2473
  }
2667
2474
  /**
2668
2475
  * Complete initialization and trigger callbacks
2669
2476
  */
2670
2477
  #finishInitialization() {
2671
- this.initialized = true;
2672
- this.initializing = false;
2673
- this.stats.initDuration = performance.now() - this.stats.initStartTime;
2674
- 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`);
2675
2482
  if (this.onInitialized) {
2676
2483
  this.onInitialized({
2677
- capabilities: this.capabilities,
2678
- stats: this.stats
2484
+ capabilities: this.#capabilities,
2485
+ bootStats: this.bootStats
2679
2486
  });
2680
2487
  }
2681
2488
  }
@@ -2688,11 +2495,11 @@ ${error.message}`);
2688
2495
  * @param {Object} config.audioContextOptions - AudioContext options
2689
2496
  */
2690
2497
  async init(config = {}) {
2691
- if (this.initialized) {
2498
+ if (this.#initialized) {
2692
2499
  console.warn("[SuperSonic] Already initialized");
2693
2500
  return;
2694
2501
  }
2695
- if (this.initializing) {
2502
+ if (this.#initializing) {
2696
2503
  console.warn("[SuperSonic] Initialization already in progress");
2697
2504
  return;
2698
2505
  }
@@ -2704,10 +2511,10 @@ ${error.message}`);
2704
2511
  ...config.audioContextOptions || {}
2705
2512
  }
2706
2513
  };
2707
- this.initializing = true;
2708
- this.stats.initStartTime = performance.now();
2514
+ this.#initializing = true;
2515
+ this.bootStats.initStartTime = performance.now();
2709
2516
  try {
2710
- this.checkCapabilities();
2517
+ this.setAndValidateCapabilities();
2711
2518
  this.#initializeSharedMemory();
2712
2519
  this.#initializeAudioContext();
2713
2520
  this.#initializeBufferManager();
@@ -2718,7 +2525,7 @@ ${error.message}`);
2718
2525
  this.#startPerformanceMonitoring();
2719
2526
  this.#finishInitialization();
2720
2527
  } catch (error) {
2721
- this.initializing = false;
2528
+ this.#initializing = false;
2722
2529
  console.error("[SuperSonic] Initialization failed:", error);
2723
2530
  if (this.onError) {
2724
2531
  this.onError(error);
@@ -2734,37 +2541,32 @@ ${error.message}`);
2734
2541
  const timeout = setTimeout(() => {
2735
2542
  reject(new Error("AudioWorklet initialization timeout"));
2736
2543
  }, 5e3);
2737
- const messageHandler = (event) => {
2544
+ const messageHandler = async (event) => {
2738
2545
  if (event.data.type === "debug") {
2739
2546
  return;
2740
2547
  }
2741
2548
  if (event.data.type === "error") {
2742
2549
  console.error("[AudioWorklet] Error:", event.data.error);
2743
2550
  clearTimeout(timeout);
2744
- this.workletNode.port.removeEventListener("message", messageHandler);
2551
+ this.#workletNode.port.removeEventListener("message", messageHandler);
2745
2552
  reject(new Error(event.data.error || "AudioWorklet error"));
2746
2553
  return;
2747
2554
  }
2748
2555
  if (event.data.type === "initialized") {
2749
2556
  clearTimeout(timeout);
2750
- this.workletNode.port.removeEventListener("message", messageHandler);
2557
+ this.#workletNode.port.removeEventListener("message", messageHandler);
2751
2558
  if (event.data.success) {
2752
2559
  if (event.data.ringBufferBase !== void 0) {
2753
- this.ringBufferBase = event.data.ringBufferBase;
2560
+ this.#ringBufferBase = event.data.ringBufferBase;
2754
2561
  } else {
2755
2562
  console.warn("[SuperSonic] Warning: ringBufferBase not provided by worklet");
2756
2563
  }
2757
2564
  if (event.data.bufferConstants !== void 0) {
2758
2565
  console.log("[SuperSonic] Received bufferConstants from worklet");
2759
- this.bufferConstants = event.data.bufferConstants;
2760
- console.log("[SuperSonic] Initializing NTP timing");
2761
- this.initializeNTPTiming();
2566
+ this.#bufferConstants = event.data.bufferConstants;
2567
+ console.log("[SuperSonic] Initializing NTP timing (waiting for audio to flow)...");
2568
+ await this.initializeNTPTiming();
2762
2569
  this.#startDriftOffsetTimer();
2763
- console.log("[SuperSonic] Resolving time offset promise, _resolveTimeOffset=", this._resolveTimeOffset);
2764
- if (this._resolveTimeOffset) {
2765
- this._resolveTimeOffset();
2766
- this._resolveTimeOffset = null;
2767
- }
2768
2570
  } else {
2769
2571
  console.warn("[SuperSonic] Warning: bufferConstants not provided by worklet");
2770
2572
  }
@@ -2775,34 +2577,24 @@ ${error.message}`);
2775
2577
  }
2776
2578
  }
2777
2579
  };
2778
- this.workletNode.port.addEventListener("message", messageHandler);
2779
- this.workletNode.port.start();
2580
+ this.#workletNode.port.addEventListener("message", messageHandler);
2581
+ this.#workletNode.port.start();
2780
2582
  });
2781
2583
  }
2782
2584
  /**
2783
2585
  * Set up message handlers for worklet
2784
2586
  */
2785
2587
  #setupMessageHandlers() {
2786
- this.workletNode.port.onmessage = (event) => {
2588
+ this.#workletNode.port.onmessage = (event) => {
2787
2589
  const { data } = event;
2788
2590
  switch (data.type) {
2789
- case "status":
2790
- if (this.onStatusUpdate) {
2791
- this.onStatusUpdate(data);
2792
- }
2793
- break;
2794
- case "metrics":
2795
- if (this.onMetricsUpdate) {
2796
- this.onMetricsUpdate(data.metrics);
2797
- }
2798
- break;
2799
2591
  case "error":
2800
2592
  console.error("[Worklet] Error:", data.error);
2801
2593
  if (data.diagnostics) {
2802
2594
  console.error("[Worklet] Diagnostics:", data.diagnostics);
2803
2595
  console.table(data.diagnostics);
2804
2596
  }
2805
- this.stats.errors++;
2597
+ this.#metrics_errors++;
2806
2598
  if (this.onError) {
2807
2599
  this.onError(new Error(data.error));
2808
2600
  }
@@ -2825,21 +2617,173 @@ ${error.message}`);
2825
2617
  };
2826
2618
  }
2827
2619
  /**
2828
- * 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
2829
2756
  */
2830
2757
  #startPerformanceMonitoring() {
2831
- setInterval(() => {
2832
- if (this.osc) {
2833
- this.osc.getStats().then((stats) => {
2834
- if (stats && this.onMetricsUpdate) {
2835
- this.onMetricsUpdate(stats);
2836
- }
2837
- });
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;
2838
2766
  }
2839
- if (this.workletNode) {
2840
- 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;
2841
2775
  }
2842
- }, 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
+ }
2843
2787
  }
2844
2788
  /**
2845
2789
  * Send OSC message with simplified syntax (auto-detects types)
@@ -2867,73 +2811,11 @@ ${error.message}`);
2867
2811
  const oscData = _SuperSonic.osc.encode(message);
2868
2812
  return this.sendOSC(oscData);
2869
2813
  }
2870
- /**
2871
- * Resolve audio file path to full URL
2872
- */
2873
- _resolveAudioPath(scPath) {
2874
- if (this.audioPathMap[scPath]) {
2875
- return this.audioPathMap[scPath];
2876
- }
2877
- if (!this.sampleBaseURL) {
2878
- throw new Error(
2879
- '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'
2880
- );
2881
- }
2882
- return this.sampleBaseURL + scPath;
2883
- }
2884
2814
  #ensureInitialized(actionDescription = "perform this operation") {
2885
- if (!this.initialized) {
2815
+ if (!this.#initialized) {
2886
2816
  throw new Error(`SuperSonic not initialized. Call init() before attempting to ${actionDescription}.`);
2887
2817
  }
2888
2818
  }
2889
- #createPendingBufferOperation(uuid, bufnum, timeoutMs = 3e4) {
2890
- return new Promise((resolve, reject) => {
2891
- const timeout = setTimeout(() => {
2892
- this.pendingBufferOps.delete(uuid);
2893
- reject(new Error(`Buffer ${bufnum} allocation timeout (${timeoutMs}ms)`));
2894
- }, timeoutMs);
2895
- this.pendingBufferOps.set(uuid, { resolve, reject, timeout });
2896
- });
2897
- }
2898
- /**
2899
- * Handle /buffer/freed message from WASM
2900
- */
2901
- _handleBufferFreed(args) {
2902
- const bufnum = args[0];
2903
- const freedPtr = args[1];
2904
- const bufferInfo = this.allocatedBuffers.get(bufnum);
2905
- if (!bufferInfo) {
2906
- if (typeof freedPtr === "number" && freedPtr !== 0) {
2907
- this.bufferPool.free(freedPtr);
2908
- }
2909
- return;
2910
- }
2911
- if (typeof freedPtr === "number" && freedPtr === bufferInfo.ptr) {
2912
- this.bufferPool.free(bufferInfo.ptr);
2913
- this.allocatedBuffers.delete(bufnum);
2914
- return;
2915
- }
2916
- if (typeof freedPtr === "number" && bufferInfo.previousAllocation && bufferInfo.previousAllocation.ptr === freedPtr) {
2917
- this.bufferPool.free(freedPtr);
2918
- bufferInfo.previousAllocation = null;
2919
- return;
2920
- }
2921
- this.bufferPool.free(bufferInfo.ptr);
2922
- this.allocatedBuffers.delete(bufnum);
2923
- }
2924
- /**
2925
- * Handle /buffer/allocated message with UUID correlation
2926
- */
2927
- _handleBufferAllocated(args) {
2928
- const uuid = args[0];
2929
- const bufnum = args[1];
2930
- const pending = this.pendingBufferOps.get(uuid);
2931
- if (pending) {
2932
- clearTimeout(pending.timeout);
2933
- pending.resolve({ bufnum });
2934
- this.pendingBufferOps.delete(uuid);
2935
- }
2936
- }
2937
2819
  /**
2938
2820
  * Send pre-encoded OSC bytes to scsynth
2939
2821
  * @param {ArrayBuffer|Uint8Array} oscData - Pre-encoded OSC data
@@ -2943,7 +2825,7 @@ ${error.message}`);
2943
2825
  this.#ensureInitialized("send OSC data");
2944
2826
  const uint8Data = this.#toUint8Array(oscData);
2945
2827
  const preparedData = await this.#prepareOutboundPacket(uint8Data);
2946
- this.stats.messagesSent++;
2828
+ this.#metrics_messagesSent++;
2947
2829
  if (this.onMessageSent) {
2948
2830
  this.onMessageSent(preparedData);
2949
2831
  }
@@ -2953,17 +2835,38 @@ ${error.message}`);
2953
2835
  sendOptions.audioTimeS = timing.audioTimeS;
2954
2836
  sendOptions.currentTimeS = timing.currentTimeS;
2955
2837
  }
2956
- 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;
2957
2860
  }
2958
2861
  /**
2959
2862
  * Get current status
2960
2863
  */
2961
2864
  getStatus() {
2962
2865
  return {
2963
- initialized: this.initialized,
2964
- capabilities: this.capabilities,
2965
- stats: this.stats,
2966
- audioContextState: this.audioContext?.state
2866
+ initialized: this.#initialized,
2867
+ capabilities: this.#capabilities,
2868
+ bootStats: this.bootStats,
2869
+ audioContextState: this.#audioContext?.state
2967
2870
  };
2968
2871
  }
2969
2872
  /**
@@ -2976,12 +2879,12 @@ ${error.message}`);
2976
2879
  * console.log('Memory layout:', config.memory);
2977
2880
  */
2978
2881
  getConfig() {
2979
- if (!this.config?.scsynth) {
2882
+ if (!this.config) {
2980
2883
  return null;
2981
2884
  }
2982
2885
  return {
2983
- memory: { ...this.config.scsynth.memory },
2984
- worldOptions: { ...this.config.scsynth.worldOptions }
2886
+ memory: { ...this.config.memory },
2887
+ worldOptions: { ...this.config.worldOptions }
2985
2888
  };
2986
2889
  }
2987
2890
  /**
@@ -2990,42 +2893,36 @@ ${error.message}`);
2990
2893
  async destroy() {
2991
2894
  console.log("[SuperSonic] Destroying...");
2992
2895
  this.#stopDriftOffsetTimer();
2993
- if (this.osc) {
2994
- this.osc.terminate();
2995
- this.osc = null;
2896
+ this.#stopPerformanceMonitoring();
2897
+ if (this.#osc) {
2898
+ this.#osc.terminate();
2899
+ this.#osc = null;
2996
2900
  }
2997
- if (this.workletNode) {
2998
- this.workletNode.disconnect();
2999
- this.workletNode = null;
2901
+ if (this.#workletNode) {
2902
+ this.#workletNode.disconnect();
2903
+ this.#workletNode = null;
3000
2904
  }
3001
- if (this.audioContext) {
3002
- await this.audioContext.close();
3003
- this.audioContext = null;
2905
+ if (this.#audioContext) {
2906
+ await this.#audioContext.close();
2907
+ this.#audioContext = null;
3004
2908
  }
3005
- for (const [uuid, pending] of this.pendingBufferOps.entries()) {
3006
- clearTimeout(pending.timeout);
3007
- pending.reject(new Error("SuperSonic instance destroyed"));
2909
+ if (this.#bufferManager) {
2910
+ this.#bufferManager.destroy();
2911
+ this.#bufferManager = null;
3008
2912
  }
3009
- this.pendingBufferOps.clear();
3010
- this.sharedBuffer = null;
3011
- this.initialized = false;
3012
- this.bufferManager = null;
3013
- this.allocatedBuffers.clear();
2913
+ this.#sharedBuffer = null;
2914
+ this.#initialized = false;
3014
2915
  this.loadedSynthDefs.clear();
3015
2916
  console.log("[SuperSonic] Destroyed");
3016
2917
  }
3017
2918
  /**
3018
- * Wait until NTP timing has been established.
3019
- * Note: NTP calculation is now done internally in C++ process_audio().
3020
- * 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
3021
2922
  */
3022
- async waitForTimeSync() {
3023
- if (!this.bufferConstants) {
3024
- if (this._timeOffsetPromise) {
3025
- await this._timeOffsetPromise;
3026
- }
3027
- }
3028
- 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);
3029
2926
  return ntpStartView[0];
3030
2927
  }
3031
2928
  /**
@@ -3061,7 +2958,7 @@ ${error.message}`);
3061
2958
  * await sonic.loadSynthDef('./extra/synthdefs/sonic-pi-beep.scsyndef');
3062
2959
  */
3063
2960
  async loadSynthDef(path) {
3064
- if (!this.initialized) {
2961
+ if (!this.#initialized) {
3065
2962
  throw new Error("SuperSonic not initialized. Call init() first.");
3066
2963
  }
3067
2964
  try {
@@ -3090,10 +2987,10 @@ ${error.message}`);
3090
2987
  * const results = await sonic.loadSynthDefs(['sonic-pi-beep', 'sonic-pi-tb303']);
3091
2988
  */
3092
2989
  async loadSynthDefs(names) {
3093
- if (!this.initialized) {
2990
+ if (!this.#initialized) {
3094
2991
  throw new Error("SuperSonic not initialized. Call init() first.");
3095
2992
  }
3096
- if (!this.synthdefBaseURL) {
2993
+ if (!this.#synthdefBaseURL) {
3097
2994
  throw new Error(
3098
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'
3099
2996
  );
@@ -3102,7 +2999,7 @@ ${error.message}`);
3102
2999
  await Promise.all(
3103
3000
  names.map(async (name) => {
3104
3001
  try {
3105
- const path = `${this.synthdefBaseURL}${name}.scsyndef`;
3002
+ const path = `${this.#synthdefBaseURL}${name}.scsyndef`;
3106
3003
  await this.loadSynthDef(path);
3107
3004
  results[name] = { success: true };
3108
3005
  } catch (error) {
@@ -3125,7 +3022,7 @@ ${error.message}`);
3125
3022
  * await sonic.sync(12345); // Wait for all synthdefs to be processed
3126
3023
  */
3127
3024
  async sync(syncId) {
3128
- if (!this.initialized) {
3025
+ if (!this.#initialized) {
3129
3026
  throw new Error("SuperSonic not initialized. Call init() first.");
3130
3027
  }
3131
3028
  if (!Number.isInteger(syncId)) {
@@ -3133,20 +3030,20 @@ ${error.message}`);
3133
3030
  }
3134
3031
  const syncPromise = new Promise((resolve, reject) => {
3135
3032
  const timeout = setTimeout(() => {
3136
- if (this._syncListeners) {
3137
- this._syncListeners.delete(syncId);
3033
+ if (this.#syncListeners) {
3034
+ this.#syncListeners.delete(syncId);
3138
3035
  }
3139
3036
  reject(new Error("Timeout waiting for /synced response"));
3140
3037
  }, 1e4);
3141
3038
  const messageHandler = (msg) => {
3142
3039
  clearTimeout(timeout);
3143
- this._syncListeners.delete(syncId);
3040
+ this.#syncListeners.delete(syncId);
3144
3041
  resolve();
3145
3042
  };
3146
- if (!this._syncListeners) {
3147
- this._syncListeners = /* @__PURE__ */ new Map();
3043
+ if (!this.#syncListeners) {
3044
+ this.#syncListeners = /* @__PURE__ */ new Map();
3148
3045
  }
3149
- this._syncListeners.set(syncId, messageHandler);
3046
+ this.#syncListeners.set(syncId, messageHandler);
3150
3047
  });
3151
3048
  await this.send("/sync", syncId);
3152
3049
  await syncPromise;
@@ -3159,15 +3056,8 @@ ${error.message}`);
3159
3056
  * const bufferAddr = sonic.allocBuffer(44100); // Allocate 1 second at 44.1kHz
3160
3057
  */
3161
3058
  allocBuffer(numSamples) {
3162
- if (!this.initialized) {
3163
- throw new Error("SuperSonic not initialized. Call init() first.");
3164
- }
3165
- const sizeBytes = numSamples * 4;
3166
- const addr = this.bufferPool.malloc(sizeBytes);
3167
- if (addr === 0) {
3168
- console.error(`[SuperSonic] Buffer allocation failed: ${numSamples} samples (${sizeBytes} bytes)`);
3169
- }
3170
- return addr;
3059
+ this.#ensureInitialized("allocate buffers");
3060
+ return this.#bufferManager.allocate(numSamples);
3171
3061
  }
3172
3062
  /**
3173
3063
  * Free a previously allocated buffer
@@ -3177,10 +3067,8 @@ ${error.message}`);
3177
3067
  * sonic.freeBuffer(bufferAddr);
3178
3068
  */
3179
3069
  freeBuffer(addr) {
3180
- if (!this.initialized) {
3181
- throw new Error("SuperSonic not initialized. Call init() first.");
3182
- }
3183
- return this.bufferPool.free(addr);
3070
+ this.#ensureInitialized("free buffers");
3071
+ return this.#bufferManager.free(addr);
3184
3072
  }
3185
3073
  /**
3186
3074
  * Get a Float32Array view of an allocated buffer
@@ -3192,10 +3080,8 @@ ${error.message}`);
3192
3080
  * view[0] = 1.0; // Write to buffer
3193
3081
  */
3194
3082
  getBufferView(addr, numSamples) {
3195
- if (!this.initialized) {
3196
- throw new Error("SuperSonic not initialized. Call init() first.");
3197
- }
3198
- return new Float32Array(this.sharedBuffer, addr, numSamples);
3083
+ this.#ensureInitialized("get buffer views");
3084
+ return this.#bufferManager.getView(addr, numSamples);
3199
3085
  }
3200
3086
  /**
3201
3087
  * Get buffer pool statistics
@@ -3205,37 +3091,13 @@ ${error.message}`);
3205
3091
  * console.log(`Available: ${stats.available} bytes`);
3206
3092
  */
3207
3093
  getBufferPoolStats() {
3208
- if (!this.initialized) {
3209
- throw new Error("SuperSonic not initialized. Call init() first.");
3210
- }
3211
- return this.bufferPool.stats();
3094
+ this.#ensureInitialized("get buffer pool stats");
3095
+ return this.#bufferManager.getStats();
3212
3096
  }
3213
3097
  getDiagnostics() {
3214
3098
  this.#ensureInitialized("get diagnostics");
3215
- const poolStats = this.bufferPool?.stats ? this.bufferPool.stats() : null;
3216
- let bytesActive = 0;
3217
- let pendingCount = 0;
3218
- for (const entry of this.allocatedBuffers.values()) {
3219
- if (!entry) continue;
3220
- bytesActive += entry.size || 0;
3221
- if (entry.pendingToken) {
3222
- pendingCount++;
3223
- }
3224
- }
3225
3099
  return {
3226
- buffers: {
3227
- active: this.allocatedBuffers.size,
3228
- pending: pendingCount,
3229
- bytesActive,
3230
- pool: poolStats ? {
3231
- total: poolStats.total || 0,
3232
- available: poolStats.available || 0,
3233
- freeBytes: poolStats.free?.size || 0,
3234
- freeBlocks: poolStats.free?.count || 0,
3235
- usedBytes: poolStats.used?.size || 0,
3236
- usedBlocks: poolStats.used?.count || 0
3237
- } : null
3238
- },
3100
+ buffers: this.#bufferManager.getDiagnostics(),
3239
3101
  synthdefs: {
3240
3102
  count: this.loadedSynthDefs.size
3241
3103
  }
@@ -3244,24 +3106,32 @@ ${error.message}`);
3244
3106
  /**
3245
3107
  * Initialize NTP timing (write-once)
3246
3108
  * Sets the NTP start time when AudioContext started
3109
+ * Blocks until audio is actually flowing (contextTime > 0)
3247
3110
  * @private
3248
3111
  */
3249
- initializeNTPTiming() {
3250
- if (!this.bufferConstants || !this.audioContext) {
3112
+ async initializeNTPTiming() {
3113
+ if (!this.#bufferConstants || !this.#audioContext) {
3251
3114
  return;
3252
3115
  }
3253
- const perfTimeMs = performance.timeOrigin + performance.now();
3116
+ let timestamp;
3117
+ while (true) {
3118
+ timestamp = this.#audioContext.getOutputTimestamp();
3119
+ if (timestamp.contextTime > 0) {
3120
+ break;
3121
+ }
3122
+ await new Promise((resolve) => setTimeout(resolve, 50));
3123
+ }
3124
+ const perfTimeMs = performance.timeOrigin + timestamp.performanceTime;
3254
3125
  const currentNTP = perfTimeMs / 1e3 + NTP_EPOCH_OFFSET;
3255
- const currentAudioCtx = this.audioContext.currentTime;
3256
- const ntpStartTime = currentNTP - currentAudioCtx;
3126
+ const ntpStartTime = currentNTP - timestamp.contextTime;
3257
3127
  const ntpStartView = new Float64Array(
3258
- this.sharedBuffer,
3259
- this.ringBufferBase + this.bufferConstants.NTP_START_TIME_START,
3128
+ this.#sharedBuffer,
3129
+ this.#ringBufferBase + this.#bufferConstants.NTP_START_TIME_START,
3260
3130
  1
3261
3131
  );
3262
3132
  ntpStartView[0] = ntpStartTime;
3263
- this._initialNTPStartTime = ntpStartTime;
3264
- console.log(`[SuperSonic] NTP timing initialized: start=${ntpStartTime.toFixed(6)}s (current NTP=${currentNTP.toFixed(3)}, AudioCtx=${currentAudioCtx.toFixed(3)}), ringBufferBase=${this.ringBufferBase}`);
3133
+ this.#initialNTPStartTime = ntpStartTime;
3134
+ console.log(`[SuperSonic] NTP timing initialized: start=${ntpStartTime.toFixed(6)}s (NTP=${currentNTP.toFixed(3)}s, contextTime=${timestamp.contextTime.toFixed(3)}s)`);
3265
3135
  }
3266
3136
  /**
3267
3137
  * Update drift offset (AudioContext → NTP drift correction)
@@ -3269,34 +3139,34 @@ ${error.message}`);
3269
3139
  * @private
3270
3140
  */
3271
3141
  updateDriftOffset() {
3272
- if (!this.bufferConstants || !this.audioContext || this._initialNTPStartTime === void 0) {
3142
+ if (!this.#bufferConstants || !this.#audioContext || this.#initialNTPStartTime === void 0) {
3273
3143
  return;
3274
3144
  }
3275
- const perfTimeMs = performance.timeOrigin + performance.now();
3145
+ const timestamp = this.#audioContext.getOutputTimestamp();
3146
+ const perfTimeMs = performance.timeOrigin + timestamp.performanceTime;
3276
3147
  const currentNTP = perfTimeMs / 1e3 + NTP_EPOCH_OFFSET;
3277
- const currentAudioCtx = this.audioContext.currentTime;
3278
- const currentNTPStartTime = currentNTP - currentAudioCtx;
3279
- const driftSeconds = currentNTPStartTime - this._initialNTPStartTime;
3148
+ const expectedContextTime = currentNTP - this.#initialNTPStartTime;
3149
+ const driftSeconds = expectedContextTime - timestamp.contextTime;
3280
3150
  const driftMs = Math.round(driftSeconds * 1e3);
3281
3151
  const driftView = new Int32Array(
3282
- this.sharedBuffer,
3283
- this.ringBufferBase + this.bufferConstants.DRIFT_OFFSET_START,
3152
+ this.#sharedBuffer,
3153
+ this.#ringBufferBase + this.#bufferConstants.DRIFT_OFFSET_START,
3284
3154
  1
3285
3155
  );
3286
3156
  Atomics.store(driftView, 0, driftMs);
3287
- console.log(`[SuperSonic] Drift offset updated: ${driftMs}ms (current NTP start=${currentNTPStartTime.toFixed(6)}, initial=${this._initialNTPStartTime.toFixed(6)})`);
3157
+ console.log(`[SuperSonic] Drift offset: ${driftMs}ms (expected=${expectedContextTime.toFixed(3)}s, actual=${timestamp.contextTime.toFixed(3)}s, NTP=${currentNTP.toFixed(3)}s)`);
3288
3158
  }
3289
3159
  /**
3290
3160
  * Get current drift offset in milliseconds
3291
3161
  * @returns {number} Current drift in milliseconds
3292
3162
  */
3293
3163
  getDriftOffset() {
3294
- if (!this.bufferConstants) {
3164
+ if (!this.#bufferConstants) {
3295
3165
  return 0;
3296
3166
  }
3297
3167
  const driftView = new Int32Array(
3298
- this.sharedBuffer,
3299
- this.ringBufferBase + this.bufferConstants.DRIFT_OFFSET_START,
3168
+ this.#sharedBuffer,
3169
+ this.#ringBufferBase + this.#bufferConstants.DRIFT_OFFSET_START,
3300
3170
  1
3301
3171
  );
3302
3172
  return Atomics.load(driftView, 0);
@@ -3307,7 +3177,7 @@ ${error.message}`);
3307
3177
  */
3308
3178
  #startDriftOffsetTimer() {
3309
3179
  this.#stopDriftOffsetTimer();
3310
- this._driftOffsetTimer = setInterval(() => {
3180
+ this.#driftOffsetTimer = setInterval(() => {
3311
3181
  this.updateDriftOffset();
3312
3182
  }, DRIFT_UPDATE_INTERVAL_MS);
3313
3183
  console.log(`[SuperSonic] Started drift offset correction (every ${DRIFT_UPDATE_INTERVAL_MS}ms)`);
@@ -3317,9 +3187,9 @@ ${error.message}`);
3317
3187
  * @private
3318
3188
  */
3319
3189
  #stopDriftOffsetTimer() {
3320
- if (this._driftOffsetTimer) {
3321
- clearInterval(this._driftOffsetTimer);
3322
- this._driftOffsetTimer = null;
3190
+ if (this.#driftOffsetTimer) {
3191
+ clearInterval(this.#driftOffsetTimer);
3192
+ this.#driftOffsetTimer = null;
3323
3193
  }
3324
3194
  }
3325
3195
  #extractSynthDefName(path) {
@@ -3441,7 +3311,7 @@ ${error.message}`);
3441
3311
  const numFrames = this.#requireIntArg(message.args, 1, "/b_alloc requires a frame count");
3442
3312
  let argIndex = 2;
3443
3313
  let numChannels = 1;
3444
- let sampleRate = this.audioContext?.sampleRate || 44100;
3314
+ let sampleRate = this.#audioContext?.sampleRate || 44100;
3445
3315
  if (this.#isNumericArg(this.#argAt(message.args, argIndex))) {
3446
3316
  numChannels = Math.max(1, this.#optionalIntArg(message.args, argIndex, 1));
3447
3317
  argIndex++;
@@ -3532,10 +3402,10 @@ ${error.message}`);
3532
3402
  });
3533
3403
  }
3534
3404
  #requireBufferManager() {
3535
- if (!this.bufferManager) {
3405
+ if (!this.#bufferManager) {
3536
3406
  throw new Error("Buffer manager not ready. Call init() before issuing buffer commands.");
3537
3407
  }
3538
- return this.bufferManager;
3408
+ return this.#bufferManager;
3539
3409
  }
3540
3410
  #isBundle(packet) {
3541
3411
  return packet && packet.timeTag !== void 0 && Array.isArray(packet.packets);
@@ -3548,16 +3418,16 @@ ${error.message}`);
3548
3418
  if (header !== "#bundle\0") {
3549
3419
  return null;
3550
3420
  }
3551
- const ntpStartView = new Float64Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.NTP_START_TIME_START, 1);
3421
+ const ntpStartView = new Float64Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.NTP_START_TIME_START, 1);
3552
3422
  const ntpStartTime = ntpStartView[0];
3553
3423
  if (ntpStartTime === 0) {
3554
3424
  console.warn("[SuperSonic] NTP start time not yet initialized");
3555
3425
  return null;
3556
3426
  }
3557
- const driftView = new Int32Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.DRIFT_OFFSET_START, 1);
3427
+ const driftView = new Int32Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.DRIFT_OFFSET_START, 1);
3558
3428
  const driftMs = Atomics.load(driftView, 0);
3559
3429
  const driftSeconds = driftMs / 1e3;
3560
- const globalView = new Int32Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.GLOBAL_OFFSET_START, 1);
3430
+ const globalView = new Int32Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.GLOBAL_OFFSET_START, 1);
3561
3431
  const globalMs = Atomics.load(globalView, 0);
3562
3432
  const globalSeconds = globalMs / 1e3;
3563
3433
  const totalOffset = ntpStartTime + driftSeconds + globalSeconds;
@@ -3569,7 +3439,7 @@ ${error.message}`);
3569
3439
  }
3570
3440
  const ntpTimeS = ntpSeconds + ntpFraction / 4294967296;
3571
3441
  const audioTimeS = ntpTimeS - totalOffset;
3572
- const currentTimeS = this.audioContext.currentTime;
3442
+ const currentTimeS = this.#audioContext.currentTime;
3573
3443
  return { audioTimeS, currentTimeS };
3574
3444
  }
3575
3445
  };