Audio Volume Indicator

The audio indicator gives you a visual feedback when a user is speaking. To understand who’s speaking we provide call.state.dominantSpeaker and call.state.activeSpeakers. On the participant you can observe participant.speaking, participant.dominantSpeaker, participant.audioLevel and participant.audioLevels.

This example shows how to render a visual UI indicator that changes based on the audio level.

First, let’s create some fake audio data for the preview and create the preview

fun fakeAudioState(): StateFlow<List<Float>> {
    val audioFlow = flow {
        val audioLevels = mutableListOf(0f, 0f, 0f, 0f, 0f)
        while (true) {
            val newValue = Random.nextFloat()
            audioLevels.removeAt(0)
            audioLevels.add(newValue)
            emit(audioLevels.toList())
            delay(300)
        }
    }
    return audioFlow.stateIn(
        scope = CoroutineScope(Dispatchers.Default),
        started = SharingStarted.Eagerly,
        initialValue = listOf(0f, 0f, 0f, 0f, 0f)
    )
}

@Preview(showBackground = true)
@Composable
fun AudioPreview() {
    val audioLevels by fakeAudioState().collectAsState()
    MyApplicationTheme {
        AudioVolumeIndicator(audioLevels)
    }
}

Next here’s a little custom audio visualization:

@Composable
fun AudioVolumeIndicator(audioState: List<Float>) {
    // based on this fun blogpost: https://proandroiddev.com/jetpack-compose-tutorial-replicating-dribbble-audio-app-part-1-513ac91c02e3
    val infiniteAnimation = rememberInfiniteTransition()
    val animations = mutableListOf<State<Float>>()

    repeat(5) {
        val durationMillis = Random.nextInt(500, 1000)
        animations += infiniteAnimation.animateFloat(
            initialValue = 0f,
            targetValue = 1f,
            animationSpec = infiniteRepeatable(
                animation = tween(durationMillis),
                repeatMode = RepeatMode.Reverse,
            )
        )
    }

    Canvas(modifier = Modifier.width(45.dp).padding(horizontal = 12.dp)) {
        val canvasCenterY = 0
        var startOffset = 0f
        val barWidthFloat = 10f
        val barMinHeight = 0f
        val barMaxHeight = 150f
        val gapWidthFloat = 1f

        repeat(5) { index ->
            val currentSize = animations[index % animations.size].value
            var barHeightPercent = audioState[index] + currentSize
            if (barHeightPercent > 1.0f) {
                val diff = barHeightPercent - 1.0f
                barHeightPercent = 1.0f - diff
            }
            val barHeight = barMinHeight + (barMaxHeight - barMinHeight) * barHeightPercent
            drawLine(
                color = Color(0xFF9CCC65),
                start = Offset(startOffset, canvasCenterY - barHeight / 2),
                end = Offset(startOffset, canvasCenterY + barHeight / 2),
                strokeWidth = barWidthFloat,
                cap = StrokeCap.Round,
            )
            startOffset += barWidthFloat + gapWidthFloat
        }
    }
}

And here’s how you can use your custom volume indicator for a single video renderer by implementing labelContent composable parameter on ParticipantVideo component like the example below:

CallContent(
    modifier = Modifier.background(color = VideoTheme.colors.appBackground),
    videoRenderer = { modifier, call, participant, style ->
        ParticipantVideo(
            modifier = modifier,
            call = call,
            participant = participant,
            style = style,
            labelContent = {
                val fakeAudio by fakeAudioState().collectAsState()
                ParticipantLabel(
                    participant = participant,
                    soundIndicatorContent = {
                        AudioVolumeIndicator(fakeAudio)
                    }
                )
            }
        )
    },
  ..
)

The end result looks like this:

Audio Volume Indicator

© Getstream.io, Inc. All Rights Reserved.