first commit
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user