changeset 1566:1f80a514ce29

Merge from branch spectrogram-export
author Chris Cannam
date Fri, 10 Jan 2020 14:54:27 +0000
parents 76e4302a3fc2 (current diff) a6a31908bd13 (diff)
children 77ffd5421627 3943553b95b0
files
diffstat 12 files changed, 532 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- 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 \
--- /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<DenseThreeDimensionalModel>(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<DenseThreeDimensionalModel>(m_sources.source);
+    auto fftModel =
+        ModelById::getAs<FFTModel>(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;
+}
+
--- /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
--- 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<Colour3DPlotExporter>(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<DenseThreeDimensionalModel>(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<Colour3DPlotExporter>(sources, params));
+    m_exporters.push_back(exporter);
+    return exporter;
+}
+
+ModelId
 Colour3DPlotLayer::getPeakCache() const
 {
     if (m_peakCache.isNone()) {
--- 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<ModelId> m_exporters; // used, waiting to be released
+    
     typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
     mutable ViewMagMap m_viewMags;
     mutable ViewMagMap m_lastRenderedMags; // when in normalizeVisibleArea mode
--- 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
--- 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 <QPainter>
 #include <QImage>
@@ -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<Colour3DPlotExporter>(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<Colour3DPlotExporter>(exporterId)) {
+            exporter->discardSources();
+        }
+        ModelById::release(exporterId);
+    }
+    m_exporters.clear();
+    
     m_fftModel = {};
     m_peakCache = {};
     m_wholeCache = {};
--- 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<QRect> &extents) const override;
+                             std::vector<QRect> &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<ModelId> m_exporters; // used, waiting to be released
+    
     void checkCacheSpace(int *suggestedPeakDivisor,
                          bool *createWholeCache) const;
     void recreateFFTModel();
--- 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();
         }
     }
 
--- 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 <QPoint>
-#include <QTime>
+#include <QElapsedTimer>
 
 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<View *> ViewSet;
--- 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()));
 
--- 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();