supersonic-scsynth 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1605,6 +1605,504 @@ var __blockSelfAddress = (dataAddress) => dataAddress > 0 ? dataAddress - SIZEOF
1605
1605
  var NTP_EPOCH_OFFSET = 2208988800;
1606
1606
  var DRIFT_UPDATE_INTERVAL_MS = 15e3;
1607
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
+ }
1856
+ }
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
+ );
1864
+ }
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
+ }
1872
+ }
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
+ }
1886
+ }
1887
+ this.#validateCombinations(worldOptions2);
1888
+ if (memoryConfig) {
1889
+ const heapSize = memoryConfig.wasmHeapSize;
1890
+ if (heapSize) {
1891
+ MemoryEstimator.validateHeapFits(worldOptions2, heapSize);
1892
+ }
1893
+ }
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");
1903
+ }
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
+ );
1909
+ }
1910
+ if (!Number.isFinite(bufferPoolOffset) || bufferPoolOffset <= 0) {
1911
+ throw new Error(
1912
+ `memory.bufferPoolOffset must be a positive number, got ${bufferPoolOffset}`
1913
+ );
1914
+ }
1915
+ if (!Number.isFinite(bufferPoolSize) || bufferPoolSize <= 0) {
1916
+ throw new Error(
1917
+ `memory.bufferPoolSize must be a positive number, got ${bufferPoolSize}`
1918
+ );
1919
+ }
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
+ );
1929
+ }
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
+ );
1937
+ }
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
+ );
1943
+ }
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.`
1949
+ );
1950
+ }
1951
+ }
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;
1992
+ }
1993
+ /**
1994
+ * Validate compatible combinations of options
1995
+ * @private
1996
+ */
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
+
1608
2106
  // js/supersonic.js
1609
2107
  var BufferManager = class {
1610
2108
  constructor(options) {
@@ -1614,7 +2112,8 @@ var BufferManager = class {
1614
2112
  bufferPool,
1615
2113
  allocatedBuffers,
1616
2114
  resolveAudioPath,
1617
- registerPendingOp
2115
+ registerPendingOp,
2116
+ maxBuffers = 1024
1618
2117
  } = options;
1619
2118
  this.audioContext = audioContext;
1620
2119
  this.sharedBuffer = sharedBuffer;
@@ -1625,7 +2124,7 @@ var BufferManager = class {
1625
2124
  this.bufferLocks = /* @__PURE__ */ new Map();
1626
2125
  this.GUARD_BEFORE = 3;
1627
2126
  this.GUARD_AFTER = 1;
1628
- this.MAX_BUFFERS = 1024;
2127
+ this.MAX_BUFFERS = maxBuffers;
1629
2128
  }
1630
2129
  #validateBufferNumber(bufnum) {
1631
2130
  if (!Number.isInteger(bufnum) || bufnum < 0 || bufnum >= this.MAX_BUFFERS) {
@@ -1922,6 +2421,14 @@ var SuperSonic = class _SuperSonic {
1922
2421
  }
1923
2422
  const workerBaseURL = options.workerBaseURL;
1924
2423
  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
+ }
1925
2432
  this.config = {
1926
2433
  wasmUrl: options.wasmUrl || wasmBaseURL + "scsynth-nrt.wasm",
1927
2434
  workletUrl: options.workletUrl || workerBaseURL + "scsynth_audio_worklet.js",
@@ -1931,7 +2438,9 @@ var SuperSonic = class _SuperSonic {
1931
2438
  audioContextOptions: {
1932
2439
  latencyHint: "interactive",
1933
2440
  sampleRate: 48e3
1934
- }
2441
+ },
2442
+ // scsynth configuration (merged defaults + user overrides)
2443
+ scsynth: scsynthConfig
1935
2444
  };
1936
2445
  this.sampleBaseURL = options.sampleBaseURL || null;
1937
2446
  this.synthdefBaseURL = options.synthdefBaseURL || null;
@@ -1978,27 +2487,54 @@ var SuperSonic = class _SuperSonic {
1978
2487
  }
1979
2488
  return this.capabilities;
1980
2489
  }
2490
+ /**
2491
+ * Merge user-provided scsynth options with defaults
2492
+ * @private
2493
+ */
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
+ }
1981
2517
  /**
1982
2518
  * Initialize shared WebAssembly memory
1983
2519
  */
1984
2520
  #initializeSharedMemory() {
1985
- const TOTAL_PAGES = 3072;
2521
+ const memConfig = this.config.scsynth.memory;
1986
2522
  this.wasmMemory = new WebAssembly.Memory({
1987
- initial: TOTAL_PAGES,
1988
- maximum: TOTAL_PAGES,
2523
+ initial: memConfig.totalPages,
2524
+ maximum: memConfig.totalPages,
1989
2525
  shared: true
1990
2526
  });
1991
2527
  this.sharedBuffer = this.wasmMemory.buffer;
1992
- const BUFFER_POOL_OFFSET = 64 * 1024 * 1024;
1993
- const BUFFER_POOL_SIZE = 128 * 1024 * 1024;
1994
2528
  this.bufferPool = new MemPool({
1995
2529
  buf: this.sharedBuffer,
1996
- start: BUFFER_POOL_OFFSET,
1997
- size: BUFFER_POOL_SIZE,
2530
+ start: memConfig.bufferPoolOffset,
2531
+ size: memConfig.bufferPoolSize,
1998
2532
  align: 8
1999
2533
  // 8-byte alignment (minimum required by MemPool)
2000
2534
  });
2001
- console.log("[SuperSonic] Buffer pool initialized: 128MB at offset 64MB");
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`);
2002
2538
  }
