Improve crackling in worklet codepath

This commit is contained in:
Mid 2025-04-15 12:09:31 +03:00
parent 0da0701d34
commit 2fffb905e3
2 changed files with 118 additions and 48 deletions

View File

@ -46,14 +46,6 @@
var outL = e.outputBuffer.getChannelData(0) var outL = e.outputBuffer.getChannelData(0)
var outR = channels > 1 ? e.outputBuffer.getChannelData(1) : null 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 leftToWrite = outL.length
var offset = 0 var offset = 0
@ -110,7 +102,22 @@
} }
var RenderStartTime, VideoStartTime 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 Statistics = {}
var TheWorker = new Worker("blarfwork.js") var TheWorker = new Worker("blarfwork.js")
TheWorker.onmessage = function(e) { TheWorker.onmessage = function(e) {
@ -123,25 +130,20 @@
// Audio may be loaded but it might not play because of autoplay permissions // 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 // 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") { if(AudCtx && AudCtx.state != "running") {
var durationInAudioQueue = AudioQueue.length ? AudioQueue.reduce((acc, el) => acc + el.left.length, 0) : 0 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) //if(AudWorklet) {
// // With audio worklets, crop to 1024 samples max to prevent ring buffer overflow
while(AudioQueue.length && durationToRemove) { //
var amount = Math.min(durationToRemove, AudioQueue[0].left.length) // crop_audio_queue(Math.max(durationInAudioQueue - 1024, 0))
//} else {
AudioQueue[0].left = AudioQueue[0].left.subarray(amount) // // Without audio worklets we use a FIFO and can crop to the duration in the video queue
AudioQueue[0].right = AudioQueue[0].left.subarray(amount) //
var durationToRemove = Math.max(durationInAudioQueue - (VideoQueue.length ? (VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t) : 0) * AudHz / 1000, 0)
if(AudioQueue[0].left.length == 0) { //
AudioQueue.shift() crop_audio_queue(durationToRemove)
} //}
durationToRemove -= amount
}
} }
} }
@ -159,7 +161,7 @@
text = text + k + ":" + (Math.floor(100 * Statistics[k].sum / Statistics[k].count) / 100) + "," text = text + k + ":" + (Math.floor(100 * Statistics[k].sum / Statistics[k].count) / 100) + ","
} }
stats.innerText = text*/ 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.onenter = matr.onenter.bind(matr)
ebml.ondata = matr.ondata.bind(matr) ebml.ondata = matr.ondata.bind(matr)
ebml.onexit = matr.onexit.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() { function reconnect_ws() {
var ws = new WebSocket(BlarfEl.getAttribute("data-target")) 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 // 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) { if(AudCtx.state == "running" && AudWorklet && AudioQueue.length) {
AudWorklet.port.postMessage({msg: "data", audio: AudioQueue}) AudWorklet.port.postMessage(merge_audio_queue())
AudioQueue.length = 0 AudioQueue.length = 0
} }
if(VideoQueue.length) {
while(document.timeline.currentTime - VideoQueue[0].t > 5000) {
VideoQueue.shift()
}
}
} }
ws.onclose = function(ev) { ws.onclose = function(ev) {
setTimeout(reconnect_ws, 5000) setTimeout(reconnect_ws, 5000)

View File

@ -1,28 +1,50 @@
// To explain succinctly, the people who designed AudioWorklet and // To explain succinctly, the people who designed AudioWorklet and
// deprecated ScriptProcessorNode are retarded and we need a worklet // 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 { class RawPCMWorklet extends AudioWorkletProcessor {
constructor() { constructor() {
super() super()
this.left = new Float32Array() this.ringL = new Float32Array(65536)
this.right = new Float32Array() 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) => { this.port.onmessage = (event) => {
var newaudioframes = event.data.audio var newaudioframes = event.data
for(var i = 0; i < newaudioframes.length; i++) { var newlen = newaudioframes.left.length
var newleft = new Float32Array(this.left.length + newaudioframes[i].left.length)
newleft.set(this.left, 0) if(newaudioframes.left.length > this.ringL.length) {
newleft.set(newaudioframes[i].left, this.left.length) newaudioframes.left = newaudioframes.left.slice(newaudioframes.left.length - this.ringL.length)
this.left = newleft newaudioframes.right = newaudioframes.right.slice(newaudioframes.right.length - this.ringL.length)
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
} }
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 left = output[0]
var right = output[1] 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)) var available = Math.min(left.length, Math.max(0, this.ringWrite - this.ringRead))
right.set(this.right.slice(0, available))
this.left = this.left.slice(available) if(this.ringRead % this.ringL.length + available <= this.ringL.length) {
this.right = this.right.slice(available) 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
} }
} }