100 lines
3.5 KiB
Kotlin
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
|
|
)
|
|
}
|
|
}
|
|
}
|