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