# HG changeset patch # User Chris Cannam # Date 1578668067 0 # Node ID 1f80a514ce296c7d89f3faa9806a28a4fbee1097 # Parent 76e4302a3fc2e6d822113265cd3876c79f75b896# Parent a6a31908bd137f82b9808a6e53db7282b6206ed1 Merge from branch spectrogram-export diff -r 76e4302a3fc2 -r 1f80a514ce29 files.pri --- a/files.pri Fri Nov 22 14:12:50 2019 +0000 +++ b/files.pri Fri Jan 10 14:54:27 2020 +0000 @@ -1,5 +1,6 @@ SVGUI_HEADERS += \ + layer/Colour3DPlotExporter.h \ layer/Colour3DPlotLayer.h \ layer/Colour3DPlotRenderer.h \ layer/ColourDatabase.h \ @@ -95,6 +96,7 @@ widgets/WindowTypeSelector.h SVGUI_SOURCES += \ + layer/Colour3DPlotExporter.cpp \ layer/Colour3DPlotLayer.cpp \ layer/Colour3DPlotRenderer.cpp \ layer/ColourDatabase.cpp \ diff -r 76e4302a3fc2 -r 1f80a514ce29 layer/Colour3DPlotExporter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/layer/Colour3DPlotExporter.cpp Fri Jan 10 14:54:27 2020 +0000 @@ -0,0 +1,244 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + + 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 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. +*/ + +#include "Colour3DPlotExporter.h" + +#include "data/model/EditableDenseThreeDimensionalModel.h" +#include "data/model/FFTModel.h" + +#include "VerticalBinLayer.h" + +Colour3DPlotExporter::Colour3DPlotExporter(Sources sources, Parameters params) : + m_sources(sources), + m_params(params) +{ + SVCERR << "Colour3DPlotExporter::Colour3DPlotExporter: constructed at " + << this << endl; +} + +Colour3DPlotExporter::~Colour3DPlotExporter() +{ + SVCERR << "Colour3DPlotExporter[" << this << "]::~Colour3DPlotExporter" + << endl; +} + +void +Colour3DPlotExporter::discardSources() +{ + SVCERR << "Colour3DPlotExporter[" << this << "]::discardSources" + << endl; + QMutexLocker locker(&m_mutex); + m_sources.verticalBinLayer = nullptr; + m_sources.source = {}; + m_sources.fft = {}; + m_sources.provider = nullptr; +} + +QString +Colour3DPlotExporter::getDelimitedDataHeaderLine(QString delimiter, + DataExportOptions) const +{ + auto model = + ModelById::getAs(m_sources.source); + + auto layer = m_sources.verticalBinLayer; + auto provider = m_sources.provider; + + if (!model || !layer) { + SVCERR << "ERROR: Colour3DPlotExporter::getDelimitedDataHeaderLine: Source model and layer required" << endl; + return {}; + } + + int minbin = 0; + int sh = model->getHeight(); + int nbins = sh; + + if (provider) { + + minbin = layer->getIBinForY(provider, provider->getPaintHeight()); + if (minbin >= sh) minbin = sh - 1; + if (minbin < 0) minbin = 0; + + nbins = layer->getIBinForY(provider, 0) - minbin + 1; + if (minbin + nbins > sh) nbins = sh - minbin; + } + + QStringList list; + + switch (m_params.timestampFormat) { + case TimestampFormat::None: + break; + case TimestampFormat::Frames: + list << "FRAME"; + break; + case TimestampFormat::Seconds: + list << "TIME"; + break; + } + + if (m_params.binDisplay == BinDisplay::PeakFrequencies) { + for (int i = 0; i < nbins/4; ++i) { + list << QString("FREQ %1").arg(i+1) + << QString("MAG %1").arg(i+1); + } + } else { + bool hasValues = model->hasBinValues(); + QString unit = (hasValues ? model->getBinValueUnit() : ""); + for (int i = minbin; i < minbin + nbins; ++i) { + QString name = model->getBinName(i); + if (name == "") { + if (hasValues) { + if (unit != "") { + name = QString("BIN %1: %2 %3") + .arg(i+1) + .arg(model->getBinValue(i)) + .arg(unit); + } else { + name = QString("BIN %1: %2") + .arg(i+1) + .arg(model->getBinValue(i)); + } + } else { + name = QString("BIN %1") + .arg(i+1); + } + } + list << name; + } + } + + return list.join(delimiter); +} + +QString +Colour3DPlotExporter::toDelimitedDataString(QString delimiter, + DataExportOptions, + sv_frame_t startFrame, + sv_frame_t duration) const +{ + QMutexLocker locker(&m_mutex); + + BinDisplay binDisplay = m_params.binDisplay; + + auto model = + ModelById::getAs(m_sources.source); + auto fftModel = + ModelById::getAs(m_sources.fft); + + auto layer = m_sources.verticalBinLayer; + auto provider = m_sources.provider; + + if (!model || !layer) { + SVCERR << "ERROR: Colour3DPlotExporter::toDelimitedDataString: Source model and layer required" << endl; + return {}; + } + if ((binDisplay == BinDisplay::PeakFrequencies) && !fftModel) { + SVCERR << "ERROR: Colour3DPlotExporter::toDelimitedDataString: FFT model required in peak frequencies mode" << endl; + return {}; + } + + int minbin = 0; + int sh = model->getHeight(); + int nbins = sh; + + if (provider) { + + minbin = layer->getIBinForY(provider, provider->getPaintHeight()); + if (minbin >= sh) minbin = sh - 1; + if (minbin < 0) minbin = 0; + + nbins = layer->getIBinForY(provider, 0) - minbin + 1; + if (minbin + nbins > sh) nbins = sh - minbin; + } + + int w = model->getWidth(); + + QString s; + + for (int i = 0; i < w; ++i) { + + sv_frame_t fr = model->getStartFrame() + i * model->getResolution(); + if (fr < startFrame || fr >= startFrame + duration) { + continue; + } + + //!!! (+ phase layer type) + + auto column = model->getColumn(i); + column = ColumnOp::Column(column.data() + minbin, + column.data() + minbin + nbins); + + // The scale factor is always applied + column = ColumnOp::applyGain(column, m_params.scaleFactor); + + QStringList list; + + switch (m_params.timestampFormat) { + case TimestampFormat::None: + break; + case TimestampFormat::Frames: + list << QString("%1").arg(fr); + break; + case TimestampFormat::Seconds: + list << RealTime::frame2RealTime(fr, model->getSampleRate()) + .toString().c_str(); + break; + } + + if (binDisplay == BinDisplay::PeakFrequencies) { + + FFTModel::PeakSet peaks = fftModel->getPeakFrequencies + (FFTModel::AllPeaks, i, minbin, minbin + nbins - 1); + + // We don't apply normalisation or gain to the output, but + // we *do* perform thresholding when exporting the + // peak-frequency spectrogram, to give the user an + // opportunity to cut irrelevant peaks. And to make that + // match the display, we have to apply both normalisation + // and gain locally for thresholding + + auto toTest = ColumnOp::normalize(column, m_params.normalization); + toTest = ColumnOp::applyGain(toTest, m_params.gain); + + for (const auto &p: peaks) { + + int bin = p.first; + + if (toTest[bin - minbin] < m_params.threshold) { + continue; + } + + double freq = p.second; + double value = column[bin - minbin]; + + list << QString("%1").arg(freq) << QString("%1").arg(value); + } + + } else { + + if (binDisplay == BinDisplay::PeakBins) { + column = ColumnOp::peakPick(column); + } + + for (auto value: column) { + list << QString("%1").arg(value); + } + } + + s += list.join(delimiter) + "\n"; + } + + return s; +} + diff -r 76e4302a3fc2 -r 1f80a514ce29 layer/Colour3DPlotExporter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/layer/Colour3DPlotExporter.h Fri Jan 10 14:54:27 2020 +0000 @@ -0,0 +1,139 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + + 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 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef COLOUR_3D_PLOT_EXPORTER_H +#define COLOUR_3D_PLOT_EXPORTER_H + +#include "Colour3DPlotRenderer.h" + +class Colour3DPlotExporter : public Model +{ + Q_OBJECT + +public: + struct Sources { + // These must all outlive this class, or else discardSources() + // must be called + const VerticalBinLayer *verticalBinLayer; // always + ModelId source; // always; a DenseThreeDimensionalModel + ModelId fft; // optionally; an FFTModel; used for phase/peak-freq modes + const LayerGeometryProvider *provider; // optionally + }; + + enum class TimestampFormat { + None, + Seconds, + Frames + }; + + struct Parameters { + Parameters() : + binDisplay(BinDisplay::AllBins), + scaleFactor(1.0), + threshold(0.0), + gain(1.0), + normalization(ColumnNormalization::None), + timestampFormat(TimestampFormat::None) { } + + /** Selection of bins to include in the export. */ + BinDisplay binDisplay; + + /** Initial scale factor (e.g. for FFT scaling). This factor + * is actually applied to exported values, in contrast to the + * gain value below based on the ColourScale parameter. */ + double scaleFactor; + + /** Threshold below which every value is mapped to background + * pixel 0 in the display, matching the ColourScale object + * parameters. This is used for thresholding in + * peak-frequency output only. */ + double threshold; + + /** Gain that is applied before thresholding, in the display, + * matching the ColourScale object parameters. This is used + * only to determined the thresholding level. The exported + * values have the scaleFactor applied, but not this gain. */ + double gain; + + /** Type of column normalization. Again, this is only used to + * calculate thresholding level. The exported values are + * un-normalized. */ + ColumnNormalization normalization; + + /** Format to use for the timestamp column. If None, no + * timestamp column will be included. */ + TimestampFormat timestampFormat; + }; + + Colour3DPlotExporter(Sources sources, Parameters parameters); + ~Colour3DPlotExporter(); + + void discardSources(); + + QString getDelimitedDataHeaderLine(QString, DataExportOptions) const override; + + QString toDelimitedDataString(QString, DataExportOptions, + sv_frame_t, sv_frame_t) const override; + + + // Further Model methods that we just delegate + + bool isOK() const override { + if (auto model = ModelById::get(m_sources.source)) { + return model->isOK(); + } + return false; + } + + sv_frame_t getStartFrame() const override { + if (auto model = ModelById::get(m_sources.source)) { + return model->getStartFrame(); + } + return 0; + } + + sv_frame_t getTrueEndFrame() const override { + if (auto model = ModelById::get(m_sources.source)) { + return model->getTrueEndFrame(); + } + return 0; + } + + sv_samplerate_t getSampleRate() const override { + if (auto model = ModelById::get(m_sources.source)) { + return model->getSampleRate(); + } + return 0; + } + + QString getTypeName() const override { + if (auto model = ModelById::get(m_sources.source)) { + return model->getTypeName(); + } + return "(exporter)"; // internal fallback, no translation needed + } + + int getCompletion() const override { + if (auto model = ModelById::get(m_sources.source)) { + return model->getCompletion(); + } + return 0; + } + +private: + Sources m_sources; + Parameters m_params; +}; + +#endif diff -r 76e4302a3fc2 -r 1f80a514ce29 layer/Colour3DPlotLayer.cpp --- a/layer/Colour3DPlotLayer.cpp Fri Nov 22 14:12:50 2019 +0000 +++ b/layer/Colour3DPlotLayer.cpp Fri Jan 10 14:54:27 2020 +0000 @@ -22,6 +22,7 @@ #include "ColourMapper.h" #include "LayerGeometryProvider.h" #include "PaintAssistant.h" +#include "Colour3DPlotExporter.h" #include "data/model/Dense3DModelPeakCache.h" @@ -70,6 +71,14 @@ Colour3DPlotLayer::~Colour3DPlotLayer() { invalidateRenderers(); + + for (auto exporterId: m_exporters) { + if (auto exporter = + ModelById::getAs(exporterId)) { + exporter->discardSources(); + } + ModelById::release(exporterId); + } } const ZoomConstraint * @@ -208,6 +217,58 @@ } ModelId +Colour3DPlotLayer::getExportModel(LayerGeometryProvider *v) const +{ + // Creating Colour3DPlotExporters is cheap, so we create one on + // every call - calls probably being infrequent - to avoid having + // to worry about view lifecycles. + + auto model = ModelById::getAs(m_model); + if (!model) return {}; + int viewId = v->getId(); + + Colour3DPlotExporter::Sources sources; + sources.verticalBinLayer = this; + sources.source = m_model; + sources.provider = v; + + double minValue = 0.0; + double maxValue = 1.0; + + if (m_normalizeVisibleArea && m_viewMags[viewId].isSet()) { + minValue = m_viewMags[viewId].getMin(); + maxValue = m_viewMags[viewId].getMax(); + } else if (m_normalization == ColumnNormalization::Hybrid) { + minValue = 0; + maxValue = log10(model->getMaximumLevel() + 1.0); + } else if (m_normalization == ColumnNormalization::None) { + minValue = model->getMinimumLevel(); + maxValue = model->getMaximumLevel(); + } + + if (maxValue <= minValue) { + maxValue = minValue + 0.1f; + + if (!(maxValue > minValue)) { // one of them must be NaN or Inf + SVCERR << "WARNING: Colour3DPlotLayer::getExportModel: resetting " + << "minValue and maxValue to zero and one" << endl; + minValue = 0.f; + maxValue = 1.f; + } + } + + Colour3DPlotExporter::Parameters params; + params.threshold = minValue; + params.gain = m_gain; // matching ColourScale in getRenderer + params.normalization = m_normalization; + + ModelId exporter = ModelById::add + (std::make_shared(sources, params)); + m_exporters.push_back(exporter); + return exporter; +} + +ModelId Colour3DPlotLayer::getPeakCache() const { if (m_peakCache.isNone()) { diff -r 76e4302a3fc2 -r 1f80a514ce29 layer/Colour3DPlotLayer.h --- a/layer/Colour3DPlotLayer.h Fri Nov 22 14:12:50 2019 +0000 +++ b/layer/Colour3DPlotLayer.h Fri Jan 10 14:54:27 2020 +0000 @@ -48,6 +48,8 @@ ModelId getModel() const override { return m_model; } + ModelId getExportModel(LayerGeometryProvider *) const override; + const ZoomConstraint *getZoomConstraint() const override; void paint(LayerGeometryProvider *v, @@ -192,6 +194,8 @@ void invalidatePeakCache(); ModelId getPeakCache() const; + mutable std::vector m_exporters; // used, waiting to be released + typedef std::map ViewMagMap; // key is view id mutable ViewMagMap m_viewMags; mutable ViewMagMap m_lastRenderedMags; // when in normalizeVisibleArea mode diff -r 76e4302a3fc2 -r 1f80a514ce29 layer/Layer.h --- a/layer/Layer.h Fri Nov 22 14:12:50 2019 +0000 +++ b/layer/Layer.h Fri Jan 10 14:54:27 2020 +0000 @@ -71,6 +71,30 @@ * model here, return None. */ ModelId getSourceModel() const; + + /** + * Return the ID of a model representing the contents of this + * layer in a form suitable for export to a tabular file format + * such as CSV. + * + * In most cases this will be the same as returned by + * getModel(). The exceptions are those layers such as + * SpectrogramLayer, that are "only" alternative views of + * time-domain sample data. For such layers, getModel() will + * return the backing time-domain data, for example as a + * ReadOnlyWaveFileModel; but getExportModel() will return a + * model, possibly "local to" the layer, which adapts this into + * the form shown in the layer for a given view so that the export + * matches the layer's visible contents. + * + * Because this is supposed to match the contents of the view + * rather than the backing model, it's necessary to pass in a view + * (or LayerGeometryProvider) so that the layer can retrieve its + * vertical extents for export. + */ + virtual ModelId getExportModel(LayerGeometryProvider *) const { + return getModel(); + } /** * Return a zoom constraint object defining the supported zoom diff -r 76e4302a3fc2 -r 1f80a514ce29 layer/SpectrogramLayer.cpp --- a/layer/SpectrogramLayer.cpp Fri Nov 22 14:12:50 2019 +0000 +++ b/layer/SpectrogramLayer.cpp Fri Jan 10 14:54:27 2020 +0000 @@ -34,6 +34,7 @@ #include "PianoScale.h" #include "PaintAssistant.h" #include "Colour3DPlotRenderer.h" +#include "Colour3DPlotExporter.h" #include #include @@ -142,6 +143,40 @@ recreateFFTModel(); } +ModelId +SpectrogramLayer::getExportModel(LayerGeometryProvider *v) const +{ + // Creating Colour3DPlotExporters is cheap, so we create one on + // every call - calls probably being infrequent - to avoid having + // to worry about view lifecycles. We can't delete them on the + // same call of course as we need to return a valid id, so we push + // them onto a list that then gets cleared (with calls to + // Colour3DPlotExporter::discardSources() and + // ModelById::release()) in deleteDerivedModels(). + + Colour3DPlotExporter::Sources sources; + sources.verticalBinLayer = this; + sources.fft = m_fftModel; + sources.source = sources.fft; + sources.provider = v; + + Colour3DPlotExporter::Parameters params; + params.binDisplay = m_binDisplay; + params.scaleFactor = 1.0; + if (m_colourScale != ColourScaleType::Phase && + m_normalization != ColumnNormalization::Hybrid) { + params.scaleFactor *= 2.f / float(getWindowSize()); + } + params.threshold = m_threshold; // matching ColourScale in getRenderer + params.gain = m_gain; // matching ColourScale in getRenderer + params.normalization = m_normalization; + + ModelId exporter = ModelById::add + (std::make_shared(sources, params)); + m_exporters.push_back(exporter); + return exporter; +} + void SpectrogramLayer::deleteDerivedModels() { @@ -149,6 +184,15 @@ ModelById::release(m_peakCache); ModelById::release(m_wholeCache); + for (auto exporterId: m_exporters) { + if (auto exporter = + ModelById::getAs(exporterId)) { + exporter->discardSources(); + } + ModelById::release(exporterId); + } + m_exporters.clear(); + m_fftModel = {}; m_peakCache = {}; m_wholeCache = {}; diff -r 76e4302a3fc2 -r 1f80a514ce29 layer/SpectrogramLayer.h --- a/layer/SpectrogramLayer.h Fri Nov 22 14:12:50 2019 +0000 +++ b/layer/SpectrogramLayer.h Fri Jan 10 14:54:27 2020 +0000 @@ -65,6 +65,9 @@ const ZoomConstraint *getZoomConstraint() const override { return this; } ModelId getModel() const override { return m_model; } + + ModelId getExportModel(LayerGeometryProvider *) const override; + void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const override; void setSynchronousPainting(bool synchronous) override; @@ -72,7 +75,7 @@ void paintVerticalScale(LayerGeometryProvider *v, bool detailed, QPainter &paint, QRect rect) const override; bool getCrosshairExtents(LayerGeometryProvider *, QPainter &, QPoint cursorPos, - std::vector &extents) const override; + std::vector &extents) const override; void paintCrosshairs(LayerGeometryProvider *, QPainter &, QPoint) const override; QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const override; @@ -331,6 +334,9 @@ ModelId m_wholeCache; // a Dense3DModelPeakCache ModelId m_peakCache; // a Dense3DModelPeakCache int m_peakCacheDivisor; + + mutable std::vector m_exporters; // used, waiting to be released + void checkCacheSpace(int *suggestedPeakDivisor, bool *createWholeCache) const; void recreateFFTModel(); diff -r 76e4302a3fc2 -r 1f80a514ce29 view/Overview.cpp --- a/view/Overview.cpp Fri Nov 22 14:12:50 2019 +0000 +++ b/view/Overview.cpp Fri Jan 10 14:54:27 2020 +0000 @@ -34,7 +34,7 @@ m_followPan = false; m_followZoom = false; setPlaybackFollow(PlaybackIgnore); - m_modelTestTime.start(); + m_modelTestTimer.start(); bool light = hasLightBackground(); if (light) m_boxColour = Qt::darkGray; @@ -57,7 +57,7 @@ } if (!zoomChanged) { - if (m_modelTestTime.elapsed() < 1000) { + if (m_modelTestTimer.elapsed() < 1000) { for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end(); ++i) { auto model = ModelById::get((*i)->getModel()); @@ -66,7 +66,7 @@ } } } else { - m_modelTestTime.restart(); + m_modelTestTimer.restart(); } } diff -r 76e4302a3fc2 -r 1f80a514ce29 view/Overview.h --- a/view/Overview.h Fri Nov 22 14:12:50 2019 +0000 +++ b/view/Overview.h Fri Jan 10 14:54:27 2020 +0000 @@ -19,7 +19,7 @@ #include "View.h" #include -#include +#include class QWidget; class QPaintEvent; @@ -68,7 +68,7 @@ QPoint m_mousePos; bool m_clickedInRange; sv_frame_t m_dragCentreFrame; - QTime m_modelTestTime; + QElapsedTimer m_modelTestTimer; QColor m_boxColour; typedef std::set ViewSet; diff -r 76e4302a3fc2 -r 1f80a514ce29 view/View.cpp --- a/view/View.cpp Fri Nov 22 14:12:50 2019 +0000 +++ b/view/View.cpp Fri Jan 10 14:54:27 2020 +0000 @@ -862,7 +862,7 @@ pbr.cancel = cancel; pbr.bar = pb; pbr.lastStallCheckValue = 0; - pbr.stallCheckTimer = new QTimer(); + pbr.stallCheckTimer = new QTimer(this); connect(pbr.stallCheckTimer, SIGNAL(timeout()), this, SLOT(progressCheckStalledTimerElapsed())); diff -r 76e4302a3fc2 -r 1f80a514ce29 widgets/ProgressDialog.h --- a/widgets/ProgressDialog.h Fri Nov 22 14:12:50 2019 +0000 +++ b/widgets/ProgressDialog.h Fri Jan 10 14:54:27 2020 +0000 @@ -27,7 +27,7 @@ public: ProgressDialog(QString message, bool cancellable, - int timeBeforeShow = 0, + int timeBeforeShow = 0, /* milliseconds */ QWidget *parent = 0, Qt::WindowModality modality = Qt::NonModal); virtual ~ProgressDialog();