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.
package/dist/supersonic.js
CHANGED
|
@@ -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 =
|
|
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
|
|
2521
|
+
const memConfig = this.config.scsynth.memory;
|
|
1986
2522
|
this.wasmMemory = new WebAssembly.Memory({
|
|
1987
|
-
initial:
|
|
1988
|
-
maximum:
|
|
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:
|
|
1997
|
-
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
|
-
|
|
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
|
*/
|
package/dist/wasm/manifest.json
CHANGED
|
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
|
-
|
|
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
|
-
|
|
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