WZP-P2-T2: Adaptive quality switching - QualityAdapter with sliding window of QualityReports - Hysteresis: 3 consecutive reports before switching profiles - Thresholds: loss>15%/rtt>200ms→CATASTROPHIC, loss>5%/rtt>100ms→DEGRADED - CallConfig::from_profile() constructor - 5 unit tests: good/degraded/catastrophic conditions, hysteresis, recovery WZP-P2-T3: AudioWorklet migration (web bridge) - audio-processor.js: WZPCaptureProcessor + WZPPlaybackProcessor - Capture: buffers 128-sample AudioWorklet blocks → 960-sample frames - Playback: ring buffer, Int16→Float32 conversion in worklet - ScriptProcessorNode fallback if AudioWorklet unavailable - Existing UI preserved (connect, room, PTT) WZP-P2-T4: Concurrent session management (relay) - SessionManager tracks active sessions with HashMap - Enforces max_sessions limit from RelayConfig - create_session/remove_session lifecycle - Wired into relay main: session created after auth+handshake, cleaned up after run_participant returns - 7 unit tests: create/remove, max enforced, room tracking, info, expiry 207 tests passing across all crates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
143 lines
4.6 KiB
JavaScript
143 lines
4.6 KiB
JavaScript
// WarzonePhone AudioWorklet processors.
|
|
// Both capture and playback handle 960-sample frames (20ms @ 48kHz).
|
|
// AudioWorklet calls process() with 128-sample blocks, so we buffer internally.
|
|
|
|
const FRAME_SIZE = 960;
|
|
|
|
class WZPCaptureProcessor extends AudioWorkletProcessor {
|
|
constructor() {
|
|
super();
|
|
// Pre-allocate ring buffer large enough for several frames
|
|
this._ring = new Float32Array(FRAME_SIZE * 4);
|
|
this._writePos = 0;
|
|
}
|
|
|
|
process(inputs, _outputs, _parameters) {
|
|
const input = inputs[0];
|
|
if (!input || !input[0]) return true;
|
|
|
|
const samples = input[0]; // Float32Array, 128 samples typically
|
|
const len = samples.length;
|
|
|
|
// Write into ring buffer
|
|
if (this._writePos + len > this._ring.length) {
|
|
// Should not happen with FRAME_SIZE * 4 capacity and timely draining,
|
|
// but handle gracefully by resizing
|
|
const bigger = new Float32Array(this._ring.length * 2);
|
|
bigger.set(this._ring.subarray(0, this._writePos));
|
|
this._ring = bigger;
|
|
}
|
|
this._ring.set(samples, this._writePos);
|
|
this._writePos += len;
|
|
|
|
// Drain complete 960-sample frames
|
|
while (this._writePos >= FRAME_SIZE) {
|
|
// Convert Float32 -> Int16 PCM
|
|
const pcm = new Int16Array(FRAME_SIZE);
|
|
for (let i = 0; i < FRAME_SIZE; i++) {
|
|
const s = this._ring[i];
|
|
pcm[i] = s < -1 ? -32768 : s > 1 ? 32767 : (s * 32767) | 0;
|
|
}
|
|
|
|
// Shift remaining data forward
|
|
this._writePos -= FRAME_SIZE;
|
|
if (this._writePos > 0) {
|
|
this._ring.copyWithin(0, FRAME_SIZE, FRAME_SIZE + this._writePos);
|
|
}
|
|
|
|
// Send the Int16 PCM buffer (1920 bytes) to the main thread
|
|
this.port.postMessage(pcm.buffer, [pcm.buffer]);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class WZPPlaybackProcessor extends AudioWorkletProcessor {
|
|
constructor() {
|
|
super();
|
|
// Ring buffer for decoded Float32 samples ready for output
|
|
this._ring = new Float32Array(FRAME_SIZE * 8);
|
|
this._readPos = 0;
|
|
this._writePos = 0;
|
|
this._maxBuffered = FRAME_SIZE * 6; // ~120ms max to prevent drift
|
|
|
|
this.port.onmessage = (e) => {
|
|
// Receive Int16 PCM from main thread, convert to Float32
|
|
const pcm = new Int16Array(e.data);
|
|
const len = pcm.length;
|
|
|
|
// Check capacity
|
|
let available = this._writePos - this._readPos;
|
|
if (available < 0) available += this._ring.length;
|
|
if (available + len > this._maxBuffered) {
|
|
// Too much buffered; drop oldest samples to prevent drift
|
|
this._readPos = this._writePos;
|
|
}
|
|
|
|
// Ensure ring buffer is big enough
|
|
if (this._ring.length < len + available + 128) {
|
|
const bigger = new Float32Array(this._ring.length * 2);
|
|
// Copy existing data contiguously
|
|
if (this._readPos <= this._writePos) {
|
|
bigger.set(this._ring.subarray(this._readPos, this._writePos));
|
|
} else {
|
|
const firstPart = this._ring.subarray(this._readPos);
|
|
const secondPart = this._ring.subarray(0, this._writePos);
|
|
bigger.set(firstPart);
|
|
bigger.set(secondPart, firstPart.length);
|
|
}
|
|
this._ring = bigger;
|
|
const count = available;
|
|
this._readPos = 0;
|
|
this._writePos = count;
|
|
}
|
|
|
|
// Write converted samples into ring buffer linearly (simpler: use linear buffer)
|
|
for (let i = 0; i < len; i++) {
|
|
this._ring[this._writePos] = pcm[i] / 32768.0;
|
|
this._writePos++;
|
|
if (this._writePos >= this._ring.length) this._writePos = 0;
|
|
}
|
|
};
|
|
}
|
|
|
|
process(_inputs, outputs, _parameters) {
|
|
const output = outputs[0];
|
|
if (!output || !output[0]) return true;
|
|
|
|
const out = output[0]; // 128 samples typically
|
|
const needed = out.length;
|
|
|
|
let available;
|
|
if (this._writePos >= this._readPos) {
|
|
available = this._writePos - this._readPos;
|
|
} else {
|
|
available = this._ring.length - this._readPos + this._writePos;
|
|
}
|
|
|
|
if (available >= needed) {
|
|
for (let i = 0; i < needed; i++) {
|
|
out[i] = this._ring[this._readPos];
|
|
this._readPos++;
|
|
if (this._readPos >= this._ring.length) this._readPos = 0;
|
|
}
|
|
} else {
|
|
// Output what we have, zero-fill the rest (underrun)
|
|
for (let i = 0; i < available; i++) {
|
|
out[i] = this._ring[this._readPos];
|
|
this._readPos++;
|
|
if (this._readPos >= this._ring.length) this._readPos = 0;
|
|
}
|
|
for (let i = available; i < needed; i++) {
|
|
out[i] = 0;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
registerProcessor('wzp-capture-processor', WZPCaptureProcessor);
|
|
registerProcessor('wzp-playback-processor', WZPPlaybackProcessor);
|