Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/core/PanadapterStream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <QNetworkDatagram>
#include <QHostAddress>
#include <QStringList>
#include <QThread>
#include <QtEndian>
#include <QSet>
#include <algorithm>
Expand Down Expand Up @@ -465,6 +466,10 @@ void PanadapterStream::clearRegisteredStreams()
m_orphanStreams.clear();
m_everRegisteredPanStreams.clear(); // new session — re-arm from scratch (#3856)
m_everRegisteredWfStreams.clear();
// The external-source suppression mask is session state too — a bit left
// set across a disconnect would silently mute that DAX channel on the
// next radio connection. (feat/kiwi-audio-to-dax)
m_externalDaxSourceMask.store(0, std::memory_order_relaxed);
resetAudioStreamStats();
qCDebug(lcVita49) << "PanadapterStream: cleared all registered streams";
}
Expand Down Expand Up @@ -776,6 +781,14 @@ void PanadapterStream::processDatagram(const QByteArray& data)

if (daxChannel >= 0) {
int channel = daxChannel;
// An external source (injectDaxAudio, e.g. a KiwiSDR) is supplying
// this channel's audio — drop the Flex payload so the two sources
// don't mix. (feat/kiwi-audio-to-dax)
if (isValidDaxChannel(channel)
&& (m_externalDaxSourceMask.load(std::memory_order_relaxed)
& (1u << channel))) {
return;
}
QByteArray pcm;
if (pcc == PCC_IF_NARROW) {
// Float32 stereo big-endian from radio → native float32 stereo
Expand Down Expand Up @@ -1458,6 +1471,39 @@ quint32 PanadapterStream::daxStreamIdForChannel(int channel) const
return 0;
}

void PanadapterStream::injectDaxAudio(int channel, const QByteArray& pcm)
{
// Feed non-Flex audio (e.g. a KiwiSDR) onto a DAX channel using the same
// signal the Flex path uses, so TciServer / the DAX bridge need no
// changes. `pcm` is already the native DAX format — 24 kHz stereo
// float32: both Flex packet-class paths above (PCC_IF_NARROW and
// PCC_IF_NARROW_REDUCED, fw 4.2.18) normalize to exactly that before
// `daxAudioReady`, and KiwiSdrClient resamples its 12 kHz feed to match.
// No repackaging is required — consumers handle arbitrary payload sizes.
// (feat/kiwi-audio-to-dax)
if (!isValidDaxChannel(channel) || pcm.isEmpty()) {
return;
}
// Every Flex-path emission of daxAudioReady happens on this object's
// network thread; AutoConnection consumers (TciServer, DaxBridge) rely on
// that to get queued delivery. Re-invoke so an injection from the GUI
// thread keeps the identical contract instead of running the consumers'
// resampler/socket work synchronously inside the caller's slot.
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(
this,
[this, channel, pcm]() { emit daxAudioReady(channel, pcm); },
Qt::QueuedConnection);
return;
}
emit daxAudioReady(channel, pcm);
}
Comment thread
skerker marked this conversation as resolved.

void PanadapterStream::setExternalDaxSourceMask(quint32 mask)
{
m_externalDaxSourceMask.store(mask, std::memory_order_relaxed);
}

