view layer/SpectrogramLayer.cpp @ 1482:c1cae369979d by-id

Comment
author Chris Cannam
date Fri, 12 Jul 2019 16:29:59 +0100
parents e540aa5d89cd
children 5d179afc0366
line wrap: on
line source
/* -*- 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 file copyright 2006-2009 Chris Cannam and QMUL.
    
    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 "SpectrogramLayer.h"

#include "view/View.h"
#include "base/Profiler.h"
#include "base/AudioLevel.h"
#include "base/Window.h"
#include "base/Pitch.h"
#include "base/Preferences.h"
#include "base/RangeMapper.h"
#include "base/LogRange.h"
#include "base/ColumnOp.h"
#include "base/Strings.h"
#include "base/StorageAdviser.h"
#include "base/Exceptions.h"
#include "widgets/CommandHistory.h"
#include "data/model/Dense3DModelPeakCache.h"

#include "ColourMapper.h"
#include "PianoScale.h"
#include "PaintAssistant.h"
#include "Colour3DPlotRenderer.h"

#include <QPainter>
#include <QImage>
#include <QPixmap>
#include <QRect>
#include <QApplication>
#include <QMessageBox>
#include <QMouseEvent>
#include <QTextStream>
#include <QSettings>

#include <iostream>

#include <cassert>
#include <cmath>

//#define DEBUG_SPECTROGRAM 1
//#define DEBUG_SPECTROGRAM_REPAINT 1

using namespace std;

SpectrogramLayer::SpectrogramLayer(Configuration config) :
    m_channel(0),
    m_windowSize(1024),
    m_windowType(HanningWindow),
    m_windowHopLevel(2),
    m_oversampling(1),
    m_gain(1.0),
    m_initialGain(1.0),
    m_threshold(1.0e-8f),
    m_initialThreshold(1.0e-8f),
    m_colourRotation(0),
    m_initialRotation(0),
    m_minFrequency(10),
    m_maxFrequency(8000),
    m_initialMaxFrequency(8000),
    m_colourScale(ColourScaleType::Log),
    m_colourScaleMultiple(1.0),
    m_colourMap(0),
    m_colourInverted(false),
    m_binScale(BinScale::Linear),
    m_binDisplay(BinDisplay::AllBins),
    m_normalization(ColumnNormalization::None),
    m_normalizeVisibleArea(false),
    m_lastEmittedZoomStep(-1),
    m_synchronous(false),
    m_haveDetailedScale(false),
    m_exiting(false),
    m_peakCacheDivisor(8)
{
    QString colourConfigName = "spectrogram-colour";
    int colourConfigDefault = int(ColourMapper::Green);
    
    if (config == FullRangeDb) {
        m_initialMaxFrequency = 0;
        setMaxFrequency(0);
    } else if (config == MelodicRange) {
        setWindowSize(8192);
        setWindowHopLevel(4);
        m_initialMaxFrequency = 1500;
        setMaxFrequency(1500);
        setMinFrequency(40);
        setColourScale(ColourScaleType::Linear);
        setColourMap(ColourMapper::Sunset);
        setBinScale(BinScale::Log);
        colourConfigName = "spectrogram-melodic-colour";
        colourConfigDefault = int(ColourMapper::Sunset);
//        setGain(20);
    } else if (config == MelodicPeaks) {
        setWindowSize(4096);
        setWindowHopLevel(5);
        m_initialMaxFrequency = 2000;
        setMaxFrequency(2000);
        setMinFrequency(40);
        setBinScale(BinScale::Log);
        setColourScale(ColourScaleType::Linear);
        setBinDisplay(BinDisplay::PeakFrequencies);
        setNormalization(ColumnNormalization::Max1);
        colourConfigName = "spectrogram-melodic-colour";
        colourConfigDefault = int(ColourMapper::Sunset);
    }

    QSettings settings;
    settings.beginGroup("Preferences");
    setColourMap(settings.value(colourConfigName, colourConfigDefault).toInt());
    settings.endGroup();
    
    Preferences *prefs = Preferences::getInstance();
    connect(prefs, SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
            this, SLOT(preferenceChanged(PropertyContainer::PropertyName)));
    setWindowType(prefs->getWindowType());
}

SpectrogramLayer::~SpectrogramLayer()
{
    invalidateRenderers();
    deleteDerivedModels();
}

void
SpectrogramLayer::deleteDerivedModels()
{
    ModelById::release(m_fftModel);
    ModelById::release(m_peakCache);
    ModelById::release(m_wholeCache);

    m_fftModel = {};
    m_peakCache = {};
    m_wholeCache = {};
}

pair<ColourScaleType, double>
SpectrogramLayer::convertToColourScale(int value)
{
    switch (value) {
    case 0: return { ColourScaleType::Linear, 1.0 };
    case 1: return { ColourScaleType::Meter, 1.0 };
    case 2: return { ColourScaleType::Log, 2.0 }; // dB^2 (i.e. log of power)
    case 3: return { ColourScaleType::Log, 1.0 }; // dB   (of magnitude)
    case 4: return { ColourScaleType::Phase, 1.0 };
    default: return { ColourScaleType::Linear, 1.0 };
    }
}

int
SpectrogramLayer::convertFromColourScale(ColourScaleType scale, double multiple)
{
    switch (scale) {
    case ColourScaleType::Linear: return 0;
    case ColourScaleType::Meter: return 1;
    case ColourScaleType::Log: return (multiple > 1.5 ? 2 : 3);
    case ColourScaleType::Phase: return 4;
    case ColourScaleType::PlusMinusOne:
    case ColourScaleType::Absolute:
    default: return 0;
    }
}

std::pair<ColumnNormalization, bool>
SpectrogramLayer::convertToColumnNorm(int value)
{
    switch (value) {
    default:
    case 0: return { ColumnNormalization::None, false };
    case 1: return { ColumnNormalization::Max1, false };
    case 2: return { ColumnNormalization::None, true }; // visible area
    case 3: return { ColumnNormalization::Hybrid, false };
    }
}

int
SpectrogramLayer::convertFromColumnNorm(ColumnNormalization norm, bool visible)
{
    if (visible) return 2;
    switch (norm) {
    case ColumnNormalization::None: return 0;
    case ColumnNormalization::Max1: return 1;
    case ColumnNormalization::Hybrid: return 3;

    case ColumnNormalization::Sum1:
    case ColumnNormalization::Range01:
    default: return 0;
    }
}

void
SpectrogramLayer::setModel(ModelId modelId)
{
    auto newModel = ModelById::getAs<DenseTimeValueModel>(modelId);
    if (!modelId.isNone() && !newModel) {
        throw std::logic_error("Not a DenseTimeValueModel");
    }
    
    if (modelId == m_model) return;
    m_model = modelId;

    if (newModel) {
        recreateFFTModel();

        connectSignals(m_model);

        connect(newModel.get(),
                SIGNAL(modelChanged(ModelId)),
                this, SLOT(cacheInvalid(ModelId)));
        connect(newModel.get(),
                SIGNAL(modelChangedWithin(ModelId, sv_frame_t, sv_frame_t)),
                this, SLOT(cacheInvalid(ModelId, sv_frame_t, sv_frame_t)));
    }
    
    emit modelReplaced();
}

Layer::PropertyList
SpectrogramLayer::getProperties() const
{
    PropertyList list;
    list.push_back("Colour");
    list.push_back("Colour Scale");
    list.push_back("Window Size");
    list.push_back("Window Increment");
    list.push_back("Oversampling");
    list.push_back("Normalization");
    list.push_back("Bin Display");
    list.push_back("Threshold");
    list.push_back("Gain");
    list.push_back("Colour Rotation");
//    list.push_back("Min Frequency");
//    list.push_back("Max Frequency");
    list.push_back("Frequency Scale");
    return list;
}

QString
SpectrogramLayer::getPropertyLabel(const PropertyName &name) const
{
    if (name == "Colour") return tr("Colour");
    if (name == "Colour Scale") return tr("Colour Scale");
    if (name == "Window Size") return tr("Window Size");
    if (name == "Window Increment") return tr("Window Overlap");
    if (name == "Oversampling") return tr("Oversampling");
    if (name == "Normalization") return tr("Normalization");
    if (name == "Bin Display") return tr("Bin Display");
    if (name == "Threshold") return tr("Threshold");
    if (name == "Gain") return tr("Gain");
    if (name == "Colour Rotation") return tr("Colour Rotation");
    if (name == "Min Frequency") return tr("Min Frequency");
    if (name == "Max Frequency") return tr("Max Frequency");
    if (name == "Frequency Scale") return tr("Frequency Scale");
    return "";
}

QString
SpectrogramLayer::getPropertyIconName(const PropertyName &) const
{
    return "";
}

Layer::PropertyType
SpectrogramLayer::getPropertyType(const PropertyName &name) const
{
    if (name == "Gain") return RangeProperty;
    if (name == "Colour Rotation") return RangeProperty;
    if (name == "Threshold") return RangeProperty;
    if (name == "Colour") return ColourMapProperty;
    return ValueProperty;
}

QString
SpectrogramLayer::getPropertyGroupName(const PropertyName &name) const
{
    if (name == "Bin Display" ||
        name == "Frequency Scale") return tr("Bins");
    if (name == "Window Size" ||
        name == "Window Increment" ||
        name == "Oversampling") return tr("Window");
    if (name == "Colour" ||
        name == "Threshold" ||
        name == "Colour Rotation") return tr("Colour");
    if (name == "Normalization" ||
        name == "Gain" ||
        name == "Colour Scale") return tr("Scale");
    return QString();
}

int
SpectrogramLayer::getPropertyRangeAndValue(const PropertyName &name,
                                           int *min, int *max, int *deflt) const
{
    int val = 0;

    int garbage0, garbage1, garbage2;
    if (!min) min = &garbage0;
    if (!max) max = &garbage1;
    if (!deflt) deflt = &garbage2;

    if (name == "Gain") {

        *min = -50;
        *max = 50;

        *deflt = int(lrint(log10(m_initialGain) * 20.0));
        if (*deflt < *min) *deflt = *min;
        if (*deflt > *max) *deflt = *max;

        val = int(lrint(log10(m_gain) * 20.0));
        if (val < *min) val = *min;
        if (val > *max) val = *max;

    } else if (name == "Threshold") {

        *min = -81;
        *max = -1;

        *deflt = int(lrint(AudioLevel::multiplier_to_dB(m_initialThreshold)));
        if (*deflt < *min) *deflt = *min;
        if (*deflt > *max) *deflt = *max;

        val = int(lrint(AudioLevel::multiplier_to_dB(m_threshold)));
        if (val < *min) val = *min;
        if (val > *max) val = *max;

    } else if (name == "Colour Rotation") {

        *min = 0;
        *max = 256;
        *deflt = m_initialRotation;

        val = m_colourRotation;

    } else if (name == "Colour Scale") {

        // linear, meter, db^2, db, phase
        *min = 0;
        *max = 4;
        *deflt = 2;

        val = convertFromColourScale(m_colourScale, m_colourScaleMultiple);

    } else if (name == "Colour") {

        *min = 0;
        *max = ColourMapper::getColourMapCount() - 1;
        *deflt = 0;

        val = m_colourMap;

    } else if (name == "Window Size") {

        *min = 0;
        *max = 10;
        *deflt = 5;
        
        val = 0;
        int ws = m_windowSize;
        while (ws > 32) { ws >>= 1; val ++; }

    } else if (name == "Window Increment") {
        
        *min = 0;
        *max = 5;
        *deflt = 2;

        val = m_windowHopLevel;

    } else if (name == "Oversampling") {

        *min = 0;
        *max = 3;
        *deflt = 0;

        val = 0;
        int ov = m_oversampling;
        while (ov > 1) { ov >>= 1; val ++; }
        
    } else if (name == "Min Frequency") {

        *min = 0;
        *max = 9;
        *deflt = 1;

        switch (m_minFrequency) {
        case 0: default: val = 0; break;
        case 10: val = 1; break;
        case 20: val = 2; break;
        case 40: val = 3; break;
        case 100: val = 4; break;
        case 250: val = 5; break;
        case 500: val = 6; break;
        case 1000: val = 7; break;
        case 4000: val = 8; break;
        case 10000: val = 9; break;
        }
    
    } else if (name == "Max Frequency") {

        *min = 0;
        *max = 9;
        *deflt = 6;

        switch (m_maxFrequency) {
        case 500: val = 0; break;
        case 1000: val = 1; break;
        case 1500: val = 2; break;
        case 2000: val = 3; break;
        case 4000: val = 4; break;
        case 6000: val = 5; break;
        case 8000: val = 6; break;
        case 12000: val = 7; break;
        case 16000: val = 8; break;
        default: val = 9; break;
        }

    } else if (name == "Frequency Scale") {

        *min = 0;
        *max = 1;
        *deflt = int(BinScale::Linear);
        val = (int)m_binScale;

    } else if (name == "Bin Display") {

        *min = 0;
        *max = 2;
        *deflt = int(BinDisplay::AllBins);
        val = (int)m_binDisplay;

    } else if (name == "Normalization") {
        
        *min = 0;
        *max = 3;
        *deflt = 0;
        
        val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea);

    } else {
        val = Layer::getPropertyRangeAndValue(name, min, max, deflt);
    }

    return val;
}

QString
SpectrogramLayer::getPropertyValueLabel(const PropertyName &name,
                                        int value) const
{
    if (name == "Colour") {
        return ColourMapper::getColourMapLabel(value);
    }
    if (name == "Colour Scale") {
        switch (value) {
        default:
        case 0: return tr("Linear");
        case 1: return tr("Meter");
        case 2: return tr("dBV^2");
        case 3: return tr("dBV");
        case 4: return tr("Phase");
        }
    }
    if (name == "Normalization") {
        switch(value) {
        default:
        case 0: return tr("None");
        case 1: return tr("Col");
        case 2: return tr("View");
        case 3: return tr("Hybrid");
        }
//        return ""; // icon only
    }
    if (name == "Window Size") {
        return QString("%1").arg(32 << value);
    }
    if (name == "Window Increment") {
        switch (value) {
        default:
        case 0: return tr("None");
        case 1: return tr("25 %");
        case 2: return tr("50 %");
        case 3: return tr("75 %");
        case 4: return tr("87.5 %");
        case 5: return tr("93.75 %");
        }
    }
    if (name == "Oversampling") {
        switch (value) {
        default:
        case 0: return tr("1x");
        case 1: return tr("2x");
        case 2: return tr("4x");
        case 3: return tr("8x");
        }
    }
    if (name == "Min Frequency") {
        switch (value) {
        default:
        case 0: return tr("No min");
        case 1: return tr("10 Hz");
        case 2: return tr("20 Hz");
        case 3: return tr("40 Hz");
        case 4: return tr("100 Hz");
        case 5: return tr("250 Hz");
        case 6: return tr("500 Hz");
        case 7: return tr("1 KHz");
        case 8: return tr("4 KHz");
        case 9: return tr("10 KHz");
        }
    }
    if (name == "Max Frequency") {
        switch (value) {
        default:
        case 0: return tr("500 Hz");
        case 1: return tr("1 KHz");
        case 2: return tr("1.5 KHz");
        case 3: return tr("2 KHz");
        case 4: return tr("4 KHz");
        case 5: return tr("6 KHz");
        case 6: return tr("8 KHz");
        case 7: return tr("12 KHz");
        case 8: return tr("16 KHz");
        case 9: return tr("No max");
        }
    }
    if (name == "Frequency Scale") {
        switch (value) {
        default:
        case 0: return tr("Linear");
        case 1: return tr("Log");
        }
    }
    if (name == "Bin Display") {
        switch (value) {
        default:
        case 0: return tr("All Bins");
        case 1: return tr("Peak Bins");
        case 2: return tr("Frequencies");
        }
    }
    return tr("<unknown>");
}

QString
SpectrogramLayer::getPropertyValueIconName(const PropertyName &name,
                                           int value) const
{
    if (name == "Normalization") {
        switch(value) {
        default:
        case 0: return "normalise-none";
        case 1: return "normalise-columns";
        case 2: return "normalise";
        case 3: return "normalise-hybrid";
        }
    }
    return "";
}

RangeMapper *
SpectrogramLayer::getNewPropertyRangeMapper(const PropertyName &name) const
{
    if (name == "Gain") {
        return new LinearRangeMapper(-50, 50, -25, 25, tr("dB"));
    }
    if (name == "Threshold") {
        return new LinearRangeMapper(-81, -1, -81, -1, tr("dB"), false,
                                     { { -81, Strings::minus_infinity } });
    }
    return nullptr;
}

void
SpectrogramLayer::setProperty(const PropertyName &name, int value)
{
    if (name == "Gain") {
        setGain(float(pow(10, float(value)/20.0)));
    } else if (name == "Threshold") {
        if (value == -81) setThreshold(0.0);
        else setThreshold(float(AudioLevel::dB_to_multiplier(value)));
    } else if (name == "Colour Rotation") {
        setColourRotation(value);
    } else if (name == "Colour") {
        setColourMap(value);
    } else if (name == "Window Size") {
        setWindowSize(32 << value);
    } else if (name == "Window Increment") {
        setWindowHopLevel(value);
    } else if (name == "Oversampling") {
        setOversampling(1 << value);
    } else if (name == "Min Frequency") {
        switch (value) {
        default:
        case 0: setMinFrequency(0); break;
        case 1: setMinFrequency(10); break;
        case 2: setMinFrequency(20); break;
        case 3: setMinFrequency(40); break;
        case 4: setMinFrequency(100); break;
        case 5: setMinFrequency(250); break;
        case 6: setMinFrequency(500); break;
        case 7: setMinFrequency(1000); break;
        case 8: setMinFrequency(4000); break;
        case 9: setMinFrequency(10000); break;
        }
        int vs = getCurrentVerticalZoomStep();
        if (vs != m_lastEmittedZoomStep) {
            emit verticalZoomChanged();
            m_lastEmittedZoomStep = vs;
        }
    } else if (name == "Max Frequency") {
        switch (value) {
        case 0: setMaxFrequency(500); break;
        case 1: setMaxFrequency(1000); break;
        case 2: setMaxFrequency(1500); break;
        case 3: setMaxFrequency(2000); break;
        case 4: setMaxFrequency(4000); break;
        case 5: setMaxFrequency(6000); break;
        case 6: setMaxFrequency(8000); break;
        case 7: setMaxFrequency(12000); break;
        case 8: setMaxFrequency(16000); break;
        default:
        case 9: setMaxFrequency(0); break;
        }
        int vs = getCurrentVerticalZoomStep();
        if (vs != m_lastEmittedZoomStep) {
            emit verticalZoomChanged();
            m_lastEmittedZoomStep = vs;
        }
    } else if (name == "Colour Scale") {
        setColourScaleMultiple(1.0);
        switch (value) {
        default:
        case 0: setColourScale(ColourScaleType::Linear); break;
        case 1: setColourScale(ColourScaleType::Meter); break;
        case 2:
            setColourScale(ColourScaleType::Log);
            setColourScaleMultiple(2.0);
            break;
        case 3: setColourScale(ColourScaleType::Log); break;
        case 4: setColourScale(ColourScaleType::Phase); break;
        }
    } else if (name == "Frequency Scale") {
        switch (value) {
        default:
        case 0: setBinScale(BinScale::Linear); break;
        case 1: setBinScale(BinScale::Log); break;
        }
    } else if (name == "Bin Display") {
        switch (value) {
        default:
        case 0: setBinDisplay(BinDisplay::AllBins); break;
        case 1: setBinDisplay(BinDisplay::PeakBins); break;
        case 2: setBinDisplay(BinDisplay::PeakFrequencies); break;
        }
    } else if (name == "Normalization") {
        auto n = convertToColumnNorm(value);
        setNormalization(n.first);
        setNormalizeVisibleArea(n.second);
    }
}

void
SpectrogramLayer::invalidateRenderers()
{
#ifdef DEBUG_SPECTROGRAM
    cerr << "SpectrogramLayer::invalidateRenderers called" << endl;
#endif

    for (ViewRendererMap::iterator i = m_renderers.begin();
         i != m_renderers.end(); ++i) {
        delete i->second;
    }
    m_renderers.clear();
}

void
SpectrogramLayer::preferenceChanged(PropertyContainer::PropertyName name)
{
    SVDEBUG << "SpectrogramLayer::preferenceChanged(" << name << ")" << endl;

    if (name == "Window Type") {
        setWindowType(Preferences::getInstance()->getWindowType());
        return;
    }
    if (name == "Spectrogram Y Smoothing") {
        invalidateRenderers();
        invalidateMagnitudes();
        emit layerParametersChanged();
    }
    if (name == "Spectrogram X Smoothing") {
        invalidateRenderers();
        invalidateMagnitudes();
        emit layerParametersChanged();
    }
    if (name == "Tuning Frequency") {
        emit layerParametersChanged();
    }
}

void
SpectrogramLayer::setChannel(int ch)
{
    if (m_channel == ch) return;

    invalidateRenderers();
    m_channel = ch;
    recreateFFTModel();

    emit layerParametersChanged();
}

int
SpectrogramLayer::getChannel() const
{
    return m_channel;
}

int
SpectrogramLayer::getFFTSize() const
{
    return m_windowSize * m_oversampling;
}

void
SpectrogramLayer::setWindowSize(int ws)
{
    if (m_windowSize == ws) return;
    invalidateRenderers();
    m_windowSize = ws;
    recreateFFTModel();
    emit layerParametersChanged();
}

int
SpectrogramLayer::getWindowSize() const
{
    return m_windowSize;
}

void
SpectrogramLayer::setWindowHopLevel(int v)
{
    if (m_windowHopLevel == v) return;
    invalidateRenderers();
    m_windowHopLevel = v;
    recreateFFTModel();
    emit layerParametersChanged();
}

int
SpectrogramLayer::getWindowHopLevel() const
{
    return m_windowHopLevel;
}

void
SpectrogramLayer::setOversampling(int oversampling)
{
    if (m_oversampling == oversampling) return;
    invalidateRenderers();
    m_oversampling = oversampling;
    recreateFFTModel();
    emit layerParametersChanged();
}

int
SpectrogramLayer::getOversampling() const
{
    return m_oversampling;
}

void
SpectrogramLayer::setWindowType(WindowType w)
{
    if (m_windowType == w) return;

    invalidateRenderers();
    
    m_windowType = w;

    recreateFFTModel();

    emit layerParametersChanged();
}

WindowType
SpectrogramLayer::getWindowType() const
{
    return m_windowType;
}

void
SpectrogramLayer::setGain(float gain)
{
//    SVDEBUG << "SpectrogramLayer::setGain(" << gain << ") (my gain is now "
//            << m_gain << ")" << endl;

    if (m_gain == gain) return;

    invalidateRenderers();
    
    m_gain = gain;
    
    emit layerParametersChanged();
}

float
SpectrogramLayer::getGain() const
{
    return m_gain;
}

void
SpectrogramLayer::setThreshold(float threshold)
{
    if (m_threshold == threshold) return;

    invalidateRenderers();
    
    m_threshold = threshold;

    emit layerParametersChanged();
}

float
SpectrogramLayer::getThreshold() const
{
    return m_threshold;
}

void
SpectrogramLayer::setMinFrequency(int mf)
{
    if (m_minFrequency == mf) return;

//    SVDEBUG << "SpectrogramLayer::setMinFrequency: " << mf << endl;

    invalidateRenderers();
    invalidateMagnitudes();
    
    m_minFrequency = mf;

    emit layerParametersChanged();
}

int
SpectrogramLayer::getMinFrequency() const
{
    return m_minFrequency;
}

void
SpectrogramLayer::setMaxFrequency(int mf)
{
    if (m_maxFrequency == mf) return;

//    SVDEBUG << "SpectrogramLayer::setMaxFrequency: " << mf << endl;

    invalidateRenderers();
    invalidateMagnitudes();
    
    m_maxFrequency = mf;
    
    emit layerParametersChanged();
}

int
SpectrogramLayer::getMaxFrequency() const
{
    return m_maxFrequency;
}

void
SpectrogramLayer::setColourRotation(int r)
{
    if (r < 0) r = 0;
    if (r > 256) r = 256;
    int distance = r - m_colourRotation;

    if (distance != 0) {
        m_colourRotation = r;
    }

    // Initially the idea with colour rotation was that we would just
    // rotate the palette of an already-generated cache. That's not
    // really practical now that cacheing is handled in a separate
    // class in which the main cache no longer has a palette.
    invalidateRenderers();
    
    emit layerParametersChanged();
}

void
SpectrogramLayer::setColourScale(ColourScaleType colourScale)
{
    if (m_colourScale == colourScale) return;

    invalidateRenderers();
    
    m_colourScale = colourScale;
    
    emit layerParametersChanged();
}

ColourScaleType
SpectrogramLayer::getColourScale() const
{
    return m_colourScale;
}

void
SpectrogramLayer::setColourScaleMultiple(double multiple)
{
    if (m_colourScaleMultiple == multiple) return;

    invalidateRenderers();
    
    m_colourScaleMultiple = multiple;
    
    emit layerParametersChanged();
}

double
SpectrogramLayer::getColourScaleMultiple() const
{
    return m_colourScaleMultiple;
}

void
SpectrogramLayer::setColourMap(int map)
{
    if (m_colourMap == map) return;

    invalidateRenderers();
    
    m_colourMap = map;

    emit layerParametersChanged();
}

int
SpectrogramLayer::getColourMap() const
{
    return m_colourMap;
}

void
SpectrogramLayer::setBinScale(BinScale binScale)
{
    if (m_binScale == binScale) return;

    invalidateRenderers();
    m_binScale = binScale;

    emit layerParametersChanged();
}

BinScale
SpectrogramLayer::getBinScale() const
{
    return m_binScale;
}

void
SpectrogramLayer::setBinDisplay(BinDisplay binDisplay)
{
    if (m_binDisplay == binDisplay) return;

    invalidateRenderers();
    m_binDisplay = binDisplay;

    emit layerParametersChanged();
}

BinDisplay
SpectrogramLayer::getBinDisplay() const
{
    return m_binDisplay;
}

void
SpectrogramLayer::setNormalization(ColumnNormalization n)
{
    if (m_normalization == n) return;

    invalidateRenderers();
    invalidateMagnitudes();
    m_normalization = n;

    emit layerParametersChanged();
}

ColumnNormalization
SpectrogramLayer::getNormalization() const
{
    return m_normalization;
}

void
SpectrogramLayer::setNormalizeVisibleArea(bool n)
{
    if (m_normalizeVisibleArea == n) return;

    invalidateRenderers();
    invalidateMagnitudes();
    m_normalizeVisibleArea = n;
    
    emit layerParametersChanged();
}

bool
SpectrogramLayer::getNormalizeVisibleArea() const
{
    return m_normalizeVisibleArea;
}

void
SpectrogramLayer::setLayerDormant(const LayerGeometryProvider *v, bool dormant)
{
    if (dormant) {

#ifdef DEBUG_SPECTROGRAM_REPAINT
        cerr << "SpectrogramLayer::setLayerDormant(" << dormant << ")"
                  << endl;
#endif

        if (isLayerDormant(v)) {
            return;
        }

        Layer::setLayerDormant(v, true);

        invalidateRenderers();
        
    } else {

        Layer::setLayerDormant(v, false);
    }
}

bool
SpectrogramLayer::isLayerScrollable(const LayerGeometryProvider *) const
{
    // we do our own cacheing, and don't want to be responsible for
    // guaranteeing to get an invisible seam if someone else scrolls
    // us and we just fill in
    return false;
}

void
SpectrogramLayer::cacheInvalid(ModelId)
{
#ifdef DEBUG_SPECTROGRAM_REPAINT
    cerr << "SpectrogramLayer::cacheInvalid()" << endl;
#endif

    invalidateRenderers();
    invalidateMagnitudes();
}

void
SpectrogramLayer::cacheInvalid(
    ModelId,
#ifdef DEBUG_SPECTROGRAM_REPAINT
    sv_frame_t from, sv_frame_t to
#else 
    sv_frame_t     , sv_frame_t
#endif
    )
{
#ifdef DEBUG_SPECTROGRAM_REPAINT
    cerr << "SpectrogramLayer::cacheInvalid(" << from << ", " << to << ")" << endl;
#endif

    // We used to call invalidateMagnitudes(from, to) to invalidate
    // only those caches whose views contained some of the (from, to)
    // range. That's the right thing to do; it has been lost in
    // pulling out the image cache code, but it might not matter very
    // much, since the underlying models for spectrogram layers don't
    // change very often. Let's see.
    invalidateRenderers();
    invalidateMagnitudes();
}

bool
SpectrogramLayer::hasLightBackground() const 
{
    return ColourMapper(m_colourMap, m_colourInverted, 1.f, 255.f)
        .hasLightBackground();
}

double
SpectrogramLayer::getEffectiveMinFrequency() const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return 0.0;
    
    sv_samplerate_t sr = model->getSampleRate();
    double minf = double(sr) / getFFTSize();

    if (m_minFrequency > 0.0) {
        int minbin = int((double(m_minFrequency) * getFFTSize()) / sr + 0.01);
        if (minbin < 1) minbin = 1;
        minf = minbin * sr / getFFTSize();
    }

    return minf;
}

double
SpectrogramLayer::getEffectiveMaxFrequency() const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return 0.0;
    
    sv_samplerate_t sr = model->getSampleRate();
    double maxf = double(sr) / 2;

    if (m_maxFrequency > 0.0) {
        int maxbin = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
        if (maxbin > getFFTSize() / 2) maxbin = getFFTSize() / 2;
        maxf = maxbin * sr / getFFTSize();
    }

    return maxf;
}

bool
SpectrogramLayer::getYBinRange(LayerGeometryProvider *v, int y, double &q0, double &q1) const
{
    Profiler profiler("SpectrogramLayer::getYBinRange");
    int h = v->getPaintHeight();
    if (y < 0 || y >= h) return false;
    q0 = getBinForY(v, y);
    q1 = getBinForY(v, y-1);
    return true;
}

double
SpectrogramLayer::getYForBin(const LayerGeometryProvider *v, double bin) const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return 0.0;
    
    double minf = getEffectiveMinFrequency();
    double maxf = getEffectiveMaxFrequency();
    bool logarithmic = (m_binScale == BinScale::Log);
    sv_samplerate_t sr = model->getSampleRate();

    double freq = (bin * sr) / getFFTSize();
    
    double y = v->getYForFrequency(freq, minf, maxf, logarithmic);
    
    return y;
}

double
SpectrogramLayer::getBinForY(const LayerGeometryProvider *v, double y) const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return 0.0;

    sv_samplerate_t sr = model->getSampleRate();
    double minf = getEffectiveMinFrequency();
    double maxf = getEffectiveMaxFrequency();

    bool logarithmic = (m_binScale == BinScale::Log);

    double freq = v->getFrequencyForY(y, minf, maxf, logarithmic);

    // Now map on to ("proportion of") actual bins
    double bin = (freq * getFFTSize()) / sr;

    return bin;
}

bool
SpectrogramLayer::getXBinRange(LayerGeometryProvider *v, int x, double &s0, double &s1) const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return false;

    sv_frame_t modelStart = model->getStartFrame();
    sv_frame_t modelEnd = model->getEndFrame();

    // Each pixel column covers an exact range of sample frames:
    sv_frame_t f0 = v->getFrameForX(x) - modelStart;
    sv_frame_t f1 = v->getFrameForX(x + 1) - modelStart - 1;

    if (f1 < int(modelStart) || f0 > int(modelEnd)) {
        return false;
    }
      
    // And that range may be drawn from a possibly non-integral
    // range of spectrogram windows:

    int windowIncrement = getWindowIncrement();
    s0 = double(f0) / windowIncrement;
    s1 = double(f1) / windowIncrement;

    return true;
}
 
bool
SpectrogramLayer::getXBinSourceRange(LayerGeometryProvider *v, int x, RealTime &min, RealTime &max) const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return false;

    double s0 = 0, s1 = 0;
    if (!getXBinRange(v, x, s0, s1)) return false;
    
    int s0i = int(s0 + 0.001);
    int s1i = int(s1);

    int windowIncrement = getWindowIncrement();
    int w0 = s0i * windowIncrement - (m_windowSize - windowIncrement)/2;
    int w1 = s1i * windowIncrement + windowIncrement +
        (m_windowSize - windowIncrement)/2 - 1;
    
    min = RealTime::frame2RealTime(w0, model->getSampleRate());
    max = RealTime::frame2RealTime(w1, model->getSampleRate());
    return true;
}

bool
SpectrogramLayer::getYBinSourceRange(LayerGeometryProvider *v, int y, double &freqMin, double &freqMax)
const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return false;

    double q0 = 0, q1 = 0;
    if (!getYBinRange(v, y, q0, q1)) return false;

    int q0i = int(q0 + 0.001);
    int q1i = int(q1);

    sv_samplerate_t sr = model->getSampleRate();

    for (int q = q0i; q <= q1i; ++q) {
        if (q == q0i) freqMin = (sr * q) / getFFTSize();
        if (q == q1i) freqMax = (sr * (q+1)) / getFFTSize();
    }
    return true;
}

bool
SpectrogramLayer::getAdjustedYBinSourceRange(LayerGeometryProvider *v, int x, int y,
                                             double &freqMin, double &freqMax,
                                             double &adjFreqMin, double &adjFreqMax)
const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model || !model->isOK() || !model->isReady()) {
        return false;
    }

    auto fft = ModelById::getAs<FFTModel>(m_fftModel);
    if (!fft) return false;

    double s0 = 0, s1 = 0;
    if (!getXBinRange(v, x, s0, s1)) return false;

    double q0 = 0, q1 = 0;
    if (!getYBinRange(v, y, q0, q1)) return false;

    int s0i = int(s0 + 0.001);
    int s1i = int(s1);

    int q0i = int(q0 + 0.001);
    int q1i = int(q1);

    sv_samplerate_t sr = model->getSampleRate();

    bool haveAdj = false;

    bool peaksOnly = (m_binDisplay == BinDisplay::PeakBins ||
                      m_binDisplay == BinDisplay::PeakFrequencies);

    for (int q = q0i; q <= q1i; ++q) {

        for (int s = s0i; s <= s1i; ++s) {

            double binfreq = (double(sr) * q) / getFFTSize();
            if (q == q0i) freqMin = binfreq;
            if (q == q1i) freqMax = binfreq;

            if (peaksOnly && !fft->isLocalPeak(s, q)) continue;

            if (!fft->isOverThreshold
                (s, q, float(m_threshold * double(getFFTSize())/2.0))) {
                continue;
            }

            double freq = binfreq;
            
            if (s < int(fft->getWidth()) - 1) {

                fft->estimateStableFrequency(s, q, freq);
            
                if (!haveAdj || freq < adjFreqMin) adjFreqMin = freq;
                if (!haveAdj || freq > adjFreqMax) adjFreqMax = freq;

                haveAdj = true;
            }
        }
    }

    if (!haveAdj) {
        adjFreqMin = adjFreqMax = 0.0;
    }

    return haveAdj;
}
    
bool
SpectrogramLayer::getXYBinSourceRange(LayerGeometryProvider *v, int x, int y,
                                      double &min, double &max,
                                      double &phaseMin, double &phaseMax) const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model || !model->isOK() || !model->isReady()) {
        return false;
    }

    double q0 = 0, q1 = 0;
    if (!getYBinRange(v, y, q0, q1)) return false;

    double s0 = 0, s1 = 0;
    if (!getXBinRange(v, x, s0, s1)) return false;
    
    int q0i = int(q0 + 0.001);
    int q1i = int(q1);

    int s0i = int(s0 + 0.001);
    int s1i = int(s1);

    bool rv = false;

    auto fft = ModelById::getAs<FFTModel>(m_fftModel);

    if (fft) {

        int cw = fft->getWidth();
        int ch = fft->getHeight();

        min = 0.0;
        max = 0.0;
        phaseMin = 0.0;
        phaseMax = 0.0;
        bool have = false;

        for (int q = q0i; q <= q1i; ++q) {
            for (int s = s0i; s <= s1i; ++s) {
                if (s >= 0 && q >= 0 && s < cw && q < ch) {

                    double value;

                    value = fft->getPhaseAt(s, q);
                    if (!have || value < phaseMin) { phaseMin = value; }
                    if (!have || value > phaseMax) { phaseMax = value; }

                    value = fft->getMagnitudeAt(s, q) / (getFFTSize()/2.0);
                    if (!have || value < min) { min = value; }
                    if (!have || value > max) { max = value; }
                    
                    have = true;
                }       
            }
        }
        
        if (have) {
            rv = true;
        }
    }

    return rv;
}
        
void
SpectrogramLayer::recreateFFTModel()
{
    SVDEBUG << "SpectrogramLayer::recreateFFTModel called" << endl;

    { // scope, avoid hanging on to this pointer
        auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
        if (!model || !model->isOK()) {
            deleteDerivedModels();
            return;
        }
    }
    
    deleteDerivedModels();

    auto newFFTModel = std::make_shared<FFTModel>(m_model,
                                                  m_channel,
                                                  m_windowType,
                                                  m_windowSize,
                                                  getWindowIncrement(),
                                                  getFFTSize());

    if (!newFFTModel->isOK()) {
        QMessageBox::critical
            (nullptr, tr("FFT cache failed"),
             tr("Failed to create the FFT model for this spectrogram.\n"
                "There may be insufficient memory or disc space to continue."));
        return;
    }

    m_fftModel = ModelById::add(newFFTModel);

    bool createWholeCache = false;
    checkCacheSpace(&m_peakCacheDivisor, &createWholeCache);
    
    if (createWholeCache) {

        auto whole = std::make_shared<Dense3DModelPeakCache>(m_fftModel, 1);
        m_wholeCache = ModelById::add(whole);

        auto peaks = std::make_shared<Dense3DModelPeakCache>(m_wholeCache,
                                                             m_peakCacheDivisor);
        m_peakCache = ModelById::add(peaks);

    } else {

        auto peaks = std::make_shared<Dense3DModelPeakCache>(m_fftModel,
                                                             m_peakCacheDivisor);
        m_peakCache = ModelById::add(peaks);
    }
}

void
SpectrogramLayer::checkCacheSpace(int *suggestedPeakDivisor,
                                  bool *createWholeCache) const
{
    *suggestedPeakDivisor = 8;
    *createWholeCache = false;

    auto fftModel = ModelById::getAs<FFTModel>(m_fftModel);
    if (!fftModel) return;

    size_t sz =
        size_t(fftModel->getWidth()) *
        size_t(fftModel->getHeight()) *
        sizeof(float);

    try {
        SVDEBUG << "Requesting advice from StorageAdviser on whether to create whole-model cache" << endl;
        // The lower amount here is the amount required for the
        // slightly higher-resolution version of the peak cache
        // without a whole-model cache; the higher amount is that for
        // the whole-model cache. The factors of 1024 are because
        // StorageAdviser rather stupidly works in kilobytes
        StorageAdviser::Recommendation recommendation =
            StorageAdviser::recommend
            (StorageAdviser::Criteria(StorageAdviser::SpeedCritical |
                                      StorageAdviser::PrecisionCritical |
                                      StorageAdviser::FrequentLookupLikely),
             (sz / 8) / 1024, sz / 1024);
        if (recommendation & StorageAdviser::UseDisc) {
            SVDEBUG << "Seems inadvisable to create whole-model cache" << endl;
        } else if (recommendation & StorageAdviser::ConserveSpace) {
            SVDEBUG << "Seems inadvisable to create whole-model cache but acceptable to use the slightly higher-resolution peak cache" << endl;
            *suggestedPeakDivisor = 4;
        } else  {
            SVDEBUG << "Seems fine to create whole-model cache" << endl;
            *createWholeCache = true;
        }
    } catch (const InsufficientDiscSpace &) {
        SVDEBUG << "Seems like a terrible idea to create whole-model cache" << endl;
    }
}

ModelId
SpectrogramLayer::getSliceableModel() const
{
    return m_fftModel;
}

void
SpectrogramLayer::invalidateMagnitudes()
{
#ifdef DEBUG_SPECTROGRAM
    cerr << "SpectrogramLayer::invalidateMagnitudes called" << endl;
#endif
    m_viewMags.clear();
}

void
SpectrogramLayer::setSynchronousPainting(bool synchronous)
{
    m_synchronous = synchronous;
}

Colour3DPlotRenderer *
SpectrogramLayer::getRenderer(LayerGeometryProvider *v) const
{
    int viewId = v->getId();
    
    if (m_renderers.find(viewId) == m_renderers.end()) {

        Colour3DPlotRenderer::Sources sources;
        sources.verticalBinLayer = this;
        sources.fft = m_fftModel;
        sources.source = sources.fft;
        if (!m_peakCache.isNone()) sources.peakCaches.push_back(m_peakCache);
        if (!m_wholeCache.isNone()) sources.peakCaches.push_back(m_wholeCache);

        ColourScale::Parameters cparams;
        cparams.colourMap = m_colourMap;
        cparams.scaleType = m_colourScale;
        cparams.multiple = m_colourScaleMultiple;

        if (m_colourScale != ColourScaleType::Phase) {
            cparams.gain = m_gain;
            cparams.threshold = m_threshold;
        }

        double minValue = 0.0f;
        double maxValue = 1.0f;
        
        if (m_normalizeVisibleArea && m_viewMags[viewId].isSet()) {
            minValue = m_viewMags[viewId].getMin();
            maxValue = m_viewMags[viewId].getMax();
        } else if (m_colourScale == ColourScaleType::Linear &&
                   m_normalization == ColumnNormalization::None) {
            maxValue = 0.1f;
        }

        if (maxValue <= minValue) {
            maxValue = minValue + 0.1f;
        }
        if (maxValue <= m_threshold) {
            maxValue = m_threshold + 0.1f;
        }

        cparams.minValue = minValue;
        cparams.maxValue = maxValue;

        m_lastRenderedMags[viewId] = MagnitudeRange(float(minValue),
                                                    float(maxValue));

        Colour3DPlotRenderer::Parameters params;
        params.colourScale = ColourScale(cparams);
        params.normalization = m_normalization;
        params.binDisplay = m_binDisplay;
        params.binScale = m_binScale;
        params.alwaysOpaque = true;
        params.invertVertical = false;
        params.scaleFactor = 1.0;
        params.colourRotation = m_colourRotation;

        if (m_colourScale != ColourScaleType::Phase &&
            m_normalization != ColumnNormalization::Hybrid) {
            params.scaleFactor *= 2.f / float(getWindowSize());
        }

        Preferences::SpectrogramSmoothing smoothing = 
            Preferences::getInstance()->getSpectrogramSmoothing();
        params.interpolate = 
            (smoothing != Preferences::NoSpectrogramSmoothing);

        m_renderers[viewId] = new Colour3DPlotRenderer(sources, params);

        m_crosshairColour =
            ColourMapper(m_colourMap, m_colourInverted, 1.f, 255.f)
            .getContrastingColour();
    }

    return m_renderers[viewId];
}

void
SpectrogramLayer::paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
{
    Colour3DPlotRenderer *renderer = getRenderer(v);

    Colour3DPlotRenderer::RenderResult result;
    MagnitudeRange magRange;
    int viewId = v->getId();

    bool continuingPaint = !renderer->geometryChanged(v);
    
    if (continuingPaint) {
        magRange = m_viewMags[viewId];
    }
    
    if (m_synchronous) {

        result = renderer->render(v, paint, rect);

    } else {

        result = renderer->renderTimeConstrained(v, paint, rect);

#ifdef DEBUG_SPECTROGRAM_REPAINT
        cerr << "rect width from this paint: " << result.rendered.width()
             << ", mag range in this paint: " << result.range.getMin() << " -> "
             << result.range.getMax() << endl;
#endif
        
        QRect uncached = renderer->getLargestUncachedRect(v);
        if (uncached.width() > 0) {
            v->updatePaintRect(uncached);
        }
    }

    magRange.sample(result.range);

    if (magRange.isSet()) {
        if (m_viewMags[viewId] != magRange) {
            m_viewMags[viewId] = magRange;
#ifdef DEBUG_SPECTROGRAM_REPAINT
            cerr << "mag range in this view has changed: "
                 << magRange.getMin() << " -> " << magRange.getMax() << endl;
#endif
        }
    }

    if (!continuingPaint && m_normalizeVisibleArea &&
        m_viewMags[viewId] != m_lastRenderedMags[viewId]) {
#ifdef DEBUG_SPECTROGRAM_REPAINT
        cerr << "mag range has changed from last rendered range: re-rendering"
             << endl;
#endif
        delete m_renderers[viewId];
        m_renderers.erase(viewId);
        v->updatePaintRect(v->getPaintRect());
    }
}

void
SpectrogramLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
{
    Profiler profiler("SpectrogramLayer::paint", false);

#ifdef DEBUG_SPECTROGRAM_REPAINT
    cerr << "SpectrogramLayer::paint() entering: m_model is " << m_model << ", zoom level is " << v->getZoomLevel() << endl;
    
    cerr << "SpectrogramLayer::paint(): rect is " << rect.x() << "," << rect.y() << " " << rect.width() << "x" << rect.height() << endl;
#endif

    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model || !model->isOK() || !model->isReady()) {
        return;
    }

    paintWithRenderer(v, paint, rect);

    illuminateLocalFeatures(v, paint);
}

void
SpectrogramLayer::illuminateLocalFeatures(LayerGeometryProvider *v, QPainter &paint) const
{
    Profiler profiler("SpectrogramLayer::illuminateLocalFeatures");

    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    
    QPoint localPos;
    if (!v->shouldIlluminateLocalFeatures(this, localPos) || !model) {
        return;
    }

#ifdef DEBUG_SPECTROGRAM_REPAINT
    cerr << "SpectrogramLayer: illuminateLocalFeatures("
              << localPos.x() << "," << localPos.y() << ")" << endl;
#endif

    double s0, s1;
    double f0, f1;

    if (getXBinRange(v, localPos.x(), s0, s1) &&
        getYBinSourceRange(v, localPos.y(), f0, f1)) {
        
        int s0i = int(s0 + 0.001);
        int s1i = int(s1);
        
        int x0 = v->getXForFrame(s0i * getWindowIncrement());
        int x1 = v->getXForFrame((s1i + 1) * getWindowIncrement());

        int y1 = int(getYForFrequency(v, f1));
        int y0 = int(getYForFrequency(v, f0));
        
#ifdef DEBUG_SPECTROGRAM_REPAINT
        cerr << "SpectrogramLayer: illuminate "
                  << x0 << "," << y1 << " -> " << x1 << "," << y0 << endl;
#endif
        
        paint.setPen(v->getForeground());

        //!!! should we be using paintCrosshairs for this?

        paint.drawRect(x0, y1, x1 - x0 + 1, y0 - y1 + 1);
    }
}

double
SpectrogramLayer::getYForFrequency(const LayerGeometryProvider *v, double frequency) const
{
    return v->getYForFrequency(frequency,
                               getEffectiveMinFrequency(),
                               getEffectiveMaxFrequency(),
                               m_binScale == BinScale::Log);
}

double
SpectrogramLayer::getFrequencyForY(const LayerGeometryProvider *v, int y) const
{
    return v->getFrequencyForY(y,
                               getEffectiveMinFrequency(),
                               getEffectiveMaxFrequency(),
                               m_binScale == BinScale::Log);
}

int
SpectrogramLayer::getCompletion(LayerGeometryProvider *) const
{
    auto fftModel = ModelById::getAs<FFTModel>(m_fftModel);
    if (!fftModel) return 100;
    int completion = fftModel->getCompletion();
#ifdef DEBUG_SPECTROGRAM_REPAINT
    cerr << "SpectrogramLayer::getCompletion: completion = " << completion << endl;
#endif
    return completion;
}

QString
SpectrogramLayer::getError(LayerGeometryProvider *) const
{
    auto fftModel = ModelById::getAs<FFTModel>(m_fftModel);
    if (!fftModel) return "";
    return fftModel->getError();
}

bool
SpectrogramLayer::getValueExtents(double &min, double &max,
                                  bool &logarithmic, QString &unit) const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return false;

    sv_samplerate_t sr = model->getSampleRate();
    min = double(sr) / getFFTSize();
    max = double(sr) / 2;
    
    logarithmic = (m_binScale == BinScale::Log);
    unit = "Hz";
    return true;
}

bool
SpectrogramLayer::getDisplayExtents(double &min, double &max) const
{
    min = getEffectiveMinFrequency();
    max = getEffectiveMaxFrequency();

//    SVDEBUG << "SpectrogramLayer::getDisplayExtents: " << min << "->" << max << endl;
    return true;
}    

bool
SpectrogramLayer::setDisplayExtents(double min, double max)
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return false;

//    SVDEBUG << "SpectrogramLayer::setDisplayExtents: " << min << "->" << max << endl;

    if (min < 0) min = 0;
    if (max > model->getSampleRate()/2.0) max = model->getSampleRate()/2.0;
    
    int minf = int(lrint(min));
    int maxf = int(lrint(max));

    if (m_minFrequency == minf && m_maxFrequency == maxf) return true;

    invalidateRenderers();
    invalidateMagnitudes();

    m_minFrequency = minf;
    m_maxFrequency = maxf;
    
    emit layerParametersChanged();

    int vs = getCurrentVerticalZoomStep();
    if (vs != m_lastEmittedZoomStep) {
        emit verticalZoomChanged();
        m_lastEmittedZoomStep = vs;
    }

    return true;
}

bool
SpectrogramLayer::getYScaleValue(const LayerGeometryProvider *v, int y,
                                 double &value, QString &unit) const
{
    value = getFrequencyForY(v, y);
    unit = "Hz";
    return true;
}

bool
SpectrogramLayer::snapToFeatureFrame(LayerGeometryProvider *,
                                     sv_frame_t &frame,
                                     int &resolution,
                                     SnapType snap) const
{
    resolution = getWindowIncrement();
    sv_frame_t left = (frame / resolution) * resolution;
    sv_frame_t right = left + resolution;

    switch (snap) {
    case SnapLeft:  frame = left;  break;
    case SnapRight: frame = right; break;
    case SnapNeighbouring:
        if (frame - left > right - frame) frame = right;
        else frame = left;
        break;
    }
    
    return true;
} 

void
SpectrogramLayer::measureDoubleClick(LayerGeometryProvider *v, QMouseEvent *e)
{
    const Colour3DPlotRenderer *renderer = getRenderer(v);
    if (!renderer) return;

    QRect rect = renderer->findSimilarRegionExtents(e->pos());
    if (rect.isValid()) {
        MeasureRect mr;
        setMeasureRectFromPixrect(v, mr, rect);
        CommandHistory::getInstance()->addCommand
            (new AddMeasurementRectCommand(this, mr));
    }
}

bool
SpectrogramLayer::getCrosshairExtents(LayerGeometryProvider *v, QPainter &paint,
                                      QPoint cursorPos,
                                      vector<QRect> &extents) const
{
    // Qt 5.13 deprecates QFontMetrics::width(), but its suggested
    // replacement (horizontalAdvance) was only added in Qt 5.11
    // which is too new for us
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"

    QRect vertical(cursorPos.x() - 12, 0, 12, v->getPaintHeight());
    extents.push_back(vertical);

    QRect horizontal(0, cursorPos.y(), cursorPos.x(), 1);
    extents.push_back(horizontal);

    int sw = getVerticalScaleWidth(v, m_haveDetailedScale, paint);

    QRect freq(sw, cursorPos.y() - paint.fontMetrics().ascent() - 2,
               paint.fontMetrics().width("123456 Hz") + 2,
               paint.fontMetrics().height());
    extents.push_back(freq);

    QRect pitch(sw, cursorPos.y() + 2,
                paint.fontMetrics().width("C#10+50c") + 2,
                paint.fontMetrics().height());
    extents.push_back(pitch);

    QRect rt(cursorPos.x(),
             v->getPaintHeight() - paint.fontMetrics().height() - 2,
             paint.fontMetrics().width("1234.567 s"),
             paint.fontMetrics().height());
    extents.push_back(rt);

    int w(paint.fontMetrics().width("1234567890") + 2);
    QRect frame(cursorPos.x() - w - 2,
                v->getPaintHeight() - paint.fontMetrics().height() - 2,
                w,
                paint.fontMetrics().height());
    extents.push_back(frame);

    return true;
}

void
SpectrogramLayer::paintCrosshairs(LayerGeometryProvider *v, QPainter &paint,
                                  QPoint cursorPos) const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return;

    paint.save();
    
    int sw = getVerticalScaleWidth(v, m_haveDetailedScale, paint);

    QFont fn = paint.font();
    if (fn.pointSize() > 8) {
        fn.setPointSize(fn.pointSize() - 1);
        paint.setFont(fn);
    }
    paint.setPen(m_crosshairColour);

    paint.drawLine(0, cursorPos.y(), cursorPos.x() - 1, cursorPos.y());
    paint.drawLine(cursorPos.x(), 0, cursorPos.x(), v->getPaintHeight());
    
    double fundamental = getFrequencyForY(v, cursorPos.y());

    PaintAssistant::drawVisibleText
        (v, paint,
         sw + 2,
         cursorPos.y() - 2,
         QString("%1 Hz").arg(fundamental),
         PaintAssistant::OutlinedText);

    if (Pitch::isFrequencyInMidiRange(fundamental)) {
        QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental);
        PaintAssistant::drawVisibleText
            (v, paint,
             sw + 2,
             cursorPos.y() + paint.fontMetrics().ascent() + 2,
             pitchLabel,
             PaintAssistant::OutlinedText);
    }

    sv_frame_t frame = v->getFrameForX(cursorPos.x());
    RealTime rt = RealTime::frame2RealTime(frame, model->getSampleRate());
    QString rtLabel = QString("%1 s").arg(rt.toText(true).c_str());
    QString frameLabel = QString("%1").arg(frame);
    PaintAssistant::drawVisibleText
        (v, paint,
         cursorPos.x() - paint.fontMetrics().width(frameLabel) - 2,
         v->getPaintHeight() - 2,
         frameLabel,
         PaintAssistant::OutlinedText);
    PaintAssistant::drawVisibleText
        (v, paint,
         cursorPos.x() + 2,
         v->getPaintHeight() - 2,
         rtLabel,
         PaintAssistant::OutlinedText);

    int harmonic = 2;

    while (harmonic < 100) {

        int hy = int(lrint(getYForFrequency(v, fundamental * harmonic)));
        if (hy < 0 || hy > v->getPaintHeight()) break;
        
        int len = 7;

        if (harmonic % 2 == 0) {
            if (harmonic % 4 == 0) {
                len = 12;
            } else {
                len = 10;
            }
        }

        paint.drawLine(cursorPos.x() - len,
                       hy,
                       cursorPos.x(),
                       hy);

        ++harmonic;
    }

    paint.restore();
}

QString
SpectrogramLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
{
    int x = pos.x();
    int y = pos.y();

    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model || !model->isOK()) return "";

    double magMin = 0, magMax = 0;
    double phaseMin = 0, phaseMax = 0;
    double freqMin = 0, freqMax = 0;
    double adjFreqMin = 0, adjFreqMax = 0;
    QString pitchMin, pitchMax;
    RealTime rtMin, rtMax;

    bool haveValues = false;

    if (!getXBinSourceRange(v, x, rtMin, rtMax)) {
        return "";
    }
    if (getXYBinSourceRange(v, x, y, magMin, magMax, phaseMin, phaseMax)) {
        haveValues = true;
    }

    QString adjFreqText = "", adjPitchText = "";

    if (m_binDisplay == BinDisplay::PeakFrequencies) {

        if (!getAdjustedYBinSourceRange(v, x, y, freqMin, freqMax,
                                        adjFreqMin, adjFreqMax)) {
            return "";
        }

        if (adjFreqMin != adjFreqMax) {
            adjFreqText = tr("Peak Frequency:\t%1 - %2 Hz\n")
                .arg(adjFreqMin).arg(adjFreqMax);
        } else {
            adjFreqText = tr("Peak Frequency:\t%1 Hz\n")
                .arg(adjFreqMin);
        }

        QString pmin = Pitch::getPitchLabelForFrequency(adjFreqMin);
        QString pmax = Pitch::getPitchLabelForFrequency(adjFreqMax);

        if (pmin != pmax) {
            adjPitchText = tr("Peak Pitch:\t%3 - %4\n").arg(pmin).arg(pmax);
        } else {
            adjPitchText = tr("Peak Pitch:\t%2\n").arg(pmin);
        }

    } else {
        
        if (!getYBinSourceRange(v, y, freqMin, freqMax)) return "";
    }

    QString text;

    if (rtMin != rtMax) {
        text += tr("Time:\t%1 - %2\n")
            .arg(rtMin.toText(true).c_str())
            .arg(rtMax.toText(true).c_str());
    } else {
        text += tr("Time:\t%1\n")
            .arg(rtMin.toText(true).c_str());
    }

    if (freqMin != freqMax) {
        text += tr("%1Bin Frequency:\t%2 - %3 Hz\n%4Bin Pitch:\t%5 - %6\n")
            .arg(adjFreqText)
            .arg(freqMin)
            .arg(freqMax)
            .arg(adjPitchText)
            .arg(Pitch::getPitchLabelForFrequency(freqMin))
            .arg(Pitch::getPitchLabelForFrequency(freqMax));
    } else {
        text += tr("%1Bin Frequency:\t%2 Hz\n%3Bin Pitch:\t%4\n")
            .arg(adjFreqText)
            .arg(freqMin)
            .arg(adjPitchText)
            .arg(Pitch::getPitchLabelForFrequency(freqMin));
    }   

    if (haveValues) {
        double dbMin = AudioLevel::multiplier_to_dB(magMin);
        double dbMax = AudioLevel::multiplier_to_dB(magMax);
        QString dbMinString;
        QString dbMaxString;
        if (dbMin == AudioLevel::DB_FLOOR) {
            dbMinString = Strings::minus_infinity;
        } else {
            dbMinString = QString("%1").arg(lrint(dbMin));
        }
        if (dbMax == AudioLevel::DB_FLOOR) {
            dbMaxString = Strings::minus_infinity;
        } else {
            dbMaxString = QString("%1").arg(lrint(dbMax));
        }
        if (lrint(dbMin) != lrint(dbMax)) {
            text += tr("dB:\t%1 - %2").arg(dbMinString).arg(dbMaxString);
        } else {
            text += tr("dB:\t%1").arg(dbMinString);
        }
        if (phaseMin != phaseMax) {
            text += tr("\nPhase:\t%1 - %2").arg(phaseMin).arg(phaseMax);
        } else {
            text += tr("\nPhase:\t%1").arg(phaseMin);
        }
    }

    return text;
}

int
SpectrogramLayer::getColourScaleWidth(QPainter &paint) const
{
    int cw;

    cw = paint.fontMetrics().width("-80dB");

    return cw;
}

int
SpectrogramLayer::getVerticalScaleWidth(LayerGeometryProvider *, bool detailed, QPainter &paint) const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model || !model->isOK()) return 0;

    int cw = 0;
    if (detailed) cw = getColourScaleWidth(paint);

    int tw = paint.fontMetrics().width(QString("%1")
                                     .arg(m_maxFrequency > 0 ?
                                          m_maxFrequency - 1 :
                                          model->getSampleRate() / 2));

    int fw = paint.fontMetrics().width(tr("43Hz"));
    if (tw < fw) tw = fw;

    int tickw = (m_binScale == BinScale::Log ? 10 : 4);
    
    return cw + tickw + tw + 13;
}

void
SpectrogramLayer::paintVerticalScale(LayerGeometryProvider *v, bool detailed,
                                     QPainter &paint, QRect rect) const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model || !model->isOK()) {
        return;
    }

    Profiler profiler("SpectrogramLayer::paintVerticalScale");

    //!!! cache this?
    
    int h = rect.height(), w = rect.width();
    int textHeight = paint.fontMetrics().height();

    if (detailed && (h > textHeight * 3 + 10)) {
        paintDetailedScale(v, paint, rect);
    }
    m_haveDetailedScale = detailed;

    int tickw = (m_binScale == BinScale::Log ? 10 : 4);
    int pkw = (m_binScale == BinScale::Log ? 10 : 0);

    int bins = getFFTSize() / 2;
    sv_samplerate_t sr = model->getSampleRate();

    if (m_maxFrequency > 0) {
        bins = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
        if (bins > getFFTSize() / 2) bins = getFFTSize() / 2;
    }

    int cw = 0;
    if (detailed) cw = getColourScaleWidth(paint);

    int py = -1;
    int toff = -textHeight + paint.fontMetrics().ascent() + 2;

    paint.drawLine(cw + 7, 0, cw + 7, h);

    int bin = -1;

    for (int y = 0; y < v->getPaintHeight(); ++y) {

        double q0, q1;
        if (!getYBinRange(v, v->getPaintHeight() - y, q0, q1)) continue;

        int vy;

        if (int(q0) > bin) {
            vy = y;
            bin = int(q0);
        } else {
            continue;
        }

        int freq = int((sr * bin) / getFFTSize());

        if (py >= 0 && (vy - py) < textHeight - 1) {
            if (m_binScale == BinScale::Linear) {
                paint.drawLine(w - tickw, h - vy, w, h - vy);
            }
            continue;
        }

        QString text = QString("%1").arg(freq);
        if (bin == 1) text = tr("%1Hz").arg(freq); // bin 0 is DC
        paint.drawLine(cw + 7, h - vy, w - pkw - 1, h - vy);

        if (h - vy - textHeight >= -2) {
            int tx = w - 3 - paint.fontMetrics().width(text) - max(tickw, pkw);
            paint.drawText(tx, h - vy + toff, text);
        }

        py = vy;
    }

    if (m_binScale == BinScale::Log) {

        // piano keyboard

        PianoScale().paintPianoVertical
            (v, paint, QRect(w - pkw - 1, 0, pkw, h),
             getEffectiveMinFrequency(), getEffectiveMaxFrequency());
    }

    m_haveDetailedScale = detailed;
}

void
SpectrogramLayer::paintDetailedScale(LayerGeometryProvider *v,
                                     QPainter &paint, QRect rect) const
{
    // The colour scale

    if (m_colourScale == ColourScaleType::Phase) {
        paintDetailedScalePhase(v, paint, rect);
        return;
    }
    
    int h = rect.height();
    int textHeight = paint.fontMetrics().height();
    int toff = -textHeight + paint.fontMetrics().ascent() + 2;

    int cw = getColourScaleWidth(paint);
    int cbw = paint.fontMetrics().width("dB");

    int topLines = 2;

    int ch = h - textHeight * (topLines + 1) - 8;
//      paint.drawRect(4, textHeight + 4, cw - 1, ch + 1);
    paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);

    QString top, bottom;
    double min = m_viewMags[v->getId()].getMin();
    double max = m_viewMags[v->getId()].getMax();

    if (min < m_threshold) min = m_threshold;
    if (max <= min) max = min + 0.1;
        
    double dBmin = AudioLevel::multiplier_to_dB(min);
    double dBmax = AudioLevel::multiplier_to_dB(max);

#ifdef DEBUG_SPECTROGRAM_REPAINT
    cerr << "paintVerticalScale: for view id " << v->getId()
         << ": min = " << min << ", max = " << max
         << ", dBmin = " << dBmin << ", dBmax = " << dBmax << endl;
#endif
        
    if (dBmax < -60.f) dBmax = -60.f;
    else top = QString("%1").arg(lrint(dBmax));

    if (dBmin < dBmax - 60.f) dBmin = dBmax - 60.f;
    bottom = QString("%1").arg(lrint(dBmin));

#ifdef DEBUG_SPECTROGRAM_REPAINT
    cerr << "adjusted dB range to min = " << dBmin << ", max = " << dBmax
         << endl;
#endif
        
    paint.drawText((cw + 6 - paint.fontMetrics().width("dBFS")) / 2,
                   2 + textHeight + toff, "dBFS");

    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
                   2 + textHeight * topLines + toff + textHeight/2, top);

    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
                   h + toff - 3 - textHeight/2, bottom);

    paint.save();
    paint.setBrush(Qt::NoBrush);

    int lasty = 0;
    int lastdb = 0;

    for (int i = 0; i < ch; ++i) {

        double dBval = dBmin + (((dBmax - dBmin) * i) / (ch - 1));
        int idb = int(dBval);

        double value = AudioLevel::dB_to_multiplier(dBval);
        paint.setPen(getRenderer(v)->getColour(value));

        int y = textHeight * topLines + 4 + ch - i;

        paint.drawLine(5 + cw - cbw, y, cw + 2, y);
        
        if (i == 0) {
            lasty = y;
            lastdb = idb;
        } else if (i < ch - paint.fontMetrics().ascent() &&
                   idb != lastdb &&
                   ((abs(y - lasty) > textHeight && 
                     idb % 10 == 0) ||
                    (abs(y - lasty) > paint.fontMetrics().ascent() && 
                     idb % 5 == 0))) {
            paint.setPen(v->getForeground());
            QString text = QString("%1").arg(idb);
            paint.drawText(3 + cw - cbw - paint.fontMetrics().width(text),
                           y + toff + textHeight/2, text);
            paint.drawLine(5 + cw - cbw, y, 8 + cw - cbw, y);
            lasty = y;
            lastdb = idb;
        }
    }
    paint.restore();
}

void
SpectrogramLayer::paintDetailedScalePhase(LayerGeometryProvider *v,
                                          QPainter &paint, QRect rect) const
{
    // The colour scale in phase mode
    
    int h = rect.height();
    int textHeight = paint.fontMetrics().height();
    int toff = -textHeight + paint.fontMetrics().ascent() + 2;

    int cw = getColourScaleWidth(paint);

    // Phase is not measured in dB of course, but this places the
    // scale at the same position as in the magnitude spectrogram
    int cbw = paint.fontMetrics().width("dB");

    int topLines = 1;

    int ch = h - textHeight * (topLines + 1) - 8;
    paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);

    QString top = Strings::pi, bottom = Strings::minus_pi, middle = "0";
    
    double min = -M_PI;
    double max =  M_PI;

    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
                   2 + textHeight * topLines + toff + textHeight/2, top);

    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(middle),
                   2 + textHeight * topLines + ch/2 + toff + textHeight/2, middle);

    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
                   h + toff - 3 - textHeight/2, bottom);

    paint.save();
    paint.setBrush(Qt::NoBrush);

    for (int i = 0; i < ch; ++i) {
        double val = min + (((max - min) * i) / (ch - 1));
        paint.setPen(getRenderer(v)->getColour(val));
        int y = textHeight * topLines + 4 + ch - i;
        paint.drawLine(5 + cw - cbw, y, cw + 2, y);
    }
    paint.restore();
}

class SpectrogramRangeMapper : public RangeMapper
{
public:
    SpectrogramRangeMapper(sv_samplerate_t sr, int /* fftsize */) :
        m_dist(sr / 2),
        m_s2(sqrt(sqrt(2))) { }
    ~SpectrogramRangeMapper() override { }
    
    int getPositionForValue(double value) const override {

        double dist = m_dist;
    
        int n = 0;

        while (dist > (value + 0.00001) && dist > 0.1) {
            dist /= m_s2;
            ++n;
        }

        return n;
    }
    
    int getPositionForValueUnclamped(double value) const override {
        // We don't really support this
        return getPositionForValue(value);
    }

    double getValueForPosition(int position) const override {

        // Vertical zoom step 0 shows the entire range from DC ->
        // Nyquist frequency.  Step 1 shows 2^(1/4) of the range of
        // step 0, and so on until the visible range is smaller than
        // the frequency step between bins at the current fft size.

        double dist = m_dist;
    
        int n = 0;
        while (n < position) {
            dist /= m_s2;
            ++n;
        }

        return dist;
    }
    
    double getValueForPositionUnclamped(int position) const override {
        // We don't really support this
        return getValueForPosition(position);
    }

    QString getUnit() const override { return "Hz"; }

