/** * mobile_audio.js — RX (listen) + TX (microphone) audio for HAMCTRL Mobile. * * Deliberately NOT lifted wholesale from ws.js — that file's audio code is * tightly coupled to its own always-on connection lifecycle (RX auto-starts * on every WS open, no toggle) and to desktop-only pieces (TxEq filter * chain, VU-meter DOM, output-sinkId picker) mobile doesn't need. This * ports just the parts that matter (same wire format, same jitter-buffer * tuning, same WebRTC signaling contract as the backend already expects), * with two deliberate differences from desktop: * * 1. RX must be started by an explicit tap (Mobile.toggleRxAudio()) — * mobile browsers require a real user gesture before an AudioContext * is allowed to play anything; desktop just "hopes" for an incidental * first click/keypress since it has no RX toggle left in its UI. * * 2. The TX microphone is acquired FRESH on every PTT press and fully * released (track.stop()) on every release — never kept alive between * presses. Desktop tried keeping a mic stream open continuously once * (see ws.js's "REVERTED (2026-08-21)" comment) and hit a live bug: on * Windows, any app holding a mic open ducks/mutes ALL other system * audio — including this app's OWN RX playback — for as long as the * mic stays open, not just while actually transmitting. A microphone * permission, once granted, re-acquires in tens of ms on every later * getUserMedia call (no repeat prompt) — so there's little latency to * gain by keeping the stream open, and doing so risks hitting the * same class of bug on whatever OS a given phone runs. * * Backend contract (see webapp.py / webrtc_rx_audio.py / webrtc_audio.py): * RX and TX both go over WebRTC (aiortc), signaling over the MAIN app WS. * RX: server creates the offer ({type:'webrtc_rx_offer'}), browser answers * ({type:'webrtc_rx_answer'}) - opposite direction from TX, since the * server has the media to send. ICE both ways via * webrtc_rx_ice/webrtc_ice. Source: AudioStream._rx_loop's PCM * capture (audio_stream.py), the same one that already fed DeepCW/ * the local waterfall - not a second capture. * TX: browser/mic offers ({type:'webrtc_offer'}), server answers * ({type:'webrtc_answer'}), sent here via window.WS.send (mobile.js's * shim), errors via webrtc_error (mobile.js forwards those to * MobileAudio.onAnswer/onRemoteIce/onWebrtcError). */ (function () { 'use strict'; // ── RX (listen) — over WebRTC ──────────────────────────────────────────── // Was direct-to-ham_audio.exe WS/Opus, A/B tested against WebRTC, then // switched over for good (2026-08-24, live-confirmed clearly better with // 1 listener; WS stalled everything behind one lost/delayed TCP segment on // LTE - classic head-of-line blocking - while a lost UDP packet here is // just a small glitch). Server has the media (creates the offer, opposite // of TX mic below, where the browser/mic offers). Playback via a plain //