From ba634095d9bbfa473eef7cb1127970fa7a0d181b Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Fri, 29 Aug 2025 11:49:04 +0200 Subject: [PATCH] Refactor: Remove smoothing and throttling from visualizer The visualizer components (FftBarsView and WaveformView) now directly render the raw data received from the `VisualizerController`. - Removed the smoothing logic (using `LaunchedEffect` and `mutableStateOf`) from both `FftBarsView` and `WaveformView`. - The `Canvas` drawing logic in both views now directly processes the input `data` byte array. - In `VisualizerController`, removed the manual throttling mechanism (using `Handler`, `minIntervalMs`, `lastWaveformDispatch`, `lastFftDispatch`). - The `Visualizer.OnDataCaptureListener` now directly invokes the `onWaveform` and `onFft` callbacks with the received data. - The capture rate for the Visualizer is now set to half of the maximum rate. --- .../visualizer/VisualizerController.kt | 29 ++--------- .../visualizer/components/FftBarsView.kt | 51 ++++--------------- .../visualizer/components/WaveformView.kt | 39 +++----------- 3 files changed, 18 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/dev/adriankuta/visualizer/VisualizerController.kt b/app/src/main/java/dev/adriankuta/visualizer/VisualizerController.kt index 956da72..061063a 100644 --- a/app/src/main/java/dev/adriankuta/visualizer/VisualizerController.kt +++ b/app/src/main/java/dev/adriankuta/visualizer/VisualizerController.kt @@ -1,9 +1,6 @@ package dev.adriankuta.visualizer import android.media.audiofx.Visualizer -import android.os.Handler -import android.os.Looper -import android.os.SystemClock /** * Simple wrapper over Android Visualizer to capture global output mix (audio session 0). @@ -15,12 +12,6 @@ class VisualizerController( ) { private var visualizer: Visualizer? = null - // Ensure UI updates happen on the main thread and at a reasonable frame rate - private val mainHandler = Handler(Looper.getMainLooper()) - private val minIntervalMs = 16L // ~60 FPS - @Volatile private var lastWaveformDispatch = 0L - @Volatile private var lastFftDispatch = 0L - /** * Starts capturing audio data via [Visualizer]. * @@ -36,20 +27,14 @@ class VisualizerController( // Use the maximum supported capture size for better resolution val captureSize = Visualizer.getCaptureSizeRange().let { it[1] } v.captureSize = captureSize - val rate = Visualizer.getMaxCaptureRate() // allow max rate; we'll throttle ourselves + val rate = Visualizer.getMaxCaptureRate() / 2 // decent rate v.setDataCaptureListener(object : Visualizer.OnDataCaptureListener { override fun onWaveFormDataCapture( visualizer: Visualizer?, waveform: ByteArray?, samplingRate: Int ) { - if (waveform == null) return - val now = SystemClock.uptimeMillis() - if (now - lastWaveformDispatch >= minIntervalMs) { - lastWaveformDispatch = now - val copy = waveform.copyOf() - mainHandler.post { onWaveform(copy) } - } + if (waveform != null) onWaveform(waveform) } override fun onFftDataCapture( @@ -57,13 +42,7 @@ class VisualizerController( fft: ByteArray?, samplingRate: Int ) { - if (fft == null) return - val now = SystemClock.uptimeMillis() - if (now - lastFftDispatch >= minIntervalMs) { - lastFftDispatch = now - val copy = fft.copyOf() - mainHandler.post { onFft(copy) } - } + if (fft != null) onFft(fft) } }, rate, true, true) v.enabled = true @@ -84,7 +63,5 @@ class VisualizerController( visualizer?.enabled = false visualizer?.release() visualizer = null - lastWaveformDispatch = 0L - lastFftDispatch = 0L } } diff --git a/app/src/main/java/dev/adriankuta/visualizer/components/FftBarsView.kt b/app/src/main/java/dev/adriankuta/visualizer/components/FftBarsView.kt index 553ae01..e0c1412 100644 --- a/app/src/main/java/dev/adriankuta/visualizer/components/FftBarsView.kt +++ b/app/src/main/java/dev/adriankuta/visualizer/components/FftBarsView.kt @@ -7,11 +7,6 @@ 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 @@ -19,7 +14,7 @@ import kotlin.math.hypot import kotlin.math.min /** - * Renders FFT magnitude as vertical bars with smoothing for natural transitions. + * Renders FFT magnitude as vertical bars. * * 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 @@ -34,37 +29,6 @@ fun FftBarsView( 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() @@ -73,13 +37,16 @@ fun FftBarsView( ) { val w = size.width val h = size.height - val values = smoothed - if (values != null && values.isNotEmpty()) { - val barCount = values.size + if (data != null && data.size >= 2) { + val n = data.size / 2 // real/imag pairs count + val barCount = min(64, n) // limit bars for readability val barWidth = w / barCount for (i in 0 until barCount) { - val v = values[i].coerceIn(0f, 1f) - val barHeight = h * v + val re = data[2 * i].toInt() + val im = data[2 * i + 1].toInt() + val mag = hypot(re.toDouble(), im.toDouble()).toFloat() + val scaled = (mag / 128f).coerceIn(0f, 1.5f) + val barHeight = h * (scaled.coerceAtMost(1f)) val left = i * barWidth drawRect( color = Color(0xFF8BC34A), diff --git a/app/src/main/java/dev/adriankuta/visualizer/components/WaveformView.kt b/app/src/main/java/dev/adriankuta/visualizer/components/WaveformView.kt index a370209..a029062 100644 --- a/app/src/main/java/dev/adriankuta/visualizer/components/WaveformView.kt +++ b/app/src/main/java/dev/adriankuta/visualizer/components/WaveformView.kt @@ -7,11 +7,6 @@ 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.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path @@ -19,7 +14,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.dp /** - * Displays the most recent audio waveform as a continuous path with smoothing to reduce jitter. + * Displays the most recent audio waveform as a continuous path. * * @param data Waveform bytes where each value is 0..255 (unsigned), as provided by Visualizer. * Pass null to render an idle baseline. @@ -31,28 +26,6 @@ fun WaveformView( modifier: Modifier = Modifier, ) { Text("Waveform", style = MaterialTheme.typography.titleMedium) - - // Keep a smoothed copy of the latest waveform for softer transitions - var smoothed by remember { mutableStateOf(null) } - - // Update smoothed values whenever new data arrives using exponential smoothing - LaunchedEffect(data) { - val bytes = data ?: return@LaunchedEffect - if (bytes.isEmpty()) return@LaunchedEffect - val target = FloatArray(bytes.size) { i -> ((bytes[i].toInt() and 0xFF) / 255f) } - val prev = smoothed - val alpha = 0.22f // smoothing factor; lower = smoother - smoothed = if (prev == null || prev.size != target.size) { - target - } else { - val out = FloatArray(target.size) - for (i in target.indices) { - out[i] = prev[i] + (target[i] - prev[i]) * alpha - } - out - } - } - Canvas( modifier = modifier .fillMaxWidth() @@ -61,8 +34,7 @@ fun WaveformView( ) { val w = size.width val h = size.height - val values = smoothed - if (values == null || values.isEmpty()) { + if (data == null || data.isEmpty()) { drawLine( color = Color.Gray, start = androidx.compose.ui.geometry.Offset(0f, h / 2), @@ -71,10 +43,11 @@ fun WaveformView( ) } else { val path = Path() - val stepX = w / (values.size - 1).coerceAtLeast(1) - for (i in values.indices) { + val stepX = w / (data.size - 1).coerceAtLeast(1) + for (i in data.indices) { val x = i * stepX - val y = h * (1f - values[i]) + val normalized = (data[i].toInt() and 0xFF) / 255f + val y = h * (1f - normalized) if (i == 0) path.moveTo(x, y) else path.lineTo(x, y) } drawPath(path = path, color = Color.Cyan, style = Stroke(width = 2f))