From b862345713dee9b84737935a02906b231733e0ea Mon Sep 17 00:00:00 2001 From: Rerrah Date: Sun, 9 Nov 2025 18:01:00 +0900 Subject: [PATCH 1/4] Log buffer underrun --- features/preview/synth.ts | 33 +++++++++++++++++++++++---------- public/worker/worker.js | 21 --------------------- public/worklet/processor.js | 3 +++ 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/features/preview/synth.ts b/features/preview/synth.ts index 8aa2ec4..293fc1d 100644 --- a/features/preview/synth.ts +++ b/features/preview/synth.ts @@ -17,15 +17,26 @@ export type Synthesizer = { changeChip: (chip: AvailableChip) => void; }; -const RING_BUFFER_SIZE = 8192; -const RING_BUFFER_CHANNELS = 2; -const RING_BUFFER_BYTE_SIZE = - RING_BUFFER_SIZE * RING_BUFFER_CHANNELS * Float32Array.BYTES_PER_ELEMENT; -const RING_BUFFER_READ_POS_SIZE = Int32Array.BYTES_PER_ELEMENT; -const RING_BUFFER_WRITE_POS_SIZE = Int32Array.BYTES_PER_ELEMENT; -const RING_BUFFER_CONTROLS_SIZE = - RING_BUFFER_READ_POS_SIZE + RING_BUFFER_WRITE_POS_SIZE; -const SAB_SIZE = RING_BUFFER_CONTROLS_SIZE + RING_BUFFER_BYTE_SIZE; +function calculateRingBufferSize( + time: number, + sampleRate: number +): { + sabSize: number; + bufferSize: number; +} { + const RING_BUFFER_READ_POS_SIZE = Int32Array.BYTES_PER_ELEMENT; + const RING_BUFFER_WRITE_POS_SIZE = Int32Array.BYTES_PER_ELEMENT; + const RING_BUFFER_CONTROLS_SIZE = + RING_BUFFER_READ_POS_SIZE + RING_BUFFER_WRITE_POS_SIZE; + + const RING_BUFFER_CHANNELS = 2; + + const bufferSize = Math.floor(time * sampleRate); + const ringBufferSize = + bufferSize * RING_BUFFER_CHANNELS * Float32Array.BYTES_PER_ELEMENT; + const sabSize = RING_BUFFER_CONTROLS_SIZE + ringBufferSize; + return { sabSize, bufferSize }; +} type KeyOnWorkerRequestMessage = { type: "keyOn"; @@ -140,7 +151,6 @@ export function useSynthesizer() { * * It is necessary to use Atomics API to read and write positions. */ - const sab = new SharedArrayBuffer(SAB_SIZE); const wasmWorker = new Worker("/worker/worker.js"); const audioContext = new AudioContext(); @@ -151,6 +161,9 @@ export function useSynthesizer() { }); synthNode.connect(audioContext.destination); + const { sabSize } = calculateRingBufferSize(0.2, audioContext.sampleRate); + const sab = new SharedArrayBuffer(sabSize); + // Setup message handlers. wasmWorker.onmessage = async ({ data, diff --git a/public/worker/worker.js b/public/worker/worker.js index ff2e79d..ba54c99 100644 --- a/public/worker/worker.js +++ b/public/worker/worker.js @@ -2,8 +2,6 @@ importScripts("/wasm/synth.js"); let wasm; -let currentInstrument; - /** @type {Int32Array} */ let control; const SAB_CONTROL_SIZE = 2; @@ -129,7 +127,6 @@ function handleSetInstrument(instrument) { } wasm.setInstrument(instrument); - currentInstrument = instrument; } /** @@ -200,20 +197,6 @@ function handleGenerate() { Atomics.store(control, SAB_WRITE_POSITION_INDEX, pos); } -function handleChangeChip(id) { - if (!wasm) { - self.postMessage(WASM_NOT_LOADED_MESSAGE); - return; - } - - wasm.changeChip(id); - messageQueue.length = 0; - - if (currentInstrument) { - wasm.setInstrument(currentInstrument); - } -} - self.onmessage = async ({ data }) => { switch (data.type) { case "loadWasm": @@ -234,10 +217,6 @@ self.onmessage = async ({ data }) => { handleReset(); break; - case "changeChip": - handleChangeChip(data.chipId); - break; - default: break; } diff --git a/public/worklet/processor.js b/public/worklet/processor.js index db0d1ab..7727642 100644 --- a/public/worklet/processor.js +++ b/public/worklet/processor.js @@ -74,6 +74,9 @@ class SynthProcessor extends AudioWorkletProcessor { const nReadables = (writePos + this.bufferSize - readPos) % this.bufferSize; let nReads = Math.min(leftChannel.length, nReadables); + if (nReads !== leftChannel.length) { + console.warn("buffer underrun!", leftChannel.length - nReads); + } let pos = readPos; while (nReads > 0) { From e20fb903726d5ea00676f73e83c578f87266468d Mon Sep 17 00:00:00 2001 From: Rerrah Date: Wed, 12 Nov 2025 21:52:08 +0900 Subject: [PATCH 2/4] Shorten buffer threshold --- public/worker/worker.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/worker/worker.js b/public/worker/worker.js index ba54c99..3dec0a3 100644 --- a/public/worker/worker.js +++ b/public/worker/worker.js @@ -78,7 +78,7 @@ function handleInitialize(sampleRate, sab) { control.byteLength + leftBuffer.byteLength, bufferSize ); - fillThreshold = (bufferSize * 3) / 4; + fillThreshold = bufferSize / 2; self.postMessage({ type: "initialized", @@ -174,8 +174,8 @@ function handleGenerate() { let nWritables = (readPos + bufferSize - 1 - writePos) % bufferSize; if (nWritables < fillThreshold) { - const TIMEOUT = 50; - Atomics.wait(control, SAB_READ_POSITION_INDEX, readPos, TIMEOUT); + const TIMEOUT_MSEC = 50; + Atomics.wait(control, SAB_READ_POSITION_INDEX, readPos, TIMEOUT_MSEC); return; } From 1fbf9d4affc8dd9673dc0b591edd8646d8fd1fc8 Mon Sep 17 00:00:00 2001 From: Rerrah Date: Sat, 15 Nov 2025 17:34:33 +0900 Subject: [PATCH 3/4] Fix incorrect circular buffering --- public/worklet/processor.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/public/worklet/processor.js b/public/worklet/processor.js index 7727642..6aebab6 100644 --- a/public/worklet/processor.js +++ b/public/worklet/processor.js @@ -74,20 +74,22 @@ class SynthProcessor extends AudioWorkletProcessor { const nReadables = (writePos + this.bufferSize - readPos) % this.bufferSize; let nReads = Math.min(leftChannel.length, nReadables); - if (nReads !== leftChannel.length) { - console.warn("buffer underrun!", leftChannel.length - nReads); - } + // if (nReads !== leftChannel.length) { + // console.warn("buffer underrun!", leftChannel.length - nReads); + // } let pos = readPos; + let destOffset = 0; while (nReads > 0) { const chunk = Math.min(nReads, this.bufferSize - pos); const end = pos + chunk; - leftChannel.set(this.leftBuffer.subarray(pos, end)); - rightChannel.set(this.rightBuffer.subarray(pos, end)); + leftChannel.set(this.leftBuffer.subarray(pos, end), destOffset); + rightChannel.set(this.rightBuffer.subarray(pos, end), destOffset); nReads -= chunk; pos = end % this.bufferSize; + destOffset += chunk; } // It is unnecessary to fill zero-samples in buffer-underrun From 8224158d38de53a447f6d6013b867b47b2bc06b2 Mon Sep 17 00:00:00 2001 From: Rerrah Date: Sat, 15 Nov 2025 17:45:55 +0900 Subject: [PATCH 4/4] Restore change chip process these were removed unintentionally --- public/worker/worker.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/public/worker/worker.js b/public/worker/worker.js index 3dec0a3..6b8dcf2 100644 --- a/public/worker/worker.js +++ b/public/worker/worker.js @@ -2,6 +2,8 @@ importScripts("/wasm/synth.js"); let wasm; +let currentInstrument; + /** @type {Int32Array} */ let control; const SAB_CONTROL_SIZE = 2; @@ -127,6 +129,7 @@ function handleSetInstrument(instrument) { } wasm.setInstrument(instrument); + currentInstrument = instrument; } /** @@ -197,6 +200,20 @@ function handleGenerate() { Atomics.store(control, SAB_WRITE_POSITION_INDEX, pos); } +function handleChangeChip(id) { + if (!wasm) { + self.postMessage(WASM_NOT_LOADED_MESSAGE); + return; + } + + wasm.changeChip(id); + messageQueue.length = 0; + + if (currentInstrument) { + wasm.setInstrument(currentInstrument); + } +} + self.onmessage = async ({ data }) => { switch (data.type) { case "loadWasm": @@ -217,6 +234,10 @@ self.onmessage = async ({ data }) => { handleReset(); break; + case "changeChip": + handleChangeChip(data.chipId); + break; + default: break; }