diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 05b20e7..5962ae9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -45,6 +45,7 @@ list(APPEND inspectrum_sources samplesource.cpp spectrogramcontrols.cpp spectrogramplot.cpp + symbolprogoutput.cpp threshold.cpp traceplot.cpp tuner.cpp diff --git a/src/cursor.cpp b/src/cursor.cpp index b9ac71d..02c3158 100644 --- a/src/cursor.cpp +++ b/src/cursor.cpp @@ -25,6 +25,9 @@ Cursor::Cursor(Qt::Orientation orientation, Qt::CursorShape mouseCursorShape, QO } +void Cursor::frozen(bool enable) { + isFrozen = enable; +} int Cursor::fromPoint(QPoint point) { return (orientation == Qt::Vertical) ? point.x() : point.y(); @@ -39,17 +42,22 @@ bool Cursor::pointOverCursor(QPoint point) bool Cursor::mouseEvent(QEvent::Type type, QMouseEvent event) { + if (isFrozen) { + return false; + } // If the mouse pointer moves over a cursor, display a resize pointer - if (pointOverCursor(event.pos()) && type != QEvent::Leave) { - if (!cursorOverrided) { - cursorOverrided = true; - QApplication::setOverrideCursor(QCursor(cursorShape)); - } - // Restore pointer if it moves off the cursor, or leaves the widget - } else if (cursorOverrided) { - cursorOverrided = false; - QApplication::restoreOverrideCursor(); - } + if (pointOverCursor(event.pos()) && type != QEvent::Leave) { + if (!cursorOverrided) { + cursorOverrided = true; + QApplication::setOverrideCursor(QCursor(cursorShape)); + } + + // Restore pointer if it moves off the cursor, or leaves the widget + } else if (cursorOverrided) { + cursorOverrided = false; + QApplication::restoreOverrideCursor(); + } + // Start dragging on left mouse button press, if over a cursor if (type == QEvent::MouseButtonPress) { diff --git a/src/cursor.h b/src/cursor.h index aceb608..b4f7bb6 100644 --- a/src/cursor.h +++ b/src/cursor.h @@ -34,6 +34,8 @@ class Cursor : public QObject void setPos(int newPos); bool mouseEvent(QEvent::Type type, QMouseEvent event); + void frozen(bool enable); + signals: void posChanged(); @@ -45,5 +47,6 @@ class Cursor : public QObject Qt::CursorShape cursorShape; bool dragging = false; bool cursorOverrided = false; + bool isFrozen = false; int cursorPosition = 0; }; diff --git a/src/cursors.cpp b/src/cursors.cpp index 8087f03..cedf4a3 100644 --- a/src/cursors.cpp +++ b/src/cursors.cpp @@ -29,6 +29,11 @@ Cursors::Cursors(QObject * parent) : QObject::QObject(parent) connect(maxCursor, &Cursor::posChanged, this, &Cursors::cursorMoved); } +void Cursors::frozen(bool enable) { + minCursor->frozen(enable); + maxCursor->frozen(enable); +} + void Cursors::cursorMoved() { // Swap cursors if one has been dragged past the other diff --git a/src/cursors.h b/src/cursors.h index 9e7e572..a12491e 100644 --- a/src/cursors.h +++ b/src/cursors.h @@ -39,6 +39,9 @@ class Cursors : public QObject void setSegments(int segments); void setSelection(range_t selection); + void frozen(bool enable); + + public slots: void cursorMoved(); diff --git a/src/frequencydemod.cpp b/src/frequencydemod.cpp index 17bdf6b..9abdc69 100644 --- a/src/frequencydemod.cpp +++ b/src/frequencydemod.cpp @@ -20,6 +20,8 @@ #include "frequencydemod.h" #include #include "util.h" +#include + FrequencyDemod::FrequencyDemod(std::shared_ptr>> src) : SampleBuffer(src) { @@ -28,11 +30,49 @@ FrequencyDemod::FrequencyDemod(std::shared_ptr> void FrequencyDemod::work(void *input, void *output, int count, size_t sampleid) { + double power_window[10]; + unsigned int window_size = 10; + unsigned int window_index = 0; + double power_sum = 0.0f; + double avg_power = 0.0f; + auto in = static_cast*>(input); auto out = static_cast(output); freqdem fdem = freqdem_create(relativeBandwidth() / 2.0); + + QSettings settings; + int sqval = settings.value("Squelch", 0).toInt(); + double squelch_threshold = pow(2, sqval+2); // 100.0 * settings.value("Squelch", 0).toInt(); + bool using_squelch = sqval ? true : false; + + if (using_squelch) { + // Initialize power window + for (unsigned int i = 0; i < window_size; i++) { + power_window[i] = 0.0f; + } + } + + for (int i = 0; i < count; i++) { - freqdem_demodulate(fdem, in[i], &out[i]); + + if (using_squelch) { + double power = in[i].real() * in[i].real() + + in[i].imag() * in[i].imag(); + // Update power averaging window + power_sum -= power_window[window_index]; // Subtract oldest power + power_window[window_index] = power; // Add new power + power_sum += power; // Update sum + window_index = (window_index + 1) % window_size; // Circular buffer index + // Compute average power + avg_power = power_sum / window_size; + } + // Check if average power exceeds squelch threshold + + if ( (!using_squelch) || (avg_power > squelch_threshold)) { + freqdem_demodulate(fdem, in[i], &out[i]); + } else { + out[i] = 0; + } } freqdem_destroy(fdem); } diff --git a/src/inputsource.cpp b/src/inputsource.cpp index ebc2840..4385bbb 100644 --- a/src/inputsource.cpp +++ b/src/inputsource.cpp @@ -461,6 +461,16 @@ double InputSource::rate() return sampleRate; } +void InputSource::setCenterFrequency(double freq) +{ + centerFreq = freq; + invalidate(); +} + +double InputSource::centerFrequency() +{ + return centerFreq; +} std::unique_ptr[]> InputSource::getSamples(size_t start, size_t length) { if (inputFile == nullptr) diff --git a/src/inputsource.h b/src/inputsource.h index c0d6761..a965428 100644 --- a/src/inputsource.h +++ b/src/inputsource.h @@ -37,6 +37,7 @@ class InputSource : public SampleSource> QFile *inputFile = nullptr; size_t sampleCount = 0; double sampleRate = 0.0; + double centerFreq = 0.0; uchar *mmapData = nullptr; std::unique_ptr sampleAdapter; std::string _fmt; @@ -54,8 +55,10 @@ class InputSource : public SampleSource> return sampleCount; }; void setSampleRate(double rate); + void setCenterFrequency(double freq); void setFormat(std::string fmt); double rate(); + double centerFrequency(); bool realSignal() { return _realSignal; }; diff --git a/src/main.cpp b/src/main.cpp index 7d3e90b..ae44c83 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,6 +45,11 @@ int main(int argc, char *argv[]) QCoreApplication::translate("main", "fmt")); parser.addOption(formatOption); + QCommandLineOption centerFreqOption(QStringList() << "c" << "centerfreq", + QCoreApplication::translate("main", "Set center frequency."), + QCoreApplication::translate("main", "Hz")); + parser.addOption(centerFreqOption); + // Process the actual command line parser.process(a); @@ -57,8 +62,8 @@ int main(int argc, char *argv[]) if (args.size()>=1) mainWin.openFile(args.at(0)); + bool ok; if (parser.isSet(rateOption)) { - bool ok; auto rate = parser.value(rateOption).toDouble(&ok); if(!ok) { fputs("ERROR: could not parse rate\n", stderr); @@ -67,6 +72,16 @@ int main(int argc, char *argv[]) mainWin.setSampleRate(rate); } + + if (parser.isSet(centerFreqOption)) { + auto centerfreq = parser.value(centerFreqOption).toDouble(&ok); + if(!ok) { + fputs("ERROR: could not parse center frequency\n", stderr); + return 1; + } + mainWin.setCenterFrequency(centerfreq); + } + mainWin.show(); return a.exec(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index dc62d76..b0b4048 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -44,11 +44,15 @@ MainWindow::MainWindow() // Connect dock inputs connect(dock, &SpectrogramControls::openFile, this, &MainWindow::openFile); + connect(dock->sampleRate, static_cast(&QLineEdit::textChanged), this, static_cast(&MainWindow::setSampleRate)); + connect(dock->centerFrequency, static_cast(&QLineEdit::textChanged), this, static_cast(&MainWindow::setCenterFrequency)); connect(dock, static_cast(&SpectrogramControls::fftOrZoomChanged), plots, &PlotView::setFFTAndZoom); connect(dock->powerMaxSlider, &QSlider::valueChanged, plots, &PlotView::setPowerMax); connect(dock->powerMinSlider, &QSlider::valueChanged, plots, &PlotView::setPowerMin); + connect(dock->squelchSlider, &QSlider::valueChanged, plots, &PlotView::setSquelch); connect(dock->cursorsCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableCursors); + connect(dock->cursorsFreezeCheckBox, &QCheckBox::stateChanged, plots, &PlotView::freezeCursors); connect(dock->scalesCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableScales); connect(dock->annosCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableAnnotations); connect(dock->annosCheckBox, &QCheckBox::stateChanged, dock, &SpectrogramControls::enableAnnotations); @@ -59,6 +63,9 @@ MainWindow::MainWindow() connect(plots, &PlotView::timeSelectionChanged, dock, &SpectrogramControls::timeSelectionChanged); connect(plots, &PlotView::zoomIn, dock, &SpectrogramControls::zoomIn); connect(plots, &PlotView::zoomOut, dock, &SpectrogramControls::zoomOut); + connect(plots, &PlotView::coordinateClick, dock, &SpectrogramControls::coordinateClick); + + void coordinateClick(double time_position, double frequency); // Set defaults after making connections so everything is in sync dock->setDefaults(); @@ -87,6 +94,14 @@ void MainWindow::openFile(QString fileName) if (!ss.fail()) { setSampleRate(rate); } + + + std::stringstream ssfreq(centerfreq.toUtf8().constData()); + double freq; + ssfreq >> freq; + if (!ssfreq.fail()) { + setCenterFrequency(freq); + } } try @@ -100,9 +115,26 @@ void MainWindow::openFile(QString fileName) } } +void MainWindow::keyPressEvent(QKeyEvent *event) { + switch (event->key()) { + case Qt::Key_C: + dock->cursorsCheckBox->setChecked(! plots->cursorsAreEnabled()); + break; + case Qt::Key_W: + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + QApplication::closeAllWindows(); + } + break; + + default: + QMainWindow::keyPressEvent(event); // Pass to base class + } +} + void MainWindow::invalidateEvent() { plots->setSampleRate(input->rate()); + plots->setCenterFrequency(input->centerFrequency()); // Only update the text box if it is not already representing // the current value. Otherwise the cursor might jump or the @@ -129,6 +161,22 @@ void MainWindow::setSampleRate(double rate) dock->sampleRate->setText(QString::number(rate)); } +void MainWindow::setCenterFrequency(QString freq) +{ + auto centerFreq = freq.toDouble(); + input->setCenterFrequency(centerFreq); + plots->setCenterFrequency(centerFreq); + + // Save the sample rate in settings as we're likely to be opening the same file across multiple runs + QSettings settings; + settings.setValue("CenterFrequency", centerFreq); +} + +void MainWindow::setCenterFrequency(double freq) +{ + dock->centerFrequency->setText(QString::number(freq)); +} + void MainWindow::setFormat(QString fmt) { input->setFormat(fmt.toUtf8().constData()); diff --git a/src/mainwindow.h b/src/mainwindow.h index 4c7cb7f..eca1960 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -31,11 +31,14 @@ class MainWindow : public QMainWindow, Subscriber public: MainWindow(); void changeSampleRate(double rate); + void keyPressEvent(QKeyEvent *event) override; public slots: void openFile(QString fileName); void setSampleRate(QString rate); void setSampleRate(double rate); + void setCenterFrequency(QString centerfreq); + void setCenterFrequency(double centerfreq); void setFormat(QString fmt); void invalidateEvent() override; diff --git a/src/plotview.cpp b/src/plotview.cpp index 839ecff..04dd47a 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -35,15 +35,24 @@ #include #include #include +#include +#include +#include +#include + #include "plots.h" +#include "symbolprogoutput.h" + + -PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0}) +PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0}), selectedSamples({0,0}) { mainSampleSource = input; setDragMode(QGraphicsView::ScrollHandDrag); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setMouseTracking(true); enableCursors(false); + freezeCursors(false); connect(&cursors, &Cursors::cursorsMoved, this, &PlotView::cursorsMoved); spectrogramPlot = new SpectrogramPlot(std::shared_ptr>>(mainSampleSource)); @@ -98,6 +107,7 @@ void PlotView::updateAnnotationTooltip(QMouseEvent *event) } } + void PlotView::contextMenuEvent(QContextMenuEvent * event) { QMenu menu; @@ -121,6 +131,7 @@ void PlotView::contextMenuEvent(QContextMenuEvent * event) // that are compatible with selectedPlot's output QMenu *plotsMenu = menu.addMenu("Add derived plot"); auto src = selectedPlot->output(); + last_src_used = src; auto compatiblePlots = as_range(Plots::plots.equal_range(src->sampleType())); for (auto p : compatiblePlots) { auto plotInfo = p.second; @@ -148,6 +159,25 @@ void PlotView::contextMenuEvent(QContextMenuEvent * event) extract->setEnabled(cursorsEnabled && (src->sampleType() == typeid(float))); extractMenu->addAction(extract); + + + // Add action to run external program + auto runProgramAction = new QAction("Feed to External...", extractMenu); + connect( + runProgramAction, &QAction::triggered, + this, [=]() { + selectAndFeedExternalProgram(src); + } + ); + + runProgramAction->setEnabled(cursorsEnabled && (src->sampleType() == typeid(float))); + extractMenu->addAction(runProgramAction); + + + + + + // Add action to extract symbols from selected plot to clipboard auto extractClipboard = new QAction("Copy to clipboard", extractMenu); connect( @@ -178,6 +208,7 @@ void PlotView::contextMenuEvent(QContextMenuEvent * event) connect( rem, &QAction::triggered, this, [=]() { + last_src_used = nullptr; plots.erase(it); } ); @@ -188,6 +219,7 @@ void PlotView::contextMenuEvent(QContextMenuEvent * event) updateViewRange(false); if(menu.exec(event->globalPos())) updateView(false); + } void PlotView::cursorsMoved() @@ -197,6 +229,8 @@ void PlotView::cursorsMoved() columnToSample(horizontalScrollBar()->value() + cursors.selection().maximum) }; + samplesPerSegment = selectedSamples.length() / cursors.segments(); + emitTimeSelection(); viewport()->update(); } @@ -208,25 +242,137 @@ void PlotView::emitTimeSelection() emit timeSelectionChanged(selectionTime); } +void PlotView::freezeCursors(bool enabled) { + + cursorsFrozen.enabled = enabled; + if (cursorsEnabled) { + if (enabled) { + cursorsFrozen.range = cursors.selection(); + } + + cursors.frozen(enabled); + } +} void PlotView::enableCursors(bool enabled) { cursorsEnabled = enabled; if (enabled) { int margin = viewport()->rect().width() / 3; - cursors.setSelection({viewport()->rect().left() + margin, viewport()->rect().right() - margin}); + + + // Update cursors + int startColumn = viewport()->rect().left() + margin; + if (selectedSamples.minimum != selectedSamples.maximum) { + + int colWidth = sampleToColumn(selectedSamples.maximum) - sampleToColumn(selectedSamples.minimum); + + range_t newSelection = { + startColumn, + startColumn + colWidth + }; + cursors.setSelection(newSelection); + } else { + cursors.setSelection({startColumn, viewport()->rect().right() - margin}); + } + + // cursorsMoved(); } viewport()->update(); } - +void PlotView::keyPressEvent(QKeyEvent *event) { + + QSettings settings; + + bool shiftMod = QApplication::keyboardModifiers() & Qt::ShiftModifier; + bool ctrlMod = QApplication::keyboardModifiers() & Qt::ControlModifier; + QScrollBar *scrollBar = horizontalScrollBar(); + std::shared_ptr plot_src; + + switch (event->key()) { + case Qt::Key_Left: + if (ctrlMod) { + scrollBar->setValue(scrollBar->value() + scrollBar->pageStep() * -1); + } else { + scrollBar->setValue(scrollBar->value() + scrollBar->singleStep() * -1); + } + + break; + case Qt::Key_Right: + if (ctrlMod) { + scrollBar->setValue(scrollBar->value() + scrollBar->pageStep()); + } else { + scrollBar->setValue(scrollBar->value() + scrollBar->singleStep()); + } + break; + + case Qt::Key_Up: + if (! (cursorsEnabled || ctrlMod) ) { + QGraphicsView::keyPressEvent(event); // Pass to base class + break; + } + if (shiftMod) + cursors.setSelection({cursors.selection().minimum,cursors.selection().maximum+10}); + else + cursors.setSelection({cursors.selection().minimum,cursors.selection().maximum+1}); + + cursorsMoved(); + break; + case Qt::Key_Down: + if (! (cursorsEnabled || ctrlMod) ) { + QGraphicsView::keyPressEvent(event); // Pass to base class + break; + } + if (shiftMod) { + if (cursors.selection().maximum - cursors.selection().minimum > 10) + cursors.setSelection({cursors.selection().minimum,cursors.selection().maximum-10}); + } else { + if (cursors.selection().maximum - cursors.selection().minimum > 2) + cursors.setSelection({cursors.selection().minimum,cursors.selection().maximum-1}); + } + + + cursorsMoved(); + break; + case Qt::Key_F: + if (spectrogramPlot->tunerEnabled()) { + break; + } + plot_src = spectrogramPlot->output(); + addPlot(Plots::frequencyPlot(plot_src)); + repaint(); + break; + case Qt::Key_R: + if (last_src_used && cursorsEnabled) { + + QString lastProgramPath = settings.value("lastProgramPath", QString("")).toString(); + if (lastProgramPath != "") { + feedSymbolsToExternalProgram(lastProgramPath, last_src_used); + } + } + break; + + + default: + QGraphicsView::keyPressEvent(event); // Pass to base class + } +} bool PlotView::viewportEvent(QEvent *event) { // Handle wheel events for zooming (before the parent's handler to stop normal scrolling) + if (event->type() == QEvent::Wheel) { QWheelEvent *wheelEvent = (QWheelEvent*)event; - if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + int delta = wheelEvent->angleDelta().y(); + + if (QApplication::keyboardModifiers() & Qt::ShiftModifier) { + QScrollBar *scrollBar = horizontalScrollBar(); + scrollBar->setValue(scrollBar->value() + scrollBar->pageStep() * -1 * delta); + + return true; + + } else if (QApplication::keyboardModifiers() & Qt::ControlModifier) { bool canZoomIn = zoomLevel < fftSize; bool canZoomOut = zoomLevel > 1; - int delta = wheelEvent->angleDelta().y(); if ((delta > 0 && canZoomIn) || (delta < 0 && canZoomOut)) { scrollZoomStepsAccumulated += delta; @@ -247,6 +393,9 @@ bool PlotView::viewportEvent(QEvent *event) { } } return true; + } else { + QScrollBar *scrollBar = horizontalScrollBar(); + scrollBar->setValue(scrollBar->value() - delta); } } @@ -258,7 +407,23 @@ bool PlotView::viewportEvent(QEvent *event) { QMouseEvent *mouseEvent = static_cast(event); + int plotY = -verticalScrollBar()->value(); + + if ( (QApplication::keyboardModifiers() & Qt::ControlModifier) && + (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease)) { + double clickTime = (columnToSample(mouseEvent->pos().x()) + viewRange.minimum) / sampleRate; + + // don't know how to translate mouse coords to frequency: TODO:FIXME -- setting at 0 for now + if (QApplication::keyboardModifiers() & Qt::ShiftModifier) { + emit coordinateClick(clickTime, 0, false); + } else { + emit coordinateClick(clickTime, 0, event->type() == QEvent::MouseButtonPress); + } + return true; + } + + for (auto&& plot : plots) { bool result = plot->mouseEvent( event->type(), @@ -284,6 +449,96 @@ bool PlotView::viewportEvent(QEvent *event) { return QGraphicsView::viewportEvent(event); } +QByteArray vectorToTextByteArray(const std::vector& data) +{ + QString text; + for (float value : data) { + text += QString::number(value, 'f', 6) + ","; // One float per line, 6 decimal places + } + return text.toUtf8(); // Convert to QByteArray with UTF-8 encoding +} + +void PlotView::feedSymbolsToExternalProgram(QString programPath, std::shared_ptr src) { + + if (!cursorsEnabled) + return; + + auto floatSrc = std::dynamic_pointer_cast>(src); + if (!floatSrc) + return; + + QProcess process(this); + + // Configure the process to allow writing to stdin + process.setProcessChannelMode(QProcess::MergedChannels); // Merge stdout and stderr for simplicity + process.start(programPath, QStringList()); // No arguments, adjust if needed + + if (!process.waitForStarted()) { + QMessageBox::critical(this, "Error", "Failed to start program: " + process.errorString()); + return; + } + + + auto samples = floatSrc->getSamples(selectedSamples.minimum, selectedSamples.length()); + auto step = (float)selectedSamples.length() / cursors.segments(); + auto symbols = std::vector(); + for (auto i = step / 2; i < selectedSamples.length(); i += step) + { + symbols.push_back(samples[i]); + } + + // Step 3: Pipe data to the program's stdin + // Assume data is a QString or QByteArray from your class + QByteArray dataToSend = vectorToTextByteArray(symbols); + process.write(dataToSend); + process.closeWriteChannel(); // Signal EOF to stdin + + // Step 4: Wait for the process to finish (optional, depending on your needs) + if (!process.waitForFinished()) { + QMessageBox::critical(this, "Error", "Program failed: " + process.errorString()); + return; + } + + // Optional: Read output if needed + QByteArray output = process.readAllStandardOutput(); + if (!output.isEmpty()) { + SymbolProgOutput outputDialog(QString(output), this); + outputDialog.exec(); + } + +} +void PlotView::selectAndFeedExternalProgram(std::shared_ptr src) { + + if (!cursorsEnabled) + return; + // Step 1: Open a file dialog to select the program + QFileDialog dialog(this); + dialog.setFileMode(QFileDialog::ExistingFile); + dialog.setWindowTitle("Select External Program"); + + + QSettings settings; + QString lastProgramPath = settings.value("lastProgramPath", QDir::homePath()).toString(); + dialog.selectFile(lastProgramPath); + // Optional: Filter for executable files (platform-specific) +#ifdef Q_OS_WIN + dialog.setNameFilter("Executables (*.exe);;All Files (*)"); +#else + dialog.setNameFilter("All Files (*)"); +#endif + + if (dialog.exec() != QDialog::Accepted || dialog.selectedFiles().isEmpty()) { + return; // User canceled or didn't select a file + } + + QString programPath = dialog.selectedFiles().first(); + + // Save the selected program path to QSettings + settings.setValue("lastProgramPath", programPath); + + feedSymbolsToExternalProgram(programPath, src); +} + void PlotView::extractSymbols(std::shared_ptr src, bool toClipboard) { @@ -421,14 +676,17 @@ void PlotView::repaint() viewport()->update(); } +#include void PlotView::setCursorSegments(int segments) { - // Calculate number of samples per segment - float sampPerSeg = (float)selectedSamples.length() / cursors.segments(); - // Alter selection to keep samples per segment the same - selectedSamples.maximum = selectedSamples.minimum + (segments * sampPerSeg + 0.5f); + int curSegments = cursors.segments(); + if (curSegments != segments) { + int deltaSegments = segments - curSegments; + // Calculate number of samples per segment + selectedSamples.maximum = selectedSamples.maximum + (deltaSegments * samplesPerSegment); + } cursors.setSegments(segments); updateView(); emitTimeSelection(); @@ -463,6 +721,13 @@ void PlotView::setPowerMin(int power) updateView(); } +void PlotView::setSquelch(int sq) { + squelch = sq; + if (spectrogramPlot != nullptr) + spectrogramPlot->setSquelch(sq); + updateView(); +} + void PlotView::setPowerMax(int power) { powerMax = power; @@ -634,6 +899,16 @@ void PlotView::setSampleRate(double rate) emitTimeSelection(); } + +void PlotView::setCenterFrequency(double freq) { + + centerFrequency = freq; + + if (spectrogramPlot != nullptr) + spectrogramPlot->setCenterFrequency(freq); + +} + void PlotView::enableScales(bool enabled) { timeScaleEnabled = enabled; diff --git a/src/plotview.h b/src/plotview.h index 326d547..7abae2e 100644 --- a/src/plotview.h +++ b/src/plotview.h @@ -29,6 +29,10 @@ #include "spectrogramplot.h" #include "traceplot.h" +typedef struct PlotViewFrozenCursors { + bool enabled; + range_t range; +} FrozenCursors; class PlotView : public QGraphicsView, Subscriber { Q_OBJECT @@ -36,15 +40,20 @@ class PlotView : public QGraphicsView, Subscriber public: PlotView(InputSource *input); void setSampleRate(double rate); - + void setCenterFrequency(double freq); + void keyPressEvent(QKeyEvent *event) override; + bool cursorsAreEnabled() { return cursorsEnabled;} signals: void timeSelectionChanged(float time); void zoomIn(); void zoomOut(); + void coordinateClick(double time_position, double frequency, bool down); + public slots: void cursorsMoved(); void enableCursors(bool enabled); + void freezeCursors(bool enabled); void enableScales(bool enabled); void enableAnnotations(bool enabled); void enableAnnotationCommentsTooltips(bool enabled); @@ -54,6 +63,7 @@ public slots: void setFFTAndZoom(int fftSize, int zoomLevel); void setPowerMin(int power); void setPowerMax(int power); + void setSquelch(int squelch); protected: void mouseMoveEvent(QMouseEvent *event) override; @@ -72,6 +82,8 @@ public slots: std::vector> plots; range_t viewRange; range_t selectedSamples; + size_t samplesPerSegment; + int zoomPos; size_t zoomSample; @@ -79,15 +91,23 @@ public slots: int zoomLevel = 1; int powerMin; int powerMax; + int squelch; bool cursorsEnabled; + FrozenCursors cursorsFrozen; double sampleRate = 0.0; + double centerFrequency = 0.0; bool timeScaleEnabled; int scrollZoomStepsAccumulated = 0; bool annotationCommentsEnabled; + std::shared_ptr last_src_used; + void addPlot(Plot *plot); void emitTimeSelection(); void extractSymbols(std::shared_ptr src, bool toClipboard); + + void selectAndFeedExternalProgram(std::shared_ptr src); + void feedSymbolsToExternalProgram(QString programPath, std::shared_ptr src); void exportSamples(std::shared_ptr src); template void exportSamples(std::shared_ptr src); int plotsHeight(); @@ -100,3 +120,5 @@ public slots: int sampleToColumn(size_t sample); size_t columnToSample(int col); }; + + diff --git a/src/spectrogramcontrols.cpp b/src/spectrogramcontrols.cpp index 0e0f9aa..cf70615 100644 --- a/src/spectrogramcontrols.cpp +++ b/src/spectrogramcontrols.cpp @@ -41,6 +41,15 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent sampleRate->setValidator(double_validator); layout->addRow(new QLabel(tr("Sample rate:")), sampleRate); + + + + centerFrequency = new QLineEdit(); + auto freq_double_validator = new QDoubleValidator(this); + freq_double_validator->setBottom(0.0); + centerFrequency->setValidator(freq_double_validator); + layout->addRow(new QLabel(tr("Center frequency:")), centerFrequency); + // Spectrogram settings layout->addRow(new QLabel()); // TODO: find a better way to add an empty row? layout->addRow(new QLabel(tr("Spectrogram"))); @@ -65,6 +74,12 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent powerMinSlider->setRange(-140, 10); layout->addRow(new QLabel(tr("Power min:")), powerMinSlider); + squelchSlider = new QSlider(Qt::Horizontal, widget); + squelchSlider->setRange(0, 21); + layout->addRow(new QLabel(tr("Squelch:")), squelchSlider); + + + scalesCheckBox = new QCheckBox(widget); scalesCheckBox->setCheckState(Qt::Checked); layout->addRow(new QLabel(tr("Scales:")), scalesCheckBox); @@ -75,6 +90,8 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent cursorsCheckBox = new QCheckBox(widget); layout->addRow(new QLabel(tr("Enable cursors:")), cursorsCheckBox); + cursorsFreezeCheckBox = new QCheckBox(widget); + layout->addRow(new QLabel(tr("Freeze cursors:")), cursorsFreezeCheckBox); cursorSymbolsSpinBox = new QSpinBox(); cursorSymbolsSpinBox->setMinimum(1); @@ -102,6 +119,25 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent commentsCheckBox = new QCheckBox(widget); layout->addRow(new QLabel(tr("Display annotation comments tooltips:")), commentsCheckBox); + + + // SigMF selection settings + layout->addRow(new QLabel()); // TODO: find a better way to add an empty row? + layout->addRow(new QLabel(tr("Time (ctrl-click, drag)"))); + + startTimeLabel = new QLabel(); + layout->addRow(new QLabel(tr("Start:")), startTimeLabel); + endTimeLabel = new QLabel(); + layout->addRow(new QLabel(tr("End:")), endTimeLabel); + deltaTimeLabel = new QLabel(); + layout->addRow(new QLabel(tr("Delta:")), deltaTimeLabel); + + + + + + + widget->setLayout(layout); setWidget(widget); @@ -111,6 +147,7 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent connect(cursorsCheckBox, &QCheckBox::stateChanged, this, &SpectrogramControls::cursorsStateChanged); connect(powerMinSlider, &QSlider::valueChanged, this, &SpectrogramControls::powerMinChanged); connect(powerMaxSlider, &QSlider::valueChanged, this, &SpectrogramControls::powerMaxChanged); + connect(squelchSlider, &QSlider::valueChanged, this, &SpectrogramControls::squelchChanged); } void SpectrogramControls::clearCursorLabels() @@ -145,7 +182,11 @@ void SpectrogramControls::setDefaults() fftSizeSlider->setValue(settings.value("FFTSize", 9).toInt()); powerMaxSlider->setValue(settings.value("PowerMax", 0).toInt()); powerMinSlider->setValue(settings.value("PowerMin", -100).toInt()); + squelchSlider->setValue(settings.value("Squelch", 0).toInt()); zoomLevelSlider->setValue(settings.value("ZoomLevel", 0).toInt()); + + int savedFreq = settings.value("CenterFrequency", 0).toInt(); + centerFrequency->setText(QString::number(savedFreq)); } void SpectrogramControls::fftOrZoomChanged(void) @@ -175,6 +216,13 @@ void SpectrogramControls::powerMinChanged(int value) settings.setValue("PowerMin", value); } +void SpectrogramControls::squelchChanged(int value) +{ + QSettings settings; + settings.setValue("Squelch", value); +} + + void SpectrogramControls::powerMaxChanged(int value) { QSettings settings; @@ -229,6 +277,31 @@ void SpectrogramControls::timeSelectionChanged(float time) } } +void SpectrogramControls::coordinateClick(double time_pos, double freq_pos, bool down) { + + if (down) { + endTimeLabel->setText(""); + deltaTimeLabel->setText(""); + startTimeLabel->setText(QString::fromStdString(formatSIValue(time_pos)) + "s"); + startTime = time_pos; + + } else { + endTime = time_pos; + if (endTime == startTime) { + deltaTimeLabel->setText(""); + endTimeLabel->setText(""); + } else { + if (endTime < startTime) { + deltaTimeLabel->setText(QString("-") + QString::fromStdString(formatSIValue(startTime - endTime)) + "s"); + } else { + deltaTimeLabel->setText(QString::fromStdString(formatSIValue(endTime - startTime)) + "s"); + } + endTimeLabel->setText(QString::fromStdString(formatSIValue(time_pos)) + "s"); + } + } +} + + void SpectrogramControls::zoomIn() { zoomLevelSlider->setValue(zoomLevelSlider->value() + 1); diff --git a/src/spectrogramcontrols.h b/src/spectrogramcontrols.h index 69e7d60..151be52 100644 --- a/src/spectrogramcontrols.h +++ b/src/spectrogramcontrols.h @@ -45,12 +45,15 @@ public slots: void zoomIn(); void zoomOut(); void enableAnnotations(bool enabled); + void coordinateClick(double time_pos, double freq_pos, bool down); + private slots: void fftSizeChanged(int value); void zoomLevelChanged(int value); void powerMinChanged(int value); void powerMaxChanged(int value); + void squelchChanged(int value); void fileOpenButtonClicked(); void cursorsStateChanged(int state); @@ -63,16 +66,27 @@ private slots: public: QPushButton *fileOpenButton; QLineEdit *sampleRate; + QLineEdit *centerFrequency; QSlider *fftSizeSlider; QSlider *zoomLevelSlider; QSlider *powerMaxSlider; QSlider *powerMinSlider; + QSlider *squelchSlider; QCheckBox *cursorsCheckBox; QSpinBox *cursorSymbolsSpinBox; + QCheckBox *cursorsFreezeCheckBox; QLabel *rateLabel; QLabel *periodLabel; QLabel *symbolRateLabel; QLabel *symbolPeriodLabel; + + QLabel *startTimeLabel; + double startTime; + double endTime; + QLabel *endTimeLabel; + QLabel *deltaTimeLabel; + + QCheckBox *scalesCheckBox; QCheckBox *annosCheckBox; QCheckBox *commentsCheckBox; diff --git a/src/spectrogramplot.cpp b/src/spectrogramplot.cpp index 3d42c0b..ec2d6da 100644 --- a/src/spectrogramplot.cpp +++ b/src/spectrogramplot.cpp @@ -40,6 +40,7 @@ SpectrogramPlot::SpectrogramPlot(std::shared_ptrrealSignal()) - painter.drawLine(0, tickny, 30, tickny); - painter.drawLine(0, tickpy, 30, tickpy); + int64_t tfreq = (bottomFreq + tick)/divisor; + /* + painter.drawLine(0, tickpy, 30, tickpy); + if (!inputSource->realSignal()) + painter.drawLine(0, tickny, 30, tickny); + */ - if (tick != 0) { - char buf[128]; + if (tfreq != 0) { + char buf[128]; + snprintf(buf, sizeof(buf), "%li %s",tfreq, suffixBuf); - if (bwPerTick % 1000000000 == 0) { - snprintf(buf, sizeof(buf), "-%lu GHz", tick / 1000000000); - } else if (bwPerTick % 1000000 == 0) { - snprintf(buf, sizeof(buf), "-%lu MHz", tick / 1000000); - } else if(bwPerTick % 1000 == 0) { - snprintf(buf, sizeof(buf), "-%lu kHz", tick / 1000); - } else { - snprintf(buf, sizeof(buf), "-%lu Hz", tick); - } + painter.drawLine(0, ticky, 40, ticky); + if (tfreq > centerFrequency/divisor) { - if (!inputSource->realSignal()) - painter.drawText(5, tickny - 5, buf); + painter.drawText(5, ticky + 15, buf); + } else { + painter.drawText(5, ticky, buf); - buf[0] = ' '; - painter.drawText(5, tickpy + 15, buf); + } } - tick += bwPerTick; + } + // Draw small ticks bwPerTick /= 10; - if (bwPerTick >= 1 ) { tick = 0; while (tick <= sampleRate / 2) { @@ -158,6 +177,7 @@ void SpectrogramPlot::paintFrequencyScale(QPainter &painter, QRect &rect) tick += bwPerTick; } } + painter.restore(); } @@ -400,6 +420,13 @@ void SpectrogramPlot::setPowerMin(int power) pixmapCache.clear(); } +void SpectrogramPlot::setSquelch(int sq) +{ + squelch = sq; + pixmapCache.clear(); + + tunerMoved(); +} void SpectrogramPlot::setZoomLevel(int zoom) { zoomLevel = zoom; @@ -410,6 +437,10 @@ void SpectrogramPlot::setSampleRate(double rate) sampleRate = rate; } + +void SpectrogramPlot::setCenterFrequency(double freq) { + centerFrequency = freq; +} void SpectrogramPlot::enableScales(bool enabled) { frequencyScaleEnabled = enabled; diff --git a/src/spectrogramplot.h b/src/spectrogramplot.h index 2e52cb4..23243c7 100644 --- a/src/spectrogramplot.h +++ b/src/spectrogramplot.h @@ -49,6 +49,7 @@ class SpectrogramPlot : public Plot bool mouseEvent(QEvent::Type type, QMouseEvent event) override; std::shared_ptr>> input() { return inputSource; }; void setSampleRate(double sampleRate); + void setCenterFrequency(double freq); bool tunerEnabled(); void enableScales(bool enabled); void enableAnnotations(bool enabled); @@ -59,6 +60,7 @@ public slots: void setFFTSize(int size); void setPowerMax(int power); void setPowerMin(int power); + void setSquelch(int power); void setZoomLevel(int zoom); void tunerMoved(); @@ -78,7 +80,9 @@ public slots: int zoomLevel; float powerMax; float powerMin; + float squelch; double sampleRate; + double centerFrequency; bool frequencyScaleEnabled; bool sigmfAnnotationsEnabled; diff --git a/src/symbolprogoutput.cpp b/src/symbolprogoutput.cpp new file mode 100644 index 0000000..a514bff --- /dev/null +++ b/src/symbolprogoutput.cpp @@ -0,0 +1,47 @@ +/* + * symbolprogoutput.cpp, part of the Inspectrum project + * + * Created on: Aug 27, 2025 + * Author: Pat Deegan + * Copyright (C) 2025 Pat Deegan, https://psychogenic.com + */ + +#include "symbolprogoutput.h" +SymbolProgOutput::SymbolProgOutput(const QString &output, QWidget *parent, size_t width, size_t height) : + QDialog(parent) +{ + setWindowTitle("Program Output"); + setMinimumSize(width, height); + + // Create layout + QVBoxLayout *layout = new QVBoxLayout(this); + + // Create text edit for output (scrollable) + textEdit = new QTextEdit(this); + textEdit->setReadOnly(true); // Prevent editing + textEdit->setText(output); // Set the output text + layout->addWidget(textEdit); + + // Create buttons + QHBoxLayout *buttonLayout = new QHBoxLayout(); + QPushButton *copyButton = new QPushButton("Copy to Clipboard", this); + QPushButton *closeButton = new QPushButton("Close", this); + buttonLayout->addWidget(copyButton); + buttonLayout->addWidget(closeButton); + layout->addLayout(buttonLayout); + + // Connect buttons + connect(copyButton, &QPushButton::clicked, this, + &SymbolProgOutput::copyToClipboard); + connect(closeButton, &QPushButton::clicked, this, &QDialog::accept); + + // Make dialog resizable + setSizeGripEnabled(true); +} + +void SymbolProgOutput::copyToClipboard() { + textEdit->selectAll(); + textEdit->copy(); + textEdit->textCursor().clearSelection(); // Deselect after copying +} + diff --git a/src/symbolprogoutput.h b/src/symbolprogoutput.h new file mode 100644 index 0000000..607c357 --- /dev/null +++ b/src/symbolprogoutput.h @@ -0,0 +1,27 @@ +/* + * symbolprogoutput.h, part of the Inspectrum project + * + * Created on: Aug 27, 2025 + * Author: Pat Deegan + * Copyright (C) 2025 Pat Deegan, https://psychogenic.com + */ + +#pragma once +#include +#include +#include +#include + +class SymbolProgOutput : public QDialog +{ + Q_OBJECT +public: + SymbolProgOutput(const QString& output, QWidget* parent = nullptr, size_t width = 700, size_t height = 500); + +private slots: + void copyToClipboard(); + +private: + QTextEdit* textEdit; +}; +