Files
amethyst/nestsClient
Claude 6e6fc4cabb feat(nestsClient): listener audio pipeline (Opus decode + AudioTrack)
Phase 3d-1 of the Clubhouse/nests integration. Adds the listener-side
audio pipeline that turns a SubscribeHandle's Flow<MoqObject> into
audible PCM through Android's MediaCodec + AudioTrack. Encoder /
AudioRecord (speaker publishing) lands in Phase 4.

commonMain (nestsclient.audio):
- `AudioFormat` constants — 48 kHz mono signed-16-bit, 20 ms frames
  (960 samples), matching the nests Opus profile.
- `OpusDecoder` interface — stateful per-track decoder.
- `AudioPlayer` interface — start/enqueue/stop, suspend on backpressure.
- `AudioRoomPlayer` — wires a Flow<MoqObject> through OpusDecoder into
  AudioPlayer. Decoder errors are surfaced via an `onError` callback
  but don't tear down the loop (one bad packet shouldn't kill the
  room); player errors are fatal. play()/stop() are single-shot per
  instance, with stop() idempotent and double-release-safe.
- `AudioException` with three canonical kinds (DecoderError,
  DeviceUnavailable, PlaybackFailed).

androidMain:
- `MediaCodecOpusDecoder` — wraps `MediaCodec("audio/opus")` with the
  RFC 7845 §5.1 Opus identification header in csd-0 and zeroed
  pre-skip / seek-pre-roll in csd-1 / csd-2. Drains the output queue
  per-packet, handles INFO_OUTPUT_FORMAT_CHANGED gracefully.
- `AudioTrackPlayer` — wraps `AudioTrack` in MODE_STREAM with USAGE_
  VOICE_COMMUNICATION / CONTENT_TYPE_SPEECH so the OS treats audio-
  room playback like a phone call (call-volume rocker, ducks
  notifications). Buffer = 4× minimum so the producer can fall behind
  ~80 ms (roughly the WebTransport datagram jitter on mobile).

Tests (commonTest, fake-based):
- `AudioRoomPlayerTest` — 7 cases covering happy-path decode +
  enqueue, decoder failure with continued loop, stop idempotency,
  double-play rejection, play-after-stop rejection, empty-PCM
  skip-enqueue, and live channel-backed flow.

The real MediaCodec / AudioTrack code is thin glue against Android
APIs and is validated manually on device — no Robolectric here, the
seams are the Fake* doubles.

Next: Phase 3d-2 wires NestsClient end-to-end (HTTP auth → MoqSession
listener subscribe → AudioRoomPlayer) plus the Amethyst-side
AudioRoomViewModel. After that the only remaining pure-MoQ work is
the Kwik handshake (Phase 3b-2).

https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
2026-04-22 07:12:25 +00:00
..