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.
- package/dist/supersonic.js +887 -1017
- package/dist/wasm/manifest.json +3 -3
- package/dist/wasm/scsynth-nrt.wasm +0 -0
- package/dist/workers/debug_worker.js +20 -26
- package/dist/workers/osc_in_worker.js +27 -31
- package/dist/workers/osc_out_prescheduler_worker.js +86 -91
- package/dist/workers/scsynth_audio_worklet.js +0 -53
- package/package.json +1 -1
package/dist/supersonic.js
CHANGED
|
@@ -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/
|
|
1605
|
-
var
|
|
1606
|
-
var
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
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 (
|
|
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
|
-
|
|
1866
|
-
|
|
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 (
|
|
1874
|
-
|
|
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
|
-
|
|
1888
|
-
|
|
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
|
-
|
|
1905
|
-
|
|
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
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
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 (
|
|
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
|
-
|
|
1921
|
-
|
|
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
|
-
|
|
1931
|
-
|
|
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 (
|
|
1939
|
-
|
|
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
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
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
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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
|
-
|
|
2144
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
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(
|
|
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
|
|
1775
|
+
ptr,
|
|
1776
|
+
sizeBytes,
|
|
2243
1777
|
numFrames: roundedFrames,
|
|
2244
1778
|
numChannels: roundedChannels,
|
|
2245
|
-
sampleRate: sampleRate || this
|
|
2246
|
-
uuid,
|
|
2247
|
-
allocationComplete: managedCompletion
|
|
1779
|
+
sampleRate: sampleRate || this.#audioContext.sampleRate
|
|
2248
1780
|
};
|
|
2249
|
-
}
|
|
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
|
|
1796
|
+
const ptr = this.#bufferPool.malloc(bytesNeeded);
|
|
2276
1797
|
if (ptr === 0) {
|
|
2277
|
-
const stats = this
|
|
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
|
|
1809
|
+
const heap = new Float32Array(this.#sharedBuffer, ptr, data.length);
|
|
2289
1810
|
heap.set(data);
|
|
2290
1811
|
}
|
|
2291
|
-
#
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
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
|
|
1823
|
+
const allocationComplete = this.#createPendingOperation(uuid, bufnum, timeoutMs);
|
|
2300
1824
|
return { uuid, allocationComplete };
|
|
2301
1825
|
}
|
|
2302
1826
|
async #acquireBufferLock(bufnum) {
|
|
2303
|
-
const prev = this
|
|
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
|
|
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
|
|
2316
|
-
this
|
|
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
|
|
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
|
|
1853
|
+
this.#allocatedBuffers.set(bufnum, entry);
|
|
2330
1854
|
return entry;
|
|
2331
1855
|
}
|
|
2332
1856
|
async #awaitPendingReplacement(bufnum) {
|
|
2333
|
-
const existing = this
|
|
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
|
|
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
|
|
1889
|
+
this.#bufferPool.free(previous.ptr);
|
|
2366
1890
|
}
|
|
2367
1891
|
return;
|
|
2368
1892
|
}
|
|
2369
1893
|
if (entry.ptr) {
|
|
2370
|
-
this
|
|
1894
|
+
this.#bufferPool.free(entry.ptr);
|
|
2371
1895
|
}
|
|
2372
1896
|
entry.pendingPromise = null;
|
|
2373
1897
|
if (previous?.ptr) {
|
|
2374
|
-
this
|
|
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
|
|
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
|
|
2393
|
-
this
|
|
2394
|
-
this
|
|
2395
|
-
this
|
|
2396
|
-
this
|
|
2397
|
-
this
|
|
2398
|
-
this
|
|
2399
|
-
this
|
|
2400
|
-
this
|
|
2401
|
-
this
|
|
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
|
|
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
|
-
//
|
|
2443
|
-
|
|
2286
|
+
// Build-time memory layout (constant)
|
|
2287
|
+
memory: MemoryLayout,
|
|
2288
|
+
// Runtime world options (merged defaults + user overrides)
|
|
2289
|
+
worldOptions
|
|
2444
2290
|
};
|
|
2445
|
-
this
|
|
2446
|
-
this
|
|
2447
|
-
this
|
|
2448
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
2461
|
-
this
|
|
2462
|
-
|
|
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
|
|
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
|
|
2480
|
-
if (this
|
|
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
|
|
2347
|
+
return this.#capabilities;
|
|
2489
2348
|
}
|
|
2490
2349
|
/**
|
|
2491
|
-
* Merge user-provided
|
|
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.
|
|
2522
|
-
this
|
|
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
|
|
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
|
|
2544
|
-
|
|
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
|
|
2562
|
-
audioContext: this
|
|
2563
|
-
sharedBuffer: this
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2606
|
-
this
|
|
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
|
|
2612
|
-
this
|
|
2418
|
+
this.#workletNode.connect(this.#audioContext.destination);
|
|
2419
|
+
this.#workletNode.port.postMessage({
|
|
2613
2420
|
type: "init",
|
|
2614
|
-
sharedBuffer: this
|
|
2421
|
+
sharedBuffer: this.#sharedBuffer
|
|
2615
2422
|
});
|
|
2616
|
-
this
|
|
2423
|
+
this.#workletNode.port.postMessage({
|
|
2617
2424
|
type: "loadWasm",
|
|
2618
2425
|
wasmBytes,
|
|
2619
|
-
wasmMemory: this
|
|
2620
|
-
worldOptions: this.config.
|
|
2621
|
-
sampleRate: this
|
|
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
|
|
2631
|
-
this
|
|
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
|
|
2443
|
+
this.#osc.onParsedOSC((msg) => {
|
|
2637
2444
|
if (msg.address === "/buffer/freed") {
|
|
2638
|
-
this
|
|
2445
|
+
this.#bufferManager?.handleBufferFreed(msg.args);
|
|
2639
2446
|
} else if (msg.address === "/buffer/allocated") {
|
|
2640
|
-
this
|
|
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
|
|
2644
|
-
const listener = this.
|
|
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
|
|
2456
|
+
this.#metrics_messagesReceived++;
|
|
2650
2457
|
this.onMessage(msg);
|
|
2651
2458
|
}
|
|
2652
2459
|
});
|
|
2653
|
-
this
|
|
2460
|
+
this.#osc.onDebugMessage((msg) => {
|
|
2654
2461
|
if (this.onDebugMessage) {
|
|
2655
2462
|
this.onDebugMessage(msg);
|
|
2656
2463
|
}
|
|
2657
2464
|
});
|
|
2658
|
-
this
|
|
2465
|
+
this.#osc.onError((error, workerName) => {
|
|
2659
2466
|
console.error(`[SuperSonic] ${workerName} error:`, error);
|
|
2660
|
-
this
|
|
2467
|
+
this.#metrics_errors++;
|
|
2661
2468
|
if (this.onError) {
|
|
2662
2469
|
this.onError(new Error(`${workerName}: ${error}`));
|
|
2663
2470
|
}
|
|
2664
2471
|
});
|
|
2665
|
-
await this
|
|
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
|
|
2672
|
-
this
|
|
2673
|
-
this.
|
|
2674
|
-
console.log(`[SuperSonic] Initialization complete in ${this.
|
|
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
|
|
2678
|
-
|
|
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
|
|
2498
|
+
if (this.#initialized) {
|
|
2692
2499
|
console.warn("[SuperSonic] Already initialized");
|
|
2693
2500
|
return;
|
|
2694
2501
|
}
|
|
2695
|
-
if (this
|
|
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
|
|
2708
|
-
this.
|
|
2514
|
+
this.#initializing = true;
|
|
2515
|
+
this.bootStats.initStartTime = performance.now();
|
|
2709
2516
|
try {
|
|
2710
|
-
this.
|
|
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
|
|
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
|
|
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
|
|
2557
|
+
this.#workletNode.port.removeEventListener("message", messageHandler);
|
|
2751
2558
|
if (event.data.success) {
|
|
2752
2559
|
if (event.data.ringBufferBase !== void 0) {
|
|
2753
|
-
this
|
|
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
|
|
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
|
|
2779
|
-
this
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
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
|
-
|
|
2840
|
-
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2964
|
-
capabilities: this
|
|
2965
|
-
|
|
2966
|
-
audioContextState: this
|
|
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
|
|
2882
|
+
if (!this.config) {
|
|
2980
2883
|
return null;
|
|
2981
2884
|
}
|
|
2982
2885
|
return {
|
|
2983
|
-
memory: { ...this.config.
|
|
2984
|
-
worldOptions: { ...this.config.
|
|
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
|
-
|
|
2994
|
-
|
|
2995
|
-
this.
|
|
2896
|
+
this.#stopPerformanceMonitoring();
|
|
2897
|
+
if (this.#osc) {
|
|
2898
|
+
this.#osc.terminate();
|
|
2899
|
+
this.#osc = null;
|
|
2996
2900
|
}
|
|
2997
|
-
if (this
|
|
2998
|
-
this
|
|
2999
|
-
this
|
|
2901
|
+
if (this.#workletNode) {
|
|
2902
|
+
this.#workletNode.disconnect();
|
|
2903
|
+
this.#workletNode = null;
|
|
3000
2904
|
}
|
|
3001
|
-
if (this
|
|
3002
|
-
await this
|
|
3003
|
-
this
|
|
2905
|
+
if (this.#audioContext) {
|
|
2906
|
+
await this.#audioContext.close();
|
|
2907
|
+
this.#audioContext = null;
|
|
3004
2908
|
}
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
2909
|
+
if (this.#bufferManager) {
|
|
2910
|
+
this.#bufferManager.destroy();
|
|
2911
|
+
this.#bufferManager = null;
|
|
3008
2912
|
}
|
|
3009
|
-
this
|
|
3010
|
-
this
|
|
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
|
-
*
|
|
3019
|
-
*
|
|
3020
|
-
*
|
|
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
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
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
|
|
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
|
|
2990
|
+
if (!this.#initialized) {
|
|
3094
2991
|
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
3095
2992
|
}
|
|
3096
|
-
if (!this
|
|
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
|
|
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
|
|
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
|
|
3137
|
-
this.
|
|
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.
|
|
3040
|
+
this.#syncListeners.delete(syncId);
|
|
3144
3041
|
resolve();
|
|
3145
3042
|
};
|
|
3146
|
-
if (!this
|
|
3147
|
-
this
|
|
3043
|
+
if (!this.#syncListeners) {
|
|
3044
|
+
this.#syncListeners = /* @__PURE__ */ new Map();
|
|
3148
3045
|
}
|
|
3149
|
-
this.
|
|
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
|
-
|
|
3163
|
-
|
|
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
|
-
|
|
3181
|
-
|
|
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
|
-
|
|
3196
|
-
|
|
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
|
-
|
|
3209
|
-
|
|
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
|
|
3112
|
+
async initializeNTPTiming() {
|
|
3113
|
+
if (!this.#bufferConstants || !this.#audioContext) {
|
|
3251
3114
|
return;
|
|
3252
3115
|
}
|
|
3253
|
-
|
|
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
|
|
3256
|
-
const ntpStartTime = currentNTP - currentAudioCtx;
|
|
3126
|
+
const ntpStartTime = currentNTP - timestamp.contextTime;
|
|
3257
3127
|
const ntpStartView = new Float64Array(
|
|
3258
|
-
this
|
|
3259
|
-
this
|
|
3128
|
+
this.#sharedBuffer,
|
|
3129
|
+
this.#ringBufferBase + this.#bufferConstants.NTP_START_TIME_START,
|
|
3260
3130
|
1
|
|
3261
3131
|
);
|
|
3262
3132
|
ntpStartView[0] = ntpStartTime;
|
|
3263
|
-
this
|
|
3264
|
-
console.log(`[SuperSonic] NTP timing initialized: start=${ntpStartTime.toFixed(6)}s (
|
|
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
|
|
3142
|
+
if (!this.#bufferConstants || !this.#audioContext || this.#initialNTPStartTime === void 0) {
|
|
3273
3143
|
return;
|
|
3274
3144
|
}
|
|
3275
|
-
const
|
|
3145
|
+
const timestamp = this.#audioContext.getOutputTimestamp();
|
|
3146
|
+
const perfTimeMs = performance.timeOrigin + timestamp.performanceTime;
|
|
3276
3147
|
const currentNTP = perfTimeMs / 1e3 + NTP_EPOCH_OFFSET;
|
|
3277
|
-
const
|
|
3278
|
-
const
|
|
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
|
|
3283
|
-
this
|
|
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
|
|
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
|
|
3164
|
+
if (!this.#bufferConstants) {
|
|
3295
3165
|
return 0;
|
|
3296
3166
|
}
|
|
3297
3167
|
const driftView = new Int32Array(
|
|
3298
|
-
this
|
|
3299
|
-
this
|
|
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
|
|
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
|
|
3321
|
-
clearInterval(this
|
|
3322
|
-
this
|
|
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
|
|
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
|
|
3405
|
+
if (!this.#bufferManager) {
|
|
3536
3406
|
throw new Error("Buffer manager not ready. Call init() before issuing buffer commands.");
|
|
3537
3407
|
}
|
|
3538
|
-
return this
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3442
|
+
const currentTimeS = this.#audioContext.currentTime;
|
|
3573
3443
|
return { audioTimeS, currentTimeS };
|
|
3574
3444
|
}
|
|
3575
3445
|
};
|