Chris@58: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@0: Chris@0: /* Chris@59: Sonic Visualiser Chris@59: An audio file viewer and annotation editor. Chris@59: Centre for Digital Music, Queen Mary, University of London. Chris@484: This file copyright 2006-2009 Chris Cannam and QMUL. Chris@0: Chris@59: This program is free software; you can redistribute it and/or Chris@59: modify it under the terms of the GNU General Public License as Chris@59: published by the Free Software Foundation; either version 2 of the Chris@59: License, or (at your option) any later version. See the file Chris@59: COPYING included with this distribution for more information. Chris@0: */ Chris@0: Chris@0: #include "SpectrogramLayer.h" Chris@0: Chris@128: #include "view/View.h" Chris@0: #include "base/Profiler.h" Chris@0: #include "base/AudioLevel.h" Chris@0: #include "base/Window.h" Chris@24: #include "base/Pitch.h" Chris@118: #include "base/Preferences.h" Chris@167: #include "base/RangeMapper.h" Chris@253: #include "base/LogRange.h" Chris@1063: #include "base/ColumnOp.h" Chris@1147: #include "base/Strings.h" Chris@1212: #include "base/StorageAdviser.h" Chris@1212: #include "base/Exceptions.h" Chris@376: #include "widgets/CommandHistory.h" Chris@1078: #include "data/model/Dense3DModelPeakCache.h" Chris@1078: Chris@376: #include "ColourMapper.h" Chris@690: #include "PianoScale.h" Chris@1078: #include "PaintAssistant.h" Chris@1089: #include "Colour3DPlotRenderer.h" Chris@0: Chris@0: #include Chris@0: #include Chris@0: #include Chris@0: #include Chris@92: #include Chris@178: #include Chris@283: #include Chris@316: #include Chris@1017: #include Chris@0: Chris@0: #include Chris@0: Chris@0: #include Chris@0: #include Chris@0: Chris@1143: //#define DEBUG_SPECTROGRAM 1 Chris@1143: //#define DEBUG_SPECTROGRAM_REPAINT 1 Chris@1025: Chris@1025: using namespace std; Chris@907: Chris@44: SpectrogramLayer::SpectrogramLayer(Configuration config) : Chris@0: m_channel(0), Chris@0: m_windowSize(1024), Chris@0: m_windowType(HanningWindow), Chris@97: m_windowHopLevel(2), Chris@1379: m_oversampling(1), Chris@0: m_gain(1.0), Chris@215: m_initialGain(1.0), Chris@1128: m_threshold(1.0e-8f), Chris@1128: m_initialThreshold(1.0e-8f), Chris@9: m_colourRotation(0), Chris@215: m_initialRotation(0), Chris@119: m_minFrequency(10), Chris@0: m_maxFrequency(8000), Chris@135: m_initialMaxFrequency(8000), Chris@1504: m_verticallyFixed(false), Chris@1105: m_colourScale(ColourScaleType::Log), Chris@1137: m_colourScaleMultiple(1.0), Chris@197: m_colourMap(0), Chris@1362: m_colourInverted(false), Chris@1103: m_binScale(BinScale::Linear), Chris@1103: m_binDisplay(BinDisplay::AllBins), Chris@1104: m_normalization(ColumnNormalization::None), Chris@1104: m_normalizeVisibleArea(false), Chris@133: m_lastEmittedZoomStep(-1), Chris@390: m_synchronous(false), Chris@608: m_haveDetailedScale(false), Chris@193: m_exiting(false), Chris@1088: m_peakCacheDivisor(8) Chris@0: { Chris@1017: QString colourConfigName = "spectrogram-colour"; Chris@1017: int colourConfigDefault = int(ColourMapper::Green); Chris@1017: Chris@215: if (config == FullRangeDb) { Chris@215: m_initialMaxFrequency = 0; Chris@215: setMaxFrequency(0); Chris@215: } else if (config == MelodicRange) { Chris@1234: setWindowSize(8192); Chris@1234: setWindowHopLevel(4); Chris@215: m_initialMaxFrequency = 1500; Chris@1234: setMaxFrequency(1500); Chris@215: setMinFrequency(40); Chris@1234: setColourScale(ColourScaleType::Linear); Chris@215: setColourMap(ColourMapper::Sunset); Chris@1103: setBinScale(BinScale::Log); Chris@1017: colourConfigName = "spectrogram-melodic-colour"; Chris@1017: colourConfigDefault = int(ColourMapper::Sunset); Chris@224: // setGain(20); Chris@37: } else if (config == MelodicPeaks) { Chris@1234: setWindowSize(4096); Chris@1234: setWindowHopLevel(5); Chris@135: m_initialMaxFrequency = 2000; Chris@1234: setMaxFrequency(2000); Chris@1234: setMinFrequency(40); Chris@1234: setBinScale(BinScale::Log); Chris@1234: setColourScale(ColourScaleType::Linear); Chris@1234: setBinDisplay(BinDisplay::PeakFrequencies); Chris@1104: setNormalization(ColumnNormalization::Max1); Chris@1017: colourConfigName = "spectrogram-melodic-colour"; Chris@1017: colourConfigDefault = int(ColourMapper::Sunset); Chris@0: } Chris@110: Chris@1017: QSettings settings; Chris@1017: settings.beginGroup("Preferences"); Chris@1017: setColourMap(settings.value(colourConfigName, colourConfigDefault).toInt()); Chris@1017: settings.endGroup(); Chris@1017: Chris@122: Preferences *prefs = Preferences::getInstance(); Chris@122: connect(prefs, SIGNAL(propertyChanged(PropertyContainer::PropertyName)), Chris@122: this, SLOT(preferenceChanged(PropertyContainer::PropertyName))); Chris@122: setWindowType(prefs->getWindowType()); Chris@0: } Chris@0: Chris@0: SpectrogramLayer::~SpectrogramLayer() Chris@0: { Chris@1106: invalidateRenderers(); Chris@1242: deleteDerivedModels(); Chris@1242: } Chris@1242: Chris@1242: void Chris@1504: SpectrogramLayer::setVerticallyFixed() Chris@1504: { Chris@1504: if (m_verticallyFixed) return; Chris@1504: m_verticallyFixed = true; Chris@1504: recreateFFTModel(); Chris@1504: } Chris@1504: Chris@1504: void Chris@1242: SpectrogramLayer::deleteDerivedModels() Chris@1242: { Chris@1473: ModelById::release(m_fftModel); Chris@1473: ModelById::release(m_peakCache); Chris@1473: ModelById::release(m_wholeCache); Chris@1473: Chris@1473: m_fftModel = {}; Chris@1473: m_peakCache = {}; Chris@1473: m_wholeCache = {}; Chris@0: } Chris@0: Chris@1137: pair Chris@1104: SpectrogramLayer::convertToColourScale(int value) Chris@1104: { Chris@1104: switch (value) { Chris@1137: case 0: return { ColourScaleType::Linear, 1.0 }; Chris@1137: case 1: return { ColourScaleType::Meter, 1.0 }; Chris@1137: case 2: return { ColourScaleType::Log, 2.0 }; // dB^2 (i.e. log of power) Chris@1137: case 3: return { ColourScaleType::Log, 1.0 }; // dB (of magnitude) Chris@1137: case 4: return { ColourScaleType::Phase, 1.0 }; Chris@1137: default: return { ColourScaleType::Linear, 1.0 }; Chris@1104: } Chris@1104: } Chris@1104: Chris@1104: int Chris@1137: SpectrogramLayer::convertFromColourScale(ColourScaleType scale, double multiple) Chris@1104: { Chris@1104: switch (scale) { Chris@1105: case ColourScaleType::Linear: return 0; Chris@1105: case ColourScaleType::Meter: return 1; Chris@1137: case ColourScaleType::Log: return (multiple > 1.5 ? 2 : 3); Chris@1105: case ColourScaleType::Phase: return 4; Chris@1105: case ColourScaleType::PlusMinusOne: Chris@1105: case ColourScaleType::Absolute: Chris@1104: default: return 0; Chris@1104: } Chris@1104: } Chris@1104: Chris@1104: std::pair Chris@1104: SpectrogramLayer::convertToColumnNorm(int value) Chris@1104: { Chris@1104: switch (value) { Chris@1104: default: Chris@1104: case 0: return { ColumnNormalization::None, false }; Chris@1104: case 1: return { ColumnNormalization::Max1, false }; Chris@1104: case 2: return { ColumnNormalization::None, true }; // visible area Chris@1104: case 3: return { ColumnNormalization::Hybrid, false }; Chris@1104: } Chris@1104: } Chris@1104: Chris@1104: int Chris@1104: SpectrogramLayer::convertFromColumnNorm(ColumnNormalization norm, bool visible) Chris@1104: { Chris@1104: if (visible) return 2; Chris@1104: switch (norm) { Chris@1104: case ColumnNormalization::None: return 0; Chris@1104: case ColumnNormalization::Max1: return 1; Chris@1104: case ColumnNormalization::Hybrid: return 3; Chris@1104: Chris@1104: case ColumnNormalization::Sum1: Chris@1251: case ColumnNormalization::Range01: Chris@1104: default: return 0; Chris@1104: } Chris@1104: } Chris@1104: Chris@0: void Chris@1471: SpectrogramLayer::setModel(ModelId modelId) Chris@0: { Chris@1471: auto newModel = ModelById::getAs(modelId); Chris@1471: if (!modelId.isNone() && !newModel) { Chris@1471: throw std::logic_error("Not a DenseTimeValueModel"); Chris@1471: } Chris@1471: Chris@1471: if (modelId == m_model) return; Chris@1471: m_model = modelId; Chris@1471: Chris@1471: if (newModel) { Chris@1471: recreateFFTModel(); Chris@1471: Chris@1471: connectSignals(m_model); Chris@1471: Chris@1481: connect(newModel.get(), Chris@1481: SIGNAL(modelChanged(ModelId)), Chris@1481: this, SLOT(cacheInvalid(ModelId))); Chris@1481: connect(newModel.get(), Chris@1481: SIGNAL(modelChangedWithin(ModelId, sv_frame_t, sv_frame_t)), Chris@1481: this, SLOT(cacheInvalid(ModelId, sv_frame_t, sv_frame_t))); Chris@1471: } Chris@1471: Chris@0: emit modelReplaced(); Chris@110: } Chris@115: Chris@0: Layer::PropertyList Chris@0: SpectrogramLayer::getProperties() const Chris@0: { Chris@0: PropertyList list; Chris@87: list.push_back("Colour"); Chris@87: list.push_back("Colour Scale"); Chris@87: list.push_back("Window Size"); Chris@97: list.push_back("Window Increment"); Chris@1379: list.push_back("Oversampling"); Chris@862: list.push_back("Normalization"); Chris@87: list.push_back("Bin Display"); Chris@87: list.push_back("Threshold"); Chris@87: list.push_back("Gain"); Chris@87: list.push_back("Colour Rotation"); Chris@153: // list.push_back("Min Frequency"); Chris@153: // list.push_back("Max Frequency"); Chris@87: list.push_back("Frequency Scale"); Chris@0: return list; Chris@0: } Chris@0: Chris@87: QString Chris@87: SpectrogramLayer::getPropertyLabel(const PropertyName &name) const Chris@87: { Chris@87: if (name == "Colour") return tr("Colour"); Chris@87: if (name == "Colour Scale") return tr("Colour Scale"); Chris@87: if (name == "Window Size") return tr("Window Size"); Chris@112: if (name == "Window Increment") return tr("Window Overlap"); Chris@1379: if (name == "Oversampling") return tr("Oversampling"); Chris@862: if (name == "Normalization") return tr("Normalization"); Chris@87: if (name == "Bin Display") return tr("Bin Display"); Chris@87: if (name == "Threshold") return tr("Threshold"); Chris@87: if (name == "Gain") return tr("Gain"); Chris@87: if (name == "Colour Rotation") return tr("Colour Rotation"); Chris@87: if (name == "Min Frequency") return tr("Min Frequency"); Chris@87: if (name == "Max Frequency") return tr("Max Frequency"); Chris@87: if (name == "Frequency Scale") return tr("Frequency Scale"); Chris@87: return ""; Chris@87: } Chris@87: Chris@335: QString Chris@862: SpectrogramLayer::getPropertyIconName(const PropertyName &) const Chris@335: { Chris@335: return ""; Chris@335: } Chris@335: Chris@0: Layer::PropertyType Chris@0: SpectrogramLayer::getPropertyType(const PropertyName &name) const Chris@0: { Chris@87: if (name == "Gain") return RangeProperty; Chris@87: if (name == "Colour Rotation") return RangeProperty; Chris@87: if (name == "Threshold") return RangeProperty; Chris@1198: if (name == "Colour") return ColourMapProperty; Chris@0: return ValueProperty; Chris@0: } Chris@0: Chris@0: QString Chris@0: SpectrogramLayer::getPropertyGroupName(const PropertyName &name) const Chris@0: { Chris@153: if (name == "Bin Display" || Chris@153: name == "Frequency Scale") return tr("Bins"); Chris@87: if (name == "Window Size" || Chris@1379: name == "Window Increment" || Chris@1379: name == "Oversampling") return tr("Window"); Chris@87: if (name == "Colour" || Chris@1234: name == "Threshold" || Chris@1234: name == "Colour Rotation") return tr("Colour"); Chris@862: if (name == "Normalization" || Chris@153: name == "Gain" || Chris@1234: name == "Colour Scale") return tr("Scale"); Chris@0: return QString(); Chris@0: } Chris@0: Chris@0: int Chris@0: SpectrogramLayer::getPropertyRangeAndValue(const PropertyName &name, Chris@1234: int *min, int *max, int *deflt) const Chris@0: { Chris@216: int val = 0; Chris@216: Chris@216: int garbage0, garbage1, garbage2; Chris@55: if (!min) min = &garbage0; Chris@55: if (!max) max = &garbage1; Chris@216: if (!deflt) deflt = &garbage2; Chris@10: Chris@87: if (name == "Gain") { Chris@0: Chris@1234: *min = -50; Chris@1234: *max = 50; Chris@0: Chris@906: *deflt = int(lrint(log10(m_initialGain) * 20.0)); Chris@1234: if (*deflt < *min) *deflt = *min; Chris@1234: if (*deflt > *max) *deflt = *max; Chris@1234: Chris@1234: val = int(lrint(log10(m_gain) * 20.0)); Chris@1234: if (val < *min) val = *min; Chris@1234: if (val > *max) val = *max; Chris@0: Chris@87: } else if (name == "Threshold") { Chris@37: Chris@1234: *min = -81; Chris@1234: *max = -1; Chris@37: Chris@906: *deflt = int(lrint(AudioLevel::multiplier_to_dB(m_initialThreshold))); Chris@1234: if (*deflt < *min) *deflt = *min; Chris@1234: if (*deflt > *max) *deflt = *max; Chris@1234: Chris@1234: val = int(lrint(AudioLevel::multiplier_to_dB(m_threshold))); Chris@1234: if (val < *min) val = *min; Chris@1234: if (val > *max) val = *max; Chris@37: Chris@87: } else if (name == "Colour Rotation") { Chris@9: Chris@1234: *min = 0; Chris@1234: *max = 256; Chris@216: *deflt = m_initialRotation; Chris@216: Chris@1234: val = m_colourRotation; Chris@9: Chris@87: } else if (name == "Colour Scale") { Chris@0: Chris@1099: // linear, meter, db^2, db, phase Chris@1234: *min = 0; Chris@1234: *max = 4; Chris@1092: *deflt = 2; Chris@216: Chris@1234: val = convertFromColourScale(m_colourScale, m_colourScaleMultiple); Chris@0: Chris@87: } else if (name == "Colour") { Chris@0: Chris@1234: *min = 0; Chris@1234: *max = ColourMapper::getColourMapCount() - 1; Chris@216: *deflt = 0; Chris@216: Chris@1234: val = m_colourMap; Chris@0: Chris@87: } else if (name == "Window Size") { Chris@0: Chris@1234: *min = 0; Chris@1234: *max = 10; Chris@216: *deflt = 5; Chris@1234: Chris@1234: val = 0; Chris@1234: int ws = m_windowSize; Chris@1234: while (ws > 32) { ws >>= 1; val ++; } Chris@0: Chris@97: } else if (name == "Window Increment") { Chris@1234: Chris@1234: *min = 0; Chris@1234: *max = 5; Chris@216: *deflt = 2; Chris@216: Chris@216: val = m_windowHopLevel; Chris@1379: Chris@1379: } else if (name == "Oversampling") { Chris@1379: Chris@1379: *min = 0; Chris@1379: *max = 3; Chris@1379: *deflt = 0; Chris@1379: Chris@1379: val = 0; Chris@1379: int ov = m_oversampling; Chris@1379: while (ov > 1) { ov >>= 1; val ++; } Chris@1379: Chris@87: } else if (name == "Min Frequency") { Chris@37: Chris@1234: *min = 0; Chris@1234: *max = 9; Chris@216: *deflt = 1; Chris@37: Chris@1234: switch (m_minFrequency) { Chris@1234: case 0: default: val = 0; break; Chris@1234: case 10: val = 1; break; Chris@1234: case 20: val = 2; break; Chris@1234: case 40: val = 3; break; Chris@1234: case 100: val = 4; break; Chris@1234: case 250: val = 5; break; Chris@1234: case 500: val = 6; break; Chris@1234: case 1000: val = 7; break; Chris@1234: case 4000: val = 8; break; Chris@1234: case 10000: val = 9; break; Chris@1234: } Chris@37: Chris@87: } else if (name == "Max Frequency") { Chris@0: Chris@1234: *min = 0; Chris@1234: *max = 9; Chris@216: *deflt = 6; Chris@0: Chris@1234: switch (m_maxFrequency) { Chris@1234: case 500: val = 0; break; Chris@1234: case 1000: val = 1; break; Chris@1234: case 1500: val = 2; break; Chris@1234: case 2000: val = 3; break; Chris@1234: case 4000: val = 4; break; Chris@1234: case 6000: val = 5; break; Chris@1234: case 8000: val = 6; break; Chris@1234: case 12000: val = 7; break; Chris@1234: case 16000: val = 8; break; Chris@1234: default: val = 9; break; Chris@1234: } Chris@0: Chris@87: } else if (name == "Frequency Scale") { Chris@0: Chris@1234: *min = 0; Chris@1234: *max = 1; Chris@1103: *deflt = int(BinScale::Linear); Chris@1234: val = (int)m_binScale; Chris@0: Chris@87: } else if (name == "Bin Display") { Chris@35: Chris@1234: *min = 0; Chris@1234: *max = 2; Chris@1103: *deflt = int(BinDisplay::AllBins); Chris@1234: val = (int)m_binDisplay; Chris@35: Chris@862: } else if (name == "Normalization") { Chris@1234: Chris@862: *min = 0; Chris@862: *max = 3; Chris@1104: *deflt = 0; Chris@1104: Chris@1104: val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea); Chris@120: Chris@0: } else { Chris@1234: val = Layer::getPropertyRangeAndValue(name, min, max, deflt); Chris@0: } Chris@0: Chris@216: return val; Chris@0: } Chris@0: Chris@0: QString Chris@0: SpectrogramLayer::getPropertyValueLabel(const PropertyName &name, Chris@1234: int value) const Chris@0: { Chris@87: if (name == "Colour") { Chris@1362: return ColourMapper::getColourMapLabel(value); Chris@0: } Chris@87: if (name == "Colour Scale") { Chris@1234: switch (value) { Chris@1234: default: Chris@1234: case 0: return tr("Linear"); Chris@1234: case 1: return tr("Meter"); Chris@1234: case 2: return tr("dBV^2"); Chris@1234: case 3: return tr("dBV"); Chris@1234: case 4: return tr("Phase"); Chris@1234: } Chris@0: } Chris@862: if (name == "Normalization") { Chris@1209: switch(value) { Chris@1209: default: Chris@1209: case 0: return tr("None"); Chris@1209: case 1: return tr("Col"); Chris@1209: case 2: return tr("View"); Chris@1209: case 3: return tr("Hybrid"); Chris@1209: } Chris@1209: // return ""; // icon only Chris@862: } Chris@87: if (name == "Window Size") { Chris@1234: return QString("%1").arg(32 << value); Chris@0: } Chris@97: if (name == "Window Increment") { Chris@1234: switch (value) { Chris@1234: default: Chris@1234: case 0: return tr("None"); Chris@1234: case 1: return tr("25 %"); Chris@1234: case 2: return tr("50 %"); Chris@1234: case 3: return tr("75 %"); Chris@1234: case 4: return tr("87.5 %"); Chris@1234: case 5: return tr("93.75 %"); Chris@1234: } Chris@0: } Chris@1379: if (name == "Oversampling") { Chris@1379: switch (value) { Chris@1379: default: Chris@1379: case 0: return tr("1x"); Chris@1379: case 1: return tr("2x"); Chris@1379: case 2: return tr("4x"); Chris@1379: case 3: return tr("8x"); Chris@1379: } Chris@1379: } Chris@87: if (name == "Min Frequency") { Chris@1234: switch (value) { Chris@1234: default: Chris@1234: case 0: return tr("No min"); Chris@1234: case 1: return tr("10 Hz"); Chris@1234: case 2: return tr("20 Hz"); Chris@1234: case 3: return tr("40 Hz"); Chris@1234: case 4: return tr("100 Hz"); Chris@1234: case 5: return tr("250 Hz"); Chris@1234: case 6: return tr("500 Hz"); Chris@1234: case 7: return tr("1 KHz"); Chris@1234: case 8: return tr("4 KHz"); Chris@1234: case 9: return tr("10 KHz"); Chris@1234: } Chris@37: } Chris@87: if (name == "Max Frequency") { Chris@1234: switch (value) { Chris@1234: default: Chris@1234: case 0: return tr("500 Hz"); Chris@1234: case 1: return tr("1 KHz"); Chris@1234: case 2: return tr("1.5 KHz"); Chris@1234: case 3: return tr("2 KHz"); Chris@1234: case 4: return tr("4 KHz"); Chris@1234: case 5: return tr("6 KHz"); Chris@1234: case 6: return tr("8 KHz"); Chris@1234: case 7: return tr("12 KHz"); Chris@1234: case 8: return tr("16 KHz"); Chris@1234: case 9: return tr("No max"); Chris@1234: } Chris@0: } Chris@87: if (name == "Frequency Scale") { Chris@1234: switch (value) { Chris@1234: default: Chris@1234: case 0: return tr("Linear"); Chris@1234: case 1: return tr("Log"); Chris@1234: } Chris@0: } Chris@87: if (name == "Bin Display") { Chris@1234: switch (value) { Chris@1234: default: Chris@1234: case 0: return tr("All Bins"); Chris@1234: case 1: return tr("Peak Bins"); Chris@1234: case 2: return tr("Frequencies"); Chris@1234: } Chris@35: } Chris@0: return tr(""); Chris@0: } Chris@0: Chris@862: QString Chris@862: SpectrogramLayer::getPropertyValueIconName(const PropertyName &name, Chris@862: int value) const Chris@862: { Chris@862: if (name == "Normalization") { Chris@862: switch(value) { Chris@862: default: Chris@862: case 0: return "normalise-none"; Chris@862: case 1: return "normalise-columns"; Chris@862: case 2: return "normalise"; Chris@862: case 3: return "normalise-hybrid"; Chris@862: } Chris@862: } Chris@862: return ""; Chris@862: } Chris@862: Chris@167: RangeMapper * Chris@167: SpectrogramLayer::getNewPropertyRangeMapper(const PropertyName &name) const Chris@167: { Chris@167: if (name == "Gain") { Chris@167: return new LinearRangeMapper(-50, 50, -25, 25, tr("dB")); Chris@167: } Chris@167: if (name == "Threshold") { Chris@1147: return new LinearRangeMapper(-81, -1, -81, -1, tr("dB"), false, Chris@1147: { { -81, Strings::minus_infinity } }); Chris@167: } Chris@1408: return nullptr; Chris@167: } Chris@167: Chris@0: void Chris@0: SpectrogramLayer::setProperty(const PropertyName &name, int value) Chris@0: { Chris@87: if (name == "Gain") { Chris@1234: setGain(float(pow(10, float(value)/20.0))); Chris@87: } else if (name == "Threshold") { Chris@1234: if (value == -81) setThreshold(0.0); Chris@1234: else setThreshold(float(AudioLevel::dB_to_multiplier(value))); Chris@87: } else if (name == "Colour Rotation") { Chris@1234: setColourRotation(value); Chris@87: } else if (name == "Colour") { Chris@197: setColourMap(value); Chris@87: } else if (name == "Window Size") { Chris@1234: setWindowSize(32 << value); Chris@97: } else if (name == "Window Increment") { Chris@97: setWindowHopLevel(value); Chris@1379: } else if (name == "Oversampling") { Chris@1379: setOversampling(1 << value); Chris@87: } else if (name == "Min Frequency") { Chris@1234: switch (value) { Chris@1234: default: Chris@1234: case 0: setMinFrequency(0); break; Chris@1234: case 1: setMinFrequency(10); break; Chris@1234: case 2: setMinFrequency(20); break; Chris@1234: case 3: setMinFrequency(40); break; Chris@1234: case 4: setMinFrequency(100); break; Chris@1234: case 5: setMinFrequency(250); break; Chris@1234: case 6: setMinFrequency(500); break; Chris@1234: case 7: setMinFrequency(1000); break; Chris@1234: case 8: setMinFrequency(4000); break; Chris@1234: case 9: setMinFrequency(10000); break; Chris@1234: } Chris@133: int vs = getCurrentVerticalZoomStep(); Chris@133: if (vs != m_lastEmittedZoomStep) { Chris@133: emit verticalZoomChanged(); Chris@133: m_lastEmittedZoomStep = vs; Chris@133: } Chris@87: } else if (name == "Max Frequency") { Chris@1234: switch (value) { Chris@1234: case 0: setMaxFrequency(500); break; Chris@1234: case 1: setMaxFrequency(1000); break; Chris@1234: case 2: setMaxFrequency(1500); break; Chris@1234: case 3: setMaxFrequency(2000); break; Chris@1234: case 4: setMaxFrequency(4000); break; Chris@1234: case 5: setMaxFrequency(6000); break; Chris@1234: case 6: setMaxFrequency(8000); break; Chris@1234: case 7: setMaxFrequency(12000); break; Chris@1234: case 8: setMaxFrequency(16000); break; Chris@1234: default: Chris@1234: case 9: setMaxFrequency(0); break; Chris@1234: } Chris@133: int vs = getCurrentVerticalZoomStep(); Chris@133: if (vs != m_lastEmittedZoomStep) { Chris@133: emit verticalZoomChanged(); Chris@133: m_lastEmittedZoomStep = vs; Chris@133: } Chris@87: } else if (name == "Colour Scale") { Chris@1137: setColourScaleMultiple(1.0); Chris@1234: switch (value) { Chris@1234: default: Chris@1234: case 0: setColourScale(ColourScaleType::Linear); break; Chris@1234: case 1: setColourScale(ColourScaleType::Meter); break; Chris@1234: case 2: Chris@1137: setColourScale(ColourScaleType::Log); Chris@1137: setColourScaleMultiple(2.0); Chris@1137: break; Chris@1234: case 3: setColourScale(ColourScaleType::Log); break; Chris@1234: case 4: setColourScale(ColourScaleType::Phase); break; Chris@1234: } Chris@87: } else if (name == "Frequency Scale") { Chris@1234: switch (value) { Chris@1234: default: Chris@1234: case 0: setBinScale(BinScale::Linear); break; Chris@1234: case 1: setBinScale(BinScale::Log); break; Chris@1234: } Chris@87: } else if (name == "Bin Display") { Chris@1234: switch (value) { Chris@1234: default: Chris@1234: case 0: setBinDisplay(BinDisplay::AllBins); break; Chris@1234: case 1: setBinDisplay(BinDisplay::PeakBins); break; Chris@1234: case 2: setBinDisplay(BinDisplay::PeakFrequencies); break; Chris@1234: } Chris@862: } else if (name == "Normalization") { Chris@1104: auto n = convertToColumnNorm(value); Chris@1104: setNormalization(n.first); Chris@1104: setNormalizeVisibleArea(n.second); Chris@0: } Chris@0: } Chris@0: Chris@0: void Chris@1106: SpectrogramLayer::invalidateRenderers() Chris@95: { Chris@1044: #ifdef DEBUG_SPECTROGRAM Chris@1106: cerr << "SpectrogramLayer::invalidateRenderers called" << endl; Chris@1044: #endif Chris@1106: Chris@1089: for (ViewRendererMap::iterator i = m_renderers.begin(); Chris@1089: i != m_renderers.end(); ++i) { Chris@1089: delete i->second; Chris@1089: } Chris@1089: m_renderers.clear(); Chris@95: } Chris@95: Chris@95: void Chris@122: SpectrogramLayer::preferenceChanged(PropertyContainer::PropertyName name) Chris@122: { Chris@587: SVDEBUG << "SpectrogramLayer::preferenceChanged(" << name << ")" << endl; Chris@122: Chris@122: if (name == "Window Type") { Chris@122: setWindowType(Preferences::getInstance()->getWindowType()); Chris@122: return; Chris@122: } Chris@490: if (name == "Spectrogram Y Smoothing") { Chris@1106: invalidateRenderers(); Chris@490: invalidateMagnitudes(); Chris@490: emit layerParametersChanged(); Chris@490: } Chris@490: if (name == "Spectrogram X Smoothing") { Chris@1106: invalidateRenderers(); Chris@122: invalidateMagnitudes(); Chris@122: emit layerParametersChanged(); Chris@122: } Chris@122: if (name == "Tuning Frequency") { Chris@122: emit layerParametersChanged(); Chris@122: } Chris@122: } Chris@122: Chris@122: void Chris@0: SpectrogramLayer::setChannel(int ch) Chris@0: { Chris@0: if (m_channel == ch) return; Chris@0: Chris@1106: invalidateRenderers(); Chris@0: m_channel = ch; Chris@1211: recreateFFTModel(); Chris@9: Chris@0: emit layerParametersChanged(); Chris@0: } Chris@0: Chris@0: int Chris@0: SpectrogramLayer::getChannel() const Chris@0: { Chris@0: return m_channel; Chris@0: } Chris@0: Chris@1086: int Chris@1379: SpectrogramLayer::getFFTSize() const Chris@1086: { Chris@1379: return m_windowSize * m_oversampling; Chris@1379: } Chris@1379: Chris@1379: void Chris@1379: SpectrogramLayer::setWindowSize(int ws) Chris@1379: { Chris@1379: if (m_windowSize == ws) return; Chris@1379: invalidateRenderers(); Chris@1379: m_windowSize = ws; Chris@1379: recreateFFTModel(); Chris@1379: emit layerParametersChanged(); Chris@1379: } Chris@1379: Chris@1379: int Chris@1379: SpectrogramLayer::getWindowSize() const Chris@1379: { Chris@1379: return m_windowSize; Chris@1379: } Chris@1379: Chris@1379: void Chris@1379: SpectrogramLayer::setWindowHopLevel(int v) Chris@1379: { Chris@1379: if (m_windowHopLevel == v) return; Chris@1379: invalidateRenderers(); Chris@1379: m_windowHopLevel = v; Chris@1379: recreateFFTModel(); Chris@1379: emit layerParametersChanged(); Chris@1379: } Chris@1379: Chris@1379: int Chris@1379: SpectrogramLayer::getWindowHopLevel() const Chris@1379: { Chris@1379: return m_windowHopLevel; Chris@1379: } Chris@1379: Chris@1379: void Chris@1379: SpectrogramLayer::setOversampling(int oversampling) Chris@1379: { Chris@1379: if (m_oversampling == oversampling) return; Chris@1379: invalidateRenderers(); Chris@1379: m_oversampling = oversampling; Chris@1379: recreateFFTModel(); Chris@1379: emit layerParametersChanged(); Chris@1379: } Chris@1379: Chris@1379: int Chris@1379: SpectrogramLayer::getOversampling() const Chris@1379: { Chris@1379: return m_oversampling; Chris@0: } Chris@0: Chris@0: void Chris@0: SpectrogramLayer::setWindowType(WindowType w) Chris@0: { Chris@0: if (m_windowType == w) return; Chris@0: Chris@1106: invalidateRenderers(); Chris@0: Chris@0: m_windowType = w; Chris@110: Chris@1211: recreateFFTModel(); Chris@9: Chris@9: emit layerParametersChanged(); Chris@0: } Chris@0: Chris@0: WindowType Chris@0: SpectrogramLayer::getWindowType() const Chris@0: { Chris@0: return m_windowType; Chris@0: } Chris@0: Chris@0: void Chris@0: SpectrogramLayer::setGain(float gain) Chris@0: { Chris@587: // SVDEBUG << "SpectrogramLayer::setGain(" << gain << ") (my gain is now " Chris@1234: // << m_gain << ")" << endl; Chris@55: Chris@40: if (m_gain == gain) return; Chris@0: Chris@1106: invalidateRenderers(); Chris@0: Chris@0: m_gain = gain; Chris@0: Chris@9: emit layerParametersChanged(); Chris@0: } Chris@0: Chris@0: float Chris@0: SpectrogramLayer::getGain() const Chris@0: { Chris@0: return m_gain; Chris@0: } Chris@0: Chris@0: void Chris@37: SpectrogramLayer::setThreshold(float threshold) Chris@37: { Chris@40: if (m_threshold == threshold) return; Chris@37: Chris@1106: invalidateRenderers(); Chris@37: Chris@37: m_threshold = threshold; Chris@37: Chris@37: emit layerParametersChanged(); Chris@37: } Chris@37: Chris@37: float Chris@37: SpectrogramLayer::getThreshold() const Chris@37: { Chris@37: return m_threshold; Chris@37: } Chris@37: Chris@37: void Chris@805: SpectrogramLayer::setMinFrequency(int mf) Chris@37: { Chris@37: if (m_minFrequency == mf) return; Chris@37: Chris@1504: if (m_verticallyFixed) { Chris@1504: throw std::logic_error("setMinFrequency called with value differing from the default, on SpectrogramLayer with verticallyFixed true"); Chris@1504: } Chris@1504: Chris@587: // SVDEBUG << "SpectrogramLayer::setMinFrequency: " << mf << endl; Chris@187: Chris@1106: invalidateRenderers(); Chris@119: invalidateMagnitudes(); Chris@37: Chris@37: m_minFrequency = mf; Chris@37: Chris@37: emit layerParametersChanged(); Chris@37: } Chris@37: Chris@805: int Chris@37: SpectrogramLayer::getMinFrequency() const Chris@37: { Chris@37: return m_minFrequency; Chris@37: } Chris@37: Chris@37: void Chris@805: SpectrogramLayer::setMaxFrequency(int mf) Chris@0: { Chris@0: if (m_maxFrequency == mf) return; Chris@0: Chris@1504: if (m_verticallyFixed) { Chris@1504: throw std::logic_error("setMaxFrequency called with value differing from the default, on SpectrogramLayer with verticallyFixed true"); Chris@1504: } Chris@1504: Chris@587: // SVDEBUG << "SpectrogramLayer::setMaxFrequency: " << mf << endl; Chris@187: Chris@1106: invalidateRenderers(); Chris@119: invalidateMagnitudes(); Chris@0: Chris@0: m_maxFrequency = mf; Chris@0: Chris@9: emit layerParametersChanged(); Chris@0: } Chris@0: Chris@805: int Chris@0: SpectrogramLayer::getMaxFrequency() const Chris@0: { Chris@0: return m_maxFrequency; Chris@0: } Chris@0: Chris@0: void Chris@9: SpectrogramLayer::setColourRotation(int r) Chris@9: { Chris@9: if (r < 0) r = 0; Chris@9: if (r > 256) r = 256; Chris@9: int distance = r - m_colourRotation; Chris@9: Chris@9: if (distance != 0) { Chris@1234: m_colourRotation = r; Chris@9: } Chris@1141: Chris@1141: // Initially the idea with colour rotation was that we would just Chris@1141: // rotate the palette of an already-generated cache. That's not Chris@1141: // really practical now that cacheing is handled in a separate Chris@1141: // class in which the main cache no longer has a palette. Chris@1141: invalidateRenderers(); Chris@1112: Chris@9: emit layerParametersChanged(); Chris@9: } Chris@9: Chris@9: void Chris@1105: SpectrogramLayer::setColourScale(ColourScaleType colourScale) Chris@0: { Chris@0: if (m_colourScale == colourScale) return; Chris@0: Chris@1106: invalidateRenderers(); Chris@0: Chris@0: m_colourScale = colourScale; Chris@0: Chris@9: emit layerParametersChanged(); Chris@0: } Chris@0: Chris@1105: ColourScaleType Chris@0: SpectrogramLayer::getColourScale() const Chris@0: { Chris@0: return m_colourScale; Chris@0: } Chris@0: Chris@0: void Chris@1137: SpectrogramLayer::setColourScaleMultiple(double multiple) Chris@1137: { Chris@1137: if (m_colourScaleMultiple == multiple) return; Chris@1137: Chris@1137: invalidateRenderers(); Chris@1137: Chris@1137: m_colourScaleMultiple = multiple; Chris@1137: Chris@1137: emit layerParametersChanged(); Chris@1137: } Chris@1137: Chris@1137: double Chris@1137: SpectrogramLayer::getColourScaleMultiple() const Chris@1137: { Chris@1137: return m_colourScaleMultiple; Chris@1137: } Chris@1137: Chris@1137: void Chris@197: SpectrogramLayer::setColourMap(int map) Chris@0: { Chris@197: if (m_colourMap == map) return; Chris@0: Chris@1106: invalidateRenderers(); Chris@0: Chris@197: m_colourMap = map; Chris@9: Chris@0: emit layerParametersChanged(); Chris@0: } Chris@0: Chris@196: int Chris@197: SpectrogramLayer::getColourMap() const Chris@0: { Chris@197: return m_colourMap; Chris@0: } Chris@0: Chris@0: void Chris@1103: SpectrogramLayer::setBinScale(BinScale binScale) Chris@0: { Chris@1093: if (m_binScale == binScale) return; Chris@0: Chris@1106: invalidateRenderers(); Chris@1093: m_binScale = binScale; Chris@9: Chris@9: emit layerParametersChanged(); Chris@0: } Chris@0: Chris@1103: BinScale Chris@1093: SpectrogramLayer::getBinScale() const Chris@0: { Chris@1093: return m_binScale; Chris@0: } Chris@0: Chris@0: void Chris@1103: SpectrogramLayer::setBinDisplay(BinDisplay binDisplay) Chris@35: { Chris@37: if (m_binDisplay == binDisplay) return; Chris@35: Chris@1106: invalidateRenderers(); Chris@37: m_binDisplay = binDisplay; Chris@35: Chris@35: emit layerParametersChanged(); Chris@35: } Chris@35: Chris@1103: BinDisplay Chris@37: SpectrogramLayer::getBinDisplay() const Chris@35: { Chris@37: return m_binDisplay; Chris@35: } Chris@35: Chris@35: void Chris@1104: SpectrogramLayer::setNormalization(ColumnNormalization n) Chris@36: { Chris@862: if (m_normalization == n) return; Chris@36: Chris@1106: invalidateRenderers(); Chris@119: invalidateMagnitudes(); Chris@862: m_normalization = n; Chris@36: Chris@36: emit layerParametersChanged(); Chris@36: } Chris@36: Chris@1104: ColumnNormalization Chris@862: SpectrogramLayer::getNormalization() const Chris@36: { Chris@862: return m_normalization; Chris@36: } Chris@36: Chris@36: void Chris@1104: SpectrogramLayer::setNormalizeVisibleArea(bool n) Chris@1104: { Chris@1104: if (m_normalizeVisibleArea == n) return; Chris@1104: Chris@1106: invalidateRenderers(); Chris@1104: invalidateMagnitudes(); Chris@1104: m_normalizeVisibleArea = n; Chris@1104: Chris@1104: emit layerParametersChanged(); Chris@1104: } Chris@1104: Chris@1104: bool Chris@1104: SpectrogramLayer::getNormalizeVisibleArea() const Chris@1104: { Chris@1104: return m_normalizeVisibleArea; Chris@1104: } Chris@1104: Chris@1104: void Chris@918: SpectrogramLayer::setLayerDormant(const LayerGeometryProvider *v, bool dormant) Chris@29: { Chris@33: if (dormant) { Chris@33: Chris@331: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@985: cerr << "SpectrogramLayer::setLayerDormant(" << dormant << ")" Chris@585: << endl; Chris@331: #endif Chris@331: Chris@131: if (isLayerDormant(v)) { Chris@131: return; Chris@131: } Chris@131: Chris@131: Layer::setLayerDormant(v, true); Chris@33: Chris@1234: invalidateRenderers(); Chris@1234: Chris@33: } else { Chris@33: Chris@131: Layer::setLayerDormant(v, false); Chris@33: } Chris@29: } Chris@29: Chris@1232: bool Chris@1232: SpectrogramLayer::isLayerScrollable(const LayerGeometryProvider *) const Chris@1232: { Chris@1419: // we do our own cacheing, and don't want to be responsible for Chris@1419: // guaranteeing to get an invisible seam if someone else scrolls Chris@1419: // us and we just fill in Chris@1232: return false; Chris@1232: } Chris@1232: Chris@29: void Chris@1481: SpectrogramLayer::cacheInvalid(ModelId) Chris@0: { Chris@391: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@985: cerr << "SpectrogramLayer::cacheInvalid()" << endl; Chris@391: #endif Chris@391: Chris@1106: invalidateRenderers(); Chris@119: invalidateMagnitudes(); Chris@0: } Chris@0: Chris@0: void Chris@1037: SpectrogramLayer::cacheInvalid( Chris@1481: ModelId, Chris@1037: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@1037: sv_frame_t from, sv_frame_t to Chris@1037: #else Chris@1037: sv_frame_t , sv_frame_t Chris@1037: #endif Chris@1037: ) Chris@0: { Chris@391: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@985: cerr << "SpectrogramLayer::cacheInvalid(" << from << ", " << to << ")" << endl; Chris@391: #endif Chris@391: Chris@1030: // We used to call invalidateMagnitudes(from, to) to invalidate Chris@1030: // only those caches whose views contained some of the (from, to) Chris@1030: // range. That's the right thing to do; it has been lost in Chris@1030: // pulling out the image cache code, but it might not matter very Chris@1030: // much, since the underlying models for spectrogram layers don't Chris@1030: // change very often. Let's see. Chris@1106: invalidateRenderers(); Chris@391: invalidateMagnitudes(); Chris@0: } Chris@0: Chris@224: bool Chris@224: SpectrogramLayer::hasLightBackground() const Chris@224: { Chris@1362: return ColourMapper(m_colourMap, m_colourInverted, 1.f, 255.f) Chris@1362: .hasLightBackground(); Chris@224: } Chris@224: Chris@905: double Chris@40: SpectrogramLayer::getEffectiveMinFrequency() const Chris@40: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return 0.0; Chris@1473: Chris@1473: sv_samplerate_t sr = model->getSampleRate(); Chris@1087: double minf = double(sr) / getFFTSize(); Chris@40: Chris@40: if (m_minFrequency > 0.0) { Chris@1234: int minbin = int((double(m_minFrequency) * getFFTSize()) / sr + 0.01); Chris@1234: if (minbin < 1) minbin = 1; Chris@1234: minf = minbin * sr / getFFTSize(); Chris@40: } Chris@40: Chris@40: return minf; Chris@40: } Chris@40: Chris@905: double Chris@40: SpectrogramLayer::getEffectiveMaxFrequency() const Chris@40: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return 0.0; Chris@1473: Chris@1473: sv_samplerate_t sr = model->getSampleRate(); Chris@905: double maxf = double(sr) / 2; Chris@40: Chris@40: if (m_maxFrequency > 0.0) { Chris@1234: int maxbin = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1); Chris@1234: if (maxbin > getFFTSize() / 2) maxbin = getFFTSize() / 2; Chris@1234: maxf = maxbin * sr / getFFTSize(); Chris@40: } Chris@40: Chris@40: return maxf; Chris@40: } Chris@40: Chris@0: bool Chris@918: SpectrogramLayer::getYBinRange(LayerGeometryProvider *v, int y, double &q0, double &q1) const Chris@0: { Chris@382: Profiler profiler("SpectrogramLayer::getYBinRange"); Chris@918: int h = v->getPaintHeight(); Chris@0: if (y < 0 || y >= h) return false; Chris@1117: q0 = getBinForY(v, y); Chris@1117: q1 = getBinForY(v, y-1); Chris@1117: return true; Chris@1117: } Chris@1117: Chris@1117: double Chris@1117: SpectrogramLayer::getYForBin(const LayerGeometryProvider *v, double bin) const Chris@1117: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return 0.0; Chris@1473: Chris@1117: double minf = getEffectiveMinFrequency(); Chris@1117: double maxf = getEffectiveMaxFrequency(); Chris@1117: bool logarithmic = (m_binScale == BinScale::Log); Chris@1473: sv_samplerate_t sr = model->getSampleRate(); Chris@1117: Chris@1117: double freq = (bin * sr) / getFFTSize(); Chris@1117: Chris@1117: double y = v->getYForFrequency(freq, minf, maxf, logarithmic); Chris@1117: Chris@1117: return y; Chris@1117: } Chris@1117: Chris@1117: double Chris@1117: SpectrogramLayer::getBinForY(const LayerGeometryProvider *v, double y) const Chris@1117: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return 0.0; Chris@1473: Chris@1473: sv_samplerate_t sr = model->getSampleRate(); Chris@905: double minf = getEffectiveMinFrequency(); Chris@905: double maxf = getEffectiveMaxFrequency(); Chris@0: Chris@1103: bool logarithmic = (m_binScale == BinScale::Log); Chris@38: Chris@1117: double freq = v->getFrequencyForY(y, minf, maxf, logarithmic); Chris@1117: Chris@1117: // Now map on to ("proportion of") actual bins Chris@1117: double bin = (freq * getFFTSize()) / sr; Chris@1117: Chris@1117: return bin; Chris@1085: } Chris@1085: Chris@0: bool Chris@918: SpectrogramLayer::getXBinRange(LayerGeometryProvider *v, int x, double &s0, double &s1) const Chris@0: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return false; Chris@1473: Chris@1473: sv_frame_t modelStart = model->getStartFrame(); Chris@1473: sv_frame_t modelEnd = model->getEndFrame(); Chris@0: Chris@0: // Each pixel column covers an exact range of sample frames: Chris@907: sv_frame_t f0 = v->getFrameForX(x) - modelStart; Chris@907: sv_frame_t f1 = v->getFrameForX(x + 1) - modelStart - 1; Chris@20: Chris@41: if (f1 < int(modelStart) || f0 > int(modelEnd)) { Chris@1234: return false; Chris@41: } Chris@20: Chris@0: // And that range may be drawn from a possibly non-integral Chris@0: // range of spectrogram windows: Chris@0: Chris@805: int windowIncrement = getWindowIncrement(); Chris@905: s0 = double(f0) / windowIncrement; Chris@905: s1 = double(f1) / windowIncrement; Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: bool Chris@918: SpectrogramLayer::getXBinSourceRange(LayerGeometryProvider *v, int x, RealTime &min, RealTime &max) const Chris@0: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return false; Chris@1473: Chris@905: double s0 = 0, s1 = 0; Chris@44: if (!getXBinRange(v, x, s0, s1)) return false; Chris@0: Chris@0: int s0i = int(s0 + 0.001); Chris@0: int s1i = int(s1); Chris@0: Chris@0: int windowIncrement = getWindowIncrement(); Chris@0: int w0 = s0i * windowIncrement - (m_windowSize - windowIncrement)/2; Chris@0: int w1 = s1i * windowIncrement + windowIncrement + Chris@1234: (m_windowSize - windowIncrement)/2 - 1; Chris@0: Chris@1473: min = RealTime::frame2RealTime(w0, model->getSampleRate()); Chris@1473: max = RealTime::frame2RealTime(w1, model->getSampleRate()); Chris@0: return true; Chris@0: } Chris@0: Chris@0: bool Chris@918: SpectrogramLayer::getYBinSourceRange(LayerGeometryProvider *v, int y, double &freqMin, double &freqMax) Chris@0: const Chris@0: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return false; Chris@1473: Chris@905: double q0 = 0, q1 = 0; Chris@44: if (!getYBinRange(v, y, q0, q1)) return false; Chris@0: Chris@0: int q0i = int(q0 + 0.001); Chris@0: int q1i = int(q1); Chris@0: Chris@1473: sv_samplerate_t sr = model->getSampleRate(); Chris@0: Chris@0: for (int q = q0i; q <= q1i; ++q) { Chris@1234: if (q == q0i) freqMin = (sr * q) / getFFTSize(); Chris@1234: if (q == q1i) freqMax = (sr * (q+1)) / getFFTSize(); Chris@0: } Chris@0: return true; Chris@0: } Chris@35: Chris@35: bool Chris@918: SpectrogramLayer::getAdjustedYBinSourceRange(LayerGeometryProvider *v, int x, int y, Chris@1234: double &freqMin, double &freqMax, Chris@1234: double &adjFreqMin, double &adjFreqMax) Chris@35: const Chris@35: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model || !model->isOK() || !model->isReady()) { Chris@1234: return false; Chris@277: } Chris@277: Chris@1473: auto fft = ModelById::getAs(m_fftModel); Chris@114: if (!fft) return false; Chris@110: Chris@905: double s0 = 0, s1 = 0; Chris@44: if (!getXBinRange(v, x, s0, s1)) return false; Chris@35: Chris@905: double q0 = 0, q1 = 0; Chris@44: if (!getYBinRange(v, y, q0, q1)) return false; Chris@35: Chris@35: int s0i = int(s0 + 0.001); Chris@35: int s1i = int(s1); Chris@35: Chris@35: int q0i = int(q0 + 0.001); Chris@35: int q1i = int(q1); Chris@35: Chris@1473: sv_samplerate_t sr = model->getSampleRate(); Chris@35: Chris@35: bool haveAdj = false; Chris@35: Chris@1103: bool peaksOnly = (m_binDisplay == BinDisplay::PeakBins || Chris@1234: m_binDisplay == BinDisplay::PeakFrequencies); Chris@37: Chris@35: for (int q = q0i; q <= q1i; ++q) { Chris@35: Chris@1234: for (int s = s0i; s <= s1i; ++s) { Chris@1234: Chris@1464: double binfreq = (double(sr) * q) / getFFTSize(); Chris@1234: if (q == q0i) freqMin = binfreq; Chris@1234: if (q == q1i) freqMax = binfreq; Chris@1234: Chris@1234: if (peaksOnly && !fft->isLocalPeak(s, q)) continue; Chris@1234: Chris@1234: if (!fft->isOverThreshold Chris@1087: (s, q, float(m_threshold * double(getFFTSize())/2.0))) { Chris@1086: continue; Chris@1086: } Chris@907: Chris@907: double freq = binfreq; Chris@1234: Chris@1234: if (s < int(fft->getWidth()) - 1) { Chris@38: Chris@277: fft->estimateStableFrequency(s, q, freq); Chris@1234: Chris@1234: if (!haveAdj || freq < adjFreqMin) adjFreqMin = freq; Chris@1234: if (!haveAdj || freq > adjFreqMax) adjFreqMax = freq; Chris@1234: Chris@1234: haveAdj = true; Chris@1234: } Chris@1234: } Chris@35: } Chris@35: Chris@35: if (!haveAdj) { Chris@1234: adjFreqMin = adjFreqMax = 0.0; Chris@35: } Chris@35: Chris@35: return haveAdj; Chris@35: } Chris@0: Chris@0: bool Chris@918: SpectrogramLayer::getXYBinSourceRange(LayerGeometryProvider *v, int x, int y, Chris@1234: double &min, double &max, Chris@1234: double &phaseMin, double &phaseMax) const Chris@0: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model || !model->isOK() || !model->isReady()) { Chris@1234: return false; Chris@277: } Chris@277: Chris@905: double q0 = 0, q1 = 0; Chris@44: if (!getYBinRange(v, y, q0, q1)) return false; Chris@0: Chris@905: double s0 = 0, s1 = 0; Chris@44: if (!getXBinRange(v, x, s0, s1)) return false; Chris@0: Chris@0: int q0i = int(q0 + 0.001); Chris@0: int q1i = int(q1); Chris@0: Chris@0: int s0i = int(s0 + 0.001); Chris@0: int s1i = int(s1); Chris@0: Chris@37: bool rv = false; Chris@37: Chris@1473: auto fft = ModelById::getAs(m_fftModel); Chris@0: Chris@114: if (fft) { Chris@114: Chris@114: int cw = fft->getWidth(); Chris@114: int ch = fft->getHeight(); Chris@0: Chris@110: min = 0.0; Chris@110: max = 0.0; Chris@110: phaseMin = 0.0; Chris@110: phaseMax = 0.0; Chris@110: bool have = false; Chris@0: Chris@110: for (int q = q0i; q <= q1i; ++q) { Chris@110: for (int s = s0i; s <= s1i; ++s) { Chris@110: if (s >= 0 && q >= 0 && s < cw && q < ch) { Chris@117: Chris@905: double value; Chris@38: Chris@114: value = fft->getPhaseAt(s, q); Chris@110: if (!have || value < phaseMin) { phaseMin = value; } Chris@110: if (!have || value > phaseMax) { phaseMax = value; } Chris@91: Chris@1087: value = fft->getMagnitudeAt(s, q) / (getFFTSize()/2.0); Chris@110: if (!have || value < min) { min = value; } Chris@110: if (!have || value > max) { max = value; } Chris@110: Chris@110: have = true; Chris@1234: } Chris@110: } Chris@110: } Chris@110: Chris@110: if (have) { Chris@110: rv = true; Chris@110: } Chris@0: } Chris@0: Chris@37: return rv; Chris@0: } Chris@1234: Chris@1211: void Chris@1211: SpectrogramLayer::recreateFFTModel() Chris@114: { Chris@1242: SVDEBUG << "SpectrogramLayer::recreateFFTModel called" << endl; Chris@1211: Chris@1473: { // scope, avoid hanging on to this pointer Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model || !model->isOK()) { Chris@1473: deleteDerivedModels(); Chris@1473: return; Chris@1473: } Chris@114: } Chris@1242: Chris@1473: deleteDerivedModels(); Chris@1473: Chris@1473: auto newFFTModel = std::make_shared(m_model, Chris@1473: m_channel, Chris@1473: m_windowType, Chris@1473: m_windowSize, Chris@1473: getWindowIncrement(), Chris@1473: getFFTSize()); Chris@1473: Chris@1473: if (!newFFTModel->isOK()) { Chris@1088: QMessageBox::critical Chris@1408: (nullptr, tr("FFT cache failed"), Chris@1088: tr("Failed to create the FFT model for this spectrogram.\n" Chris@1088: "There may be insufficient memory or disc space to continue.")); Chris@1211: return; Chris@114: } Chris@1212: Chris@1504: if (m_verticallyFixed) { Chris@1504: newFFTModel->setMaximumFrequency(getMaxFrequency()); Chris@1504: } Chris@1503: Chris@1481: m_fftModel = ModelById::add(newFFTModel); Chris@1242: Chris@1450: bool createWholeCache = false; Chris@1450: checkCacheSpace(&m_peakCacheDivisor, &createWholeCache); Chris@1450: Chris@1450: if (createWholeCache) { Chris@1473: Chris@1473: auto whole = std::make_shared(m_fftModel, 1); Chris@1481: m_wholeCache = ModelById::add(whole); Chris@1473: Chris@1501: auto peaks = std::make_shared(m_fftModel, Chris@1473: m_peakCacheDivisor); Chris@1481: m_peakCache = ModelById::add(peaks); Chris@1473: Chris@1212: } else { Chris@1473: Chris@1473: auto peaks = std::make_shared(m_fftModel, Chris@1473: m_peakCacheDivisor); Chris@1481: m_peakCache = ModelById::add(peaks); Chris@1212: } Chris@484: } Chris@484: Chris@1450: void Chris@1450: SpectrogramLayer::checkCacheSpace(int *suggestedPeakDivisor, Chris@1450: bool *createWholeCache) const Chris@1212: { Chris@1450: *suggestedPeakDivisor = 8; Chris@1450: *createWholeCache = false; Chris@1473: Chris@1473: auto fftModel = ModelById::getAs(m_fftModel); Chris@1473: if (!fftModel) return; Chris@1212: Chris@1212: size_t sz = Chris@1473: size_t(fftModel->getWidth()) * Chris@1473: size_t(fftModel->getHeight()) * Chris@1212: sizeof(float); Chris@1212: Chris@1212: try { Chris@1212: SVDEBUG << "Requesting advice from StorageAdviser on whether to create whole-model cache" << endl; Chris@1450: // The lower amount here is the amount required for the Chris@1450: // slightly higher-resolution version of the peak cache Chris@1450: // without a whole-model cache; the higher amount is that for Chris@1450: // the whole-model cache. The factors of 1024 are because Chris@1450: // StorageAdviser rather stupidly works in kilobytes Chris@1212: StorageAdviser::Recommendation recommendation = Chris@1212: StorageAdviser::recommend Chris@1212: (StorageAdviser::Criteria(StorageAdviser::SpeedCritical | Chris@1212: StorageAdviser::PrecisionCritical | Chris@1212: StorageAdviser::FrequentLookupLikely), Chris@1450: (sz / 8) / 1024, sz / 1024); Chris@1450: if (recommendation & StorageAdviser::UseDisc) { Chris@1212: SVDEBUG << "Seems inadvisable to create whole-model cache" << endl; Chris@1450: } else if (recommendation & StorageAdviser::ConserveSpace) { Chris@1450: SVDEBUG << "Seems inadvisable to create whole-model cache but acceptable to use the slightly higher-resolution peak cache" << endl; Chris@1450: *suggestedPeakDivisor = 4; Chris@1450: } else { Chris@1212: SVDEBUG << "Seems fine to create whole-model cache" << endl; Chris@1450: *createWholeCache = true; Chris@1212: } Chris@1212: } catch (const InsufficientDiscSpace &) { Chris@1212: SVDEBUG << "Seems like a terrible idea to create whole-model cache" << endl; Chris@1212: } Chris@1212: } Chris@1212: Chris@1473: ModelId Chris@193: SpectrogramLayer::getSliceableModel() const Chris@193: { Chris@1088: return m_fftModel; Chris@193: } Chris@193: Chris@114: void Chris@119: SpectrogramLayer::invalidateMagnitudes() Chris@119: { Chris@1044: #ifdef DEBUG_SPECTROGRAM Chris@1044: cerr << "SpectrogramLayer::invalidateMagnitudes called" << endl; Chris@1044: #endif Chris@119: m_viewMags.clear(); Chris@119: } Chris@1134: Chris@119: void Chris@389: SpectrogramLayer::setSynchronousPainting(bool synchronous) Chris@389: { Chris@389: m_synchronous = synchronous; Chris@389: } Chris@389: Chris@1089: Colour3DPlotRenderer * Chris@1089: SpectrogramLayer::getRenderer(LayerGeometryProvider *v) const Chris@1089: { Chris@1136: int viewId = v->getId(); Chris@1136: Chris@1136: if (m_renderers.find(viewId) == m_renderers.end()) { Chris@1089: Chris@1089: Colour3DPlotRenderer::Sources sources; Chris@1089: sources.verticalBinLayer = this; Chris@1473: sources.fft = m_fftModel; Chris@1090: sources.source = sources.fft; Chris@1473: if (!m_peakCache.isNone()) sources.peakCaches.push_back(m_peakCache); Chris@1473: if (!m_wholeCache.isNone()) sources.peakCaches.push_back(m_wholeCache); Chris@1089: Chris@1092: ColourScale::Parameters cparams; Chris@1092: cparams.colourMap = m_colourMap; Chris@1137: cparams.scaleType = m_colourScale; Chris@1137: cparams.multiple = m_colourScaleMultiple; Chris@1129: Chris@1129: if (m_colourScale != ColourScaleType::Phase) { Chris@1129: cparams.gain = m_gain; Chris@1129: cparams.threshold = m_threshold; Chris@1129: } Chris@1093: Chris@1234: double minValue = 0.0f; Chris@1234: double maxValue = 1.0f; Chris@1136: Chris@1136: if (m_normalizeVisibleArea && m_viewMags[viewId].isSet()) { Chris@1136: minValue = m_viewMags[viewId].getMin(); Chris@1136: maxValue = m_viewMags[viewId].getMax(); Chris@1136: } else if (m_colourScale == ColourScaleType::Linear && Chris@1136: m_normalization == ColumnNormalization::None) { Chris@1136: maxValue = 0.1f; Chris@1093: } Chris@1129: Chris@1136: if (maxValue <= minValue) { Chris@1136: maxValue = minValue + 0.1f; Chris@1136: } Chris@1136: if (maxValue <= m_threshold) { Chris@1136: maxValue = m_threshold + 0.1f; Chris@1136: } Chris@1136: Chris@1136: cparams.minValue = minValue; Chris@1136: cparams.maxValue = maxValue; Chris@1136: Chris@1234: m_lastRenderedMags[viewId] = MagnitudeRange(float(minValue), Chris@1234: float(maxValue)); Chris@1136: Chris@1089: Colour3DPlotRenderer::Parameters params; Chris@1092: params.colourScale = ColourScale(cparams); Chris@1089: params.normalization = m_normalization; Chris@1093: params.binDisplay = m_binDisplay; Chris@1093: params.binScale = m_binScale; Chris@1141: params.alwaysOpaque = true; Chris@1093: params.invertVertical = false; Chris@1125: params.scaleFactor = 1.0; Chris@1112: params.colourRotation = m_colourRotation; Chris@1093: Chris@1145: if (m_colourScale != ColourScaleType::Phase && Chris@1145: m_normalization != ColumnNormalization::Hybrid) { Chris@1403: params.scaleFactor *= 2.f / float(getWindowSize()); Chris@1125: } Chris@1125: Chris@1093: Preferences::SpectrogramSmoothing smoothing = Chris@1093: Preferences::getInstance()->getSpectrogramSmoothing(); Chris@1093: params.interpolate = Chris@1399: (smoothing != Preferences::NoSpectrogramSmoothing); Chris@1089: Chris@1234: m_renderers[viewId] = new Colour3DPlotRenderer(sources, params); Chris@1239: Chris@1239: m_crosshairColour = Chris@1362: ColourMapper(m_colourMap, m_colourInverted, 1.f, 255.f) Chris@1362: .getContrastingColour(); Chris@1089: } Chris@1089: Chris@1234: return m_renderers[viewId]; Chris@1089: } Chris@1089: Chris@1089: void Chris@1106: SpectrogramLayer::paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const Chris@1106: { Chris@1121: Colour3DPlotRenderer *renderer = getRenderer(v); Chris@1121: Chris@1121: Colour3DPlotRenderer::RenderResult result; Chris@1122: MagnitudeRange magRange; Chris@1122: int viewId = v->getId(); Chris@1122: Chris@1136: bool continuingPaint = !renderer->geometryChanged(v); Chris@1136: Chris@1136: if (continuingPaint) { Chris@1122: magRange = m_viewMags[viewId]; Chris@1122: } Chris@1106: Chris@1106: if (m_synchronous) { Chris@1121: Chris@1121: result = renderer->render(v, paint, rect); Chris@1121: Chris@1121: } else { Chris@1121: Chris@1121: result = renderer->renderTimeConstrained(v, paint, rect); Chris@1121: Chris@1143: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@1122: cerr << "rect width from this paint: " << result.rendered.width() Chris@1122: << ", mag range in this paint: " << result.range.getMin() << " -> " Chris@1121: << result.range.getMax() << endl; Chris@1143: #endif Chris@1121: Chris@1121: QRect uncached = renderer->getLargestUncachedRect(v); Chris@1121: if (uncached.width() > 0) { Chris@1121: v->updatePaintRect(uncached); Chris@1121: } Chris@1106: } Chris@1106: Chris@1122: magRange.sample(result.range); Chris@1122: Chris@1122: if (magRange.isSet()) { Chris@1136: if (m_viewMags[viewId] != magRange) { Chris@1122: m_viewMags[viewId] = magRange; Chris@1143: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@1136: cerr << "mag range in this view has changed: " Chris@1136: << magRange.getMin() << " -> " << magRange.getMax() << endl; Chris@1143: #endif Chris@1122: } Chris@1122: } Chris@1136: Chris@1136: if (!continuingPaint && m_normalizeVisibleArea && Chris@1136: m_viewMags[viewId] != m_lastRenderedMags[viewId]) { Chris@1143: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@1136: cerr << "mag range has changed from last rendered range: re-rendering" Chris@1136: << endl; Chris@1143: #endif Chris@1136: delete m_renderers[viewId]; Chris@1136: m_renderers.erase(viewId); Chris@1136: v->updatePaintRect(v->getPaintRect()); Chris@1136: } Chris@1106: } Chris@1106: Chris@1106: void Chris@916: SpectrogramLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const Chris@0: { Chris@334: Profiler profiler("SpectrogramLayer::paint", false); Chris@334: Chris@0: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@1026: cerr << "SpectrogramLayer::paint() entering: m_model is " << m_model << ", zoom level is " << v->getZoomLevel() << endl; Chris@95: Chris@1026: cerr << "SpectrogramLayer::paint(): rect is " << rect.x() << "," << rect.y() << " " << rect.width() << "x" << rect.height() << endl; Chris@0: #endif Chris@95: Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model || !model->isOK() || !model->isReady()) { Chris@1234: return; Chris@29: } Chris@29: Chris@1106: paintWithRenderer(v, paint, rect); Chris@1140: Chris@1140: illuminateLocalFeatures(v, paint); Chris@480: } Chris@477: Chris@121: void Chris@918: SpectrogramLayer::illuminateLocalFeatures(LayerGeometryProvider *v, QPainter &paint) const Chris@121: { Chris@382: Profiler profiler("SpectrogramLayer::illuminateLocalFeatures"); Chris@382: Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: Chris@121: QPoint localPos; Chris@1473: if (!v->shouldIlluminateLocalFeatures(this, localPos) || !model) { Chris@121: return; Chris@121: } Chris@121: Chris@1143: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@1134: cerr << "SpectrogramLayer: illuminateLocalFeatures(" Chris@1134: << localPos.x() << "," << localPos.y() << ")" << endl; Chris@1143: #endif Chris@121: Chris@905: double s0, s1; Chris@905: double f0, f1; Chris@121: Chris@121: if (getXBinRange(v, localPos.x(), s0, s1) && Chris@121: getYBinSourceRange(v, localPos.y(), f0, f1)) { Chris@121: Chris@121: int s0i = int(s0 + 0.001); Chris@121: int s1i = int(s1); Chris@121: Chris@121: int x0 = v->getXForFrame(s0i * getWindowIncrement()); Chris@121: int x1 = v->getXForFrame((s1i + 1) * getWindowIncrement()); Chris@121: Chris@248: int y1 = int(getYForFrequency(v, f1)); Chris@248: int y0 = int(getYForFrequency(v, f0)); Chris@121: Chris@1143: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@1134: cerr << "SpectrogramLayer: illuminate " Chris@1134: << x0 << "," << y1 << " -> " << x1 << "," << y0 << endl; Chris@1143: #endif Chris@121: Chris@287: paint.setPen(v->getForeground()); Chris@133: Chris@133: //!!! should we be using paintCrosshairs for this? Chris@133: Chris@121: paint.drawRect(x0, y1, x1 - x0 + 1, y0 - y1 + 1); Chris@121: } Chris@121: } Chris@121: Chris@905: double Chris@918: SpectrogramLayer::getYForFrequency(const LayerGeometryProvider *v, double frequency) const Chris@42: { Chris@44: return v->getYForFrequency(frequency, Chris@1234: getEffectiveMinFrequency(), Chris@1234: getEffectiveMaxFrequency(), Chris@1234: m_binScale == BinScale::Log); Chris@42: } Chris@42: Chris@905: double Chris@918: SpectrogramLayer::getFrequencyForY(const LayerGeometryProvider *v, int y) const Chris@42: { Chris@44: return v->getFrequencyForY(y, Chris@1234: getEffectiveMinFrequency(), Chris@1234: getEffectiveMaxFrequency(), Chris@1234: m_binScale == BinScale::Log); Chris@42: } Chris@42: Chris@0: int Chris@1090: SpectrogramLayer::getCompletion(LayerGeometryProvider *) const Chris@0: { Chris@1473: auto fftModel = ModelById::getAs(m_fftModel); Chris@1473: if (!fftModel) return 100; Chris@1473: int completion = fftModel->getCompletion(); Chris@224: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@985: cerr << "SpectrogramLayer::getCompletion: completion = " << completion << endl; Chris@224: #endif Chris@0: return completion; Chris@0: } Chris@0: Chris@583: QString Chris@1090: SpectrogramLayer::getError(LayerGeometryProvider *) const Chris@583: { Chris@1473: auto fftModel = ModelById::getAs(m_fftModel); Chris@1473: if (!fftModel) return ""; Chris@1473: return fftModel->getError(); Chris@583: } Chris@583: Chris@28: bool Chris@905: SpectrogramLayer::getValueExtents(double &min, double &max, Chris@101: bool &logarithmic, QString &unit) const Chris@79: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return false; Chris@1473: Chris@1473: sv_samplerate_t sr = model->getSampleRate(); Chris@1087: min = double(sr) / getFFTSize(); Chris@905: max = double(sr) / 2; Chris@133: Chris@1103: logarithmic = (m_binScale == BinScale::Log); Chris@79: unit = "Hz"; Chris@79: return true; Chris@79: } Chris@79: Chris@79: bool Chris@905: SpectrogramLayer::getDisplayExtents(double &min, double &max) const Chris@101: { Chris@101: min = getEffectiveMinFrequency(); Chris@101: max = getEffectiveMaxFrequency(); Chris@253: Chris@587: // SVDEBUG << "SpectrogramLayer::getDisplayExtents: " << min << "->" << max << endl; Chris@101: return true; Chris@101: } Chris@101: Chris@101: bool Chris@905: SpectrogramLayer::setDisplayExtents(double min, double max) Chris@120: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return false; Chris@187: Chris@587: // SVDEBUG << "SpectrogramLayer::setDisplayExtents: " << min << "->" << max << endl; Chris@187: Chris@120: if (min < 0) min = 0; Chris@1473: if (max > model->getSampleRate()/2.0) max = model->getSampleRate()/2.0; Chris@120: Chris@907: int minf = int(lrint(min)); Chris@907: int maxf = int(lrint(max)); Chris@120: Chris@120: if (m_minFrequency == minf && m_maxFrequency == maxf) return true; Chris@120: Chris@1106: invalidateRenderers(); Chris@120: invalidateMagnitudes(); Chris@120: Chris@1504: if (m_verticallyFixed && Chris@1504: (m_minFrequency != minf || m_maxFrequency != maxf)) { Chris@1504: throw std::logic_error("setDisplayExtents called with values differing from the defaults, on SpectrogramLayer with verticallyFixed true"); Chris@1504: } Chris@1504: Chris@120: m_minFrequency = minf; Chris@120: m_maxFrequency = maxf; Chris@120: Chris@120: emit layerParametersChanged(); Chris@120: Chris@133: int vs = getCurrentVerticalZoomStep(); Chris@133: if (vs != m_lastEmittedZoomStep) { Chris@133: emit verticalZoomChanged(); Chris@133: m_lastEmittedZoomStep = vs; Chris@133: } Chris@133: Chris@120: return true; Chris@120: } Chris@120: Chris@120: bool Chris@918: SpectrogramLayer::getYScaleValue(const LayerGeometryProvider *v, int y, Chris@905: double &value, QString &unit) const Chris@261: { Chris@261: value = getFrequencyForY(v, y); Chris@261: unit = "Hz"; Chris@261: return true; Chris@261: } Chris@261: Chris@261: bool Chris@918: SpectrogramLayer::snapToFeatureFrame(LayerGeometryProvider *, Chris@907: sv_frame_t &frame, Chris@1234: int &resolution, Chris@1547: SnapType snap, int) const Chris@13: { Chris@13: resolution = getWindowIncrement(); Chris@907: sv_frame_t left = (frame / resolution) * resolution; Chris@907: sv_frame_t right = left + resolution; Chris@28: Chris@28: switch (snap) { Chris@28: case SnapLeft: frame = left; break; Chris@28: case SnapRight: frame = right; break; Chris@28: case SnapNeighbouring: Chris@1234: if (frame - left > right - frame) frame = right; Chris@1234: else frame = left; Chris@1234: break; Chris@28: } Chris@28: Chris@28: return true; Chris@28: } Chris@13: Chris@283: void Chris@1139: SpectrogramLayer::measureDoubleClick(LayerGeometryProvider *v, QMouseEvent *e) Chris@283: { Chris@1139: const Colour3DPlotRenderer *renderer = getRenderer(v); Chris@1139: if (!renderer) return; Chris@1139: Chris@1139: QRect rect = renderer->findSimilarRegionExtents(e->pos()); Chris@283: if (rect.isValid()) { Chris@283: MeasureRect mr; Chris@283: setMeasureRectFromPixrect(v, mr, rect); Chris@283: CommandHistory::getInstance()->addCommand Chris@283: (new AddMeasurementRectCommand(this, mr)); Chris@283: } Chris@283: } Chris@283: Chris@77: bool Chris@918: SpectrogramLayer::getCrosshairExtents(LayerGeometryProvider *v, QPainter &paint, Chris@77: QPoint cursorPos, Chris@1025: vector &extents) const Chris@77: { Chris@1473: // Qt 5.13 deprecates QFontMetrics::width(), but its suggested Chris@1473: // replacement (horizontalAdvance) was only added in Qt 5.11 Chris@1473: // which is too new for us Chris@1473: #pragma GCC diagnostic ignored "-Wdeprecated-declarations" Chris@1473: Chris@918: QRect vertical(cursorPos.x() - 12, 0, 12, v->getPaintHeight()); Chris@77: extents.push_back(vertical); Chris@77: Chris@77: QRect horizontal(0, cursorPos.y(), cursorPos.x(), 1); Chris@77: extents.push_back(horizontal); Chris@77: Chris@608: int sw = getVerticalScaleWidth(v, m_haveDetailedScale, paint); Chris@264: Chris@280: QRect freq(sw, cursorPos.y() - paint.fontMetrics().ascent() - 2, Chris@280: paint.fontMetrics().width("123456 Hz") + 2, Chris@280: paint.fontMetrics().height()); Chris@280: extents.push_back(freq); Chris@264: Chris@279: QRect pitch(sw, cursorPos.y() + 2, Chris@279: paint.fontMetrics().width("C#10+50c") + 2, Chris@279: paint.fontMetrics().height()); Chris@279: extents.push_back(pitch); Chris@279: Chris@280: QRect rt(cursorPos.x(), Chris@918: v->getPaintHeight() - paint.fontMetrics().height() - 2, Chris@280: paint.fontMetrics().width("1234.567 s"), Chris@280: paint.fontMetrics().height()); Chris@280: extents.push_back(rt); Chris@280: Chris@280: int w(paint.fontMetrics().width("1234567890") + 2); Chris@280: QRect frame(cursorPos.x() - w - 2, Chris@918: v->getPaintHeight() - paint.fontMetrics().height() - 2, Chris@280: w, Chris@280: paint.fontMetrics().height()); Chris@280: extents.push_back(frame); Chris@280: Chris@77: return true; Chris@77: } Chris@77: Chris@77: void Chris@918: SpectrogramLayer::paintCrosshairs(LayerGeometryProvider *v, QPainter &paint, Chris@77: QPoint cursorPos) const Chris@77: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return; Chris@1473: Chris@77: paint.save(); Chris@1437: Chris@608: int sw = getVerticalScaleWidth(v, m_haveDetailedScale, paint); Chris@283: Chris@282: QFont fn = paint.font(); Chris@282: if (fn.pointSize() > 8) { Chris@282: fn.setPointSize(fn.pointSize() - 1); Chris@282: paint.setFont(fn); Chris@282: } Chris@77: paint.setPen(m_crosshairColour); Chris@77: Chris@77: paint.drawLine(0, cursorPos.y(), cursorPos.x() - 1, cursorPos.y()); Chris@918: paint.drawLine(cursorPos.x(), 0, cursorPos.x(), v->getPaintHeight()); Chris@77: Chris@905: double fundamental = getFrequencyForY(v, cursorPos.y()); Chris@77: Chris@1473: PaintAssistant::drawVisibleText Chris@1473: (v, paint, Chris@1473: sw + 2, Chris@1473: cursorPos.y() - 2, Chris@1473: QString("%1 Hz").arg(fundamental), Chris@1473: PaintAssistant::OutlinedText); Chris@278: Chris@279: if (Pitch::isFrequencyInMidiRange(fundamental)) { Chris@279: QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental); Chris@1473: PaintAssistant::drawVisibleText Chris@1473: (v, paint, Chris@1473: sw + 2, Chris@1473: cursorPos.y() + paint.fontMetrics().ascent() + 2, Chris@1473: pitchLabel, Chris@1473: PaintAssistant::OutlinedText); Chris@279: } Chris@279: Chris@907: sv_frame_t frame = v->getFrameForX(cursorPos.x()); Chris@1473: RealTime rt = RealTime::frame2RealTime(frame, model->getSampleRate()); Chris@280: QString rtLabel = QString("%1 s").arg(rt.toText(true).c_str()); Chris@280: QString frameLabel = QString("%1").arg(frame); Chris@1473: PaintAssistant::drawVisibleText Chris@1473: (v, paint, Chris@1473: cursorPos.x() - paint.fontMetrics().width(frameLabel) - 2, Chris@1473: v->getPaintHeight() - 2, Chris@1473: frameLabel, Chris@1473: PaintAssistant::OutlinedText); Chris@1473: PaintAssistant::drawVisibleText Chris@1473: (v, paint, Chris@1473: cursorPos.x() + 2, Chris@1473: v->getPaintHeight() - 2, Chris@1473: rtLabel, Chris@1473: PaintAssistant::OutlinedText); Chris@264: Chris@77: int harmonic = 2; Chris@77: Chris@77: while (harmonic < 100) { Chris@77: Chris@907: int hy = int(lrint(getYForFrequency(v, fundamental * harmonic))); Chris@918: if (hy < 0 || hy > v->getPaintHeight()) break; Chris@77: Chris@77: int len = 7; Chris@77: Chris@77: if (harmonic % 2 == 0) { Chris@77: if (harmonic % 4 == 0) { Chris@77: len = 12; Chris@77: } else { Chris@77: len = 10; Chris@77: } Chris@77: } Chris@77: Chris@77: paint.drawLine(cursorPos.x() - len, Chris@907: hy, Chris@77: cursorPos.x(), Chris@907: hy); Chris@77: Chris@77: ++harmonic; Chris@77: } Chris@77: Chris@77: paint.restore(); Chris@77: } Chris@77: Chris@25: QString Chris@918: SpectrogramLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const Chris@25: { Chris@25: int x = pos.x(); Chris@25: int y = pos.y(); Chris@0: Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model || !model->isOK()) return ""; Chris@0: Chris@905: double magMin = 0, magMax = 0; Chris@905: double phaseMin = 0, phaseMax = 0; Chris@905: double freqMin = 0, freqMax = 0; Chris@905: double adjFreqMin = 0, adjFreqMax = 0; Chris@25: QString pitchMin, pitchMax; Chris@0: RealTime rtMin, rtMax; Chris@0: Chris@38: bool haveValues = false; Chris@0: Chris@44: if (!getXBinSourceRange(v, x, rtMin, rtMax)) { Chris@1234: return ""; Chris@38: } Chris@44: if (getXYBinSourceRange(v, x, y, magMin, magMax, phaseMin, phaseMax)) { Chris@1234: haveValues = true; Chris@38: } Chris@0: Chris@35: QString adjFreqText = "", adjPitchText = ""; Chris@35: Chris@1103: if (m_binDisplay == BinDisplay::PeakFrequencies) { Chris@35: Chris@1234: if (!getAdjustedYBinSourceRange(v, x, y, freqMin, freqMax, Chris@1234: adjFreqMin, adjFreqMax)) { Chris@1234: return ""; Chris@1234: } Chris@1234: Chris@1234: if (adjFreqMin != adjFreqMax) { Chris@1234: adjFreqText = tr("Peak Frequency:\t%1 - %2 Hz\n") Chris@1234: .arg(adjFreqMin).arg(adjFreqMax); Chris@1234: } else { Chris@1234: adjFreqText = tr("Peak Frequency:\t%1 Hz\n") Chris@1234: .arg(adjFreqMin); Chris@1234: } Chris@1234: Chris@1234: QString pmin = Pitch::getPitchLabelForFrequency(adjFreqMin); Chris@1234: QString pmax = Pitch::getPitchLabelForFrequency(adjFreqMax); Chris@1234: Chris@1234: if (pmin != pmax) { Chris@1234: adjPitchText = tr("Peak Pitch:\t%3 - %4\n").arg(pmin).arg(pmax); Chris@1234: } else { Chris@1234: adjPitchText = tr("Peak Pitch:\t%2\n").arg(pmin); Chris@1234: } Chris@35: Chris@35: } else { Chris@1234: Chris@1234: if (!getYBinSourceRange(v, y, freqMin, freqMax)) return ""; Chris@35: } Chris@35: Chris@25: QString text; Chris@25: Chris@25: if (rtMin != rtMax) { Chris@1234: text += tr("Time:\t%1 - %2\n") Chris@1234: .arg(rtMin.toText(true).c_str()) Chris@1234: .arg(rtMax.toText(true).c_str()); Chris@25: } else { Chris@1234: text += tr("Time:\t%1\n") Chris@1234: .arg(rtMin.toText(true).c_str()); Chris@0: } Chris@0: Chris@25: if (freqMin != freqMax) { Chris@1234: text += tr("%1Bin Frequency:\t%2 - %3 Hz\n%4Bin Pitch:\t%5 - %6\n") Chris@1234: .arg(adjFreqText) Chris@1234: .arg(freqMin) Chris@1234: .arg(freqMax) Chris@1234: .arg(adjPitchText) Chris@1234: .arg(Pitch::getPitchLabelForFrequency(freqMin)) Chris@1234: .arg(Pitch::getPitchLabelForFrequency(freqMax)); Chris@65: } else { Chris@1234: text += tr("%1Bin Frequency:\t%2 Hz\n%3Bin Pitch:\t%4\n") Chris@1234: .arg(adjFreqText) Chris@1234: .arg(freqMin) Chris@1234: .arg(adjPitchText) Chris@1234: .arg(Pitch::getPitchLabelForFrequency(freqMin)); Chris@1234: } Chris@25: Chris@38: if (haveValues) { Chris@1234: double dbMin = AudioLevel::multiplier_to_dB(magMin); Chris@1234: double dbMax = AudioLevel::multiplier_to_dB(magMax); Chris@1234: QString dbMinString; Chris@1234: QString dbMaxString; Chris@1234: if (dbMin == AudioLevel::DB_FLOOR) { Chris@1234: dbMinString = Strings::minus_infinity; Chris@1234: } else { Chris@1234: dbMinString = QString("%1").arg(lrint(dbMin)); Chris@1234: } Chris@1234: if (dbMax == AudioLevel::DB_FLOOR) { Chris@1234: dbMaxString = Strings::minus_infinity; Chris@1234: } else { Chris@1234: dbMaxString = QString("%1").arg(lrint(dbMax)); Chris@1234: } Chris@1234: if (lrint(dbMin) != lrint(dbMax)) { Chris@1234: text += tr("dB:\t%1 - %2").arg(dbMinString).arg(dbMaxString); Chris@1234: } else { Chris@1234: text += tr("dB:\t%1").arg(dbMinString); Chris@1234: } Chris@1234: if (phaseMin != phaseMax) { Chris@1234: text += tr("\nPhase:\t%1 - %2").arg(phaseMin).arg(phaseMax); Chris@1234: } else { Chris@1234: text += tr("\nPhase:\t%1").arg(phaseMin); Chris@1234: } Chris@25: } Chris@25: Chris@25: return text; Chris@0: } Chris@25: Chris@0: int Chris@40: SpectrogramLayer::getColourScaleWidth(QPainter &paint) const Chris@40: { Chris@40: int cw; Chris@40: Chris@119: cw = paint.fontMetrics().width("-80dB"); Chris@119: Chris@40: return cw; Chris@40: } Chris@40: Chris@40: int Chris@918: SpectrogramLayer::getVerticalScaleWidth(LayerGeometryProvider *, bool detailed, QPainter &paint) const Chris@0: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model || !model->isOK()) return 0; Chris@0: Chris@607: int cw = 0; Chris@607: if (detailed) cw = getColourScaleWidth(paint); Chris@40: Chris@0: int tw = paint.fontMetrics().width(QString("%1") Chris@1234: .arg(m_maxFrequency > 0 ? Chris@1234: m_maxFrequency - 1 : Chris@1473: model->getSampleRate() / 2)); Chris@0: Chris@234: int fw = paint.fontMetrics().width(tr("43Hz")); Chris@0: if (tw < fw) tw = fw; Chris@40: Chris@1103: int tickw = (m_binScale == BinScale::Log ? 10 : 4); Chris@0: Chris@40: return cw + tickw + tw + 13; Chris@0: } Chris@0: Chris@0: void Chris@1142: SpectrogramLayer::paintVerticalScale(LayerGeometryProvider *v, bool detailed, Chris@1142: QPainter &paint, QRect rect) const Chris@0: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model || !model->isOK()) { Chris@1234: return; Chris@0: } Chris@0: Chris@382: Profiler profiler("SpectrogramLayer::paintVerticalScale"); Chris@122: Chris@120: //!!! cache this? Chris@1142: Chris@0: int h = rect.height(), w = rect.width(); Chris@1142: int textHeight = paint.fontMetrics().height(); Chris@1142: Chris@1142: if (detailed && (h > textHeight * 3 + 10)) { Chris@1142: paintDetailedScale(v, paint, rect); Chris@1142: } Chris@1142: m_haveDetailedScale = detailed; Chris@0: Chris@1103: int tickw = (m_binScale == BinScale::Log ? 10 : 4); Chris@1103: int pkw = (m_binScale == BinScale::Log ? 10 : 0); Chris@40: Chris@1087: int bins = getFFTSize() / 2; Chris@1473: sv_samplerate_t sr = model->getSampleRate(); Chris@0: Chris@0: if (m_maxFrequency > 0) { Chris@1234: bins = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1); Chris@1234: if (bins > getFFTSize() / 2) bins = getFFTSize() / 2; Chris@0: } Chris@0: Chris@607: int cw = 0; Chris@607: if (detailed) cw = getColourScaleWidth(paint); Chris@40: Chris@0: int py = -1; Chris@0: int toff = -textHeight + paint.fontMetrics().ascent() + 2; Chris@0: Chris@40: paint.drawLine(cw + 7, 0, cw + 7, h); Chris@40: Chris@0: int bin = -1; Chris@0: Chris@918: for (int y = 0; y < v->getPaintHeight(); ++y) { Chris@0: Chris@1234: double q0, q1; Chris@1234: if (!getYBinRange(v, v->getPaintHeight() - y, q0, q1)) continue; Chris@1234: Chris@1234: int vy; Chris@1234: Chris@1234: if (int(q0) > bin) { Chris@1234: vy = y; Chris@1234: bin = int(q0); Chris@1234: } else { Chris@1234: continue; Chris@1234: } Chris@1234: Chris@1234: int freq = int((sr * bin) / getFFTSize()); Chris@1234: Chris@1234: if (py >= 0 && (vy - py) < textHeight - 1) { Chris@1234: if (m_binScale == BinScale::Linear) { Chris@1234: paint.drawLine(w - tickw, h - vy, w, h - vy); Chris@1234: } Chris@1234: continue; Chris@1234: } Chris@1234: Chris@1234: QString text = QString("%1").arg(freq); Chris@1234: if (bin == 1) text = tr("%1Hz").arg(freq); // bin 0 is DC Chris@1234: paint.drawLine(cw + 7, h - vy, w - pkw - 1, h - vy); Chris@1234: Chris@1234: if (h - vy - textHeight >= -2) { Chris@1234: int tx = w - 3 - paint.fontMetrics().width(text) - max(tickw, pkw); Chris@1234: paint.drawText(tx, h - vy + toff, text); Chris@1234: } Chris@1234: Chris@1234: py = vy; Chris@0: } Chris@40: Chris@1103: if (m_binScale == BinScale::Log) { Chris@40: Chris@277: // piano keyboard Chris@277: Chris@690: PianoScale().paintPianoVertical Chris@690: (v, paint, QRect(w - pkw - 1, 0, pkw, h), Chris@690: getEffectiveMinFrequency(), getEffectiveMaxFrequency()); Chris@40: } Chris@608: Chris@608: m_haveDetailedScale = detailed; Chris@0: } Chris@0: Chris@1142: void Chris@1142: SpectrogramLayer::paintDetailedScale(LayerGeometryProvider *v, Chris@1142: QPainter &paint, QRect rect) const Chris@1142: { Chris@1142: // The colour scale Chris@1143: Chris@1143: if (m_colourScale == ColourScaleType::Phase) { Chris@1143: paintDetailedScalePhase(v, paint, rect); Chris@1143: return; Chris@1143: } Chris@1142: Chris@1142: int h = rect.height(); Chris@1142: int textHeight = paint.fontMetrics().height(); Chris@1142: int toff = -textHeight + paint.fontMetrics().ascent() + 2; Chris@1142: Chris@1142: int cw = getColourScaleWidth(paint); Chris@1142: int cbw = paint.fontMetrics().width("dB"); Chris@1142: Chris@1142: int topLines = 2; Chris@1142: Chris@1142: int ch = h - textHeight * (topLines + 1) - 8; Chris@1234: // paint.drawRect(4, textHeight + 4, cw - 1, ch + 1); Chris@1142: paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1); Chris@1142: Chris@1142: QString top, bottom; Chris@1142: double min = m_viewMags[v->getId()].getMin(); Chris@1142: double max = m_viewMags[v->getId()].getMax(); Chris@1142: Chris@1142: if (min < m_threshold) min = m_threshold; Chris@1142: if (max <= min) max = min + 0.1; Chris@1142: Chris@1142: double dBmin = AudioLevel::multiplier_to_dB(min); Chris@1142: double dBmax = AudioLevel::multiplier_to_dB(max); Chris@1142: Chris@1142: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@1142: cerr << "paintVerticalScale: for view id " << v->getId() Chris@1142: << ": min = " << min << ", max = " << max Chris@1142: << ", dBmin = " << dBmin << ", dBmax = " << dBmax << endl; Chris@1142: #endif Chris@1142: Chris@1142: if (dBmax < -60.f) dBmax = -60.f; Chris@1142: else top = QString("%1").arg(lrint(dBmax)); Chris@1142: Chris@1142: if (dBmin < dBmax - 60.f) dBmin = dBmax - 60.f; Chris@1142: bottom = QString("%1").arg(lrint(dBmin)); Chris@1142: Chris@1142: #ifdef DEBUG_SPECTROGRAM_REPAINT Chris@1142: cerr << "adjusted dB range to min = " << dBmin << ", max = " << dBmax Chris@1142: << endl; Chris@1142: #endif Chris@1142: Chris@1143: paint.drawText((cw + 6 - paint.fontMetrics().width("dBFS")) / 2, Chris@1143: 2 + textHeight + toff, "dBFS"); Chris@1142: Chris@1142: paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top), Chris@1142: 2 + textHeight * topLines + toff + textHeight/2, top); Chris@1142: Chris@1142: paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom), Chris@1142: h + toff - 3 - textHeight/2, bottom); Chris@1142: Chris@1142: paint.save(); Chris@1142: paint.setBrush(Qt::NoBrush); Chris@1142: Chris@1142: int lasty = 0; Chris@1142: int lastdb = 0; Chris@1142: Chris@1142: for (int i = 0; i < ch; ++i) { Chris@1142: Chris@1142: double dBval = dBmin + (((dBmax - dBmin) * i) / (ch - 1)); Chris@1142: int idb = int(dBval); Chris@1142: Chris@1142: double value = AudioLevel::dB_to_multiplier(dBval); Chris@1142: paint.setPen(getRenderer(v)->getColour(value)); Chris@1142: Chris@1142: int y = textHeight * topLines + 4 + ch - i; Chris@1142: Chris@1142: paint.drawLine(5 + cw - cbw, y, cw + 2, y); Chris@1144: Chris@1142: if (i == 0) { Chris@1142: lasty = y; Chris@1142: lastdb = idb; Chris@1142: } else if (i < ch - paint.fontMetrics().ascent() && Chris@1142: idb != lastdb && Chris@1142: ((abs(y - lasty) > textHeight && Chris@1142: idb % 10 == 0) || Chris@1142: (abs(y - lasty) > paint.fontMetrics().ascent() && Chris@1142: idb % 5 == 0))) { Chris@1144: paint.setPen(v->getForeground()); Chris@1142: QString text = QString("%1").arg(idb); Chris@1142: paint.drawText(3 + cw - cbw - paint.fontMetrics().width(text), Chris@1142: y + toff + textHeight/2, text); Chris@1142: paint.drawLine(5 + cw - cbw, y, 8 + cw - cbw, y); Chris@1142: lasty = y; Chris@1142: lastdb = idb; Chris@1142: } Chris@1142: } Chris@1142: paint.restore(); Chris@1142: } Chris@1142: Chris@1143: void Chris@1143: SpectrogramLayer::paintDetailedScalePhase(LayerGeometryProvider *v, Chris@1143: QPainter &paint, QRect rect) const Chris@1143: { Chris@1143: // The colour scale in phase mode Chris@1143: Chris@1143: int h = rect.height(); Chris@1143: int textHeight = paint.fontMetrics().height(); Chris@1143: int toff = -textHeight + paint.fontMetrics().ascent() + 2; Chris@1143: Chris@1143: int cw = getColourScaleWidth(paint); Chris@1143: Chris@1143: // Phase is not measured in dB of course, but this places the Chris@1143: // scale at the same position as in the magnitude spectrogram Chris@1143: int cbw = paint.fontMetrics().width("dB"); Chris@1143: Chris@1143: int topLines = 1; Chris@1143: Chris@1143: int ch = h - textHeight * (topLines + 1) - 8; Chris@1143: paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1); Chris@1143: Chris@1147: QString top = Strings::pi, bottom = Strings::minus_pi, middle = "0"; Chris@1143: Chris@1143: double min = -M_PI; Chris@1143: double max = M_PI; Chris@1143: Chris@1143: paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top), Chris@1143: 2 + textHeight * topLines + toff + textHeight/2, top); Chris@1143: Chris@1143: paint.drawText(3 + cw - cbw - paint.fontMetrics().width(middle), Chris@1143: 2 + textHeight * topLines + ch/2 + toff + textHeight/2, middle); Chris@1143: Chris@1143: paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom), Chris@1143: h + toff - 3 - textHeight/2, bottom); Chris@1143: Chris@1143: paint.save(); Chris@1143: paint.setBrush(Qt::NoBrush); Chris@1143: Chris@1143: for (int i = 0; i < ch; ++i) { Chris@1143: double val = min + (((max - min) * i) / (ch - 1)); Chris@1143: paint.setPen(getRenderer(v)->getColour(val)); Chris@1143: int y = textHeight * topLines + 4 + ch - i; Chris@1143: paint.drawLine(5 + cw - cbw, y, cw + 2, y); Chris@1143: } Chris@1143: paint.restore(); Chris@1143: } Chris@1143: Chris@187: class SpectrogramRangeMapper : public RangeMapper Chris@187: { Chris@187: public: Chris@901: SpectrogramRangeMapper(sv_samplerate_t sr, int /* fftsize */) : Chris@901: m_dist(sr / 2), Chris@901: m_s2(sqrt(sqrt(2))) { } Chris@1405: ~SpectrogramRangeMapper() override { } Chris@187: Chris@1405: int getPositionForValue(double value) const override { Chris@901: Chris@901: double dist = m_dist; Chris@187: Chris@187: int n = 0; Chris@187: Chris@901: while (dist > (value + 0.00001) && dist > 0.1) { Chris@187: dist /= m_s2; Chris@187: ++n; Chris@187: } Chris@187: Chris@187: return n; Chris@187: } Chris@724: Chris@1405: int getPositionForValueUnclamped(double value) const override { Chris@724: // We don't really support this Chris@724: return getPositionForValue(value); Chris@724: } Chris@187: Chris@1405: double getValueForPosition(int position) const override { Chris@187: Chris@187: // Vertical zoom step 0 shows the entire range from DC -> Chris@187: // Nyquist frequency. Step 1 shows 2^(1/4) of the range of Chris@187: // step 0, and so on until the visible range is smaller than Chris@187: // the frequency step between bins at the current fft size. Chris@187: Chris@901: double dist = m_dist; Chris@187: Chris@187: int n = 0; Chris@187: while (n < position) { Chris@187: dist /= m_s2; Chris@187: ++n; Chris@187: } Chris@187: Chris@187: return dist; Chris@187: } Chris@187: Chris@1405: double getValueForPositionUnclamped(int position) const override { Chris@724: // We don't really support this Chris@724: return getValueForPosition(position); Chris@724: } Chris@724: Chris@1405: QString getUnit() const override { return "Hz"; } Chris@187: Chris@187: protected: Chris@901: double m_dist; Chris@901: double m_s2; Chris@187: }; Chris@187: Chris@133: int Chris@133: SpectrogramLayer::getVerticalZoomSteps(int &defaultStep) const Chris@133: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return 0; Chris@1473: Chris@1473: sv_samplerate_t sr = model->getSampleRate(); Chris@187: Chris@1087: SpectrogramRangeMapper mapper(sr, getFFTSize()); Chris@1087: Chris@1087: // int maxStep = mapper.getPositionForValue((double(sr) / getFFTSize()) + 0.001); Chris@187: int maxStep = mapper.getPositionForValue(0); Chris@905: int minStep = mapper.getPositionForValue(double(sr) / 2); Chris@250: Chris@805: int initialMax = m_initialMaxFrequency; Chris@907: if (initialMax == 0) initialMax = int(sr / 2); Chris@250: Chris@250: defaultStep = mapper.getPositionForValue(initialMax) - minStep; Chris@250: Chris@587: // SVDEBUG << "SpectrogramLayer::getVerticalZoomSteps: " << maxStep - minStep << " (" << maxStep <<"-" << minStep << "), default is " << defaultStep << " (from initial max freq " << initialMax << ")" << endl; Chris@187: Chris@187: return maxStep - minStep; Chris@133: } Chris@133: Chris@133: int Chris@133: SpectrogramLayer::getCurrentVerticalZoomStep() const Chris@133: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return 0; Chris@133: Chris@905: double dmin, dmax; Chris@133: getDisplayExtents(dmin, dmax); Chris@133: Chris@1473: SpectrogramRangeMapper mapper(model->getSampleRate(), getFFTSize()); Chris@187: int n = mapper.getPositionForValue(dmax - dmin); Chris@587: // SVDEBUG << "SpectrogramLayer::getCurrentVerticalZoomStep: " << n << endl; Chris@133: return n; Chris@133: } Chris@133: Chris@133: void Chris@133: SpectrogramLayer::setVerticalZoomStep(int step) Chris@133: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return; Chris@187: Chris@905: double dmin = m_minFrequency, dmax = m_maxFrequency; Chris@253: // getDisplayExtents(dmin, dmax); Chris@253: Chris@682: // cerr << "current range " << dmin << " -> " << dmax << ", range " << dmax-dmin << ", mid " << (dmax + dmin)/2 << endl; Chris@133: Chris@1473: sv_samplerate_t sr = model->getSampleRate(); Chris@1087: SpectrogramRangeMapper mapper(sr, getFFTSize()); Chris@905: double newdist = mapper.getValueForPosition(step); Chris@905: Chris@905: double newmin, newmax; Chris@253: Chris@1103: if (m_binScale == BinScale::Log) { Chris@253: Chris@253: // need to pick newmin and newmax such that Chris@253: // Chris@253: // (log(newmin) + log(newmax)) / 2 == logmid Chris@253: // and Chris@253: // newmax - newmin = newdist Chris@253: // Chris@253: // so log(newmax - newdist) + log(newmax) == 2logmid Chris@253: // log(newmax(newmax - newdist)) == 2logmid Chris@253: // newmax.newmax - newmax.newdist == exp(2logmid) Chris@253: // newmax^2 + (-newdist)newmax + -exp(2logmid) == 0 Chris@253: // quadratic with a = 1, b = -newdist, c = -exp(2logmid), all known Chris@253: // Chris@253: // positive root Chris@253: // newmax = (newdist + sqrt(newdist^2 + 4exp(2logmid))) / 2 Chris@253: // Chris@253: // but logmid = (log(dmin) + log(dmax)) / 2 Chris@253: // so exp(2logmid) = exp(log(dmin) + log(dmax)) Chris@253: // = exp(log(dmin.dmax)) Chris@253: // = dmin.dmax Chris@253: // so newmax = (newdist + sqrtf(newdist^2 + 4dmin.dmax)) / 2 Chris@253: Chris@907: newmax = (newdist + sqrt(newdist*newdist + 4*dmin*dmax)) / 2; Chris@253: newmin = newmax - newdist; Chris@253: Chris@682: // cerr << "newmin = " << newmin << ", newmax = " << newmax << endl; Chris@253: Chris@253: } else { Chris@905: double dmid = (dmax + dmin) / 2; Chris@253: newmin = dmid - newdist / 2; Chris@253: newmax = dmid + newdist / 2; Chris@253: } Chris@187: Chris@905: double mmin, mmax; Chris@187: mmin = 0; Chris@905: mmax = double(sr) / 2; Chris@133: Chris@187: if (newmin < mmin) { Chris@187: newmax += (mmin - newmin); Chris@187: newmin = mmin; Chris@187: } Chris@187: if (newmax > mmax) { Chris@187: newmax = mmax; Chris@187: } Chris@133: Chris@587: // SVDEBUG << "SpectrogramLayer::setVerticalZoomStep: " << step << ": " << newmin << " -> " << newmax << " (range " << newdist << ")" << endl; Chris@253: Chris@907: setMinFrequency(int(lrint(newmin))); Chris@907: setMaxFrequency(int(lrint(newmax))); Chris@187: } Chris@187: Chris@187: RangeMapper * Chris@187: SpectrogramLayer::getNewVerticalZoomRangeMapper() const Chris@187: { Chris@1473: auto model = ModelById::getAs(m_model); Chris@1473: if (!model) return nullptr; Chris@1473: return new SpectrogramRangeMapper(model->getSampleRate(), getFFTSize()); Chris@133: } Chris@133: Chris@273: void Chris@918: SpectrogramLayer::updateMeasureRectYCoords(LayerGeometryProvider *v, const MeasureRect &r) const Chris@273: { Chris@273: int y0 = 0; Chris@907: if (r.startY > 0.0) y0 = int(getYForFrequency(v, r.startY)); Chris@273: Chris@273: int y1 = y0; Chris@907: if (r.endY > 0.0) y1 = int(getYForFrequency(v, r.endY)); Chris@273: Chris@587: // SVDEBUG << "SpectrogramLayer::updateMeasureRectYCoords: start " << r.startY << " -> " << y0 << ", end " << r.endY << " -> " << y1 << endl; Chris@273: Chris@273: r.pixrect = QRect(r.pixrect.x(), y0, r.pixrect.width(), y1 - y0); Chris@273: } Chris@273: Chris@273: void Chris@918: SpectrogramLayer::setMeasureRectYCoord(LayerGeometryProvider *v, MeasureRect &r, bool start, int y) const Chris@273: { Chris@273: if (start) { Chris@273: r.startY = getFrequencyForY(v, y); Chris@273: r.endY = r.startY; Chris@273: } else { Chris@273: r.endY = getFrequencyForY(v, y); Chris@273: } Chris@587: // SVDEBUG << "SpectrogramLayer::setMeasureRectYCoord: start " << r.startY << " <- " << y << ", end " << r.endY << " <- " << y << endl; Chris@273: Chris@273: } Chris@273: Chris@316: void Chris@316: SpectrogramLayer::toXml(QTextStream &stream, Chris@316: QString indent, QString extraAttributes) const Chris@6: { Chris@6: QString s; Chris@6: Chris@6: s += QString("channel=\"%1\" " Chris@1234: "windowSize=\"%2\" " Chris@1234: "windowHopLevel=\"%3\" " Chris@1382: "oversampling=\"%4\" " Chris@1382: "gain=\"%5\" " Chris@1382: "threshold=\"%6\" ") Chris@1234: .arg(m_channel) Chris@1234: .arg(m_windowSize) Chris@1234: .arg(m_windowHopLevel) Chris@1382: .arg(m_oversampling) Chris@1234: .arg(m_gain) Chris@1234: .arg(m_threshold); Chris@37: Chris@37: s += QString("minFrequency=\"%1\" " Chris@1234: "maxFrequency=\"%2\" " Chris@1234: "colourScale=\"%3\" " Chris@1362: "colourRotation=\"%4\" " Chris@1362: "frequencyScale=\"%5\" " Chris@1362: "binDisplay=\"%6\" ") Chris@1234: .arg(m_minFrequency) Chris@1234: .arg(m_maxFrequency) Chris@1234: .arg(convertFromColourScale(m_colourScale, m_colourScaleMultiple)) Chris@1234: .arg(m_colourRotation) Chris@1234: .arg(int(m_binScale)) Chris@1234: .arg(int(m_binDisplay)); Chris@761: Chris@1362: // New-style colour map attribute, by string id rather than by Chris@1362: // number Chris@1362: Chris@1362: s += QString("colourMap=\"%1\" ") Chris@1362: .arg(ColourMapper::getColourMapId(m_colourMap)); Chris@1362: Chris@1362: // Old-style colour map attribute Chris@1362: Chris@1362: s += QString("colourScheme=\"%1\" ") Chris@1362: .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap)); Chris@1362: Chris@1009: // New-style normalization attributes, allowing for more types of Chris@1009: // normalization in future: write out the column normalization Chris@1009: // type separately, and then whether we are normalizing visible Chris@1009: // area as well afterwards Chris@1009: Chris@1009: s += QString("columnNormalization=\"%1\" ") Chris@1104: .arg(m_normalization == ColumnNormalization::Max1 ? "peak" : Chris@1104: m_normalization == ColumnNormalization::Hybrid ? "hybrid" : "none"); Chris@1009: Chris@1009: // Old-style normalization attribute. We *don't* write out Chris@1009: // normalizeHybrid here because the only release that would accept Chris@1009: // it (Tony v1.0) has a totally different scale factor for Chris@1009: // it. We'll just have to accept that session files from Tony Chris@1009: // v2.0+ will look odd in Tony v1.0 Chris@1009: Chris@1009: s += QString("normalizeColumns=\"%1\" ") Chris@1234: .arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false"); Chris@1009: Chris@1009: // And this applies to both old- and new-style attributes Chris@1009: Chris@1009: s += QString("normalizeVisibleArea=\"%1\" ") Chris@1104: .arg(m_normalizeVisibleArea ? "true" : "false"); Chris@1009: Chris@316: Layer::toXml(stream, indent, extraAttributes + " " + s); Chris@6: } Chris@6: Chris@11: void Chris@11: SpectrogramLayer::setProperties(const QXmlAttributes &attributes) Chris@11: { Chris@11: bool ok = false; Chris@11: Chris@11: int channel = attributes.value("channel").toInt(&ok); Chris@11: if (ok) setChannel(channel); Chris@11: Chris@805: int windowSize = attributes.value("windowSize").toUInt(&ok); Chris@11: if (ok) setWindowSize(windowSize); Chris@11: Chris@805: int windowHopLevel = attributes.value("windowHopLevel").toUInt(&ok); Chris@97: if (ok) setWindowHopLevel(windowHopLevel); Chris@97: else { Chris@805: int windowOverlap = attributes.value("windowOverlap").toUInt(&ok); Chris@97: // a percentage value Chris@97: if (ok) { Chris@97: if (windowOverlap == 0) setWindowHopLevel(0); Chris@97: else if (windowOverlap == 25) setWindowHopLevel(1); Chris@97: else if (windowOverlap == 50) setWindowHopLevel(2); Chris@97: else if (windowOverlap == 75) setWindowHopLevel(3); Chris@97: else if (windowOverlap == 90) setWindowHopLevel(4); Chris@97: } Chris@97: } Chris@11: Chris@1382: int oversampling = attributes.value("oversampling").toUInt(&ok); Chris@1382: if (ok) setOversampling(oversampling); Chris@1382: Chris@11: float gain = attributes.value("gain").toFloat(&ok); Chris@11: if (ok) setGain(gain); Chris@11: Chris@37: float threshold = attributes.value("threshold").toFloat(&ok); Chris@37: if (ok) setThreshold(threshold); Chris@37: Chris@805: int minFrequency = attributes.value("minFrequency").toUInt(&ok); Chris@187: if (ok) { Chris@587: SVDEBUG << "SpectrogramLayer::setProperties: setting min freq to " << minFrequency << endl; Chris@187: setMinFrequency(minFrequency); Chris@187: } Chris@37: Chris@805: int maxFrequency = attributes.value("maxFrequency").toUInt(&ok); Chris@187: if (ok) { Chris@587: SVDEBUG << "SpectrogramLayer::setProperties: setting max freq to " << maxFrequency << endl; Chris@187: setMaxFrequency(maxFrequency); Chris@187: } Chris@11: Chris@1137: auto colourScale = convertToColourScale Chris@1092: (attributes.value("colourScale").toInt(&ok)); Chris@1137: if (ok) { Chris@1137: setColourScale(colourScale.first); Chris@1137: setColourScaleMultiple(colourScale.second); Chris@1137: } Chris@11: Chris@1362: QString colourMapId = attributes.value("colourMap"); Chris@1362: int colourMap = ColourMapper::getColourMapById(colourMapId); Chris@1362: if (colourMap >= 0) { Chris@1362: setColourMap(colourMap); Chris@1362: } else { Chris@1362: colourMap = attributes.value("colourScheme").toInt(&ok); Chris@1362: if (ok && colourMap < ColourMapper::getColourMapCount()) { Chris@1362: setColourMap(colourMap); Chris@1362: } Chris@1362: } Chris@11: Chris@37: int colourRotation = attributes.value("colourRotation").toInt(&ok); Chris@37: if (ok) setColourRotation(colourRotation); Chris@37: Chris@1103: BinScale binScale = (BinScale) Chris@1234: attributes.value("frequencyScale").toInt(&ok); Chris@1093: if (ok) setBinScale(binScale); Chris@1093: Chris@1103: BinDisplay binDisplay = (BinDisplay) Chris@1234: attributes.value("binDisplay").toInt(&ok); Chris@37: if (ok) setBinDisplay(binDisplay); Chris@36: Chris@1009: bool haveNewStyleNormalization = false; Chris@1009: Chris@1009: QString columnNormalization = attributes.value("columnNormalization"); Chris@1009: Chris@1009: if (columnNormalization != "") { Chris@1009: Chris@1009: haveNewStyleNormalization = true; Chris@1009: Chris@1009: if (columnNormalization == "peak") { Chris@1104: setNormalization(ColumnNormalization::Max1); Chris@1009: } else if (columnNormalization == "hybrid") { Chris@1104: setNormalization(ColumnNormalization::Hybrid); Chris@1009: } else if (columnNormalization == "none") { Chris@1104: setNormalization(ColumnNormalization::None); Chris@1009: } else { Chris@1265: SVCERR << "NOTE: Unknown or unsupported columnNormalization attribute \"" Chris@1009: << columnNormalization << "\"" << endl; Chris@1009: } Chris@1009: } Chris@1009: Chris@1009: if (!haveNewStyleNormalization) { Chris@1009: Chris@1009: bool normalizeColumns = Chris@1009: (attributes.value("normalizeColumns").trimmed() == "true"); Chris@1009: if (normalizeColumns) { Chris@1104: setNormalization(ColumnNormalization::Max1); Chris@1009: } Chris@1009: Chris@1009: bool normalizeHybrid = Chris@1009: (attributes.value("normalizeHybrid").trimmed() == "true"); Chris@1009: if (normalizeHybrid) { Chris@1104: setNormalization(ColumnNormalization::Hybrid); Chris@1009: } Chris@862: } Chris@153: Chris@153: bool normalizeVisibleArea = Chris@1099: (attributes.value("normalizeVisibleArea").trimmed() == "true"); Chris@1104: setNormalizeVisibleArea(normalizeVisibleArea); Chris@1104: Chris@1104: if (!haveNewStyleNormalization && m_normalization == ColumnNormalization::Hybrid) { Chris@1009: // Tony v1.0 is (and hopefully will remain!) the only released Chris@1009: // SV-a-like to use old-style attributes when saving sessions Chris@1009: // that ask for hybrid normalization. It saves them with the Chris@1009: // wrong gain factor, so hack in a fix for that here -- this Chris@1009: // gives us backward but not forward compatibility. Chris@1087: setGain(m_gain / float(getFFTSize() / 2)); Chris@862: } Chris@11: } Chris@11: