package dev.adriankuta.visualizer.components import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import kotlin.math.hypot import kotlin.math.min /** * Renders FFT magnitude as vertical bars with smoothing for natural transitions. * * The input is the raw FFT byte array from Visualizer where values are interleaved * real and imaginary parts: [re0, im0, re1, im1, ...]. This composable computes the * magnitude per bin and draws a limited number of bars for readability. * * @param data Interleaved real/imag FFT bytes; null shows a baseline. * @param modifier Compose modifier for sizing and padding. */ @Composable fun FftBarsView( data: ByteArray?, modifier: Modifier = Modifier, ) { Text("FFT (magnitude bars)", style = MaterialTheme.typography.titleMedium) var smoothed by remember { mutableStateOf(null) } // Update smoothed magnitudes whenever new FFT data arrives LaunchedEffect(data) { val bytes = data ?: return@LaunchedEffect if (bytes.size < 2) return@LaunchedEffect val n = bytes.size / 2 val barCount = min(64, n) val target = FloatArray(barCount) { i -> val re = bytes[2 * i].toInt() val im = bytes[2 * i + 1].toInt() val mag = hypot(re.toDouble(), im.toDouble()).toFloat() // Scale to 0..1 range (with a bit of headroom capped later in draw) (mag / 128f).coerceIn(0f, 1.5f).coerceAtMost(1f) } val prev = smoothed val rise = 0.35f val decay = 0.12f smoothed = if (prev == null || prev.size != target.size) { target } else { val out = FloatArray(target.size) for (i in target.indices) { val a = if (target[i] > prev[i]) rise else decay out[i] = prev[i] + (target[i] - prev[i]) * a } out } } Canvas( modifier = modifier .fillMaxWidth() .height(160.dp) .padding(vertical = 4.dp) ) { val w = size.width val h = size.height val values = smoothed if (values != null && values.isNotEmpty()) { val barCount = values.size val barWidth = w / barCount for (i in 0 until barCount) { val v = values[i].coerceIn(0f, 1f) val barHeight = h * v val left = i * barWidth drawRect( color = Color(0xFF8BC34A), topLeft = androidx.compose.ui.geometry.Offset(left, h - barHeight), size = androidx.compose.ui.geometry.Size(barWidth * 0.9f, barHeight) ) } } else { drawLine( color = Color.Gray, start = androidx.compose.ui.geometry.Offset(0f, h - 1f), end = androidx.compose.ui.geometry.Offset(w, h - 1f), strokeWidth = 1f ) } } }