mirror of
https://github.com/miek/inspectrum.git
synced 2026-02-20 01:31:35 +01:00
552 lines
16 KiB
C++
552 lines
16 KiB
C++
/*
|
|
* 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 <iostream>
|
|
#include <fstream>
|
|
#include <QApplication>
|
|
#include <QDebug>
|
|
#include <QMenu>
|
|
#include <QPainter>
|
|
#include <QScrollBar>
|
|
#include <QFileDialog>
|
|
#include <QRadioButton>
|
|
#include <QVBoxLayout>
|
|
#include <QGroupBox>
|
|
#include <QGridLayout>
|
|
#include <QSpinBox>
|
|
#include "plots.h"
|
|
|
|
PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0})
|
|
{
|
|
mainSampleSource = input;
|
|
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
|
|
setMouseTracking(true);
|
|
enableCursors(false);
|
|
connect(&cursors, SIGNAL(cursorsMoved()), this, SLOT(cursorsMoved()));
|
|
|
|
spectrogramPlot = new SpectrogramPlot(std::shared_ptr<SampleSource<std::complex<float>>>(mainSampleSource));
|
|
auto tunerOutput = std::dynamic_pointer_cast<SampleSource<std::complex<float>>>(spectrogramPlot->output());
|
|
|
|
enableScales(true);
|
|
|
|
addPlot(spectrogramPlot);
|
|
|
|
mainSampleSource->subscribe(this);
|
|
}
|
|
|
|
void PlotView::addPlot(Plot *plot)
|
|
{
|
|
plots.emplace_back(plot);
|
|
connect(plot, &Plot::repaint, this, &PlotView::repaint);
|
|
}
|
|
|
|
void PlotView::contextMenuEvent(QContextMenuEvent * event)
|
|
{
|
|
QMenu menu;
|
|
|
|
// Get selected plot
|
|
Plot *selectedPlot = nullptr;
|
|
auto it = plots.begin();
|
|
int y = -verticalScrollBar()->value();
|
|
for (; it != plots.end(); it++) {
|
|
auto&& plot = *it;
|
|
if (range_t<int>{y, y + plot->height()}.contains(event->pos().y())) {
|
|
selectedPlot = plot.get();
|
|
break;
|
|
}
|
|
y += plot->height();
|
|
}
|
|
if (selectedPlot == nullptr)
|
|
return;
|
|
|
|
// Add actions to add derived plots
|
|
// that are compatible with selectedPlot's output
|
|
QMenu *plotsMenu = menu.addMenu("Add derived plot");
|
|
auto src = selectedPlot->output();
|
|
auto compatiblePlots = as_range(Plots::plots.equal_range(src->sampleType()));
|
|
for (auto p : compatiblePlots) {
|
|
auto plotInfo = p.second;
|
|
auto action = new QAction(QString("Add %1").arg(plotInfo.name), plotsMenu);
|
|
auto plotCreator = plotInfo.creator;
|
|
connect(
|
|
action, &QAction::triggered,
|
|
this, [=]() {
|
|
addPlot(plotCreator(src));
|
|
}
|
|
);
|
|
plotsMenu->addAction(action);
|
|
}
|
|
|
|
// Add action to extract symbols from selected plot
|
|
auto extract = new QAction("Extract symbols (to stdout)...", &menu);
|
|
connect(
|
|
extract, &QAction::triggered,
|
|
this, [=]() {
|
|
extractSymbols(src);
|
|
}
|
|
);
|
|
extract->setEnabled(cursorsEnabled && (src->sampleType() == typeid(float)));
|
|
menu.addAction(extract);
|
|
|
|
// Add action to export the selected samples into a file
|
|
auto save = new QAction("Export samples to file...", &menu);
|
|
connect(
|
|
save, &QAction::triggered,
|
|
this, [=]() {
|
|
if (selectedPlot == spectrogramPlot) {
|
|
exportSamples(spectrogramPlot->tunerEnabled() ? spectrogramPlot->output() : spectrogramPlot->input());
|
|
} else {
|
|
exportSamples(src);
|
|
}
|
|
}
|
|
);
|
|
menu.addAction(save);
|
|
|
|
// Add action to remove the selected plot
|
|
auto rem = new QAction("Remove plot", &menu);
|
|
connect(
|
|
rem, &QAction::triggered,
|
|
this, [=]() {
|
|
plots.erase(it);
|
|
}
|
|
);
|
|
// Don't allow remove the first plot (the spectrogram)
|
|
rem->setEnabled(it != plots.begin());
|
|
menu.addAction(rem);
|
|
|
|
updateViewRange(false);
|
|
if(menu.exec(event->globalPos()))
|
|
updateView(false);
|
|
}
|
|
|
|
void PlotView::cursorsMoved()
|
|
{
|
|
selectedSamples = {
|
|
horizontalScrollBar()->value() + cursors.selection().minimum * samplesPerLine(),
|
|
horizontalScrollBar()->value() + cursors.selection().maximum * samplesPerLine()
|
|
};
|
|
|
|
emitTimeSelection();
|
|
viewport()->update();
|
|
}
|
|
|
|
void PlotView::emitTimeSelection()
|
|
{
|
|
off_t sampleCount = selectedSamples.length();
|
|
float selectionTime = sampleCount / (float)mainSampleSource->rate();
|
|
emit timeSelectionChanged(selectionTime);
|
|
}
|
|
|
|
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});
|
|
cursorsMoved();
|
|
}
|
|
viewport()->update();
|
|
}
|
|
|
|
bool PlotView::viewportEvent(QEvent *event) {
|
|
// Handle wheel events for zooming (before the parent's handler to stop normal scrolling)
|
|
if (event->type() == QEvent::Wheel) {
|
|
QWheelEvent *wheelEvent = (QWheelEvent*)event;
|
|
if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
|
|
if (wheelEvent->angleDelta().y() > 0) {
|
|
emit zoomIn();
|
|
} else if (wheelEvent->angleDelta().y() < 0) {
|
|
emit zoomOut();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Handle parent eveents
|
|
QAbstractScrollArea::viewportEvent(event);
|
|
|
|
// Pass mouse events to individual plot objects
|
|
if (event->type() == QEvent::MouseButtonPress ||
|
|
event->type() == QEvent::MouseMove ||
|
|
event->type() == QEvent::MouseButtonRelease ||
|
|
event->type() == QEvent::Leave) {
|
|
|
|
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
|
|
|
int plotY = -verticalScrollBar()->value();
|
|
for (auto&& plot : plots) {
|
|
bool result = plot->mouseEvent(
|
|
event->type(),
|
|
QMouseEvent(
|
|
event->type(),
|
|
QPoint(mouseEvent->pos().x(), mouseEvent->pos().y() - plotY),
|
|
mouseEvent->button(),
|
|
mouseEvent->buttons(),
|
|
QApplication::keyboardModifiers()
|
|
)
|
|
);
|
|
if (result)
|
|
return true;
|
|
plotY += plot->height();
|
|
}
|
|
|
|
if (cursorsEnabled)
|
|
if (cursors.mouseEvent(event->type(), *mouseEvent))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void PlotView::extractSymbols(std::shared_ptr<AbstractSampleSource> src)
|
|
{
|
|
if (!cursorsEnabled)
|
|
return;
|
|
auto floatSrc = std::dynamic_pointer_cast<SampleSource<float>>(src);
|
|
if (!floatSrc)
|
|
return;
|
|
auto samples = floatSrc->getSamples(selectedSamples.minimum, selectedSamples.length());
|
|
auto step = (float)selectedSamples.length() / cursors.segments();
|
|
auto symbols = std::vector<float>();
|
|
for (auto i = step / 2; i < selectedSamples.length(); i += step)
|
|
{
|
|
symbols.push_back(samples[i]);
|
|
}
|
|
for (auto f : symbols)
|
|
std::cout << f << ", ";
|
|
std::cout << std::endl << std::flush;
|
|
}
|
|
|
|
void PlotView::exportSamples(std::shared_ptr<AbstractSampleSource> src)
|
|
{
|
|
if (src->sampleType() == typeid(std::complex<float>)) {
|
|
exportSamples<std::complex<float>>(src);
|
|
} else {
|
|
exportSamples<float>(src);
|
|
}
|
|
}
|
|
|
|
template<typename SOURCETYPE>
|
|
void PlotView::exportSamples(std::shared_ptr<AbstractSampleSource> src)
|
|
{
|
|
auto sampleSrc = std::dynamic_pointer_cast<SampleSource<SOURCETYPE>>(src);
|
|
if (!sampleSrc) {
|
|
return;
|
|
}
|
|
|
|
QFileDialog dialog(this);
|
|
dialog.setAcceptMode(QFileDialog::AcceptSave);
|
|
dialog.setFileMode(QFileDialog::AnyFile);
|
|
dialog.setNameFilter(getFileNameFilter<SOURCETYPE>());
|
|
dialog.setOption(QFileDialog::DontUseNativeDialog, true);
|
|
|
|
QGroupBox groupBox("Selection To Export", &dialog);
|
|
QVBoxLayout vbox(&groupBox);
|
|
|
|
QRadioButton cursorSelection("Cursor Selection", &groupBox);
|
|
QRadioButton currentView("Current View", &groupBox);
|
|
QRadioButton completeFile("Complete File (Experimental)", &groupBox);
|
|
|
|
if (cursorsEnabled) {
|
|
cursorSelection.setChecked(true);
|
|
} else {
|
|
currentView.setChecked(true);
|
|
cursorSelection.setEnabled(false);
|
|
}
|
|
|
|
vbox.addWidget(&cursorSelection);
|
|
vbox.addWidget(¤tView);
|
|
vbox.addWidget(&completeFile);
|
|
vbox.addStretch(1);
|
|
|
|
groupBox.setLayout(&vbox);
|
|
|
|
QGridLayout *l = dialog.findChild<QGridLayout*>();
|
|
l->addWidget(&groupBox, 4, 1);
|
|
|
|
QGroupBox groupBox2("Decimation");
|
|
QSpinBox decimation(&groupBox2);
|
|
decimation.setValue(1 / sampleSrc->relativeBandwidth());
|
|
|
|
QVBoxLayout vbox2;
|
|
vbox2.addWidget(&decimation);
|
|
|
|
groupBox2.setLayout(&vbox2);
|
|
l->addWidget(&groupBox2, 4, 2);
|
|
|
|
if (dialog.exec()) {
|
|
QStringList fileNames = dialog.selectedFiles();
|
|
|
|
off_t start, end;
|
|
if (cursorSelection.isChecked()) {
|
|
start = selectedSamples.minimum;
|
|
end = start + selectedSamples.length();
|
|
} else if(currentView.isChecked()) {
|
|
start = viewRange.minimum;
|
|
end = start + viewRange.length();
|
|
} else {
|
|
start = 0;
|
|
end = sampleSrc->count();
|
|
}
|
|
|
|
std::ofstream os (fileNames[0].toStdString(), std::ios::binary);
|
|
|
|
off_t index;
|
|
// viewRange.length() is used as some less arbitrary step value
|
|
off_t step = viewRange.length();
|
|
|
|
for (index = start; index < end; index += step) {
|
|
off_t length = std::min(step, end - index);
|
|
auto samples = sampleSrc->getSamples(index, length);
|
|
if (samples != nullptr) {
|
|
for (auto i = 0; i < length; i += decimation.value()) {
|
|
os.write((const char*)&samples[i], sizeof(SOURCETYPE));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PlotView::invalidateEvent()
|
|
{
|
|
horizontalScrollBar()->setMinimum(0);
|
|
horizontalScrollBar()->setMaximum(mainSampleSource->count());
|
|
}
|
|
|
|
void PlotView::repaint()
|
|
{
|
|
viewport()->update();
|
|
}
|
|
|
|
void PlotView::setCursorSegments(int segments)
|
|
{
|
|
// Calculate number of samples per segment
|
|
float sampPerSeg = (float)selectedSamples.length() / cursors.segments();
|
|
|
|
// Alter selection to keep samples per segment the same
|
|
selectedSamples.maximum = selectedSamples.minimum + (segments * sampPerSeg + 0.5f);
|
|
|
|
cursors.setSegments(segments);
|
|
updateView();
|
|
emitTimeSelection();
|
|
}
|
|
|
|
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(true);
|
|
}
|
|
|
|
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); \
|
|
y += plot->height(); \
|
|
} \
|
|
}
|
|
|
|
PLOT_LAYER(paintBack);
|
|
PLOT_LAYER(paintMid);
|
|
PLOT_LAYER(paintFront);
|
|
if (cursorsEnabled)
|
|
cursors.paintFront(painter, rect, viewRange);
|
|
|
|
if (timeScaleEnabled) {
|
|
paintTimeScale(painter, rect, viewRange);
|
|
}
|
|
|
|
|
|
#undef PLOT_LAYER
|
|
}
|
|
|
|
void PlotView::paintTimeScale(QPainter &painter, QRect &rect, range_t<off_t> sampleRange)
|
|
{
|
|
float startTime = (float)sampleRange.minimum / sampleRate;
|
|
float stopTime = (float)sampleRange.maximum / sampleRate;
|
|
float duration = stopTime - startTime;
|
|
|
|
if (duration <= 0)
|
|
return;
|
|
|
|
painter.save();
|
|
|
|
QPen pen(Qt::white, 1, Qt::SolidLine);
|
|
painter.setPen(pen);
|
|
QFontMetrics fm(painter.font());
|
|
|
|
int tickWidth = 80;
|
|
int maxTicks = rect.width() / tickWidth;
|
|
|
|
double durationPerTick = 10 * pow(10, floor(log(duration / maxTicks) / log(10)));
|
|
|
|
double firstTick = int(startTime / durationPerTick) * durationPerTick;
|
|
|
|
double tick = firstTick;
|
|
|
|
while (tick <= stopTime) {
|
|
|
|
off_t tickSample = tick * sampleRate;
|
|
int tickLine = (tickSample - sampleRange.minimum) / samplesPerLine();
|
|
|
|
char buf[128];
|
|
snprintf(buf, sizeof(buf), "%.06f", tick);
|
|
painter.drawLine(tickLine, 0, tickLine, 30);
|
|
painter.drawText(tickLine + 2, 25, buf);
|
|
|
|
tick += durationPerTick;
|
|
}
|
|
|
|
// Draw small ticks
|
|
durationPerTick /= 10;
|
|
firstTick = int(startTime / durationPerTick) * durationPerTick;
|
|
tick = firstTick;
|
|
while (tick <= stopTime) {
|
|
|
|
off_t tickSample = tick * sampleRate;
|
|
int tickLine = (tickSample - sampleRange.minimum) / samplesPerLine();
|
|
|
|
painter.drawLine(tickLine, 0, tickLine, 10);
|
|
tick += durationPerTick;
|
|
}
|
|
|
|
painter.restore();
|
|
}
|
|
|
|
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::updateViewRange(bool reCenter)
|
|
{
|
|
// Store old view for recentering
|
|
auto oldViewRange = viewRange;
|
|
|
|
// Update current view
|
|
viewRange = {
|
|
horizontalScrollBar()->value(),
|
|
std::min(horizontalScrollBar()->value() + width() * samplesPerLine(), mainSampleSource->count())
|
|
};
|
|
|
|
// Adjust time offset to zoom around central sample
|
|
if (reCenter) {
|
|
horizontalScrollBar()->setValue(
|
|
horizontalScrollBar()->value() + (oldViewRange.length() - viewRange.length()) / 2
|
|
);
|
|
}
|
|
}
|
|
|
|
void PlotView::updateView(bool reCenter)
|
|
{
|
|
updateViewRange(reCenter);
|
|
horizontalScrollBar()->setMaximum(std::max(off_t(0), mainSampleSource->count() - ((width() - 1) * samplesPerLine())));
|
|
|
|
verticalScrollBar()->setMaximum(std::max(0, plotsHeight() - viewport()->height()));
|
|
|
|
// Update cursors
|
|
range_t<int> newSelection = {
|
|
(int)((selectedSamples.minimum - horizontalScrollBar()->value()) / samplesPerLine()),
|
|
(int)((selectedSamples.maximum - horizontalScrollBar()->value()) / samplesPerLine())
|
|
};
|
|
cursors.setSelection(newSelection);
|
|
|
|
// Re-paint
|
|
viewport()->update();
|
|
}
|
|
|
|
void PlotView::setSampleRate(off_t rate)
|
|
{
|
|
sampleRate = rate;
|
|
|
|
if (spectrogramPlot != nullptr)
|
|
spectrogramPlot->setSampleRate(rate);
|
|
|
|
emitTimeSelection();
|
|
}
|
|
|
|
void PlotView::enableScales(bool enabled)
|
|
{
|
|
timeScaleEnabled = enabled;
|
|
|
|
if (spectrogramPlot != nullptr)
|
|
spectrogramPlot->enableScales(enabled);
|
|
|
|
viewport()->update();
|
|
}
|
|
|