NR/NB modes, SNB/ANF flags, and drive % were stored only in the widgets
themselves (Btn*.Tag, TrkDrive.Position) — the last hard blocker to a
headless/daemon mode, since without those controls the state had nowhere
to live and CAT/Web read them directly.
- NR/NB: BtnNR.Tag/BtnNB.Tag -> FController.FNRMode/FNBMode (Integer).
- SNB/ANF: BtnSNB.Tag/BtnANF.Tag (0/1) -> FController.FSNB/FANF (Boolean).
- Drive %: TrkDrive.Position -> new FController.FDrivePercent; the slider
is now pure input (TrkDriveChange pushes its value into the controller),
while FDriveLevel stays the derived hardware byte.
Buttons/sliders become pure view+input: click handlers and UpdateXButton
read/write the controller field; CAT getters, the web snapshot, and
GetSnapshot all source state from the controller, not the widgets.
No behavioral change. Builds clean (cocoa).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decouple MainForm from CAT/Web by introducing a headless core,
TRadioController, as the single home for radio state and engine objects.
This is groundwork for swappable UIs, a daemon (Web-only) mode, and a
future hardware control panel — each becomes a frontend/adapter over the
same controller API.
RadioController.pas (new):
- Radio state + engine objects (Network/DSP/Audio/Settings/Channels) as
public fields named exactly as in MainForm, so wiring is mechanical.
- Command API: absolute Set* plus relative *By/Up/Down (for encoders).
- OnStateChanged(field) notifications and GetSnapshot for frontend sync.
- Invoke/OnInvoke (TUIInvokeEvent) thread-marshalling seam (GUI uses
TThread.Synchronize; daemon supplies its own).
- Commands are scaffolding for now (update field + fire event); engine
logic stays in MainForm and migrates in later phases (TODO-wiring).
MainForm.pas:
- Owns FController; creates it first in FormCreate, frees it last in
FormDestroy. Engines are still created/freed by MainForm into the
controller's fields (ownership move is a later phase).
- ~1400 FXxx references redirected to FController.FXxx (implementation
only); dead field declarations removed from the interface.
No behavioral change — pure state relocation. Builds clean (cocoa).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a "Send audio stream to radio (built-in speaker)" checkbox on the
Settings → Audio tab. It is a single switch for the radio's built-in
speaker:
- ON — RX audio is streamed to the radio via the DDC Audio packet
(port 1028, 64 L/R pairs per packet) and the speaker is unmuted
(High Priority byte 1400 bit1 = 0).
- OFF — speaker is muted (byte 1400 bit1 = 1, ANAN-8000DLE IO1 output)
and no audio stream is sent.
The float->int16 conversion and 64-pair packet accumulation live in
THPSDRNetwork (SendSpeakerAudio/SetSpeakerAudio), next to SendDDCAudio;
MainForm only reads the setting and forwards the audio. State is
persisted per-MAC (send_audio_to_radio, default off) and re-applied on
each radio start.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the existing Wideband gradient fill option for the main spectrum:
a "Spectrum gradient fill" checkbox on the Display settings tab gates the
under-curve gradient in TSpectrumView.DrawSpectrum. The curve, grid,
filter band and VFO markers always draw; only the gradient is optional.
Persisted as spectrum_fill in settings JSON (default True, preserving
current appearance). Wired through Settings -> SettingsForm visibility
event -> MainForm -> TSpectrumView.FillSpectrum, same path as
WidebandFill.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The span-selector panel was rendered directly onto the spectrum canvas
every frame via DrawSelf, doing ~30 TextWidth + ~11 TextOut Canvas calls.
On Cocoa these text ops are very expensive (~10.6 ms/frame, ~30% of a
core) and dominated DrawSpectrum.
Render the panel once into a per-pixel-alpha cache bitmap and only
composite it over the spectrum each frame; rebuild only when state
changes (rate, hover, hidden, bounds). The cache is built with the
two-background method (render over black and white, derive straight
alpha + color) so layered semi-transparent button fills and antialiased
text composite over the live spectrum exactly as before.
Cuts the overlay phase from ~10.6 ms to ~0.2 ms per frame; DrawSpectrum
drops ~13.8 ms -> ~2.7 ms. Mirrors the caching VfoOverlay already uses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On Cocoa the full-bitmap alpha-restore pass is unnecessary: Canvas
operations keep alpha at $FF and the cached grid background is opaque,
so removing it cuts a full-frame O(W*H) pass per spectrum frame with no
visual change (verified on macOS).
Kept under {$IFNDEF DARWIN} for Qt6/win32 (different drawing backends
may zero alpha on antialiased edges) until visually verified there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WaterfallView: drop per-frame TLazIntfImage + LoadFromIntfImage (device
bitmap recreation + full pixel-format conversion every frame). Write
pixels directly into the bitmap via BeginUpdate/ScanLine like
SpectrumView already does, with platform-native byte order (ARGB on
Cocoa, BGRA elsewhere).
Replace per-pixel float colorization with a 512-entry dB->color LUT
rebuilt only when WfLow/WfHigh/theme change beyond a small threshold.
Turn the bitmap itself into a ring buffer: each frame builds a single
new row and writes one ScanLine; scrolling is done at paint time via two
CopyRect calls. Removes both full-frame passes per frame (image memmove
+ buffer->bitmap copy), cutting per-frame cost from O(W*H) to O(W). TX
mode preserved by leaving out-of-span columns untouched in the row buf.
SpectrumView: rewrite the per-frame alpha-restore pass from a strided
per-byte loop to a sequential per-dword OR with the alpha mask
(bit-identical output, sequential memory access).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On macOS Cocoa, TBitmap.pf32bit stores pixels in ARGB order
(byte0=A, byte1=R, byte2=G, byte3=B) rather than BGRA used on
Linux/Windows. All raw ScanLine pixel blending and channel mapping
is now guarded with { DARWIN} to use the correct offsets.
Also fix PortAudio library discovery on macOS: probe .dylib names
(including Homebrew paths) before falling through to .so/.dll.