2003
2539
  /**
2004
2540
  * Initialize AudioContext and set up time offset calculation
@@ -2028,7 +2564,8 @@ var SuperSonic = class _SuperSonic {
2028
2564
  bufferPool: this.bufferPool,
2029
2565
  allocatedBuffers: this.allocatedBuffers,
2030
2566
  resolveAudioPath: (path) => this._resolveAudioPath(path),
2031
- registerPendingOp: (uuid, bufnum, timeoutMs) => this.#createPendingBufferOperation(uuid, bufnum, timeoutMs)
2567
+ registerPendingOp: (uuid, bufnum, timeoutMs) => this.#createPendingBufferOperation(uuid, bufnum, timeoutMs),
2568
+ maxBuffers: this.config.scsynth.worldOptions.numBuffers
2032
2569
  });
2033
2570
  }
2034
2571
  /**
@@ -2079,7 +2616,10 @@ var SuperSonic = class _SuperSonic {
2079
2616
  this.workletNode.port.postMessage({
2080
2617
  type: "loadWasm",
2081
2618
  wasmBytes,
2082
- wasmMemory: this.wasmMemory
2619
+ wasmMemory: this.wasmMemory,
2620
+ worldOptions: this.config.scsynth.worldOptions,
2621
+ sampleRate: this.audioContext.sampleRate
2622
+ // Pass actual AudioContext sample rate
2083
2623
  });
2084
2624
  await this.#waitForWorkletInit();
2085
2625
  }
@@ -2426,6 +2966,24 @@ var SuperSonic = class _SuperSonic {
2426
2966
  audioContextState: this.audioContext?.state
2427
2967
  };
2428
2968
  }
2969
+ /**
2970
+ * Get current configuration (merged defaults + user overrides)
2971
+ * Useful for debugging and displaying in UI
2972
+ * @returns {Object} Current scsynth configuration
2973
+ * @example
2974
+ * const config = sonic.getConfig();
2975
+ * console.log('Buffer limit:', config.worldOptions.numBuffers);
2976
+ * console.log('Memory layout:', config.memory);
2977
+ */
2978
+ getConfig() {
2979
+ if (!this.config?.scsynth) {
2980
+ return null;
2981
+ }
2982
+ return {
2983
+ memory: { ...this.config.scsynth.memory },
2984
+ worldOptions: { ...this.config.scsynth.worldOptions }
2985
+ };
2986
+ }
2429
2987
  /**
2430
2988
  * Destroy the orchestrator and clean up resources
2431
2989
  */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "wasmFile": "scsynth-nrt.wasm",
3
- "buildId": "20251118-093603",
4
- "buildTime": "2025-11-18T09:36:03Z",
5
- "gitHash": "d43c4be"
3
+ "buildId": "20251120-122240",
4
+ "buildTime": "2025-11-20T12:22:40Z",
5
+ "gitHash": "5c5371aa09"
6
6
  }
Binary file
@@ -143,6 +143,38 @@ class ScsynthProcessor extends AudioWorkletProcessor {
143
143
 
144
144
  }
145
145
 
