diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e00be0e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,45 @@ +name: Build + +on: [push] + +env: + BUILD_TYPE: Release + # For macOS qt keg-only package + CMAKE_PREFIX_PATH: '/usr/local/opt/qt@5' + +jobs: + build: + strategy: + matrix: + os: ['macos-latest', 'ubuntu-20.04', 'ubuntu-18.04'] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + + - name: Install dependencies (macOS) + run: brew install fftw liquid-dsp qt@5 + if: matrix.os == 'macos-latest' + + - name: Install dependencies (Ubuntu) + run: sudo apt install libfftw3-dev libliquid-dev qtbase5-dev + if: startsWith(matrix.os, 'ubuntu-') + + - name: Create Build Environment + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure CMake + shell: bash + working-directory: ${{runner.workspace}}/build + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE + + - name: Build + working-directory: ${{runner.workspace}}/build + shell: bash + run: cmake --build . --config $BUILD_TYPE + + - name: Test + working-directory: ${{runner.workspace}}/build + shell: bash + run: ctest -C $BUILD_TYPE + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ee3f9fa..0000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -language: c - -cache: apt - -sudo: required -dist: bionic - -os: - - linux - - osx - -compiler: - - gcc - - clang - -matrix: - exclude: - # /usr/bin/gcc on OS X is clang, so it's not meaningful to build against both. - - os: osx - compiler: gcc - -addons: - apt: - sources: -# - ubuntu-sdk-team - packages: - - qtbase5-dev - - qtdeclarative5-dev - - libfftw3-dev - - libliquid-dev - -before_install: - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi - # cmake and pkg-config are already installed. - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install qt5 fftw liquid-dsp; fi - -script: - - mkdir build - - cd build - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export CMAKE_PREFIX_PATH=$(brew --prefix qt5)/lib/cmake; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then DYLIB_SUFFIX=dylib; else DYLIB_SUFFIX=so; fi - - CFLAGS="-g -Wall -Wextra -Werror -Wno-zero-length-array" cmake .. - - make diff --git a/CMakeLists.txt b/CMakeLists.txt index ddb0bd9..462a682 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,89 +1,5 @@ -cmake_minimum_required(VERSION 2.8.11) +cmake_minimum_required(VERSION 3.1) project(inspectrum CXX) enable_testing() -set(CMAKE_AUTOMOC ON) -set(CMAKE_INCLUDE_CURRENT_DIR ON) -list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/Modules) - -# For OSX - don't clear RPATH on install -set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) - -if (MSVC) - #force std::complex<> typedefs in liquiddsp - add_definitions(-D_LIBCPP_COMPLEX) - - #enable math definitions in math.h - add_definitions(-D_USE_MATH_DEFINES) - - #build a graphical application without the console - option(BUILD_WIN32 "Build win32 app, false for console" TRUE) - if (BUILD_WIN32) - set(EXE_ARGS WIN32) - set(CMAKE_EXE_LINKER_FLAGS "/entry:mainCRTStartup ${CMAKE_EXE_LINKER_FLAGS}") - endif (BUILD_WIN32) -endif (MSVC) - -if (NOT CMAKE_CXX_FLAGS) - set(CMAKE_CXX_FLAGS "-O2") -endif (NOT CMAKE_CXX_FLAGS) - -# This only works in cmake >3.1 -#set_property(TARGET inspectrum PROPERTY CXX_STANDARD 11) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11") - -list(APPEND inspectrum_sources - abstractsamplesource.cpp - amplitudedemod.cpp - cursor.cpp - cursors.cpp - main.cpp - fft.cpp - frequencydemod.cpp - mainwindow.cpp - inputsource.cpp - phasedemod.cpp - plot.cpp - plots.cpp - plotview.cpp - samplebuffer.cpp - samplesource.cpp - spectrogramcontrols.cpp - spectrogramplot.cpp - threshold.cpp - traceplot.cpp - tuner.cpp - tunertransform.cpp - util.cpp -) - -find_package(Qt5Widgets REQUIRED) -find_package(Qt5Concurrent REQUIRED) -find_package(FFTW REQUIRED) -find_package(Liquid REQUIRED) - -include_directories( - ${FFTW_INCLUDES} - ${LIQUID_INCLUDES} -) - -add_executable(inspectrum ${EXE_ARGS} ${inspectrum_sources}) - -target_link_libraries(inspectrum - Qt5::Core Qt5::Widgets Qt5::Concurrent - ${FFTW_LIBRARIES} - ${LIQUID_LIBRARIES} -) -set(INSTALL_DEFAULT_BINDIR "bin" CACHE STRING "Appended to CMAKE_INSTALL_PREFIX") - -install(TARGETS inspectrum RUNTIME DESTINATION ${INSTALL_DEFAULT_BINDIR}) - -# Create uninstall target -configure_file( - ${PROJECT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake -@ONLY) - -add_custom_target(uninstall - ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake -) +add_subdirectory(src) diff --git a/README.md b/README.md index b5d439c..102ff71 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ inspectrum is a tool for analysing captured signals, primarily from software-def ## Try it ### Prerequisites - * cmake >= 2.8.11 + * cmake >= 3.1 * fftw 3.x * [liquid-dsp](https://github.com/jgaeddert/liquid-dsp) >= v1.3.0 * pkg-config * qt5 + * [libsigmf](https://github.com/deepsig/libsigmf) (optional, for SigMF support) ### Build instructions @@ -22,6 +23,7 @@ Build instructions can be found here: https://github.com/miek/inspectrum/wiki/Bu ## Input inspectrum supports the following file types: + * `*.sigmf-meta, *.sigmf-data` - SigMF recordings * `*.cf32`, `*.cfile` - Complex 32-bit floating point samples (GNURadio, osmocom_fft) * `*.cs16` - Complex 16-bit signed integer samples (BladeRF) * `*.cs8` - Complex 8-bit signed integer samples (HackRF) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..3bdffde --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,96 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/Modules) + +# For OSX - don't clear RPATH on install +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + +if (MSVC) + #force std::complex<> typedefs in liquiddsp + add_definitions(-D_LIBCPP_COMPLEX) + + #enable math definitions in math.h + add_definitions(-D_USE_MATH_DEFINES) + + #build a graphical application without the console + option(BUILD_WIN32 "Build win32 app, false for console" TRUE) + if (BUILD_WIN32) + set(EXE_ARGS WIN32) + set(CMAKE_EXE_LINKER_FLAGS "/entry:mainCRTStartup ${CMAKE_EXE_LINKER_FLAGS}") + endif (BUILD_WIN32) +endif (MSVC) + +if (NOT CMAKE_CXX_FLAGS) + set(CMAKE_CXX_FLAGS "-O2") +endif (NOT CMAKE_CXX_FLAGS) + +# This only works in cmake >3.1 +set(CMAKE_CXX_STANDARD 14) + +list(APPEND inspectrum_sources + abstractsamplesource.cpp + amplitudedemod.cpp + cursor.cpp + cursors.cpp + main.cpp + fft.cpp + frequencydemod.cpp + mainwindow.cpp + inputsource.cpp + phasedemod.cpp + plot.cpp + plots.cpp + plotview.cpp + samplebuffer.cpp + samplesource.cpp + spectrogramcontrols.cpp + spectrogramplot.cpp + threshold.cpp + traceplot.cpp + tuner.cpp + tunertransform.cpp + util.cpp +) + +find_package(Qt5Widgets REQUIRED) +find_package(Qt5Concurrent REQUIRED) +find_package(FFTW REQUIRED) +find_package(Liquid REQUIRED) +find_package(libsigmf QUIET) + +include_directories( + ${FFTW_INCLUDES} + ${LIQUID_INCLUDES} +) + +add_executable(inspectrum ${EXE_ARGS} ${inspectrum_sources}) + +target_link_libraries(inspectrum + Qt5::Core Qt5::Widgets Qt5::Concurrent + ${FFTW_LIBRARIES} + ${LIQUID_LIBRARIES} +) + +if (libsigmf_FOUND) + message("-- libsigmf found. Enabling SigMF support") + target_link_libraries(inspectrum + libsigmf::libsigmf + ) + add_definitions(-DENABLE_SIGMF) +else() + message("-- libsigmf not found. Disabling SigMF support") +endif() + +set(INSTALL_DEFAULT_BINDIR "bin" CACHE STRING "Appended to CMAKE_INSTALL_PREFIX") + +install(TARGETS inspectrum RUNTIME DESTINATION ${INSTALL_DEFAULT_BINDIR}) + +# Create uninstall target +configure_file( + ${PROJECT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake +@ONLY) + +add_custom_target(uninstall + ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake +) diff --git a/abstractsamplesource.cpp b/src/abstractsamplesource.cpp similarity index 100% rename from abstractsamplesource.cpp rename to src/abstractsamplesource.cpp diff --git a/abstractsamplesource.h b/src/abstractsamplesource.h similarity index 100% rename from abstractsamplesource.h rename to src/abstractsamplesource.h diff --git a/amplitudedemod.cpp b/src/amplitudedemod.cpp similarity index 100% rename from amplitudedemod.cpp rename to src/amplitudedemod.cpp diff --git a/amplitudedemod.h b/src/amplitudedemod.h similarity index 100% rename from amplitudedemod.h rename to src/amplitudedemod.h diff --git a/cursor.cpp b/src/cursor.cpp similarity index 100% rename from cursor.cpp rename to src/cursor.cpp diff --git a/cursor.h b/src/cursor.h similarity index 100% rename from cursor.h rename to src/cursor.h diff --git a/cursors.cpp b/src/cursors.cpp similarity index 100% rename from cursors.cpp rename to src/cursors.cpp diff --git a/cursors.h b/src/cursors.h similarity index 100% rename from cursors.h rename to src/cursors.h diff --git a/fft.cpp b/src/fft.cpp similarity index 100% rename from fft.cpp rename to src/fft.cpp diff --git a/fft.h b/src/fft.h similarity index 100% rename from fft.h rename to src/fft.h diff --git a/frequencydemod.cpp b/src/frequencydemod.cpp similarity index 100% rename from frequencydemod.cpp rename to src/frequencydemod.cpp diff --git a/frequencydemod.h b/src/frequencydemod.h similarity index 100% rename from frequencydemod.h rename to src/frequencydemod.h diff --git a/inputsource.cpp b/src/inputsource.cpp similarity index 63% rename from inputsource.cpp rename to src/inputsource.cpp index 6a7ebac..fd90020 100644 --- a/inputsource.cpp +++ b/src/inputsource.cpp @@ -29,6 +29,21 @@ #include +#if ENABLE_SIGMF +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + class ComplexF32SampleAdapter : public SampleAdapter { public: size_t sampleSize() override { @@ -181,44 +196,131 @@ void InputSource::cleanup() } } +#if ENABLE_SIGMF +void InputSource::readMetaData(const QString &filename) +{ + QFile datafile(filename); + if (!datafile.open(QFile::ReadOnly | QIODevice::Text)) { + throw std::runtime_error("Error while opening meta data file: " + datafile.errorString().toStdString()); + } + + sigmf::SigMF, + sigmf::Capture, + sigmf::Annotation > metaData = json::parse(datafile.readAll()); + + auto global_core = metaData.global.access(); + + if(global_core.datatype.compare("cf32_le") == 0) { + sampleAdapter = std::make_unique(); + } else if(global_core.datatype.compare("ci16_le") == 0) { + sampleAdapter = std::make_unique(); + } else if(global_core.datatype.compare("ci8") == 0) { + sampleAdapter = std::make_unique(); + } else if(global_core.datatype.compare("cu8") == 0) { + sampleAdapter = std::make_unique(); + } else if(global_core.datatype.compare("rf32_le") == 0) { + sampleAdapter = std::make_unique(); + _realSignal = true; + } else if(global_core.datatype.compare("ri16_le") == 0) { + sampleAdapter = std::make_unique(); + _realSignal = true; + } else if(global_core.datatype.compare("ri8") == 0) { + sampleAdapter = std::make_unique(); + _realSignal = true; + } else if(global_core.datatype.compare("ru8") == 0) { + sampleAdapter = std::make_unique(); + _realSignal = true; + } else { + throw std::runtime_error("SigMF meta data specifies unsupported datatype"); + } + + setSampleRate(global_core.sample_rate); + + for(auto capture : metaData.captures) { + auto core = capture.access(); + frequency = core.frequency; + } + + for(auto annotation : metaData.annotations) { + Annotation a; + auto core = annotation.access(); + + a.sampleRange = range_t{core.sample_start, core.sample_start + core.sample_count - 1}; + a.frequencyRange = range_t{core.freq_lower_edge, core.freq_upper_edge}; + a.description = QString::fromStdString(core.description); + + annotationList.append(a); + } +} +#endif + + void InputSource::openFile(const char *filename) { QFileInfo fileInfo(filename); std::string suffix = std::string(fileInfo.suffix().toLower().toUtf8().constData()); if(_fmt!=""){ suffix = _fmt; } // allow fmt override if ((suffix == "cfile") || (suffix == "cf32") || (suffix == "fc32")) { - sampleAdapter = std::unique_ptr(new ComplexF32SampleAdapter()); + sampleAdapter = std::make_unique(); } else if ((suffix == "cs16") || (suffix == "sc16") || (suffix == "c16")) { - sampleAdapter = std::unique_ptr(new ComplexS16SampleAdapter()); + sampleAdapter = std::make_unique(); } else if ((suffix == "cs8") || (suffix == "sc8") || (suffix == "c8")) { - sampleAdapter = std::unique_ptr(new ComplexS8SampleAdapter()); + sampleAdapter = std::make_unique(); } else if ((suffix == "cu8") || (suffix == "uc8")) { - sampleAdapter = std::unique_ptr(new ComplexU8SampleAdapter()); + sampleAdapter = std::make_unique(); } else if (suffix == "f32") { - sampleAdapter = std::unique_ptr(new RealF32SampleAdapter()); + sampleAdapter = std::make_unique(); _realSignal = true; } else if (suffix == "s16") { - sampleAdapter = std::unique_ptr(new RealS16SampleAdapter()); + sampleAdapter = std::make_unique(); _realSignal = true; } else if (suffix == "s8") { - sampleAdapter = std::unique_ptr(new RealS8SampleAdapter()); + sampleAdapter = std::make_unique(); _realSignal = true; } else if (suffix == "u8") { - sampleAdapter = std::unique_ptr(new RealU8SampleAdapter()); + sampleAdapter = std::make_unique(); _realSignal = true; } else { - sampleAdapter = std::unique_ptr(new ComplexF32SampleAdapter()); + sampleAdapter = std::make_unique(); + } + + QString dataFilename; + +#if ENABLE_SIGMF + annotationList.clear(); + QString metaFilename; + + if (suffix == "sigmf-meta") { + dataFilename = fileInfo.path() + "/" + fileInfo.completeBaseName() + ".sigmf-data"; + metaFilename = filename; + readMetaData(metaFilename); + } + else if (suffix == "sigmf-data") { + dataFilename = filename; + metaFilename = fileInfo.path() + "/" + fileInfo.completeBaseName() + ".sigmf-meta"; + readMetaData(metaFilename); + } + else if (suffix == "sigmf") { + throw std::runtime_error("SigMF archives are not supported. Consider extracting a recording."); + } +#else + if (suffix == "sigmf-meta" || suffix == "sigmf-data" || suffix == "sigmf") { + throw std::runtime_error("Support for SigMF recordings is not enabled"); + } +#endif + else { + dataFilename = filename; } - std::unique_ptr file(new QFile(filename)); + auto file = std::make_unique(dataFilename); if (!file->open(QFile::ReadOnly)) { throw std::runtime_error(file->errorString().toStdString()); } @@ -263,7 +365,7 @@ std::unique_ptr[]> InputSource::getSamples(size_t start, siz if (start + length > sampleCount) return nullptr; - std::unique_ptr[]> dest(new std::complex[length]); + auto dest = std::make_unique[]>(length); sampleAdapter->copyRange(mmapData, start, length, dest.get()); return dest; diff --git a/inputsource.h b/src/inputsource.h similarity index 95% rename from inputsource.h rename to src/inputsource.h index d6d76e0..47b457c 100644 --- a/inputsource.h +++ b/src/inputsource.h @@ -28,6 +28,7 @@ class SampleAdapter { public: virtual size_t sampleSize() = 0; virtual void copyRange(const void* const src, size_t start, size_t length, std::complex* const dest) = 0; + virtual ~SampleAdapter() { }; }; class InputSource : public SampleSource> @@ -41,6 +42,8 @@ class InputSource : public SampleSource> std::string _fmt; bool _realSignal = false; + void readMetaData(const QString &filename); + public: InputSource(); ~InputSource(); diff --git a/main.cpp b/src/main.cpp similarity index 100% rename from main.cpp rename to src/main.cpp diff --git a/mainwindow.cpp b/src/mainwindow.cpp similarity index 85% rename from mainwindow.cpp rename to src/mainwindow.cpp index b5aa3ac..f1adf7a 100644 --- a/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -37,8 +37,12 @@ MainWindow::MainWindow() addDockWidget(Qt::LeftDockWidgetArea, dock); input = new InputSource(); + input->subscribe(this); - plots = new PlotView(input); + QSettings settings; + tuner = new Tuner(settings.value("FFTSize", 9).toInt(), this); + + plots = new PlotView(input, tuner); setCentralWidget(plots); // Connect dock inputs @@ -50,6 +54,7 @@ MainWindow::MainWindow() connect(dock->cursorsCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableCursors); connect(dock->scalesCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableScales); connect(dock->cursorSymbolsSpinBox, static_cast(&QSpinBox::valueChanged), plots, &PlotView::setCursorSegments); + connect(tuner, &Tuner::tunerMoved, dock, &SpectrogramControls::tunerMoved); // Connect dock outputs connect(plots, &PlotView::timeSelectionChanged, dock, &SpectrogramControls::timeSelectionChanged); @@ -96,6 +101,19 @@ void MainWindow::openFile(QString fileName) } } +void MainWindow::invalidateEvent() +{ + plots->setSampleRate(input->rate()); + + // Only update the text box if it is not already representing + // the current value. Otherwise the cursor might jump or the + // representation might change (e.g. to scientific). + double currentValue = dock->sampleRate->text().toDouble(); + if(QString::number(input->rate()) != QString::number(currentValue)) { + setSampleRate(input->rate()); + } +} + void MainWindow::setSampleRate(QString rate) { auto sampleRate = rate.toDouble(); diff --git a/mainwindow.h b/src/mainwindow.h similarity index 92% rename from mainwindow.h rename to src/mainwindow.h index 187400c..c7a45a6 100644 --- a/mainwindow.h +++ b/src/mainwindow.h @@ -24,7 +24,7 @@ #include "spectrogramcontrols.h" #include "plotview.h" -class MainWindow : public QMainWindow +class MainWindow : public QMainWindow, Subscriber { Q_OBJECT @@ -37,9 +37,11 @@ public slots: void setSampleRate(QString rate); void setSampleRate(double rate); void setFormat(QString fmt); + void invalidateEvent() override; private: SpectrogramControls *dock; PlotView *plots; InputSource *input; + Tuner *tuner; }; diff --git a/phasedemod.cpp b/src/phasedemod.cpp similarity index 100% rename from phasedemod.cpp rename to src/phasedemod.cpp diff --git a/phasedemod.h b/src/phasedemod.h similarity index 100% rename from phasedemod.h rename to src/phasedemod.h diff --git a/plot.cpp b/src/plot.cpp similarity index 100% rename from plot.cpp rename to src/plot.cpp diff --git a/plot.h b/src/plot.h similarity index 100% rename from plot.h rename to src/plot.h diff --git a/plots.cpp b/src/plots.cpp similarity index 100% rename from plots.cpp rename to src/plots.cpp diff --git a/plots.h b/src/plots.h similarity index 100% rename from plots.h rename to src/plots.h diff --git a/plotview.cpp b/src/plotview.cpp similarity index 98% rename from plotview.cpp rename to src/plotview.cpp index 5db1b2a..d74496a 100644 --- a/plotview.cpp +++ b/src/plotview.cpp @@ -34,7 +34,7 @@ #include #include "plots.h" -PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0}) +PlotView::PlotView(InputSource *input, Tuner *tuner) : cursors(this), viewRange({0, 0}) { mainSampleSource = input; setDragMode(QGraphicsView::ScrollHandDrag); @@ -43,9 +43,11 @@ PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0}) enableCursors(false); connect(&cursors, &Cursors::cursorsMoved, this, &PlotView::cursorsMoved); - spectrogramPlot = new SpectrogramPlot(std::shared_ptr>>(mainSampleSource)); + spectrogramPlot = new SpectrogramPlot(std::shared_ptr>>(mainSampleSource), tuner); auto tunerOutput = std::dynamic_pointer_cast>>(spectrogramPlot->output()); + connect(tuner, &Tuner::tunerMoved, spectrogramPlot, &SpectrogramPlot::tunerMoved); + enableScales(true); addPlot(spectrogramPlot); @@ -532,6 +534,11 @@ void PlotView::scrollContentsBy(int dx, int dy) updateView(); } +void PlotView::showEvent(QShowEvent *event) +{ + // Intentionally left blank. See #171 +} + void PlotView::updateViewRange(bool reCenter) { // Update current view diff --git a/plotview.h b/src/plotview.h similarity index 96% rename from plotview.h rename to src/plotview.h index d558936..528add4 100644 --- a/plotview.h +++ b/src/plotview.h @@ -34,7 +34,7 @@ class PlotView : public QGraphicsView, Subscriber Q_OBJECT public: - PlotView(InputSource *input); + PlotView(InputSource *input, Tuner *tuner); void setSampleRate(double rate); signals: @@ -59,6 +59,7 @@ public slots: void resizeEvent(QResizeEvent * event) override; void scrollContentsBy(int dx, int dy) override; bool viewportEvent(QEvent *event) override; + void showEvent(QShowEvent *event) override; private: Cursors cursors; diff --git a/samplebuffer.cpp b/src/samplebuffer.cpp similarity index 94% rename from samplebuffer.cpp rename to src/samplebuffer.cpp index f008a3c..4fb0e09 100644 --- a/samplebuffer.cpp +++ b/src/samplebuffer.cpp @@ -42,8 +42,8 @@ std::unique_ptr SampleBuffer::getSamples(size_t start, size_t if (samples == nullptr) return nullptr; - std::unique_ptr temp(new Tout[history + length]); - std::unique_ptr dest(new Tout[length]); + auto temp = std::make_unique(history + length); + auto dest = std::make_unique(length); QMutexLocker ml(&mutex); work(samples.get(), temp.get(), history + length, start); memcpy(dest.get(), temp.get() + history, length * sizeof(Tout)); diff --git a/samplebuffer.h b/src/samplebuffer.h similarity index 100% rename from samplebuffer.h rename to src/samplebuffer.h diff --git a/samplesource.cpp b/src/samplesource.cpp similarity index 91% rename from samplesource.cpp rename to src/samplesource.cpp index 619a2cd..3d6eaf0 100644 --- a/samplesource.cpp +++ b/src/samplesource.cpp @@ -25,5 +25,11 @@ std::type_index SampleSource::sampleType() return typeid(T); } +template +double SampleSource::getFrequency() +{ + return frequency; +} + template class SampleSource>; template class SampleSource; diff --git a/samplesource.h b/src/samplesource.h similarity index 82% rename from samplesource.h rename to src/samplesource.h index ed0c0e3..9995eb4 100644 --- a/samplesource.h +++ b/src/samplesource.h @@ -23,9 +23,23 @@ #include #include "abstractsamplesource.h" +#include "util.h" +#include +#include + +class Annotation +{ +public: + range_t sampleRange; + range_t frequencyRange; + QString description; +}; + template class SampleSource : public AbstractSampleSource { +protected: + double frequency; public: virtual ~SampleSource() {}; @@ -35,6 +49,8 @@ class SampleSource : public AbstractSampleSource virtual size_t count() = 0; virtual double rate() = 0; virtual float relativeBandwidth() = 0; + QList annotationList; std::type_index sampleType() override; virtual bool realSignal() { return false; }; + double getFrequency(); }; diff --git a/spectrogramcontrols.cpp b/src/spectrogramcontrols.cpp similarity index 93% rename from spectrogramcontrols.cpp rename to src/spectrogramcontrols.cpp index 08007d9..6d6e7f8 100644 --- a/spectrogramcontrols.cpp +++ b/src/spectrogramcontrols.cpp @@ -93,6 +93,9 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent symbolPeriodLabel = new QLabel(); layout->addRow(new QLabel(tr("Symbol period:")), symbolPeriodLabel); + bandwidthLabel = new QLabel(); + layout->addRow(new QLabel(tr("Bandwidth:")), bandwidthLabel); + widget->setLayout(layout); setWidget(widget); @@ -110,6 +113,7 @@ void SpectrogramControls::clearCursorLabels() rateLabel->setText(""); symbolPeriodLabel->setText(""); symbolRateLabel->setText(""); + bandwidthLabel->setText(""); } void SpectrogramControls::cursorsStateChanged(int state) @@ -226,3 +230,16 @@ void SpectrogramControls::zoomOut() { zoomLevelSlider->setValue(zoomLevelSlider->value() - 1); } + +void SpectrogramControls::tunerMoved(int deviation) +{ + // int deviation is width in pixels from plot + bandwidthLabel->setText(QString::number(getBandwidth(deviation)) + "kHz"); +} + +int SpectrogramControls::getBandwidth(int deviation) { + double rate = sampleRate->text().toDouble(); + double fftSize = pow(2, fftSizeSlider->value()); + double hzPerPx = rate / fftSize; + return deviation * hzPerPx / 1000 * 2; +} \ No newline at end of file diff --git a/spectrogramcontrols.h b/src/spectrogramcontrols.h similarity index 94% rename from spectrogramcontrols.h rename to src/spectrogramcontrols.h index abe06d6..f1a1255 100644 --- a/spectrogramcontrols.h +++ b/src/spectrogramcontrols.h @@ -27,6 +27,7 @@ #include #include #include +#include "tuner.h" class SpectrogramControls : public QDockWidget { @@ -44,6 +45,7 @@ public slots: void timeSelectionChanged(float time); void zoomIn(); void zoomOut(); + void tunerMoved(int deviation); private slots: void fftSizeChanged(int value); @@ -58,6 +60,7 @@ private slots: QFormLayout *layout; void clearCursorLabels(); void fftOrZoomChanged(void); + int getBandwidth(int deviation); public: QPushButton *fileOpenButton; @@ -72,5 +75,7 @@ private slots: QLabel *periodLabel; QLabel *symbolRateLabel; QLabel *symbolPeriodLabel; + QLabel *bandwidthLabel; QCheckBox *scalesCheckBox; + }; diff --git a/spectrogramplot.cpp b/src/spectrogramplot.cpp similarity index 73% rename from spectrogramplot.cpp rename to src/spectrogramplot.cpp index 975c69b..ba719ef 100644 --- a/spectrogramplot.cpp +++ b/src/spectrogramplot.cpp @@ -31,8 +31,10 @@ #include "util.h" -SpectrogramPlot::SpectrogramPlot(std::shared_ptr>> src) : Plot(src), inputSource(src), fftSize(512), tuner(fftSize, this) +SpectrogramPlot::SpectrogramPlot(std::shared_ptr>> src, Tuner *tuner) + : Plot(src), inputSource(src), fftSize(512) //, tuner(fftSize, this) { + this->tuner = tuner; setFFTSize(fftSize); zoomLevel = 1; powerMax = 0.0f; @@ -46,7 +48,7 @@ SpectrogramPlot::SpectrogramPlot(std::shared_ptr(src); - connect(&tuner, &Tuner::tunerMoved, this, &SpectrogramPlot::tunerMoved); +// connect(&tuner, &Tuner::tunerMoved, this, &SpectrogramPlot::tunerMoved); } void SpectrogramPlot::invalidateEvent() @@ -62,14 +64,23 @@ void SpectrogramPlot::invalidateEvent() void SpectrogramPlot::paintFront(QPainter &painter, QRect &rect, range_t sampleRange) { if (tunerEnabled()) - tuner.paintFront(painter, rect, sampleRange); + tuner->paintFront(painter, rect, sampleRange); if (frequencyScaleEnabled) paintFrequencyScale(painter, rect); + paintAnnotations(painter, rect, sampleRange); } void SpectrogramPlot::paintFrequencyScale(QPainter &painter, QRect &rect) { + if (sampleRate == 0) { + return; + } + + if (sampleRate / 2 > UINT64_MAX) { + return; + } + // At which pixel is F_+sampleRate/2 int y = rect.y(); @@ -80,7 +91,7 @@ void SpectrogramPlot::paintFrequencyScale(QPainter &painter, QRect &rect) double bwPerPixel = (double)sampleRate / plotHeight; int tickHeight = 50; - int bwPerTick = 10 * pow(10, floor(log(bwPerPixel * tickHeight) / log(10))); + uint64_t bwPerTick = 10 * pow(10, floor(log(bwPerPixel * tickHeight) / log(10))); if (bwPerTick < 1) { return; @@ -93,7 +104,7 @@ void SpectrogramPlot::paintFrequencyScale(QPainter &painter, QRect &rect) QFontMetrics fm(painter.font()); - int tick = 0; + uint64_t tick = 0; while (tick <= sampleRate / 2) { @@ -107,12 +118,14 @@ void SpectrogramPlot::paintFrequencyScale(QPainter &painter, QRect &rect) if (tick != 0) { char buf[128]; - if (bwPerTick % 1000000 == 0) { - snprintf(buf, sizeof(buf), "-%d MHz", (int)tick / 1000000); + 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), "-%d kHz", tick / 1000); + snprintf(buf, sizeof(buf), "-%lu kHz", tick / 1000); } else { - snprintf(buf, sizeof(buf), "-%d Hz", tick); + snprintf(buf, sizeof(buf), "-%lu Hz", tick); } if (!inputSource->realSignal()) @@ -145,6 +158,48 @@ void SpectrogramPlot::paintFrequencyScale(QPainter &painter, QRect &rect) painter.restore(); } +void SpectrogramPlot::paintAnnotations(QPainter &painter, QRect &rect, range_t sampleRange) +{ + // Pixel (from the top) at which 0 Hz sits + int zero = rect.y() + rect.height() / 2; + + painter.save(); + QPen pen(Qt::white, 1, Qt::SolidLine); + painter.setPen(pen); + QFontMetrics fm(painter.font()); + + for (int i = 0; i < inputSource->annotationList.size(); i++) { + Annotation a = inputSource->annotationList.at(i); + + size_t descriptionLength = fm.boundingRect(a.description).width() * getStride(); + + // Check if: + // (1) End of annotation (might be maximum, or end of description text) is still visible in time + // (2) Part of the annotation is already visible in time + // + // Currently there is no check if the annotation is visible in frequency. This is a + // possible performance improvement + // + size_t start = a.sampleRange.minimum; + size_t end = std::max(a.sampleRange.minimum + descriptionLength, a.sampleRange.maximum); + + if(start <= sampleRange.maximum && end >= sampleRange.minimum) { + + double frequency = a.frequencyRange.maximum - inputSource->getFrequency(); + int x = (a.sampleRange.minimum - sampleRange.minimum) / getStride(); + int y = zero - frequency / sampleRate * rect.height(); + int height = (a.frequencyRange.maximum - a.frequencyRange.minimum) / sampleRate * rect.height(); + int width = (a.sampleRange.maximum - a.sampleRange.minimum) / getStride(); + + // Draw the description 2 pixels above the box + painter.drawText(x, y - 2, a.description); + painter.drawRect(x, y, width, height); + } + } + + painter.restore(); +} + void SpectrogramPlot::paintMid(QPainter &painter, QRect &rect, range_t sampleRange) { if (!inputSource || inputSource->count() == 0) @@ -248,20 +303,20 @@ int SpectrogramPlot::getStride() float SpectrogramPlot::getTunerPhaseInc() { - auto freq = 0.5f - tuner.centre() / (float)fftSize; + auto freq = 0.5f - tuner->centre() / (float)fftSize; return freq * Tau; } std::vector SpectrogramPlot::getTunerTaps() { - float cutoff = tuner.deviation() / (float)fftSize; + float cutoff = tuner->deviation() / (float)fftSize; float gain = pow(10.0f, powerMax / -10.0f); auto atten = 60.0f; auto len = estimate_req_filter_len(std::min(cutoff, 0.05f), atten); auto taps = std::vector(len); liquid_firdes_kaiser(len, cutoff, atten, 0.0f, taps.data()); std::transform(taps.begin(), taps.end(), taps.begin(), - std::bind1st(std::multiplies(), gain)); + std::bind(std::multiplies(), std::placeholders::_1, gain)); return taps; } @@ -273,7 +328,7 @@ int SpectrogramPlot::linesPerTile() bool SpectrogramPlot::mouseEvent(QEvent::Type type, QMouseEvent event) { if (tunerEnabled()) - return tuner.mouseEvent(type, event); + return tuner->mouseEvent(type, event); return false; } @@ -299,18 +354,18 @@ void SpectrogramPlot::setFFTSize(int size) } else { setHeight(fftSize); } - auto dev = tuner.deviation(); - auto centre = tuner.centre(); - tuner.setHeight(height()); - tuner.setDeviation( dev * sizeScale ); - tuner.setCentre( centre * sizeScale ); + auto dev = tuner->deviation(); + auto centre = tuner->centre(); + tuner->setHeight(height()); + tuner->setDeviation( dev * sizeScale ); + tuner->setCentre( centre * sizeScale ); } void SpectrogramPlot::setPowerMax(int power) { powerMax = power; pixmapCache.clear(); - tunerMoved(); + tunerMoved(666); } void SpectrogramPlot::setPowerMin(int power) @@ -324,7 +379,7 @@ void SpectrogramPlot::setZoomLevel(int zoom) zoomLevel = zoom; } -void SpectrogramPlot::setSampleRate(size_t rate) +void SpectrogramPlot::setSampleRate(double rate) { sampleRate = rate; } @@ -339,11 +394,11 @@ bool SpectrogramPlot::tunerEnabled() return (tunerTransform->subscriberCount() > 0); } -void SpectrogramPlot::tunerMoved() +void SpectrogramPlot::tunerMoved(int deviation) { tunerTransform->setFrequency(getTunerPhaseInc()); tunerTransform->setTaps(getTunerTaps()); - tunerTransform->setRelativeBandwith(tuner.deviation() * 2.0 / height()); + tunerTransform->setRelativeBandwith(deviation * 2.0 / height()); // TODO: for invalidating traceplot cache, this shouldn't really go here QPixmapCache::clear(); diff --git a/spectrogramplot.h b/src/spectrogramplot.h similarity index 92% rename from spectrogramplot.h rename to src/spectrogramplot.h index ffe1d6a..af4b794 100644 --- a/spectrogramplot.h +++ b/src/spectrogramplot.h @@ -38,14 +38,14 @@ class SpectrogramPlot : public Plot Q_OBJECT public: - SpectrogramPlot(std::shared_ptr>> src); + SpectrogramPlot(std::shared_ptr>> src, Tuner *tuner); void invalidateEvent() override; std::shared_ptr output() override; void paintFront(QPainter &painter, QRect &rect, range_t sampleRange) override; void paintMid(QPainter &painter, QRect &rect, range_t sampleRange) override; bool mouseEvent(QEvent::Type type, QMouseEvent event) override; std::shared_ptr>> input() { return inputSource; }; - void setSampleRate(size_t sampleRate); + void setSampleRate(double sampleRate); bool tunerEnabled(); void enableScales(bool enabled); @@ -54,12 +54,13 @@ public slots: void setPowerMax(int power); void setPowerMin(int power); void setZoomLevel(int zoom); - void tunerMoved(); + void tunerMoved(int deviation); private: const int linesPerGraduation = 50; static const int tileSize = 65536; // This must be a multiple of the maximum FFT size + Tuner *tuner; std::shared_ptr>> inputSource; std::unique_ptr fft; std::unique_ptr window; @@ -71,10 +72,9 @@ public slots: int zoomLevel; float powerMax; float powerMin; - size_t sampleRate; + double sampleRate; bool frequencyScaleEnabled; - Tuner tuner; std::shared_ptr tunerTransform; QPixmap* getPixmapTile(size_t tile); @@ -85,6 +85,7 @@ public slots: std::vector getTunerTaps(); int linesPerTile(); void paintFrequencyScale(QPainter &painter, QRect &rect); + void paintAnnotations(QPainter &painter, QRect &rect, range_t sampleRange); }; class TileCacheKey diff --git a/subscriber.h b/src/subscriber.h similarity index 100% rename from subscriber.h rename to src/subscriber.h diff --git a/threshold.cpp b/src/threshold.cpp similarity index 100% rename from threshold.cpp rename to src/threshold.cpp diff --git a/threshold.h b/src/threshold.h similarity index 100% rename from threshold.h rename to src/threshold.h diff --git a/traceplot.cpp b/src/traceplot.cpp similarity index 100% rename from traceplot.cpp rename to src/traceplot.cpp diff --git a/traceplot.h b/src/traceplot.h similarity index 100% rename from traceplot.h rename to src/traceplot.h diff --git a/tuner.cpp b/src/tuner.cpp similarity index 98% rename from tuner.cpp rename to src/tuner.cpp index 6ccac61..c6a5b46 100644 --- a/tuner.cpp +++ b/src/tuner.cpp @@ -109,6 +109,7 @@ void Tuner::setDeviation(int dev) { _deviation = std::max(1, dev); updateCursors(); + emit tunerMoved(_deviation); } void Tuner::setHeight(int height) @@ -120,5 +121,5 @@ void Tuner::updateCursors() { minCursor->setPos(cfCursor->pos() - _deviation); maxCursor->setPos(cfCursor->pos() + _deviation); - emit tunerMoved(); + emit tunerMoved(_deviation); } diff --git a/tuner.h b/src/tuner.h similarity index 97% rename from tuner.h rename to src/tuner.h index 958d9a7..d761baa 100644 --- a/tuner.h +++ b/src/tuner.h @@ -44,7 +44,7 @@ public slots: void cursorMoved(); signals: - void tunerMoved(); + void tunerMoved(int deviation); private: void updateCursors(); diff --git a/tunertransform.cpp b/src/tunertransform.cpp similarity index 96% rename from tunertransform.cpp rename to src/tunertransform.cpp index 11fed07..fc89a36 100644 --- a/tunertransform.cpp +++ b/src/tunertransform.cpp @@ -29,7 +29,7 @@ TunerTransform::TunerTransform(std::shared_ptr> void TunerTransform::work(void *input, void *output, int count, size_t sampleid) { auto out = static_cast*>(output); - std::unique_ptr[]> temp(new std::complex[count]); + auto temp = std::make_unique[]>(count); // Mix down nco_crcf mix = nco_crcf_create(LIQUID_NCO); diff --git a/tunertransform.h b/src/tunertransform.h similarity index 100% rename from tunertransform.h rename to src/tunertransform.h diff --git a/util.cpp b/src/util.cpp similarity index 100% rename from util.cpp rename to src/util.cpp diff --git a/util.h b/src/util.h similarity index 99% rename from util.h rename to src/util.h index 0e22e9d..157d401 100644 --- a/util.h +++ b/src/util.h @@ -57,6 +57,7 @@ struct range_t { range_t& operator=(const range_t &other) { minimum = other.minimum; maximum = other.maximum; + return *this; } range_t& operator=(const std::initializer_list &other) {