From 7cc9cd1bf1ebc216791da7e77229b2e7352981d4 Mon Sep 17 00:00:00 2001 From: srcejon Date: Wed, 22 Nov 2023 14:28:35 +0000 Subject: [PATCH] Add instant replay --- plugins/samplesource/airspyhf/airspyhfgui.cpp | 105 +++++ plugins/samplesource/airspyhf/airspyhfgui.h | 9 + plugins/samplesource/airspyhf/airspyhfgui.ui | 111 +++++- .../samplesource/airspyhf/airspyhfinput.cpp | 37 +- plugins/samplesource/airspyhf/airspyhfinput.h | 24 +- .../airspyhf/airspyhfsettings.cpp | 36 ++ .../samplesource/airspyhf/airspyhfsettings.h | 4 + .../samplesource/airspyhf/airspyhfworker.cpp | 177 +++++---- .../samplesource/airspyhf/airspyhfworker.h | 4 +- plugins/samplesource/rtlsdr/rtlsdrgui.cpp | 105 +++++ plugins/samplesource/rtlsdr/rtlsdrgui.h | 9 + plugins/samplesource/rtlsdr/rtlsdrgui.ui | 157 ++++++-- plugins/samplesource/rtlsdr/rtlsdrinput.cpp | 24 +- plugins/samplesource/rtlsdr/rtlsdrinput.h | 21 + .../samplesource/rtlsdr/rtlsdrsettings.cpp | 36 ++ plugins/samplesource/rtlsdr/rtlsdrsettings.h | 4 + plugins/samplesource/rtlsdr/rtlsdrthread.cpp | 359 ++++++++++-------- plugins/samplesource/rtlsdr/rtlsdrthread.h | 4 +- .../samplesource/sdrplayv3/sdrplayv3gui.cpp | 106 ++++++ plugins/samplesource/sdrplayv3/sdrplayv3gui.h | 9 + .../samplesource/sdrplayv3/sdrplayv3gui.ui | 122 +++++- .../samplesource/sdrplayv3/sdrplayv3input.cpp | 26 +- .../samplesource/sdrplayv3/sdrplayv3input.h | 21 + .../sdrplayv3/sdrplayv3settings.cpp | 36 ++ .../sdrplayv3/sdrplayv3settings.h | 4 + .../sdrplayv3/sdrplayv3thread.cpp | 357 +++++++++-------- .../samplesource/sdrplayv3/sdrplayv3thread.h | 4 +- plugins/samplesource/usrpinput/usrpinput.cpp | 25 +- plugins/samplesource/usrpinput/usrpinput.h | 21 + .../samplesource/usrpinput/usrpinputgui.cpp | 105 +++++ plugins/samplesource/usrpinput/usrpinputgui.h | 9 + .../samplesource/usrpinput/usrpinputgui.ui | 121 +++++- .../usrpinput/usrpinputsettings.cpp | 36 ++ .../usrpinput/usrpinputsettings.h | 4 + .../usrpinput/usrpinputthread.cpp | 90 +++-- .../samplesource/usrpinput/usrpinputthread.h | 5 +- sdrbase/CMakeLists.txt | 1 + sdrbase/dsp/replaybuffer.h | 218 +++++++++++ sdrgui/gui/basicdevicesettingsdialog.cpp | 47 +++ sdrgui/gui/basicdevicesettingsdialog.h | 10 + sdrgui/gui/basicdevicesettingsdialog.ui | 127 ++++++- 41 files changed, 2270 insertions(+), 460 deletions(-) create mode 100644 sdrbase/dsp/replaybuffer.h diff --git a/plugins/samplesource/airspyhf/airspyhfgui.cpp b/plugins/samplesource/airspyhf/airspyhfgui.cpp index 338e66693..59e464db7 100644 --- a/plugins/samplesource/airspyhf/airspyhfgui.cpp +++ b/plugins/samplesource/airspyhf/airspyhfgui.cpp @@ -235,6 +235,10 @@ void AirspyHFGui::displaySettings() ui->dcOffset->setChecked(m_settings.m_dcBlock); ui->iqImbalance->setChecked(m_settings.m_iqCorrection); displayAGC(); + displayReplayLength(); + displayReplayOffset(); + displayReplayStep(); + ui->replayLoop->setChecked(m_settings.m_replayLoop); blockApplySettings(false); } @@ -500,6 +504,9 @@ void AirspyHFGui::openDeviceSettingsDialog(const QPoint& p) if (m_contextMenuType == ContextMenuDeviceSettings) { BasicDeviceSettingsDialog dialog(this); + dialog.setReplayBytesPerSecond(getDevSampleRate(m_settings.m_devSampleRateIndex) * 2 * sizeof(float)); + dialog.setReplayLength(m_settings.m_replayLength); + dialog.setReplayStep(m_settings.m_replayStep); dialog.setUseReverseAPI(m_settings.m_useReverseAPI); dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); @@ -513,10 +520,17 @@ void AirspyHFGui::openDeviceSettingsDialog(const QPoint& p) m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_replayLength = dialog.getReplayLength(); + m_settings.m_replayStep = dialog.getReplayStep(); + displayReplayLength(); + displayReplayOffset(); + displayReplayStep(); m_settingsKeys.append("useReverseAPI"); m_settingsKeys.append("reverseAPIAddress"); m_settingsKeys.append("reverseAPIPort"); m_settingsKeys.append("reverseAPIDeviceIndex"); + m_settingsKeys.append("replayLength"); + m_settingsKeys.append("replayStep"); sendSettings(); } @@ -524,6 +538,91 @@ void AirspyHFGui::openDeviceSettingsDialog(const QPoint& p) resetContextMenuType(); } +void AirspyHFGui::displayReplayLength() +{ + bool replayEnabled = m_settings.m_replayLength > 0.0f; + if (!replayEnabled) { + ui->replayOffset->setMaximum(0); + } else { + ui->replayOffset->setMaximum(m_settings.m_replayLength * 10 - 1); + } + ui->replayLabel->setEnabled(replayEnabled); + ui->replayOffset->setEnabled(replayEnabled); + ui->replayOffsetText->setEnabled(replayEnabled); + ui->replaySave->setEnabled(replayEnabled); +} + +void AirspyHFGui::displayReplayOffset() +{ + bool replayEnabled = m_settings.m_replayLength > 0.0f; + ui->replayOffset->setValue(m_settings.m_replayOffset * 10); + ui->replayOffsetText->setText(QString("%1s").arg(m_settings.m_replayOffset, 0, 'f', 1)); + ui->replayNow->setEnabled(replayEnabled && (m_settings.m_replayOffset > 0.0f)); + ui->replayPlus->setEnabled(replayEnabled && (std::round(m_settings.m_replayOffset * 10) < ui->replayOffset->maximum())); + ui->replayMinus->setEnabled(replayEnabled && (m_settings.m_replayOffset > 0.0f)); +} + +void AirspyHFGui::displayReplayStep() +{ + QString step; + float intpart; + float frac = modf(m_settings.m_replayStep, &intpart); + if (frac == 0.0f) { + step = QString::number((int)intpart); + } else { + step = QString::number(m_settings.m_replayStep, 'f', 1); + } + ui->replayPlus->setText(QString("+%1s").arg(step)); + ui->replayPlus->setToolTip(QString("Add %1 seconds to time delay").arg(step)); + ui->replayMinus->setText(QString("-%1s").arg(step)); + ui->replayMinus->setToolTip(QString("Remove %1 seconds from time delay").arg(step)); +} + +void AirspyHFGui::on_replayOffset_valueChanged(int value) +{ + m_settings.m_replayOffset = value / 10.0f; + displayReplayOffset(); + m_settingsKeys.append("replayOffset"); + sendSettings(); +} + +void AirspyHFGui::on_replayNow_clicked() +{ + ui->replayOffset->setValue(0); +} + +void AirspyHFGui::on_replayPlus_clicked() +{ + ui->replayOffset->setValue(ui->replayOffset->value() + m_settings.m_replayStep * 10); +} + +void AirspyHFGui::on_replayMinus_clicked() +{ + ui->replayOffset->setValue(ui->replayOffset->value() - m_settings.m_replayStep * 10); +} + +void AirspyHFGui::on_replaySave_clicked() +{ + QFileDialog fileDialog(nullptr, "Select file to save IQ data to", "", "*.wav"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + AirspyHFInput::MsgSaveReplay *message = AirspyHFInput::MsgSaveReplay::create(fileNames[0]); + m_sampleSource->getInputMessageQueue()->push(message); + } + } +} + +void AirspyHFGui::on_replayLoop_toggled(bool checked) +{ + m_settings.m_replayLoop = checked; + m_settingsKeys.append("replayLoop"); + sendSettings(); +} + void AirspyHFGui::makeUIConnections() { QObject::connect(ui->centerFrequency, &ValueDial::changed, this, &AirspyHFGui::on_centerFrequency_changed); @@ -540,4 +639,10 @@ void AirspyHFGui::makeUIConnections() QObject::connect(ui->lna, &ButtonSwitch::toggled, this, &AirspyHFGui::on_lna_toggled); QObject::connect(ui->agc, QOverload::of(&QComboBox::currentIndexChanged), this, &AirspyHFGui::on_agc_currentIndexChanged); QObject::connect(ui->att, QOverload::of(&QComboBox::currentIndexChanged), this, &AirspyHFGui::on_att_currentIndexChanged); + QObject::connect(ui->replayOffset, &QSlider::valueChanged, this, &AirspyHFGui::on_replayOffset_valueChanged); + QObject::connect(ui->replayNow, &QToolButton::clicked, this, &AirspyHFGui::on_replayNow_clicked); + QObject::connect(ui->replayPlus, &QToolButton::clicked, this, &AirspyHFGui::on_replayPlus_clicked); + QObject::connect(ui->replayMinus, &QToolButton::clicked, this, &AirspyHFGui::on_replayMinus_clicked); + QObject::connect(ui->replaySave, &QToolButton::clicked, this, &AirspyHFGui::on_replaySave_clicked); + QObject::connect(ui->replayLoop, &ButtonSwitch::toggled, this, &AirspyHFGui::on_replayLoop_toggled); } diff --git a/plugins/samplesource/airspyhf/airspyhfgui.h b/plugins/samplesource/airspyhf/airspyhfgui.h index d5018884a..877dd3e90 100644 --- a/plugins/samplesource/airspyhf/airspyhfgui.h +++ b/plugins/samplesource/airspyhf/airspyhfgui.h @@ -70,6 +70,9 @@ private: void displaySettings(); void displaySampleRates(); void displayAGC(); + void displayReplayLength(); + void displayReplayOffset(); + void displayReplayStep(); void sendSettings(); void updateSampleRateAndFrequency(); void updateFrequencyLimits(); @@ -94,6 +97,12 @@ private slots: void updateHardware(); void updateStatus(); void handleInputMessages(); + void on_replayOffset_valueChanged(int value); + void on_replayNow_clicked(); + void on_replayPlus_clicked(); + void on_replayMinus_clicked(); + void on_replaySave_clicked(); + void on_replayLoop_toggled(bool checked); void openDeviceSettingsDialog(const QPoint& p); }; diff --git a/plugins/samplesource/airspyhf/airspyhfgui.ui b/plugins/samplesource/airspyhf/airspyhfgui.ui index ab1849681..89ca8a751 100644 --- a/plugins/samplesource/airspyhf/airspyhfgui.ui +++ b/plugins/samplesource/airspyhf/airspyhfgui.ui @@ -7,7 +7,7 @@ 0 0 360 - 137 + 175 @@ -19,13 +19,13 @@ 360 - 137 + 175 380 - 177 + 175 @@ -584,6 +584,111 @@ + + + + Qt::Horizontal + + + + + + + + + + 65 + 0 + + + + Time Delay + + + + + + + Replay time delay in seconds + + + 500 + + + Qt::Horizontal + + + + + + + Replay time delay in seconds + + + 0.0s + + + + + + + Set time delay to 0 seconds + + + Now + + + + + + + Add displayed number of seconds to time delay + + + +5s + + + + + + + Remove displayed number of seconds from time delay + + + -5s + + + + + + + Repeatedly replay data in replay buffer + + + + + + + :/playloop.png:/playloop.png + + + + + + + Save replay buffer to a file + + + + + + + :/save.png:/save.png + + + + + diff --git a/plugins/samplesource/airspyhf/airspyhfinput.cpp b/plugins/samplesource/airspyhf/airspyhfinput.cpp index fb8f32ad6..9fc247381 100644 --- a/plugins/samplesource/airspyhf/airspyhfinput.cpp +++ b/plugins/samplesource/airspyhf/airspyhfinput.cpp @@ -44,6 +44,7 @@ MESSAGE_CLASS_DEFINITION(AirspyHFInput::MsgConfigureAirspyHF, Message) MESSAGE_CLASS_DEFINITION(AirspyHFInput::MsgStartStop, Message) +MESSAGE_CLASS_DEFINITION(AirspyHFInput::MsgSaveReplay, Message) const qint64 AirspyHFInput::loLowLimitFreqHF = 9000L; const qint64 AirspyHFInput::loHighLimitFreqHF = 31000000L; @@ -181,7 +182,7 @@ bool AirspyHFInput::start() } m_airspyHFWorkerThread = new QThread(); - m_airspyHFWorker = new AirspyHFWorker(m_dev, &m_sampleFifo); + m_airspyHFWorker = new AirspyHFWorker(m_dev, &m_sampleFifo, &m_replayBuffer); m_airspyHFWorker->moveToThread(m_airspyHFWorkerThread); int sampleRateIndex = m_settings.m_devSampleRateIndex; @@ -289,6 +290,18 @@ int AirspyHFInput::getSampleRate() const } } +uint32_t AirspyHFInput::getSampleRateFromIndex(int devSampleRateIndex) const +{ + if (devSampleRateIndex >= m_sampleRates.size()) { + devSampleRateIndex = m_sampleRates.size() - 1; + } + if (devSampleRateIndex >= 0) { + return m_sampleRates[devSampleRateIndex]; + } else { + return 0; + } +} + quint64 AirspyHFInput::getCenterFrequency() const { return m_settings.m_centerFrequency; @@ -346,6 +359,12 @@ bool AirspyHFInput::handleMessage(const Message& message) return true; } + else if (MsgSaveReplay::match(message)) + { + MsgSaveReplay& cmd = (MsgSaveReplay&) message; + m_replayBuffer.save(cmd.getFilename(), getSampleRateFromIndex(m_settings.m_devSampleRateIndex), getCenterFrequency()); + return true; + } else { return false; @@ -416,6 +435,10 @@ bool AirspyHFInput::applySettings(const AirspyHFSettings& settings, const QList< m_airspyHFWorker->setSamplerate(m_sampleRates[sampleRateIndex]); } } + + if (settings.m_devSampleRateIndex != m_settings.m_devSampleRateIndex) { + m_replayBuffer.clear(); + } } if (settingsKeys.contains("log2Decim") || force) @@ -568,6 +591,18 @@ bool AirspyHFInput::applySettings(const AirspyHFSettings& settings, const QList< m_settings.applySettings(settingsKeys, settings); } + if (settingsKeys.contains("replayLength") || settingsKeys.contains("devSampleRate") || force) { + m_replayBuffer.setSize(m_settings.m_replayLength, getSampleRateFromIndex(m_settings.m_devSampleRateIndex)); + } + + if (settingsKeys.contains("replayOffset") || settingsKeys.contains("devSampleRate") || force) { + m_replayBuffer.setReadOffset(((unsigned)(m_settings.m_replayOffset * getSampleRateFromIndex(m_settings.m_devSampleRateIndex))) * 2); + } + + if (settingsKeys.contains("replayLoop") || force) { + m_replayBuffer.setLoop(m_settings.m_replayLoop); + } + return true; } diff --git a/plugins/samplesource/airspyhf/airspyhfinput.h b/plugins/samplesource/airspyhf/airspyhfinput.h index 68ec36920..07ac749ee 100644 --- a/plugins/samplesource/airspyhf/airspyhfinput.h +++ b/plugins/samplesource/airspyhf/airspyhfinput.h @@ -28,6 +28,7 @@ #include #include +#include "dsp/replaybuffer.h" #include #include "airspyhfsettings.h" @@ -85,7 +86,26 @@ public: { } }; - AirspyHFInput(DeviceAPI *deviceAPI); + class MsgSaveReplay : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getFilename() const { return m_filename; } + + static MsgSaveReplay* create(const QString& filename) { + return new MsgSaveReplay(filename); + } + + protected: + QString m_filename; + + MsgSaveReplay(const QString& filename) : + Message(), + m_filename(filename) + { } + }; + + AirspyHFInput(DeviceAPI *deviceAPI); virtual ~AirspyHFInput(); virtual void destroy(); @@ -155,6 +175,7 @@ private: bool m_running; QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; + ReplayBuffer m_replayBuffer; bool openDevice(); void closeDevice(); @@ -164,6 +185,7 @@ private: void webapiFormatDeviceReport(SWGSDRangel::SWGDeviceReport& response); void webapiReverseSendSettings(const QList& deviceSettingsKeys, const AirspyHFSettings& settings, bool force); void webapiReverseSendStartStop(bool start); + uint32_t getSampleRateFromIndex(int devSampleRateIndex) const; private slots: void networkManagerFinished(QNetworkReply *reply); diff --git a/plugins/samplesource/airspyhf/airspyhfsettings.cpp b/plugins/samplesource/airspyhf/airspyhfsettings.cpp index 375950b9f..d3ed0e947 100644 --- a/plugins/samplesource/airspyhf/airspyhfsettings.cpp +++ b/plugins/samplesource/airspyhf/airspyhfsettings.cpp @@ -46,6 +46,10 @@ void AirspyHFSettings::resetToDefaults() m_attenuatorSteps = 0; m_dcBlock = false; m_iqCorrection = false; + m_replayOffset = 0.0f; + m_replayLength = 20.0f; + m_replayStep = 5.0f; + m_replayLoop = false; } QByteArray AirspyHFSettings::serialize() const @@ -70,6 +74,10 @@ QByteArray AirspyHFSettings::serialize() const s.writeBool(19, m_dcBlock); s.writeBool(20, m_iqCorrection); s.writeBool(21, m_iqOrder); + s.writeFloat(22, m_replayOffset); + s.writeFloat(23, m_replayLength); + s.writeFloat(24, m_replayStep); + s.writeBool(25, m_replayLoop); return s.final(); } @@ -117,6 +125,10 @@ bool AirspyHFSettings::deserialize(const QByteArray& data) d.readBool(19, &m_dcBlock, false); d.readBool(20, &m_iqCorrection, false); d.readBool(21, &m_iqOrder, true); + d.readFloat(22, &m_replayOffset, 0.0f); + d.readFloat(23, &m_replayLength, 20.0f); + d.readFloat(24, &m_replayStep, 5.0f); + d.readBool(25, &m_replayLoop, false); return true; } @@ -186,6 +198,18 @@ void AirspyHFSettings::applySettings(const QStringList& settingsKeys, const Airs if (settingsKeys.contains("iqCorrection")) { m_iqCorrection = settings.m_iqCorrection; } + if (settingsKeys.contains("replayOffset")) { + m_replayOffset = settings.m_replayOffset; + } + if (settingsKeys.contains("replayLength")) { + m_replayLength = settings.m_replayLength; + } + if (settingsKeys.contains("replayStep")) { + m_replayStep = settings.m_replayStep; + } + if (settingsKeys.contains("replayLoop")) { + m_replayLoop = settings.m_replayLoop; + } } QString AirspyHFSettings::getDebugString(const QStringList& settingsKeys, bool force) const @@ -249,6 +273,18 @@ QString AirspyHFSettings::getDebugString(const QStringList& settingsKeys, bool f if (settingsKeys.contains("iqCorrection") || force) { ostr << " m_iqCorrection: " << m_iqCorrection; } + if (settingsKeys.contains("replayOffset") || force) { + ostr << " m_replayOffset: " << m_replayOffset; + } + if (settingsKeys.contains("replayLength") || force) { + ostr << " m_replayLength: " << m_replayLength; + } + if (settingsKeys.contains("replayStep") || force) { + ostr << " m_replayStep: " << m_replayStep; + } + if (settingsKeys.contains("replayLoop") || force) { + ostr << " m_replayLoop: " << m_replayLoop; + } return QString(ostr.str().c_str()); } diff --git a/plugins/samplesource/airspyhf/airspyhfsettings.h b/plugins/samplesource/airspyhf/airspyhfsettings.h index ae8b3f52e..39b1f5c9b 100644 --- a/plugins/samplesource/airspyhf/airspyhfsettings.h +++ b/plugins/samplesource/airspyhf/airspyhfsettings.h @@ -44,6 +44,10 @@ struct AirspyHFSettings quint32 m_attenuatorSteps; bool m_dcBlock; bool m_iqCorrection; + float m_replayOffset; //!< Replay offset in seconds + float m_replayLength; //!< Replay buffer size in seconds + float m_replayStep; //!< Replay forward/back step size in seconds + bool m_replayLoop; //!< Replay buffer repeatedly without recording new data AirspyHFSettings(); void resetToDefaults(); diff --git a/plugins/samplesource/airspyhf/airspyhfworker.cpp b/plugins/samplesource/airspyhf/airspyhfworker.cpp index 5fd50157d..82d6549ad 100644 --- a/plugins/samplesource/airspyhf/airspyhfworker.cpp +++ b/plugins/samplesource/airspyhf/airspyhfworker.cpp @@ -24,11 +24,12 @@ #include "dsp/samplesinkfifo.h" #include "airspyhfworker.h" -AirspyHFWorker::AirspyHFWorker(airspyhf_device_t* dev, SampleSinkFifo* sampleFifo, QObject* parent) : +AirspyHFWorker::AirspyHFWorker(airspyhf_device_t* dev, SampleSinkFifo* sampleFifo, ReplayBuffer *replayBuffer, QObject* parent) : QObject(parent), m_dev(dev), m_convertBuffer(AIRSPYHF_BLOCKSIZE), m_sampleFifo(sampleFifo), + m_replayBuffer(replayBuffer), m_samplerate(10), m_log2Decim(0), m_iqOrder(true) @@ -74,83 +75,129 @@ void AirspyHFWorker::setLog2Decimation(unsigned int log2_decim) } // Decimate according to specified log2 (ex: log2=4 => decim=16) -void AirspyHFWorker::callbackIQ(const float* buf, qint32 len) +void AirspyHFWorker::callbackIQ(const float* inBuf, qint32 len) { SampleVector::iterator it = m_convertBuffer.begin(); - switch (m_log2Decim) - { - case 0: - m_decimatorsIQ.decimate1(&it, buf, len); - break; - case 1: - m_decimatorsIQ.decimate2_cen(&it, buf, len); - break; - case 2: - m_decimatorsIQ.decimate4_cen(&it, buf, len); - break; - case 3: - m_decimatorsIQ.decimate8_cen(&it, buf, len); - break; - case 4: - m_decimatorsIQ.decimate16_cen(&it, buf, len); - break; - case 5: - m_decimatorsIQ.decimate32_cen(&it, buf, len); - break; - case 6: - m_decimatorsIQ.decimate64_cen(&it, buf, len); - break; - case 7: - m_decimatorsIQ.decimate128_cen(&it, buf, len); - break; - case 8: - m_decimatorsIQ.decimate256_cen(&it, buf, len); - break; - default: - break; + // Save data to replay buffer + m_replayBuffer->lock(); + bool replayEnabled = m_replayBuffer->getSize() > 0; + if (replayEnabled) { + m_replayBuffer->write(inBuf, len); + } + + const float* buf = inBuf; + qint32 remaining = len; + + while (remaining > 0) + { + // Choose between live data or replayed data + if (replayEnabled && m_replayBuffer->useReplay()) { + len = m_replayBuffer->read(remaining, buf); + } else { + len = remaining; + } + remaining -= len; + + switch (m_log2Decim) + { + case 0: + m_decimatorsIQ.decimate1(&it, buf, len); + break; + case 1: + m_decimatorsIQ.decimate2_cen(&it, buf, len); + break; + case 2: + m_decimatorsIQ.decimate4_cen(&it, buf, len); + break; + case 3: + m_decimatorsIQ.decimate8_cen(&it, buf, len); + break; + case 4: + m_decimatorsIQ.decimate16_cen(&it, buf, len); + break; + case 5: + m_decimatorsIQ.decimate32_cen(&it, buf, len); + break; + case 6: + m_decimatorsIQ.decimate64_cen(&it, buf, len); + break; + case 7: + m_decimatorsIQ.decimate128_cen(&it, buf, len); + break; + case 8: + m_decimatorsIQ.decimate256_cen(&it, buf, len); + break; + default: + break; + } } + m_replayBuffer->unlock(); + m_sampleFifo->write(m_convertBuffer.begin(), it); } -void AirspyHFWorker::callbackQI(const float* buf, qint32 len) +void AirspyHFWorker::callbackQI(const float* inBuf, qint32 len) { SampleVector::iterator it = m_convertBuffer.begin(); - switch (m_log2Decim) - { - case 0: - m_decimatorsQI.decimate1(&it, buf, len); - break; - case 1: - m_decimatorsQI.decimate2_cen(&it, buf, len); - break; - case 2: - m_decimatorsQI.decimate4_cen(&it, buf, len); - break; - case 3: - m_decimatorsQI.decimate8_cen(&it, buf, len); - break; - case 4: - m_decimatorsQI.decimate16_cen(&it, buf, len); - break; - case 5: - m_decimatorsQI.decimate32_cen(&it, buf, len); - break; - case 6: - m_decimatorsQI.decimate64_cen(&it, buf, len); - break; - case 7: - m_decimatorsQI.decimate128_cen(&it, buf, len); - break; - case 8: - m_decimatorsQI.decimate256_cen(&it, buf, len); - break; - default: - break; + // Save data to replay buffer + m_replayBuffer->lock(); + bool replayEnabled = m_replayBuffer->getSize() > 0; + if (replayEnabled) { + m_replayBuffer->write(inBuf, len); + } + + const float* buf = inBuf; + qint32 remaining = len; + + while (remaining > 0) + { + // Choose between live data or replayed data + if (replayEnabled && m_replayBuffer->useReplay()) { + len = m_replayBuffer->read(remaining, buf); + } else { + len = remaining; + } + remaining -= len; + + switch (m_log2Decim) + { + case 0: + m_decimatorsQI.decimate1(&it, buf, len); + break; + case 1: + m_decimatorsQI.decimate2_cen(&it, buf, len); + break; + case 2: + m_decimatorsQI.decimate4_cen(&it, buf, len); + break; + case 3: + m_decimatorsQI.decimate8_cen(&it, buf, len); + break; + case 4: + m_decimatorsQI.decimate16_cen(&it, buf, len); + break; + case 5: + m_decimatorsQI.decimate32_cen(&it, buf, len); + break; + case 6: + m_decimatorsQI.decimate64_cen(&it, buf, len); + break; + case 7: + m_decimatorsQI.decimate128_cen(&it, buf, len); + break; + case 8: + m_decimatorsQI.decimate256_cen(&it, buf, len); + break; + default: + break; + } } + m_replayBuffer->unlock(); + m_sampleFifo->write(m_convertBuffer.begin(), it); } diff --git a/plugins/samplesource/airspyhf/airspyhfworker.h b/plugins/samplesource/airspyhf/airspyhfworker.h index da00f597b..24809b5d8 100644 --- a/plugins/samplesource/airspyhf/airspyhfworker.h +++ b/plugins/samplesource/airspyhf/airspyhfworker.h @@ -23,6 +23,7 @@ #define INCLUDE_AIRSPYHFWORKER_H #include +#include "dsp/replaybuffer.h" #include #include @@ -34,7 +35,7 @@ class AirspyHFWorker : public QObject { Q_OBJECT public: - AirspyHFWorker(airspyhf_device_t* dev, SampleSinkFifo* sampleFifo, QObject* parent = 0); + AirspyHFWorker(airspyhf_device_t* dev, SampleSinkFifo* sampleFifo, ReplayBuffer *replayBuffer, QObject* parent = 0); ~AirspyHFWorker(); void startWork(); @@ -49,6 +50,7 @@ private: qint16 m_buf[2*AIRSPYHF_BLOCKSIZE]; SampleVector m_convertBuffer; SampleSinkFifo* m_sampleFifo; + ReplayBuffer *m_replayBuffer; int m_samplerate; unsigned int m_log2Decim; diff --git a/plugins/samplesource/rtlsdr/rtlsdrgui.cpp b/plugins/samplesource/rtlsdr/rtlsdrgui.cpp index ac55af373..963e224c4 100644 --- a/plugins/samplesource/rtlsdr/rtlsdrgui.cpp +++ b/plugins/samplesource/rtlsdr/rtlsdrgui.cpp @@ -334,6 +334,10 @@ void RTLSDRGui::displaySettings() ui->lowSampleRate->setChecked(m_settings.m_lowSampleRate); ui->offsetTuning->setChecked(m_settings.m_offsetTuning); ui->biasT->setChecked(m_settings.m_biasTee); + displayReplayLength(); + displayReplayOffset(); + displayReplayStep(); + ui->replayLoop->setChecked(m_settings.m_replayLoop); } void RTLSDRGui::sendSettings() @@ -568,6 +572,9 @@ void RTLSDRGui::openDeviceSettingsDialog(const QPoint& p) if (m_contextMenuType == ContextMenuDeviceSettings) { BasicDeviceSettingsDialog dialog(this); + dialog.setReplayBytesPerSecond(m_settings.m_devSampleRate * 2); + dialog.setReplayLength(m_settings.m_replayLength); + dialog.setReplayStep(m_settings.m_replayStep); dialog.setUseReverseAPI(m_settings.m_useReverseAPI); dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); @@ -581,10 +588,17 @@ void RTLSDRGui::openDeviceSettingsDialog(const QPoint& p) m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_replayLength = dialog.getReplayLength(); + m_settings.m_replayStep = dialog.getReplayStep(); + displayReplayLength(); + displayReplayOffset(); + displayReplayStep(); m_settingsKeys.append("useReverseAPI"); m_settingsKeys.append("reverseAPIAddress"); m_settingsKeys.append("reverseAPIPort"); m_settingsKeys.append("reverseAPIDeviceIndex"); + m_settingsKeys.append("replayLength"); + m_settingsKeys.append("replayStep"); sendSettings(); } @@ -592,6 +606,91 @@ void RTLSDRGui::openDeviceSettingsDialog(const QPoint& p) resetContextMenuType(); } +void RTLSDRGui::displayReplayLength() +{ + bool replayEnabled = m_settings.m_replayLength > 0.0f; + if (!replayEnabled) { + ui->replayOffset->setMaximum(0); + } else { + ui->replayOffset->setMaximum(m_settings.m_replayLength * 10 - 1); + } + ui->replayLabel->setEnabled(replayEnabled); + ui->replayOffset->setEnabled(replayEnabled); + ui->replayOffsetText->setEnabled(replayEnabled); + ui->replaySave->setEnabled(replayEnabled); +} + +void RTLSDRGui::displayReplayOffset() +{ + bool replayEnabled = m_settings.m_replayLength > 0.0f; + ui->replayOffset->setValue(m_settings.m_replayOffset * 10); + ui->replayOffsetText->setText(QString("%1s").arg(m_settings.m_replayOffset, 0, 'f', 1)); + ui->replayNow->setEnabled(replayEnabled && (m_settings.m_replayOffset > 0.0f)); + ui->replayPlus->setEnabled(replayEnabled && (std::round(m_settings.m_replayOffset * 10) < ui->replayOffset->maximum())); + ui->replayMinus->setEnabled(replayEnabled && (m_settings.m_replayOffset > 0.0f)); +} + +void RTLSDRGui::displayReplayStep() +{ + QString step; + float intpart; + float frac = modf(m_settings.m_replayStep, &intpart); + if (frac == 0.0f) { + step = QString::number((int)intpart); + } else { + step = QString::number(m_settings.m_replayStep, 'f', 1); + } + ui->replayPlus->setText(QString("+%1s").arg(step)); + ui->replayPlus->setToolTip(QString("Add %1 seconds to time delay").arg(step)); + ui->replayMinus->setText(QString("-%1s").arg(step)); + ui->replayMinus->setToolTip(QString("Remove %1 seconds from time delay").arg(step)); +} + +void RTLSDRGui::on_replayOffset_valueChanged(int value) +{ + m_settings.m_replayOffset = value / 10.0f; + displayReplayOffset(); + m_settingsKeys.append("replayOffset"); + sendSettings(); +} + +void RTLSDRGui::on_replayNow_clicked() +{ + ui->replayOffset->setValue(0); +} + +void RTLSDRGui::on_replayPlus_clicked() +{ + ui->replayOffset->setValue(ui->replayOffset->value() + m_settings.m_replayStep * 10); +} + +void RTLSDRGui::on_replayMinus_clicked() +{ + ui->replayOffset->setValue(ui->replayOffset->value() - m_settings.m_replayStep * 10); +} + +void RTLSDRGui::on_replaySave_clicked() +{ + QFileDialog fileDialog(nullptr, "Select file to save IQ data to", "", "*.wav"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + RTLSDRInput::MsgSaveReplay *message = RTLSDRInput::MsgSaveReplay::create(fileNames[0]); + m_sampleSource->getInputMessageQueue()->push(message); + } + } +} + +void RTLSDRGui::on_replayLoop_toggled(bool checked) +{ + m_settings.m_replayLoop = checked; + m_settingsKeys.append("replayLoop"); + sendSettings(); +} + void RTLSDRGui::makeUIConnections() { QObject::connect(ui->centerFrequency, &ValueDial::changed, this, &RTLSDRGui::on_centerFrequency_changed); @@ -611,4 +710,10 @@ void RTLSDRGui::makeUIConnections() QObject::connect(ui->transverter, &TransverterButton::clicked, this, &RTLSDRGui::on_transverter_clicked); QObject::connect(ui->sampleRateMode, &QToolButton::toggled, this, &RTLSDRGui::on_sampleRateMode_toggled); QObject::connect(ui->biasT, &QCheckBox::stateChanged, this, &RTLSDRGui::on_biasT_stateChanged); + QObject::connect(ui->replayOffset, &QSlider::valueChanged, this, &RTLSDRGui::on_replayOffset_valueChanged); + QObject::connect(ui->replayNow, &QToolButton::clicked, this, &RTLSDRGui::on_replayNow_clicked); + QObject::connect(ui->replayPlus, &QToolButton::clicked, this, &RTLSDRGui::on_replayPlus_clicked); + QObject::connect(ui->replayMinus, &QToolButton::clicked, this, &RTLSDRGui::on_replayMinus_clicked); + QObject::connect(ui->replaySave, &QToolButton::clicked, this, &RTLSDRGui::on_replaySave_clicked); + QObject::connect(ui->replayLoop, &ButtonSwitch::toggled, this, &RTLSDRGui::on_replayLoop_toggled); } diff --git a/plugins/samplesource/rtlsdr/rtlsdrgui.h b/plugins/samplesource/rtlsdr/rtlsdrgui.h index 7ca3063a3..3cac63c4d 100644 --- a/plugins/samplesource/rtlsdr/rtlsdrgui.h +++ b/plugins/samplesource/rtlsdr/rtlsdrgui.h @@ -70,6 +70,9 @@ private: void displaySampleRate(); void displayFcTooltip(); void displaySettings(); + void displayReplayLength(); + void displayReplayOffset(); + void displayReplayStep(); void sendSettings(); void updateSampleRateAndFrequency(); void updateFrequencyLimits(); @@ -96,6 +99,12 @@ private slots: void on_transverter_clicked(); void on_sampleRateMode_toggled(bool checked); void on_biasT_stateChanged(int state); + void on_replayOffset_valueChanged(int value); + void on_replayNow_clicked(); + void on_replayPlus_clicked(); + void on_replayMinus_clicked(); + void on_replaySave_clicked(); + void on_replayLoop_toggled(bool checked); void openDeviceSettingsDialog(const QPoint& p); void updateHardware(); void updateStatus(); diff --git a/plugins/samplesource/rtlsdr/rtlsdrgui.ui b/plugins/samplesource/rtlsdr/rtlsdrgui.ui index 190f2236a..6bf6c320a 100644 --- a/plugins/samplesource/rtlsdr/rtlsdrgui.ui +++ b/plugins/samplesource/rtlsdr/rtlsdrgui.ui @@ -7,7 +7,7 @@ 0 0 360 - 217 + 240 @@ -19,13 +19,13 @@ 360 - 217 + 240 380 - 229 + 240 @@ -598,29 +598,6 @@ 3 - - - - Toggles RTLSDR AGC - - - AGC - - - - - - - - 0 - 0 - - - - Gain - - - @@ -640,6 +617,19 @@ + + + + + 0 + 0 + + + + Gain + + + @@ -656,6 +646,121 @@ + + + + Toggles RTLSDR AGC + + + AGC + + + + + + + + + Qt::Horizontal + + + + + + + + + + 65 + 0 + + + + Time Delay + + + + + + + Replay time delay in seconds + + + 500 + + + Qt::Horizontal + + + + + + + Replay time delay in seconds + + + 0.0s + + + + + + + Set time delay to 0 seconds + + + Now + + + + + + + Add displayed number of seconds to time delay + + + +5s + + + + + + + Remove displayed number of seconds from time delay + + + -5s + + + + + + + Repeatedly replay data in replay buffer + + + + + + + :/playloop.png:/playloop.png + + + + + + + Save replay buffer to a file + + + + + + + :/save.png:/save.png + + + diff --git a/plugins/samplesource/rtlsdr/rtlsdrinput.cpp b/plugins/samplesource/rtlsdr/rtlsdrinput.cpp index 57fb13455..a791d3704 100644 --- a/plugins/samplesource/rtlsdr/rtlsdrinput.cpp +++ b/plugins/samplesource/rtlsdr/rtlsdrinput.cpp @@ -45,6 +45,7 @@ MESSAGE_CLASS_DEFINITION(RTLSDRInput::MsgConfigureRTLSDR, Message) MESSAGE_CLASS_DEFINITION(RTLSDRInput::MsgStartStop, Message) +MESSAGE_CLASS_DEFINITION(RTLSDRInput::MsgSaveReplay, Message) const quint64 RTLSDRInput::frequencyLowRangeMin = 0UL; const quint64 RTLSDRInput::frequencyLowRangeMax = 275000UL; @@ -238,7 +239,7 @@ bool RTLSDRInput::start() if (m_running) stop(); - m_rtlSDRThread = new RTLSDRThread(m_dev, &m_sampleFifo); + m_rtlSDRThread = new RTLSDRThread(m_dev, &m_sampleFifo, &m_replayBuffer); m_rtlSDRThread->setSamplerate(m_settings.m_devSampleRate); m_rtlSDRThread->setLog2Decimation(m_settings.m_log2Decim); m_rtlSDRThread->setFcPos((int) m_settings.m_fcPos); @@ -374,6 +375,12 @@ bool RTLSDRInput::handleMessage(const Message& message) return true; } + else if (MsgSaveReplay::match(message)) + { + MsgSaveReplay& cmd = (MsgSaveReplay&) message; + m_replayBuffer.save(cmd.getFilename(), m_settings.m_devSampleRate, getCenterFrequency()); + return true; + } else { return false; @@ -432,6 +439,9 @@ bool RTLSDRInput::applySettings(const RTLSDRSettings& settings, const QList #include "dsp/devicesamplesource.h" +#include "dsp/replaybuffer.h" #include "rtlsdrsettings.h" #include @@ -83,6 +84,25 @@ public: { } }; + class MsgSaveReplay : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getFilename() const { return m_filename; } + + static MsgSaveReplay* create(const QString& filename) { + return new MsgSaveReplay(filename); + } + + protected: + QString m_filename; + + MsgSaveReplay(const QString& filename) : + Message(), + m_filename(filename) + { } + }; + RTLSDRInput(DeviceAPI *deviceAPI); virtual ~RTLSDRInput(); virtual void destroy(); @@ -161,6 +181,7 @@ private: bool m_running; QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; + ReplayBuffer m_replayBuffer; bool openDevice(); void closeDevice(); diff --git a/plugins/samplesource/rtlsdr/rtlsdrsettings.cpp b/plugins/samplesource/rtlsdr/rtlsdrsettings.cpp index 854e2e6e7..7768467cc 100644 --- a/plugins/samplesource/rtlsdr/rtlsdrsettings.cpp +++ b/plugins/samplesource/rtlsdr/rtlsdrsettings.cpp @@ -43,6 +43,10 @@ void RTLSDRSettings::resetToDefaults() m_rfBandwidth = 2500 * 1000; // Hz m_offsetTuning = false; m_biasTee = false; + m_replayOffset = 0.0f; + m_replayLength = 20.0f; + m_replayStep = 5.0f; + m_replayLoop = false; m_useReverseAPI = false; m_reverseAPIAddress = "127.0.0.1"; m_reverseAPIPort = 8888; @@ -73,6 +77,10 @@ QByteArray RTLSDRSettings::serialize() const s.writeU32(19, m_reverseAPIDeviceIndex); s.writeBool(20, m_iqOrder); s.writeBool(21, m_biasTee); + s.writeFloat(22, m_replayOffset); + s.writeFloat(23, m_replayLength); + s.writeFloat(24, m_replayStep); + s.writeBool(25, m_replayLoop); return s.final(); } @@ -121,6 +129,10 @@ bool RTLSDRSettings::deserialize(const QByteArray& data) m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; d.readBool(20, &m_iqOrder, true); d.readBool(21, &m_biasTee, false); + d.readFloat(22, &m_replayOffset, 0.0f); + d.readFloat(23, &m_replayLength, 20.0f); + d.readFloat(24, &m_replayStep, 5.0f); + d.readBool(25, &m_replayLoop, false); return true; } @@ -187,6 +199,18 @@ void RTLSDRSettings::applySettings(const QStringList& settingsKeys, const RTLSDR if (settingsKeys.contains("biasTee")) { m_biasTee = settings.m_biasTee; } + if (settingsKeys.contains("replayOffset")) { + m_replayOffset = settings.m_replayOffset; + } + if (settingsKeys.contains("replayLength")) { + m_replayLength = settings.m_replayLength; + } + if (settingsKeys.contains("replayStep")) { + m_replayStep = settings.m_replayStep; + } + if (settingsKeys.contains("replayLoop")) { + m_replayLoop = settings.m_replayLoop; + } if (settingsKeys.contains("useReverseAPI")) { m_useReverseAPI = settings.m_useReverseAPI; } @@ -256,6 +280,18 @@ QString RTLSDRSettings::getDebugString(const QStringList& settingsKeys, bool for if (settingsKeys.contains("biasTee") || force) { ostr << " m_biasTee: " << m_biasTee; } + if (settingsKeys.contains("replayOffset") || force) { + ostr << " m_replayOffset: " << m_replayOffset; + } + if (settingsKeys.contains("replayLength") || force) { + ostr << " m_replayLength: " << m_replayLength; + } + if (settingsKeys.contains("replayStep") || force) { + ostr << " m_replayStep: " << m_replayStep; + } + if (settingsKeys.contains("replayLoop") || force) { + ostr << " m_replayLoop: " << m_replayLoop; + } if (settingsKeys.contains("useReverseAPI") || force) { ostr << " m_useReverseAPI: " << m_useReverseAPI; } diff --git a/plugins/samplesource/rtlsdr/rtlsdrsettings.h b/plugins/samplesource/rtlsdr/rtlsdrsettings.h index 29f8164a3..046d8af9b 100644 --- a/plugins/samplesource/rtlsdr/rtlsdrsettings.h +++ b/plugins/samplesource/rtlsdr/rtlsdrsettings.h @@ -47,6 +47,10 @@ struct RTLSDRSettings { quint32 m_rfBandwidth; //!< RF filter bandwidth in Hz bool m_offsetTuning; bool m_biasTee; + float m_replayOffset; //!< Replay offset in seconds + float m_replayLength; //!< Replay buffer size in seconds + float m_replayStep; //!< Replay forward/back step size in seconds + bool m_replayLoop; //!< Replay buffer repeatedly without recording new data bool m_useReverseAPI; QString m_reverseAPIAddress; uint16_t m_reverseAPIPort; diff --git a/plugins/samplesource/rtlsdr/rtlsdrthread.cpp b/plugins/samplesource/rtlsdr/rtlsdrthread.cpp index 74bbd79e3..a0c63b0b1 100644 --- a/plugins/samplesource/rtlsdr/rtlsdrthread.cpp +++ b/plugins/samplesource/rtlsdr/rtlsdrthread.cpp @@ -18,6 +18,8 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// +#include + #include #include #include "rtlsdrthread.h" @@ -26,12 +28,13 @@ #define FCD_BLOCKSIZE 16384 -RTLSDRThread::RTLSDRThread(rtlsdr_dev_t* dev, SampleSinkFifo* sampleFifo, QObject* parent) : +RTLSDRThread::RTLSDRThread(rtlsdr_dev_t* dev, SampleSinkFifo* sampleFifo, ReplayBuffer *replayBuffer, QObject* parent) : QThread(parent), m_running(false), m_dev(dev), m_convertBuffer(FCD_BLOCKSIZE), m_sampleFifo(sampleFifo), + m_replayBuffer(replayBuffer), m_samplerate(288000), m_log2Decim(4), m_fcPos(0), @@ -92,192 +95,241 @@ void RTLSDRThread::run() } // Decimate according to specified log2 (ex: log2=4 => decim=16) -void RTLSDRThread::callbackIQ(const quint8* buf, qint32 len) +// Len is total samples (i.e. one I and Q pair will have len=2) +void RTLSDRThread::callbackIQ(const quint8* inBuf, qint32 len) { SampleVector::iterator it = m_convertBuffer.begin(); - if (m_log2Decim == 0) - { - m_decimatorsIQ.decimate1(&it, buf, len); + // Save data to replay buffer + m_replayBuffer->lock(); + bool replayEnabled = m_replayBuffer->getSize() > 0; + if (replayEnabled) { + m_replayBuffer->write(inBuf, len); } - else + + const quint8* buf = inBuf; + qint32 remaining = len; + + while (remaining > 0) { - if (m_fcPos == 0) // Infradyne - { - switch (m_log2Decim) - { - case 1: - m_decimatorsIQ.decimate2_inf(&it, buf, len); - break; - case 2: - m_decimatorsIQ.decimate4_inf(&it, buf, len); - break; - case 3: - m_decimatorsIQ.decimate8_inf(&it, buf, len); - break; - case 4: - m_decimatorsIQ.decimate16_inf(&it, buf, len); - break; - case 5: - m_decimatorsIQ.decimate32_inf(&it, buf, len); - break; - case 6: - m_decimatorsIQ.decimate64_inf(&it, buf, len); - break; - default: - break; - } + // Choose between live data or replayed data + if (replayEnabled && m_replayBuffer->useReplay()) { + len = m_replayBuffer->read(remaining, buf); + qDebug() << "Replay"; + } else { + len = remaining; + qDebug() << "Live" << m_replayBuffer->useReplay(); } - else if (m_fcPos == 1) // Supradyne + remaining -= len; + + if (m_log2Decim == 0) { - switch (m_log2Decim) - { - case 1: - m_decimatorsIQ.decimate2_sup(&it, buf, len); - break; - case 2: - m_decimatorsIQ.decimate4_sup(&it, buf, len); - break; - case 3: - m_decimatorsIQ.decimate8_sup(&it, buf, len); - break; - case 4: - m_decimatorsIQ.decimate16_sup(&it, buf, len); - break; - case 5: - m_decimatorsIQ.decimate32_sup(&it, buf, len); - break; - case 6: - m_decimatorsIQ.decimate64_sup(&it, buf, len); - break; - default: - break; - } + m_decimatorsIQ.decimate1(&it, buf, len); } - else // Centered + else { - switch (m_log2Decim) + if (m_fcPos == 0) // Infradyne { - case 1: - m_decimatorsIQ.decimate2_cen(&it, buf, len); - break; - case 2: - m_decimatorsIQ.decimate4_cen(&it, buf, len); - break; - case 3: - m_decimatorsIQ.decimate8_cen(&it, buf, len); - break; - case 4: - m_decimatorsIQ.decimate16_cen(&it, buf, len); - break; - case 5: - m_decimatorsIQ.decimate32_cen(&it, buf, len); - break; - case 6: - m_decimatorsIQ.decimate64_cen(&it, buf, len); - break; - default: - break; + switch (m_log2Decim) + { + case 1: + m_decimatorsIQ.decimate2_inf(&it, buf, len); + break; + case 2: + m_decimatorsIQ.decimate4_inf(&it, buf, len); + break; + case 3: + m_decimatorsIQ.decimate8_inf(&it, buf, len); + break; + case 4: + m_decimatorsIQ.decimate16_inf(&it, buf, len); + break; + case 5: + m_decimatorsIQ.decimate32_inf(&it, buf, len); + break; + case 6: + m_decimatorsIQ.decimate64_inf(&it, buf, len); + break; + default: + break; + } + } + else if (m_fcPos == 1) // Supradyne + { + switch (m_log2Decim) + { + case 1: + m_decimatorsIQ.decimate2_sup(&it, buf, len); + break; + case 2: + m_decimatorsIQ.decimate4_sup(&it, buf, len); + break; + case 3: + m_decimatorsIQ.decimate8_sup(&it, buf, len); + break; + case 4: + m_decimatorsIQ.decimate16_sup(&it, buf, len); + break; + case 5: + m_decimatorsIQ.decimate32_sup(&it, buf, len); + break; + case 6: + m_decimatorsIQ.decimate64_sup(&it, buf, len); + break; + default: + break; + } + } + else // Centered + { + switch (m_log2Decim) + { + case 1: + m_decimatorsIQ.decimate2_cen(&it, buf, len); + break; + case 2: + m_decimatorsIQ.decimate4_cen(&it, buf, len); + break; + case 3: + m_decimatorsIQ.decimate8_cen(&it, buf, len); + break; + case 4: + m_decimatorsIQ.decimate16_cen(&it, buf, len); + break; + case 5: + m_decimatorsIQ.decimate32_cen(&it, buf, len); + break; + case 6: + m_decimatorsIQ.decimate64_cen(&it, buf, len); + break; + default: + break; + } } } } + m_replayBuffer->unlock(); + m_sampleFifo->write(m_convertBuffer.begin(), it); if(!m_running) rtlsdr_cancel_async(m_dev); } -void RTLSDRThread::callbackQI(const quint8* buf, qint32 len) +void RTLSDRThread::callbackQI(const quint8* inBuf, qint32 len) { SampleVector::iterator it = m_convertBuffer.begin(); - if (m_log2Decim == 0) - { - m_decimatorsQI.decimate1(&it, buf, len); + // Save data to replay buffer + m_replayBuffer->lock(); + bool replayEnabled = m_replayBuffer->getSize() > 0; + if (replayEnabled) { + m_replayBuffer->write(inBuf, len); } - else + + const quint8* buf = inBuf; + qint32 remaining = len; + + while (remaining > 0) { - if (m_fcPos == 0) // Infradyne - { - switch (m_log2Decim) - { - case 1: - m_decimatorsQI.decimate2_inf(&it, buf, len); - break; - case 2: - m_decimatorsQI.decimate4_inf(&it, buf, len); - break; - case 3: - m_decimatorsQI.decimate8_inf(&it, buf, len); - break; - case 4: - m_decimatorsQI.decimate16_inf(&it, buf, len); - break; - case 5: - m_decimatorsQI.decimate32_inf(&it, buf, len); - break; - case 6: - m_decimatorsQI.decimate64_inf(&it, buf, len); - break; - default: - break; - } + // Choose between live data or replayed data + if (replayEnabled && m_replayBuffer->useReplay()) { + len = m_replayBuffer->read(remaining, buf); + } else { + len = remaining; } - else if (m_fcPos == 1) // Supradyne + remaining -= len; + + if (m_log2Decim == 0) { - switch (m_log2Decim) - { - case 1: - m_decimatorsQI.decimate2_sup(&it, buf, len); - break; - case 2: - m_decimatorsQI.decimate4_sup(&it, buf, len); - break; - case 3: - m_decimatorsQI.decimate8_sup(&it, buf, len); - break; - case 4: - m_decimatorsQI.decimate16_sup(&it, buf, len); - break; - case 5: - m_decimatorsQI.decimate32_sup(&it, buf, len); - break; - case 6: - m_decimatorsQI.decimate64_sup(&it, buf, len); - break; - default: - break; - } + m_decimatorsQI.decimate1(&it, buf, len); } - else // Centered + else { - switch (m_log2Decim) + if (m_fcPos == 0) // Infradyne { - case 1: - m_decimatorsQI.decimate2_cen(&it, buf, len); - break; - case 2: - m_decimatorsQI.decimate4_cen(&it, buf, len); - break; - case 3: - m_decimatorsQI.decimate8_cen(&it, buf, len); - break; - case 4: - m_decimatorsQI.decimate16_cen(&it, buf, len); - break; - case 5: - m_decimatorsQI.decimate32_cen(&it, buf, len); - break; - case 6: - m_decimatorsQI.decimate64_cen(&it, buf, len); - break; - default: - break; + switch (m_log2Decim) + { + case 1: + m_decimatorsQI.decimate2_inf(&it, buf, len); + break; + case 2: + m_decimatorsQI.decimate4_inf(&it, buf, len); + break; + case 3: + m_decimatorsQI.decimate8_inf(&it, buf, len); + break; + case 4: + m_decimatorsQI.decimate16_inf(&it, buf, len); + break; + case 5: + m_decimatorsQI.decimate32_inf(&it, buf, len); + break; + case 6: + m_decimatorsQI.decimate64_inf(&it, buf, len); + break; + default: + break; + } + } + else if (m_fcPos == 1) // Supradyne + { + switch (m_log2Decim) + { + case 1: + m_decimatorsQI.decimate2_sup(&it, buf, len); + break; + case 2: + m_decimatorsQI.decimate4_sup(&it, buf, len); + break; + case 3: + m_decimatorsQI.decimate8_sup(&it, buf, len); + break; + case 4: + m_decimatorsQI.decimate16_sup(&it, buf, len); + break; + case 5: + m_decimatorsQI.decimate32_sup(&it, buf, len); + break; + case 6: + m_decimatorsQI.decimate64_sup(&it, buf, len); + break; + default: + break; + } + } + else // Centered + { + switch (m_log2Decim) + { + case 1: + m_decimatorsQI.decimate2_cen(&it, buf, len); + break; + case 2: + m_decimatorsQI.decimate4_cen(&it, buf, len); + break; + case 3: + m_decimatorsQI.decimate8_cen(&it, buf, len); + break; + case 4: + m_decimatorsQI.decimate16_cen(&it, buf, len); + break; + case 5: + m_decimatorsQI.decimate32_cen(&it, buf, len); + break; + case 6: + m_decimatorsQI.decimate64_cen(&it, buf, len); + break; + default: + break; + } } } } + m_replayBuffer->unlock(); + m_sampleFifo->write(m_convertBuffer.begin(), it); if(!m_running) @@ -294,4 +346,3 @@ void RTLSDRThread::callbackHelper(unsigned char* buf, uint32_t len, void* ctx) thread->callbackQI(buf, len); } } - diff --git a/plugins/samplesource/rtlsdr/rtlsdrthread.h b/plugins/samplesource/rtlsdr/rtlsdrthread.h index fb3d4a63f..1489545cf 100644 --- a/plugins/samplesource/rtlsdr/rtlsdrthread.h +++ b/plugins/samplesource/rtlsdr/rtlsdrthread.h @@ -26,6 +26,7 @@ #include #include +#include "dsp/replaybuffer.h" #include "dsp/samplesinkfifo.h" #include "dsp/decimatorsu.h" @@ -33,7 +34,7 @@ class RTLSDRThread : public QThread { Q_OBJECT public: - RTLSDRThread(rtlsdr_dev_t* dev, SampleSinkFifo* sampleFifo, QObject* parent = NULL); + RTLSDRThread(rtlsdr_dev_t* dev, SampleSinkFifo* sampleFifo, ReplayBuffer *replayBuffer, QObject* parent = NULL); ~RTLSDRThread(); void startWork(); @@ -51,6 +52,7 @@ private: rtlsdr_dev_t* m_dev; SampleVector m_convertBuffer; SampleSinkFifo* m_sampleFifo; + ReplayBuffer *m_replayBuffer; int m_samplerate; unsigned int m_log2Decim; diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3gui.cpp b/plugins/samplesource/sdrplayv3/sdrplayv3gui.cpp index bd7c2f6e7..1b7d6e43c 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3gui.cpp +++ b/plugins/samplesource/sdrplayv3/sdrplayv3gui.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "sdrplayv3gui.h" #include "sdrplayv3input.h" @@ -306,6 +307,10 @@ void SDRPlayV3Gui::displaySettings() ui->gainIF->setValue(gain); QString gainText = QStringLiteral("%1").arg(gain, 2, 10, QLatin1Char('0')); ui->gainIFText->setText(gainText); + displayReplayLength(); + displayReplayOffset(); + displayReplayStep(); + ui->replayLoop->setChecked(m_settings.m_replayLoop); } void SDRPlayV3Gui::updateLNAValues() @@ -563,6 +568,9 @@ void SDRPlayV3Gui::openDeviceSettingsDialog(const QPoint& p) if (m_contextMenuType == ContextMenuDeviceSettings) { BasicDeviceSettingsDialog dialog(this); + dialog.setReplayBytesPerSecond(m_settings.m_devSampleRate * 2 * sizeof(qint16)); + dialog.setReplayLength(m_settings.m_replayLength); + dialog.setReplayStep(m_settings.m_replayStep); dialog.setUseReverseAPI(m_settings.m_useReverseAPI); dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); @@ -576,10 +584,17 @@ void SDRPlayV3Gui::openDeviceSettingsDialog(const QPoint& p) m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_replayLength = dialog.getReplayLength(); + m_settings.m_replayStep = dialog.getReplayStep(); + displayReplayLength(); + displayReplayOffset(); + displayReplayStep(); m_settingsKeys.append("useReverseAPI"); m_settingsKeys.append("reverseAPIAddress"); m_settingsKeys.append("reverseAPIPort"); m_settingsKeys.append("reverseAPIDeviceIndex"); + m_settingsKeys.append("replayLength"); + m_settingsKeys.append("replayStep"); sendSettings(); } @@ -587,6 +602,91 @@ void SDRPlayV3Gui::openDeviceSettingsDialog(const QPoint& p) resetContextMenuType(); } +void SDRPlayV3Gui::displayReplayLength() +{ + bool replayEnabled = m_settings.m_replayLength > 0.0f; + if (!replayEnabled) { + ui->replayOffset->setMaximum(0); + } else { + ui->replayOffset->setMaximum(m_settings.m_replayLength * 10 - 1); + } + ui->replayLabel->setEnabled(replayEnabled); + ui->replayOffset->setEnabled(replayEnabled); + ui->replayOffsetText->setEnabled(replayEnabled); + ui->replaySave->setEnabled(replayEnabled); +} + +void SDRPlayV3Gui::displayReplayOffset() +{ + bool replayEnabled = m_settings.m_replayLength > 0.0f; + ui->replayOffset->setValue(m_settings.m_replayOffset * 10); + ui->replayOffsetText->setText(QString("%1s").arg(m_settings.m_replayOffset, 0, 'f', 1)); + ui->replayNow->setEnabled(replayEnabled && (m_settings.m_replayOffset > 0.0f)); + ui->replayPlus->setEnabled(replayEnabled && (std::round(m_settings.m_replayOffset * 10) < ui->replayOffset->maximum())); + ui->replayMinus->setEnabled(replayEnabled && (m_settings.m_replayOffset > 0.0f)); +} + +void SDRPlayV3Gui::displayReplayStep() +{ + QString step; + float intpart; + float frac = modf(m_settings.m_replayStep, &intpart); + if (frac == 0.0f) { + step = QString::number((int)intpart); + } else { + step = QString::number(m_settings.m_replayStep, 'f', 1); + } + ui->replayPlus->setText(QString("+%1s").arg(step)); + ui->replayPlus->setToolTip(QString("Add %1 seconds to time delay").arg(step)); + ui->replayMinus->setText(QString("-%1s").arg(step)); + ui->replayMinus->setToolTip(QString("Remove %1 seconds from time delay").arg(step)); +} + +void SDRPlayV3Gui::on_replayOffset_valueChanged(int value) +{ + m_settings.m_replayOffset = value / 10.0f; + displayReplayOffset(); + m_settingsKeys.append("replayOffset"); + sendSettings(); +} + +void SDRPlayV3Gui::on_replayNow_clicked() +{ + ui->replayOffset->setValue(0); +} + +void SDRPlayV3Gui::on_replayPlus_clicked() +{ + ui->replayOffset->setValue(ui->replayOffset->value() + m_settings.m_replayStep * 10); +} + +void SDRPlayV3Gui::on_replayMinus_clicked() +{ + ui->replayOffset->setValue(ui->replayOffset->value() - m_settings.m_replayStep * 10); +} + +void SDRPlayV3Gui::on_replaySave_clicked() +{ + QFileDialog fileDialog(nullptr, "Select file to save IQ data to", "", "*.wav"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + SDRPlayV3Input::MsgSaveReplay *message = SDRPlayV3Input::MsgSaveReplay::create(fileNames[0]); + m_sdrPlayV3Input->getInputMessageQueue()->push(message); + } + } +} + +void SDRPlayV3Gui::on_replayLoop_toggled(bool checked) +{ + m_settings.m_replayLoop = checked; + m_settingsKeys.append("replayLoop"); + sendSettings(); +} + void SDRPlayV3Gui::makeUIConnections() { QObject::connect(ui->centerFrequency, &ValueDial::changed, this, &SDRPlayV3Gui::on_centerFrequency_changed); @@ -610,4 +710,10 @@ void SDRPlayV3Gui::makeUIConnections() QObject::connect(ui->gainIF, &QDial::valueChanged, this, &SDRPlayV3Gui::on_gainIF_valueChanged); QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &SDRPlayV3Gui::on_startStop_toggled); QObject::connect(ui->transverter, &TransverterButton::clicked, this, &SDRPlayV3Gui::on_transverter_clicked); + QObject::connect(ui->replayOffset, &QSlider::valueChanged, this, &SDRPlayV3Gui::on_replayOffset_valueChanged); + QObject::connect(ui->replayNow, &QToolButton::clicked, this, &SDRPlayV3Gui::on_replayNow_clicked); + QObject::connect(ui->replayPlus, &QToolButton::clicked, this, &SDRPlayV3Gui::on_replayPlus_clicked); + QObject::connect(ui->replayMinus, &QToolButton::clicked, this, &SDRPlayV3Gui::on_replayMinus_clicked); + QObject::connect(ui->replaySave, &QToolButton::clicked, this, &SDRPlayV3Gui::on_replaySave_clicked); + QObject::connect(ui->replayLoop, &ButtonSwitch::toggled, this, &SDRPlayV3Gui::on_replayLoop_toggled); } diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3gui.h b/plugins/samplesource/sdrplayv3/sdrplayv3gui.h index 4ecb5a813..7a130bebc 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3gui.h +++ b/plugins/samplesource/sdrplayv3/sdrplayv3gui.h @@ -64,6 +64,9 @@ private: void blockApplySettings(bool block) { m_doApplySettings = !block; } void displaySettings(); + void displayReplayLength(); + void displayReplayOffset(); + void displayReplayStep(); void updateLNAValues(); void sendSettings(); void updateSampleRateAndFrequency(); @@ -96,6 +99,12 @@ private slots: void on_gainIF_valueChanged(int value); void on_startStop_toggled(bool checked); void on_transverter_clicked(); + void on_replayOffset_valueChanged(int value); + void on_replayNow_clicked(); + void on_replayPlus_clicked(); + void on_replayMinus_clicked(); + void on_replaySave_clicked(); + void on_replayLoop_toggled(bool checked); void openDeviceSettingsDialog(const QPoint& p); }; diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3gui.ui b/plugins/samplesource/sdrplayv3/sdrplayv3gui.ui index b3f5a04e1..0f11d089c 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3gui.ui +++ b/plugins/samplesource/sdrplayv3/sdrplayv3gui.ui @@ -7,7 +7,7 @@ 0 0 360 - 234 + 280 @@ -19,13 +19,13 @@ 360 - 234 + 280 409 - 297 + 280 @@ -636,7 +636,6 @@ Liberation Mono 12 - 50 false @@ -764,20 +763,125 @@ + + + + Qt::Horizontal + + + + + + + + + + 65 + 0 + + + + Time Delay + + + + + + + Replay time delay in seconds + + + 500 + + + Qt::Horizontal + + + + + + + Replay time delay in seconds + + + 0.0s + + + + + + + Set time delay to 0 seconds + + + Now + + + + + + + Add displayed number of seconds to time delay + + + +5s + + + + + + + Remove displayed number of seconds from time delay + + + -5s + + + + + + + Repeatedly replay data in replay buffer + + + + + + + :/playloop.png:/playloop.png + + + + + + + Save replay buffer to a file + + + + + + + :/save.png:/save.png + + + + + - - ButtonSwitch - QToolButton -
gui/buttonswitch.h
-
ValueDial QWidget
gui/valuedial.h
1
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
TransverterButton QPushButton diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3input.cpp b/plugins/samplesource/sdrplayv3/sdrplayv3input.cpp index e62df7d5b..ef1aec12e 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3input.cpp +++ b/plugins/samplesource/sdrplayv3/sdrplayv3input.cpp @@ -42,6 +42,7 @@ MESSAGE_CLASS_DEFINITION(SDRPlayV3Input::MsgConfigureSDRPlayV3, Message) MESSAGE_CLASS_DEFINITION(SDRPlayV3Input::MsgStartStop, Message) +MESSAGE_CLASS_DEFINITION(SDRPlayV3Input::MsgSaveReplay, Message) SDRPlayV3Input::SDRPlayV3Input(DeviceAPI *deviceAPI) : m_deviceAPI(deviceAPI), @@ -155,7 +156,7 @@ bool SDRPlayV3Input::start() if (m_running) stop(); - m_sdrPlayThread = new SDRPlayV3Thread(m_dev, &m_sampleFifo); + m_sdrPlayThread = new SDRPlayV3Thread(m_dev, &m_sampleFifo, &m_replayBuffer); m_sdrPlayThread->setLog2Decimation(m_settings.m_log2Decim); m_sdrPlayThread->setFcPos((int) m_settings.m_fcPos); m_sdrPlayThread->startWork(); @@ -306,6 +307,12 @@ bool SDRPlayV3Input::handleMessage(const Message& message) return true; } + else if (MsgSaveReplay::match(message)) + { + MsgSaveReplay& cmd = (MsgSaveReplay&) message; + m_replayBuffer.save(cmd.getFilename(), m_settings.m_devSampleRate, getCenterFrequency()); + return true; + } else { return false; @@ -408,7 +415,10 @@ bool SDRPlayV3Input::applySettings(const SDRPlayV3Settings& settings, const QLis else qDebug() << "SDRPlayV3Input::applySettings: sample rate set to " << sampleRate; forwardChange = true; - } + if (settings.m_devSampleRate != m_settings.m_devSampleRate) { + m_replayBuffer.clear(); + } + } } if (settingsKeys.contains("log2Decim") || force) @@ -696,6 +706,18 @@ bool SDRPlayV3Input::applySettings(const SDRPlayV3Settings& settings, const QLis m_settings.applySettings(settingsKeys, settings); } + if (settingsKeys.contains("replayLength") || settingsKeys.contains("devSampleRate") || force) { + m_replayBuffer.setSize(m_settings.m_replayLength, m_settings.m_devSampleRate); + } + + if (settingsKeys.contains("replayOffset") || settingsKeys.contains("devSampleRate") || force) { + m_replayBuffer.setReadOffset(((unsigned)(m_settings.m_replayOffset * m_settings.m_devSampleRate)) * 2); + } + + if (settingsKeys.contains("replayLoop") || force) { + m_replayBuffer.setLoop(m_settings.m_replayLoop); + } + if (forwardChange) { int sampleRate = getSampleRate(); diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3input.h b/plugins/samplesource/sdrplayv3/sdrplayv3input.h index 02fd30108..ad84651a6 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3input.h +++ b/plugins/samplesource/sdrplayv3/sdrplayv3input.h @@ -27,6 +27,7 @@ #include #include "dsp/devicesamplesource.h" +#include "dsp/replaybuffer.h" #include "sdrplayv3settings.h" class QNetworkAccessManager; @@ -82,6 +83,25 @@ public: { } }; + class MsgSaveReplay : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getFilename() const { return m_filename; } + + static MsgSaveReplay* create(const QString& filename) { + return new MsgSaveReplay(filename); + } + + protected: + QString m_filename; + + MsgSaveReplay(const QString& filename) : + Message(), + m_filename(filename) + { } + }; + SDRPlayV3Input(DeviceAPI *deviceAPI); virtual ~SDRPlayV3Input(); virtual void destroy(); @@ -149,6 +169,7 @@ private: bool m_running; QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; + ReplayBuffer m_replayBuffer; bool openDevice(); void closeDevice(); diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3settings.cpp b/plugins/samplesource/sdrplayv3/sdrplayv3settings.cpp index 9df2ac8d8..c14f4667f 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3settings.cpp +++ b/plugins/samplesource/sdrplayv3/sdrplayv3settings.cpp @@ -50,6 +50,10 @@ void SDRPlayV3Settings::resetToDefaults() m_transverterMode = false; m_iqOrder = true; m_transverterDeltaFrequency = 0; + m_replayOffset = 0.0f; + m_replayLength = 20.0f; + m_replayStep = 5.0f; + m_replayLoop = false; m_useReverseAPI = false; m_reverseAPIAddress = "127.0.0.1"; m_reverseAPIPort = 8888; @@ -85,6 +89,10 @@ QByteArray SDRPlayV3Settings::serialize() const s.writeBool(26, m_transverterMode); s.writeS64(27, m_transverterDeltaFrequency); s.writeBool(28, m_iqOrder); + s.writeFloat(29, m_replayOffset); + s.writeFloat(30, m_replayLength); + s.writeFloat(31, m_replayStep); + s.writeBool(32, m_replayLoop); return s.final(); } @@ -138,6 +146,10 @@ bool SDRPlayV3Settings::deserialize(const QByteArray& data) d.readBool(26, &m_transverterMode, false); d.readS64(27, &m_transverterDeltaFrequency, 0); d.readBool(28, &m_iqOrder, true); + d.readFloat(29, &m_replayOffset, 0.0f); + d.readFloat(30, &m_replayLength, 20.0f); + d.readFloat(31, &m_replayStep, 5.0f); + d.readBool(32, &m_replayLoop, false); return true; } @@ -216,6 +228,18 @@ void SDRPlayV3Settings::applySettings(const QStringList& settingsKeys, const SDR if (settingsKeys.contains("m_transverterDeltaFrequency")) { m_transverterDeltaFrequency = settings.m_transverterDeltaFrequency; } + if (settingsKeys.contains("replayOffset")) { + m_replayOffset = settings.m_replayOffset; + } + if (settingsKeys.contains("replayLength")) { + m_replayLength = settings.m_replayLength; + } + if (settingsKeys.contains("replayStep")) { + m_replayStep = settings.m_replayStep; + } + if (settingsKeys.contains("replayLoop")) { + m_replayLoop = settings.m_replayLoop; + } if (settingsKeys.contains("useReverseAPI")) { m_useReverseAPI = settings.m_useReverseAPI; } @@ -300,6 +324,18 @@ QString SDRPlayV3Settings::getDebugString(const QStringList& settingsKeys, bool if (settingsKeys.contains("transverterDeltaFrequency") || force) { ostr << " m_transverterDeltaFrequency: " << m_transverterDeltaFrequency; } + if (settingsKeys.contains("replayOffset") || force) { + ostr << " m_replayOffset: " << m_replayOffset; + } + if (settingsKeys.contains("replayLength") || force) { + ostr << " m_replayLength: " << m_replayLength; + } + if (settingsKeys.contains("replayStep") || force) { + ostr << " m_replayStep: " << m_replayStep; + } + if (settingsKeys.contains("replayLoop") || force) { + ostr << " m_replayLoop: " << m_replayLoop; + } if (settingsKeys.contains("useReverseAPI") || force) { ostr << " m_useReverseAPI: " << m_useReverseAPI; } diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3settings.h b/plugins/samplesource/sdrplayv3/sdrplayv3settings.h index 76cadb48c..7cb283e59 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3settings.h +++ b/plugins/samplesource/sdrplayv3/sdrplayv3settings.h @@ -53,6 +53,10 @@ struct SDRPlayV3Settings { bool m_transverterMode; bool m_iqOrder; qint64 m_transverterDeltaFrequency; + float m_replayOffset; //!< Replay offset in seconds + float m_replayLength; //!< Replay buffer size in seconds + float m_replayStep; //!< Replay forward/back step size in seconds + bool m_replayLoop; //!< Replay buffer repeatedly without recording new data bool m_useReverseAPI; QString m_reverseAPIAddress; uint16_t m_reverseAPIPort; diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3thread.cpp b/plugins/samplesource/sdrplayv3/sdrplayv3thread.cpp index 09867f15b..212e8e4c2 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3thread.cpp +++ b/plugins/samplesource/sdrplayv3/sdrplayv3thread.cpp @@ -27,13 +27,14 @@ #include -SDRPlayV3Thread::SDRPlayV3Thread(sdrplay_api_DeviceT* dev, SampleSinkFifo* sampleFifo, QObject* parent) : +SDRPlayV3Thread::SDRPlayV3Thread(sdrplay_api_DeviceT* dev, SampleSinkFifo* sampleFifo, ReplayBuffer *replayBuffer, QObject* parent) : QThread(parent), m_running(false), m_dev(dev), m_convertBuffer(SDRPLAYV3_INIT_NBSAMPLES), m_sampleFifo(sampleFifo), m_samplerate(2000000), + m_replayBuffer(replayBuffer), m_log2Decim(0), m_fcPos(0), m_iqOrder(true), @@ -171,188 +172,234 @@ void SDRPlayV3Thread::callbackHelper(short *xi, short *xq, sdrplay_api_StreamCbP } } -void SDRPlayV3Thread::callbackIQ(const qint16* buf, qint32 len) +void SDRPlayV3Thread::callbackIQ(const qint16* inBuf, qint32 len) { SampleVector::iterator it = m_convertBuffer.begin(); - if (m_log2Decim == 0) - { - m_decimatorsIQ.decimate1(&it, buf, len); - } - else - { - if (m_fcPos == 0) // Infradyne + // Save data to replay buffer + m_replayBuffer->lock(); + bool replayEnabled = m_replayBuffer->getSize() > 0; + if (replayEnabled) { + m_replayBuffer->write(inBuf, len); + } + + const qint16* buf = inBuf; + qint32 remaining = len; + + while (remaining > 0) + { + // Choose between live data or replayed data + if (replayEnabled && m_replayBuffer->useReplay()) { + len = m_replayBuffer->read(remaining, buf); + } else { + len = remaining; + } + remaining -= len; + + if (m_log2Decim == 0) { - switch (m_log2Decim) - { - case 1: - m_decimatorsIQ.decimate2_inf(&it, buf, len); - break; - case 2: - m_decimatorsIQ.decimate4_inf(&it, buf, len); - break; - case 3: - m_decimatorsIQ.decimate8_inf(&it, buf, len); - break; - case 4: - m_decimatorsIQ.decimate16_inf(&it, buf, len); - break; - case 5: - m_decimatorsIQ.decimate32_inf(&it, buf, len); - break; - case 6: - m_decimatorsIQ.decimate64_inf(&it, buf, len); - break; - default: - break; - } + m_decimatorsIQ.decimate1(&it, buf, len); } - else if (m_fcPos == 1) // Supradyne + else { - switch (m_log2Decim) + if (m_fcPos == 0) // Infradyne { - case 1: - m_decimatorsIQ.decimate2_sup(&it, buf, len); - break; - case 2: - m_decimatorsIQ.decimate4_sup(&it, buf, len); - break; - case 3: - m_decimatorsIQ.decimate8_sup(&it, buf, len); - break; - case 4: - m_decimatorsIQ.decimate16_sup(&it, buf, len); - break; - case 5: - m_decimatorsIQ.decimate32_sup(&it, buf, len); - break; - case 6: - m_decimatorsIQ.decimate64_sup(&it, buf, len); - break; - default: - break; + switch (m_log2Decim) + { + case 1: + m_decimatorsIQ.decimate2_inf(&it, buf, len); + break; + case 2: + m_decimatorsIQ.decimate4_inf(&it, buf, len); + break; + case 3: + m_decimatorsIQ.decimate8_inf(&it, buf, len); + break; + case 4: + m_decimatorsIQ.decimate16_inf(&it, buf, len); + break; + case 5: + m_decimatorsIQ.decimate32_inf(&it, buf, len); + break; + case 6: + m_decimatorsIQ.decimate64_inf(&it, buf, len); + break; + default: + break; + } } - } - else // Centered - { - switch (m_log2Decim) + else if (m_fcPos == 1) // Supradyne { - case 1: - m_decimatorsIQ.decimate2_cen(&it, buf, len); - break; - case 2: - m_decimatorsIQ.decimate4_cen(&it, buf, len); - break; - case 3: - m_decimatorsIQ.decimate8_cen(&it, buf, len); - break; - case 4: - m_decimatorsIQ.decimate16_cen(&it, buf, len); - break; - case 5: - m_decimatorsIQ.decimate32_cen(&it, buf, len); - break; - case 6: - m_decimatorsIQ.decimate64_cen(&it, buf, len); - break; - default: - break; + switch (m_log2Decim) + { + case 1: + m_decimatorsIQ.decimate2_sup(&it, buf, len); + break; + case 2: + m_decimatorsIQ.decimate4_sup(&it, buf, len); + break; + case 3: + m_decimatorsIQ.decimate8_sup(&it, buf, len); + break; + case 4: + m_decimatorsIQ.decimate16_sup(&it, buf, len); + break; + case 5: + m_decimatorsIQ.decimate32_sup(&it, buf, len); + break; + case 6: + m_decimatorsIQ.decimate64_sup(&it, buf, len); + break; + default: + break; + } + } + else // Centered + { + switch (m_log2Decim) + { + case 1: + m_decimatorsIQ.decimate2_cen(&it, buf, len); + break; + case 2: + m_decimatorsIQ.decimate4_cen(&it, buf, len); + break; + case 3: + m_decimatorsIQ.decimate8_cen(&it, buf, len); + break; + case 4: + m_decimatorsIQ.decimate16_cen(&it, buf, len); + break; + case 5: + m_decimatorsIQ.decimate32_cen(&it, buf, len); + break; + case 6: + m_decimatorsIQ.decimate64_cen(&it, buf, len); + break; + default: + break; + } } } } + m_replayBuffer->unlock(); + m_sampleFifo->write(m_convertBuffer.begin(), it); } -void SDRPlayV3Thread::callbackQI(const qint16* buf, qint32 len) +void SDRPlayV3Thread::callbackQI(const qint16* inBuf, qint32 len) { SampleVector::iterator it = m_convertBuffer.begin(); - if (m_log2Decim == 0) - { - m_decimatorsQI.decimate1(&it, buf, len); - } - else - { - if (m_fcPos == 0) // Infradyne + // Save data to replay buffer + m_replayBuffer->lock(); + bool replayEnabled = m_replayBuffer->getSize() > 0; + if (replayEnabled) { + m_replayBuffer->write(inBuf, len); + } + + const qint16* buf = inBuf; + qint32 remaining = len; + + while (remaining > 0) + { + // Choose between live data or replayed data + if (replayEnabled && m_replayBuffer->useReplay()) { + len = m_replayBuffer->read(remaining, buf); + } else { + len = remaining; + } + remaining -= len; + + if (m_log2Decim == 0) { - switch (m_log2Decim) - { - case 1: - m_decimatorsQI.decimate2_inf(&it, buf, len); - break; - case 2: - m_decimatorsQI.decimate4_inf(&it, buf, len); - break; - case 3: - m_decimatorsQI.decimate8_inf(&it, buf, len); - break; - case 4: - m_decimatorsQI.decimate16_inf(&it, buf, len); - break; - case 5: - m_decimatorsQI.decimate32_inf(&it, buf, len); - break; - case 6: - m_decimatorsQI.decimate64_inf(&it, buf, len); - break; - default: - break; - } + m_decimatorsQI.decimate1(&it, buf, len); } - else if (m_fcPos == 1) // Supradyne + else { - switch (m_log2Decim) + if (m_fcPos == 0) // Infradyne { - case 1: - m_decimatorsQI.decimate2_sup(&it, buf, len); - break; - case 2: - m_decimatorsQI.decimate4_sup(&it, buf, len); - break; - case 3: - m_decimatorsQI.decimate8_sup(&it, buf, len); - break; - case 4: - m_decimatorsQI.decimate16_sup(&it, buf, len); - break; - case 5: - m_decimatorsQI.decimate32_sup(&it, buf, len); - break; - case 6: - m_decimatorsQI.decimate64_sup(&it, buf, len); - break; - default: - break; + switch (m_log2Decim) + { + case 1: + m_decimatorsQI.decimate2_inf(&it, buf, len); + break; + case 2: + m_decimatorsQI.decimate4_inf(&it, buf, len); + break; + case 3: + m_decimatorsQI.decimate8_inf(&it, buf, len); + break; + case 4: + m_decimatorsQI.decimate16_inf(&it, buf, len); + break; + case 5: + m_decimatorsQI.decimate32_inf(&it, buf, len); + break; + case 6: + m_decimatorsQI.decimate64_inf(&it, buf, len); + break; + default: + break; + } } - } - else // Centered - { - switch (m_log2Decim) + else if (m_fcPos == 1) // Supradyne { - case 1: - m_decimatorsQI.decimate2_cen(&it, buf, len); - break; - case 2: - m_decimatorsQI.decimate4_cen(&it, buf, len); - break; - case 3: - m_decimatorsQI.decimate8_cen(&it, buf, len); - break; - case 4: - m_decimatorsQI.decimate16_cen(&it, buf, len); - break; - case 5: - m_decimatorsQI.decimate32_cen(&it, buf, len); - break; - case 6: - m_decimatorsQI.decimate64_cen(&it, buf, len); - break; - default: - break; + switch (m_log2Decim) + { + case 1: + m_decimatorsQI.decimate2_sup(&it, buf, len); + break; + case 2: + m_decimatorsQI.decimate4_sup(&it, buf, len); + break; + case 3: + m_decimatorsQI.decimate8_sup(&it, buf, len); + break; + case 4: + m_decimatorsQI.decimate16_sup(&it, buf, len); + break; + case 5: + m_decimatorsQI.decimate32_sup(&it, buf, len); + break; + case 6: + m_decimatorsQI.decimate64_sup(&it, buf, len); + break; + default: + break; + } + } + else // Centered + { + switch (m_log2Decim) + { + case 1: + m_decimatorsQI.decimate2_cen(&it, buf, len); + break; + case 2: + m_decimatorsQI.decimate4_cen(&it, buf, len); + break; + case 3: + m_decimatorsQI.decimate8_cen(&it, buf, len); + break; + case 4: + m_decimatorsQI.decimate16_cen(&it, buf, len); + break; + case 5: + m_decimatorsQI.decimate32_cen(&it, buf, len); + break; + case 6: + m_decimatorsQI.decimate64_cen(&it, buf, len); + break; + default: + break; + } } } } + m_replayBuffer->unlock(); + m_sampleFifo->write(m_convertBuffer.begin(), it); } diff --git a/plugins/samplesource/sdrplayv3/sdrplayv3thread.h b/plugins/samplesource/sdrplayv3/sdrplayv3thread.h index 4dc7af7f9..8952446c6 100644 --- a/plugins/samplesource/sdrplayv3/sdrplayv3thread.h +++ b/plugins/samplesource/sdrplayv3/sdrplayv3thread.h @@ -29,6 +29,7 @@ #include #include "dsp/samplesinkfifo.h" #include "dsp/decimators.h" +#include "dsp/replaybuffer.h" #define SDRPLAYV3_INIT_NBSAMPLES (1<<14) @@ -36,7 +37,7 @@ class SDRPlayV3Thread : public QThread { Q_OBJECT public: - SDRPlayV3Thread(sdrplay_api_DeviceT* dev, SampleSinkFifo* sampleFifo, QObject* parent = NULL); + SDRPlayV3Thread(sdrplay_api_DeviceT* dev, SampleSinkFifo* sampleFifo, ReplayBuffer *replayBuffer, QObject* parent = NULL); ~SDRPlayV3Thread(); void startWork(); @@ -58,6 +59,7 @@ private: sdrplay_api_DeviceT *m_dev; SampleVector m_convertBuffer; SampleSinkFifo* m_sampleFifo; + ReplayBuffer *m_replayBuffer; int m_samplerate; unsigned int m_log2Decim; diff --git a/plugins/samplesource/usrpinput/usrpinput.cpp b/plugins/samplesource/usrpinput/usrpinput.cpp index c807e6605..b35cfb0f2 100644 --- a/plugins/samplesource/usrpinput/usrpinput.cpp +++ b/plugins/samplesource/usrpinput/usrpinput.cpp @@ -47,6 +47,7 @@ MESSAGE_CLASS_DEFINITION(USRPInput::MsgGetStreamInfo, Message) MESSAGE_CLASS_DEFINITION(USRPInput::MsgGetDeviceInfo, Message) MESSAGE_CLASS_DEFINITION(USRPInput::MsgReportStreamInfo, Message) MESSAGE_CLASS_DEFINITION(USRPInput::MsgStartStop, Message) +MESSAGE_CLASS_DEFINITION(USRPInput::MsgSaveReplay, Message) USRPInput::USRPInput(DeviceAPI *deviceAPI) : m_deviceAPI(deviceAPI), @@ -427,7 +428,7 @@ bool USRPInput::start() // start / stop streaming is done in the thread. - m_usrpInputThread = new USRPInputThread(m_streamId, m_bufSamples, &m_sampleFifo); + m_usrpInputThread = new USRPInputThread(m_streamId, m_bufSamples, &m_sampleFifo, &m_replayBuffer); qDebug("USRPInput::start: thread created"); m_usrpInputThread->setLog2Decimation(m_settings.m_log2SoftDecim); @@ -705,6 +706,12 @@ bool USRPInput::handleMessage(const Message& message) return true; } + else if (MsgSaveReplay::match(message)) + { + MsgSaveReplay& cmd = (MsgSaveReplay&) message; + m_replayBuffer.save(cmd.getFilename(), m_settings.m_devSampleRate, getCenterFrequency()); + return true; + } else { return false; @@ -768,6 +775,10 @@ bool USRPInput::applySettings(const USRPInputSettings& settings, const QList #include "dsp/devicesamplesource.h" +#include "dsp/replaybuffer.h" #include "usrp/deviceusrpshared.h" #include "usrpinputsettings.h" @@ -161,6 +162,25 @@ public: { } }; + class MsgSaveReplay : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getFilename() const { return m_filename; } + + static MsgSaveReplay* create(const QString& filename) { + return new MsgSaveReplay(filename); + } + + protected: + QString m_filename; + + MsgSaveReplay(const QString& filename) : + Message(), + m_filename(filename) + { } + }; + USRPInput(DeviceAPI *deviceAPI); virtual ~USRPInput(); virtual void destroy(); @@ -235,6 +255,7 @@ private: size_t m_bufSamples; QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; + ReplayBuffer m_replayBuffer; bool openDevice(); void closeDevice(); diff --git a/plugins/samplesource/usrpinput/usrpinputgui.cpp b/plugins/samplesource/usrpinput/usrpinputgui.cpp index 11b9b67a2..523dff5a5 100644 --- a/plugins/samplesource/usrpinput/usrpinputgui.cpp +++ b/plugins/samplesource/usrpinput/usrpinputgui.cpp @@ -407,6 +407,10 @@ void USRPInputGUI::displaySettings() } else { ui->gain->setEnabled(true); } + displayReplayLength(); + displayReplayOffset(); + displayReplayStep(); + ui->replayLoop->setChecked(m_settings.m_replayLoop); } void USRPInputGUI::setCenterFrequencyDisplay() @@ -646,6 +650,9 @@ void USRPInputGUI::openDeviceSettingsDialog(const QPoint& p) if (m_contextMenuType == ContextMenuDeviceSettings) { BasicDeviceSettingsDialog dialog(this); + dialog.setReplayBytesPerSecond(m_settings.m_devSampleRate * 2 * sizeof(qint16)); + dialog.setReplayLength(m_settings.m_replayLength); + dialog.setReplayStep(m_settings.m_replayStep); dialog.setUseReverseAPI(m_settings.m_useReverseAPI); dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); @@ -659,10 +666,17 @@ void USRPInputGUI::openDeviceSettingsDialog(const QPoint& p) m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_replayLength = dialog.getReplayLength(); + m_settings.m_replayStep = dialog.getReplayStep(); + displayReplayLength(); + displayReplayOffset(); + displayReplayStep(); m_settingsKeys.append("useReverseAPI"); m_settingsKeys.append("reverseAPIAddress"); m_settingsKeys.append("reverseAPIPort"); m_settingsKeys.append("reverseAPIDeviceIndex"); + m_settingsKeys.append("replayLength"); + m_settingsKeys.append("replayStep"); sendSettings(); } @@ -670,6 +684,91 @@ void USRPInputGUI::openDeviceSettingsDialog(const QPoint& p) resetContextMenuType(); } +void USRPInputGUI::displayReplayLength() +{ + bool replayEnabled = m_settings.m_replayLength > 0.0f; + if (!replayEnabled) { + ui->replayOffset->setMaximum(0); + } else { + ui->replayOffset->setMaximum(m_settings.m_replayLength * 10 - 1); + } + ui->replayLabel->setEnabled(replayEnabled); + ui->replayOffset->setEnabled(replayEnabled); + ui->replayOffsetText->setEnabled(replayEnabled); + ui->replaySave->setEnabled(replayEnabled); +} + +void USRPInputGUI::displayReplayOffset() +{ + bool replayEnabled = m_settings.m_replayLength > 0.0f; + ui->replayOffset->setValue(m_settings.m_replayOffset * 10); + ui->replayOffsetText->setText(QString("%1s").arg(m_settings.m_replayOffset, 0, 'f', 1)); + ui->replayNow->setEnabled(replayEnabled && (m_settings.m_replayOffset > 0.0f)); + ui->replayPlus->setEnabled(replayEnabled && (std::round(m_settings.m_replayOffset * 10) < ui->replayOffset->maximum())); + ui->replayMinus->setEnabled(replayEnabled && (m_settings.m_replayOffset > 0.0f)); +} + +void USRPInputGUI::displayReplayStep() +{ + QString step; + float intpart; + float frac = modf(m_settings.m_replayStep, &intpart); + if (frac == 0.0f) { + step = QString::number((int)intpart); + } else { + step = QString::number(m_settings.m_replayStep, 'f', 1); + } + ui->replayPlus->setText(QString("+%1s").arg(step)); + ui->replayPlus->setToolTip(QString("Add %1 seconds to time delay").arg(step)); + ui->replayMinus->setText(QString("-%1s").arg(step)); + ui->replayMinus->setToolTip(QString("Remove %1 seconds from time delay").arg(step)); +} + +void USRPInputGUI::on_replayOffset_valueChanged(int value) +{ + m_settings.m_replayOffset = value / 10.0f; + displayReplayOffset(); + m_settingsKeys.append("replayOffset"); + sendSettings(); +} + +void USRPInputGUI::on_replayNow_clicked() +{ + ui->replayOffset->setValue(0); +} + +void USRPInputGUI::on_replayPlus_clicked() +{ + ui->replayOffset->setValue(ui->replayOffset->value() + m_settings.m_replayStep * 10); +} + +void USRPInputGUI::on_replayMinus_clicked() +{ + ui->replayOffset->setValue(ui->replayOffset->value() - m_settings.m_replayStep * 10); +} + +void USRPInputGUI::on_replaySave_clicked() +{ + QFileDialog fileDialog(nullptr, "Select file to save IQ data to", "", "*.wav"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + USRPInput::MsgSaveReplay *message = USRPInput::MsgSaveReplay::create(fileNames[0]); + m_usrpInput->getInputMessageQueue()->push(message); + } + } +} + +void USRPInputGUI::on_replayLoop_toggled(bool checked) +{ + m_settings.m_replayLoop = checked; + m_settingsKeys.append("replayLoop"); + sendSettings(); +} + void USRPInputGUI::makeUIConnections() { QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &USRPInputGUI::on_startStop_toggled); @@ -686,4 +785,10 @@ void USRPInputGUI::makeUIConnections() QObject::connect(ui->clockSource, QOverload::of(&QComboBox::currentIndexChanged), this, &USRPInputGUI::on_clockSource_currentIndexChanged); QObject::connect(ui->transverter, &TransverterButton::clicked, this, &USRPInputGUI::on_transverter_clicked); QObject::connect(ui->sampleRateMode, &QToolButton::toggled, this, &USRPInputGUI::on_sampleRateMode_toggled); + QObject::connect(ui->replayOffset, &QSlider::valueChanged, this, &USRPInputGUI::on_replayOffset_valueChanged); + QObject::connect(ui->replayNow, &QToolButton::clicked, this, &USRPInputGUI::on_replayNow_clicked); + QObject::connect(ui->replayPlus, &QToolButton::clicked, this, &USRPInputGUI::on_replayPlus_clicked); + QObject::connect(ui->replayMinus, &QToolButton::clicked, this, &USRPInputGUI::on_replayMinus_clicked); + QObject::connect(ui->replaySave, &QToolButton::clicked, this, &USRPInputGUI::on_replaySave_clicked); + QObject::connect(ui->replayLoop, &ButtonSwitch::toggled, this, &USRPInputGUI::on_replayLoop_toggled); } diff --git a/plugins/samplesource/usrpinput/usrpinputgui.h b/plugins/samplesource/usrpinput/usrpinputgui.h index d17e44c55..9a0f3dfcc 100644 --- a/plugins/samplesource/usrpinput/usrpinputgui.h +++ b/plugins/samplesource/usrpinput/usrpinputgui.h @@ -71,6 +71,9 @@ private: void displaySettings(); void displaySampleRate(); + void displayReplayLength(); + void displayReplayOffset(); + void displayReplayStep(); void setCenterFrequencyDisplay(); void setCenterFrequencySetting(uint64_t kHzValue); void sendSettings(); @@ -97,6 +100,12 @@ private slots: void on_clockSource_currentIndexChanged(int index); void on_transverter_clicked(); void on_sampleRateMode_toggled(bool checked); + void on_replayOffset_valueChanged(int value); + void on_replayNow_clicked(); + void on_replayPlus_clicked(); + void on_replayMinus_clicked(); + void on_replaySave_clicked(); + void on_replayLoop_toggled(bool checked); void openDeviceSettingsDialog(const QPoint& p); void updateHardware(); diff --git a/plugins/samplesource/usrpinput/usrpinputgui.ui b/plugins/samplesource/usrpinput/usrpinputgui.ui index 3ed387d02..b20bc042b 100644 --- a/plugins/samplesource/usrpinput/usrpinputgui.ui +++ b/plugins/samplesource/usrpinput/usrpinputgui.ui @@ -7,7 +7,7 @@ 0 0 360 - 192 + 230
@@ -19,13 +19,13 @@ 360 - 192 + 230 380 - 192 + 230 @@ -767,6 +767,111 @@ + + + + Qt::Horizontal + + + + + + + + + + 65 + 0 + + + + Time Delay + + + + + + + Replay time delay in seconds + + + 500 + + + Qt::Horizontal + + + + + + + Replay time delay in seconds + + + 0.0s + + + + + + + Set time delay to 0 seconds + + + Now + + + + + + + Add displayed number of seconds to time delay + + + +5s + + + + + + + Remove displayed number of seconds from time delay + + + -5s + + + + + + + Repeatedly replay data in replay buffer + + + + + + + :/playloop.png:/playloop.png + + + + + + + Save replay buffer to a file + + + + + + + :/save.png:/save.png + + + + + @@ -781,17 +886,17 @@ QToolButton
gui/buttonswitch.h
+ + TransverterButton + QPushButton +
gui/transverterbutton.h
+
ValueDialZ QWidget
gui/valuedialz.h
1
- - TransverterButton - QPushButton -
gui/transverterbutton.h
-
diff --git a/plugins/samplesource/usrpinput/usrpinputsettings.cpp b/plugins/samplesource/usrpinput/usrpinputsettings.cpp index 848c7e847..e1b1381a2 100644 --- a/plugins/samplesource/usrpinput/usrpinputsettings.cpp +++ b/plugins/samplesource/usrpinput/usrpinputsettings.cpp @@ -42,6 +42,10 @@ void USRPInputSettings::resetToDefaults() m_clockSource = "internal"; m_transverterMode = false; m_transverterDeltaFrequency = 0; + m_replayOffset = 0.0f; + m_replayLength = 20.0f; + m_replayStep = 5.0f; + m_replayLoop = false; m_useReverseAPI = false; m_reverseAPIAddress = "127.0.0.1"; m_reverseAPIPort = 8888; @@ -68,6 +72,10 @@ QByteArray USRPInputSettings::serialize() const s.writeU32(14, m_reverseAPIPort); s.writeU32(15, m_reverseAPIDeviceIndex); s.writeS32(16, m_loOffset); + s.writeFloat(17, m_replayOffset); + s.writeFloat(18, m_replayLength); + s.writeFloat(19, m_replayStep); + s.writeBool(20, m_replayLoop); return s.final(); } @@ -112,6 +120,10 @@ bool USRPInputSettings::deserialize(const QByteArray& data) d.readU32(15, &uintval, 0); m_reverseAPIDeviceIndex = uintval > 99 ? 99 : uintval; d.readS32(16, &m_loOffset, 0); + d.readFloat(17, &m_replayOffset, 0.0f); + d.readFloat(18, &m_replayLength, 20.0f); + d.readFloat(19, &m_replayStep, 5.0f); + d.readBool(20, &m_replayLoop, false); return true; } @@ -167,6 +179,18 @@ void USRPInputSettings::applySettings(const QStringList& settingsKeys, const USR if (settingsKeys.contains("transverterDeltaFrequency")) { m_transverterDeltaFrequency = settings.m_transverterDeltaFrequency; } + if (settingsKeys.contains("replayOffset")) { + m_replayOffset = settings.m_replayOffset; + } + if (settingsKeys.contains("replayLength")) { + m_replayLength = settings.m_replayLength; + } + if (settingsKeys.contains("replayStep")) { + m_replayStep = settings.m_replayStep; + } + if (settingsKeys.contains("replayLoop")) { + m_replayLoop = settings.m_replayLoop; + } if (settingsKeys.contains("useReverseAPI")) { m_useReverseAPI = settings.m_useReverseAPI; } @@ -227,6 +251,18 @@ QString USRPInputSettings::getDebugString(const QStringList& settingsKeys, bool if (settingsKeys.contains("transverterDeltaFrequency") || force) { ostr << " m_transverterDeltaFrequency: " << m_transverterDeltaFrequency; } + if (settingsKeys.contains("replayOffset") || force) { + ostr << " m_replayOffset: " << m_replayOffset; + } + if (settingsKeys.contains("replayLength") || force) { + ostr << " m_replayLength: " << m_replayLength; + } + if (settingsKeys.contains("replayStep") || force) { + ostr << " m_replayStep: " << m_replayStep; + } + if (settingsKeys.contains("replayLoop") || force) { + ostr << " m_replayLoop: " << m_replayLoop; + } if (settingsKeys.contains("useReverseAPI") || force) { ostr << " m_useReverseAPI: " << m_useReverseAPI; } diff --git a/plugins/samplesource/usrpinput/usrpinputsettings.h b/plugins/samplesource/usrpinput/usrpinputsettings.h index 322bcc092..6bd90fa36 100644 --- a/plugins/samplesource/usrpinput/usrpinputsettings.h +++ b/plugins/samplesource/usrpinput/usrpinputsettings.h @@ -52,6 +52,10 @@ struct USRPInputSettings QString m_clockSource; bool m_transverterMode; qint64 m_transverterDeltaFrequency; + float m_replayOffset; //!< Replay offset in seconds + float m_replayLength; //!< Replay buffer size in seconds + float m_replayStep; //!< Replay forward/back step size in seconds + bool m_replayLoop; //!< Replay buffer repeatedly without recording new data bool m_useReverseAPI; QString m_reverseAPIAddress; uint16_t m_reverseAPIPort; diff --git a/plugins/samplesource/usrpinput/usrpinputthread.cpp b/plugins/samplesource/usrpinput/usrpinputthread.cpp index 5dcd9dea8..5f2ffd5c8 100644 --- a/plugins/samplesource/usrpinput/usrpinputthread.cpp +++ b/plugins/samplesource/usrpinput/usrpinputthread.cpp @@ -26,13 +26,15 @@ #include "usrpinputsettings.h" #include "usrpinputthread.h" -USRPInputThread::USRPInputThread(uhd::rx_streamer::sptr stream, size_t bufSamples, SampleSinkFifo* sampleFifo, QObject* parent) : +USRPInputThread::USRPInputThread(uhd::rx_streamer::sptr stream, size_t bufSamples, + SampleSinkFifo* sampleFifo, ReplayBuffer *replayBuffer, QObject* parent) : QThread(parent), m_running(false), m_stream(stream), m_bufSamples(bufSamples), m_convertBuffer(bufSamples), m_sampleFifo(sampleFifo), + m_replayBuffer(replayBuffer), m_log2Decim(0) { // *2 as samples are I+Q @@ -53,8 +55,15 @@ void USRPInputThread::issueStreamCmd(bool start) stream_cmd.stream_now = true; stream_cmd.time_spec = uhd::time_spec_t(); - m_stream->issue_stream_cmd(stream_cmd); - qDebug() << "USRPInputThread::issueStreamCmd " << (start ? "start" : "stop"); + if (m_stream) + { + m_stream->issue_stream_cmd(stream_cmd); + qDebug() << "USRPInputThread::issueStreamCmd " << (start ? "start" : "stop"); + } + else + { + qDebug() << "USRPInputThread::issueStreamCmd m_stream is null"; + } } void USRPInputThread::startWork() @@ -184,37 +193,60 @@ void USRPInputThread::run() } // Decimate according to specified log2 (ex: log2=4 => decim=16) -void USRPInputThread::callbackIQ(const qint16* buf, qint32 len) +void USRPInputThread::callbackIQ(const qint16* inBuf, qint32 len) { SampleVector::iterator it = m_convertBuffer.begin(); - switch (m_log2Decim) - { - case 0: - m_decimatorsIQ.decimate1(&it, buf, len); - break; - case 1: - m_decimatorsIQ.decimate2_cen(&it, buf, len); - break; - case 2: - m_decimatorsIQ.decimate4_cen(&it, buf, len); - break; - case 3: - m_decimatorsIQ.decimate8_cen(&it, buf, len); - break; - case 4: - m_decimatorsIQ.decimate16_cen(&it, buf, len); - break; - case 5: - m_decimatorsIQ.decimate32_cen(&it, buf, len); - break; - case 6: - m_decimatorsIQ.decimate64_cen(&it, buf, len); - break; - default: - break; + // Save data to replay buffer + m_replayBuffer->lock(); + bool replayEnabled = m_replayBuffer->getSize() > 0; + if (replayEnabled) { + m_replayBuffer->write(inBuf, len); + } + + const qint16* buf = inBuf; + qint32 remaining = len; + + while (remaining > 0) + { + // Choose between live data or replayed data + if (replayEnabled && m_replayBuffer->useReplay()) { + len = m_replayBuffer->read(remaining, buf); + } else { + len = remaining; + } + remaining -= len; + + switch (m_log2Decim) + { + case 0: + m_decimatorsIQ.decimate1(&it, buf, len); + break; + case 1: + m_decimatorsIQ.decimate2_cen(&it, buf, len); + break; + case 2: + m_decimatorsIQ.decimate4_cen(&it, buf, len); + break; + case 3: + m_decimatorsIQ.decimate8_cen(&it, buf, len); + break; + case 4: + m_decimatorsIQ.decimate16_cen(&it, buf, len); + break; + case 5: + m_decimatorsIQ.decimate32_cen(&it, buf, len); + break; + case 6: + m_decimatorsIQ.decimate64_cen(&it, buf, len); + break; + default: + break; + } } + m_replayBuffer->unlock(); + m_sampleFifo->write(m_convertBuffer.begin(), it); } diff --git a/plugins/samplesource/usrpinput/usrpinputthread.h b/plugins/samplesource/usrpinput/usrpinputthread.h index 58b4f99a2..95e2c1380 100644 --- a/plugins/samplesource/usrpinput/usrpinputthread.h +++ b/plugins/samplesource/usrpinput/usrpinputthread.h @@ -31,6 +31,7 @@ #include "dsp/samplesinkfifo.h" #include "dsp/decimators.h" +#include "dsp/replaybuffer.h" #include "usrp/deviceusrpshared.h" #include "usrp/deviceusrp.h" @@ -39,7 +40,8 @@ class USRPInputThread : public QThread, public DeviceUSRPShared::ThreadInterface Q_OBJECT public: - USRPInputThread(uhd::rx_streamer::sptr stream, size_t bufSamples, SampleSinkFifo* sampleFifo, QObject* parent = 0); + USRPInputThread(uhd::rx_streamer::sptr stream, size_t bufSamples, SampleSinkFifo* sampleFifo, + ReplayBuffer *replayBuffer, QObject* parent = 0); ~USRPInputThread(); virtual void startWork(); @@ -64,6 +66,7 @@ private: size_t m_bufSamples; SampleVector m_convertBuffer; SampleSinkFifo* m_sampleFifo; + ReplayBuffer *m_replayBuffer; unsigned int m_log2Decim; // soft decimation diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 348dd6e99..923dd1da3 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -399,6 +399,7 @@ set(sdrbase_HEADERS dsp/projector.h dsp/raisedcosine.h dsp/recursivefilters.h + dsp/replaybuffer.h dsp/rootraisedcosine.h dsp/samplemififo.h dsp/samplemofifo.h diff --git a/sdrbase/dsp/replaybuffer.h b/sdrbase/dsp/replaybuffer.h new file mode 100644 index 000000000..94ecd71bf --- /dev/null +++ b/sdrbase/dsp/replaybuffer.h @@ -0,0 +1,218 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_REPLAYBUFFER_H +#define INCLUDE_REPLAYBUFFER_H + +#include +#include +#include + +#include + +#include "dsp/wavfilerecord.h" + +// Circular buffer for storing and replaying IQ samples +// lock/unlock should be called manually before write/read/getSize +// (so it only needs to be locked once for write then multiple reads). +template +class ReplayBuffer { + +public: + ReplayBuffer() : + m_data(1000000*2, 0), + m_write(0), + m_read(0), + m_readOffset(0), + m_count(0) + { + } + + bool useReplay() const + { + return (m_readOffset > 0) || m_loop; + } + + void setSize(float lengthInSeconds, int sampleRate) + { + QMutexLocker locker(&m_mutex); + unsigned int newSize = lengthInSeconds * sampleRate * 2; + unsigned int oldSize = m_data.size(); + + if (newSize == oldSize) { + return; + } + + // Save most recent data + if (m_write >= newSize) + { + memmove(&m_data[0], &m_data[m_write-newSize], newSize); + m_write = 0; + m_count = newSize; + m_data.resize(newSize); + } + else if (newSize < oldSize) + { + memmove(&m_data[m_write], &m_data[oldSize-(newSize-m_write)], newSize-m_write); + m_count = std::min(m_count, newSize); + m_data.resize(newSize); + } + else + { + m_data.resize(newSize); + memmove(&m_data[newSize-(oldSize-m_write)], &m_data[m_write], oldSize-m_write); + } + } + + // lock()/unlock() should be called before/after calling this function + int getSize() const { + return m_data.size(); + } + + void setLoop(bool loop) { + m_loop = loop; + } + + bool getLoop() const { + return m_loop; + } + + // Copy count samples into circular buffer (1 I and Q pair should have count = 2) + // When loop is set, samples aren't copied, but write pointer is still updated + // lock()/unlock() should be called before/after calling this function + void write(const T* data, unsigned int count) + { + unsigned int totalLen = count; + while (totalLen > 0) + { + unsigned int len = std::min((unsigned int)m_data.size() - m_write, totalLen); + if (!m_loop) { + memcpy(&m_data[m_write], data, len * sizeof(T)); + } + m_write += len; + if (m_write >= m_data.size()) { + m_write = 0; + } + m_count += len; + if (m_count > m_data.size()) { + m_count = m_data.size(); + } + data += len; + totalLen -= len; + } + } + + // Get pointer to count samples - actual number available is returned + // lock()/unlock() should be called before/after calling this function + unsigned int read(unsigned int count, const T*& ptr) + { + unsigned int totalLen = count; + unsigned int len = std::min((unsigned int)m_data.size() - m_read, totalLen); + ptr = &m_data[m_read]; + m_read += len; + if (m_read >= m_data.size()) { + m_read = 0; + } + return len; + } + + void setReadOffset(unsigned int offset) + { + QMutexLocker locker(&m_mutex); + m_readOffset = offset; + offset = std::min(offset, (unsigned int)(m_data.size() - 1)); + int read = m_write - offset; + while (read < 0) { + read += m_data.size(); + } + m_read = (unsigned int) read; + } + + unsigned int getReadOffset() + { + return m_readOffset; + } + + // Save buffer to .wav file + void save(const QString& filename, quint32 sampleRate, quint64 centerFrequency) + { + QMutexLocker locker(&m_mutex); + + WavFileRecord wavFile(sampleRate, centerFrequency); + QString baseName = filename; + QFileInfo fileInfo(baseName); + QString suffix = fileInfo.suffix(); + if (!suffix.isEmpty()) { + baseName.chop(suffix.length() + 1); + } + wavFile.setFileName(baseName); + + wavFile.startRecording(); + int offset = m_write + m_data.size() - m_count; + for (int i = 0; i < m_count; i += 2) + { + int idx = (i + offset) % m_data.size(); + qint16 l = conv(m_data[idx]); + qint16 r = conv(m_data[idx+1]); + wavFile.write(l, r); + } + wavFile.stopRecording(); + } + + void clear() + { + QMutexLocker locker(&m_mutex); + std::fill(m_data.begin(), m_data.end(), 0); + m_count = 0; + } + + void lock() + { + m_mutex.lock(); + } + + void unlock() + { + m_mutex.unlock(); + } + +private: + std::vector m_data; + unsigned int m_write; // Write index + unsigned int m_read; // Read index + unsigned int m_readOffset; + unsigned int m_count; // Count of number of valid samples in the buffer + bool m_loop; + QMutex m_mutex; + + qint16 conv(quint8 data) const + { + return (data - 128) << 8; + } + + qint16 conv(qint16 data) const + { + return data; + } + + qint16 conv(float data) const + { + return (qint16)(data * SDR_RX_SCALEF); + } +}; + +#endif // INCLUDE_REPLAYBUFFER_H diff --git a/sdrgui/gui/basicdevicesettingsdialog.cpp b/sdrgui/gui/basicdevicesettingsdialog.cpp index eb1c86c39..1e88cbc0c 100644 --- a/sdrgui/gui/basicdevicesettingsdialog.cpp +++ b/sdrgui/gui/basicdevicesettingsdialog.cpp @@ -15,6 +15,9 @@ // You should have received a copy of the GNU General Public License // // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////////// + +#include + #include "gui/pluginpresetsdialog.h" #include "gui/dialogpositioner.h" #include "device/deviceapi.h" @@ -35,6 +38,8 @@ BasicDeviceSettingsDialog::BasicDeviceSettingsDialog(QWidget *parent) : setReverseAPIAddress("127.0.0.1"); setReverseAPIPort(8888); setReverseAPIDeviceIndex(0); + setReplayBytesPerSecond(0); + setReplayStep(5.0f); } BasicDeviceSettingsDialog::~BasicDeviceSettingsDialog() @@ -42,6 +47,48 @@ BasicDeviceSettingsDialog::~BasicDeviceSettingsDialog() delete ui; } +void BasicDeviceSettingsDialog::setReplayBytesPerSecond(int bytesPerSecond) +{ + bool enabled = bytesPerSecond > 0; + ui->replayLengthLabel->setEnabled(enabled); + ui->replayLength->setEnabled(enabled); + ui->replayLengthUnits->setEnabled(enabled); + ui->replayLengthSize->setEnabled(enabled); + ui->replayStepLabel->setEnabled(enabled); + ui->replayStep->setEnabled(enabled); + ui->replayStepUnits->setEnabled(enabled); + m_replayBytesPerSecond = bytesPerSecond; +} + +void BasicDeviceSettingsDialog::setReplayLength(float replayLength) +{ + m_replayLength = replayLength; + ui->replayLength->setValue(replayLength); +} + +void BasicDeviceSettingsDialog::on_replayLength_valueChanged(double value) +{ + m_replayLength = (float)value; + float size = m_replayLength * m_replayBytesPerSecond; + if (size < 1e6) { + ui->replayLengthSize->setText("(<1MB)"); + } else { + ui->replayLengthSize->setText(QString("(%1MB)").arg(std::ceil(size/1e6))); + } +} + +void BasicDeviceSettingsDialog::setReplayStep(float replayStep) +{ + m_replayStep = replayStep; + ui->replayStep->setValue(replayStep); +} + +void BasicDeviceSettingsDialog::on_replayStep_valueChanged(double value) + +{ + m_replayStep = value; +} + void BasicDeviceSettingsDialog::setUseReverseAPI(bool useReverseAPI) { m_useReverseAPI = useReverseAPI; diff --git a/sdrgui/gui/basicdevicesettingsdialog.h b/sdrgui/gui/basicdevicesettingsdialog.h index bdc55e382..6e485b76e 100644 --- a/sdrgui/gui/basicdevicesettingsdialog.h +++ b/sdrgui/gui/basicdevicesettingsdialog.h @@ -42,6 +42,11 @@ public: void setReverseAPIAddress(const QString& address); void setReverseAPIPort(uint16_t port); void setReverseAPIDeviceIndex(uint16_t deviceIndex); + void setReplayBytesPerSecond(int bytesPerSecond); + void setReplayLength(float replayLength); + float getReplayLength() const { return m_replayLength; } + void setReplayStep(float replayStep); + float getReplayStep() const { return m_replayStep; } private slots: void on_reverseAPI_toggled(bool checked); @@ -49,6 +54,8 @@ private slots: void on_reverseAPIPort_editingFinished(); void on_reverseAPIDeviceIndex_editingFinished(); void on_presets_clicked(); + void on_replayLength_valueChanged(double value); + void on_replayStep_valueChanged(double value); void accept(); private: @@ -58,6 +65,9 @@ private: uint16_t m_reverseAPIPort; uint16_t m_reverseAPIDeviceIndex; bool m_hasChanged; + int m_replayBytesPerSecond; + float m_replayLength; + float m_replayStep; }; #endif // BASICDEVICESETTINGSDIALOG_H diff --git a/sdrgui/gui/basicdevicesettingsdialog.ui b/sdrgui/gui/basicdevicesettingsdialog.ui index 185bf9b0c..7f1ad133b 100644 --- a/sdrgui/gui/basicdevicesettingsdialog.ui +++ b/sdrgui/gui/basicdevicesettingsdialog.ui @@ -7,7 +7,7 @@ 0 0 394 - 77 + 137
@@ -133,6 +133,131 @@ + + + + + + + 120 + 0 + + + + Replay buffer length + + + + + + + + 80 + 0 + + + + Length of replay buffer in seconds + + + 1 + + + 100000.000000000000000 + + + + + + + s + + + + + + + Size of replay buffer in megabytes + + + (100MB) + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 120 + 0 + + + + Replay step + + + + + + + + 80 + 0 + + + + Step time in seconds + + + 1 + + + 0.100000000000000 + + + 1000.000000000000000 + + + + + + + s + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + +