Skip to content

Commit 3c94f16

Browse files
committed
modem: show the waterfall while transmitting
Every radio shows you your own signal on transmit; mercury froze the display on key-up, because the spectrum slot was fed only from the capture path. Feed it from the transmitted burst as well. Same slot, same lock, same sequence counter the RX path uses, so the publisher thread and the on-the-wire spectrum frame need no change whatsoever. Only one writer is ever active -- the link is half duplex, so RX decode and TX modulation never overlap -- and the FFT runs inline under the lock exactly as it already does for RX. Two details that decide whether this looks right: - Publishing once per burst would paint a single line every 3.7 s on DATAC16. Instead it publishes from inside the drain loop that send_modulated_data already runs while the burst plays out, indexed by elapsed playout. The ring is filled far faster than it drains, so drain progress -- not write progress -- is what paces the display. - Samples are taken at modem rate, which is what g_spectrum_sample_rate already reports, so the frequency axis matches RX and the display does not jump scale on key-up. Gated by g_spectrum_enabled, the switch the RX FFT already uses, so the UI checkbox and -W turn off both directions at once: one waterfall control, one setter. Unlike RX there is no second consumer to keep it alive -- the channel-busy detector shares the RX FFT, but occupancy means nothing while we are the ones occupying the channel. The stats struct is opened lazily and TX now gets there first -- mercury sends CALL before it has decoded anything -- so the open is done here too. Omitting it ran the first burst of every connect over an unopened MODEM_STATS, which hung the connect; caught by the integration suite, which is the only gate that transmits. Gate: unit suite green; integration 245.1 s against a 245-246 s baseline; mercury and the embedded fyne UI both build, go vet clean.
1 parent dc26a95 commit 3c94f16

2 files changed

Lines changed: 88 additions & 3 deletions

File tree

modem/modem.c

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,65 @@ void modem_set_spectrum_enabled(bool enabled)
400400
atomic_store_explicit(&g_spectrum_enabled, enabled, memory_order_relaxed);
401401
}
402402

