From 2fffb905e303576b7aad721e99892722ac68a101 Mon Sep 17 00:00:00 2001 From: Mid <> Date: Tue, 15 Apr 2025 12:09:31 +0300 Subject: [PATCH] Improve crackling in worklet codepath --- blarf.js | 82 ++++++++++++++++++++++++++++++---------------- rawpcmworklet.js | 84 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 118 insertions(+), 48 deletions(-) diff --git a/blarf.js b/blarf.js index 9d3dba9..6d805cb 100644 --- a/blarf.js +++ b/blarf.js @@ -46,14 +46,6 @@ var outL = e.outputBuffer.getChannelData(0) var outR = channels > 1 ? e.outputBuffer.getChannelData(1) : null - /*for(var i = 0; i < outL.length; i++) { - outL[i] = Math.sin(440 * 2 * 3.14159 * (DebugSine / AudHz)) - DebugSine++ - } - - AudioQueue = [] - return*/ - var leftToWrite = outL.length var offset = 0 @@ -110,7 +102,22 @@ } var RenderStartTime, VideoStartTime - + + function crop_audio_queue(durationToRemove) { + while(AudioQueue.length && durationToRemove) { + var amount = Math.min(durationToRemove, AudioQueue[0].left.length) + + AudioQueue[0].left = AudioQueue[0].left.subarray(amount) + AudioQueue[0].right = AudioQueue[0].right.subarray(amount) + + if(AudioQueue[0].left.length == 0) { + AudioQueue.shift() + } + + durationToRemove -= amount + } + } + var Statistics = {} var TheWorker = new Worker("blarfwork.js") TheWorker.onmessage = function(e) { @@ -123,25 +130,20 @@ // Audio may be loaded but it might not play because of autoplay permissions // In this case the audio queue will fill up and cause ever-increasing AV desync - // To prevent this, manually crop the audio to the duration in the video queue - if(AudCtx && AudCtx.state != "running") { var durationInAudioQueue = AudioQueue.length ? AudioQueue.reduce((acc, el) => acc + el.left.length, 0) : 0 - var durationToRemove = Math.max(durationInAudioQueue - (VideoQueue.length ? (VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t) : 0) * AudHz / 1000, 0) - - while(AudioQueue.length && durationToRemove) { - var amount = Math.min(durationToRemove, AudioQueue[0].left.length) - - AudioQueue[0].left = AudioQueue[0].left.subarray(amount) - AudioQueue[0].right = AudioQueue[0].left.subarray(amount) - - if(AudioQueue[0].left.length == 0) { - AudioQueue.shift() - } - - durationToRemove -= amount - } + //if(AudWorklet) { + // // With audio worklets, crop to 1024 samples max to prevent ring buffer overflow + // + // crop_audio_queue(Math.max(durationInAudioQueue - 1024, 0)) + //} else { + // // Without audio worklets we use a FIFO and can crop to the duration in the video queue + // + var durationToRemove = Math.max(durationInAudioQueue - (VideoQueue.length ? (VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t) : 0) * AudHz / 1000, 0) + // + crop_audio_queue(durationToRemove) + //} } } @@ -159,7 +161,7 @@ text = text + k + ":" + (Math.floor(100 * Statistics[k].sum / Statistics[k].count) / 100) + "," } stats.innerText = text*/ - stats.innerHTML = (VideoQueue.length ? (VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t) : "0") + "v" + (AudioQueue.reduce(function(acc, obj) {return acc + obj.left.length * 1000 / AudHz}, 0)|0) + "a" + stats.innerText = (VideoQueue.length ? (VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t) : "0") + "v" + (AudioQueue.reduce(function(acc, obj) {return acc + obj.left.length * 1000 / AudHz}, 0)|0) + "a" } } @@ -393,6 +395,27 @@ ebml.onenter = matr.onenter.bind(matr) ebml.ondata = matr.ondata.bind(matr) ebml.onexit = matr.onexit.bind(matr) + + function merge_audio_queue() { + var s = 0 + + for(var i = 0; i < AudioQueue.length; i++) { + s += AudioQueue[i].left.length + } + + var L = new Float32Array(s) + var R = new Float32Array(s) + + s = 0 + + for(var i = 0; i < AudioQueue.length; i++) { + L.set(AudioQueue[i].left, s) + R.set(AudioQueue[i].right, s) + s += AudioQueue[i].left.length + } + + return {msg: "data", left: L, right: R} + } function reconnect_ws() { var ws = new WebSocket(BlarfEl.getAttribute("data-target")) @@ -403,9 +426,14 @@ // It would make more sense for this to be in `render` but we need the guarantee that this will run when the tab is out of focus if(AudCtx.state == "running" && AudWorklet && AudioQueue.length) { - AudWorklet.port.postMessage({msg: "data", audio: AudioQueue}) + AudWorklet.port.postMessage(merge_audio_queue()) AudioQueue.length = 0 } + if(VideoQueue.length) { + while(document.timeline.currentTime - VideoQueue[0].t > 5000) { + VideoQueue.shift() + } + } } ws.onclose = function(ev) { setTimeout(reconnect_ws, 5000) diff --git a/rawpcmworklet.js b/rawpcmworklet.js index 238dc0c..7c08ac1 100644 --- a/rawpcmworklet.js +++ b/rawpcmworklet.js @@ -1,28 +1,50 @@ // To explain succinctly, the people who designed AudioWorklet and // deprecated ScriptProcessorNode are retarded and we need a worklet -// that does basically nothing to get glitchless audio. +// that does basically nothing + +// Must be careful to create as little garbage as possible otherwise +// even this will produce cracks/pops/clicks/blips. +// It was like this on Firefox; Chromium managed. class RawPCMWorklet extends AudioWorkletProcessor { constructor() { super() - this.left = new Float32Array() - this.right = new Float32Array() - + this.ringL = new Float32Array(65536) + this.ringR = new Float32Array(65536) + this.ringWrite = 0 + this.ringRead = 0 + + for(var z = 0; z < 65536; z++) { + this.ringL[z] = Math.sin(z / 128 * 2 * Math.PI) * 0.3 + } + this.port.onmessage = (event) => { - var newaudioframes = event.data.audio + var newaudioframes = event.data - for(var i = 0; i < newaudioframes.length; i++) { - var newleft = new Float32Array(this.left.length + newaudioframes[i].left.length) - newleft.set(this.left, 0) - newleft.set(newaudioframes[i].left, this.left.length) - this.left = newleft - - var newright = new Float32Array(this.right.length + newaudioframes[i].right.length) - newright.set(this.right, 0) - newright.set(newaudioframes[i].right, this.right.length) - this.right = newright + var newlen = newaudioframes.left.length + + if(newaudioframes.left.length > this.ringL.length) { + newaudioframes.left = newaudioframes.left.slice(newaudioframes.left.length - this.ringL.length) + newaudioframes.right = newaudioframes.right.slice(newaudioframes.right.length - this.ringL.length) } + + if(this.ringWrite % this.ringL.length + newaudioframes.left.length <= this.ringL.length) { + this.ringL.set(newaudioframes.left, this.ringWrite % this.ringL.length) + this.ringR.set(newaudioframes.right, this.ringWrite % this.ringL.length) + } else { + var boundary = this.ringL.length - this.ringWrite % this.ringL.length + + this.ringL.set(newaudioframes.left.slice(0, boundary), this.ringWrite % this.ringL.length) + this.ringL.set(newaudioframes.left.slice(boundary), 0) + + this.ringR.set(newaudioframes.right.slice(0, boundary), this.ringWrite % this.ringL.length) + this.ringR.set(newaudioframes.right.slice(boundary), 0) + } + + this.ringWrite += newlen + + console.log(this.ringWrite - this.ringRead) } } @@ -32,15 +54,35 @@ class RawPCMWorklet extends AudioWorkletProcessor { var left = output[0] var right = output[1] - var available = Math.min(left.length, this.left.length) + /*if(this.ringWrite < 16384) { + return true + }*/ - left.set(this.left.slice(0, available)) - right.set(this.right.slice(0, available)) + var available = Math.min(left.length, Math.max(0, this.ringWrite - this.ringRead)) - this.left = this.left.slice(available) - this.right = this.right.slice(available) + if(this.ringRead % this.ringL.length + available <= this.ringL.length) { + left.set(this.ringL.slice(this.ringRead % this.ringL.length, this.ringRead % this.ringL.length + available)) + right.set(this.ringR.slice(this.ringRead % this.ringL.length, this.ringRead % this.ringL.length + available)) + } else { + left.set(this.ringL.slice(this.ringRead % this.ringL.length)) + right.set(this.ringR.slice(this.ringRead % this.ringL.length)) + + var boundary = this.ringL.length - this.ringRead % this.ringL.length + + left.set(this.ringL.slice(0, available - boundary), boundary) + right.set(this.ringR.slice(0, available - boundary), boundary) + } - return true; + this.ringRead += left.length + + /*for(var s = 0; s < available; s++) { + var sw = Math.sin((this.debug + s) / 48000 * 440 * 2 * 3.1415926) * 0.3 + left[s] = sw + right[s] = sw + } + this.debug += available*/ + + return true } }