Gogledd-Orllewin

Log

Android

Audio processing

Analyse audio from ExoPlayer to create reactive visuals.

Audio processor

package io.orllewin.pcr.radio

import androidx.media3.common.C
import androidx.media3.common.audio.AudioProcessor
import androidx.media3.common.audio.BaseAudioProcessor
import androidx.media3.common.util.UnstableApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.sqrt

/**
 * Pass-through processor that calculates a smoothed RMS amplitude from the audio stream and
 * exposes it as a flow.
 */
@UnstableApi
class AmplitudeProcessor : BaseAudioProcessor() {

    private var encoding = C.ENCODING_INVALID

    private val _amplitude = MutableStateFlow(0f)
    val amplitude: StateFlow = _amplitude.asStateFlow()

    override fun onConfigure(
        inputAudioFormat: AudioProcessor.AudioFormat
    ): AudioProcessor.AudioFormat {
        encoding = inputAudioFormat.encoding
        return inputAudioFormat // pass-through, no format change
    }

    override fun queueInput(inputBuffer: ByteBuffer) {
        val remaining = inputBuffer.remaining()
        if (remaining == 0) return

        // Duplicate buffer
        computeAmplitude(inputBuffer.duplicate().order(inputBuffer.order()))

        // Then forward the original untouched
        replaceOutputBuffer(remaining).put(inputBuffer).flip()
    }

    override fun onReset() {
        super.onReset()
        encoding = C.ENCODING_INVALID
        _amplitude.value = 0f
    }

    private fun computeAmplitude(buf: ByteBuffer) {
        var sum = 0.0
        var count = 0

        when (encoding) {
            C.ENCODING_PCM_16BIT -> {
                val shorts = buf.order(ByteOrder.nativeOrder()).asShortBuffer()
                while (shorts.hasRemaining()) {
                    val s = shorts.get().toFloat() / Short.MAX_VALUE
                    sum += s * s
                    count++
                }
            }
            C.ENCODING_PCM_FLOAT -> {
                val floats = buf.order(ByteOrder.nativeOrder()).asFloatBuffer()
                while (floats.hasRemaining()) {
                    val s = floats.get()
                    sum += s * s
                    count++
                }
            }
            else -> return
        }

        if (count == 0) return

        // Root mean square:
        // https://en.wikipedia.org/wiki/Root_mean_square
        // https://rosettacode.org/wiki/Averages/Root_mean_square
        val rms = sqrt(sum / count).toFloat().coerceIn(0f, 1f)

        // Fast attack, slow decay tail.
        val current = _amplitude.value
        val smoothed = if (rms > current) {
            current + (rms - current) * 0.65f
        } else {
            current + (rms - current) * 0.15f
        }
        _amplitude.value = smoothed.coerceIn(0f, 1f)
    }
}

ExoPlayer sink

val amplitudeProcessor = AmplitudeProcessor()
//...
val renderersFactory = object : DefaultRenderersFactory(context) {
    override fun buildAudioSink(
        context: Context,
        enableFloatOutput: Boolean,
        enableAudioTrackPlaybackParams: Boolean
    ): AudioSink {
        return DefaultAudioSink.Builder(context)
            .setAudioProcessorChain(
                DefaultAudioSink.DefaultAudioProcessorChain(amplitudeProcessor)
            )
            .setEnableFloatOutput(enableFloatOutput)
            .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
            .build()
    }
}

val exoPlayer = ExoPlayer.Builder(context, renderersFactory).build()

Read amplitude from flow


val amplitude: StateFlow = amplitudeProcessor.amplitude