Compare commits

..

4 Commits

Author SHA1 Message Date
Mid
2fffb905e3 Improve crackling in worklet codepath 2025-04-15 12:09:31 +03:00
Mid
0da0701d34 Suspend immediately 2025-04-15 12:08:11 +03:00
Mid
0408433fc3 WebSocket receive bug fix 2025-04-15 12:07:43 +03:00
Mid
1acf98ef54 Avoid SIGPIPE 2025-04-15 12:07:27 +03:00
3 changed files with 129 additions and 51 deletions

View File

@ -31,6 +31,7 @@
var DebugSine = 0 var DebugSine = 0
AudCtx = new AudioContext({sampleRate: hz}) AudCtx = new AudioContext({sampleRate: hz})
AudCtx.suspend()
if(AudCtx.audioWorklet) { if(AudCtx.audioWorklet) {
AudCtx.audioWorklet.addModule("rawpcmworklet.js").then(function() { AudCtx.audioWorklet.addModule("rawpcmworklet.js").then(function() {
@ -45,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,6 +103,21 @@
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) {
@ -122,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
}
} }
} }
@ -158,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 +396,27 @@
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"))
ws.binaryType = "arraybuffer" ws.binaryType = "arraybuffer"
@ -402,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)

13
main2.c
View File

@ -101,15 +101,18 @@ static void consume(Client *cli, size_t n) {
cli->len -= n; cli->len -= n;
} }
static void transmit(Client *cli, const char *buf, size_t sz) { static int transmit(Client *cli, const char *buf, size_t sz) {
while(sz) { while(sz) {
ssize_t s = send(cli->fd, buf, sz, 0); ssize_t s = send(cli->fd, buf, sz, MSG_NOSIGNAL);
if(s >= 0) { if(s >= 0) {
buf += s; buf += s;
sz -= s; sz -= s;
} else {
return 0;
} }
} }
return 1;
} }
static void transmit_all(const char *buf, size_t sz) { static void transmit_all(const char *buf, size_t sz) {
@ -306,7 +309,9 @@ static int handle(Client *cli) {
size_t rsize = cli->len; size_t rsize = cli->len;
int pret = phr_decode_chunked(&cli->chudec, cli->buf, &rsize); int pret = phr_decode_chunked(&cli->chudec, cli->buf, &rsize);
if(pret == -1) return 0; if(pret == -1) {
return 0;
}
stream_step(cli->buf, rsize); stream_step(cli->buf, rsize);
@ -367,6 +372,8 @@ static int handle(Client *cli) {
cli->ws.incoming = realloc(cli->ws.incoming, cli->ws.incomingSz + payloadSz); cli->ws.incoming = realloc(cli->ws.incoming, cli->ws.incomingSz + payloadSz);
memcpy(cli->ws.incoming + cli->ws.incomingSz, cli->buf + i + 4, payloadSz); memcpy(cli->ws.incoming + cli->ws.incomingSz, cli->buf + i + 4, payloadSz);
consume(cli, i + 4 + payloadSz);
if(fin) { if(fin) {
receive_ws(cli); receive_ws(cli);

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)
newleft.set(newaudioframes[i].left, this.left.length)
this.left = newleft
var newright = new Float32Array(this.right.length + newaudioframes[i].right.length) if(newaudioframes.left.length > this.ringL.length) {
newright.set(this.right, 0) newaudioframes.left = newaudioframes.left.slice(newaudioframes.left.length - this.ringL.length)
newright.set(newaudioframes[i].right, this.right.length) newaudioframes.right = newaudioframes.right.slice(newaudioframes.right.length - this.ringL.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))
return true; 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)
}
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
} }
} }