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.
This commit is contained in:
		| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<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() | ||||
| @@ -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), | ||||
|   | ||||
| @@ -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<FloatArray?>(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)) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	![github-actions[bot]@users.noreply.github.com](/assets/img/avatar_default.png) GitHub Actions Bot
					GitHub Actions Bot