fix(cw): keep the shift marker visible when the pitch readout goes negative

The guard suppressed every marker, the target line included, whenever the
reported pitch was not positive. Shifting a low tone UP makes that routine:
pitch is (loudestBin * 12.5 - shiftHz), so with a 100 Hz tone shifted +700 Hz it
goes negative for 25 of the 65 bins, down to -300 Hz, and updateSignalMetrics
applies no prominence test so mains hum in a key-up gap is enough to park the
argmax down there. 77 reachable (tone, bin) pairs across 100-350 Hz produce it.
The result was the display showing nothing at all while the shift was active -
exactly what the previous commit set out to fix.

The target line is now drawn on the strength of the shift alone, since a shift
being applied is the fact worth showing and it does not depend on the pitch. A
non-positive pitch marks the low edge, which is where such a tone actually is,
and only the numeric label is suppressed because the number itself is nonsense.
A NaN pitch previously slipped past all three comparisons and rendered the HIGH
edge marker labelled "0 Hz"; it now draws the target line only.

TONE_SHIFT_TARGET_HZ reads CwToneShifter.TARGET_HZ instead of recomputing the
window midpoint. The two are equal today by coincidence, not construction:
retuning either would leave the green line marking a frequency nothing is
delivered to, silently. CwToneShifterTest now pins TARGET_HZ inside the window
and clear of its edges, which is the one part of this the JVM suite can hold.

The label side now tips at the target rather than the window maximum, so a pitch
sitting on the upper edge gets its text on the same side as its line.
This commit is contained in:
mckero committed 2026-08-23 01:15:03 +00:00
1 parent 5a45aab2b1
commit 23f47d9122
2 files changed
+80 -36

No files matched your search

@@ -94,6 +94,29 @@ class CwToneShifterTest {
}
}
/**
* The waterfall draws a marker at [CwToneShifter.TARGET_HZ] to show the operator where
* a shifted tone is being delivered. Moving the target outside the model's window, or
* moving the window off the target, would leave that marker pointing at a frequency
* nothing arrives at — and nothing else in the build would object.
*/
@Test
fun `the shift target sits inside the model window, clear of its edges`() {
assertTrue(
"TARGET_HZ ${CwToneShifter.TARGET_HZ} is outside the model window " +
"${CwDeepSpectrogram.MIN_FREQ_HZ}-${CwDeepSpectrogram.MAX_FREQ_HZ} Hz",
CwToneShifter.isInsideWindow(CwToneShifter.TARGET_HZ.toFloat())
)
// Clear of the edges by a decent margin, so a tone landing a little off target
// still lands inside: a target hugging an edge would make the shift pointless.
val margin = (CwDeepSpectrogram.MAX_FREQ_HZ - CwDeepSpectrogram.MIN_FREQ_HZ) / 4
assertTrue(
"TARGET_HZ ${CwToneShifter.TARGET_HZ} is within $margin Hz of a window edge",
CwToneShifter.TARGET_HZ >= CwDeepSpectrogram.MIN_FREQ_HZ + margin &&
CwToneShifter.TARGET_HZ <= CwDeepSpectrogram.MAX_FREQ_HZ - margin
)
}
@Test
fun `out-of-window tones move to the target with no competing tone`() {
for (tone in listOf(150.0, 200.0, 250.0, 300.0, 350.0, 1300.0, 1400.0, 1500.0)) {