diff layer/SpectrumLayer.cpp @ 1395:32bbb86094c3

Merge from branch spectrogramparam
author Chris Cannam
date Wed, 14 Nov 2018 14:23:17 +0000
parents 4a36f6130056
children 2e316a724336
line wrap: on
line diff
--- a/layer/SpectrumLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/SpectrumLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -39,6 +39,7 @@
     m_windowSize(4096),
     m_windowType(HanningWindow),
     m_windowHopLevel(3),
+    m_oversampling(1),
     m_showPeaks(false),
     m_newFFTNeeded(true)
 {
@@ -112,18 +113,20 @@
         return;
     }
 
+    int fftSize = getFFTSize();
+
     FFTModel *newFFT = new FFTModel(m_originModel,
                                     m_channel,
                                     m_windowType,
                                     m_windowSize,
                                     getWindowIncrement(),
-                                    m_windowSize);
+                                    fftSize);
 
     setSliceableModel(newFFT);
 
     m_biasCurve.clear();
-    for (int i = 0; i < m_windowSize; ++i) {
-        m_biasCurve.push_back(1.f / (float(m_windowSize)/2.f));
+    for (int i = 0; i < fftSize; ++i) {
+        m_biasCurve.push_back(1.f / (float(fftSize)/2.f));
     }
 
     m_newFFTNeeded = false;
@@ -135,6 +138,7 @@
     PropertyList list = SliceLayer::getProperties();
     list.push_back("Window Size");
     list.push_back("Window Increment");
+    list.push_back("Oversampling");
     list.push_back("Show Peak Frequencies");
     return list;
 }
@@ -144,6 +148,7 @@
 {
     if (name == "Window Size") return tr("Window Size");
     if (name == "Window Increment") return tr("Window Overlap");
+    if (name == "Oversampling") return tr("Oversampling");
     if (name == "Show Peak Frequencies") return tr("Show Peak Frequencies");
     return SliceLayer::getPropertyLabel(name);
 }
@@ -160,6 +165,7 @@
 {
     if (name == "Window Size") return ValueProperty;
     if (name == "Window Increment") return ValueProperty;
+    if (name == "Oversampling") return ValueProperty;
     if (name == "Show Peak Frequencies") return ToggleProperty;
     return SliceLayer::getPropertyType(name);
 }
@@ -168,7 +174,8 @@
 SpectrumLayer::getPropertyGroupName(const PropertyName &name) const
 {
     if (name == "Window Size" ||
-        name == "Window Increment") return tr("Window");
+        name == "Window Increment" ||
+        name == "Oversampling") return tr("Window");
     if (name == "Show Peak Frequencies") return tr("Bins");
     return SliceLayer::getPropertyGroupName(name);
 }
@@ -202,6 +209,16 @@
         
         val = m_windowHopLevel;
     
+    } else if (name == "Oversampling") {
+
+        *min = 0;
+        *max = 3;
+        *deflt = 0;
+
+        val = 0;
+        int ov = m_oversampling;
+        while (ov > 1) { ov >>= 1; val ++; }
+        
     } else if (name == "Show Peak Frequencies") {
 
         return m_showPeaks ? 1 : 0;
@@ -232,6 +249,15 @@
         case 5: return tr("93.75 %");
         }
     }
+    if (name == "Oversampling") {
+        switch (value) {
+        default:
+        case 0: return tr("1x");
+        case 1: return tr("2x");
+        case 2: return tr("4x");
+        case 3: return tr("8x");
+        }
+    }
     return SliceLayer::getPropertyValueLabel(name, value);
 }
 