403+
/* ---- TX waterfall -------------------------------------------------------
404+
* A radio shows your own signal while you transmit. Mercury froze the display
405+
* on key-up, because the spectrum slot was fed only from the capture path.
406+
* Feed it from the transmitted burst too.
407+
*
408+
* Same slot, same lock, same sequence counter as RX, so the publisher thread
409+
* and the wire format need no change at all. Only one writer is ever active:
410+
* the link is half duplex, so RX decode and TX modulation do not overlap. The
411+
* FFT runs inline under the lock exactly as the RX path does.
412+
*
413+
* Samples are at modem rate -- what g_spectrum_sample_rate already reports --
414+
* so the frequency axis matches RX and the display does not jump on key-up.
415+
*
416+
* Gated by g_spectrum_enabled, the same switch the RX FFT above uses: one
417+
* waterfall control for both directions, so turning it off in the UI stops the
418+
* work rather than computing frames nobody consumes. Unlike RX, there is no
419+
* channel-busy consumer to keep it alive -- occupancy is meaningless while we
420+
* are the ones occupying the channel. */
421+
422+
/* Publish one slice of a burst, `pos` samples in. Called repeatedly while the
423+
* burst drains, so the waterfall scrolls in step with the audio actually going
424+
* out instead of painting a single line per burst. */
425+
static void publish_tx_spectrum_at(const int32_t *buf, size_t total,
426+
size_t pos, int sample_rate)
427+
{
428+
if (!atomic_load_explicit(&g_spectrum_enabled, memory_order_relaxed)
429+
|| buf == NULL || pos >= total)
430+
return;
431+
432+
size_t n = total - pos;
433+
if (n > MODEM_STATS_NSPEC)
434+
n = MODEM_STATS_NSPEC;
435+
436+
static COMP tx_fdm[MODEM_STATS_NSPEC];
437+
for (size_t i = 0; i < n; i++) {
438+
/* tx_sample_with_gain() emits int32 full scale; the spectrum code wants
439+
* raw i16 amplitude and normalises that itself. */
440+
tx_fdm[i].real = (float)(buf[pos + i] >> 16);
441+
tx_fdm[i].imag = 0.0f;
442+
}
443+
444+
pthread_mutex_lock(&g_spectrum_lock);
445+
/* The stats struct is opened lazily, and TX gets there first: mercury sends
446+
* CALL before it has decoded anything, so without this the very first burst
447+
* of a connect runs the FFT over an unopened MODEM_STATS. */
448+
if (!g_spectrum_stats_inited)
449+
{
450+
modem_stats_open(&g_spectrum_stats);
451+
g_spectrum_stats_inited = true;
452+
}
453+
modem_stats_get_rx_spectrum(&g_spectrum_stats, g_rx_spectrum_dB, tx_fdm, (int)n);
454+
g_spectrum_sample_rate = sample_rate;
455+
g_spectrum_seq++;
456+
pthread_mutex_unlock(&g_spectrum_lock);
457+
}
458+
459+
/* One line per publisher tick; faster is wasted, the UI runs at 20 fps. */
460+
#define TX_SPECTRUM_STEP_MS 50
461+
403462
/* --- Channel-busy (occupancy) detector --------------------------------------
404463
* VARA-style "channel busy" detection off the RX spectrum FFT. Opt-in
405464
* (disabled by default); when enabled the RX worker classifies occupancy and
@@ -1157,10 +1216,22 @@ int send_modulated_data(generic_modem_t *g_modem, uint8_t *bytes_in, int frames_
11571216

11581217
write_buffer(playback_buffer, (uint8_t *)tx_buffer, total_samples * sizeof(int32_t));
11591218

1219+
uint64_t next_spec_ms = t0_ms;
11601220
while (!shutdown_ &&
11611221
(size_buffer(playback_buffer) > 0 ||
11621222
time_now_ms() < t0_ms + burst_ms + tail_ms))
1223+
{
1224+
uint64_t now = time_now_ms();
1225+
if (now >= next_spec_ms)
1226+
{
1227+
/* Paced by elapsed playout, not by write progress: the ring is
1228+
* filled far faster than it drains. */
1229+
size_t pos = (size_t)(((now - t0_ms) * FREEDV_FS_8000) / 1000ULL);
1230+
publish_tx_spectrum_at(tx_buffer, total_samples, pos, FREEDV_FS_8000);
1231+
next_spec_ms = now + TX_SPECTRUM_STEP_MS;
1232+
}
11631233
usleep(1000);
1234+
}
11641235
}
11651236
else
11661237
{
@@ -1171,9 +1242,22 @@ int send_modulated_data(generic_modem_t *g_modem, uint8_t *bytes_in, int frames_
11711242
/* Write entire pre-generated buffer to playback */
11721243
write_buffer(playback_buffer, (uint8_t *)tx_buffer, total_samples * sizeof(int32_t));
11731244

1174-
/* Wait for all samples to be played out */
1245+
/* Wait for all samples to be played out, publishing the waterfall as we
1246+
* go so the UI scrolls during the burst rather than after it. */
11751247
uint64_t playback_duration_us = ((uint64_t)total_samples * 1000000ULL) / FREEDV_FS_8000;
1176-
usleep((useconds_t)playback_duration_us);
1248+
uint64_t waited_us = 0;
1249+
while (waited_us < playback_duration_us)
1250+
{
1251+
uint64_t step_us = (uint64_t)TX_SPECTRUM_STEP_MS * 1000ULL;
1252+
if (step_us > playback_duration_us - waited_us)
1253+
step_us = playback_duration_us - waited_us;
1254+
1255+
size_t pos = (size_t)((waited_us * FREEDV_FS_8000) / 1000000ULL);
1256+
publish_tx_spectrum_at(tx_buffer, total_samples, pos, FREEDV_FS_8000);
1257+
1258+
usleep((useconds_t)step_us);
1259+
waited_us += step_us;
1260+
}
11771261

11781262
/* Give some tail time before turning off PTT */
11791263
usleep(TAIL_TIME_US);

modem/modem.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ int modem_get_rx_spectrum(float *out_dB, int max_bins);
6666
* that is how a local UI and a remote one can both run at full rate. */
6767
int modem_get_rx_spectrum_seq(float *out_dB, int max_bins, uint64_t *seq_out);
6868

69-
/* Enable/disable the RX spectrum FFT (skipped when no UI consumes it). */
69+
/* Enable/disable the spectrum FFT, both the received and the transmitted one
70+
* (skipped when no UI consumes it). */
7071
void modem_set_spectrum_enabled(bool enabled);
7172

7273
/* Channel-busy (occupancy) detector — VARA-style "BUSY ON"/"BUSY OFF".

0 commit comments

Comments
 (0)