diff --git a/doc/img/DemodAnalyzer_A.png b/doc/img/DemodAnalyzer_A.png index 27fc93b2f..d742132a6 100644 Binary files a/doc/img/DemodAnalyzer_A.png and b/doc/img/DemodAnalyzer_A.png differ diff --git a/doc/img/DemodAnalyzer_A.xcf b/doc/img/DemodAnalyzer_A.xcf index 9a5f5c37a..60cc4c4ba 100644 Binary files a/doc/img/DemodAnalyzer_A.xcf and b/doc/img/DemodAnalyzer_A.xcf differ diff --git a/doc/img/DemodAnalyzer_plugin.png b/doc/img/DemodAnalyzer_plugin.png index 601936190..8e5e6b0fd 100644 Binary files a/doc/img/DemodAnalyzer_plugin.png and b/doc/img/DemodAnalyzer_plugin.png differ diff --git a/doc/img/DemodAnalyzer_plugin.xcf b/doc/img/DemodAnalyzer_plugin.xcf index 6ea329953..549540992 100644 Binary files a/doc/img/DemodAnalyzer_plugin.xcf and b/doc/img/DemodAnalyzer_plugin.xcf differ diff --git a/plugins/feature/demodanalyzer/demodanalyzer.cpp b/plugins/feature/demodanalyzer/demodanalyzer.cpp index df74327b5..9f3439e53 100644 --- a/plugins/feature/demodanalyzer/demodanalyzer.cpp +++ b/plugins/feature/demodanalyzer/demodanalyzer.cpp @@ -220,6 +220,10 @@ bool DemodAnalyzer::handleMessage(const Message& cmd) m_sampleRate = report.getSampleRate(); m_scopeVis.setLiveRate(m_sampleRate); + if (m_running) { + m_worker->applySampleRate(m_sampleRate); + } + DSPSignalNotification *msg = new DSPSignalNotification(0, m_sampleRate); m_spectrumVis.getInputMessageQueue()->push(msg); diff --git a/plugins/feature/demodanalyzer/demodanalyzergui.cpp b/plugins/feature/demodanalyzer/demodanalyzergui.cpp index 440685cd6..697821ebf 100644 --- a/plugins/feature/demodanalyzer/demodanalyzergui.cpp +++ b/plugins/feature/demodanalyzer/demodanalyzergui.cpp @@ -16,6 +16,7 @@ /////////////////////////////////////////////////////////////////////////////////// #include +#include #include "feature/featureuiset.h" #include "dsp/spectrumvis.h" @@ -206,6 +207,11 @@ void DemodAnalyzerGUI::displaySettings() setTitle(m_settings.m_title); blockApplySettings(true); ui->log2Decim->setCurrentIndex(m_settings.m_log2Decim); + ui->record->setChecked(m_settings.m_recordToFile); + ui->fileNameText->setText(m_settings.m_fileRecordName); + ui->showFileDialog->setEnabled(!m_settings.m_recordToFile); + ui->recordSilenceTime->setValue(m_settings.m_recordSilenceTime); + ui->recordSilenceText->setText(tr("%1").arg(m_settings.m_recordSilenceTime / 10.0, 0, 'f', 1)); getRollupContents()->restoreState(m_rollupState); blockApplySettings(false); } @@ -327,6 +333,47 @@ void DemodAnalyzerGUI::on_log2Decim_currentIndexChanged(int index) applySettings(); } +void DemodAnalyzerGUI::on_record_toggled(bool checked) +{ + ui->showFileDialog->setEnabled(!checked); + m_settings.m_recordToFile = checked; + applySettings(); +} + +void DemodAnalyzerGUI::on_showFileDialog_clicked(bool checked) +{ + (void) checked; + QFileDialog fileDialog( + this, + tr("Save record file"), + m_settings.m_fileRecordName, + tr("WAV Files (*.wav)") + ); + + fileDialog.setOptions(QFileDialog::DontUseNativeDialog); + fileDialog.setFileMode(QFileDialog::AnyFile); + QStringList fileNames; + + if (fileDialog.exec()) + { + fileNames = fileDialog.selectedFiles(); + + if (fileNames.size() > 0) + { + m_settings.m_fileRecordName = fileNames.at(0); + ui->fileNameText->setText(m_settings.m_fileRecordName); + applySettings(); + } + } +} + +void DemodAnalyzerGUI::on_recordSilenceTime_valueChanged(int value) +{ + m_settings.m_recordSilenceTime = value; + ui->recordSilenceText->setText(tr("%1").arg(value / 10.0, 0, 'f', 1)); + applySettings(); +} + void DemodAnalyzerGUI::tick() { m_channelPowerAvg(m_demodAnalyzer->getMagSqAvg()); @@ -379,4 +426,7 @@ void DemodAnalyzerGUI::makeUIConnections() QObject::connect(ui->channels, qOverload(&QComboBox::currentIndexChanged), this, &DemodAnalyzerGUI::on_channels_currentIndexChanged); QObject::connect(ui->channelApply, &QPushButton::clicked, this, &DemodAnalyzerGUI::on_channelApply_clicked); QObject::connect(ui->log2Decim, qOverload(&QComboBox::currentIndexChanged), this, &DemodAnalyzerGUI::on_log2Decim_currentIndexChanged); + QObject::connect(ui->record, &ButtonSwitch::toggled, this, &DemodAnalyzerGUI::on_record_toggled); + QObject::connect(ui->showFileDialog, &QPushButton::clicked, this, &DemodAnalyzerGUI::on_showFileDialog_clicked); + QObject::connect(ui->recordSilenceTime, &QSlider::valueChanged, this, &DemodAnalyzerGUI::on_recordSilenceTime_valueChanged); } diff --git a/plugins/feature/demodanalyzer/demodanalyzergui.h b/plugins/feature/demodanalyzer/demodanalyzergui.h index 84483f668..062f04052 100644 --- a/plugins/feature/demodanalyzer/demodanalyzergui.h +++ b/plugins/feature/demodanalyzer/demodanalyzergui.h @@ -93,6 +93,9 @@ private slots: void on_channels_currentIndexChanged(int index); void on_channelApply_clicked(); void on_log2Decim_currentIndexChanged(int index); + void on_record_toggled(bool checked); + void on_showFileDialog_clicked(bool checked); + void on_recordSilenceTime_valueChanged(int value); void updateStatus(); void tick(); }; diff --git a/plugins/feature/demodanalyzer/demodanalyzergui.ui b/plugins/feature/demodanalyzer/demodanalyzergui.ui index a4ca773de..1246ca5eb 100644 --- a/plugins/feature/demodanalyzer/demodanalyzergui.ui +++ b/plugins/feature/demodanalyzer/demodanalyzergui.ui @@ -7,7 +7,7 @@ 0 0 739 - 778 + 785 @@ -37,7 +37,7 @@ 0 10 718 - 41 + 70 @@ -72,6 +72,12 @@ + + + 0 + 22 + + start/stop acquisition @@ -195,12 +201,6 @@ - - - 80 - 0 - - Analyzer (sink) sample rate @@ -249,6 +249,119 @@ + + + + + + + 24 + 16777215 + + + + Start/stop recording + + + + + + + :/record_off.png:/record_off.png + + + + + + + + 24 + 24 + + + + + 24 + 24 + + + + Open file + + + + + + + :/preset-load.png:/preset-load.png + + + + + + + true + + + Current recording file + + + ... + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 24 + 24 + + + + Silence time (s) before recording is stopoed. 0 for continuous recording. + + + 0 + + + 100 + + + 1 + + + 1 + + + 0 + + + + + + + + 30 + 16777215 + + + + Silence time (s) before recording is stopoed + + + 10.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + diff --git a/plugins/feature/demodanalyzer/demodanalyzersettings.cpp b/plugins/feature/demodanalyzer/demodanalyzersettings.cpp index aa894abd3..05e9f2d23 100644 --- a/plugins/feature/demodanalyzer/demodanalyzersettings.cpp +++ b/plugins/feature/demodanalyzer/demodanalyzersettings.cpp @@ -83,6 +83,9 @@ void DemodAnalyzerSettings::resetToDefaults() m_reverseAPIFeatureSetIndex = 0; m_reverseAPIFeatureIndex = 0; m_workspaceIndex = 0; + m_fileRecordName.clear(); + m_recordToFile = false; + m_recordSilenceTime = 0; } QByteArray DemodAnalyzerSettings::serialize() const @@ -112,6 +115,9 @@ QByteArray DemodAnalyzerSettings::serialize() const s.writeS32(13, m_workspaceIndex); s.writeBlob(14, m_geometryBytes); + s.writeString(15, m_fileRecordName); + s.writeBool(16, m_recordToFile); + s.writeS32(17, m_recordSilenceTime); return s.final(); } @@ -170,6 +176,9 @@ bool DemodAnalyzerSettings::deserialize(const QByteArray& data) d.readS32(13, &m_workspaceIndex, 0); d.readBlob(14, &m_geometryBytes); + d.readString(15, &m_fileRecordName); + d.readBool(16, &m_recordToFile, false); + d.readS32(17, &m_recordSilenceTime, 0); return true; } diff --git a/plugins/feature/demodanalyzer/demodanalyzersettings.h b/plugins/feature/demodanalyzer/demodanalyzersettings.h index d3670d181..7508cdf86 100644 --- a/plugins/feature/demodanalyzer/demodanalyzersettings.h +++ b/plugins/feature/demodanalyzer/demodanalyzersettings.h @@ -49,6 +49,9 @@ struct DemodAnalyzerSettings uint16_t m_reverseAPIPort; uint16_t m_reverseAPIFeatureSetIndex; uint16_t m_reverseAPIFeatureIndex; + QString m_fileRecordName; + bool m_recordToFile; + int m_recordSilenceTime; //!< 100's ms Serializable *m_spectrumGUI; Serializable *m_scopeGUI; Serializable *m_rollupState; diff --git a/plugins/feature/demodanalyzer/demodanalyzerworker.cpp b/plugins/feature/demodanalyzer/demodanalyzerworker.cpp index 866d4e562..c88db5707 100644 --- a/plugins/feature/demodanalyzer/demodanalyzerworker.cpp +++ b/plugins/feature/demodanalyzer/demodanalyzerworker.cpp @@ -19,6 +19,7 @@ #include "dsp/scopevis.h" #include "dsp/datafifo.h" +#include "dsp/wavfilerecord.h" #include "demodanalyzerworker.h" @@ -28,7 +29,11 @@ MESSAGE_CLASS_DEFINITION(DemodAnalyzerWorker::MsgConnectFifo, Message) DemodAnalyzerWorker::DemodAnalyzerWorker() : m_dataFifo(nullptr), m_msgQueueToFeature(nullptr), - m_sampleBufferSize(0) + m_sampleBufferSize(0), + m_wavFileRecord(nullptr), + m_recordSilenceNbSamples(0), + m_recordSilenceCount(0), + m_nbBytes(0) { qDebug("DemodAnalyzerWorker::DemodAnalyzerWorker"); } @@ -47,12 +52,15 @@ void DemodAnalyzerWorker::reset() void DemodAnalyzerWorker::startWork() { QMutexLocker mutexLocker(&m_mutex); + m_wavFileRecord = new WavFileRecord(m_sinkSampleRate); connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); } void DemodAnalyzerWorker::stopWork() { QMutexLocker mutexLocker(&m_mutex); + delete m_wavFileRecord; + m_wavFileRecord = nullptr; disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); } @@ -74,6 +82,13 @@ void DemodAnalyzerWorker::feedPart( nbBytes = 2; } + if ((nbBytes != m_nbBytes) && m_wavFileRecord) + { + m_wavFileRecord->stopRecording(); + m_wavFileRecord->setMono(nbBytes == 2); + } + + m_nbBytes = nbBytes; int countSamples = (end - begin) / nbBytes; if (countSamples > m_sampleBufferSize) @@ -93,6 +108,62 @@ void DemodAnalyzerWorker::feedPart( vbegin.push_back(m_sampleBuffer.begin()); m_scopeVis->feed(vbegin, countSamples/(1<isRecording()) { + m_wavFileRecord->stopRecording(); + } + } + } + else + { + if (!m_wavFileRecord->isRecording()) { + m_wavFileRecord->startRecording(); + } + writeSampleToFile(sample); + m_recordSilenceCount = 0; + } + } + } +} + +void DemodAnalyzerWorker::writeSampleToFile(const Sample& sample) +{ + if (SDR_RX_SAMP_SZ == 16) + { + if (m_nbBytes == 2) { + m_wavFileRecord->writeMono(sample.m_real); + } else { + m_wavFileRecord->write(sample.m_real, sample.m_imag); + } + } + else + { + if (m_nbBytes == 2) { + m_wavFileRecord->writeMono(sample.m_real >> 8); + } else { + m_wavFileRecord->write(sample.m_real >> 8, sample.m_imag >> 8); + } + } } void DemodAnalyzerWorker::decimate(int countSamples) @@ -190,11 +261,95 @@ void DemodAnalyzerWorker::applySettings(const DemodAnalyzerSettings& settings, b << " m_title: " << settings.m_title << " m_rgbColor: " << settings.m_rgbColor << " m_log2Decim: " << settings.m_log2Decim + << " m_fileRecordName: " << settings.m_fileRecordName + << " m_recordToFile: " << settings.m_recordToFile + << " m_recordSilenceTime: " << settings.m_recordSilenceTime << " force: " << force; + if ((m_settings.m_fileRecordName != settings.m_fileRecordName) || force) + { + if (m_wavFileRecord) + { + QStringList dotBreakout = settings.m_fileRecordName.split(QLatin1Char('.')); + + if (dotBreakout.size() > 1) + { + QString extension = dotBreakout.last(); + + if (extension != "wav") { + dotBreakout.last() = "wav"; + } + } + else + { + dotBreakout.append("wav"); + } + + QString newFileRecordName = dotBreakout.join(QLatin1Char('.')); + QString fileBase; + FileRecordInterface::guessTypeFromFileName(newFileRecordName, fileBase); + qDebug("DemodAnalyzerWorker::applySettings: newFileRecordName: %s fileBase: %s", qPrintable(newFileRecordName), qPrintable(fileBase)); + m_wavFileRecord->setFileName(fileBase); + } + } + + if ((m_settings.m_recordToFile != settings.m_recordToFile) || force) + { + if (m_wavFileRecord) + { + if (settings.m_recordToFile) + { + if (!m_wavFileRecord->isRecording()) { + m_wavFileRecord->startRecording(); + } + } + else + { + if (m_wavFileRecord->isRecording()) { + m_wavFileRecord->stopRecording(); + } + } + + m_recordSilenceCount = 0; + } + } + + if ((m_settings.m_recordSilenceTime != settings.m_recordSilenceTime) + || (m_settings.m_log2Decim != settings.m_log2Decim) || force) + { + m_recordSilenceNbSamples = (settings.m_recordSilenceTime * (m_sinkSampleRate / (1<isRecording()) { + m_wavFileRecord->stopRecording(); + } + + m_wavFileRecord->setSampleRate(m_sinkSampleRate / (1<isRecording()) { + m_wavFileRecord->stopRecording(); + } + + m_wavFileRecord->setSampleRate(m_sinkSampleRate / (1< m_channelPowerAvg; ScopeVis* m_scopeVis; + WavFileRecord* m_wavFileRecord; + int m_recordSilenceNbSamples; + int m_recordSilenceCount; + int m_nbBytes; QRecursiveMutex m_mutex; + void feedPart( + const QByteArray::const_iterator& begin, + const QByteArray::const_iterator& end, + DataFifo::DataType dataType + ); + bool handleMessage(const Message& cmd); void decimate(int countSamples); + void writeSampleToFile(const Sample& sample); inline void processSample( DataFifo::DataType dataType, diff --git a/plugins/feature/demodanalyzer/readme.md b/plugins/feature/demodanalyzer/readme.md index 465efb3c1..39d42d4f4 100644 --- a/plugins/feature/demodanalyzer/readme.md +++ b/plugins/feature/demodanalyzer/readme.md @@ -73,6 +73,35 @@ This is the resulting sample rate after possible decimation that is used by the Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band. +

A.8. Record as .wav file

+ +Use this button to toggle recording. Start or stop recording + +Format is always 16 bit little-endian and can be mono (1 channel) or stereo (2 channels) depending on data type. + +

A.9. Select recording output file

+ +Click on this icon to open a file selection dialog that lets you specify the location and name of the output files. + +Each recording is written in a new file with the starting timestamp before the `.wav` extension in `yyyy-MM-ddTHH_mm_ss_zzz` format. It keeps the first dot limited groups of the filename before the `.wav` extension if there are two such groups or before the two last groups if there are more than two groups. Examples: + + - Given file name: `test.wav` then a recording file will be like: `test.2020-08-05T21_39_07_974.wav` + - Given file name: `test.2020-08-05T20_36_15_974.wav` then a recording file will be like (with timestamp updated): `test.2020-08-05T21_41_21_173.wav` + - Given file name: `test.first.wav` then a recording file will be like: `test.2020-08-05T22_00_07_974.wav` + - Given file name: `record.test.first.eav` then a recording file will be like: `reocrd.test.2020-08-05T21_39_52_974.wav` + +If a filename is given without `.wav` extension then the `.wav` extension is appended automatically before the above algorithm is applied. If a filename is given with an extension different of `.wav` then the extension is replaced by `.wav` automatically before the above algorithm is applied. + +The file path currently being written (or last closed) appears at the right of the button (A.1.10). + +

