Files
Visualizer/app/src/main/java/dev/adriankuta/visualizer/components/FftBarsView.kt
GitHub Actions Bot 2a46c6143b first commit
2025-08-29 11:47:23 +02:00

100 lines
3.5 KiB
Kotlin

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<FloatArray?>(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
)
}
}
}