From 523e8a92ec8cfd368f5f46c250f458293fdadfca Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 20:50:12 +0000 Subject: [PATCH] fix(nests): play audio-room output through STREAM_MUSIC, not STREAM_VOICE_CALL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AudioTrackPlayer` was constructing its `AudioTrack` with `USAGE_VOICE_COMMUNICATION` + `CONTENT_TYPE_SPEECH` so the room would behave like a phone call (volume rocker controls call volume, ducks notifications). The `AudioTrack` then renders on `STREAM_VOICE_CALL`, which Android only services audibly while the device is in `MODE_IN_COMMUNICATION`. Nests never drives `AudioManager.mode` (only NIP-100's `CallAudioManager` does), so on most devices the playback either dropped to the earpiece at near-zero volume or produced no audio at all — making rooms appear silent on both phones. Fix: route through `USAGE_MEDIA` + `CONTENT_TYPE_SPEECH` (= `STREAM_MUSIC`) so audio-room playback comes out of the loudspeaker by default and the volume rocker controls it any time. Same approach Twitter/X Spaces and Clubhouse take for hands-free audio rooms. Echo cancellation still works on the capture side via `MediaRecorder.AudioSource.VOICE_COMMUNICATION` regardless of the playback usage. Also update `NestForegroundService.requestAudioFocus` to request focus under the matching `USAGE_MEDIA` attributes so the focus claim and the playback attributes line up. --- .../service/nests/NestForegroundService.kt | 7 ++++- .../nestsclient/audio/AudioTrackPlayer.kt | 26 +++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nests/NestForegroundService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nests/NestForegroundService.kt index f1de44d8e..c79670aff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nests/NestForegroundService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nests/NestForegroundService.kt @@ -74,6 +74,11 @@ class NestForegroundService : Service() { * call lowers the room volume cleanly instead of mixing two * voices on top of each other. Acquired once per service * lifetime; released in [onDestroy]. + * + * Matches the playback `AudioAttributes` we set on `AudioTrack` + * in `AudioTrackPlayer` (USAGE_MEDIA + CONTENT_TYPE_SPEECH) so + * the focus request applies to the same stream the audio + * actually renders on. */ private fun requestAudioFocus() { if (audioFocusRequest != null) return @@ -81,7 +86,7 @@ class NestForegroundService : Service() { val attrs = android.media.AudioAttributes .Builder() - .setUsage(android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION) + .setUsage(android.media.AudioAttributes.USAGE_MEDIA) .setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH) .build() val request = diff --git a/nestsClient/src/androidMain/kotlin/com/vitorpamplona/nestsclient/audio/AudioTrackPlayer.kt b/nestsClient/src/androidMain/kotlin/com/vitorpamplona/nestsclient/audio/AudioTrackPlayer.kt index c0be0e0dd..01dfddd07 100644 --- a/nestsClient/src/androidMain/kotlin/com/vitorpamplona/nestsclient/audio/AudioTrackPlayer.kt +++ b/nestsClient/src/androidMain/kotlin/com/vitorpamplona/nestsclient/audio/AudioTrackPlayer.kt @@ -21,23 +21,36 @@ package com.vitorpamplona.nestsclient.audio import android.media.AudioAttributes -import android.media.AudioManager import android.media.AudioTrack import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import android.media.AudioFormat as AndroidAudioFormat /** - * [AudioPlayer] backed by Android's [AudioTrack] in `MODE_STREAM`. Targets the - * voice-call usage stream so the OS treats audio-room playback like a phone - * call (volume rocker controls call volume, ducks notifications, etc.). + * [AudioPlayer] backed by Android's [AudioTrack] in `MODE_STREAM`. + * + * Routes through `USAGE_MEDIA` + `CONTENT_TYPE_SPEECH` (= `STREAM_MUSIC`) + * so audio-room playback comes out of the loudspeaker by default and the + * volume rocker controls media volume — same approach Twitter/X Spaces + * and Clubhouse use for hands-free audio rooms. + * + * Originally this used `USAGE_VOICE_COMMUNICATION` to get call-style + * volume / ducking, but that routes through `STREAM_VOICE_CALL`, which + * Android only services audibly while the device is in + * `MODE_IN_COMMUNICATION`. Nests doesn't drive `AudioManager.mode` + * (only the NIP-100 `CallAudioManager` does), so the playback either + * dropped to the earpiece at near-zero volume or produced no audio at + * all depending on the device — making rooms appear silent on both + * phones. Echo cancellation still works on the capture side via + * `MediaRecorder.AudioSource.VOICE_COMMUNICATION` regardless of the + * playback usage. * * Buffer sizing: 4× minimum so the producer can fall behind by ~80 ms before * dropouts, which roughly matches the jitter the WebTransport datagram path * introduces over typical mobile networks. */ class AudioTrackPlayer( - private val usage: Int = AudioAttributes.USAGE_VOICE_COMMUNICATION, + private val usage: Int = AudioAttributes.USAGE_MEDIA, private val contentType: Int = AudioAttributes.CONTENT_TYPE_SPEECH, ) : AudioPlayer { private var track: AudioTrack? = null @@ -144,9 +157,6 @@ class AudioTrackPlayer( runCatching { t.release() } } - @Suppress("unused") - val voiceCallUsage: Int get() = AudioManager.STREAM_VOICE_CALL // kept for documentation - private fun applyMuteVolume(track: AudioTrack) { // setVolume is preferred over pause(): it keeps the streaming pipeline // running so unmute is sample-accurate and there's no AudioTrack-restart