protected:
    double m_dist;
    double m_s2;
};

int
SpectrogramLayer::getVerticalZoomSteps(int &defaultStep) const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return 0;

    sv_samplerate_t sr = model->getSampleRate();

    SpectrogramRangeMapper mapper(sr, getFFTSize());

//    int maxStep = mapper.getPositionForValue((double(sr) / getFFTSize()) + 0.001);
    int maxStep = mapper.getPositionForValue(0);
    int minStep = mapper.getPositionForValue(double(sr) / 2);

    int initialMax = m_initialMaxFrequency;
    if (initialMax == 0) initialMax = int(sr / 2);

    defaultStep = mapper.getPositionForValue(initialMax) - minStep;

//    SVDEBUG << "SpectrogramLayer::getVerticalZoomSteps: " << maxStep - minStep << " (" << maxStep <<"-" << minStep << "), default is " << defaultStep << " (from initial max freq " << initialMax << ")" << endl;

    return maxStep - minStep;
}

int
SpectrogramLayer::getCurrentVerticalZoomStep() const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return 0;

    double dmin, dmax;
    getDisplayExtents(dmin, dmax);
    
    SpectrogramRangeMapper mapper(model->getSampleRate(), getFFTSize());
    int n = mapper.getPositionForValue(dmax - dmin);
//    SVDEBUG << "SpectrogramLayer::getCurrentVerticalZoomStep: " << n << endl;
    return n;
}

void
SpectrogramLayer::setVerticalZoomStep(int step)
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return;

    double dmin = m_minFrequency, dmax = m_maxFrequency;
//    getDisplayExtents(dmin, dmax);

//    cerr << "current range " << dmin << " -> " << dmax << ", range " << dmax-dmin << ", mid " << (dmax + dmin)/2 << endl;
    
    sv_samplerate_t sr = model->getSampleRate();
    SpectrogramRangeMapper mapper(sr, getFFTSize());
    double newdist = mapper.getValueForPosition(step);

    double newmin, newmax;

    if (m_binScale == BinScale::Log) {

        // need to pick newmin and newmax such that
        //
        // (log(newmin) + log(newmax)) / 2 == logmid
        // and
        // newmax - newmin = newdist
        //
        // so log(newmax - newdist) + log(newmax) == 2logmid
        // log(newmax(newmax - newdist)) == 2logmid
        // newmax.newmax - newmax.newdist == exp(2logmid)
        // newmax^2 + (-newdist)newmax + -exp(2logmid) == 0
        // quadratic with a = 1, b = -newdist, c = -exp(2logmid), all known
        // 
        // positive root
        // newmax = (newdist + sqrt(newdist^2 + 4exp(2logmid))) / 2
        //
        // but logmid = (log(dmin) + log(dmax)) / 2
        // so exp(2logmid) = exp(log(dmin) + log(dmax))
        // = exp(log(dmin.dmax))
        // = dmin.dmax
        // so newmax = (newdist + sqrtf(newdist^2 + 4dmin.dmax)) / 2

        newmax = (newdist + sqrt(newdist*newdist + 4*dmin*dmax)) / 2;
        newmin = newmax - newdist;

//        cerr << "newmin = " << newmin << ", newmax = " << newmax << endl;

    } else {
        double dmid = (dmax + dmin) / 2;
        newmin = dmid - newdist / 2;
        newmax = dmid + newdist / 2;
    }

    double mmin, mmax;
    mmin = 0;
    mmax = double(sr) / 2;
    
    if (newmin < mmin) {
        newmax += (mmin - newmin);
        newmin = mmin;
    }
    if (newmax > mmax) {
        newmax = mmax;
    }
    
//    SVDEBUG << "SpectrogramLayer::setVerticalZoomStep: " << step << ": " << newmin << " -> " << newmax << " (range " << newdist << ")" << endl;

    setMinFrequency(int(lrint(newmin)));
    setMaxFrequency(int(lrint(newmax)));
}

RangeMapper *
SpectrogramLayer::getNewVerticalZoomRangeMapper() const
{
    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
    if (!model) return nullptr;
    return new SpectrogramRangeMapper(model->getSampleRate(), getFFTSize());
}

void
SpectrogramLayer::updateMeasureRectYCoords(LayerGeometryProvider *v, const MeasureRect &r) const
{
    int y0 = 0;
    if (r.startY > 0.0) y0 = int(getYForFrequency(v, r.startY));
    
    int y1 = y0;
    if (r.endY > 0.0) y1 = int(getYForFrequency(v, r.endY));

//    SVDEBUG << "SpectrogramLayer::updateMeasureRectYCoords: start " << r.startY << " -> " << y0 << ", end " << r.endY << " -> " << y1 << endl;

    r.pixrect = QRect(r.pixrect.x(), y0, r.pixrect.width(), y1 - y0);
}

void
SpectrogramLayer::setMeasureRectYCoord(LayerGeometryProvider *v, MeasureRect &r, bool start, int y) const
{
    if (start) {
        r.startY = getFrequencyForY(v, y);
        r.endY = r.startY;
    } else {
        r.endY = getFrequencyForY(v, y);
    }
//    SVDEBUG << "SpectrogramLayer::setMeasureRectYCoord: start " << r.startY << " <- " << y << ", end " << r.endY << " <- " << y << endl;

}

void
SpectrogramLayer::toXml(QTextStream &stream,
                        QString indent, QString extraAttributes) const
{
    QString s;
    
    s += QString("channel=\"%1\" "
                 "windowSize=\"%2\" "
                 "windowHopLevel=\"%3\" "
                 "oversampling=\"%4\" "
                 "gain=\"%5\" "
                 "threshold=\"%6\" ")
        .arg(m_channel)
        .arg(m_windowSize)
        .arg(m_windowHopLevel)
        .arg(m_oversampling)
        .arg(m_gain)
        .arg(m_threshold);

    s += QString("minFrequency=\"%1\" "
                 "maxFrequency=\"%2\" "
                 "colourScale=\"%3\" "
                 "colourRotation=\"%4\" "
                 "frequencyScale=\"%5\" "
                 "binDisplay=\"%6\" ")
        .arg(m_minFrequency)
        .arg(m_maxFrequency)
        .arg(convertFromColourScale(m_colourScale, m_colourScaleMultiple))
        .arg(m_colourRotation)
        .arg(int(m_binScale))
        .arg(int(m_binDisplay));

    // New-style colour map attribute, by string id rather than by
    // number

    s += QString("colourMap=\"%1\" ")
        .arg(ColourMapper::getColourMapId(m_colourMap));

    // Old-style colour map attribute

    s += QString("colourScheme=\"%1\" ")
        .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap));
    
    // New-style normalization attributes, allowing for more types of
    // normalization in future: write out the column normalization
    // type separately, and then whether we are normalizing visible
    // area as well afterwards
    
    s += QString("columnNormalization=\"%1\" ")
        .arg(m_normalization == ColumnNormalization::Max1 ? "peak" :
             m_normalization == ColumnNormalization::Hybrid ? "hybrid" : "none");

    // Old-style normalization attribute. We *don't* write out
    // normalizeHybrid here because the only release that would accept
    // it (Tony v1.0) has a totally different scale factor for
    // it. We'll just have to accept that session files from Tony
    // v2.0+ will look odd in Tony v1.0
    
    s += QString("normalizeColumns=\"%1\" ")
        .arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false");

    // And this applies to both old- and new-style attributes
    
    s += QString("normalizeVisibleArea=\"%1\" ")
        .arg(m_normalizeVisibleArea ? "true" : "false");
    
    Layer::toXml(stream, indent, extraAttributes + " " + s);
}

void
SpectrogramLayer::setProperties(const QXmlAttributes &attributes)
{
    bool ok = false;

    int channel = attributes.value("channel").toInt(&ok);
    if (ok) setChannel(channel);

    int windowSize = attributes.value("windowSize").toUInt(&ok);
    if (ok) setWindowSize(windowSize);

    int windowHopLevel = attributes.value("windowHopLevel").toUInt(&ok);
    if (ok) setWindowHopLevel(windowHopLevel);
    else {
        int windowOverlap = attributes.value("windowOverlap").toUInt(&ok);
        // a percentage value
        if (ok) {
            if (windowOverlap == 0) setWindowHopLevel(0);
            else if (windowOverlap == 25) setWindowHopLevel(1);
            else if (windowOverlap == 50) setWindowHopLevel(2);
            else if (windowOverlap == 75) setWindowHopLevel(3);
            else if (windowOverlap == 90) setWindowHopLevel(4);
        }
    }

    int oversampling = attributes.value("oversampling").toUInt(&ok);
    if (ok) setOversampling(oversampling);

    float gain = attributes.value("gain").toFloat(&ok);
    if (ok) setGain(gain);

    float threshold = attributes.value("threshold").toFloat(&ok);
    if (ok) setThreshold(threshold);

    int minFrequency = attributes.value("minFrequency").toUInt(&ok);
    if (ok) {
        SVDEBUG << "SpectrogramLayer::setProperties: setting min freq to " << minFrequency << endl;
        setMinFrequency(minFrequency);
    }

    int maxFrequency = attributes.value("maxFrequency").toUInt(&ok);
    if (ok) {
        SVDEBUG << "SpectrogramLayer::setProperties: setting max freq to " << maxFrequency << endl;
        setMaxFrequency(maxFrequency);
    }

    auto colourScale = convertToColourScale
        (attributes.value("colourScale").toInt(&ok));
    if (ok) {
        setColourScale(colourScale.first);
        setColourScaleMultiple(colourScale.second);
    }

    QString colourMapId = attributes.value("colourMap");
    int colourMap = ColourMapper::getColourMapById(colourMapId);
    if (colourMap >= 0) {
        setColourMap(colourMap);
    } else {
        colourMap = attributes.value("colourScheme").toInt(&ok);
        if (ok && colourMap < ColourMapper::getColourMapCount()) {
            setColourMap(colourMap);
        }
    }

    int colourRotation = attributes.value("colourRotation").toInt(&ok);
    if (ok) setColourRotation(colourRotation);

    BinScale binScale = (BinScale)
        attributes.value("frequencyScale").toInt(&ok);
    if (ok) setBinScale(binScale);

    BinDisplay binDisplay = (BinDisplay)
        attributes.value("binDisplay").toInt(&ok);
    if (ok) setBinDisplay(binDisplay);

    bool haveNewStyleNormalization = false;
    
    QString columnNormalization = attributes.value("columnNormalization");

    if (columnNormalization != "") {

        haveNewStyleNormalization = true;

        if (columnNormalization == "peak") {
            setNormalization(ColumnNormalization::Max1);
        } else if (columnNormalization == "hybrid") {
            setNormalization(ColumnNormalization::Hybrid);
        } else if (columnNormalization == "none") {
            setNormalization(ColumnNormalization::None);
        } else {
            SVCERR << "NOTE: Unknown or unsupported columnNormalization attribute \""
                 << columnNormalization << "\"" << endl;
        }
    }

    if (!haveNewStyleNormalization) {

        bool normalizeColumns =
            (attributes.value("normalizeColumns").trimmed() == "true");
        if (normalizeColumns) {
            setNormalization(ColumnNormalization::Max1);
        }

        bool normalizeHybrid =
            (attributes.value("normalizeHybrid").trimmed() == "true");
        if (normalizeHybrid) {
            setNormalization(ColumnNormalization::Hybrid);
        }
    }

    bool normalizeVisibleArea =
        (attributes.value("normalizeVisibleArea").trimmed() == "true");
    setNormalizeVisibleArea(normalizeVisibleArea);

    if (!haveNewStyleNormalization && m_normalization == ColumnNormalization::Hybrid) {
        // Tony v1.0 is (and hopefully will remain!) the only released
        // SV-a-like to use old-style attributes when saving sessions
        // that ask for hybrid normalization. It saves them with the
        // wrong gain factor, so hack in a fix for that here -- this
        // gives us backward but not forward compatibility.
        setGain(m_gain / float(getFFTSize() / 2));
    }
}