feat(audio-rooms): M1 listener-only wire-up — Connect button + Connected chip + mute
Wires `connectNestsListener` into the existing NIP-53 audio-room "stage" so
a user can tap Connect, see the live connection state, and hear hosts
speaking. Per the audio-rooms completion plan
(`nestsClient/plans/2026-04-26-audio-rooms-completion.md`) phase M1.
UI (amethyst):
- AudioRoomStage gets a state-chip / Connect / Disconnect / Mute row
above the existing hand-raise. State chip walks ResolvingRoom →
OpeningTransport → MoQ-handshake → Connected; Failed surfaces the
reason inline with a Retry button. The 30312 `service` tag drives
visibility — rooms hosted on non-nests servers show "Audio not
available" and the rest of the stage UI continues to work.
ViewModel (commons):
- New `AudioRoomViewModel` in commons/.../viewmodels/ owns one
`NestsListener` and one `AudioRoomPlayer` per active speaker, exposes
a single `StateFlow<AudioRoomUiState>`, and reconciles subscriptions
whenever the screen pushes a new speaker set via `updateSpeakers`.
Audio-pipeline construction is injected via `decoderFactory` /
`playerFactory` so a future desktop port can reuse the orchestration
once Compose Desktop has WebTransport. Unit-tested with a fake
`NestsListenerConnector` driving state transitions directly.
nestsClient:
- `AudioPlayer.setMuted(Boolean)` joins the interface; `AudioTrackPlayer`
routes it through `AudioTrack.setVolume(0f/1f)`. Mute keeps the
decode + network pipeline running so unmute is sample-accurate.
Build:
- `:commons` commonMain now `implementation(project(":nestsClient"))` so
the shared VM can see `NestsListener` / `AudioRoomPlayer` / interfaces.
Concrete OkHttp / Quic / MediaCodec / AudioTrack actuals stay in
`:nestsClient`'s platform source sets and are bound by an
Android-side `AudioRoomViewModelFactory` co-located with the screen.
Verified: `./gradlew spotlessApply :commons:jvmTest :nestsClient:jvmTest
:quic:jvmTest :amethyst:compilePlayDebugKotlin` all green.
This commit is contained in:
+17
@@ -41,6 +41,7 @@ class AudioTrackPlayer(
|
||||
private val contentType: Int = AudioAttributes.CONTENT_TYPE_SPEECH,
|
||||
) : AudioPlayer {
|
||||
private var track: AudioTrack? = null
|
||||
private var muted: Boolean = false
|
||||
|
||||
override fun start() {
|
||||
if (track != null) return
|
||||
@@ -104,6 +105,7 @@ class AudioTrackPlayer(
|
||||
t,
|
||||
)
|
||||
}
|
||||
applyMuteVolume(newTrack, muted)
|
||||
track = newTrack
|
||||
}
|
||||
|
||||
@@ -122,6 +124,11 @@ class AudioTrackPlayer(
|
||||
}
|
||||
}
|
||||
|
||||
override fun setMuted(muted: Boolean) {
|
||||
this.muted = muted
|
||||
track?.let { applyMuteVolume(it, muted) }
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
val t = track ?: return
|
||||
track = null
|
||||
@@ -133,4 +140,14 @@ class AudioTrackPlayer(
|
||||
|
||||
@Suppress("unused")
|
||||
val voiceCallUsage: Int get() = AudioManager.STREAM_VOICE_CALL // kept for documentation
|
||||
|
||||
private fun applyMuteVolume(
|
||||
track: AudioTrack,
|
||||
muted: Boolean,
|
||||
) {
|
||||
// setVolume is preferred over pause(): it keeps the streaming pipeline
|
||||
// running so unmute is sample-accurate and there's no AudioTrack-restart
|
||||
// glitch. AudioTrack default gain is 1.0 (RFC: AudioTrack#setVolume).
|
||||
runCatching { track.setVolume(if (muted) 0f else 1f) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,17 @@ interface AudioPlayer {
|
||||
*/
|
||||
suspend fun enqueue(pcm: ShortArray)
|
||||
|
||||
/**
|
||||
* Toggle output silence without halting the underlying decode/network
|
||||
* pipeline. The producer keeps pushing PCM into [enqueue]; the device
|
||||
* just stops emitting sound. Default is unmuted.
|
||||
*
|
||||
* Used so the audio-room UI mute button is instant — nothing to
|
||||
* re-handshake on unmute. Setting before [start] is allowed; the value
|
||||
* is applied when the device opens.
|
||||
*/
|
||||
fun setMuted(muted: Boolean)
|
||||
|
||||
/** Stop playback and release resources. After this, the player is unusable. */
|
||||
fun stop()
|
||||
}
|
||||
|
||||
+6
@@ -206,6 +206,8 @@ class AudioRoomPlayerTest {
|
||||
private set
|
||||
val stopped: Boolean get() = stopCount > 0
|
||||
val queued = mutableListOf<ShortArray>()
|
||||
var muted: Boolean = false
|
||||
private set
|
||||
|
||||
override fun start() {
|
||||
started = true
|
||||
@@ -215,6 +217,10 @@ class AudioRoomPlayerTest {
|
||||
queued.add(pcm)
|
||||
}
|
||||
|
||||
override fun setMuted(muted: Boolean) {
|
||||
this.muted = muted
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
stopCount++
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user