@@ -248,6 +274,8 @@
         setWindowSize(32 << value);
     } else if (name == "Window Increment") {
         setWindowHopLevel(value);
+    } else if (name == "Oversampling") {
+        setOversampling(1 << value);
     } else if (name == "Show Peak Frequencies") {
         setShowPeaks(value ? true : false);
     } else {
@@ -259,6 +287,16 @@
 SpectrumLayer::setWindowSize(int ws)
 {
     if (m_windowSize == ws) return;
+
+    SVDEBUG << "setWindowSize: from " << m_windowSize
+            << " to " << ws << ": updating min and max bins from "
+            << m_minbin << " and " << m_maxbin << " to ";
+    
+    m_minbin = int(round((double(m_minbin) / m_windowSize) * ws));
+    m_maxbin = int(round((double(m_maxbin) / m_windowSize) * ws));
+
+    SVDEBUG << m_minbin << " and " << m_maxbin << endl;
+
     m_windowSize = ws;
     m_newFFTNeeded = true;
     emit layerParametersChanged();
@@ -283,6 +321,32 @@
 }
 
 void
+SpectrumLayer::setOversampling(int oversampling)
+{
+    if (m_oversampling == oversampling) return;
+
+    SVDEBUG << "setOversampling: from " << m_oversampling
+            << " to " << oversampling << ": updating min and max bins from "
+            << m_minbin << " and " << m_maxbin << " to ";
+    
+    m_minbin = int(round((double(m_minbin) / m_oversampling) * oversampling));
+    m_maxbin = int(round((double(m_maxbin) / m_oversampling) * oversampling));
+
+    SVDEBUG << m_minbin << " and " << m_maxbin << endl;
+    
+    m_oversampling = oversampling;
+    m_newFFTNeeded = true;
+    
+    emit layerParametersChanged();
+}
+
+int
+SpectrumLayer::getOversampling() const
+{
+    return m_oversampling;
+}
+
+void
 SpectrumLayer::setShowPeaks(bool show)
 {
     if (m_showPeaks == show) return;
@@ -294,32 +358,91 @@
 SpectrumLayer::preferenceChanged(PropertyContainer::PropertyName name)
 {
     if (name == "Window Type") {
-        setWindowType(Preferences::getInstance()->getWindowType());
+        auto type = Preferences::getInstance()->getWindowType();
+        SVDEBUG << "SpectrumLayer::preferenceChanged: Window type changed to "
+                << type << endl;
+        setWindowType(type);
         return;
     }
 }
 
 double
+SpectrumLayer::getBinForFrequency(double freq) const
+{
+    if (!m_sliceableModel) return 0;
+    double bin = (freq * getFFTSize()) / m_sliceableModel->getSampleRate();
+    // we assume the frequency of a bin corresponds to the centre of
+    // its visual range
+    bin += 0.5;
+    return bin;
+}
+
+double
+SpectrumLayer::getBinForX(const LayerGeometryProvider *v, double x) const
+{
+    if (!m_sliceableModel) return 0;
+    double bin = getBinForFrequency(getFrequencyForX(v, x));
+    return bin;
+}
+
+double
 SpectrumLayer::getFrequencyForX(const LayerGeometryProvider *v, double x) const
 {
     if (!m_sliceableModel) return 0;
-    double bin = getBinForX(v, x);
+
+    double fmin = getFrequencyForBin(m_minbin);
+
+    if (m_binScale == LogBins && m_minbin == 0) {
+        // Avoid too much space going to the first bin, but do so in a
+        // way that usually avoids us shifting left/right as the
+        // window size or oversampling ratio change - i.e. base this
+        // on frequency rather than bin number unless we have a lot of
+        // very low-resolution content
+        fmin = getFrequencyForBin(0.8);
+        if (fmin > 6.0) fmin = 6.0;
+    }
+    
+    double fmax = getFrequencyForBin(m_maxbin);
+
+    double freq = getScalePointForX(v, x, fmin, fmax);
+    return freq;
+}
+
+double
+SpectrumLayer::getFrequencyForBin(double bin) const
+{
+    if (!m_sliceableModel) return 0;
     // we assume the frequency of a bin corresponds to the centre of
     // its visual range
     bin -= 0.5;
-    return (m_sliceableModel->getSampleRate() * bin) /
-        (m_sliceableModel->getHeight() * 2);
+    double freq = (bin * m_sliceableModel->getSampleRate()) / getFFTSize();
+    return freq;
+}
+
+double
+SpectrumLayer::getXForBin(const LayerGeometryProvider *v, double bin) const
+{
+    if (!m_sliceableModel) return 0;
+    double x = getXForFrequency(v, getFrequencyForBin(bin));
+    return x;
 }
 
 double
 SpectrumLayer::getXForFrequency(const LayerGeometryProvider *v, double freq) const
 {
     if (!m_sliceableModel) return 0;
-    double bin = (freq * m_sliceableModel->getHeight() * 2) /
-        m_sliceableModel->getSampleRate();
-    // we want the centre of the bin range
-    bin += 0.5;
-    return getXForBin(v, bin);
+
+    double fmin = getFrequencyForBin(m_minbin);
+    if (m_binScale == LogBins && m_minbin == 0) {
+        // See comment in getFrequencyForX above
+        fmin = getFrequencyForBin(0.8);
+        if (fmin > 6.0) fmin = 6.0;
+    }
+    
+    double fmax = getFrequencyForBin(m_maxbin);
+
+    double x = getXForScalePoint(v, freq, fmin, fmax);
+    return x;
 }
 
 bool
@@ -427,13 +550,13 @@
     
     double fundamental = getFrequencyForX(v, cursorPos.x());
 
-    int hoffset = 2;
-    if (m_binScale == LogBins) hoffset = 13;
+    int hoffset = getHorizontalScaleHeight(v, paint) +
+        2 * paint.fontMetrics().height();
 
     PaintAssistant::drawVisibleText(v, paint,
                                     cursorPos.x() + 2,
                                     v->getPaintHeight() - 2 - hoffset,
-                                    QString("%1 Hz").arg(fundamental),
+                                    tr("%1 Hz").arg(fundamental),
                                     PaintAssistant::OutlinedText);
 
     if (Pitch::isFrequencyInMidiRange(fundamental)) {
@@ -447,10 +570,6 @@
     }
 
     double value = getValueForY(v, cursorPos.y());
-    double thresh = m_threshold;
-    double db = thresh;
-    if (value > 0.0) db = 10.0 * log10(value);
-    if (db < thresh) db = thresh;
 
     PaintAssistant::drawVisibleText(v, paint,
                        xorigin + 2,
@@ -458,11 +577,15 @@
                        QString("%1 V").arg(value),
                        PaintAssistant::OutlinedText);
 
-    PaintAssistant::drawVisibleText(v, paint,
-                       xorigin + 2,
-                       cursorPos.y() + 2 + paint.fontMetrics().ascent(),
-                       QString("%1 dBV").arg(db),
-                       PaintAssistant::OutlinedText);
+    if (value > m_threshold) {
+        double db = 10.0 * log10(value);
+        PaintAssistant::drawVisibleText(v, paint,
+                                        xorigin + 2,
+                                        cursorPos.y() + 2 +
+                                        paint.fontMetrics().ascent(),
+                                        QString("%1 dBV").arg(db),
+                                        PaintAssistant::OutlinedText);
+    }
     
     int harmonic = 2;
 
@@ -518,10 +641,10 @@
     QString binstr;
     QString hzstr;
     int minfreq = int(lrint((minbin * m_sliceableModel->getSampleRate()) /
-                            m_windowSize));
+                            getFFTSize()));
     int maxfreq = int(lrint((std::max(maxbin, minbin)
                              * m_sliceableModel->getSampleRate()) /
-                            m_windowSize));
+                            getFFTSize()));
 
     if (maxbin != minbin) {
         binstr = tr("%1 - %2").arg(minbin+1).arg(maxbin+1);
@@ -602,11 +725,16 @@
     FFTModel *fft = dynamic_cast<FFTModel *>
         (const_cast<DenseThreeDimensionalModel *>(m_sliceableModel));
 
-    double thresh = (pow(10, -6) / m_gain) * (m_windowSize / 2.0); // -60dB adj
+    double thresh = (pow(10, -6) / m_gain) * (getFFTSize() / 2.0); // -60dB adj
 
     int xorigin = getVerticalScaleWidth(v, false, paint) + 1;
     int scaleHeight = getHorizontalScaleHeight(v, paint);
 
+    QPoint localPos;
+    bool shouldIlluminate = v->shouldIlluminateLocalFeatures(this, localPos);
+
+//    cerr << "shouldIlluminate = " << shouldIlluminate << ", localPos = " << localPos.x() << "," << localPos.y() << endl;
+
     if (fft && m_showPeaks) {
 
         // draw peak lines
@@ -624,7 +752,8 @@
         int peakminbin = 0;
         int peakmaxbin = fft->getHeight() - 1;
         double peakmaxfreq = Pitch::getFrequencyForPitch(128);
-        peakmaxbin = int(((peakmaxfreq * fft->getHeight() * 2) / fft->getSampleRate()));
+        peakmaxbin = int(((peakmaxfreq * fft->getHeight() * 2) /
+                          fft->getSampleRate()));
         
         FFTModel::PeakSet peaks = fft->getPeakFrequencies
             (FFTModel::MajorPitchAdaptivePeaks, col, peakminbin, peakmaxbin);
@@ -633,32 +762,73 @@
         getBiasCurve(curve);
         int cs = int(curve.size());
 
-        std::vector<double> values;
+        int px = -1;
+
+        int fuzz = ViewManager::scalePixelSize(3);
+        bool illuminatedSomething = false;
         
-        for (int bin = 0; bin < fft->getHeight(); ++bin) {
-            double value = m_sliceableModel->getValueAt(col, bin);
-            if (bin < cs) value *= curve[bin];
-            values.push_back(value);
-        }
-
         for (FFTModel::PeakSet::iterator i = peaks.begin();
              i != peaks.end(); ++i) {
 
+            double freq = i->second;
+            int x = int(lrint(getXForFrequency(v, freq)));
+            if (x == px) {
+                continue;
+            }
+            
             int bin = i->first;
             
 //            cerr << "bin = " << bin << ", thresh = " << thresh << ", value = " << fft->getMagnitudeAt(col, bin) << endl;
 
-            if (!fft->isOverThreshold(col, bin, float(thresh))) continue;
+            double value = fft->getValueAt(col, bin);
+            if (value < thresh) continue;
+            if (bin < cs) value *= curve[bin];
             
-            double freq = i->second;
-          
-            int x = int(lrint(getXForFrequency(v, freq)));
+            double norm = 0.f;
+            // we need the norm here for colour map; the y coord is
+            // only used to pick a label height if illuminating the
+            // local point
+            double y = getYForValue(v, value, norm);
 
-            double norm = 0.f;
-            (void)getYForValue(v, values[bin], norm); // don't need return value, need norm
+            QColor colour = mapper.map(norm);
+            
+            paint.setPen(QPen(colour, 1));
+            paint.drawLine(x, 0, x, v->getPaintHeight() - scaleHeight - 1);
 
-            paint.setPen(mapper.map(norm));
-            paint.drawLine(x, 0, x, v->getPaintHeight() - scaleHeight - 1);
+            bool illuminateThis = false;
+            if (shouldIlluminate && !illuminatedSomething &&
+                std::abs(localPos.x() - x) <= fuzz) {
+                illuminateThis = true;
+            }
+
+            if (illuminateThis) {
+                int labelY = v->getPaintHeight() -
+                    getHorizontalScaleHeight(v, paint) -
+                    paint.fontMetrics().height() * 3;
+                QString text = tr("%1 Hz").arg(freq);
+                int lw = paint.fontMetrics().width(text);
+                int gap = ViewManager::scalePixelSize(3);
+                double half = double(gap)/2.0;
+                int labelX = x - lw - gap;
+                if (labelX < getVerticalScaleWidth(v, false, paint)) {
+                    labelX = x + gap;
+                }
+                PaintAssistant::drawVisibleText
+                    (v, paint, labelX, labelY,
+                     text, PaintAssistant::OutlinedText);
+                if (Pitch::isFrequencyInMidiRange(freq)) {
+                    QString pitchLabel = Pitch::getPitchLabelForFrequency(freq);
+                    PaintAssistant::drawVisibleText
+                        (v, paint,
+                         labelX, labelY + paint.fontMetrics().ascent() + gap,
+                         pitchLabel, PaintAssistant::OutlinedText);
+                }
+                paint.fillRect(QRectF(x - half, labelY + gap, gap, gap),
+                               colour);
+                illuminatedSomething = true;
+            }
+            
+            px = x;
         }
 
         paint.restore();
@@ -746,9 +916,11 @@
 {
     QString s = QString("windowSize=\"%1\" "
                         "windowHopLevel=\"%2\" "
-                        "showPeaks=\"%3\" ")
+                        "oversampling=\"%3\" "
+                        "showPeaks=\"%4\" ")
         .arg(m_windowSize)
         .arg(m_windowHopLevel)
+        .arg(m_oversampling)
         .arg(m_showPeaks ? "true" : "false");
 
     SliceLayer::toXml(stream, indent, extraAttributes + " " + s);
@@ -767,6 +939,9 @@
     int windowHopLevel = attributes.value("windowHopLevel").toUInt(&ok);
     if (ok) setWindowHopLevel(windowHopLevel);
 
+    int oversampling = attributes.value("oversampling").toUInt(&ok);
+    if (ok) setOversampling(oversampling);
+
     bool showPeaks = (attributes.value("showPeaks").trimmed() == "true");
     setShowPeaks(showPeaks);
 }