146
+ // Write worldOptions to SharedArrayBuffer for C++ to read
147
+ // WorldOptions are written after ring buffer storage (65536 bytes)
148
+ writeWorldOptionsToMemory() {
149
+ if (!this.worldOptions || !this.wasmMemory) {
150
+ return;
151
+ }
152
+
153
+ // WorldOptions location: ringBufferBase + 65536 (after ring_buffer_storage)
154
+ const WORLD_OPTIONS_OFFSET = this.ringBufferBase + 65536;
155
+ const uint32View = new Uint32Array(this.wasmMemory.buffer, WORLD_OPTIONS_OFFSET, 32);
156
+ const float32View = new Float32Array(this.wasmMemory.buffer, WORLD_OPTIONS_OFFSET, 32);
157
+
158
+ // Write worldOptions as uint32/float32 values
159
+ // Order must match C++ reading code in audio_processor.cpp
160
+ uint32View[0] = this.worldOptions.numBuffers || 1024;
161
+ uint32View[1] = this.worldOptions.maxNodes || 1024;
162
+ uint32View[2] = this.worldOptions.maxGraphDefs || 1024;
163
+ uint32View[3] = this.worldOptions.maxWireBufs || 64;
164
+ uint32View[4] = this.worldOptions.numAudioBusChannels || 128;
165
+ uint32View[5] = this.worldOptions.numInputBusChannels || 0;
166
+ uint32View[6] = this.worldOptions.numOutputBusChannels || 2;
167
+ uint32View[7] = this.worldOptions.numControlBusChannels || 4096;
168
+ uint32View[8] = this.worldOptions.bufLength || 128;
169
+ uint32View[9] = this.worldOptions.realTimeMemorySize || 16384;
170
+ uint32View[10] = this.worldOptions.numRGens || 64;
171
+ uint32View[11] = this.worldOptions.realTime ? 1 : 0;
172
+ uint32View[12] = this.worldOptions.memoryLocking ? 1 : 0;
173
+ uint32View[13] = this.worldOptions.loadGraphDefs || 0;
174
+ uint32View[14] = this.worldOptions.preferredSampleRate || 0;
175
+ uint32View[15] = this.worldOptions.verbosity || 0;
176
+ }
177
+
146
178
  // Write debug message to SharedArrayBuffer DEBUG ring buffer
147
179
  js_debug(message) {
148
180
  if (!this.uint8View || !this.atomicView || !this.CONTROL_INDICES || !this.ringBufferBase) {
@@ -217,6 +249,10 @@ class ScsynthProcessor extends AudioWorkletProcessor {
217
249
  // Save memory reference for later use (WASM imports memory, doesn't export it)
218
250
  this.wasmMemory = memory;
219
251
 
252
+ // Store worldOptions and sampleRate for C++ initialization
253
+ this.worldOptions = data.worldOptions || {};
254
+ this.sampleRate = data.sampleRate || 48000; // Fallback to 48000 if not provided
255
+
220
256
  // Import object for WASM
221
257
  // scsynth with pthread support requires these imports
222
258
  // (pthread stubs are no-ops - AudioWorklet is single-threaded)
@@ -271,9 +307,13 @@ class ScsynthProcessor extends AudioWorkletProcessor {
271
307
 
272
308
  this.calculateBufferIndices(this.ringBufferBase);
273
309
 
310
+ // Write worldOptions to SharedArrayBuffer for C++ to read
311
+ this.writeWorldOptionsToMemory();
312
+
274
313
  // Initialize WASM memory
275
314
  if (this.wasmInstance.exports.init_memory) {
276
- this.wasmInstance.exports.init_memory(48000.0);
315
+ // Pass actual sample rate from AudioContext (not hardcoded!)
316
+ this.wasmInstance.exports.init_memory(this.sampleRate);
277
317
 
278
318
  this.isInitialized = true;
279
319
 
@@ -299,9 +339,13 @@ class ScsynthProcessor extends AudioWorkletProcessor {
299
339
 
300
340
  this.calculateBufferIndices(this.ringBufferBase);
301
341
 
342
+ // Write worldOptions to SharedArrayBuffer for C++ to read
343
+ this.writeWorldOptionsToMemory();
344
+
302
345
  // Initialize WASM memory
303
346
  if (this.wasmInstance.exports.init_memory) {
304
- this.wasmInstance.exports.init_memory(48000.0);
347
+ // Pass actual sample rate from AudioContext (not hardcoded!)
348
+ this.wasmInstance.exports.init_memory(this.sampleRate);
305
349
 
306
350
  this.isInitialized = true;
307
351
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supersonic-scsynth",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "SuperCollider scsynth WebAssembly port for AudioWorklet - Run SuperCollider synthesis in the browser",
5
5
  "main": "dist/supersonic.js",
6
6
  "unpkg": "dist/supersonic.js",