// AudioWorklet-Prozessor für die isolierte "Nur Minecraft"-Tonaufnahme. // // loopback-capture liefert PCM-Häppchen unregelmäßig getaktet über IPC (mal // etwas früher, mal etwas später). Würde man sie direkt nacheinander als // einzelne AudioBufferSourceNode-Aufrufe abspielen, würde jede kleine // Verzögerung als hörbares Knacksen/Aussetzer auffallen. Stattdessen landen // ankommende Häppchen hier in einem Ringpuffer; der Audio-Rendering-Thread // liest in gleichmäßigem Takt (process() läuft alle 128 Samples) daraus. // Kleine Zeitschwankungen bei der Anlieferung werden so unhörbar // abgefedert. Läuft der Puffer doch mal leer, gibt es kurz Stille statt // eines Knacksens. class PcmRingBufferProcessor extends AudioWorkletProcessor { constructor() { super(); this.capacity = sampleRate * 2; // 2 Sekunden Puffer je Kanal this.ring = [new Float32Array(this.capacity), new Float32Array(this.capacity)]; this.writeIdx = 0; this.readIdx = 0; this.available = 0; // gepufferte Frames (nicht Samples) this.port.onmessage = (e) => this._enqueue(e.data); } _enqueue([left, right]) { const n = left.length; for (let i = 0; i < n; i++) { const idx = (this.writeIdx + i) % this.capacity; this.ring[0][idx] = left[i]; this.ring[1][idx] = right[i]; } this.writeIdx = (this.writeIdx + n) % this.capacity; this.available = Math.min(this.capacity, this.available + n); } process(_inputs, outputs) { const output = outputs[0]; const frames = output[0].length; const playable = Math.min(frames, this.available); for (let i = 0; i < playable; i++) { const idx = (this.readIdx + i) % this.capacity; output[0][i] = this.ring[0][idx]; output[1][i] = this.ring[1][idx]; } for (let i = playable; i < frames; i++) { output[0][i] = 0; output[1][i] = 0; } this.readIdx = (this.readIdx + playable) % this.capacity; this.available -= playable; return true; // Prozessor am Leben halten, auch wenn gerade nichts ankommt } } registerProcessor('pcm-player', PcmRingBufferProcessor);