diff --git a/CMakeLists.txt b/CMakeLists.txt index 74d60dc..ca6c214 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,12 +10,12 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/Modules) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) if (WIN32) - find_library (MMAN mman) - if(NOT(MMAN)) - message(FATAL_ERROR "please install mman-win32") - else(NOT(MMAN)) - set (extraLibs ${extraLibs} ${MMAN}) - endif(NOT(MMAN)) + find_library (MMAN mman) + if(NOT(MMAN)) + message(FATAL_ERROR "please install mman-win32") + else(NOT(MMAN)) + set (extraLibs ${extraLibs} ${MMAN}) + endif(NOT(MMAN)) ENDIF (WIN32) if (NOT CMAKE_CXX_FLAGS) @@ -27,32 +27,52 @@ endif (NOT CMAKE_CXX_FLAGS) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11") list(APPEND inspectrum_sources - main.cpp - fft.cpp - mainwindow.cpp - inputsource.cpp - spectrogram.cpp - spectrogramcontrols.cpp + cursors.cpp + main.cpp + fft.cpp + mainwindow.cpp + grsamplebuffer.cpp + inputsource.cpp + memory_sink_impl.cc + memory_source_impl.cc + plot.cpp + plotview.cpp + samplebuffer.cpp + samplesource.cpp + spectrogramcontrols.cpp + spectrogramplot.cpp + traceplot.cpp ) INCLUDE(FindPkgConfig) + find_package(Qt5Widgets REQUIRED) +find_package(Boost COMPONENTS system program_options REQUIRED) +set(GR_REQUIRED_COMPONENTS RUNTIME ANALOG BLOCKS FILTER) +find_package(Gnuradio REQUIRED) pkg_check_modules(FFTW REQUIRED fftw3f) include_directories( - ${QT_INCLUDES} - ${FFTW_INCLUDEDIR} - ${FFTW_INCLUDE_DIRS} + ${GNURADIO_RUNTIME_INCLUDE_DIRS} + ${QT_INCLUDES} + ${FFTW_INCLUDEDIR} + ${FFTW_INCLUDE_DIRS} ) link_directories( - ${FFTW_LIBRARY_DIRS} + ${FFTW_LIBRARY_DIRS} ) add_executable(inspectrum ${inspectrum_sources}) qt5_use_modules(inspectrum Widgets) -target_link_libraries(inspectrum ${QT_LIBRARIES} ${FFTW_LIBRARIES} ${extraLibs}) +target_link_libraries(inspectrum + ${Boost_LIBRARIES} + ${GNURADIO_ALL_LIBRARIES} + ${QT_LIBRARIES} + ${FFTW_LIBRARIES} + ${extraLibs} +) set(INSTALL_DEFAULT_BINDIR "bin" CACHE STRING "Appended to CMAKE_INSTALL_PREFIX") install(TARGETS inspectrum RUNTIME DESTINATION ${INSTALL_DEFAULT_BINDIR}) diff --git a/README.md b/README.md index 3af7f91..e398fe9 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ inspectrum is a tool for analysing captured signals, primarily from software-def ## Try it ### Prerequisites + * boost >=1.35 + * gnuradio 3.7.x * qt5 * fftw 3.x * cmake diff --git a/abstractsamplesource.h b/abstractsamplesource.h new file mode 100644 index 0000000..f8fa1c6 --- /dev/null +++ b/abstractsamplesource.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +class AbstractSampleSource +{ + +public: + virtual ~AbstractSampleSource() {}; +}; diff --git a/cursors.cpp b/cursors.cpp new file mode 100644 index 0000000..9af4ad8 --- /dev/null +++ b/cursors.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2016, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include "cursors.h" + +Cursors::Cursors(QObject * parent) : QObject::QObject(parent) +{ + +} + +// Return true if point is over a cursor, put cursor ID in `cursor` +bool Cursors::pointOverCursor(QPoint point, int &cursor) +{ + int margin = 5; + for (int i = 0; i < 2; i++) { + range_t range = {cursorPositions[i] - margin, cursorPositions[i] + margin}; + if (range.contains(point.x())) { + cursor = i; + return true; + } + } + return false; +} + +bool Cursors::eventFilter(QObject *obj, QEvent *event) +{ + // Start dragging on left mouse button press, if over a cursor + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent *mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { + if (pointOverCursor(mouseEvent->pos(), selectedCursor)) { + dragging = true; + return true; + } + } + + // Update current cursor positon if we're dragging + } else if (event->type() == QEvent::MouseMove) { + QMouseEvent *mouseEvent = static_cast(event); + if (dragging) { + cursorPositions[selectedCursor] = mouseEvent->pos().x(); + emit cursorsMoved(); + } + + // Stop dragging on left mouse button release + } else if (event->type() == QEvent::MouseButtonRelease) { + QMouseEvent *mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { + dragging = false; + return true; + } + } + return false; +} + +void Cursors::paintFront(QPainter &painter, QRect &rect, range_t sampleRange) +{ + painter.save(); + + QRect cursorRect(cursorPositions[0], rect.top(), cursorPositions[1] - cursorPositions[0], rect.height()); + // Draw translucent white fill for highlight + painter.fillRect( + cursorRect, + QBrush(QColor(255, 255, 255, 50)) + ); + + // Draw vertical edges for individual bits + painter.setPen(QPen(Qt::gray, 1, Qt::DashLine)); + for (int i = 1; i < bitCount; i++) { + int pos = cursorPositions[0] + (i * cursorRect.width() / bitCount); + painter.drawLine(pos, rect.top(), pos, rect.bottom()); + } + + // Draw vertical edges + painter.setPen(QPen(Qt::white, 1, Qt::SolidLine)); + painter.drawLine(cursorPositions[0], rect.top(), cursorPositions[0], rect.bottom()); + painter.drawLine(cursorPositions[1], rect.top(), cursorPositions[1], rect.bottom()); + + painter.restore(); +} + +range_t Cursors::selection() +{ + // TODO: ensure correct ordering during dragging, not here + if (cursorPositions[0] < cursorPositions[1]) { + return {cursorPositions[0], cursorPositions[1]}; + + } else { + return {cursorPositions[1], cursorPositions[0]}; + } +} + +void Cursors::setBits(int bits) +{ + bitCount = std::max(bits, 1); + +} + +void Cursors::setSelection(range_t selection) +{ + cursorPositions[0] = selection.minimum; + cursorPositions[1] = selection.maximum; + emit cursorsMoved(); +} diff --git a/cursors.h b/cursors.h new file mode 100644 index 0000000..9c6cedc --- /dev/null +++ b/cursors.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include "util.h" + +class Cursors : public QObject +{ + Q_OBJECT + +public: + Cursors(QObject * parent); + void paintFront(QPainter &painter, QRect &rect, range_t sampleRange); + range_t selection(); + void setBits(int bits); + void setSelection(range_t selection); + +signals: + void cursorsMoved(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +private: + bool pointOverCursor(QPoint point, int &cursor); + + int bitCount = 1; + bool dragging = false; + int selectedCursor = 0; + int cursorPositions[2] = {0, 50}; + +}; diff --git a/fft.h b/fft.h index d30c456..adec9a7 100644 --- a/fft.h +++ b/fft.h @@ -21,12 +21,15 @@ #include -class FFT { +class FFT +{ public: FFT(int size); ~FFT(); void process(void *dest, void *source); - int getSize() { return fftSize; } + int getSize() { + return fftSize; + } private: int fftSize; diff --git a/grsamplebuffer.cpp b/grsamplebuffer.cpp new file mode 100644 index 0000000..5bddce1 --- /dev/null +++ b/grsamplebuffer.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "grsamplebuffer.h" + +template +void GRSampleBuffer::work(void *input, void *output, int length) +{ + mem_source->set_source(input, length); + mem_sink->set_sink(output, length); + tb->run(); +} + +template class GRSampleBuffer, std::complex>; +template class GRSampleBuffer, float>; diff --git a/grsamplebuffer.h b/grsamplebuffer.h new file mode 100644 index 0000000..585202f --- /dev/null +++ b/grsamplebuffer.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include "memory_sink.h" +#include "memory_source.h" + +#include "samplebuffer.h" + +template +class GRSampleBuffer : public SampleBuffer +{ +private: + gr::top_block_sptr tb; + gr::blocks::memory_source::sptr mem_source; + gr::blocks::memory_sink::sptr mem_sink; + +public: + GRSampleBuffer(SampleSource *src, gr::top_block_sptr tb, gr::blocks::memory_source::sptr mem_source, gr::blocks::memory_sink::sptr mem_sink) + : SampleBuffer(src), tb(tb), mem_source(mem_source), mem_sink(mem_sink) {}; + virtual void work(void *input, void *output, int count); +}; diff --git a/inputsource.cpp b/inputsource.cpp index ca03fd3..664f8cc 100644 --- a/inputsource.cpp +++ b/inputsource.cpp @@ -27,35 +27,80 @@ #include -InputSource::InputSource(const char *filename) { - m_file = fopen(filename, "rb"); - if (m_file == nullptr) +InputSource::InputSource() +{ +} + +InputSource::~InputSource() +{ + cleanup(); +} + +void InputSource::cleanup() +{ + if (mmapData != nullptr) { + munmap(mmapData, fileSize); + mmapData = nullptr; + fileSize = 0; + } + + if (inputFile != nullptr) { + fclose(inputFile); + inputFile = nullptr; + } +} + +void InputSource::openFile(const char *filename) +{ + FILE *file = fopen(filename, "rb"); + if (file == nullptr) throw std::runtime_error("Error opening file"); struct stat sb; - if (fstat(fileno(m_file), &sb) != 0) + if (fstat(fileno(file), &sb) != 0) throw std::runtime_error("Error fstating file"); - m_file_size = sb.st_size; - sampleCount = m_file_size / sizeof(fftwf_complex); + off_t size = sb.st_size; + sampleCount = size / sizeof(std::complex); - m_data = (fftwf_complex*)mmap(NULL, m_file_size, PROT_READ, MAP_SHARED, fileno(m_file), 0); - if (m_data == 0) + auto data = (std::complex*)mmap(NULL, size, PROT_READ, MAP_SHARED, fileno(file), 0); + if (data == nullptr) throw std::runtime_error("Error mmapping file"); + + cleanup(); + + inputFile = file; + fileSize = size; + mmapData = data; + + invalidate(); } -InputSource::~InputSource() { - munmap(m_data, m_file_size); - fclose(m_file); -} - -bool InputSource::getSamples(fftwf_complex *dest, off_t start, int length) +void InputSource::setSampleRate(off_t rate) { + sampleRate = rate; + invalidate(); +} + +off_t InputSource::rate() +{ + return sampleRate; +} + +std::unique_ptr[]> InputSource::getSamples(off_t start, off_t length) +{ + if (inputFile == nullptr) + return nullptr; + + if (mmapData == nullptr) + return nullptr; + if(start < 0 || length < 0) - return false; + return nullptr; if (start + length >= sampleCount) - return false; + return nullptr; - memcpy(dest, &m_data[start], length * sizeof(fftwf_complex)); - return true; + std::unique_ptr[]> dest(new std::complex[length]); + memcpy(dest.get(), &mmapData[start], length * sizeof(std::complex)); + return dest; } diff --git a/inputsource.h b/inputsource.h index ad310f2..e991cef 100644 --- a/inputsource.h +++ b/inputsource.h @@ -19,22 +19,27 @@ #pragma once -#include "fft.h" -#include -#include +#include +#include "samplesource.h" -class InputSource +class InputSource : public SampleSource> { private: - FILE *m_file; - off_t m_file_size; - off_t sampleCount; - fftwf_complex *m_data; + FILE *inputFile = nullptr; + off_t fileSize = 0; + off_t sampleCount = 0; + off_t sampleRate = 0; + std::complex *mmapData = nullptr; public: - InputSource(const char *filename); + InputSource(); ~InputSource(); - - bool getSamples(fftwf_complex *dest, off_t start, int length); - off_t getSampleCount() { return sampleCount; }; + void cleanup(); + void openFile(const char *filename); + std::unique_ptr[]> getSamples(off_t start, off_t length); + off_t count() { + return sampleCount; + }; + void setSampleRate(off_t rate); + off_t rate(); }; diff --git a/main.cpp b/main.cpp index c922599..701e197 100644 --- a/main.cpp +++ b/main.cpp @@ -35,27 +35,27 @@ int main(int argc, char *argv[]) // Add options QCommandLineOption rateOption(QStringList() << "r" << "rate", - QCoreApplication::translate("main", "Set sample rate."), - QCoreApplication::translate("main", "Hz")); + QCoreApplication::translate("main", "Set sample rate."), + QCoreApplication::translate("main", "Hz")); parser.addOption(rateOption); // Process the actual command line parser.process(a); - if (parser.isSet(rateOption)){ - bool ok; - // Use toDouble just for scientific notation support - int rate = parser.value(rateOption).toDouble(&ok); - if(!ok){ - fputs("ERROR: could not parse rate\n", stderr); - return 1; - } - mainWin.changeSampleRate(rate); + if (parser.isSet(rateOption)) { + bool ok; + // Use toDouble just for scientific notation support + int rate = parser.value(rateOption).toDouble(&ok); + if(!ok) { + fputs("ERROR: could not parse rate\n", stderr); + return 1; + } + mainWin.setSampleRate(rate); } const QStringList args = parser.positionalArguments(); if (args.size()>=1) - mainWin.openFile(args.at(0)); + mainWin.openFile(args.at(0)); mainWin.show(); return a.exec(); diff --git a/mainwindow.cpp b/mainwindow.cpp index 3d185d0..b29574c 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -18,96 +18,53 @@ */ #include +#include #include "mainwindow.h" +#include "util.h" MainWindow::MainWindow() { setWindowTitle(tr("inspectrum")); - scrollArea.setWidget(&spectrogram); - scrollArea.viewport()->installEventFilter(this); - setCentralWidget(&scrollArea); dock = new SpectrogramControls(tr("Controls"), this); dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); addDockWidget(Qt::LeftDockWidgetArea, dock); + input = new InputSource(); + + plots = new PlotView(input); + setCentralWidget(plots); + + // Connect dock inputs connect(dock, SIGNAL(openFile(QString)), this, SLOT(openFile(QString))); connect(dock->sampleRate, SIGNAL(textChanged(QString)), this, SLOT(setSampleRate(QString))); - connect(dock, SIGNAL(fftSizeChanged(int)), this, SLOT(setFFTSize(int))); - connect(dock->zoomLevelSlider, SIGNAL(valueChanged(int)), this, SLOT(setZoomLevel(int))); - connect(dock->powerMaxSlider, SIGNAL(valueChanged(int)), &spectrogram, SLOT(setPowerMax(int))); - connect(dock->powerMinSlider, SIGNAL(valueChanged(int)), &spectrogram, SLOT(setPowerMin(int))); - connect(dock->timeScaleCheckBox, SIGNAL(stateChanged(int)), &spectrogram, SLOT(setTimeScaleEnable(int))); - connect(&spectrogram, SIGNAL(cursorFrequencyChanged(QString)), dock->cursorFrequencyLabel, SLOT(setText(QString))); - connect(&spectrogram, SIGNAL(cursorTimeChanged(QString)), dock->cursorTimeLabel, SLOT(setText(QString))); - connect(dock->deltaDragCheckBox, SIGNAL(stateChanged(int)), &spectrogram, SLOT(setDeltaDragEnable(int))); - connect(&spectrogram, SIGNAL(deltaFrequencyChanged(QString)), dock->deltaFrequencyLabel, SLOT(setText(QString))); - connect(&spectrogram, SIGNAL(deltaTimeChanged(QString)), dock->deltaTimeLabel, SLOT(setText(QString))); -} + connect(dock, SIGNAL(fftOrZoomChanged(int, int)), plots, SLOT(setFFTAndZoom(int, int))); + connect(dock->powerMaxSlider, SIGNAL(valueChanged(int)), plots, SLOT(setPowerMax(int))); + connect(dock->powerMinSlider, SIGNAL(valueChanged(int)), plots, SLOT(setPowerMin(int))); + connect(dock->cursorsCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableCursors); + connect(dock->cursorBitsSpinBox, static_cast(&QSpinBox::valueChanged), plots, &PlotView::setCursorBits); -bool MainWindow::eventFilter(QObject * /*obj*/, QEvent *event) -{ - if (event->type() == QEvent::Wheel) { - QWheelEvent *wheelEvent = (QWheelEvent*)event; - QSlider *slider = nullptr; - if (QApplication::keyboardModifiers() & Qt::ControlModifier) { - slider = dock->zoomLevelSlider; - } else if (QApplication::keyboardModifiers() & Qt::ShiftModifier) { - slider = dock->fftSizeSlider; - } - if (slider != nullptr) { - if (wheelEvent->angleDelta().y() > 0) { - slider->setValue(slider->value() + 1); - } else if (wheelEvent->angleDelta().y() < 0) { - slider->setValue(slider->value() - 1); - } - return true; - } - } - return false; -} + // Connect dock outputs + connect(plots, SIGNAL(timeSelectionChanged(float)), dock, SLOT(timeSelectionChanged(float))); -void MainWindow::setSampleRate(QString rate) -{ - spectrogram.setSampleRate(rate.toInt()); -} - -void MainWindow::changeSampleRate(int rate) -{ - spectrogram.setSampleRate(rate); - dock->sampleRate->setText(QString::number(rate)); -} - -void MainWindow::setFFTSize(int size) -{ - off_t sample = getCenterSample(); - spectrogram.setFFTSize(size); - scrollArea.verticalScrollBar()->setValue(getScrollPos(sample)); -} - -void MainWindow::setZoomLevel(int zoom) -{ - off_t sample = getCenterSample(); - spectrogram.setZoomLevel(zoom); - scrollArea.verticalScrollBar()->setValue(getScrollPos(sample)); -} - -off_t MainWindow::getCenterSample() -{ - int height = scrollArea.height(); - return (scrollArea.verticalScrollBar()->value() + height / 2) * spectrogram.getStride(); -} - -int MainWindow::getScrollPos(off_t sample) -{ - int height = scrollArea.height(); - return sample / spectrogram.getStride() - height / 2; + // Set defaults after making connections so everything is in sync + dock->setDefaults(); } void MainWindow::openFile(QString fileName) { QString title="%1: %2"; this->setWindowTitle(title.arg(QApplication::applicationName(),fileName.section('/',-1,-1))); - spectrogram.openFile(fileName); + input->openFile(fileName.toUtf8().constData()); +} + +void MainWindow::setSampleRate(QString rate) +{ + input->setSampleRate(rate.toInt()); +} + +void MainWindow::setSampleRate(int rate) +{ + dock->sampleRate->setText(QString::number(rate)); } diff --git a/mainwindow.h b/mainwindow.h index bdc53a4..4f628a9 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -21,8 +21,8 @@ #include #include -#include "spectrogram.h" #include "spectrogramcontrols.h" +#include "plotview.h" class MainWindow : public QMainWindow { @@ -34,18 +34,11 @@ public: public slots: void openFile(QString fileName); - void setSampleRate(QString rate); - void setFFTSize(int size); - void setZoomLevel(int zoom); - -protected: - bool eventFilter(QObject *obj, QEvent *event); + void setSampleRate(QString rate); + void setSampleRate(int rate); private: - QScrollArea scrollArea; - Spectrogram spectrogram; SpectrogramControls *dock; - - off_t getCenterSample(); - int getScrollPos(off_t sample); + PlotView *plots; + InputSource *input; }; diff --git a/memory_sink.h b/memory_sink.h new file mode 100644 index 0000000..afcd2cb --- /dev/null +++ b/memory_sink.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INCLUDED_GR_MEMORY_SINK_H +#define INCLUDED_GR_MEMORY_SINK_H + +#include +#include + +namespace gr +{ +namespace blocks +{ +class memory_sink : virtual public sync_block +{ +public: + typedef boost::shared_ptr sptr; + + static sptr make(size_t itemsize); + + virtual void set_sink(void *sink, size_t length) = 0; +}; + +} /* namespace blocks */ +} /* namespace gr */ + +#endif /* INCLUDED_GR_MEMORY_SINK_H */ \ No newline at end of file diff --git a/memory_sink_impl.cc b/memory_sink_impl.cc new file mode 100644 index 0000000..7d57c3e --- /dev/null +++ b/memory_sink_impl.cc @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "memory_sink_impl.h" +#include +#include + +namespace gr { + namespace blocks { + + memory_sink::sptr + memory_sink::make(size_t itemsize) + { + return gnuradio::get_initial_sptr + (new memory_sink_impl(itemsize)); + } + + memory_sink_impl::memory_sink_impl(size_t itemsize) + : sync_block("memory_sink", + io_signature::make(1, 1, itemsize), + io_signature::make(0, 0, 0)), + d_itemsize(itemsize) + { + } + + memory_sink_impl::~memory_sink_impl() + { + } + + void + memory_sink_impl::set_sink(void *sink, size_t length) + { + d_sink = sink; + d_length = length; + d_ptr = 0; + } + + int + memory_sink_impl::work(int noutput_items, + gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items) + { + char *inbuf = (char*)input_items[0]; + long nwritten = 0; + + if(!d_sink) + return noutput_items; + + nwritten = std::min((long)(d_length - d_ptr), (long)noutput_items); + if (nwritten >= 0) { + memcpy((char*)d_sink + d_ptr * d_itemsize, inbuf, nwritten * d_itemsize); + } + d_ptr += nwritten; + + return (nwritten == 0) ? -1 : nwritten; + } + + } /* namespace blocks */ +} /* namespace gr */ diff --git a/memory_sink_impl.h b/memory_sink_impl.h new file mode 100644 index 0000000..c2fdac5 --- /dev/null +++ b/memory_sink_impl.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INCLUDED_GR_MEMORY_SINK_IMPL_H +#define INCLUDED_GR_MEMORY_SINK_IMPL_H + +#include "memory_sink.h" + +namespace gr +{ +namespace blocks +{ + +class memory_sink_impl : public memory_sink +{ +private: + size_t d_itemsize; + void *d_sink; + size_t d_length; + size_t d_ptr = 0; + +public: + memory_sink_impl(size_t itemsize); + ~memory_sink_impl(); + + void set_sink(void *sink, size_t length); + + int work(int noutput_items, + gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items); +}; + +} /* namespace blocks */ +} /* namespace gr */ + +#endif /* INCLUDED_GR_MEMORY_SINK_IMPL_H */ \ No newline at end of file diff --git a/memory_source.h b/memory_source.h new file mode 100644 index 0000000..10c3c11 --- /dev/null +++ b/memory_source.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INCLUDED_GR_MEMORY_SOURCE_H +#define INCLUDED_GR_MEMORY_SOURCE_H + +#include +#include + +namespace gr +{ +namespace blocks +{ +class memory_source : virtual public sync_block +{ +public: + typedef boost::shared_ptr sptr; + + static sptr make(size_t itemsize); + + virtual void set_source(void *source, size_t length) = 0; +}; + +} /* namespace blocks */ +} /* namespace gr */ + +#endif /* INCLUDED_GR_MEMORY_SOURCE_H */ \ No newline at end of file diff --git a/memory_source_impl.cc b/memory_source_impl.cc new file mode 100644 index 0000000..5a324d0 --- /dev/null +++ b/memory_source_impl.cc @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "memory_source_impl.h" +#include +#include + +namespace gr { + namespace blocks { + + memory_source::sptr + memory_source::make(size_t itemsize) + { + return gnuradio::get_initial_sptr + (new memory_source_impl(itemsize)); + } + + memory_source_impl::memory_source_impl(size_t itemsize) + : sync_block("memory_source", + io_signature::make(0, 0, 0), + io_signature::make(1, 1, itemsize)), + d_itemsize(itemsize) + { + } + + memory_source_impl::~memory_source_impl() + { + } + + void + memory_source_impl::set_source(void *source, size_t length) + { + d_source = source; + d_length = length; + d_ptr = 0; + } + + int + memory_source_impl::work(int noutput_items, + gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items) + { + char *outbuf = (char*)output_items[0]; + long nwritten = 0; + + if(!d_source) + return noutput_items; + + nwritten = std::min((long)(d_length - d_ptr), (long)noutput_items); + + if (nwritten >= 0) { + memcpy(outbuf, (char*)d_source + d_ptr * d_itemsize, nwritten * d_itemsize); + } + d_ptr += nwritten; + + return (nwritten == 0) ? -1 : nwritten; + } + + } /* namespace blocks */ +} /* namespace gr */ diff --git a/memory_source_impl.h b/memory_source_impl.h new file mode 100644 index 0000000..7c57a0e --- /dev/null +++ b/memory_source_impl.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + *ha + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INCLUDED_GR_MEMORY_SOURCE_IMPL_H +#define INCLUDED_GR_MEMORY_SOURCE_IMPL_H + +#include "memory_source.h" + +namespace gr +{ +namespace blocks +{ + +class memory_source_impl : public memory_source +{ +private: + size_t d_itemsize; + void *d_source; + size_t d_length; + size_t d_ptr = 0; + +public: + memory_source_impl(size_t itemsize); + ~memory_source_impl(); + + void set_source(void *source, size_t length); + + int work(int noutput_items, + gr_vector_const_void_star &input_items, + gr_vector_void_star &output_items); +}; + +} /* namespace blocks */ +} /* namespace gr */ + +#endif /* INCLUDED_GR_MEMORY_SOURCE_IMPL_H */ \ No newline at end of file diff --git a/plot.cpp b/plot.cpp new file mode 100644 index 0000000..84de2b9 --- /dev/null +++ b/plot.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "plot.h" + +void Plot::paintBack(QPainter &painter, QRect &rect, range_t sampleRange) +{ + painter.save(); + QPen pen(Qt::white, 1, Qt::DashLine); + painter.setPen(pen); + painter.drawLine(rect.left(), rect.center().y(), rect.right(), rect.center().y()); + painter.restore(); +} + +void Plot::paintMid(QPainter &painter, QRect &rect, range_t sampleRange) +{ + +} + +void Plot::paintFront(QPainter &painter, QRect &rect, range_t sampleRange) +{ + +} diff --git a/plot.h b/plot.h new file mode 100644 index 0000000..14f6cc4 --- /dev/null +++ b/plot.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include "util.h" + +class Plot : public QObject +{ + +public: + virtual void paintBack(QPainter &painter, QRect &rect, range_t sampleRange); + virtual void paintMid(QPainter &painter, QRect &rect, range_t sampleRange); + virtual void paintFront(QPainter &painter, QRect &rect, range_t sampleRange); + int height() const { return _height; }; + +protected: + void setHeight(int height) { _height = height; }; + +private: + // TODO: don't hardcode this + int _height = 200; +}; diff --git a/plotview.cpp b/plotview.cpp new file mode 100644 index 0000000..b2f8da5 --- /dev/null +++ b/plotview.cpp @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2015-2016, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "plotview.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "grsamplebuffer.h" +#include "memory_sink.h" +#include "memory_source.h" + +PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0}) +{ + mainSampleSource = input; + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + enableCursors(false); + viewport()->installEventFilter(&cursors); + connect(&cursors, SIGNAL(cursorsMoved()), this, SLOT(cursorsMoved())); + + spectrogramPlot = new SpectrogramPlot(mainSampleSource); + plots.emplace_back(spectrogramPlot); + + mainSampleSource->subscribe(this); +} + +TracePlot* PlotView::createIQPlot(SampleSource> *src) +{ + gr::top_block_sptr iq_tb = gr::make_top_block("multiply"); + auto iq_mem_source = gr::blocks::memory_source::make(8); + auto iq_mem_sink = gr::blocks::memory_sink::make(8); + auto multiply = gr::blocks::multiply_const_cc::make(20); + if (selection || true) { + float centre = -0.05; //(selectionFreq.first + selectionFreq.second) / 2; + float cutoff = 0.02; //std::abs(selectionFreq.first - centre); + auto lp_taps = gr::filter::firdes::low_pass(1.0, 1.0, cutoff, cutoff / 2); + auto filter = gr::filter::freq_xlating_fir_filter_ccf::make(1, lp_taps, centre, 1.0); + + iq_tb->connect(iq_mem_source, 0, filter, 0); + iq_tb->connect(filter, 0, multiply, 0); + iq_tb->connect(multiply, 0, iq_mem_sink, 0); + } else { + iq_tb->connect(iq_mem_source, 0, multiply, 0); + iq_tb->connect(multiply, 0, iq_mem_sink, 0); + } + + auto iq_src = std::make_shared, std::complex>>(mainSampleSource, iq_tb, iq_mem_source, iq_mem_sink); + return new TracePlot(iq_src); +} + +TracePlot* PlotView::createQuadratureDemodPlot(SampleSource> *src) +{ + gr::top_block_sptr quad_demod_tb = gr::make_top_block("quad_demod"); + auto quad_demod_mem_source = gr::blocks::memory_source::make(8); + auto quad_demod_mem_sink = gr::blocks::memory_sink::make(4); + auto quad_demod = gr::analog::quadrature_demod_cf::make(5); + quad_demod_tb->connect(quad_demod_mem_source, 0, quad_demod, 0); + quad_demod_tb->connect(quad_demod, 0, quad_demod_mem_sink, 0); + + return new TracePlot( + std::make_shared, float>>( + dynamic_cast>*>(src), quad_demod_tb, quad_demod_mem_source, quad_demod_mem_sink + ) + ); +} + +void PlotView::cursorsMoved() +{ + selectedSamples = { + horizontalScrollBar()->value() + cursors.selection().minimum * samplesPerLine(), + horizontalScrollBar()->value() + cursors.selection().maximum * samplesPerLine() + }; + + off_t sampleCount = selectedSamples.length(); + float selectionTime = sampleCount / (float)mainSampleSource->rate(); + emit timeSelectionChanged(selectionTime); + viewport()->update(); +} + +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}); + } + viewport()->update(); +} + +void PlotView::invalidateEvent() +{ + horizontalScrollBar()->setMinimum(0); + horizontalScrollBar()->setMaximum(mainSampleSource->count()); +} + +void PlotView::setCursorBits(int bits) +{ + cursors.setBits(bits); + cursorsMoved(); + viewport()->update(); +} + +void PlotView::setFFTAndZoom(int size, int zoom) +{ + // Set new FFT size + fftSize = size; + if (spectrogramPlot != nullptr) + spectrogramPlot->setFFTSize(size); + + // Set new zoom level + zoomLevel = zoom; + if (spectrogramPlot != nullptr) + spectrogramPlot->setZoomLevel(zoom); + + // Update horizontal (time) scrollbar + horizontalScrollBar()->setSingleStep(size * 10 / zoomLevel); + horizontalScrollBar()->setPageStep(size * 100 / zoomLevel); + + updateView(); +} + +void PlotView::setPowerMin(int power) +{ + powerMin = power; + if (spectrogramPlot != nullptr) + spectrogramPlot->setPowerMin(power); + updateView(); +} + +void PlotView::setPowerMax(int power) +{ + powerMax = power; + if (spectrogramPlot != nullptr) + spectrogramPlot->setPowerMax(power); + updateView(); +} + +void PlotView::paintEvent(QPaintEvent *event) +{ + if (mainSampleSource == nullptr) return; + + QRect rect = QRect(0, 0, width(), height()); + QPainter painter(viewport()); + painter.fillRect(rect, Qt::black); + + +#define PLOT_LAYER(paintFunc) \ + { \ + int y = -verticalScrollBar()->value(); \ + for (auto&& plot : plots) { \ + QRect rect = QRect(0, y, width(), plot->height()); \ + plot->paintFunc(painter, rect, {viewRange.first, viewRange.second});\ + y += plot->height(); \ + } \ + } + + PLOT_LAYER(paintBack); + PLOT_LAYER(paintMid); + PLOT_LAYER(paintFront); + if (cursorsEnabled) + cursors.paintFront(painter, rect, {viewRange.first, viewRange.second}); + +#undef PLOT_LAYER +} + +int PlotView::plotsHeight() +{ + int height = 0; + for (auto&& plot : plots) { + height += plot->height(); + } + return height; +} + +void PlotView::resizeEvent(QResizeEvent * event) +{ + updateView(); +} + +off_t PlotView::samplesPerLine() +{ + return fftSize / zoomLevel; +} + +void PlotView::scrollContentsBy(int dx, int dy) +{ + updateView(); +} + +void PlotView::updateView() +{ + // Update current view + viewRange = { + horizontalScrollBar()->value(), + horizontalScrollBar()->value() + width() * samplesPerLine() + }; + horizontalScrollBar()->setMaximum(mainSampleSource->count() - ((width() - 1) * samplesPerLine())); + + verticalScrollBar()->setMaximum(std::max(0, plotsHeight() - viewport()->height())); + + // Update cursors + QRect rect = viewport()->rect(); + range_t newSelection = { + (int)((selectedSamples.minimum - horizontalScrollBar()->value()) / samplesPerLine()), + (int)((selectedSamples.maximum - horizontalScrollBar()->value()) / samplesPerLine()) + }; + cursors.setSelection(newSelection); + + // Re-paint + viewport()->update(); +} diff --git a/plotview.h b/plotview.h new file mode 100644 index 0000000..ca8d04d --- /dev/null +++ b/plotview.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015-2016, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include "cursors.h" +#include "inputsource.h" +#include "plot.h" +#include "samplesource.h" +#include "spectrogramplot.h" +#include "traceplot.h" + +class PlotView : public QAbstractScrollArea, Subscriber +{ + Q_OBJECT + +public: + PlotView(InputSource *input); + +signals: + void timeSelectionChanged(float time); + +public slots: + void cursorsMoved(); + void enableCursors(bool enable); + void invalidateEvent(); + void setCursorBits(int bits); + void setFFTAndZoom(int fftSize, int zoomLevel); + void setPowerMin(int power); + void setPowerMax(int power); + +protected: + void paintEvent(QPaintEvent *event); + void resizeEvent(QResizeEvent * event); + void scrollContentsBy(int dx, int dy); + +private: + Cursors cursors; + SampleSource> *mainSampleSource = nullptr; + SpectrogramPlot *spectrogramPlot = nullptr; + TracePlot *iqPlot = nullptr; + std::vector> plots; + std::pair viewRange; + bool selection = false; + range_t selectedSamples; + std::pair selectionFreq; + + int fftSize = 1024; + int zoomLevel = 0; + int powerMin; + int powerMax; + bool cursorsEnabled; + + TracePlot* createIQPlot(SampleSource> *src); + TracePlot* createQuadratureDemodPlot(SampleSource> *src); + int plotsHeight(); + off_t samplesPerLine(); + void updateView(); +}; \ No newline at end of file diff --git a/samplebuffer.cpp b/samplebuffer.cpp new file mode 100644 index 0000000..6965ff7 --- /dev/null +++ b/samplebuffer.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "samplebuffer.h" + +template +SampleBuffer::SampleBuffer(SampleSource *src) : src(src) +{ + src->subscribe(this); +} + +template +SampleBuffer::~SampleBuffer() +{ + src->unsubscribe(this); +} + +template +std::unique_ptr SampleBuffer::getSamples(off_t start, off_t length) +{ + auto samples = src->getSamples(start, length); + if (samples == nullptr) + return nullptr; + + std::unique_ptr dest(new Tout[length]); + work(samples.get(), dest.get(), length); + return dest; +} + +template +void SampleBuffer::invalidateEvent() +{ + SampleSource::invalidate(); +} + +template class SampleBuffer, std::complex>; +template class SampleBuffer, float>; diff --git a/samplebuffer.h b/samplebuffer.h new file mode 100644 index 0000000..cfbac64 --- /dev/null +++ b/samplebuffer.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include "samplesource.h" + +template +class SampleBuffer : public SampleSource, public Subscriber +{ +private: + SampleSource *src; + +public: + SampleBuffer(SampleSource *src); + ~SampleBuffer(); + void invalidateEvent(); + virtual std::unique_ptr getSamples(off_t start, off_t length); + virtual void work(void *input, void *output, int count) = 0; + virtual off_t count() { + return src->count(); + }; + off_t rate() { + return src->rate(); + }; +}; diff --git a/samplesource.cpp b/samplesource.cpp new file mode 100644 index 0000000..123a203 --- /dev/null +++ b/samplesource.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "samplesource.h" + +template +void SampleSource::subscribe(Subscriber *subscriber) +{ + subscribers.insert(subscriber); +} + +template +void SampleSource::invalidate() +{ + for (auto subscriber : subscribers) { + subscriber->invalidateEvent(); + } +} + +template +void SampleSource::unsubscribe(Subscriber *subscriber) +{ + subscribers.erase(subscriber); +} + +template class SampleSource>; +template class SampleSource; diff --git a/samplesource.h b/samplesource.h new file mode 100644 index 0000000..e69ab8c --- /dev/null +++ b/samplesource.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include "abstractsamplesource.h" +#include "subscriber.h" + +template +class SampleSource : public AbstractSampleSource +{ + +public: + virtual ~SampleSource() {}; + + virtual std::unique_ptr getSamples(off_t start, off_t length) = 0; + virtual void invalidateEvent() { }; + virtual off_t count() = 0; + virtual off_t rate() = 0; + void subscribe(Subscriber *subscriber); + void unsubscribe(Subscriber *subscriber); + +protected: + virtual void invalidate(); + +private: + std::set subscribers; +}; diff --git a/spectrogram.cpp b/spectrogram.cpp deleted file mode 100644 index 195522d..0000000 --- a/spectrogram.cpp +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright (C) 2015, Mike Walters - * - * This file is part of inspectrum. - * - * 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, either 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 for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "spectrogram.h" - -#include -#include -#include -#include -#include - -#include - - -Spectrogram::Spectrogram() -{ - sampleRate = 8000000; - setFFTSize(1024); - zoomLevel = 0; - powerMax = 0.0f; - powerMin = -50.0f; - timeScaleIsEnabled = true; - deltaDragIsEnabled = true; - - for (int i = 0; i < 256; i++) { - float p = (float)i / 256; - colormap[i] = QColor::fromHsvF(p * 0.83f, 1.0, 1.0 - p).rgba(); - } - - setMouseTracking(true); -} - -Spectrogram::~Spectrogram() -{ - delete fft; - delete inputSource; -} - -QSize Spectrogram::sizeHint() const { - return QSize(1024, 2048); -} - -void Spectrogram::openFile(QString fileName) -{ - if (fileName != nullptr) { - try { - InputSource *newFile = new InputSource(fileName.toUtf8().constData()); - delete inputSource; - pixmapCache.clear(); - fftCache.clear(); - inputSource = newFile; - resize(fftSize, getHeight()); - } catch (std::runtime_error e) { - // TODO: display error - } - } -} - -template const T& clamp (const T& value, const T& min, const T& max) { - return std::min(max, std::max(min, value)); -} - -void Spectrogram::xyToFreqTime(int x, int y, float *freq, float *time) { - *freq = labs(x - (fftSize / 2)) * sampleRate / 2 / (float)fftSize; - *time = (float)lineToSample(y) / sampleRate; -} - -void Spectrogram::mouseReleaseEvent(QMouseEvent *event) { - if (deltaDragIsEnabled) { - cursorStartX = -1; - update(); - } -} - -void Spectrogram::mouseMoveEvent(QMouseEvent *event) { - float freq, time; - xyToFreqTime(event->x(), event->y(), &freq, &time); - emit cursorFrequencyChanged(QString::number(freq) + " Hz"); - emit cursorTimeChanged(QString::number(time) + " s"); - if (cursorStartX != -1) { - float s_freq, s_time; - xyToFreqTime(cursorStartX, cursorStartY, &s_freq, &s_time); - emit deltaFrequencyChanged(QString::number(fabs(s_freq - freq)) + " Hz"); - emit deltaTimeChanged(QString::number(fabs(s_time - time)) + " s"); - cursorEndX = event->x(); - cursorEndY = event->y(); - update(); - } -} - -void Spectrogram::mousePressEvent(QMouseEvent *event) { - if (cursorStartX == -1) { - cursorEndX = cursorStartX = event->x(); - cursorEndY = cursorStartY = event->y(); - } else { - cursorStartX = -1; - } - update(); -} - -void Spectrogram::paintEvent(QPaintEvent *event) -{ - QRect rect = event->rect(); - QPainter painter(this); - painter.fillRect(rect, Qt::black); - - if (inputSource != nullptr) { - int height = rect.height(); - off_t y = rect.y(); - - QImage image(fftSize, height, QImage::Format_RGB32); - - while (height > 0) { - int tileOffset = y % linesPerTile(); // To handle drawing a partial first tile - int drawHeight = std::min(linesPerTile() - tileOffset, height); // Draw rest of first tile, full tile, or partial final tile - off_t tileId = lineToSample(y - tileOffset); - QPixmap *tile = getPixmapTile(tileId); - painter.drawPixmap(QRect(0, y, fftSize, drawHeight), *tile, QRect(0, tileOffset, fftSize, drawHeight)); - y += drawHeight; - height -= drawHeight; - } - - paintTimeAxis(&painter, rect); - paintCursors(&painter, rect); - } -} - -QPixmap* Spectrogram::getPixmapTile(off_t tile) -{ - QPixmap *obj = pixmapCache.object(TileCacheKey(fftSize, zoomLevel, tile)); - if (obj != 0) - return obj; - - float *fftTile = getFFTTile(tile); - obj = new QPixmap(fftSize, linesPerTile()); - QImage image(fftSize, linesPerTile(), QImage::Format_RGB32); - for (int y = 0; y < linesPerTile(); y++) { - float *line = &fftTile[y * fftSize]; - for (int x = 0; x < fftSize; x++) { - float powerRange = std::abs(int(powerMin - powerMax)); - float normPower = (line[x] - powerMax) * -1.0f / powerRange; - normPower = clamp(normPower, 0.0f, 1.0f); - - image.setPixel(x, y, colormap[(uint8_t)(normPower * (256 - 1))]); - } - } - obj->convertFromImage(image); - pixmapCache.insert(TileCacheKey(fftSize, zoomLevel, tile), obj); - return obj; -} - -float* Spectrogram::getFFTTile(off_t tile) -{ - float *obj = fftCache.object(TileCacheKey(fftSize, zoomLevel, tile)); - if (obj != 0) - return obj; - - float *dest = new float[tileSize]; - float *ptr = dest; - off_t sample = tile; - while ((ptr - dest) < tileSize) { - getLine(ptr, sample); - sample += getStride(); - ptr += fftSize; - } - fftCache.insert(TileCacheKey(fftSize, zoomLevel, tile), dest); - return dest; -} - -void Spectrogram::getLine(float *dest, off_t sample) -{ - if (inputSource && fft) { - fftwf_complex buffer[fftSize]; - inputSource->getSamples(buffer, sample, fftSize); - - for (int i = 0; i < fftSize; i++) { - buffer[i][0] *= window[i]; - buffer[i][1] *= window[i]; - } - - fft->process(buffer, buffer); - for (int i = 0; i < fftSize; i++) { - int k = (i + fftSize / 2) % fftSize; - float re = buffer[k][0]; - float im = buffer[k][1]; - float mag = sqrt(re * re + im * im) / fftSize; - float magdb = 10 * log2(mag) / log2(10); - *dest = magdb; - dest++; - } - } -} - -void Spectrogram::paintCursors(QPainter *painter, QRect rect) -{ - if (cursorStartX != -1) { - painter->save(); - QPen pen(Qt::white, 1, Qt::DashLine); - painter->setPen(pen); - painter->drawLine(rect.left(), cursorStartY, rect.right(), cursorStartY); - painter->drawLine(cursorStartX, rect.top(), cursorStartX, rect.bottom()); - painter->drawLine(rect.left(), cursorEndY, rect.right(), cursorEndY); - painter->drawLine(cursorEndX, rect.top(), cursorEndX, rect.bottom()); - painter->restore(); - - } -} - -void Spectrogram::paintTimeAxis(QPainter *painter, QRect rect) -{ - if (timeScaleIsEnabled){ - // Round up for firstLine and round each to nearest linesPerGraduation - int firstLine = ((rect.y() + linesPerGraduation - 1) / linesPerGraduation) * linesPerGraduation; - int lastLine = ((rect.y() + rect.height()) / linesPerGraduation) * linesPerGraduation; - - painter->save(); - QPen pen(Qt::white, 1, Qt::SolidLine); - painter->setPen(pen); - QFontMetrics fm(painter->font()); - int textOffset = fm.ascent() / 2 - 1; - for (int line = firstLine; line <= lastLine; line += linesPerGraduation) { - painter->drawLine(0, line, 10, line); - painter->drawText(12, line + textOffset, sampleToTime(lineToSample(line))); - } - painter->restore(); - } -} - -void Spectrogram::setSampleRate(int rate) -{ - sampleRate = rate; - update(); -} - -void Spectrogram::setFFTSize(int size) -{ - fftSize = size; - delete fft; - fft = new FFT(fftSize); - - window.reset(new float[fftSize]); - for (int i = 0; i < fftSize; i++) { - window[i] = 0.5f * (1.0f - cos(Tau * i / (fftSize - 1))); - } - - resize(fftSize, getHeight()); -} - -void Spectrogram::setPowerMax(int power) -{ - powerMax = power; - pixmapCache.clear(); - update(); -} - -void Spectrogram::setPowerMin(int power) -{ - powerMin = power; - pixmapCache.clear(); - update(); -} - -void Spectrogram::setZoomLevel(int zoom) -{ - zoomLevel = clamp(zoom, 0, (int)log2(fftSize)); - resize(fftSize, getHeight()); -} - -void Spectrogram::setTimeScaleEnable(int state) -{ - timeScaleIsEnabled = (state == Qt::Checked); - pixmapCache.clear(); - update(); -} - -void Spectrogram::setDeltaDragEnable(int state) -{ - deltaDragIsEnabled = (state == Qt::Checked); -} - - -int Spectrogram::getHeight() -{ - if (!inputSource) - return 0; - - return inputSource->getSampleCount() / getStride(); -} - -int Spectrogram::getStride() -{ - return fftSize / pow(2, zoomLevel); -} - -off_t Spectrogram::lineToSample(off_t line) { - return line * getStride(); -} - -int Spectrogram::sampleToLine(off_t sample) { - return sample / getStride(); -} - -QString Spectrogram::sampleToTime(off_t sample) -{ - return QString::number((float)sample / sampleRate).append("s"); -} - -int Spectrogram::linesPerTile() { - return tileSize / fftSize; -} - -uint qHash(const TileCacheKey &key, uint seed) { - return key.fftSize ^ key.zoomLevel ^ key.sample ^ seed; -} diff --git a/spectrogram.h b/spectrogram.h deleted file mode 100644 index d96b708..0000000 --- a/spectrogram.h +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2015, Mike Walters - * - * This file is part of inspectrum. - * - * 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, either 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 for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include "fft.h" -#include "inputsource.h" - -#include - -static const double Tau = M_PI * 2.0; - -class TileCacheKey; - -class Spectrogram : public QWidget { - Q_OBJECT - -public: - Spectrogram(); - ~Spectrogram(); - QSize sizeHint() const; - int getHeight(); - int getStride(); - -signals: - void cursorFrequencyChanged(QString); - void cursorTimeChanged(QString); - void deltaFrequencyChanged(QString); - void deltaTimeChanged(QString); - -public slots: - void openFile(QString fileName); - void setSampleRate(int rate); - void setFFTSize(int size); - void setPowerMax(int power); - void setPowerMin(int power); - void setZoomLevel(int zoom); - void setTimeScaleEnable(int state); - void setDeltaDragEnable(int state); - -protected: - void paintEvent(QPaintEvent *event); - void mouseReleaseEvent(QMouseEvent * event); - void mouseMoveEvent(QMouseEvent * event); - void mousePressEvent(QMouseEvent * event); - - -private: - const int linesPerGraduation = 50; - const int tileSize = 65536; // This must be a multiple of the maximum FFT size - - InputSource *inputSource = nullptr; - FFT *fft = nullptr; - std::unique_ptr window; - fftwf_complex *lineBuffer = nullptr; - QCache pixmapCache; - QCache fftCache; - uint colormap[256]; - - int sampleRate; - int fftSize; - int zoomLevel; - float powerMax; - float powerMin; - bool timeScaleIsEnabled; - bool deltaDragIsEnabled; - int cursorStartX = -1, cursorStartY; - int cursorEndX, cursorEndY; - - QPixmap* getPixmapTile(off_t tile); - float* getFFTTile(off_t tile); - void getLine(float *dest, off_t sample); - void paintTimeAxis(QPainter *painter, QRect rect); - void paintCursors(QPainter *painter, QRect rect); - off_t lineToSample(off_t line); - int sampleToLine(off_t sample); - QString sampleToTime(off_t sample); - int linesPerTile(); - void xyToFreqTime(int x, int y, float *freq, float *time); -}; - -class TileCacheKey { - -public: - TileCacheKey(int fftSize, int zoomLevel, off_t sample) { - this->fftSize = fftSize; - this->zoomLevel = zoomLevel; - this->sample = sample; - } - - bool operator==(const TileCacheKey &k2) const { - return (this->fftSize == k2.fftSize) && - (this->zoomLevel == k2.zoomLevel) && - (this->sample == k2.sample); - } - - int fftSize; - int zoomLevel; - off_t sample; -}; diff --git a/spectrogramcontrols.cpp b/spectrogramcontrols.cpp index 9f737b1..cfcb102 100644 --- a/spectrogramcontrols.cpp +++ b/spectrogramcontrols.cpp @@ -24,74 +24,120 @@ #include SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent) - : QDockWidget::QDockWidget(title, parent) + : QDockWidget::QDockWidget(title, parent) { - widget = new QWidget(this); - layout = new QFormLayout(widget); + widget = new QWidget(this); + layout = new QFormLayout(widget); - fileOpenButton = new QPushButton("Open file...", widget); - layout->addRow(fileOpenButton); + fileOpenButton = new QPushButton("Open file...", widget); + layout->addRow(fileOpenButton); - sampleRate = new QLineEdit("8000000"); - sampleRate->setValidator(new QIntValidator(this)); - layout->addRow(new QLabel(tr("Sample rate:")), sampleRate); + sampleRate = new QLineEdit(); + sampleRate->setValidator(new QIntValidator(this)); + layout->addRow(new QLabel(tr("Sample rate:")), sampleRate); - fftSizeSlider = new QSlider(Qt::Horizontal, widget); - fftSizeSlider->setRange(7, 13); - fftSizeSlider->setValue(10); - layout->addRow(new QLabel(tr("FFT size:")), fftSizeSlider); + // Spectrogram settings + layout->addRow(new QLabel()); // TODO: find a better way to add an empty row? + layout->addRow(new QLabel(tr("Spectrogram"))); - zoomLevelSlider = new QSlider(Qt::Horizontal, widget); - zoomLevelSlider->setRange(0, 5); - zoomLevelSlider->setValue(0); - layout->addRow(new QLabel(tr("Zoom:")), zoomLevelSlider); + fftSizeSlider = new QSlider(Qt::Horizontal, widget); + fftSizeSlider->setRange(7, 13); + layout->addRow(new QLabel(tr("FFT size:")), fftSizeSlider); - powerMaxSlider = new QSlider(Qt::Horizontal, widget); - powerMaxSlider->setRange(-100, 20); - powerMaxSlider->setValue(0); - layout->addRow(new QLabel(tr("Power max:")), powerMaxSlider); + zoomLevelSlider = new QSlider(Qt::Horizontal, widget); + zoomLevelSlider->setRange(0, 10); + layout->addRow(new QLabel(tr("Zoom:")), zoomLevelSlider); - powerMinSlider = new QSlider(Qt::Horizontal, widget); - powerMinSlider->setRange(-100, 20); - powerMinSlider->setValue(-50); - layout->addRow(new QLabel(tr("Power min:")), powerMinSlider); + powerMaxSlider = new QSlider(Qt::Horizontal, widget); + powerMaxSlider->setRange(-100, 20); + layout->addRow(new QLabel(tr("Power max:")), powerMaxSlider); - timeScaleCheckBox = new QCheckBox(widget); - timeScaleCheckBox->setCheckState(Qt::Checked); - layout->addRow(new QLabel(tr("time overlay:")), timeScaleCheckBox); + powerMinSlider = new QSlider(Qt::Horizontal, widget); + powerMinSlider->setRange(-100, 20); + layout->addRow(new QLabel(tr("Power min:")), powerMinSlider); - cursorFrequencyLabel = new QLabel(); - layout->addRow(new QLabel(tr("Cursor frequency:")), cursorFrequencyLabel); + // Time selection settings + layout->addRow(new QLabel()); // TODO: find a better way to add an empty row? + layout->addRow(new QLabel(tr("Time selection"))); - cursorTimeLabel = new QLabel(); - layout->addRow(new QLabel(tr("Cursor time:")), cursorTimeLabel); + cursorsCheckBox = new QCheckBox(widget); + layout->addRow(new QLabel(tr("Enable cursors:")), cursorsCheckBox); - deltaDragCheckBox = new QCheckBox(widget); - deltaDragCheckBox->setCheckState(Qt::Checked); - layout->addRow(new QLabel(tr("Delta dragging:")), deltaDragCheckBox); + cursorBitsSpinBox = new QSpinBox(); + cursorBitsSpinBox->setMinimum(1); + cursorBitsSpinBox->setMaximum(9999); + layout->addRow(new QLabel(tr("Bits:")), cursorBitsSpinBox); - deltaFrequencyLabel = new QLabel(); - layout->addRow(new QLabel(tr("Delta frequency:")), deltaFrequencyLabel); + timeSelectionFreqLabel = new QLabel(); + layout->addRow(new QLabel(tr("Frequency:")), timeSelectionFreqLabel); - deltaTimeLabel = new QLabel(); - layout->addRow(new QLabel(tr("Delta time:")), deltaTimeLabel); + timeSelectionTimeLabel = new QLabel(); + layout->addRow(new QLabel(tr("Time:")), timeSelectionTimeLabel); - widget->setLayout(layout); - setWidget(widget); + bitSelectionFreqLabel = new QLabel(); + layout->addRow(new QLabel(tr("Bit frequency:")), bitSelectionFreqLabel); - connect(fftSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(fftSizeSliderChanged(int))); - connect(fileOpenButton, SIGNAL(clicked()), this, SLOT(fileOpenButtonClicked())); + bitSelectionTimeLabel = new QLabel(); + layout->addRow(new QLabel(tr("Bit time:")), bitSelectionTimeLabel); + + widget->setLayout(layout); + setWidget(widget); + + connect(fftSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(fftOrZoomChanged(int))); + connect(zoomLevelSlider, SIGNAL(valueChanged(int)), this, SLOT(fftOrZoomChanged(int))); + connect(fileOpenButton, SIGNAL(clicked()), this, SLOT(fileOpenButtonClicked())); + connect(cursorsCheckBox, SIGNAL(stateChanged(int)), this, SLOT(cursorsStateChanged(int))); } -void SpectrogramControls::fftSizeSliderChanged(int size) +void SpectrogramControls::clearCursorLabels() { - emit fftSizeChanged((int)pow(2, size)); + timeSelectionTimeLabel->setText(""); + timeSelectionFreqLabel->setText(""); + bitSelectionTimeLabel->setText(""); + bitSelectionFreqLabel->setText(""); +} + +void SpectrogramControls::cursorsStateChanged(int state) +{ + if (state == Qt::Unchecked) { + clearCursorLabels(); + } +} + +void SpectrogramControls::setDefaults() +{ + sampleRate->setText("8000000"); + fftSizeSlider->setValue(9); + zoomLevelSlider->setValue(0); + powerMaxSlider->setValue(0); + powerMinSlider->setValue(-50); + cursorsCheckBox->setCheckState(Qt::Unchecked); + cursorBitsSpinBox->setValue(1); +} + +void SpectrogramControls::fftOrZoomChanged(int value) +{ + int fftSize = pow(2, fftSizeSlider->value()); + int zoomLevel = std::min(fftSize, (int)pow(2, zoomLevelSlider->value())); + emit fftOrZoomChanged(fftSize, zoomLevel); } void SpectrogramControls::fileOpenButtonClicked() { - QString fileName = QFileDialog::getOpenFileName( - this, tr("Open File"), "", tr("Sample file (*.cfile *.bin);;All files (*)") - ); - emit openFile(fileName); + QString fileName = QFileDialog::getOpenFileName( + this, tr("Open File"), "", tr("Sample file (*.cfile *.bin);;All files (*)") + ); + emit openFile(fileName); +} + +void SpectrogramControls::timeSelectionChanged(float time) +{ + if (cursorsCheckBox->checkState() == Qt::Checked) { + timeSelectionTimeLabel->setText(QString::number(time) + "s"); + timeSelectionFreqLabel->setText(QString::number(1 / time) + "Hz"); + + int bits = cursorBitsSpinBox->value(); + bitSelectionTimeLabel->setText(QString::number(time / bits) + "s"); + bitSelectionFreqLabel->setText(QString::number(bits / time) + "Hz"); + } } diff --git a/spectrogramcontrols.h b/spectrogramcontrols.h index 3f835aa..09c0a5e 100644 --- a/spectrogramcontrols.h +++ b/spectrogramcontrols.h @@ -24,37 +24,46 @@ #include #include #include +#include #include #include -class SpectrogramControls : public QDockWidget { - Q_OBJECT +class SpectrogramControls : public QDockWidget +{ + Q_OBJECT public: - SpectrogramControls(const QString & title, QWidget * parent); + SpectrogramControls(const QString & title, QWidget * parent); + void setDefaults(); signals: - void fftSizeChanged(int size); - void openFile(QString fileName); + void fftOrZoomChanged(int fftSize, int zoomLevel); + void openFile(QString fileName); + +public slots: + void timeSelectionChanged(float time); private slots: - void fftSizeSliderChanged(int size); - void fileOpenButtonClicked(); + void fftOrZoomChanged(int value); + void fileOpenButtonClicked(); + void cursorsStateChanged(int state); private: - QWidget *widget; - QFormLayout *layout; + QWidget *widget; + QFormLayout *layout; + void clearCursorLabels(); + public: - QPushButton *fileOpenButton; - QLineEdit *sampleRate; - QSlider *fftSizeSlider; - QSlider *zoomLevelSlider; - QSlider *powerMaxSlider; - QSlider *powerMinSlider; - QCheckBox *timeScaleCheckBox; - QLabel *cursorFrequencyLabel; - QLabel *cursorTimeLabel; - QCheckBox *deltaDragCheckBox; - QLabel *deltaFrequencyLabel; - QLabel *deltaTimeLabel; + QPushButton *fileOpenButton; + QLineEdit *sampleRate; + QSlider *fftSizeSlider; + QSlider *zoomLevelSlider; + QSlider *powerMaxSlider; + QSlider *powerMinSlider; + QCheckBox *cursorsCheckBox; + QSpinBox *cursorBitsSpinBox; + QLabel *timeSelectionFreqLabel; + QLabel *timeSelectionTimeLabel; + QLabel *bitSelectionFreqLabel; + QLabel *bitSelectionTimeLabel; }; diff --git a/spectrogramplot.cpp b/spectrogramplot.cpp new file mode 100644 index 0000000..92fdee8 --- /dev/null +++ b/spectrogramplot.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "spectrogramplot.h" + +#include +#include +#include +#include +#include + +#include +#include "util.h" + + +SpectrogramPlot::SpectrogramPlot(SampleSource> *src) +{ + inputSource = src; + sampleRate = 8000000; + setFFTSize(512); + zoomLevel = 0; + powerMax = 0.0f; + powerMin = -50.0f; + + for (int i = 0; i < 256; i++) { + float p = (float)i / 256; + colormap[i] = QColor::fromHsvF(p * 0.83f, 1.0, 1.0 - p).rgba(); + } + +} + +void SpectrogramPlot::paintMid(QPainter &painter, QRect &rect, range_t sampleRange) +{ + if (!inputSource || inputSource->count() == 0) + return; + + off_t sampleOffset = sampleRange.minimum % getStride(); + off_t tileID = sampleRange.minimum - sampleOffset; + int xoffset = sampleOffset / fftSize; + for (int x = rect.left(); x < rect.right(); x += linesPerTile()) { + QPixmap *tile = getPixmapTile(tileID); + // TODO: don't draw past rect.right() + // TODO: handle partial final tile + painter.drawPixmap(QRect(x, rect.y(), linesPerTile() - xoffset, fftSize), *tile, QRect(xoffset, 0, linesPerTile() - xoffset, fftSize)); + xoffset = 0; + tileID += getStride() * linesPerTile(); + } +} + +QPixmap* SpectrogramPlot::getPixmapTile(off_t tile) +{ + QPixmap *obj = pixmapCache.object(TileCacheKey(fftSize, zoomLevel, tile)); + if (obj != 0) + return obj; + + float *fftTile = getFFTTile(tile); + obj = new QPixmap(linesPerTile(), fftSize); + QImage image(linesPerTile(), fftSize, QImage::Format_RGB32); + for (int x = 0; x < linesPerTile(); x++) { + float *line = &fftTile[x * fftSize]; + for (int y = 0; y < fftSize; y++) { + float powerRange = std::abs(int(powerMin - powerMax)); + float normPower = (line[y] - powerMax) * -1.0f / powerRange; + normPower = clamp(normPower, 0.0f, 1.0f); + + image.setPixel(x, fftSize - y - 1, colormap[(uint8_t)(normPower * (256 - 1))]); + } + } + obj->convertFromImage(image); + pixmapCache.insert(TileCacheKey(fftSize, zoomLevel, tile), obj); + return obj; +} + +float* SpectrogramPlot::getFFTTile(off_t tile) +{ + float *obj = fftCache.object(TileCacheKey(fftSize, zoomLevel, tile)); + if (obj != 0) + return obj; + + float *dest = new float[tileSize]; + float *ptr = dest; + off_t sample = tile; + while ((ptr - dest) < tileSize) { + getLine(ptr, sample); + sample += getStride(); + ptr += fftSize; + } + fftCache.insert(TileCacheKey(fftSize, zoomLevel, tile), dest); + return dest; +} + +void SpectrogramPlot::getLine(float *dest, off_t sample) +{ + if (inputSource && fft) { + auto buffer = inputSource->getSamples(sample, fftSize); + if (buffer == nullptr) + return; + + for (int i = 0; i < fftSize; i++) { + buffer[i].real(buffer[i].real() * window[i]); + buffer[i].imag(buffer[i].imag() * window[i]); + } + + fft->process(buffer.get(), buffer.get()); + for (int i = 0; i < fftSize; i++) { + int k = (i + fftSize / 2) % fftSize; + float re = buffer[k].real(); + float im = buffer[k].imag(); + float mag = sqrt(re * re + im * im) / fftSize; + float magdb = 10 * log2(mag) / log2(10); + *dest = magdb; + dest++; + } + } +} + +void SpectrogramPlot::setSampleRate(int rate) +{ + sampleRate = rate; +} + +void SpectrogramPlot::setFFTSize(int size) +{ + fftSize = size; + fft.reset(new FFT(fftSize)); + + window.reset(new float[fftSize]); + for (int i = 0; i < fftSize; i++) { + window[i] = 0.5f * (1.0f - cos(Tau * i / (fftSize - 1))); + } + + setHeight(fftSize); +} + +void SpectrogramPlot::setPowerMax(int power) +{ + powerMax = power; + pixmapCache.clear(); +} + +void SpectrogramPlot::setPowerMin(int power) +{ + powerMin = power; + pixmapCache.clear(); +} + +void SpectrogramPlot::setZoomLevel(int zoom) +{ + zoomLevel = zoom; +} + +int SpectrogramPlot::getHeight() +{ + if (!inputSource) + return 0; + + return inputSource->count() / getStride(); +} + +int SpectrogramPlot::getStride() +{ + return fftSize / zoomLevel; +} + +off_t SpectrogramPlot::lineToSample(off_t line) +{ + return line * getStride(); +} + +int SpectrogramPlot::sampleToLine(off_t sample) +{ + return sample / getStride(); +} + +QString SpectrogramPlot::sampleToTime(off_t sample) +{ + return QString::number((float)sample / sampleRate).append("s"); +} + +int SpectrogramPlot::linesPerTile() +{ + return tileSize / fftSize; +} + +uint qHash(const TileCacheKey &key, uint seed) +{ + return key.fftSize ^ key.zoomLevel ^ key.sample ^ seed; +} diff --git a/spectrogramplot.h b/spectrogramplot.h new file mode 100644 index 0000000..4a067c0 --- /dev/null +++ b/spectrogramplot.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2015, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include "fft.h" +#include "inputsource.h" +#include "plot.h" + +#include +#include + +static const double Tau = M_PI * 2.0; + +class TileCacheKey; + +class SpectrogramPlot : public Plot +{ + Q_OBJECT + +public: + SpectrogramPlot(SampleSource> *src); + + void paintMid(QPainter &painter, QRect &rect, range_t sampleRange); + + QSize sizeHint() const; + int getHeight(); + int getStride(); + off_t lineToSample(off_t line); + + SampleSource> *inputSource = nullptr; + +public slots: + void setSampleRate(int rate); + void setFFTSize(int size); + void setPowerMax(int power); + void setPowerMin(int power); + void setZoomLevel(int zoom); + +protected: + void mouseReleaseEvent(QMouseEvent * event); + void mouseMoveEvent(QMouseEvent * event); + void mousePressEvent(QMouseEvent * event); + + +private: + const int linesPerGraduation = 50; + const int tileSize = 65536; // This must be a multiple of the maximum FFT size + + std::unique_ptr fft; + std::unique_ptr window; + fftwf_complex *lineBuffer = nullptr; + QCache pixmapCache; + QCache fftCache; + uint colormap[256]; + + int sampleRate; + int fftSize; + int zoomLevel; + float powerMax; + float powerMin; + + QPixmap* getPixmapTile(off_t tile); + float* getFFTTile(off_t tile); + void getLine(float *dest, off_t sample); + void paintCursors(QPainter *painter, QRect rect); + int sampleToLine(off_t sample); + QString sampleToTime(off_t sample); + int linesPerTile(); +}; + +class TileCacheKey +{ + +public: + TileCacheKey(int fftSize, int zoomLevel, off_t sample) { + this->fftSize = fftSize; + this->zoomLevel = zoomLevel; + this->sample = sample; + } + + bool operator==(const TileCacheKey &k2) const { + return (this->fftSize == k2.fftSize) && + (this->zoomLevel == k2.zoomLevel) && + (this->sample == k2.sample); + } + + int fftSize; + int zoomLevel; + off_t sample; +}; diff --git a/subscriber.h b/subscriber.h new file mode 100644 index 0000000..bbf4c67 --- /dev/null +++ b/subscriber.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + + +class Subscriber +{ +public: + virtual void invalidateEvent() = 0; +}; diff --git a/traceplot.cpp b/traceplot.cpp new file mode 100644 index 0000000..c056465 --- /dev/null +++ b/traceplot.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "samplesource.h" +#include "traceplot.h" + +TracePlot::TracePlot(std::shared_ptr source) : sampleSource(source) { + +} + +void TracePlot::paintMid(QPainter &painter, QRect &rect, range_t sampleRange) +{ + auto firstSample = sampleRange.minimum; + auto length = sampleRange.length(); + + // Is it a 2-channel (complex) trace? + if (auto src = dynamic_cast>*>(sampleSource.get())) { + auto samples = src->getSamples(firstSample, length); + if (samples == nullptr) + return; + + painter.setPen(Qt::red); + plotTrace(painter, rect, reinterpret_cast(samples.get()), length, 2); + painter.setPen(Qt::blue); + plotTrace(painter, rect, reinterpret_cast(samples.get())+1, length, 2); + + // Otherwise is it single channel? + } else if (auto src = dynamic_cast*>(sampleSource.get())) { + auto samples = src->getSamples(firstSample, length); + if (samples == nullptr) + return; + + painter.setPen(Qt::green); + plotTrace(painter, rect, samples.get(), length, 1); + } else { + throw std::runtime_error("TracePlot::paintMid: Unsupported source type"); + } +} + +void TracePlot::plotTrace(QPainter &painter, QRect &rect, float *samples, off_t count, int step = 1) +{ + int xprev = 0; + int yprev = 0; + for (off_t i = 0; i < count; i++) { + float sample = samples[i*step]; + int x = (float)i / count * rect.width(); + int y = rect.height() - ((sample * rect.height()/2) + rect.height()/2); + + if (x < 0) x = 0; + if (y < 0) y = 0; + if (x >= rect.width()-1) x = rect.width()-2; + if (y >= rect.height()-1) y = rect.height()-2; + + painter.drawLine(xprev + rect.x(), yprev + rect.y(), x + rect.x(), y + rect.y()); + xprev = x; + yprev = y; + } +} diff --git a/traceplot.h b/traceplot.h new file mode 100644 index 0000000..3c56d6b --- /dev/null +++ b/traceplot.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016, Mike Walters + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once +#include +#include "abstractsamplesource.h" +#include "plot.h" +#include "util.h" + +class TracePlot : public Plot +{ + Q_OBJECT + +public: + TracePlot(std::shared_ptr source); + + void paintMid(QPainter &painter, QRect &rect, range_t sampleRange); + std::shared_ptr source() { return sampleSource; }; + +private: + std::shared_ptr sampleSource; + + void plotTrace(QPainter &painter, QRect &rect, float *samples, off_t count, int step); +}; diff --git a/util.h b/util.h new file mode 100644 index 0000000..8f1129b --- /dev/null +++ b/util.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016, Mike Walters + * Copyright (C) 2016, Jared Boone, ShareBrained Technology, Inc. + * + * This file is part of inspectrum. + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once +#include + +template const T& clamp (const T& value, const T& min, const T& max) +{ + return std::min(max, std::max(min, value)); +} + +template +struct range_t { + T minimum; + T maximum; + + range_t& operator=(const range_t &other) { + minimum = other.minimum; + maximum = other.maximum; + } + + range_t& operator=(const std::initializer_list &other) { + if (other.size() == 2) { + minimum = *other.begin(); + maximum = *(other.begin() + 1); + } + return *this; + } + + const T length() { + return maximum - minimum; + } + + const T& clip(const T& value) const { + return clamp(value, minimum, maximum); + } + + void reset_if_outside(T& value, const T& reset_value) const { + if( (value < minimum ) || + (value > maximum ) ) { + value = reset_value; + } + } + + bool below_range(const T& value) const { + return value < minimum; + } + + bool contains(const T& value) const { + // TODO: Subtle gotcha here! Range test doesn't include maximum! + return (value >= minimum) && (value < maximum); + } + + bool out_of_range(const T& value) const { + // TODO: Subtle gotcha here! Range test in contains() doesn't include maximum! + return !contains(value); + } +};