feat(nests): pulse the speaker ring with live audio level
Adds an end-to-end audio-amplitude pipeline so on-stage speakers' green ring throbs in time with their voice (closer to Spaces / Clubhouse than the previous binary "is in speakingNow" indicator). NestPlayer.play() now takes an onLevel callback and computes the normalized peak of each decoded 16-bit PCM frame. NestViewModel exposes audioLevels: StateFlow<Map<String, Float>>; raw 50 Hz updates from the decode loop are coalesced into a 10 Hz publish tick (LEVEL_TICK_MS) so the StateFlow doesn't spam recompositions across a busy stage. The map is cleared on speaker close, on the speaking-timeout sweep, and on teardown. In MemberCell the speaking ring's width animates between AVATAR_RING_WIDTH (3 dp) and MAX_RING_WIDTH (7 dp) via animateDpAsState, smoothing the tick into a continuous halo. Muted-publisher and idle states are unchanged. Adds NestPlayerTest coverage for the new callback (level math + empty-PCM short-circuit).
This commit is contained in:
+44
@@ -150,6 +150,50 @@ class NestPlayerTest {
|
||||
sut.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun on_level_reports_normalized_peak_per_decoded_frame() =
|
||||
runTest {
|
||||
// Decode each input byte to a single 16-bit sample with a
|
||||
// known peak so we can assert the level math directly.
|
||||
// 0x01 -> 1/32768 (~0), 0x40 -> 0x4000 (0.5), 0x7F -> 0x7FFF (max).
|
||||
val decoder = FakeOpusDecoder { bytes -> ShortArray(1) { (bytes[0].toInt() shl 8).toShort() } }
|
||||
val player = FakeAudioPlayer()
|
||||
val levels = mutableListOf<Float>()
|
||||
|
||||
val sut = NestPlayer(decoder, player, this)
|
||||
sut.play(
|
||||
objects =
|
||||
flowOf(
|
||||
moqObject(byteArrayOf(0x40)),
|
||||
moqObject(byteArrayOf(0x7F)),
|
||||
),
|
||||
onLevel = { levels.add(it) },
|
||||
)
|
||||
testScheduler.advanceUntilIdle()
|
||||
|
||||
assertEquals(2, levels.size)
|
||||
// 0x40 << 8 = 0x4000 = 16384 → 16384/32768 = 0.5
|
||||
assertTrue(levels[0] in 0.49f..0.51f, "expected ~0.5, got ${levels[0]}")
|
||||
// 0x7F << 8 = 0x7F00 → 0x7F00/32768 ≈ 0.992
|
||||
assertTrue(levels[1] > 0.98f, "expected near-max, got ${levels[1]}")
|
||||
sut.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun on_level_is_skipped_when_decoder_returns_empty_pcm() =
|
||||
runTest {
|
||||
val decoder = FakeOpusDecoder { ShortArray(0) }
|
||||
val levels = mutableListOf<Float>()
|
||||
val sut = NestPlayer(decoder, FakeAudioPlayer(), this)
|
||||
sut.play(
|
||||
objects = flowOf(moqObject(byteArrayOf(0x01))),
|
||||
onLevel = { levels.add(it) },
|
||||
)
|
||||
testScheduler.advanceUntilIdle()
|
||||
assertEquals(0, levels.size)
|
||||
sut.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun objects_arriving_after_play_are_streamed_through_the_pipeline() =
|
||||
runTest {
|
||||
|
||||
Reference in New Issue
Block a user