Chris@133: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@133: Chris@133: /* Chris@133: Sonic Visualiser Chris@133: An audio file viewer and annotation editor. Chris@133: Centre for Digital Music, Queen Mary, University of London. Chris@195: This file copyright 2006-2007 QMUL. Chris@133: Chris@133: This program is free software; you can redistribute it and/or Chris@133: modify it under the terms of the GNU General Public License as Chris@133: published by the Free Software Foundation; either version 2 of the Chris@133: License, or (at your option) any later version. See the file Chris@133: COPYING included with this distribution for more information. Chris@133: */ Chris@133: Chris@133: #include "SpectrumLayer.h" Chris@133: Chris@133: #include "data/model/FFTModel.h" Chris@133: #include "view/View.h" Chris@153: #include "base/AudioLevel.h" Chris@153: #include "base/Preferences.h" Chris@167: #include "base/RangeMapper.h" Chris@277: #include "base/Pitch.h" Chris@1147: #include "base/Strings.h" Chris@1078: Chris@376: #include "ColourMapper.h" Chris@1078: #include "PaintAssistant.h" Chris@1276: #include "PianoScale.h" Chris@1281: #include "HorizontalFrequencyScale.h" Chris@254: Chris@254: #include Chris@316: #include Chris@316: Chris@133: Chris@133: SpectrumLayer::SpectrumLayer() : Chris@153: m_channel(-1), Chris@153: m_channelSet(false), Chris@290: m_windowSize(4096), Chris@153: m_windowType(HanningWindow), Chris@290: m_windowHopLevel(3), Chris@1382: m_oversampling(1), Chris@284: m_showPeaks(false), Chris@1403: m_newFFTNeeded(true), Chris@1403: m_freqOfMinBin(0.0) Chris@133: { Chris@1400: m_binAlignment = BinsCentredOnScalePoints; Chris@1400: Chris@153: Preferences *prefs = Preferences::getInstance(); Chris@153: connect(prefs, SIGNAL(propertyChanged(PropertyContainer::PropertyName)), Chris@153: this, SLOT(preferenceChanged(PropertyContainer::PropertyName))); Chris@153: setWindowType(prefs->getWindowType()); Chris@195: Chris@195: setBinScale(LogBins); Chris@133: } Chris@133: Chris@133: SpectrumLayer::~SpectrumLayer() Chris@133: { Chris@1473: ModelById::release(m_sliceableModel); Chris@133: } Chris@133: Chris@133: void Chris@1471: SpectrumLayer::setModel(ModelId modelId) Chris@133: { 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@345: Chris@1471: if (m_originModel == modelId) return; Chris@1471: m_originModel = modelId; Chris@349: Chris@349: m_newFFTNeeded = true; Chris@349: Chris@349: emit layerParametersChanged(); Chris@349: } Chris@349: Chris@349: void Chris@349: SpectrumLayer::setChannel(int channel) Chris@349: { Chris@587: SVDEBUG << "SpectrumLayer::setChannel(" << channel << ") from " << m_channel << endl; Chris@349: Chris@349: m_channelSet = true; Chris@349: Chris@349: if (m_channel == channel) return; Chris@349: Chris@349: m_channel = channel; Chris@349: Chris@349: m_newFFTNeeded = true; Chris@349: Chris@349: emit layerParametersChanged(); Chris@153: } Chris@153: Chris@153: void Chris@193: SpectrumLayer::setupFFT() Chris@153: { Chris@1473: ModelById::release(m_sliceableModel); Chris@1473: m_sliceableModel = {}; Chris@349: Chris@1473: if (m_originModel.isNone()) { Chris@349: return; Chris@153: } Chris@153: Chris@1382: int fftSize = getFFTSize(); Chris@1382: Chris@1473: auto newFFT = std::make_shared(m_originModel, Chris@1473: m_channel, Chris@1473: m_windowType, Chris@1473: m_windowSize, Chris@1473: getWindowIncrement(), Chris@1473: fftSize); Chris@153: Chris@1399: if (m_minbin == 0 && m_maxbin == 0) { Chris@1399: m_minbin = 1; Chris@1403: m_freqOfMinBin = double(m_minbin * newFFT->getSampleRate()) Chris@1403: / getFFTSize(); Chris@1399: m_maxbin = newFFT->getHeight(); Chris@1399: } Chris@1400: Chris@1481: setSliceableModel(ModelById::add(newFFT)); Chris@193: Chris@254: m_biasCurve.clear(); Chris@1382: for (int i = 0; i < fftSize; ++i) { Chris@1403: // Scale by the window size, not the FFT size, because we Chris@1403: // don't want to scale down by all the zero bins Chris@1403: m_biasCurve.push_back(1.f / (float(m_windowSize)/2.f)); Chris@254: } Chris@254: Chris@349: m_newFFTNeeded = false; Chris@133: } Chris@133: Chris@153: Layer::PropertyList Chris@153: SpectrumLayer::getProperties() const Chris@153: { Chris@193: PropertyList list = SliceLayer::getProperties(); Chris@153: list.push_back("Window Size"); Chris@153: list.push_back("Window Increment"); Chris@1382: list.push_back("Oversampling"); Chris@284: list.push_back("Show Peak Frequencies"); Chris@153: return list; Chris@153: } Chris@153: Chris@153: QString Chris@153: SpectrumLayer::getPropertyLabel(const PropertyName &name) const Chris@153: { Chris@153: if (name == "Window Size") return tr("Window Size"); Chris@153: if (name == "Window Increment") return tr("Window Overlap"); Chris@1382: if (name == "Oversampling") return tr("Oversampling"); Chris@284: if (name == "Show Peak Frequencies") return tr("Show Peak Frequencies"); Chris@193: return SliceLayer::getPropertyLabel(name); Chris@153: } Chris@153: Chris@335: QString Chris@335: SpectrumLayer::getPropertyIconName(const PropertyName &name) const Chris@335: { Chris@335: if (name == "Show Peak Frequencies") return "show-peaks"; Chris@335: return SliceLayer::getPropertyIconName(name); Chris@335: } Chris@335: Chris@153: Layer::PropertyType Chris@153: SpectrumLayer::getPropertyType(const PropertyName &name) const Chris@153: { Chris@193: if (name == "Window Size") return ValueProperty; Chris@193: if (name == "Window Increment") return ValueProperty; Chris@1382: if (name == "Oversampling") return ValueProperty; Chris@284: if (name == "Show Peak Frequencies") return ToggleProperty; Chris@193: return SliceLayer::getPropertyType(name); Chris@153: } Chris@153: Chris@153: QString Chris@153: SpectrumLayer::getPropertyGroupName(const PropertyName &name) const Chris@153: { Chris@153: if (name == "Window Size" || Chris@1382: name == "Window Increment" || Chris@1382: name == "Oversampling") return tr("Window"); Chris@557: if (name == "Show Peak Frequencies") return tr("Bins"); Chris@193: return SliceLayer::getPropertyGroupName(name); Chris@153: } Chris@153: Chris@153: int Chris@153: SpectrumLayer::getPropertyRangeAndValue(const PropertyName &name, Chris@216: int *min, int *max, int *deflt) const Chris@153: { Chris@216: int val = 0; Chris@153: Chris@216: int garbage0, garbage1, garbage2; Chris@153: if (!min) min = &garbage0; Chris@153: if (!max) max = &garbage1; Chris@216: if (!deflt) deflt = &garbage2; Chris@153: Chris@193: if (name == "Window Size") { Chris@153: Chris@1266: *min = 0; Chris@1266: *max = 15; Chris@216: *deflt = 5; Chris@1266: Chris@1266: val = 0; Chris@1266: int ws = m_windowSize; Chris@1266: while (ws > 32) { ws >>= 1; val ++; } Chris@153: Chris@153: } else if (name == "Window Increment") { Chris@1266: Chris@1266: *min = 0; Chris@1266: *max = 5; Chris@216: *deflt = 2; Chris@1266: Chris@216: val = m_windowHopLevel; Chris@153: Chris@1382: } else if (name == "Oversampling") { Chris@1382: Chris@1382: *min = 0; Chris@1382: *max = 3; Chris@1382: *deflt = 0; Chris@1382: Chris@1382: val = 0; Chris@1382: int ov = m_oversampling; Chris@1382: while (ov > 1) { ov >>= 1; val ++; } Chris@1382: Chris@284: } else if (name == "Show Peak Frequencies") { Chris@284: Chris@284: return m_showPeaks ? 1 : 0; Chris@284: Chris@153: } else { Chris@193: Chris@216: val = SliceLayer::getPropertyRangeAndValue(name, min, max, deflt); Chris@153: } Chris@153: Chris@216: return val; Chris@153: } Chris@153: Chris@153: QString Chris@153: SpectrumLayer::getPropertyValueLabel(const PropertyName &name, Chris@1266: int value) const Chris@153: { Chris@153: if (name == "Window Size") { Chris@1266: return QString("%1").arg(32 << value); Chris@153: } Chris@153: if (name == "Window Increment") { Chris@1266: switch (value) { Chris@1266: default: Chris@1266: case 0: return tr("None"); Chris@1266: case 1: return tr("25 %"); Chris@1266: case 2: return tr("50 %"); Chris@1266: case 3: return tr("75 %"); Chris@1266: case 4: return tr("87.5 %"); Chris@1266: case 5: return tr("93.75 %"); Chris@1266: } Chris@153: } Chris@1382: if (name == "Oversampling") { Chris@1382: switch (value) { Chris@1382: default: Chris@1382: case 0: return tr("1x"); Chris@1382: case 1: return tr("2x"); Chris@1382: case 2: return tr("4x"); Chris@1382: case 3: return tr("8x"); Chris@1382: } Chris@1382: } Chris@193: return SliceLayer::getPropertyValueLabel(name, value); Chris@153: } Chris@153: Chris@167: RangeMapper * Chris@167: SpectrumLayer::getNewPropertyRangeMapper(const PropertyName &name) const Chris@167: { Chris@193: return SliceLayer::getNewPropertyRangeMapper(name); Chris@167: } Chris@167: Chris@133: void Chris@153: SpectrumLayer::setProperty(const PropertyName &name, int value) Chris@133: { Chris@193: if (name == "Window Size") { Chris@1266: setWindowSize(32 << value); Chris@153: } else if (name == "Window Increment") { Chris@153: setWindowHopLevel(value); Chris@1382: } else if (name == "Oversampling") { Chris@1382: setOversampling(1 << value); Chris@284: } else if (name == "Show Peak Frequencies") { Chris@284: setShowPeaks(value ? true : false); Chris@193: } else { Chris@193: SliceLayer::setProperty(name, value); Chris@153: } Chris@153: } Chris@153: Chris@153: void Chris@805: SpectrumLayer::setWindowSize(int ws) Chris@153: { Chris@153: if (m_windowSize == ws) return; Chris@1389: Chris@1389: SVDEBUG << "setWindowSize: from " << m_windowSize Chris@1389: << " to " << ws << ": updating min and max bins from " Chris@1389: << m_minbin << " and " << m_maxbin << " to "; Chris@1389: Chris@1403: int previousWs = m_windowSize; Chris@1400: m_windowSize = ws; Chris@1403: Chris@1403: m_minbin = int(round(getBinForFrequency(m_freqOfMinBin))); Chris@1403: m_maxbin = int(round((double(m_maxbin) / previousWs) * m_windowSize)); Chris@1400: Chris@1400: int h = getFFTSize() / 2 + 1; Chris@1400: if (m_minbin > h) m_minbin = h; Chris@1400: if (m_maxbin > h) m_maxbin = h; Chris@1400: Chris@1389: SVDEBUG << m_minbin << " and " << m_maxbin << endl; Chris@1389: Chris@275: m_newFFTNeeded = true; Chris@153: emit layerParametersChanged(); Chris@153: } Chris@153: Chris@153: void Chris@805: SpectrumLayer::setWindowHopLevel(int v) Chris@153: { Chris@153: if (m_windowHopLevel == v) return; Chris@153: m_windowHopLevel = v; Chris@275: m_newFFTNeeded = true; Chris@153: emit layerParametersChanged(); Chris@153: } Chris@153: Chris@153: void Chris@153: SpectrumLayer::setWindowType(WindowType w) Chris@153: { Chris@153: if (m_windowType == w) return; Chris@153: m_windowType = w; Chris@275: m_newFFTNeeded = true; Chris@153: emit layerParametersChanged(); Chris@153: } Chris@153: Chris@153: void Chris@1382: SpectrumLayer::setOversampling(int oversampling) Chris@1382: { Chris@1382: if (m_oversampling == oversampling) return; Chris@1389: Chris@1389: SVDEBUG << "setOversampling: from " << m_oversampling Chris@1389: << " to " << oversampling << ": updating min and max bins from " Chris@1389: << m_minbin << " and " << m_maxbin << " to "; Chris@1389: Chris@1403: int previousOversampling = m_oversampling; Chris@1400: m_oversampling = oversampling; Chris@1403: Chris@1403: m_minbin = int(round(getBinForFrequency(m_freqOfMinBin))); Chris@1403: m_maxbin = int(round((double(m_maxbin) / previousOversampling) * Chris@1403: m_oversampling)); Chris@1400: Chris@1400: int h = getFFTSize() / 2 + 1; Chris@1400: if (m_minbin > h) m_minbin = h; Chris@1400: if (m_maxbin > h) m_maxbin = h; Chris@1400: Chris@1389: SVDEBUG << m_minbin << " and " << m_maxbin << endl; Chris@1389: Chris@1382: m_newFFTNeeded = true; Chris@1382: emit layerParametersChanged(); Chris@1382: } Chris@1382: Chris@1382: int Chris@1382: SpectrumLayer::getOversampling() const Chris@1382: { Chris@1382: return m_oversampling; Chris@1382: } Chris@1382: Chris@1382: void Chris@284: SpectrumLayer::setShowPeaks(bool show) Chris@284: { Chris@284: if (m_showPeaks == show) return; Chris@284: m_showPeaks = show; Chris@284: emit layerParametersChanged(); Chris@284: } Chris@284: Chris@284: void Chris@153: SpectrumLayer::preferenceChanged(PropertyContainer::PropertyName name) Chris@153: { Chris@153: if (name == "Window Type") { Chris@1382: auto type = Preferences::getInstance()->getWindowType(); Chris@1382: SVDEBUG << "SpectrumLayer::preferenceChanged: Window type changed to " Chris@1382: << type << endl; Chris@1382: setWindowType(type); Chris@153: return; Chris@153: } Chris@153: } Chris@153: Chris@1403: bool Chris@1403: SpectrumLayer::setDisplayExtents(double min, double max) Chris@1403: { Chris@1403: bool result = SliceLayer::setDisplayExtents(min, max); Chris@1403: if (result) { Chris@1403: m_freqOfMinBin = getFrequencyForBin(m_minbin); Chris@1403: } Chris@1403: return result; Chris@1403: } Chris@1403: Chris@1238: double Chris@1386: SpectrumLayer::getBinForFrequency(double freq) const Chris@1386: { Chris@1473: auto sliceableModel = ModelById::getAs Chris@1473: (m_sliceableModel); Chris@1473: if (!sliceableModel) return 0; Chris@1473: double bin = (freq * getFFTSize()) / sliceableModel->getSampleRate(); Chris@1386: return bin; Chris@1386: } Chris@1386: Chris@1386: double Chris@1386: SpectrumLayer::getBinForX(const LayerGeometryProvider *v, double x) const Chris@1386: { Chris@1473: auto sliceableModel = ModelById::getAs Chris@1473: (m_sliceableModel); Chris@1473: if (!sliceableModel) return 0; Chris@1386: double bin = getBinForFrequency(getFrequencyForX(v, x)); Chris@1386: return bin; Chris@1386: } Chris@1386: Chris@1386: double Chris@1238: SpectrumLayer::getFrequencyForX(const LayerGeometryProvider *v, double x) const Chris@133: { Chris@1473: auto sliceableModel = ModelById::getAs Chris@1473: (m_sliceableModel); Chris@1473: if (!sliceableModel) return 0; Chris@1394: Chris@1394: double fmin = getFrequencyForBin(m_minbin); Chris@1394: double fmax = getFrequencyForBin(m_maxbin); Chris@1394: Chris@1394: double freq = getScalePointForX(v, x, fmin, fmax); Chris@1386: return freq; Chris@1386: } Chris@1386: Chris@1386: double Chris@1386: SpectrumLayer::getFrequencyForBin(double bin) const Chris@1386: { Chris@1473: auto sliceableModel = ModelById::getAs Chris@1473: (m_sliceableModel); Chris@1473: if (!sliceableModel) return 0; Chris@1473: double freq = (bin * sliceableModel->getSampleRate()) / getFFTSize(); Chris@1386: return freq; Chris@1386: } Chris@1386: Chris@1386: double Chris@1386: SpectrumLayer::getXForBin(const LayerGeometryProvider *v, double bin) const Chris@1386: { Chris@1473: auto sliceableModel = ModelById::getAs Chris@1473: (m_sliceableModel); Chris@1473: if (!sliceableModel) return 0; Chris@1386: double x = getXForFrequency(v, getFrequencyForBin(bin)); Chris@1386: return x; Chris@133: } Chris@133: Chris@908: double Chris@1238: SpectrumLayer::getXForFrequency(const LayerGeometryProvider *v, double freq) const Chris@265: { Chris@1473: auto sliceableModel = ModelById::getAs Chris@1473: (m_sliceableModel); Chris@1473: if (!sliceableModel) return 0; Chris@1394: Chris@1394: double fmin = getFrequencyForBin(m_minbin); Chris@1394: double fmax = getFrequencyForBin(m_maxbin); Chris@1394: double x = getXForScalePoint(v, freq, fmin, fmax); Chris@1399: Chris@1386: return x; Chris@254: } Chris@254: Chris@260: bool Chris@918: SpectrumLayer::getXScaleValue(const LayerGeometryProvider *v, int x, Chris@908: double &value, QString &unit) const Chris@260: { Chris@1238: value = getFrequencyForX(v, x); Chris@260: unit = "Hz"; Chris@260: return true; Chris@260: } Chris@260: Chris@264: bool Chris@918: SpectrumLayer::getYScaleValue(const LayerGeometryProvider *v, int y, Chris@908: double &value, QString &unit) const Chris@274: { Chris@1238: value = getValueForY(v, y); Chris@274: Chris@274: if (m_energyScale == dBScale || m_energyScale == MeterScale) { Chris@274: Chris@908: if (value > 0.0) { Chris@908: value = 10.0 * log10(value); Chris@1473: if (value < m_threshold) { Chris@1473: value = m_threshold; Chris@1473: } Chris@1473: } else { Chris@1473: value = m_threshold; Chris@1473: } Chris@274: Chris@274: unit = "dBV"; Chris@274: Chris@274: } else { Chris@274: unit = "V"; Chris@274: } Chris@274: Chris@274: return true; Chris@274: } Chris@274: Chris@274: bool Chris@918: SpectrumLayer::getYScaleDifference(const LayerGeometryProvider *v, int y0, int y1, Chris@908: double &diff, QString &unit) const Chris@274: { Chris@274: bool rv = SliceLayer::getYScaleDifference(v, y0, y1, diff, unit); Chris@274: if (rv && (unit == "dBV")) unit = "dB"; Chris@274: return rv; Chris@274: } Chris@274: Chris@274: Chris@274: bool Chris@918: SpectrumLayer::getCrosshairExtents(LayerGeometryProvider *v, QPainter &paint, Chris@264: QPoint cursorPos, Chris@264: std::vector &extents) const Chris@264: { Chris@918: QRect vertical(cursorPos.x(), cursorPos.y(), 1, v->getPaintHeight() - cursorPos.y()); Chris@264: extents.push_back(vertical); Chris@264: Chris@918: QRect horizontal(0, cursorPos.y(), v->getPaintWidth(), 12); Chris@264: extents.push_back(horizontal); Chris@264: Chris@280: int hoffset = 2; Chris@280: if (m_binScale == LogBins) hoffset = 13; Chris@278: Chris@607: int sw = getVerticalScaleWidth(v, false, paint); Chris@280: 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@280: QRect value(sw, cursorPos.y() - paint.fontMetrics().ascent() - 2, Chris@280: paint.fontMetrics().width("0.0000001 V") + 2, Chris@264: paint.fontMetrics().height()); Chris@280: extents.push_back(value); Chris@280: Chris@280: QRect log(sw, cursorPos.y() + 2, Chris@280: paint.fontMetrics().width("-80.000 dBV") + 2, Chris@280: paint.fontMetrics().height()); Chris@280: extents.push_back(log); Chris@280: Chris@280: QRect freq(cursorPos.x(), Chris@918: v->getPaintHeight() - paint.fontMetrics().height() - hoffset, Chris@280: paint.fontMetrics().width("123456 Hz") + 2, Chris@280: paint.fontMetrics().height()); Chris@280: extents.push_back(freq); Chris@264: Chris@278: int w(paint.fontMetrics().width("C#10+50c") + 2); Chris@278: QRect pitch(cursorPos.x() - w, Chris@918: v->getPaintHeight() - paint.fontMetrics().height() - hoffset, Chris@278: w, Chris@278: paint.fontMetrics().height()); Chris@278: extents.push_back(pitch); Chris@278: Chris@264: return true; Chris@264: } Chris@264: Chris@254: void Chris@918: SpectrumLayer::paintCrosshairs(LayerGeometryProvider *v, QPainter &paint, Chris@254: QPoint cursorPos) const Chris@254: { Chris@1473: auto sliceableModel = ModelById::getAs Chris@1473: (m_sliceableModel); Chris@1473: if (!sliceableModel) return; Chris@280: Chris@254: paint.save(); 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@254: Chris@1362: ColourMapper mapper(m_colourMap, m_colourInverted, 0, 1); Chris@254: paint.setPen(mapper.getContrastingColour()); Chris@254: Chris@1238: int xorigin = m_xorigins[v->getId()]; Chris@918: paint.drawLine(xorigin, cursorPos.y(), v->getPaintWidth(), cursorPos.y()); Chris@918: paint.drawLine(cursorPos.x(), cursorPos.y(), cursorPos.x(), v->getPaintHeight()); Chris@254: Chris@1238: double fundamental = getFrequencyForX(v, cursorPos.x()); Chris@254: Chris@1392: int hoffset = getHorizontalScaleHeight(v, paint) + Chris@1392: 2 * paint.fontMetrics().height(); Chris@278: Chris@1078: PaintAssistant::drawVisibleText(v, paint, Chris@1238: cursorPos.x() + 2, Chris@1238: v->getPaintHeight() - 2 - hoffset, Chris@1392: tr("%1 Hz").arg(fundamental), Chris@1238: PaintAssistant::OutlinedText); Chris@278: Chris@278: if (Pitch::isFrequencyInMidiRange(fundamental)) { Chris@278: QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental); Chris@1078: PaintAssistant::drawVisibleText(v, paint, Chris@1238: cursorPos.x() - Chris@1238: paint.fontMetrics().width(pitchLabel) - 2, Chris@1238: v->getPaintHeight() - 2 - hoffset, Chris@1238: pitchLabel, Chris@1238: PaintAssistant::OutlinedText); Chris@278: } Chris@264: Chris@1238: double value = getValueForY(v, cursorPos.y()); Chris@280: Chris@1078: PaintAssistant::drawVisibleText(v, paint, Chris@280: xorigin + 2, Chris@280: cursorPos.y() - 2, Chris@280: QString("%1 V").arg(value), Chris@1078: PaintAssistant::OutlinedText); Chris@280: Chris@1392: if (value > m_threshold) { Chris@1392: double db = 10.0 * log10(value); Chris@1392: PaintAssistant::drawVisibleText(v, paint, Chris@1392: xorigin + 2, Chris@1392: cursorPos.y() + 2 + Chris@1392: paint.fontMetrics().ascent(), Chris@1392: QString("%1 dBV").arg(db), Chris@1392: PaintAssistant::OutlinedText); Chris@1392: } Chris@280: Chris@254: int harmonic = 2; Chris@254: Chris@254: while (harmonic < 100) { Chris@254: Chris@1238: int hx = int(lrint(getXForFrequency(v, fundamental * harmonic))); Chris@254: Chris@918: if (hx < xorigin || hx > v->getPaintWidth()) break; Chris@254: Chris@254: int len = 7; Chris@254: Chris@254: if (harmonic % 2 == 0) { Chris@254: if (harmonic % 4 == 0) { Chris@254: len = 12; Chris@254: } else { Chris@254: len = 10; Chris@254: } Chris@254: } Chris@254: Chris@908: paint.drawLine(hx, Chris@254: cursorPos.y(), Chris@908: hx, Chris@254: cursorPos.y() + len); Chris@254: Chris@254: ++harmonic; Chris@254: } Chris@254: Chris@254: paint.restore(); Chris@254: } Chris@254: Chris@199: QString Chris@918: SpectrumLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &p) const Chris@199: { Chris@1473: auto sliceableModel = ModelById::getAs Chris@1473: (m_sliceableModel); Chris@1473: if (!sliceableModel) return ""; Chris@199: Chris@199: int minbin = 0, maxbin = 0, range = 0; Chris@805: QString genericDesc = SliceLayer::getFeatureDescriptionAux Chris@199: (v, p, false, minbin, maxbin, range); Chris@199: Chris@199: if (genericDesc == "") return ""; Chris@199: Chris@1238: int i0 = minbin - m_minbin; Chris@1238: int i1 = maxbin - m_minbin; Chris@1238: Chris@1238: float minvalue = 0.0; Chris@1238: if (in_range_for(m_values, i0)) minvalue = m_values[i0]; Chris@199: Chris@1238: float maxvalue = minvalue; Chris@1238: if (in_range_for(m_values, i1)) maxvalue = m_values[i1]; Chris@1238: Chris@199: if (minvalue > maxvalue) std::swap(minvalue, maxvalue); Chris@199: Chris@199: QString binstr; Chris@199: QString hzstr; Chris@1473: int minfreq = int(lrint((minbin * sliceableModel->getSampleRate()) / Chris@1382: getFFTSize())); Chris@1256: int maxfreq = int(lrint((std::max(maxbin, minbin) Chris@1473: * sliceableModel->getSampleRate()) / Chris@1382: getFFTSize())); Chris@199: Chris@199: if (maxbin != minbin) { Chris@199: binstr = tr("%1 - %2").arg(minbin+1).arg(maxbin+1); Chris@199: } else { Chris@199: binstr = QString("%1").arg(minbin+1); Chris@199: } Chris@199: if (minfreq != maxfreq) { Chris@199: hzstr = tr("%1 - %2 Hz").arg(minfreq).arg(maxfreq); Chris@199: } else { Chris@199: hzstr = tr("%1 Hz").arg(minfreq); Chris@199: } Chris@199: Chris@199: QString valuestr; Chris@199: if (maxvalue != minvalue) { Chris@199: valuestr = tr("%1 - %2").arg(minvalue).arg(maxvalue); Chris@199: } else { Chris@199: valuestr = QString("%1").arg(minvalue); Chris@199: } Chris@199: Chris@199: QString dbstr; Chris@908: double mindb = AudioLevel::multiplier_to_dB(minvalue); Chris@908: double maxdb = AudioLevel::multiplier_to_dB(maxvalue); Chris@199: QString mindbstr; Chris@199: QString maxdbstr; Chris@199: if (mindb == AudioLevel::DB_FLOOR) { Chris@1147: mindbstr = Strings::minus_infinity; Chris@199: } else { Chris@908: mindbstr = QString("%1").arg(lrint(mindb)); Chris@199: } Chris@199: if (maxdb == AudioLevel::DB_FLOOR) { Chris@1147: maxdbstr = Strings::minus_infinity; Chris@199: } else { Chris@908: maxdbstr = QString("%1").arg(lrint(maxdb)); Chris@199: } Chris@908: if (lrint(mindb) != lrint(maxdb)) { Chris@199: dbstr = tr("%1 - %2").arg(mindbstr).arg(maxdbstr); Chris@199: } else { Chris@199: dbstr = tr("%1").arg(mindbstr); Chris@199: } Chris@199: Chris@199: QString description; Chris@199: Chris@1473: if (range > int(sliceableModel->getResolution())) { Chris@199: description = tr("%1\nBin:\t%2 (%3)\n%4 value:\t%5\ndB:\t%6") Chris@199: .arg(genericDesc) Chris@199: .arg(binstr) Chris@199: .arg(hzstr) Chris@199: .arg(m_samplingMode == NearestSample ? tr("First") : Chris@199: m_samplingMode == SampleMean ? tr("Mean") : tr("Peak")) Chris@199: .arg(valuestr) Chris@199: .arg(dbstr); Chris@199: } else { Chris@199: description = tr("%1\nBin:\t%2 (%3)\nValue:\t%4\ndB:\t%5") Chris@199: .arg(genericDesc) Chris@199: .arg(binstr) Chris@199: .arg(hzstr) Chris@199: .arg(valuestr) Chris@199: .arg(dbstr); Chris@199: } Chris@199: Chris@199: return description; Chris@199: } Chris@199: Chris@254: void Chris@916: SpectrumLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const Chris@275: { Chris@1473: auto originModel = ModelById::get(m_originModel); Chris@1473: if (!originModel || !originModel->isOK() || !originModel->isReady()) { Chris@587: SVDEBUG << "SpectrumLayer::paint: no origin model, or origin model not OK or not ready" << endl; Chris@349: return; Chris@349: } Chris@275: Chris@275: if (m_newFFTNeeded) { Chris@587: SVDEBUG << "SpectrumLayer::paint: new FFT needed, calling setupFFT" << endl; Chris@275: const_cast(this)->setupFFT(); //ugh Chris@275: } Chris@277: Chris@1473: auto fft = ModelById::getAs(m_sliceableModel); Chris@1473: if (!fft) return; Chris@277: Chris@1382: double thresh = (pow(10, -6) / m_gain) * (getFFTSize() / 2.0); // -60dB adj Chris@277: Chris@607: int xorigin = getVerticalScaleWidth(v, false, paint) + 1; Chris@1281: int scaleHeight = getHorizontalScaleHeight(v, paint); Chris@345: Chris@1391: QPoint localPos; Chris@1391: bool shouldIlluminate = v->shouldIlluminateLocalFeatures(this, localPos); Chris@1391: Chris@1398: int illuminateX = 0; Chris@1398: double illuminateFreq = 0.0; Chris@1398: double illuminateLevel = 0.0; Chris@1398: Chris@1398: ColourMapper mapper = Chris@1398: hasLightBackground() ? Chris@1398: ColourMapper(ColourMapper::BlackOnWhite, m_colourInverted, 0, 1) : Chris@1398: ColourMapper(ColourMapper::WhiteOnBlack, m_colourInverted, 0, 1); Chris@1398: Chris@1392: // cerr << "shouldIlluminate = " << shouldIlluminate << ", localPos = " << localPos.x() << "," << localPos.y() << endl; Chris@1392: Chris@284: if (fft && m_showPeaks) { Chris@277: Chris@277: // draw peak lines Chris@277: Chris@908: int col = int(v->getCentreFrame() / fft->getResolution()); Chris@277: Chris@277: paint.save(); Chris@277: paint.setRenderHint(QPainter::Antialiasing, false); Chris@1281: Chris@290: int peakminbin = 0; Chris@290: int peakmaxbin = fft->getHeight() - 1; Chris@908: double peakmaxfreq = Pitch::getFrequencyForPitch(128); Chris@1387: peakmaxbin = int(((peakmaxfreq * fft->getHeight() * 2) / Chris@1387: fft->getSampleRate())); Chris@290: Chris@280: FFTModel::PeakSet peaks = fft->getPeakFrequencies Chris@290: (FFTModel::MajorPitchAdaptivePeaks, col, peakminbin, peakmaxbin); Chris@280: Chris@277: BiasCurve curve; Chris@277: getBiasCurve(curve); Chris@908: int cs = int(curve.size()); Chris@280: Chris@1387: int px = -1; Chris@1387: Chris@1392: int fuzz = ViewManager::scalePixelSize(3); Chris@1391: Chris@280: for (FFTModel::PeakSet::iterator i = peaks.begin(); Chris@280: i != peaks.end(); ++i) { Chris@280: Chris@1387: double freq = i->second; Chris@1387: int x = int(lrint(getXForFrequency(v, freq))); Chris@1387: if (x == px) { Chris@1387: continue; Chris@1387: } Chris@1391: Chris@805: int bin = i->first; Chris@277: Chris@682: // cerr << "bin = " << bin << ", thresh = " << thresh << ", value = " << fft->getMagnitudeAt(col, bin) << endl; Chris@280: Chris@1385: double value = fft->getValueAt(col, bin); Chris@1385: if (value < thresh) continue; Chris@1385: if (bin < cs) value *= curve[bin]; Chris@1392: Chris@1392: double norm = 0.f; Chris@1396: // we only need the norm here, for the colour map Chris@1396: (void)getYForValue(v, value, norm); Chris@1391: Chris@1392: QColor colour = mapper.map(norm); Chris@1392: Chris@1392: paint.setPen(QPen(colour, 1)); Chris@1392: paint.drawLine(x, 0, x, v->getPaintHeight() - scaleHeight - 1); Chris@1392: Chris@1398: if (shouldIlluminate && std::abs(localPos.x() - x) <= fuzz) { Chris@1398: illuminateX = x; Chris@1398: illuminateFreq = freq; Chris@1398: illuminateLevel = norm; Chris@1391: } Chris@277: Chris@1387: px = x; Chris@277: } Chris@277: Chris@277: paint.restore(); Chris@277: } Chris@275: Chris@1281: paint.save(); Chris@1281: Chris@275: SliceLayer::paint(v, paint, rect); Chris@1281: Chris@1281: paintHorizontalScale(v, paint, xorigin); Chris@277: Chris@1281: paint.restore(); Chris@1398: Chris@1398: if (illuminateFreq > 0.0) { Chris@1398: Chris@1398: QColor colour = mapper.map(illuminateLevel); Chris@1398: paint.setPen(QPen(colour, 1)); Chris@1398: Chris@1398: int labelY = v->getPaintHeight() - Chris@1398: getHorizontalScaleHeight(v, paint) - Chris@1398: paint.fontMetrics().height() * 4; Chris@1398: Chris@1398: QString text = tr("%1 Hz").arg(illuminateFreq); Chris@1398: int lw = paint.fontMetrics().width(text); Chris@1398: Chris@1398: int gap = ViewManager::scalePixelSize(v->getXForViewX(3)); Chris@1398: double half = double(gap)/2.0; Chris@1398: Chris@1398: int labelX = illuminateX - lw - gap; Chris@1398: if (labelX < getVerticalScaleWidth(v, false, paint)) { Chris@1398: labelX = illuminateX + gap; Chris@1398: } Chris@1398: Chris@1398: PaintAssistant::drawVisibleText Chris@1398: (v, paint, labelX, labelY, Chris@1398: text, PaintAssistant::OutlinedText); Chris@1398: Chris@1398: if (Pitch::isFrequencyInMidiRange(illuminateFreq)) { Chris@1398: QString pitchLabel = Pitch::getPitchLabelForFrequency Chris@1398: (illuminateFreq); Chris@1398: PaintAssistant::drawVisibleText Chris@1398: (v, paint, Chris@1398: labelX, labelY + paint.fontMetrics().ascent() + gap, Chris@1398: pitchLabel, PaintAssistant::OutlinedText); Chris@1398: } Chris@1398: paint.fillRect(QRectF(illuminateX - half, labelY + gap, gap, gap), Chris@1398: colour); Chris@1398: } Chris@1281: } Chris@1281: Chris@1281: int Chris@1281: SpectrumLayer::getHorizontalScaleHeight(LayerGeometryProvider *v, Chris@1281: QPainter &paint) const Chris@1281: { Chris@1281: int pkh = int(paint.fontMetrics().height() * 0.7 + 0.5); Chris@1281: if (pkh < 10) pkh = 10; Chris@1281: Chris@1281: int scaleh = HorizontalFrequencyScale().getHeight(v, paint); Chris@1281: Chris@1281: return pkh + scaleh; Chris@1281: } Chris@1281: Chris@1281: void Chris@1281: SpectrumLayer::paintHorizontalScale(LayerGeometryProvider *v, Chris@1281: QPainter &paint, Chris@1281: int xorigin) const Chris@1281: { Chris@278: //!!! All of this stuff relating to depicting frequencies Chris@1238: // (keyboard, crosshairs etc) should be applicable to any slice Chris@1238: // layer whose model has a vertical scale unit of Hz. However, Chris@1238: // the dense 3d model at the moment doesn't record its vertical Chris@1238: // scale unit -- we need to fix that and hoist this code as Chris@1238: // appropriate. Same really goes for any code in SpectrogramLayer Chris@1238: // that could be relevant to Colour3DPlotLayer with unit Hz, but Chris@1238: // that's a bigger proposition. Chris@278: Chris@1281: if (!v->getViewManager()->shouldShowHorizontalValueScale()) { Chris@1281: return; Chris@1281: } Chris@1281: Chris@1281: int totalScaleHeight = getHorizontalScaleHeight(v, paint); // inc piano Chris@1281: int freqScaleHeight = HorizontalFrequencyScale().getHeight(v, paint); Chris@1281: int paintHeight = v->getPaintHeight(); Chris@1281: int paintWidth = v->getPaintWidth(); Chris@277: Chris@1238: PianoScale().paintPianoHorizontal Chris@1276: (v, this, paint, Chris@1281: QRect(xorigin, paintHeight - totalScaleHeight - 1, Chris@1281: paintWidth - 1, totalScaleHeight - freqScaleHeight)); Chris@345: Chris@1281: int scaleLeft = int(getXForBin(v, 1)); Chris@1281: Chris@1281: paint.drawLine(int(getXForBin(v, 0)), paintHeight - freqScaleHeight, Chris@1281: scaleLeft, paintHeight - freqScaleHeight); Chris@1281: Chris@1281: QString hz = tr("Hz"); Chris@1281: int hzw = paint.fontMetrics().width(hz); Chris@1281: if (scaleLeft > hzw + 5) { Chris@1281: paint.drawText Chris@1281: (scaleLeft - hzw - 5, Chris@1281: paintHeight - freqScaleHeight + paint.fontMetrics().ascent() + 5, Chris@1281: hz); Chris@1281: } Chris@1281: Chris@1281: HorizontalFrequencyScale().paintScale Chris@1276: (v, this, paint, Chris@1281: QRect(scaleLeft, paintHeight - freqScaleHeight, Chris@1281: paintWidth, totalScaleHeight), Chris@1281: m_binScale == LogBins); Chris@275: } Chris@275: Chris@275: void Chris@254: SpectrumLayer::getBiasCurve(BiasCurve &curve) const Chris@254: { Chris@254: curve = m_biasCurve; Chris@254: } Chris@199: Chris@316: void Chris@316: SpectrumLayer::toXml(QTextStream &stream, Chris@316: QString indent, QString extraAttributes) const Chris@220: { Chris@316: QString s = QString("windowSize=\"%1\" " Chris@456: "windowHopLevel=\"%2\" " Chris@1382: "oversampling=\"%3\" " Chris@1382: "showPeaks=\"%4\" ") Chris@220: .arg(m_windowSize) Chris@456: .arg(m_windowHopLevel) Chris@1382: .arg(m_oversampling) Chris@456: .arg(m_showPeaks ? "true" : "false"); Chris@220: Chris@316: SliceLayer::toXml(stream, indent, extraAttributes + " " + s); Chris@220: } Chris@220: Chris@220: void Chris@220: SpectrumLayer::setProperties(const QXmlAttributes &attributes) Chris@220: { Chris@220: SliceLayer::setProperties(attributes); Chris@220: Chris@220: bool ok = false; Chris@220: Chris@805: int windowSize = attributes.value("windowSize").toUInt(&ok); Chris@220: if (ok) setWindowSize(windowSize); Chris@220: Chris@805: int windowHopLevel = attributes.value("windowHopLevel").toUInt(&ok); Chris@220: if (ok) setWindowHopLevel(windowHopLevel); Chris@456: Chris@1382: int oversampling = attributes.value("oversampling").toUInt(&ok); Chris@1382: if (ok) setOversampling(oversampling); Chris@1382: Chris@456: bool showPeaks = (attributes.value("showPeaks").trimmed() == "true"); Chris@456: setShowPeaks(showPeaks); Chris@220: } Chris@220: Chris@220: