baudy/docs/ggwave-gotchas.md
Corey Johnson 37dd74c30d Refactor server monolith into focused modules with sound-only communication
Split 783-line src/server/index.tsx into:
- src/server/audio.ts: ggwave init, playback, mic listener
- src/server/game.ts: pure game logic, returns GuessResult
- src/server/terminal.ts: console output, startup, handshake routing
- src/pages/phone.tsx: Forge components + serialized client JS

Phone page is fully standalone after load — all communication via ggwave
audio (HELLO/HEY BUDDY handshake, guess responses). Added sendAndWait()
for clean half-duplex request/response flow with configurable timeout.
Server waits 500ms before replying to give phone time to switch to listening.
Added TLS support for getUserMedia on mobile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:57:18 -07:00

28 lines
2.6 KiB
Markdown

# ggwave Gotchas
## WASM
- **WASM heap copies**: `encode()` returns Int8Array backed by WASM heap memory. Must copy data out (`new Uint8Array(rawBytes.length); copy.set(...)`) before using, or it gets corrupted.
- **Same instance required**: Using separate ggwave instances for encode/decode causes WASM heap pointer corruption. Use one instance for both. Max 4 instances allowed.
- **Encode output format**: `encode()` returns Int8Array of raw F32 *bytes*, not Float32Array. Reinterpret with `new Float32Array(bytesCopy.buffer)` for AudioBuffer.
- **API naming**: README says `TxProtocolId` but actual API uses `ProtocolId`. Check `Object.keys(ggwave)` when in doubt.
## iOS Safari Audio
- **AudioContext must be created synchronously** inside a user gesture handler (click/tap). Any `await` before `new AudioContext()` breaks the gesture chain and Safari blocks audio permanently for that context.
- **Silent mode bypass**: `navigator.audioSession.type = 'playback'` (iOS 17+) switches WebAudio from ringer channel to media channel, bypassing the hardware mute switch. Without this, the silent switch kills all WebAudio output.
- **Unlock pattern**: Create AudioContext → play a silent buffer → then await async init. Never reverse this order.
## macOS Microphone (sox + CoreAudio)
- **Explicit device names produce all-zero data**: `sox -t coreaudio "MacBook Air Microphone"` gets zeros due to macOS TCC permission scoping. Only `sox -d` (default device) gets actual mic access granted to the terminal app.
- **Workaround**: Change the default input device in System Settings > Sound > Input, then use `sox -d`. Or install `switchaudio-osx` (`brew install switchaudio-osx`) to change it programmatically.
- **MacBook Air mic disabled when lid is closed**: If using a monitor, the laptop mic won't work. Use the monitor's mic (e.g., Studio Display) instead.
- **Sample rate must match**: ggwave needs matching sample rates for encode/decode. Browser uses 48000Hz. Server must also use 48000Hz — sox will resample from the device's native rate automatically.
## Half-Duplex Audio
- Both sides (server and browser) use the same audible protocol (GGWAVE_PROTOCOL_AUDIBLE_FAST). A `playing` flag stops mic processing during playback to prevent self-hearing/feedback. 300ms gap after playback before resuming listening.
- The browser uses `ScriptProcessorNode` to feed mic audio frames to ggwave for decoding. Frames are accumulated to `samplesPerFrame` size before decoding.
- Browser mic requires `getUserMedia` with `echoCancellation: false`, `noiseSuppression: false`, `autoGainControl: false` to preserve signal integrity.