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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ msix-root-*/
*.msixupload
*.pfx
packaging/windows/certs/
Resumen para empezar de nuevo.txt
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,8 @@ set(CORE_SOURCES
src/core/SleepInhibitor.cpp
src/core/MemoryCsvCompat.cpp
src/core/MemoryRecallPolicy.cpp
src/core/WfmDemodulator.cpp
src/core/WaveOutWriter.cpp
src/core/tnc/AetherAx25LibmodemShim.cpp
src/core/tnc/Ax25FrameFormatter.cpp
src/core/tnc/Ax25.cpp
Expand Down Expand Up @@ -634,6 +636,7 @@ set(GUI_SOURCES
src/gui/MainWindow.cpp
src/gui/AgcCalibrationDialog.cpp
src/gui/AudioDeviceChangeDialog.cpp
src/gui/WfmDeviceDialog.cpp
src/gui/ConnectionPanel.cpp
src/gui/ClientDisconnectDialog.cpp
src/gui/ConnectedStationsDialog.cpp
Expand Down
86 changes: 86 additions & 0 deletions src/core/WaveOutWriter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#include "core/WaveOutWriter.h"
#include "core/LogManager.h"
#include <QMediaDevices>

namespace AetherSDR {

WaveOutWriter::WaveOutWriter(QObject* parent)
: QObject(parent)
{}

WaveOutWriter::~WaveOutWriter()
{
close();
}

bool WaveOutWriter::open(const QString& deviceId, int sampleRate, int channelCount)
{
close();

// Find the requested device by its persistent ID string.
QAudioDevice found;
const auto outputs = QMediaDevices::audioOutputs();
qCDebug(lcAudio) << "WaveOutWriter::open looking for" << deviceId
<< "among" << outputs.size() << "devices";
for (const QAudioDevice& dev : outputs) {
if (dev.id() == deviceId.toUtf8()) {
found = dev;
break;
}
}
if (found.isNull()) {
qCDebug(lcAudio) << "WaveOutWriter::open: device not found —" << deviceId;
return false;
}

QAudioFormat fmt;
fmt.setSampleRate(sampleRate);
fmt.setChannelCount(channelCount);
fmt.setSampleFormat(QAudioFormat::Int16);

if (!found.isFormatSupported(fmt)) {
qCDebug(lcAudio) << "WaveOutWriter::open: Int16 format not supported by"
<< found.description() << "— trying default format";
fmt = found.preferredFormat();
}

m_sink = new QAudioSink(found, fmt, this);
m_io = m_sink->start();

if (!m_io || m_sink->error() != QAudio::NoError) {
qCDebug(lcAudio) << "WaveOutWriter::open: QAudioSink::start() failed, error="
<< m_sink->error();
delete m_sink;
m_sink = nullptr;
m_io = nullptr;
return false;
}

m_deviceName = found.description();
qCDebug(lcAudio) << "WaveOutWriter opened:" << m_deviceName
<< "rate=" << fmt.sampleRate()
<< "ch=" << fmt.channelCount()
<< "fmt=" << fmt.sampleFormat();
return true;
}

void WaveOutWriter::close()
{
if (m_sink) {
m_sink->stop();
delete m_sink;
m_sink = nullptr;
m_io = nullptr;
qCDebug(lcAudio) << "WaveOutWriter closed";
}
m_deviceName.clear();
}

void WaveOutWriter::write(const QByteArray& pcm)
{
if (!m_io || pcm.isEmpty())
return;
m_io->write(pcm);
}

} // namespace AetherSDR
43 changes: 43 additions & 0 deletions src/core/WaveOutWriter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#pragma once
#include <QAudioDevice>
#include <QAudioFormat>
#include <QAudioSink>
#include <QByteArray>
#include <QIODevice>
#include <QObject>
#include <QString>

namespace AetherSDR {

// Cross-platform audio output wrapper backed by QAudioSink (Qt6 Multimedia).
// Accepts Int16 stereo PCM at a fixed sample rate and writes it to the
// selected output device — works on Windows (WASAPI), macOS (CoreAudio),
// and Linux (PipeWire / PulseAudio / ALSA).
class WaveOutWriter : public QObject
{
Q_OBJECT
public:
explicit WaveOutWriter(QObject* parent = nullptr);
~WaveOutWriter() override;

// Open the device whose description contains |deviceId| (the
// QAudioDevice::id() string stored in WfmSettings). Returns true on
// success. |sampleRate| is the output rate in Hz (typically 48000).
bool open(const QString& deviceId, int sampleRate, int channelCount = 2);

void close();

// Write Int16 interleaved PCM samples. Thread-safe: may be called from
// any thread; internally posts to the Qt event loop.
void write(const QByteArray& pcm);

bool isOpen() const { return m_sink != nullptr; }
QString deviceName() const { return m_deviceName; }

private:
QAudioSink* m_sink{nullptr};
QIODevice* m_io{nullptr};
QString m_deviceName;
};

} // namespace AetherSDR
174 changes: 174 additions & 0 deletions src/core/WfmDemodulator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#include "core/WfmDemodulator.h"
#include "core/WaveOutWriter.h"
#include "core/LogManager.h"
#include "models/DaxIqModel.h"

#include <cmath>
#include <algorithm>

namespace AetherSDR {

WfmDemodulator::WfmDemodulator(QObject* parent)
: QObject(parent)
{}

WfmDemodulator::~WfmDemodulator()
{
stop();
}

void WfmDemodulator::start(DaxIqModel* daxIq, const QString& deviceId,
const QString& panId, float freqOffsetHz)
{
if (m_active) stop();

m_daxIq = daxIq;
m_prevI = 0.0f;
m_prevQ = 0.0f;
m_corrCos = 1.0f;
m_corrSin = 0.0f;
m_panId = panId;
m_panSent = false;

// Pre-compute the per-sample rotation for frequency correction.
const float step = -2.0f * static_cast<float>(M_PI) * freqOffsetHz / IQ_RATE;
m_corrCosStep = std::cos(step);
m_corrSinStep = std::sin(step);

qCDebug(lcAudio) << "WfmDemodulator::start deviceId=" << deviceId
<< "freqOffsetHz=" << freqOffsetHz;

m_waveOut = new WaveOutWriter(this);
if (!m_waveOut->open(deviceId, AUDIO_RATE, 2)) {
qCDebug(lcAudio) << "WfmDemodulator: failed to open audio device:" << deviceId;
delete m_waveOut;
m_waveOut = nullptr;
return;
}

// Wire VITA-49 DaxIqModel path (cross-platform).
connect(m_daxIq, &DaxIqModel::iqSamplesReady,
this, &WfmDemodulator::onIqSamples);
connect(m_daxIq, &DaxIqModel::streamChanged,
this, &WfmDemodulator::onStreamChanged);
m_daxIq->createStream(DAX_CHANNEL);

m_active = true;
}

void WfmDemodulator::stop()
{
if (!m_active) return;
m_active = false;

if (m_daxIq) {
m_daxIq->removeStream(DAX_CHANNEL);
disconnect(m_daxIq, nullptr, this, nullptr);
m_daxIq = nullptr;
}
if (m_waveOut) {
m_waveOut->close();
delete m_waveOut;
m_waveOut = nullptr;
}
}

void WfmDemodulator::onStreamChanged(int channel)
{
const auto& s = m_daxIq->stream(DAX_CHANNEL);
qCDebug(lcAudio) << "WfmDemodulator::onStreamChanged ch=" << channel
<< "panSent=" << m_panSent << "panId=" << m_panId
<< "exists=" << s.exists << "active=" << s.active
<< "streamId=0x" + QString::number(s.streamId, 16);
if (channel != DAX_CHANNEL || m_panSent || m_panId.isEmpty()) return;
if (!s.exists || s.streamId == 0) return;
const QString cmd = QString("stream set 0x%1 pan=%2")
.arg(s.streamId, 0, 16).arg(m_panId);
qCDebug(lcAudio) << "WfmDemodulator: sending" << cmd;
emit commandReady(cmd);
m_panSent = true;
}

void WfmDemodulator::onIqSamples(int channel, QVector<float> iqInterleaved, int /*sampleRate*/)
{
if (channel != DAX_CHANNEL || !m_active || !m_waveOut) return;
processSamples(iqInterleaved);
}

void WfmDemodulator::processSamples(const QVector<float>& iqInterleaved)
{
const int numSamples = iqInterleaved.size() / 2;
if (numSamples <= 0) return;

QByteArray pcm(numSamples * 2 * sizeof(qint16), Qt::Uninitialized);
auto* out = reinterpret_cast<qint16*>(pcm.data());

float prevI = m_prevI;
float prevQ = m_prevQ;

for (int i = 0; i < numSamples; ++i) {
float I = iqInterleaved[2 * i];
float Q = iqInterleaved[2 * i + 1];

// Frequency correction: rotate IQ by the running phasor.
{
const float Ic = I * m_corrCos - Q * m_corrSin;
const float Qc = I * m_corrSin + Q * m_corrCos;
I = Ic; Q = Qc;
const float newCos = m_corrCos * m_corrCosStep - m_corrSin * m_corrSinStep;
const float newSin = m_corrCos * m_corrSinStep + m_corrSin * m_corrCosStep;
m_corrCos = newCos;
m_corrSin = newSin;
}

// Normalize to unit circle (carrier lock)
const float amp = std::sqrt(I * I + Q * Q);
if (amp > 1e-9f) { I /= amp; Q /= amp; }
else { I = prevI; Q = prevQ; }

// Phase-difference FM discriminator
const float cross = I * prevQ - Q * prevI;
const float dot = I * prevI + Q * prevQ;
float audio = std::atan2(cross, dot) * (GAIN / static_cast<float>(M_PI));
audio = std::max(-1.0f, std::min(1.0f, audio));

prevI = I;
prevQ = Q;

const qint16 s16 = static_cast<qint16>(audio * 32767.0f * m_volume);
out[i * 2] = s16;
out[i * 2 + 1] = s16;
}

m_prevI = prevI;
m_prevQ = prevQ;

// Renormalize frequency-correction phasor every block to prevent drift.
{
const float norm = std::sqrt(m_corrCos * m_corrCos + m_corrSin * m_corrSin);
if (norm > 1e-9f) { m_corrCos /= norm; m_corrSin /= norm; }
}

// Periodic signal-level diagnostic (every 100 blocks ≈ every ~2 s at 48 kHz/512)
static int s_blk = 0;
if (++s_blk % 100 == 0) {
float iqRms = 0, audioRms = 0, audioMax = 0;
for (int i = 0; i < numSamples; ++i) {
const float rI = iqInterleaved[2*i], rQ = iqInterleaved[2*i+1];
iqRms += rI*rI + rQ*rQ;
const float a = std::abs(out[i*2] / 32767.0f);
audioRms += a*a;
if (a > audioMax) audioMax = a;
}
iqRms = std::sqrt(iqRms / numSamples);
audioRms = std::sqrt(audioRms / numSamples);
qCDebug(lcAudio) << "WfmDemodulator blk#" << s_blk
<< "IQ_rms=" << iqRms
<< "audio_rms=" << audioRms
<< "audio_max=" << audioMax;
}

m_waveOut->write(pcm);
}

} // namespace AetherSDR
Loading
Loading