diff layer/SpectrogramLayer.cpp @ 0:2a4f26e85b4c

initial import
author Chris Cannam
date Tue, 10 Jan 2006 16:33:16 +0000
parents
children ab83c415a6cd
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/SpectrogramLayer.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,1670 @@
+/* -*- c-basic-offset: 4 -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    A waveform viewer and audio annotation editor.
+    Chris Cannam, Queen Mary University of London, 2005
+    
+    This is experimental software.  Not for distribution.
+*/
+
+#include "SpectrogramLayer.h"
+
+#include "base/View.h"
+#include "base/Profiler.h"
+#include "base/AudioLevel.h"
+#include "base/Window.h"
+
+#include <QPainter>
+#include <QImage>
+#include <QPixmap>
+#include <QRect>
+#include <QTimer>
+
+#include <iostream>
+
+#include <cassert>
+#include <cmath>
+
+//#define DEBUG_SPECTROGRAM_REPAINT 1
+
+
+SpectrogramLayer::SpectrogramLayer(View *w, Configuration config) :
+    Layer(w),
+    m_model(0),
+    m_channel(0),
+    m_windowSize(1024),
+    m_windowType(HanningWindow),
+    m_windowOverlap(50),
+    m_gain(1.0),
+    m_maxFrequency(8000),
+    m_colourScale(dBColourScale),
+    m_colourScheme(DefaultColours),
+    m_frequencyScale(LinearFrequencyScale),
+    m_cache(0),
+    m_cacheInvalid(true),
+    m_maxCachedFrequency(0),
+    m_pixmapCache(0),
+    m_pixmapCacheInvalid(true),
+    m_fillThread(0),
+    m_updateTimer(0),
+    m_lastFillExtent(0),
+    m_exiting(false)
+{
+    if (config == MelodicRange) {
+	setWindowSize(8192);
+	setWindowOverlap(90);
+	setWindowType(ParzenWindow);
+	setMaxFrequency(1000);
+	setColourScale(LinearColourScale);
+    }
+
+    if (m_view) m_view->setLightBackground(false);
+    m_view->addLayer(this);
+}
+
+SpectrogramLayer::~SpectrogramLayer()
+{
+    delete m_updateTimer;
+    m_updateTimer = 0;
+
+    m_exiting = true;
+    m_condition.wakeAll();
+    if (m_fillThread) m_fillThread->wait();
+    delete m_fillThread;
+    
+    delete m_cache;
+}
+
+void
+SpectrogramLayer::setModel(const DenseTimeValueModel *model)
+{
+    m_mutex.lock();
+    m_model = model;
+    m_mutex.unlock();
+
+    if (!m_model || !m_model->isOK()) return;
+
+    connect(m_model, SIGNAL(modelChanged()), this, SIGNAL(modelChanged()));
+    connect(m_model, SIGNAL(modelChanged(size_t, size_t)),
+	    this, SIGNAL(modelChanged(size_t, size_t)));
+
+    connect(m_model, SIGNAL(completionChanged()),
+	    this, SIGNAL(modelCompletionChanged()));
+
+    connect(m_model, SIGNAL(modelChanged()), this, SLOT(cacheInvalid()));
+    connect(m_model, SIGNAL(modelChanged(size_t, size_t)),
+	    this, SLOT(cacheInvalid(size_t, size_t)));
+
+    emit modelReplaced();
+    fillCache();
+}
+
+Layer::PropertyList
+SpectrogramLayer::getProperties() const
+{
+    PropertyList list;
+    list.push_back(tr("Colour"));
+    list.push_back(tr("Colour Scale"));
+    list.push_back(tr("Window Type"));
+    list.push_back(tr("Window Size"));
+    list.push_back(tr("Window Overlap"));
+    list.push_back(tr("Gain"));
+    list.push_back(tr("Max Frequency"));
+    list.push_back(tr("Frequency Scale"));
+    return list;
+}
+
+Layer::PropertyType
+SpectrogramLayer::getPropertyType(const PropertyName &name) const
+{
+    if (name == tr("Gain")) return RangeProperty;
+    return ValueProperty;
+}
+
+QString
+SpectrogramLayer::getPropertyGroupName(const PropertyName &name) const
+{
+    if (name == tr("Window Size") ||
+	name == tr("Window Overlap")) return tr("Window");
+    if (name == tr("Gain") ||
+	name == tr("Colour Scale")) return tr("Scale");
+    if (name == tr("Max Frequency") ||
+	name == tr("Frequency Scale")) return tr("Frequency");
+    return QString();
+}
+
+int
+SpectrogramLayer::getPropertyRangeAndValue(const PropertyName &name,
+					    int *min, int *max) const
+{
+    int deft = 0;
+
+    if (name == tr("Gain")) {
+
+	*min = -50;
+	*max = 50;
+
+	deft = lrint(log10(m_gain) * 20.0);
+	if (deft < *min) deft = *min;
+	if (deft > *max) deft = *max;
+
+    } else if (name == tr("Colour Scale")) {
+
+	*min = 0;
+	*max = 3;
+
+	deft = (int)m_colourScale;
+
+    } else if (name == tr("Colour")) {
+
+	*min = 0;
+	*max = 5;
+
+	deft = (int)m_colourScheme;
+
+    } else if (name == tr("Window Type")) {
+
+	*min = 0;
+	*max = 6;
+
+	deft = (int)m_windowType;
+
+    } else if (name == tr("Window Size")) {
+
+	*min = 0;
+	*max = 10;
+	
+	deft = 0;
+	int ws = m_windowSize;
+	while (ws > 32) { ws >>= 1; deft ++; }
+
+    } else if (name == tr("Window Overlap")) {
+	
+	*min = 0;
+	*max = 4;
+	
+	deft = m_windowOverlap / 25;
+	if (m_windowOverlap == 90) deft = 4;
+    
+    } else if (name == tr("Max Frequency")) {
+
+	*min = 0;
+	*max = 9;
+
+	switch (m_maxFrequency) {
+	case 500: deft = 0; break;
+	case 1000: deft = 1; break;
+	case 1500: deft = 2; break;
+	case 2000: deft = 3; break;
+	case 4000: deft = 4; break;
+	case 6000: deft = 5; break;
+	case 8000: deft = 6; break;
+	case 12000: deft = 7; break;
+	case 16000: deft = 8; break;
+	default: deft = 9; break;
+	}
+
+    } else if (name == tr("Frequency Scale")) {
+
+	*min = 0;
+	*max = 1;
+	deft = (int)m_frequencyScale;
+
+    } else {
+	deft = Layer::getPropertyRangeAndValue(name, min, max);
+    }
+
+    return deft;
+}
+
+QString
+SpectrogramLayer::getPropertyValueLabel(const PropertyName &name,
+				    int value) const
+{
+    if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0: return tr("Default");
+	case 1: return tr("White on Black");
+	case 2: return tr("Black on White");
+	case 3: return tr("Red on Blue");
+	case 4: return tr("Yellow on Black");
+	case 5: return tr("Red on Black");
+	}
+    }
+    if (name == tr("Colour Scale")) {
+	switch (value) {
+	default:
+	case 0: return tr("Level Linear");
+	case 1: return tr("Level Meter");
+	case 2: return tr("Level dB");
+	case 3: return tr("Phase");
+	}
+    }
+    if (name == tr("Window Type")) {
+	switch ((WindowType)value) {
+	default:
+	case RectangularWindow: return tr("Rectangular");
+	case BartlettWindow: return tr("Bartlett");
+	case HammingWindow: return tr("Hamming");
+	case HanningWindow: return tr("Hanning");
+	case BlackmanWindow: return tr("Blackman");
+	case GaussianWindow: return tr("Gaussian");
+	case ParzenWindow: return tr("Parzen");
+	}
+    }
+    if (name == tr("Window Size")) {
+	return QString("%1").arg(32 << value);
+    }
+    if (name == tr("Window Overlap")) {
+	switch (value) {
+	default:
+	case 0: return tr("None");
+	case 1: return tr("25 %");
+	case 2: return tr("50 %");
+	case 3: return tr("75 %");
+	case 4: return tr("90 %");
+	}
+    }
+    if (name == tr("Max Frequency")) {
+	switch (value) {
+	default:
+	case 0: return tr("500 Hz");
+	case 1: return tr("1 KHz");
+	case 2: return tr("1.5 KHz");
+	case 3: return tr("2 KHz");
+	case 4: return tr("4 KHz");
+	case 5: return tr("6 KHz");
+	case 6: return tr("8 KHz");
+	case 7: return tr("12 KHz");
+	case 8: return tr("16 KHz");
+	case 9: return tr("All");
+	}
+    }
+    if (name == tr("Frequency Scale")) {
+	switch (value) {
+	default:
+	case 0: return tr("Linear");
+	case 1: return tr("Log");
+	}
+    }
+    return tr("<unknown>");
+}
+
+void
+SpectrogramLayer::setProperty(const PropertyName &name, int value)
+{
+    if (name == tr("Gain")) {
+	setGain(pow(10, float(value)/20.0));
+    } else if (name == tr("Colour")) {
+	if (m_view) m_view->setLightBackground(value == 2);
+	switch (value) {
+	default:
+	case 0:	setColourScheme(DefaultColours); break;
+	case 1: setColourScheme(WhiteOnBlack); break;
+	case 2: setColourScheme(BlackOnWhite); break;
+	case 3: setColourScheme(RedOnBlue); break;
+	case 4: setColourScheme(YellowOnBlack); break;
+	case 5: setColourScheme(RedOnBlack); break;
+	}
+    } else if (name == tr("Window Type")) {
+	setWindowType(WindowType(value));
+    } else if (name == tr("Window Size")) {
+	setWindowSize(32 << value);
+    } else if (name == tr("Window Overlap")) {
+	if (value == 4) setWindowOverlap(90);
+	else setWindowOverlap(25 * value);
+    } else if (name == tr("Max Frequency")) {
+	switch (value) {
+	case 0: setMaxFrequency(500); break;
+	case 1: setMaxFrequency(1000); break;
+	case 2: setMaxFrequency(1500); break;
+	case 3: setMaxFrequency(2000); break;
+	case 4: setMaxFrequency(4000); break;
+	case 5: setMaxFrequency(6000); break;
+	case 6: setMaxFrequency(8000); break;
+	case 7: setMaxFrequency(12000); break;
+	case 8: setMaxFrequency(16000); break;
+	default:
+	case 9: setMaxFrequency(0); break;
+	}
+    } else if (name == tr("Colour Scale")) {
+	switch (value) {
+	default:
+	case 0: setColourScale(LinearColourScale); break;
+	case 1: setColourScale(MeterColourScale); break;
+	case 2: setColourScale(dBColourScale); break;
+	case 3: setColourScale(PhaseColourScale); break;
+	}
+    } else if (name == tr("Frequency Scale")) {
+	switch (value) {
+	default:
+	case 0: setFrequencyScale(LinearFrequencyScale); break;
+	case 1: setFrequencyScale(LogFrequencyScale); break;
+	}
+    }
+}
+
+void
+SpectrogramLayer::setChannel(int ch)
+{
+    if (m_channel == ch) return;
+
+    m_mutex.lock();
+    m_cacheInvalid = true;
+    m_pixmapCacheInvalid = true;
+    
+    m_channel = ch;
+    emit layerParametersChanged();
+    
+    m_mutex.unlock();
+    fillCache();
+
+}
+
+int
+SpectrogramLayer::getChannel() const
+{
+    return m_channel;
+}
+
+void
+SpectrogramLayer::setWindowSize(size_t ws)
+{
+    if (m_windowSize == ws) return;
+
+    m_mutex.lock();
+    m_cacheInvalid = true;
+    m_pixmapCacheInvalid = true;
+    
+    m_windowSize = ws;
+    emit layerParametersChanged();
+    
+    m_mutex.unlock();
+    fillCache();
+
+}
+
+size_t
+SpectrogramLayer::getWindowSize() const
+{
+    return m_windowSize;
+}
+
+void
+SpectrogramLayer::setWindowOverlap(size_t wi)
+{
+    if (m_windowOverlap == wi) return;
+
+    m_mutex.lock();
+    m_cacheInvalid = true;
+    m_pixmapCacheInvalid = true;
+    
+    m_windowOverlap = wi;
+    emit layerParametersChanged();
+    
+    m_mutex.unlock();
+    fillCache();
+}
+
+size_t
+SpectrogramLayer::getWindowOverlap() const
+{
+    return m_windowOverlap;
+}
+
+void
+SpectrogramLayer::setWindowType(WindowType w)
+{
+    if (m_windowType == w) return;
+
+    m_mutex.lock();
+    m_cacheInvalid = true;
+    m_pixmapCacheInvalid = true;
+    
+    m_windowType = w;
+    emit layerParametersChanged();
+    
+    m_mutex.unlock();
+    fillCache();
+}
+
+WindowType
+SpectrogramLayer::getWindowType() const
+{
+    return m_windowType;
+}
+
+void
+SpectrogramLayer::setGain(float gain)
+{
+    if (m_gain == gain) return; //!!! inadequate for floats!
+
+    m_mutex.lock();
+    m_cacheInvalid = true;
+    m_pixmapCacheInvalid = true;
+    
+    m_gain = gain;
+    emit layerParametersChanged();
+    
+    m_mutex.unlock();
+    fillCache();
+}
+
+float
+SpectrogramLayer::getGain() const
+{
+    return m_gain;
+}
+
+void
+SpectrogramLayer::setMaxFrequency(size_t mf)
+{
+    if (m_maxFrequency == mf) return;
+
+    m_mutex.lock();
+
+    // don't need to invalidate main cache here...
+
+    m_pixmapCacheInvalid = true;
+    
+    m_maxFrequency = mf;
+    emit layerParametersChanged();
+    
+    m_mutex.unlock();
+
+    // ... but we do still need to do this, in case m_maxFrequency
+    // now > m_maxCachedFrequency
+    fillCache();
+}
+
+size_t
+SpectrogramLayer::getMaxFrequency() const
+{
+    return m_maxFrequency;
+}
+
+void
+SpectrogramLayer::setColourScale(ColourScale colourScale)
+{
+    if (m_colourScale == colourScale) return;
+
+    m_mutex.lock();
+    m_cacheInvalid = true;
+    m_pixmapCacheInvalid = true;
+    
+    m_colourScale = colourScale;
+    emit layerParametersChanged();
+    
+    m_mutex.unlock();
+    fillCache();
+}
+
+SpectrogramLayer::ColourScale
+SpectrogramLayer::getColourScale() const
+{
+    return m_colourScale;
+}
+
+void
+SpectrogramLayer::setColourScheme(ColourScheme scheme)
+{
+    if (m_colourScheme == scheme) return;
+
+    m_mutex.lock();
+    // don't need to invalidate main cache here
+    m_pixmapCacheInvalid = true;
+    
+    m_colourScheme = scheme;
+    setCacheColourmap();
+    emit layerParametersChanged();
+    
+    m_mutex.unlock();
+}
+
+SpectrogramLayer::ColourScheme
+SpectrogramLayer::getColourScheme() const
+{
+    return m_colourScheme;
+}
+
+void
+SpectrogramLayer::setFrequencyScale(FrequencyScale frequencyScale)
+{
+    if (m_frequencyScale == frequencyScale) return;
+
+    m_mutex.lock();
+    // don't need to invalidate main cache here
+    m_pixmapCacheInvalid = true;
+    
+    m_frequencyScale = frequencyScale;
+    emit layerParametersChanged();
+    
+    m_mutex.unlock();
+}
+
+SpectrogramLayer::FrequencyScale
+SpectrogramLayer::getFrequencyScale() const
+{
+    return m_frequencyScale;
+}
+
+void
+SpectrogramLayer::cacheInvalid()
+{
+    m_cacheInvalid = true;
+    m_pixmapCacheInvalid = true;
+    m_cachedInitialVisibleArea = false;
+    fillCache();
+}
+
+void
+SpectrogramLayer::cacheInvalid(size_t, size_t)
+{
+    // for now (or forever?)
+    cacheInvalid();
+}
+
+void
+SpectrogramLayer::fillCache()
+{
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    std::cerr << "SpectrogramLayer::fillCache" << std::endl;
+#endif
+    QMutexLocker locker(&m_mutex);
+
+    m_lastFillExtent = 0;
+
+    delete m_updateTimer;
+    m_updateTimer = new QTimer(this);
+    connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(fillTimerTimedOut()));
+    m_updateTimer->start(200);
+
+    if (!m_fillThread) {
+	std::cerr << "SpectrogramLayer::fillCache creating thread" << std::endl;
+	m_fillThread = new CacheFillThread(*this);
+	m_fillThread->start();
+    }
+
+    m_condition.wakeAll();
+}   
+
+void
+SpectrogramLayer::fillTimerTimedOut()
+{
+    if (m_fillThread && m_model) {
+	size_t fillExtent = m_fillThread->getFillExtent();
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+	std::cerr << "SpectrogramLayer::fillTimerTimedOut: extent " << fillExtent << ", last " << m_lastFillExtent << ", total " << m_model->getEndFrame() << std::endl;
+#endif
+	if (fillExtent >= m_lastFillExtent) {
+	    if (fillExtent >= m_model->getEndFrame() && m_lastFillExtent > 0) {
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+		std::cerr << "complete!" << std::endl;
+#endif
+		emit modelChanged();
+		m_pixmapCacheInvalid = true;
+		delete m_updateTimer;
+		m_updateTimer = 0;
+		m_lastFillExtent = 0;
+	    } else if (fillExtent > m_lastFillExtent) {
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+		std::cerr << "SpectrogramLayer: emitting modelChanged("
+			  << m_lastFillExtent << "," << fillExtent << ")" << std::endl;
+#endif
+		emit modelChanged(m_lastFillExtent, fillExtent);
+		m_pixmapCacheInvalid = true;
+		m_lastFillExtent = fillExtent;
+	    }
+	} else {
+	    if (m_view) {
+		size_t sf = 0;
+		if (m_view->getStartFrame() > 0) sf = m_view->getStartFrame();
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+		std::cerr << "SpectrogramLayer: going backwards, emitting modelChanged("
+			  << sf << "," << m_view->getEndFrame() << ")" << std::endl;
+#endif
+		emit modelChanged(sf, m_view->getEndFrame());
+		m_pixmapCacheInvalid = true;
+	    }
+	    m_lastFillExtent = fillExtent;
+	}
+    }
+}
+
+void
+SpectrogramLayer::setCacheColourmap()
+{
+    if (m_cacheInvalid || !m_cache) return;
+
+    m_cache->setNumColors(256);
+    
+    m_cache->setColor(0, qRgb(255, 255, 255));
+
+    for (int pixel = 1; pixel < 256; ++pixel) {
+
+	QColor colour;
+	int hue, px;
+
+	switch (m_colourScheme) {
+
+	default:
+	case DefaultColours:
+	    hue = 256 - pixel;
+	    colour = QColor::fromHsv(hue, pixel/2 + 128, pixel);
+	    break;
+
+	case WhiteOnBlack:
+	    colour = QColor(pixel, pixel, pixel);
+	    break;
+
+	case BlackOnWhite:
+	    colour = QColor(256-pixel, 256-pixel, 256-pixel);
+	    break;
+
+	case RedOnBlue:
+	    colour = QColor(pixel > 128 ? (pixel - 128) * 2 : 0, 0,
+			    pixel < 128 ? pixel : (256 - pixel));
+	    break;
+
+	case YellowOnBlack:
+	    px = 256 - pixel;
+	    colour = QColor(px < 64 ? 255 - px/2 :
+			    px < 128 ? 224 - (px - 64) :
+			    px < 192 ? 160 - (px - 128) * 3 / 2 :
+			    256 - px,
+			    pixel,
+			    pixel / 4);
+	    break;
+
+	case RedOnBlack:
+	    colour = QColor::fromHsv(10, pixel, pixel);
+	    break;
+	}
+
+	m_cache->setColor
+	    (pixel, qRgb(colour.red(), colour.green(), colour.blue()));
+    }
+}
+
+bool
+SpectrogramLayer::fillCacheColumn(int column, double *input,
+				  fftw_complex *output,
+				  fftw_plan plan, 
+				  const Window<double> &windower,
+				  bool lock) const
+{
+    size_t increment = m_windowSize - m_windowSize * m_windowOverlap / 100;
+    int startFrame = increment * column;
+    int endFrame = startFrame + m_windowSize;
+
+    startFrame -= int(m_windowSize - increment) / 2;
+    endFrame   -= int(m_windowSize - increment) / 2;
+    size_t pfx = 0;
+
+    if (startFrame < 0) {
+	pfx = size_t(-startFrame);
+	for (size_t i = 0; i < pfx; ++i) {
+	    input[i] = 0.0;
+	}
+    }
+
+    size_t got = m_model->getValues(m_channel, startFrame + pfx,
+				    endFrame, input + pfx);
+    while (got + pfx < m_windowSize) {
+	input[got + pfx] = 0.0;
+	++got;
+    }
+
+    if (m_gain != 1.0) {
+	for (size_t i = 0; i < m_windowSize; ++i) {
+	    input[i] *= m_gain;
+	}
+    }
+
+    windower.cut(input);
+
+    fftw_execute(plan);
+
+    if (lock) m_mutex.lock();
+    bool interrupted = false;
+
+    for (size_t i = 0; i < m_windowSize / 2; ++i) {
+
+	if (int(i) >= m_cache->height()) break;
+
+	int value = 0;
+
+	if (m_colourScale == PhaseColourScale) {
+
+	    double phase = atan2(-output[i][1], output[i][0]);
+	    value = int((phase * 128 / M_PI) + 128);
+
+	} else {
+	    double mag = sqrt(output[i][0] * output[i][0] +
+			      output[i][1] * output[i][1]);
+	    mag /= m_windowSize / 2;
+
+	    switch (m_colourScale) {
+		
+	    default:
+	    case LinearColourScale:
+		value = int(mag * 50 * 256);
+		break;
+		
+	    case MeterColourScale:
+		value = AudioLevel::multiplier_to_preview(mag * 50, 256);
+	    break;
+
+	    case dBColourScale:
+		mag = 20.0 * log10(mag);
+		mag = (mag + 80.0) / 80.0;
+		if (mag < 0.0) mag = 0.0;
+		if (mag > 1.0) mag = 1.0;
+		value = int(mag * 256);
+	    }
+	}
+
+	if (value > 254) value = 254;
+	if (value < 0) value = 0;
+
+	if (m_cacheInvalid || m_exiting) {
+	    interrupted = true;
+	    break;
+	}
+
+	if (column < m_cache->width()) {
+	    m_cache->setPixel(column, i, value + 1); // 0 is "unset"
+	}
+    }
+
+    if (lock) m_mutex.unlock();
+    return !interrupted;
+}
+
+void
+SpectrogramLayer::CacheFillThread::run()
+{
+//    std::cerr << "SpectrogramLayer::CacheFillThread::run" << std::endl;
+
+    m_layer.m_mutex.lock();
+
+    while (!m_layer.m_exiting) {
+
+	bool interrupted = false;
+
+//	std::cerr << "SpectrogramLayer::CacheFillThread::run in loop" << std::endl;
+
+	if (m_layer.m_model &&
+	    (m_layer.m_cacheInvalid ||
+	     m_layer.m_maxFrequency > m_layer.m_maxCachedFrequency)) {
+
+//	    std::cerr << "SpectrogramLayer::CacheFillThread::run: something to do" << std::endl;
+
+	    while (!m_layer.m_model->isReady()) {
+		m_layer.m_condition.wait(&m_layer.m_mutex, 100);
+	    }
+
+	    size_t minFreq = 0;
+	    if (!m_layer.m_cacheInvalid) {
+		minFreq = m_layer.m_maxCachedFrequency;
+	    }
+
+	    m_layer.m_cachedInitialVisibleArea = false;
+	    m_layer.m_cacheInvalid = false;
+	    m_fillExtent = 0;
+	    m_fillCompletion = 0;
+
+	    std::cerr << "SpectrogramLayer::CacheFillThread::run: model is ready" << std::endl;
+
+	    size_t start = m_layer.m_model->getStartFrame();
+	    size_t end = m_layer.m_model->getEndFrame();
+	    size_t windowSize = m_layer.m_windowSize;
+	    size_t windowIncrement = m_layer.getWindowIncrement();
+
+	    size_t visibleStart = start;
+	    size_t visibleEnd = end;
+
+	    if (m_layer.m_view) {
+		if (m_layer.m_view->getStartFrame() < 0) {
+		    visibleStart = 0;
+		} else {
+		    visibleStart = m_layer.m_view->getStartFrame();
+		    visibleStart = (visibleStart / windowIncrement) *
+			windowIncrement;
+		}
+		visibleEnd = m_layer.m_view->getEndFrame();
+	    }
+
+	    delete m_layer.m_cache;
+	    size_t bins = windowSize / 2;
+	    if (m_layer.m_maxFrequency > 0) {
+		int sr = m_layer.m_model->getSampleRate();
+		bins = int((double(m_layer.m_maxFrequency) * windowSize) / sr + 0.1);
+		if (bins > windowSize / 2) bins = windowSize / 2;
+	    }
+	    m_layer.m_cache = new QImage((end - start) / windowIncrement + 1,
+					 bins, //!!!
+					 QImage::Format_Indexed8);
+    
+	    m_layer.setCacheColourmap();
+    
+	    m_layer.m_cache->fill(0);
+	    m_layer.m_mutex.unlock();
+
+	    double *input = (double *)
+		fftw_malloc(windowSize * sizeof(double));
+
+	    fftw_complex *output = (fftw_complex *)
+		fftw_malloc(windowSize * sizeof(fftw_complex));
+
+	    fftw_plan plan = fftw_plan_dft_r2c_1d(windowSize, input,
+						  output, FFTW_MEASURE);
+
+	    Window<double> windower(m_layer.m_windowType, m_layer.m_windowSize);
+
+	    if (!plan) {
+		std::cerr << "WARNING: fftw_plan(" << windowSize << ") failed!" << std::endl;
+		fftw_free(input);
+		fftw_free(output);
+		m_layer.m_mutex.lock();
+		continue;
+	    }
+
+	    int counter = 0;
+	    int updateAt = (end / windowIncrement) / 20;
+	    if (updateAt < 100) updateAt = 100;
+
+	    bool doVisibleFirst = (visibleStart != start && visibleEnd != end);
+
+	    if (doVisibleFirst) {
+
+		m_layer.m_mutex.lock();
+
+		for (size_t f = visibleStart; f < visibleEnd; f += windowIncrement) {
+	    
+		    m_layer.fillCacheColumn(int((f - start) / windowIncrement),
+					    input, output, plan, windower, false);
+
+		    m_layer.m_mutex.unlock();
+		    m_layer.m_mutex.lock();
+
+		    if (m_layer.m_cacheInvalid || m_layer.m_exiting) {
+			interrupted = true;
+			m_fillExtent = 0;
+			break;
+		    }
+
+		    if (++counter == updateAt) {
+			if (f < end) m_fillExtent = f;
+			m_fillCompletion = size_t(100 * fabsf(float(f - visibleStart) /
+							      float(end - start)));
+			counter = 0;
+		    }
+		}
+
+		m_layer.m_mutex.unlock();
+	    }
+
+	    m_layer.m_cachedInitialVisibleArea = true;
+
+	    if (!interrupted && doVisibleFirst) {
+		
+		for (size_t f = visibleEnd; f < end; f += windowIncrement) {
+	    
+		    if (!m_layer.fillCacheColumn(int((f - start) / windowIncrement),
+						 input, output, plan, windower, true)) {
+			interrupted = true;
+			m_fillExtent = 0;
+			break;
+		    }
+
+
+		    if (++counter == updateAt) {
+			if (f < end) m_fillExtent = f;
+			m_fillCompletion = size_t(100 * fabsf(float(f - visibleStart) /
+							      float(end - start)));
+			counter = 0;
+		    }
+		}
+	    }
+
+	    if (!interrupted) {
+
+		size_t remainingEnd = end;
+		if (doVisibleFirst) {
+		    remainingEnd = visibleStart;
+		    if (remainingEnd > start) --remainingEnd;
+		    else remainingEnd = start;
+		}
+		size_t baseCompletion = m_fillCompletion;
+
+		for (size_t f = start; f < remainingEnd; f += windowIncrement) {
+
+		    if (!m_layer.fillCacheColumn(int((f - start) / windowIncrement),
+						 input, output, plan, windower, true)) {
+			interrupted = true;
+			m_fillExtent = 0;
+			break;
+		    }
+		    
+		    if (++counter == updateAt) {
+			m_fillExtent = f;
+			m_fillCompletion = baseCompletion +
+			    size_t(100 * fabsf(float(f - start) /
+					       float(end - start)));
+			counter = 0;
+		    }
+		}
+	    }
+
+	    fftw_destroy_plan(plan);
+	    fftw_free(output);
+	    fftw_free(input);
+
+	    if (!interrupted) {
+		m_fillExtent = end;
+		m_fillCompletion = 100;
+	    }
+
+	    m_layer.m_mutex.lock();
+	}
+
+	if (!interrupted) m_layer.m_condition.wait(&m_layer.m_mutex, 2000);
+    }
+}
+
+bool
+SpectrogramLayer::getYBinRange(int y, float &q0, float &q1) const
+{
+    int h = m_view->height();
+    if (y < 0 || y >= h) return false;
+
+    // Each pixel in a column is drawn from a possibly non-
+    // integral set of frequency bins.
+
+    if (m_frequencyScale == LinearFrequencyScale) {
+
+	size_t bins = m_windowSize / 2;
+    
+	if (m_maxFrequency > 0) {
+	    int sr = m_model->getSampleRate();
+	    bins = int((double(m_maxFrequency) * m_windowSize) / sr + 0.1);
+	    if (bins > m_windowSize / 2) bins = m_windowSize / 2;
+	}
+	
+	q0 = float(h - y - 1) * bins / h;
+	q1 = float(h - y) * bins / h;
+
+    } else {
+
+	// This is all most ad-hoc.  I'm not at my brightest.
+
+	int sr = m_model->getSampleRate();
+
+	float maxf = m_maxFrequency;
+	if (maxf == 0.0) maxf = float(sr) / 2;
+
+	float minf = float(sr) / m_windowSize;
+	
+	float maxlogf = log10f(maxf);
+	float minlogf = log10f(minf);
+
+	float logf0 = minlogf + ((maxlogf - minlogf) * (h - y - 1)) / h;
+	float logf1 = minlogf + ((maxlogf - minlogf) * (h - y)) / h;
+	
+	float f0 = pow(10.f, logf0);
+	float f1 = pow(10.f, logf1);
+
+	q0 = ((f0 * m_windowSize) / sr) - 1;
+	q1 = ((f1 * m_windowSize) / sr) - 1;
+
+//	std::cout << "y=" << y << " h=" << h << " maxf=" << maxf << " maxlogf="
+//		  << maxlogf << " logf0=" << logf0 << " f0=" << f0 << " q0="
+//		  << q0 << std::endl;
+    }	
+
+    return true;
+}
+
+bool
+SpectrogramLayer::getXBinRange(int x, float &s0, float &s1, LayerRange *range) const
+{
+    long   startFrame;
+    int    zoomLevel;
+    size_t modelStart;
+    size_t modelEnd;
+
+    if (range) {
+	startFrame = range->startFrame;
+	zoomLevel  = range->zoomLevel;
+	modelStart = range->modelStart;
+	modelEnd   = range->modelEnd;
+    } else {
+	startFrame = m_view->getStartFrame();
+        zoomLevel  = m_view->getZoomLevel();
+	modelStart = m_model->getStartFrame();
+	modelEnd   = m_model->getEndFrame();
+    }
+
+    // Each pixel column covers an exact range of sample frames:
+    int f0 = startFrame + x * zoomLevel - modelStart;
+    int f1 = f0 + zoomLevel - 1;
+    
+    if (f1 < int(modelStart) || f0 > int(modelEnd)) return false;
+    
+    // And that range may be drawn from a possibly non-integral
+    // range of spectrogram windows:
+
+    size_t windowIncrement = getWindowIncrement();
+
+    s0 = float(f0) / windowIncrement;
+    s1 = float(f1) / windowIncrement;
+
+    return true;
+}
+ 
+bool
+SpectrogramLayer::getXBinSourceRange(int x, RealTime &min, RealTime &max) const
+{
+    float s0 = 0, s1 = 0;
+    if (!getXBinRange(x, s0, s1)) return false;
+    
+    int s0i = int(s0 + 0.001);
+    int s1i = int(s1);
+
+    int windowIncrement = getWindowIncrement();
+    int w0 = s0i * windowIncrement - (m_windowSize - windowIncrement)/2;
+    int w1 = s1i * windowIncrement + windowIncrement +
+	(m_windowSize - windowIncrement)/2 - 1;
+    
+    min = RealTime::frame2RealTime(w0, m_model->getSampleRate());
+    max = RealTime::frame2RealTime(w1, m_model->getSampleRate());
+    return true;
+}
+
+bool
+SpectrogramLayer::getYBinSourceRange(int y, float &freqMin, float &freqMax)
+const
+{
+    float q0 = 0, q1 = 0;
+    if (!getYBinRange(y, q0, q1)) return false;
+
+    int q0i = int(q0 + 0.001);
+    int q1i = int(q1);
+
+    int sr = m_model->getSampleRate();
+
+    for (int q = q0i; q <= q1i; ++q) {
+	int binfreq = (sr * (q + 1)) / m_windowSize;
+	if (q == q0i) freqMin = binfreq;
+	if (q == q1i) freqMax = binfreq;
+    }
+    return true;
+}
+    
+bool
+SpectrogramLayer::getXYBinSourceRange(int x, int y, float &dbMin, float &dbMax) const
+{
+    float q0 = 0, q1 = 0;
+    if (!getYBinRange(y, q0, q1)) return false;
+
+    float s0 = 0, s1 = 0;
+    if (!getXBinRange(x, s0, s1)) return false;
+    
+    int q0i = int(q0 + 0.001);
+    int q1i = int(q1);
+
+    int s0i = int(s0 + 0.001);
+    int s1i = int(s1);
+
+    if (m_mutex.tryLock()) {
+	if (m_cache && !m_cacheInvalid) {
+
+	    int cw = m_cache->width();
+	    int ch = m_cache->height();
+
+	    int min = -1, max = -1;
+
+	    for (int q = q0i; q <= q1i; ++q) {
+		for (int s = s0i; s <= s1i; ++s) {
+		    if (s >= 0 && q >= 0 && s < cw && q < ch) {
+			int value = m_cache->scanLine(q)[s];
+			if (min == -1 || value < min) min = value;
+			if (max == -1 || value > max) max = value;
+		    }	
+		}
+	    }
+
+	    if (min < 0) return false;
+
+	    dbMin = (float(min) / 256.0) * 80.0 - 80.0;
+	    dbMax = (float(max + 1) / 256.0) * 80.0 - 80.1;
+
+	    m_mutex.unlock();
+	    return true;
+	}
+
+	m_mutex.unlock();
+    }
+
+    return false;
+}
+   
+void
+SpectrogramLayer::paint(QPainter &paint, QRect rect) const
+{
+//    Profiler profiler("SpectrogramLayer::paint", true);
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    std::cerr << "SpectrogramLayer::paint(): m_model is " << m_model << ", zoom level is " << m_view->getZoomLevel() << ", m_updateTimer " << m_updateTimer << ", pixmap cache invalid " << m_pixmapCacheInvalid << std::endl;
+#endif
+
+    if (!m_model || !m_model->isOK() || !m_model->isReady()) {
+	return;
+    }
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    std::cerr << "SpectrogramLayer::paint(): About to lock" << std::endl;
+#endif
+
+/*
+    if (m_cachedInitialVisibleArea) {
+	if (!m_mutex.tryLock()) {
+	    m_view->update();
+	    return;
+	}
+    } else {
+*/
+	m_mutex.lock();
+//    }
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    std::cerr << "SpectrogramLayer::paint(): locked" << std::endl;
+#endif
+
+    if (m_cacheInvalid) { // lock the mutex before checking this
+	m_mutex.unlock();
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+	std::cerr << "SpectrogramLayer::paint(): Cache invalid, returning" << std::endl;
+#endif
+	return;
+    }
+
+    bool stillCacheing = (m_updateTimer != 0);
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    std::cerr << "SpectrogramLayer::paint(): Still cacheing = " << stillCacheing << std::endl;
+#endif
+
+    long startFrame = m_view->getStartFrame();
+    int zoomLevel = m_view->getZoomLevel();
+
+    int x0 = 0;
+    int x1 = m_view->width();
+    int y0 = 0;
+    int y1 = m_view->height();
+
+    bool recreateWholePixmapCache = true;
+
+    if (!m_pixmapCacheInvalid) {
+
+	//!!! This cache may have been obsoleted entirely by the
+	//scrolling cache in View.  Perhaps experiment with
+	//removing it and see if it makes things even quicker (or else
+	//make it optional)
+
+	if (int(m_pixmapCacheZoomLevel) == zoomLevel &&
+	    m_pixmapCache->width() == m_view->width() &&
+	    m_pixmapCache->height() == m_view->height()) {
+
+	    if (m_pixmapCacheStartFrame / zoomLevel ==
+		startFrame / zoomLevel) {
+	    
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+		std::cerr << "SpectrogramLayer: pixmap cache good" << std::endl;
+#endif
+
+		m_mutex.unlock();
+		paint.drawPixmap(rect, *m_pixmapCache, rect);
+		return;
+
+	    } else {
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+		std::cerr << "SpectrogramLayer: pixmap cache partially OK" << std::endl;
+#endif
+
+		recreateWholePixmapCache = false;
+
+		int dx = (m_pixmapCacheStartFrame - startFrame) / zoomLevel;
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+		std::cerr << "SpectrogramLayer: dx = " << dx << " (pixmap cache " << m_pixmapCache->width() << "x" << m_pixmapCache->height() << ")" << std::endl;
+#endif
+
+		if (dx > -m_pixmapCache->width() && dx < m_pixmapCache->width()) {
+
+#if defined(Q_WS_WIN32) || defined(Q_WS_MAC)
+		    // Copying a pixmap to itself doesn't work
+		    // properly on Windows or Mac (it only works when
+		    // moving in one direction).
+
+		    //!!! Need a utility function for this
+
+		    static QPixmap *tmpPixmap = 0;
+		    if (!tmpPixmap ||
+			tmpPixmap->width() != m_pixmapCache->width() ||
+			tmpPixmap->height() != m_pixmapCache->height()) {
+			delete tmpPixmap;
+			tmpPixmap = new QPixmap(m_pixmapCache->width(),
+						m_pixmapCache->height());
+		    }
+		    QPainter cachePainter;
+		    cachePainter.begin(tmpPixmap);
+		    cachePainter.drawPixmap(0, 0, *m_pixmapCache);
+		    cachePainter.end();
+		    cachePainter.begin(m_pixmapCache);
+		    cachePainter.drawPixmap(dx, 0, *tmpPixmap);
+		    cachePainter.end();
+#else
+		    QPainter cachePainter(m_pixmapCache);
+		    cachePainter.drawPixmap(dx, 0, *m_pixmapCache);
+		    cachePainter.end();
+#endif
+
+		    paint.drawPixmap(rect, *m_pixmapCache, rect);
+
+		    if (dx < 0) {
+			x0 = m_pixmapCache->width() + dx;
+			x1 = m_pixmapCache->width();
+		    } else {
+			x0 = 0;
+			x1 = dx;
+		    }
+		}
+	    }
+	} else {
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+	    std::cerr << "SpectrogramLayer: pixmap cache useless" << std::endl;
+#endif
+	}
+    }
+
+    if (stillCacheing) {
+	x0 = rect.left();
+	x1 = rect.right() + 1;
+	y0 = rect.top();
+	y1 = rect.bottom() + 1;
+    }
+
+    int w = x1 - x0;
+    int h = y1 - y0;
+
+//    std::cerr << "x0 " << x0 << ", x1 " << x1 << ", w " << w << ", h " << h << std::endl;
+
+    QImage scaled(w, h, QImage::Format_RGB32);
+
+    LayerRange range = { m_view->getStartFrame(), m_view->getZoomLevel(),
+			m_model->getStartFrame(), m_model->getEndFrame() };
+
+    m_mutex.unlock();
+
+    for (int y = 0; y < h; ++y) {
+
+	m_mutex.lock();
+	if (m_cacheInvalid) {
+	    m_mutex.unlock();
+	    break;
+	}
+
+	int cw = m_cache->width();
+	int ch = m_cache->height();
+
+	float q0 = 0, q1 = 0;
+
+	if (!getYBinRange(y0 + y, q0, q1)) {
+	    for (int x = 0; x < w; ++x) {
+		assert(x <= scaled.width());
+		scaled.setPixel(x, y, qRgb(0, 0, 0));
+	    }
+	    m_mutex.unlock();
+	    continue;
+	}
+
+	int q0i = int(q0 + 0.001);
+	int q1i = int(q1);
+
+	for (int x = 0; x < w; ++x) {
+
+	    float s0 = 0, s1 = 0;
+
+	    if (!getXBinRange(x0 + x, s0, s1, &range)) {
+		assert(x <= scaled.width());
+		scaled.setPixel(x, y, qRgb(0, 0, 0));
+		continue;
+	    }
+
+	    int s0i = int(s0 + 0.001);
+	    int s1i = int(s1);
+
+	    float total = 0, divisor = 0;
+
+	    for (int s = s0i; s <= s1i; ++s) {
+
+		float sprop = 1.0;
+		if (s == s0i) sprop *= (s + 1) - s0;
+		if (s == s1i) sprop *= s1 - s;
+
+		for (int q = q0i; q <= q1i; ++q) {
+
+		    float qprop = sprop;
+		    if (q == q0i) qprop *= (q + 1) - q0;
+		    if (q == q1i) qprop *= q1 - q;
+
+		    if (s >= 0 && q >= 0 && s < cw && q < ch) {
+			total += qprop * m_cache->scanLine(q)[s];
+			divisor += qprop;
+		    }
+		}
+	    }
+		    
+	    if (divisor > 0.0) {
+		int pixel = int(total / divisor);
+		if (pixel > 255) pixel = 255;
+		if (pixel < 1) pixel = 1;
+		assert(x <= scaled.width());
+		scaled.setPixel(x, y, m_cache->color(pixel));
+	    } else {
+		assert(x <= scaled.width());
+		scaled.setPixel(x, y, qRgb(0, 0, 0));
+	    }
+	}
+
+	m_mutex.unlock();
+    }
+
+    paint.drawImage(x0, y0, scaled);
+
+    if (recreateWholePixmapCache) {
+	delete m_pixmapCache;
+	m_pixmapCache = new QPixmap(w, h);
+    }
+
+    QPainter cachePainter(m_pixmapCache);
+    cachePainter.drawImage(x0, y0, scaled);
+    cachePainter.end();
+    
+    m_pixmapCacheInvalid = false;
+    m_pixmapCacheStartFrame = startFrame;
+    m_pixmapCacheZoomLevel = zoomLevel;
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    std::cerr << "SpectrogramLayer::paint() returning" << std::endl;
+#endif
+
+//!!!    drawLocalFeatureDescription(paint);
+}
+
+int
+SpectrogramLayer::getCompletion() const
+{
+    if (m_updateTimer == 0) return 100;
+    size_t completion = m_fillThread->getFillCompletion();
+//    std::cerr << "SpectrogramLayer::getCompletion: completion = " << completion << std::endl;
+    return completion;
+}
+
+QRect
+SpectrogramLayer::getFeatureDescriptionRect(QPainter &paint, QPoint pos) const
+{
+    if (!m_model || !m_model->isOK()) return QRect();
+
+    QString timeLabel = tr("Time: ");
+    QString freqLabel = tr("Hz: ");
+    QString dBLabel = tr("dB: ");
+
+    // assume time is widest
+    RealTime rtMin, rtMax;
+    if (!getXBinSourceRange(pos.x(), rtMin, rtMax)) return QRect();
+    QString timeMinText = QString("%1").arg(rtMin.toText(true).c_str());
+    QString timeMaxText = QString(" - %1").arg(rtMax.toText(true).c_str());
+
+    QFontMetrics metrics = paint.fontMetrics();
+
+    int labelwidth = 
+	std::max(std::max(metrics.width(timeLabel),
+			  metrics.width(freqLabel)),
+		 metrics.width(dBLabel));
+
+    int boxwidth = labelwidth + 
+	metrics.width(timeMinText) + metrics.width(timeMaxText);
+
+    int fontHeight = metrics.height();
+    int boxheight = fontHeight * 3 + 4;
+
+    return QRect(0, 0, boxwidth + 20, boxheight + 15);
+}
+
+void
+SpectrogramLayer::paintLocalFeatureDescription(QPainter &paint,
+					       QRect rect, QPoint pos) const
+{
+    int x = pos.x();
+    int y = pos.y();
+
+    if (!m_model || !m_model->isOK()) return;
+
+    float dbMin = 0, dbMax = 0;
+    float freqMin = 0, freqMax = 0;
+    RealTime rtMin, rtMax;
+
+    bool haveDb = false;
+
+    if (!getXBinSourceRange(x, rtMin, rtMax)) return;
+    if (!getYBinSourceRange(y, freqMin, freqMax)) return;
+    if (getXYBinSourceRange(x, y, dbMin, dbMax)) haveDb = true;
+
+    QString timeLabel = tr("Time: ");
+    QString freqLabel = tr("Hz: ");
+    QString dBLabel = tr("dB: ");
+
+    QString timeMinText = QString("%1").arg(rtMin.toText(true).c_str());
+    QString timeMaxText = QString(" - %1").arg(rtMax.toText(true).c_str());
+
+    QString freqMinText = QString("%1").arg(freqMin);
+    QString freqMaxText = "";
+    if (freqMax != freqMin) {
+	freqMaxText = QString(" - %1").arg(freqMax);
+    }
+
+    QString dBMinText = "";
+    QString dBMaxText = "";
+
+    if (haveDb) {
+	int dbmxi = int(dbMax - 0.001);
+	int dbmni = int(dbMin - 0.001);
+	dBMinText = QString("%1").arg(dbmni);
+	if (dbmxi != dbmni) dBMaxText = QString(" - %1").arg(dbmxi);
+    }
+
+    QFontMetrics metrics = paint.fontMetrics();
+
+    int labelwidth = 
+	std::max(std::max(metrics.width(timeLabel),
+			  metrics.width(freqLabel)),
+		 metrics.width(dBLabel));
+
+    int minwidth = 
+	std::max(std::max(metrics.width(timeMinText),
+			  metrics.width(freqMinText)),
+		 metrics.width(dBMinText));
+
+    int maxwidth = 
+	std::max(std::max(metrics.width(timeMaxText),
+			  metrics.width(freqMaxText)),
+		 metrics.width(dBMaxText));
+
+    int boxwidth = labelwidth + minwidth + maxwidth;
+
+    int fontAscent = metrics.ascent();
+    int fontHeight = metrics.height();
+
+    int boxheight = fontHeight * 3 + 4;
+
+//    paint.setPen(Qt::white);
+//    paint.setBrush(Qt::NoBrush);
+
+//!!!    int xbase = m_view->width() - boxwidth - 20;
+    int xbase = rect.x() + 5;
+    int ybase = rect.y() + 5;
+
+    paint.drawRect(xbase, ybase, boxwidth + 10,
+		   boxheight + 10 - metrics.descent() + 1);
+
+    paint.drawText(xbase + 5 + labelwidth - metrics.width(timeLabel),
+		   ybase + 5 + fontAscent, timeLabel);
+
+    paint.drawText(xbase + 5 + labelwidth - metrics.width(freqLabel),
+		   ybase + 7 + fontAscent + fontHeight, freqLabel);
+
+    paint.drawText(xbase + 5 + labelwidth - metrics.width(dBLabel),
+		   ybase + 9 + fontAscent + fontHeight * 2, dBLabel);
+    
+    paint.drawText(xbase + 5 + labelwidth + minwidth - metrics.width(timeMinText),
+		   ybase + 5 + fontAscent, timeMinText);
+
+    paint.drawText(xbase + 5 + labelwidth + minwidth - metrics.width(freqMinText),
+		   ybase + 7 + fontAscent + fontHeight, freqMinText);
+
+    paint.drawText(xbase + 5 + labelwidth + minwidth - metrics.width(dBMinText),
+		   ybase + 9 + fontAscent + fontHeight * 2, dBMinText);
+    
+    paint.drawText(xbase + 5 + labelwidth + minwidth,
+		   ybase + 5 + fontAscent, timeMaxText);
+
+    paint.drawText(xbase + 5 + labelwidth + minwidth,
+		   ybase + 7 + fontAscent + fontHeight, freqMaxText);
+    
+    paint.drawText(xbase + 5 + labelwidth + minwidth,
+		   ybase + 9 + fontAscent + fontHeight * 2, dBMaxText);
+}
+
+/*!!!
+
+bool
+SpectrogramLayer::identifyLocalFeatures(bool on, int x, int y)
+{
+    return true; //!!!
+
+    m_identify = on;
+    m_identifyX = x;
+    m_identifyY = y;
+
+    m_view->update();
+*/
+/*
+    if (!m_model || !m_model->isOK()) return false;
+
+    std::cerr << "SpectrogramLayer::identifyLocalFeatures(" << on << "," << x << "," << y << ")" << std::endl;
+
+    float dbMin = 0, dbMax = 0;
+    float freqMin = 0, freqMax = 0;
+    RealTime rtMin, rtMax;
+
+    if (getXBinSourceRange(x, rtMin, rtMax)) {
+	std::cerr << "Times: " << rtMin << " -> " << rtMax << std::endl;
+    } else return false;
+
+    if (getYBinSourceRange(y, freqMin, freqMax)) {
+	std::cerr << "Frequencies: " << freqMin << " -> " << freqMax << std::endl;
+    } else return false;
+
+    if (getXYBinSourceRange(x, y, dbMin, dbMax)) {
+	std::cerr << "dB: " << dbMin << " -> " << dbMax << std::endl;
+    }
+
+    m_identifyX = x;
+    m_identifyY = y;
+    m_identify = true;
+*/
+    /*!!!
+    return true;
+}
+    */
+int
+SpectrogramLayer::getVerticalScaleWidth(QPainter &paint) const
+{
+    if (!m_model || !m_model->isOK()) return 0;
+
+    int tw = paint.fontMetrics().width(QString("%1")
+				     .arg(m_maxFrequency > 0 ?
+					  m_maxFrequency - 1 :
+					  m_model->getSampleRate() / 2));
+
+    int fw = paint.fontMetrics().width(QString("43Hz"));
+    if (tw < fw) tw = fw;
+    
+    return tw + 13;
+}
+
+void
+SpectrogramLayer::paintVerticalScale(QPainter &paint, QRect rect) const
+{
+    if (!m_model || !m_model->isOK()) {
+	return;
+    }
+
+    int h = rect.height(), w = rect.width();
+
+    size_t bins = m_windowSize / 2;
+    int sr = m_model->getSampleRate();
+
+    if (m_maxFrequency > 0) {
+	bins = int((double(m_maxFrequency) * m_windowSize) / sr + 0.1);
+	if (bins > m_windowSize / 2) bins = m_windowSize / 2;
+    }
+
+    int py = -1;
+    int textHeight = paint.fontMetrics().height();
+    int toff = -textHeight + paint.fontMetrics().ascent() + 2;
+
+    int bin = -1;
+
+    for (int y = 0; y < m_view->height(); ++y) {
+
+	float q0, q1;
+	if (!getYBinRange(m_view->height() - y, q0, q1)) continue;
+
+	int vy;
+
+	if (int(q0) > bin) {
+	    vy = y;
+	    bin = int(q0);
+	} else {
+	    continue;
+	}
+
+	int freq = (sr * (bin + 1)) / m_windowSize;
+
+	if (py >= 0 && (vy - py) < textHeight - 1) {
+	    paint.drawLine(w - 4, h - vy, w, h - vy);
+	    continue;
+	}
+
+	QString text = QString("%1").arg(freq);
+	if (bin == 0) text = QString("%1Hz").arg(freq);
+	paint.drawLine(0, h - vy, w, h - vy);
+
+	if (h - vy - textHeight >= -2) {
+	    int tx = w - 10 - paint.fontMetrics().width(text);
+	    paint.drawText(tx, h - vy + toff, text);
+	}
+
+	py = vy;
+    }
+}
+
+#ifdef INCLUDE_MOCFILES
+#include "SpectrogramLayer.moc.cpp"
+#endif
+