From 838c3426ba8e6e403f734bcf08ea1eb929aa8aed Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Thu, 28 May 2026 10:51:55 -0400 Subject: [PATCH] chat.html: force G.711/G.722 codec over WebRTC so the modem survives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebRTC defaulted to Opus, a perceptual codec that re-quantizes audio in 20ms frames and smears the MARK/SPACE bit-edges the amplitude modem rides on — which is why the PulseAudio C path does ~800 baud but the WebRTC path couldn't carry frames at all. Pin the audio transceiver to memoryless G.711 (PCMU/PCMA), then G.722, ahead of Opus via setCodecPreferences, restoring a clean amplitude channel. Decoder resolution is now bounded by the AudioWorklet quantum (~375Hz), not the codec. --- web/chat.html | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/web/chat.html b/web/chat.html index b564d93..e142f3c 100644 --- a/web/chat.html +++ b/web/chat.html @@ -1150,6 +1150,28 @@ function ensurePC() { if (!carrierOn) throw new Error('start carrier first'); pc = new RTCPeerConnection(RTC_CONFIG); for (const tr of outboundStream.getTracks()) pc.addTrack(tr, outboundStream); + /* Force a memoryless codec (G.711 PCMU/PCMA, else G.722) ahead of Opus. + * Opus is a perceptual codec that re-quantizes in 20ms frames and smears the + * MARK/SPACE bit-edges this modem rides on; G.711 companding is per-sample, + * preserving the carrier envelope like the PulseAudio path. */ + try { + const caps = (RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities('audio')) || null; + if (caps && caps.codecs) { + const rank = (mime) => { + const m = (mime || '').toLowerCase(); + if (m.endsWith('/pcmu')) return 0; + if (m.endsWith('/pcma')) return 1; + if (m.endsWith('/g722')) return 2; + if (m.includes('opus')) return 99; + return 50; + }; + const ordered = caps.codecs.slice().sort((a, b) => rank(a.mimeType) - rank(b.mimeType)); + for (const t of pc.getTransceivers()) { + if (t.setCodecPreferences) { try { t.setCodecPreferences(ordered); } catch (_) {} } + } + logLine('sys', 'codec preference: G.711/G.722 ahead of Opus (clean amplitude)'); + } + } catch (_) {} pc.ontrack = (ev) => { const stream = ev.streams[0] || new MediaStream([ev.track]); attachInboundTrack(stream, ev.track.id);