fix(cw): make the waterfall redraw as new spectra arrive

The waterfall backed its pixels with a plain FloatArray and never
signalled Compose, so the Canvas drew once (empty) and stayed frozen —
no spectrum ever appeared. Add a monotonic frame-counter State that
pushSamples bumps per FFT frame; the Canvas reads it in composition to
trigger redraws. Also switch to log-ish intensity scaling so quiet bins
stay dark while strong CW tones pop, matching the DeepCW look.

Applies to both the CW decode screen and the radar transceiver panel.
This commit is contained in:
mckero committed 2026-08-08 15:04:12 +00:00
1 parent 5dd7a35a23
commit ec40f29f28
2 files changed
+22 -5

No files matched your search

@@ -368,12 +368,15 @@ class CwWaterfallState {
private var colCount = 0
private val pending = ArrayList<Float>(4096)
private val fftWindow = 512
private var acc = 0f
private var accN = 0
/** Monotonic frame counter — reading this in composition triggers redraw. */
private val _version = androidx.compose.runtime.mutableIntStateOf(0)
val version: androidx.compose.runtime.State<Int> = _version
@Synchronized
fun pushSamples(samples: FloatArray) {
pending.addAll(samples.toList())
var frames = 0
while (pending.size >= fftWindow) {
val window = FloatArray(fftWindow) { pending[it] }
repeat(fftWindow) { pending.removeAt(0) }
@@ -387,7 +390,9 @@ class CwWaterfallState {
data[(rows - 1) * columns + c] = spectrum[bin.coerceAtMost(spectrum.size - 1)]
}
if (colCount < rows) colCount++
frames++
}
if (frames > 0) _version.intValue++
}
/** Magnitude spectrum via radix-2 FFT on a Hann-windowed frame. */
@@ -466,6 +471,8 @@ class CwWaterfallState {
private fun CwWaterfallView(state: CwWaterfallState) {
val columns = 160
val rows = 64
// Read the frame counter so the canvas redraws as new spectra arrive.
state.version.value
Canvas(
modifier = Modifier
.fillMaxSize()
@@ -478,9 +485,10 @@ private fun CwWaterfallView(state: CwWaterfallState) {
val col = state.getColumn(c)
for (r in 0 until rows) {
val v = col[r]
val intensity = (v * 4000f).coerceIn(0f, 255f)
if (intensity > 6f) {
val green = (intensity * 1.3f).coerceAtMost(255f)
// log-ish scaling: quiet bins stay dark, strong CW tones pop
val intensity = (kotlin.math.ln1p(v * 600f) * 55f).coerceIn(0f, 255f)
if (intensity > 4f) {
val green = (intensity * 1.25f).coerceAtMost(255f)
drawRect(
color = Color(0f, green / 255f, 0f, 1f),
topLeft = Offset(c * cellW, r * cellH),
@@ -906,9 +906,14 @@ private class CwPanelWaterfallState {
private val pending = ArrayList<Float>(4096)
private val fftWindow = 512
/** Monotonic frame counter — reading this in composition triggers redraw. */
private val _version = androidx.compose.runtime.mutableIntStateOf(0)
val version: androidx.compose.runtime.State<Int> = _version
@Synchronized
fun pushSamples(samples: FloatArray) {
pending.addAll(samples.toList())
var frames = 0
while (pending.size >= fftWindow) {
val window = FloatArray(fftWindow) { pending[it] }
repeat(fftWindow) { pending.removeAt(0) }
@@ -920,7 +925,9 @@ private class CwPanelWaterfallState {
val bin = c * spectrum.size / columns
data[(rows - 1) * columns + c] = spectrum[bin.coerceAtMost(spectrum.size - 1)]
}
frames++
}
if (frames > 0) _version.intValue++
}
private fun computeSpectrum(window: FloatArray): FloatArray {
@@ -995,6 +1002,8 @@ private class CwPanelWaterfallState {
private fun CwPanelWaterfallView(state: CwPanelWaterfallState) {
val columns = 120
val rows = 48
// Read the frame counter so the canvas redraws as new spectra arrive.
state.version.value
Canvas(
modifier = Modifier
.fillMaxSize()