Merge branch 'refactor'

This commit is contained in:
Mike
2016-03-07 14:25:57 +00:00
37 changed files with 2015 additions and 661 deletions

View File

@@ -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})

View File

@@ -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

30
abstractsamplesource.h Normal file
View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <complex>
#include <memory>
class AbstractSampleSource
{
public:
virtual ~AbstractSampleSource() {};
};

122
cursors.cpp Normal file
View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) 2016, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <QDebug>
#include <QMouseEvent>
#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<int> 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<QMouseEvent *>(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<QMouseEvent *>(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<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::LeftButton) {
dragging = false;
return true;
}
}
return false;
}
void Cursors::paintFront(QPainter &painter, QRect &rect, range_t<off_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<int> 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<int> selection)
{
cursorPositions[0] = selection.minimum;
cursorPositions[1] = selection.maximum;
emit cursorsMoved();
}

52
cursors.h Normal file
View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2016, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <QPainter>
#include <QPoint>
#include "util.h"
class Cursors : public QObject
{
Q_OBJECT
public:
Cursors(QObject * parent);
void paintFront(QPainter &painter, QRect &rect, range_t<off_t> sampleRange);
range_t<int> selection();
void setBits(int bits);
void setSelection(range_t<int> 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};
};

7
fft.h
View File

@@ -21,12 +21,15 @@
#include <fftw3.h>
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;

31
grsamplebuffer.cpp Normal file
View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "grsamplebuffer.h"
template<typename Tin, typename Tout>
void GRSampleBuffer<Tin, Tout>::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<float>, std::complex<float>>;
template class GRSampleBuffer<std::complex<float>, float>;

41
grsamplebuffer.h Normal file
View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <complex>
#include <gnuradio/top_block.h>
#include "memory_sink.h"
#include "memory_source.h"
#include "samplebuffer.h"
template <typename Tin, typename Tout>
class GRSampleBuffer : public SampleBuffer<Tin, Tout>
{
private:
gr::top_block_sptr tb;
gr::blocks::memory_source::sptr mem_source;
gr::blocks::memory_sink::sptr mem_sink;
public:
GRSampleBuffer(SampleSource<Tin> *src, gr::top_block_sptr tb, gr::blocks::memory_source::sptr mem_source, gr::blocks::memory_sink::sptr mem_sink)
: SampleBuffer<Tin, Tout>(src), tb(tb), mem_source(mem_source), mem_sink(mem_sink) {};
virtual void work(void *input, void *output, int count);
};

View File

@@ -27,35 +27,80 @@
#include <stdexcept>
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<float>);
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<float>*)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<std::complex<float>[]> 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<std::complex<float>[]> dest(new std::complex<float>[length]);
memcpy(dest.get(), &mmapData[start], length * sizeof(std::complex<float>));
return dest;
}

View File

@@ -19,22 +19,27 @@
#pragma once
#include "fft.h"
#include <fftw3.h>
#include <memory>
#include <complex>
#include "samplesource.h"
class InputSource
class InputSource : public SampleSource<std::complex<float>>
{
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<float> *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<std::complex<float>[]> getSamples(off_t start, off_t length);
off_t count() {
return sampleCount;
};
void setSampleRate(off_t rate);
off_t rate();
};

View File

@@ -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();

View File

@@ -18,96 +18,53 @@
*/
#include <QtWidgets>
#include <QRubberBand>
#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<void (QSpinBox::*)(int)>(&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));
}

View File

@@ -21,8 +21,8 @@
#include <QMainWindow>
#include <QScrollArea>
#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;
};

