Skip to content

[codex] Stabilize MIDI jog-wheel VFO tuning#2422

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
s53zo:codex/fix-midi-jog-wheel-vfo-tuning
May 7, 2026
Merged

[codex] Stabilize MIDI jog-wheel VFO tuning#2422
ten9876 merged 1 commit into
aethersdr:mainfrom
s53zo:codex/fix-midi-jog-wheel-vfo-tuning

Conversation

@s53zo

@s53zo s53zo commented May 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #2421.

This stabilizes MIDI VFO Tune Knob behavior for touch-sensitive DJ controllers such as the Hercules DJControl Starlight. The fix keeps the existing radio command path (slice tune <id> <freq> autopan=0) but corrects how MIDI learn and MIDI relative tuning compute the target before that command is issued.

Root Cause

The Starlight jog wheel emits two different kinds of events:

  • a touch NoteOn before wheel movement
  • relative CC pulses for movement (1 one direction, 127 the other)

AetherSDR could learn the touch sensor instead of the movement CC, which made press/release look like VFO commands. Separately, relative tuning based each new target on SliceModel::frequency(), which can be rewritten by delayed radio RF_frequency echoes while tune commands are still in flight.

That combination made the wheel feel jumpy: the app could bind to the wrong source, interpret relative pulses as absolute endpoints, or base the next step on stale radio state.

What Changed

  • Slider-style MIDI learn now ignores touch-only NoteOn events and waits for continuous movement data (CC or PitchBend).
  • Learned VFO Tune Knob CC bindings default to relative mode.
  • Existing non-relative VFO bindings that emit common 1/127 relative pulses are handled as relative pulses for backward compatibility.
  • VFO Tune Knob detents are emitted exactly, without MIDI-side acceleration or halving.
  • MainWindow keeps a short-lived MIDI tune target while the wheel is active, so repeated pulses build from local intent instead of delayed radio echo.
  • The MIDI control help text now documents the learn behavior and VFO detent semantics.

User Impact

Operators using touch-sensitive MIDI controllers should be able to map jog wheels to VFO Tune Knob without accidental large jumps. Existing bad touch-sensor bindings should be ignored at runtime, but users should still re-learn the VFO control by rotating the wheel so the saved binding points at the movement CC.

Validation

  • Captured live Starlight MIDI messages with a small RtMidi probe on macOS/CoreMIDI.
  • Confirmed jog movement emits relative CC pulses and touch emits NoteOn events.
  • Built the app:
cmake --build build --target AetherSDR -j4
  • Ran the MIDI settings regression test:
ctest --test-dir build -R midi_settings_test --output-on-failure

Follow-up

A separate enhancement could combine Starlight coarse/fine CC pairs for 14-bit slider resolution, but that is intentionally out of scope for this VFO jog-wheel fix.

Touch-sensitive DJ controllers can emit a NoteOn touch sensor before their continuous CC movement data, and radio RF_frequency echoes can lag behind rapid tune commands. That combination made learned VFO bindings and subsequent wheel steps jump or fight stale radio state.

This teaches MIDI learn to wait for continuous movement data for slider-style targets, decodes common 1/127 relative CC pulses for VFO tuning, and keeps a short-lived MIDI tune target so the next wheel step is based on local intent rather than delayed radio echo.

Constraint: Hercules DJControl Starlight jog wheels emit touch NoteOn events before rotation CC events.

Constraint: SliceModel accepts asynchronous RF_frequency status echoes from the radio while tune commands are in flight.

Rejected: Keep MIDI-side acceleration for VFO tune | controller detents already encode movement density and extra acceleration caused overshoot.

Confidence: medium

Scope-risk: narrow

Directive: Do not bind slider-style MIDI targets to touch NoteOn events unless the target explicitly represents a button or gate.

Tested: cmake --build build --target AetherSDR -j4

Tested: ctest --test-dir build -R midi_settings_test --output-on-failure

Not-tested: Physical radio command timing across high-latency SmartLink paths
@s53zo s53zo marked this pull request as ready for review May 6, 2026 20:00

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @s53zo — this is a thorough, well-scoped fix with a clear root-cause writeup and good in-line comments explaining the why (which makes the diff easy to follow). The three concerns it addresses (touch-sensor learn confusion, unit-pulse misinterpretation, and stale frequency() echo as the next base) are all real and the chosen mitigations look right. A few small notes:

Looks good

  • Touch-NoteOn filter is correctly scoped to MidiParamType::Slider learn, so button bindings still capture NoteOn.
  • The m_midiTuneIdleTimer member + lambda is constructed on the main thread and the relativeAction connection arrives queued from m_extCtrlThread, so all timer mutation happens on the main thread — no affinity issues.
  • setVfoFrequency() for immediate cursor feedback follows the same pattern already used at MainWindow.cpp:2926/8456/10280.
  • The lock branch correctly clears both m_midiTuneTargetMhz and stops the idle timer.

Minor — feel free to fold in or punt

  1. MainWindow.cpp re-snap clause is hard to reach. In the guard:

    if (m_midiTuneTargetMhz < 0.0
        || (!m_midiTuneIdleTimer.isActive()
            && std::abs(m_midiTuneTargetMhz - s->frequency()) > 0.001)) {

    The single-shot timer's timeout lambda always resets m_midiTuneTargetMhz to -1.0, so the second clause is only reachable in the brief window between QTimer going inactive and the timeout slot running. It works as defensive belt-and-suspenders but a one-line comment to that effect would save the next reader a few minutes.

  2. accumulateRelativeStep is a private member but it's a pure transformation over m_relativeAccum and m_relativeTimer. Not a blocker, just noting it could be a free function in the anon namespace if you want to keep it symmetric with relativeCcDelta / isUnitRelativeCcPulse.

  3. Pre-existing, not yours: the static int slowDivider inside flushRelativeAccum is shared across all relative params (so two simultaneous non-VFO relative knobs would interleave its parity). This PR doesn't make it worse — VFO tune now bypasses that branch — but worth flagging as a follow-up if anyone hits it.

  4. 250 ms idle window is reasonable for typical jog-then-stop cadence; might want it as a named constant if it ever needs tuning per controller, but fine as a magic number for now.

No blocking issues from me. Nice work isolating the touch vs. movement events with the RtMidi probe before patching.

@ten9876 ten9876 merged commit 0228630 into aethersdr:main May 7, 2026
5 checks passed
@ten9876

ten9876 commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Claude here — three independent failure modes (touch-sensor learn, legacy unit-pulse decoding, radio-echo lag) cleanly separated into three layered fixes. The in-flight tune target with the 1 kHz divergence re-anchor is a nice touch — handles the mixed mouse/MIDI case without extra state machines. Backward-compat for legacy bindings is the cherry on top. Thanks for the live RtMidi capture work, s53zo. Merged.

73, Jeremy KK7GWY & Claude (AI dev partner)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MIDI VFO Tune Knob jumps with touch-sensitive DJ jog wheels

2 participants