void PanadapterStream::unregisterDaxStream(quint32 streamId)
{
int channel = 0;
Expand Down
29 changes: 29 additions & 0 deletions src/core/PanadapterStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,30 @@ class PanadapterStream : public QObject {
};
QVector<DaxChannelSnapshot> daxChannelSnapshot() const;

// External DAX audio injection (feat/kiwi-audio-to-dax).
// Emits `daxAudioReady(channel, pcm)` for audio that did not come from the
// Flex (e.g. a KiwiSDR replacing a slice's receive source). `pcm` must be
// the native DAX format: 24 kHz stereo float32 — both Flex packet-class
// paths (PCC_IF_NARROW and PCC_IF_NARROW_REDUCED, fw 4.2.18) normalize to
// that before `daxAudioReady`, and the external source must match it; a
// firmware DAX-rate change breaks this assumption (pitch shift), so
// re-verify here first. Callable from any thread — the emission is
// re-invoked onto this object's (network) thread so consumers see the
// same thread contract as the Flex path. `setExternalDaxSourceMask` marks
// channels (bit N = channel N) whose *Flex* DAX payload must be dropped,
// so the injected audio is the only thing WSJT-X hears on that channel;
// read lock-free on the RX thread.
void injectDaxAudio(int channel, const QByteArray& pcm);
void setExternalDaxSourceMask(quint32 mask);

// The one definition of a routable DAX audio channel (1..4 on current
// radios; bounded above by the 32-bit suppression mask). Keep every
// channel-validity check on this predicate so the bounds can't diverge.
static constexpr bool isValidDaxChannel(int channel)
{
return channel >= 1 && channel < 32;
}

// DAX IQ stream routing
void registerIqStream(quint32 streamId, int channel);
void unregisterIqStream(quint32 streamId);
Expand Down Expand Up @@ -406,6 +430,11 @@ private slots:
int audioPacketJitterMs() const { return m_audioPacketJitterMs.load(); }

private:
// Bit N set => drop the Flex DAX payload for channel N (an external
// source — e.g. a KiwiSDR — is supplying that channel's audio via
// injectDaxAudio instead). Read on the RX parse thread, written from the
// GUI thread. (feat/kiwi-audio-to-dax)
std::atomic<quint32> m_externalDaxSourceMask{0};
std::atomic<qint64> m_totalRxBytes{0};
std::atomic<qint64> m_totalTxBytes{0};
std::atomic<int> m_audioPacketGapMs{0};
Expand Down
1 change: 1 addition & 0 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5275,6 +5275,7 @@ void MainWindow::onConnectionStateChanged(bool connected)
m_suppressStartupPanLayoutRearrange = false;
m_layoutRestoreUntilMs = 0;
clearKiwiSdrPanDisplaySourceOverrides();
resetKiwiSdrDaxSuppressionState();
if (m_appletPanel) {
m_appletPanel->clearSliceButtons();
}
Expand Down
25 changes: 25 additions & 0 deletions src/gui/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,15 @@ private slots:
const QString& profileId,
bool selectSlice);
void clearKiwiSdrVirtualAntennaForSlice(int sliceId);
// Route a KiwiSDR profile's decoded audio onto its slice's DAX channel so
// WSJT-X (over DAX/TCI) decodes the Kiwi. The "suppress the Flex payload
// on Kiwi-fed DAX channels" mask is kept current event-driven — see
// refreshKiwiSdrDaxSuppression — and kiwiSdrDaxStallTick feeds silence
// while a suppressed channel's Kiwi source stalls. (feat/kiwi-audio-to-dax)
void routeKiwiSdrAudioToDax(const QString& profileId, const QByteArray& pcm);
void refreshKiwiSdrDaxSuppression();
void kiwiSdrDaxStallTick();
void resetKiwiSdrDaxSuppressionState();
void prepareKiwiSdrBandRecallForPan(const QString& panId);
bool finishPreparedKiwiSdrBandRecallForSlice(SliceModel* slice);
void finishPreparedKiwiSdrBandRecallForPan(const QString& panId);
Expand Down Expand Up @@ -1017,6 +1026,22 @@ private slots:
// band recall that superseded it within the grace window.
QHash<QString, quint64> m_kiwiSdrBandRecallGenerations;
QSet<QString> m_kiwiSdrFlexDisplayPans;
// Kiwi→DAX stall watchdog (feat/kiwi-audio-to-dax): monotonic clock,
// per-channel last-Kiwi-chunk timestamps for suppressed channels, the
// channels currently being silence-filled, and the tick timer (runs only
// while the suppression mask is non-zero).
QElapsedTimer m_kiwiDaxClock;
QHash<int, qint64> m_kiwiDaxLastAudioMs;
QSet<int> m_kiwiDaxStalledChannels;
QTimer* m_kiwiDaxStallTimer{nullptr};
// Suppression latch (mirrors TciServer::m_channelTrx, #3669): sliceId →
// the last DAX channel that slice held while Kiwi-fed. Keeps the
// suppression bit set through the radio's transient dax=0→N status
// rebroadcasts so Flex payload can't dual-feed the consumer mid-flip.
// Invalidated on clear/slice-removal (refresh prune), genuine stream
// release (RadioModel::daxStreamUnregistered), channel takeover by
// another slice, and disconnect (resetKiwiSdrDaxSuppressionState).
QHash<int, int> m_kiwiDaxLatchedChannels;
// Retain client-side receiver state across the slice teardown/rebuild a
// FLEX band-stack recall performs. The trackers are pure lifecycle policy;
// MainWindow owns their side effects and shared grace window.
Expand Down
Loading
Loading