43
memory_sink.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_GR_MEMORY_SINK_H
#define INCLUDED_GR_MEMORY_SINK_H
#include <gnuradio/blocks/api.h>
#include <gnuradio/sync_block.h>
namespace gr
{
namespace blocks
{
class memory_sink : virtual public sync_block
{
public:
typedef boost::shared_ptr<memory_sink> 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 */

75
memory_sink_impl.cc Normal file
View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "memory_sink_impl.h"
#include <gnuradio/io_signature.h>
#include <stdexcept>
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 */

52
memory_sink_impl.h Normal file
View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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 */

43
memory_source.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_GR_MEMORY_SOURCE_H
#define INCLUDED_GR_MEMORY_SOURCE_H
#include <gnuradio/blocks/api.h>
#include <gnuradio/sync_block.h>
namespace gr
{
namespace blocks
{
class memory_source : virtual public sync_block
{
public:
typedef boost::shared_ptr<memory_source> 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 */

76
memory_source_impl.cc Normal file
View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "memory_source_impl.h"
#include <gnuradio/io_signature.h>
#include <stdexcept>
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 */

52
memory_source_impl.h Normal file
View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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 */

39
plot.cpp Normal file
View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2016, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "plot.h"
void Plot::paintBack(QPainter &painter, QRect &rect, range_t<off_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<off_t> sampleRange)
{
}
void Plot::paintFront(QPainter &painter, QRect &rect, range_t<off_t> sampleRange)
{
}

41
plot.h Normal file
View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2016, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <QPainter>
#include "util.h"
class Plot : public QObject
{
public:
virtual void paintBack(QPainter &painter, QRect &rect, range_t<off_t> sampleRange);
virtual void paintMid(QPainter &painter, QRect &rect, range_t<off_t> sampleRange);
virtual void paintFront(QPainter &painter, QRect &rect, range_t<off_t> sampleRange);
int height() const { return _height; };
protected:
void setHeight(int height) { _height = height; };
private:
// TODO: don't hardcode this
int _height = 200;
};

231
plotview.cpp Normal file
View File

@@ -0,0 +1,231 @@
/*
* Copyright (C) 2015-2016, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "plotview.h"
#include <QDebug>
#include <QPainter>
#include <QScrollBar>
#include <gnuradio/top_block.h>
#include <gnuradio/analog/quadrature_demod_cf.h>
#include <gnuradio/blocks/multiply_const_cc.h>
#include <gnuradio/filter/firdes.h>
#include <gnuradio/filter/freq_xlating_fir_filter_ccf.h>
#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<std::complex<float>> *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<GRSampleBuffer<std::complex<float>, std::complex<float>>>(mainSampleSource, iq_tb, iq_mem_source, iq_mem_sink);
return new TracePlot(iq_src);
}
TracePlot* PlotView::createQuadratureDemodPlot(SampleSource<std::complex<float>> *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<GRSampleBuffer<std::complex<float>, float>>(
dynamic_cast<SampleSource<std::complex<float>>*>(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<int> newSelection = {
(int)((selectedSamples.minimum - horizontalScrollBar()->value()) / samplesPerLine()),
(int)((selectedSamples.maximum - horizontalScrollBar()->value()) / samplesPerLine())
};
cursors.setSelection(newSelection);
// Re-paint
viewport()->update();
}

78
plotview.h Normal file
View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2015-2016, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QAbstractScrollArea>
#include <QPaintEvent>
#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<std::complex<float>> *mainSampleSource = nullptr;
SpectrogramPlot *spectrogramPlot = nullptr;
TracePlot *iqPlot = nullptr;
std::vector<std::unique_ptr<Plot>> plots;
std::pair<off_t, off_t> viewRange;
bool selection = false;
range_t<off_t> selectedSamples;
std::pair<float, float> selectionFreq;
int fftSize = 1024;
int zoomLevel = 0;
int powerMin;
int powerMax;
bool cursorsEnabled;
TracePlot* createIQPlot(SampleSource<std::complex<float>> *src);
TracePlot* createQuadratureDemodPlot(SampleSource<std::complex<float>> *src);
int plotsHeight();
off_t samplesPerLine();
void updateView();
};

53
samplebuffer.cpp Normal file
View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "samplebuffer.h"
template <typename Tin, typename Tout>
SampleBuffer<Tin, Tout>::SampleBuffer(SampleSource<Tin> *src) : src(src)
{
src->subscribe(this);
}
template <typename Tin, typename Tout>
SampleBuffer<Tin, Tout>::~SampleBuffer()
{
src->unsubscribe(this);
}
template <typename Tin, typename Tout>
std::unique_ptr<Tout[]> SampleBuffer<Tin, Tout>::getSamples(off_t start, off_t length)
{
auto samples = src->getSamples(start, length);
if (samples == nullptr)
return nullptr;
std::unique_ptr<Tout[]> dest(new Tout[length]);
work(samples.get(), dest.get(), length);
return dest;
}
template <typename Tin, typename Tout>
void SampleBuffer<Tin, Tout>::invalidateEvent()
{
SampleSource<Tout>::invalidate();
}
template class SampleBuffer<std::complex<float>, std::complex<float>>;
template class SampleBuffer<std::complex<float>, float>;

44
samplebuffer.h Normal file
View File

@@ -0,0 +1,44 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <complex>
#include <memory>
#include "samplesource.h"
template <typename Tin, typename Tout>
class SampleBuffer : public SampleSource<Tout>, public Subscriber
{
private:
SampleSource<Tin> *src;
public:
SampleBuffer(SampleSource<Tin> *src);
~SampleBuffer();
void invalidateEvent();
virtual std::unique_ptr<Tout[]> 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();
};
};

43
samplesource.cpp Normal file
View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "samplesource.h"
template<typename T>
void SampleSource<T>::subscribe(Subscriber *subscriber)
{
subscribers.insert(subscriber);
}
template<typename T>
void SampleSource<T>::invalidate()
{
for (auto subscriber : subscribers) {
subscriber->invalidateEvent();
}
}
template<typename T>
void SampleSource<T>::unsubscribe(Subscriber *subscriber)
{
subscribers.erase(subscriber);
}
template class SampleSource<std::complex<float>>;
template class SampleSource<float>;

47
samplesource.h Normal file
View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <complex>
#include <memory>
#include <set>
#include "abstractsamplesource.h"
#include "subscriber.h"
template<typename T>
class SampleSource : public AbstractSampleSource
{
public:
virtual ~SampleSource() {};
virtual std::unique_ptr<T[]> 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<Subscriber*> subscribers;
};

View File

@@ -1,330 +0,0 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "spectrogram.h"
#include <QDebug>
#include <QElapsedTimer>
#include <QPainter>
#include <QPaintEvent>
#include <QRect>
#include <cstdlib>
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 <class T> 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;
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QCache>
#include <QWidget>
#include "fft.h"
#include "inputsource.h"
#include <math.h>
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<float[]> window;
fftwf_complex *lineBuffer = nullptr;
QCache<TileCacheKey, QPixmap> pixmapCache;
QCache<TileCacheKey, float> 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;
};

View File

@@ -24,74 +24,120 @@
#include <cmath>
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("<b>Spectrogram</b>")));
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("<b>Time selection</b>")));
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");
}
}

View File

@@ -24,37 +24,46 @@
#include <QLineEdit>
#include <QPushButton>
#include <QSlider>
#include <QSpinBox>
#include <QCheckBox>
#include <QLabel>
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;
};

204
spectrogramplot.cpp Normal file
View File

@@ -0,0 +1,204 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "spectrogramplot.h"
#include <QDebug>
#include <QElapsedTimer>
#include <QPainter>
#include <QPaintEvent>
#include <QRect>
#include <cstdlib>
#include "util.h"
SpectrogramPlot::SpectrogramPlot(SampleSource<std::complex<float>> *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<off_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;
}

109
spectrogramplot.h Normal file
View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2015, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QCache>
#include <QWidget>
#include "fft.h"
#include "inputsource.h"
#include "plot.h"
#include <memory>
#include <math.h>
static const double Tau = M_PI * 2.0;
class TileCacheKey;
class SpectrogramPlot : public Plot
{
Q_OBJECT
public:
SpectrogramPlot(SampleSource<std::complex<float>> *src);
void paintMid(QPainter &painter, QRect &rect, range_t<off_t> sampleRange);
QSize sizeHint() const;
int getHeight();
int getStride();
off_t lineToSample(off_t line);
SampleSource<std::complex<float>> *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> fft;
std::unique_ptr<float[]> window;
fftwf_complex *lineBuffer = nullptr;
QCache<TileCacheKey, QPixmap> pixmapCache;
QCache<TileCacheKey, float> 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;
};

27
subscriber.h Normal file
View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2016, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
class Subscriber
{
public:
virtual void invalidateEvent() = 0;
};

74
traceplot.cpp Normal file
View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2016, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "samplesource.h"
#include "traceplot.h"
TracePlot::TracePlot(std::shared_ptr<AbstractSampleSource> source) : sampleSource(source) {
}
void TracePlot::paintMid(QPainter &painter, QRect &rect, range_t<off_t> sampleRange)
{
auto firstSample = sampleRange.minimum;
auto length = sampleRange.length();
// Is it a 2-channel (complex) trace?
if (auto src = dynamic_cast<SampleSource<std::complex<float>>*>(sampleSource.get())) {
auto samples = src->getSamples(firstSample, length);
if (samples == nullptr)
return;
painter.setPen(Qt::red);
plotTrace(painter, rect, reinterpret_cast<float*>(samples.get()), length, 2);
painter.setPen(Qt::blue);
plotTrace(painter, rect, reinterpret_cast<float*>(samples.get())+1, length, 2);
// Otherwise is it single channel?
} else if (auto src = dynamic_cast<SampleSource<float>*>(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;
}
}

40
traceplot.h Normal file
View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2016, Mike Walters <mike@flomp.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include "abstractsamplesource.h"
#include "plot.h"
#include "util.h"
class TracePlot : public Plot
{
Q_OBJECT
public:
TracePlot(std::shared_ptr<AbstractSampleSource> source);
void paintMid(QPainter &painter, QRect &rect, range_t<off_t> sampleRange);
std::shared_ptr<AbstractSampleSource> source() { return sampleSource; };
private:
std::shared_ptr<AbstractSampleSource> sampleSource;
void plotTrace(QPainter &painter, QRect &rect, float *samples, off_t count, int step);
};

75
util.h Normal file
View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2016, Mike Walters <mike@flomp.net>
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <algorithm>
template <class T> const T& clamp (const T& value, const T& min, const T& max)
{
return std::min(max, std::max(min, value));
}
template<class T>
struct range_t {
T minimum;
T maximum;
range_t<T>& operator=(const range_t<T> &other) {
minimum = other.minimum;
maximum = other.maximum;
}
range_t<T>& operator=(const std::initializer_list<T> &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);
}
};