A.1.10. Recording file path

+ +This is the file path currently being written (or last closed). + +

A.1.11 Record silence time

+ +This is the time in seconds (between 0.1 and 10.0) of silence (null samples) before recording stops. When non null samples come again this will start a new recording. Set the value to 0 to record continuously. +

B. Spectrum view

This is the same display as with the channel analyzer spectrum view. This is the spectrum of a real signal so it is symmetrical around zero frequency. Details on the spectrum view and controls can be found [here](../../../sdrgui/gui/spectrum.md) diff --git a/sdrbase/dsp/wavfilerecord.cpp b/sdrbase/dsp/wavfilerecord.cpp index 62236e16a..2cc572f07 100644 --- a/sdrbase/dsp/wavfilerecord.cpp +++ b/sdrbase/dsp/wavfilerecord.cpp @@ -82,8 +82,9 @@ void WavFileRecord::feed(const SampleVector::const_iterator& begin, const Sample { (void) positiveOnly; - if(!m_recordOn) + if (!m_recordOn) { return; + } if (begin < end) // if there is something to put out { diff --git a/sdrbase/dsp/wavfilerecord.h b/sdrbase/dsp/wavfilerecord.h index b717706ea..6e1e49317 100644 --- a/sdrbase/dsp/wavfilerecord.h +++ b/sdrbase/dsp/wavfilerecord.h @@ -94,6 +94,7 @@ public: virtual int getBytesPerSample() override { return 4; }; const QString& getCurrentFileName() override { return m_currentFileName; } void setMono(bool mono) { m_nbChannels = mono ? 1 : 2; } + void setSampleRate(quint32 sampleRate) { m_sampleRate = sampleRate; } void genUniqueFileName(uint deviceUID, int istream = -1);