changeset 0:2a4f26e85b4c

initial import
author Chris Cannam
date Tue, 10 Jan 2006 16:33:16 +0000
parents
children ab83c415a6cd
files layer/Colour3DPlotLayer.cpp layer/Colour3DPlotLayer.h layer/LayerFactory.cpp layer/LayerFactory.h layer/SpectrogramLayer.cpp layer/SpectrogramLayer.h layer/TimeInstantLayer.cpp layer/TimeInstantLayer.h layer/TimeRulerLayer.cpp layer/TimeRulerLayer.h layer/TimeValueLayer.cpp layer/TimeValueLayer.h layer/WaveformLayer.cpp layer/WaveformLayer.h widgets/AudioDial.cpp widgets/AudioDial.h widgets/Fader.cpp widgets/Fader.h widgets/Pane.cpp widgets/Pane.h widgets/PaneStack.cpp widgets/PaneStack.h widgets/Panner.cpp widgets/Panner.h widgets/PropertyBox.cpp widgets/PropertyBox.h widgets/PropertyStack.cpp widgets/PropertyStack.h
diffstat 28 files changed, 6911 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/Colour3DPlotLayer.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,268 @@
+/* -*- 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 "Colour3DPlotLayer.h"
+
+#include "base/View.h"
+#include "base/Profiler.h"
+
+#include <QPainter>
+#include <QImage>
+#include <QRect>
+
+#include <iostream>
+
+#include <cassert>
+
+
+Colour3DPlotLayer::Colour3DPlotLayer(View *w) :
+    Layer(w),
+    m_model(0),
+    m_cache(0)
+{
+    m_view->addLayer(this);
+}
+
+Colour3DPlotLayer::~Colour3DPlotLayer()
+{
+}
+
+void
+Colour3DPlotLayer::setModel(const DenseThreeDimensionalModel *model)
+{
+    m_model = model;
+    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();
+}
+
+void
+Colour3DPlotLayer::cacheInvalid()
+{
+    delete m_cache; 
+    m_cache = 0;
+}
+
+void
+Colour3DPlotLayer::cacheInvalid(size_t, size_t)
+{
+    cacheInvalid();
+}
+
+void
+Colour3DPlotLayer::paint(QPainter &paint, QRect rect) const
+{
+//    Profiler profiler("Colour3DPlotLayer::paint");
+//    std::cerr << "Colour3DPlotLayer::paint(): m_model is " << m_model << ", zoom level is " << m_view->getZoomLevel() << std::endl;
+
+    //!!! This doesn't yet accommodate the fact that the model may
+    //have a different sample rate from an underlying model.  At the
+    //moment our paint mechanism assumes all models have the same
+    //sample rate.  If that isn't the case, they won't align and the
+    //time ruler will match whichever model was used to construct it.
+    //Obviously it is not going to be the case in general that models
+    //will have the same samplerate, so we need a pane samplerate as
+    //well which we trivially realign to.  (We can probably require
+    //the waveform and spectrogram layers to display at the pane
+    //samplerate.)
+
+    int completion = 0;
+    if (!m_model || !m_model->isOK() || !m_model->isReady(&completion)) {
+	if (completion > 0) {
+	    paint.fillRect(0, 10, m_view->width() * completion / 100,
+			   10, QColor(120, 120, 120));
+	}
+	return;
+    }
+
+    long startFrame = m_view->getStartFrame();
+    int zoomLevel = m_view->getZoomLevel();
+
+    size_t modelStart = m_model->getStartFrame();
+    size_t modelEnd = m_model->getEndFrame();
+    size_t modelWindow = m_model->getWindowSize();
+
+    if (!m_cache) {
+
+	m_cache = new QImage((modelEnd - modelStart) / modelWindow + 1,
+			     m_model->getYBinCount(),
+			     QImage::Format_Indexed8);
+
+	m_cache->setNumColors(256);
+	DenseThreeDimensionalModel::BinValueSet values;
+/*
+	for (int pixel = 0; pixel < 256; ++pixel) {
+	    int hue = 256 - pixel;
+//	    int hue = 220 - pixel;
+//	    if (hue < 0) hue += 360;
+	    QColor color = QColor::fromHsv(hue, pixel/2 + 128, pixel);
+	    m_cache->setColor(pixel, qRgb(color.red(), color.green(), color.blue()));
+	}
+*/
+
+	float min = m_model->getMinimumLevel();
+	float max = m_model->getMaximumLevel();
+
+	if (max == min) max = min + 1.0;
+
+//	int min = lrintf(m_model->getMinimumLevel());
+//	int max = lrintf(m_model->getMaximumLevel());
+	for (int value = 0; value < 256; ++value) {
+//	    int spread = ((value - min) * 256) / (max - min);
+//	    int hue = 256 - spread;
+//	    QColor color = QColor::fromHsv(hue, spread/2 + 128, spread);
+	    int hue = 256 - value;
+	    QColor color = QColor::fromHsv(hue, value/2 + 128, value);
+	    m_cache->setColor(value, qRgba(color.red(), color.green(), color.blue(), 80));
+//	    std::cerr << "Colour3DPlotLayer: Index " << value << ": hue " << hue << std::endl;
+	}
+
+	m_cache->fill(min);
+
+	for (size_t f = modelStart; f <= modelEnd; f += modelWindow) {
+	
+	    values.clear();
+	    m_model->getBinValues(f, values);
+	    
+	    for (size_t y = 0; y < m_model->getYBinCount(); ++y) {
+
+		float value = min;
+		if (y < values.size()) value = values[y];
+
+		//!!! divide-by-zero!
+		int pixel = int(((value - min) * 256) / (max - min));
+
+		m_cache->setPixel(f / modelWindow, y, pixel);
+	    }
+	}
+    }
+
+    int x0 = rect.left();
+    int x1 = rect.right() + 1;
+
+//    int y0 = rect.top();
+//    int y1 = rect.bottom();
+    int w = x1 - x0;
+    int h = m_view->height();
+
+    // The cache is from the model's start frame to the model's end
+    // frame at the model's window increment frames per pixel.  We
+    // want to draw from our start frame + x0 * zoomLevel to our start
+    // frame + x1 * zoomLevel at zoomLevel frames per pixel.
+
+    //!!! Strictly speaking we want quite different paint mechanisms
+    //for models that have more than one bin per pixel in either
+    //direction.  This one is only really appropriate for models with
+    //far fewer bins in both directions.
+
+    int sx0 = ((startFrame + x0 * zoomLevel) - int(modelStart)) /
+	int(modelWindow);
+    int sx1 = ((startFrame + x1 * zoomLevel) - int(modelStart)) / 
+	int(modelWindow);
+    int sw = sx1 - sx0;
+    int sh = m_model->getYBinCount();
+
+/*
+    std::cerr << "Colour3DPlotLayer::paint: w " << w << ", h " << h << ", sx0 " << sx0 << ", sx1 " << sx1 << ", sw " << sw << ", sh " << sh << std::endl;
+    std::cerr << "Colour3DPlotLayer: sample rate is " << m_model->getSampleRate() << ", window size " << m_model->getWindowSize() << std::endl;
+*/
+
+    for (int sx = sx0 - 1; sx <= sx1; ++sx) {
+
+	int fx = sx * int(modelWindow);
+
+	if (fx + modelWindow < int(modelStart) ||
+	    fx > int(modelEnd)) continue;
+
+	for (int sy = 0; sy < sh; ++sy) {
+
+	    int rx0 = ((fx + int(modelStart))
+		       - int(startFrame)) / zoomLevel;
+	    int rx1 = ((fx + int(modelWindow) + int(modelStart))
+		       - int(startFrame)) / zoomLevel;
+
+	    int ry0 = h - (sy * h) / sh - 1;
+	    int ry1 = h - ((sy + 1) * h) / sh - 2;
+	    QRgb pixel = qRgb(255, 255, 255);
+	    if (sx >= 0 && sx < m_cache->width() &&
+		sy >= 0 && sy < m_cache->height()) {
+		pixel = m_cache->pixel(sx, sy);
+	    }
+
+	    QColor pen(255, 255, 255, 80);
+//	    QColor pen(pixel);
+	    QColor brush(pixel);
+	    brush.setAlpha(160);
+//	    paint.setPen(pen);
+	    paint.setPen(Qt::NoPen);
+	    paint.setBrush(brush);
+
+	    int w = rx1 - rx0;
+	    if (w < 1) w = 1;
+	    paint.drawRect(rx0, ry0 - h / sh - 1, w, h / sh + 1);
+
+	    if (sx >= 0 && sx < m_cache->width() &&
+		sy >= 0 && sy < m_cache->height()) {
+		int dv = m_cache->pixelIndex(sx, sy);
+		if (dv != 0 && paint.fontMetrics().height() < (h / sh)) {
+		    QString text = QString("%1").arg(dv);
+		    if (paint.fontMetrics().width(text) < w - 3) {
+			paint.setPen(Qt::white);
+			paint.drawText(rx0 + 2,
+				       ry0 - h / sh - 1 + 2 + paint.fontMetrics().ascent(),
+				       QString("%1").arg(dv));
+		    }
+		}
+	    }
+	}
+    }
+    
+/*
+    QRect targetRect(x0, 0, w, h);
+    QRect sourceRect(sx0, 0, sw, sh);
+
+    QImage scaled(w, h, QImage::Format_RGB32);
+
+    for (int x = 0; x < w; ++x) {
+	for (int y = 0; y < h; ++y) {
+
+	    
+
+	    int sx = sx0 + (x * sw) / w;
+	    int sy = sh - (y * sh) / h - 1;
+//	    std::cerr << "Colour3DPlotLayer::paint: sx " << sx << ", sy " << sy << ", cache w " << m_cache->width() << ", cache h " << m_cache->height() << std::endl;
+	    if (sx >= 0 && sy >= 0 &&
+		sx < m_cache->width() && sy < m_cache->height()) {
+		scaled.setPixel(x, y, m_cache->pixel(sx, sy));
+	    } else {
+		scaled.setPixel(x, y, qRgba(255, 255, 255, 80));
+	    }
+	}
+    }
+
+    paint.drawImage(x0, 0, scaled);
+*/
+}
+
+#ifdef INCLUDE_MOCFILES
+#include "Colour3DPlotLayer.moc.cpp"
+#endif
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/Colour3DPlotLayer.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,71 @@
+/* -*- 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.
+*/
+
+#ifndef _COLOUR_3D_PLOT_H_
+#define _COLOUR_3D_PLOT_H_
+
+#include "base/Layer.h"
+
+#include "model/DenseThreeDimensionalModel.h"
+
+class View;
+class QPainter;
+class QImage;
+
+/**
+ * This is a view that displays dense 3-D data (time, some sort of
+ * binned y-axis range, value) as a colour plot with value mapped to
+ * colour range.  Its source is a DenseThreeDimensionalModel.
+ *
+ * This was the original implementation for the spectrogram view, but
+ * it was replaced with a more efficient implementation that derived
+ * the spectrogram itself from a DenseTimeValueModel instead of using
+ * a three-dimensional model.  This class is retained in case it
+ * becomes useful, but it will probably need some cleaning up if it's
+ * ever actually used.
+ */
+
+class Colour3DPlotLayer : public Layer
+{
+    Q_OBJECT
+
+public:
+    Colour3DPlotLayer(View *w);
+    ~Colour3DPlotLayer();
+
+    virtual const ZoomConstraint *getZoomConstraint() const { return m_model; }
+    virtual const Model *getModel() const { return m_model; }
+    virtual void paint(QPainter &paint, QRect rect) const;
+
+    void setModel(const DenseThreeDimensionalModel *model);
+
+
+/*
+    virtual PropertyList getProperties() const;
+    virtual PropertyType getPropertyType(const PropertyName &) const;
+    virtual int getPropertyRangeAndValue(const PropertyName &,
+					   int *min, int *max) const;
+    virtual QString getPropertyValueLabel(const PropertyName &,
+					  int value) const;
+    virtual void setProperty(const PropertyName &, int value);
+*/
+
+    virtual QString getPropertyContainerIconName() const { return "colour3d"; }
+
+protected slots:
+    void cacheInvalid();
+    void cacheInvalid(size_t startFrame, size_t endFrame);
+
+protected:
+    const DenseThreeDimensionalModel *m_model; // I do not own this
+    
+    mutable QImage *m_cache;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/LayerFactory.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,180 @@
+/* -*- 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 "LayerFactory.h"
+
+#include "WaveformLayer.h"
+#include "SpectrogramLayer.h"
+#include "TimeRulerLayer.h"
+#include "TimeInstantLayer.h"
+#include "TimeValueLayer.h"
+#include "Colour3DPlotLayer.h"
+
+#include "model/RangeSummarisableTimeValueModel.h"
+#include "model/DenseTimeValueModel.h"
+#include "model/SparseOneDimensionalModel.h"
+#include "model/SparseTimeValueModel.h"
+#include "model/DenseThreeDimensionalModel.h"
+
+LayerFactory *
+LayerFactory::m_instance = new LayerFactory;
+
+LayerFactory *
+LayerFactory::instance()
+{
+    return m_instance;
+}
+
+LayerFactory::~LayerFactory()
+{
+}
+
+QString
+LayerFactory::getLayerPresentationName(LayerType type)
+{
+    switch (type) {
+    case Waveform:     return Layer::tr("Waveform");
+    case Spectrogram:  return Layer::tr("Spectrogram");
+    case TimeRuler:    return Layer::tr("Ruler");
+    case TimeInstants: return Layer::tr("Time Instants");
+    case TimeValues:   return Layer::tr("Time Values");
+    case Colour3DPlot: return Layer::tr("Colour 3D Plot");
+
+    case MelodicRangeSpectrogram:
+	// The user can change all the parameters of this after the
+	// fact -- there's nothing permanently melodic-range about it
+	// that should be encoded in its name
+	return Layer::tr("Spectrogram");
+    }
+
+    return Layer::tr("Layer");
+}
+
+LayerFactory::LayerTypeSet
+LayerFactory::getValidLayerTypes(Model *model)
+{
+    LayerTypeSet types;
+
+    if (dynamic_cast<DenseThreeDimensionalModel *>(model)) {
+	types.insert(Colour3DPlot);
+    }
+
+    if (dynamic_cast<DenseTimeValueModel *>(model)) {
+	types.insert(Spectrogram);
+	types.insert(MelodicRangeSpectrogram);
+    }
+
+    if (dynamic_cast<RangeSummarisableTimeValueModel *>(model)) {
+	types.insert(Waveform);
+    }
+
+    if (dynamic_cast<SparseOneDimensionalModel *>(model)) {
+	types.insert(TimeInstants);
+    }
+
+    if (dynamic_cast<SparseTimeValueModel *>(model)) {
+	types.insert(TimeValues);
+    }
+
+    // We don't count TimeRuler here as it doesn't actually display
+    // the data, although it can be backed by any model
+
+    return types;
+}
+
+LayerFactory::LayerType
+LayerFactory::getLayerType(Layer *layer)
+{
+    if (dynamic_cast<WaveformLayer *>(layer)) return Waveform;
+    if (dynamic_cast<SpectrogramLayer *>(layer)) return Spectrogram;
+    if (dynamic_cast<TimeRulerLayer *>(layer)) return TimeRuler;
+    if (dynamic_cast<TimeInstantLayer *>(layer)) return TimeInstants;
+    if (dynamic_cast<TimeValueLayer *>(layer)) return TimeValues;
+    if (dynamic_cast<Colour3DPlotLayer *>(layer)) return Colour3DPlot;
+    return UnknownLayer;
+}
+
+void
+LayerFactory::setModel(Layer *layer, Model *model)
+{
+    if (trySetModel<WaveformLayer, RangeSummarisableTimeValueModel>(layer, model))
+	return;
+
+    if (trySetModel<SpectrogramLayer, DenseTimeValueModel>(layer, model))
+	return;
+
+    if (trySetModel<TimeRulerLayer, Model>(layer, model))
+	return;
+
+    if (trySetModel<TimeInstantLayer, SparseOneDimensionalModel>(layer, model))
+	return;
+
+    if (trySetModel<TimeValueLayer, SparseTimeValueModel>(layer, model))
+	return;
+
+    if (trySetModel<Colour3DPlotLayer, DenseThreeDimensionalModel>(layer, model))
+	return;
+
+    if (trySetModel<SpectrogramLayer, DenseTimeValueModel>(layer, model))
+	return;
+}
+
+Layer *
+LayerFactory::createLayer(LayerType type, View *view,
+			  Model *model, int channel)
+{
+    Layer *layer = 0;
+
+    switch (type) {
+
+    case Waveform:
+	layer = new WaveformLayer(view);
+	static_cast<WaveformLayer *>(layer)->setChannel(channel);
+	break;
+
+    case Spectrogram:
+	layer = new SpectrogramLayer(view);
+	static_cast<SpectrogramLayer *>(layer)->setChannel(channel);
+	break;
+
+    case TimeRuler:
+	layer = new TimeRulerLayer(view);
+	break;
+
+    case TimeInstants:
+	layer = new TimeInstantLayer(view);
+	break;
+
+    case TimeValues:
+	layer = new TimeValueLayer(view);
+	break;
+
+    case Colour3DPlot:
+	layer = new Colour3DPlotLayer(view);
+	break;
+
+    case MelodicRangeSpectrogram: 
+	layer = new SpectrogramLayer(view, SpectrogramLayer::MelodicRange);
+	static_cast<SpectrogramLayer *>(layer)->setChannel(channel);
+	break;
+    }
+
+    if (!layer) {
+	std::cerr << "LayerFactory::createLayer: Unknown layer type " 
+		  << type << std::endl;
+    } else {
+	setModel(layer, model);
+	std::cerr << "LayerFactory::createLayer: Setting object name "
+		  << getLayerPresentationName(type).toStdString() << " on " << layer << std::endl;
+	layer->setObjectName(getLayerPresentationName(type));
+    }
+
+    return layer;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/LayerFactory.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,70 @@
+/* -*- 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.
+*/
+
+#ifndef _LAYER_FACTORY_H_
+#define _LAYER_FACTORY_H_
+
+#include <QString>
+#include <set>
+
+class Layer;
+class View;
+class Model;
+
+class LayerFactory
+{
+public:
+    enum LayerType {
+
+	// Standard layers
+	Waveform,
+	Spectrogram,
+	TimeRuler,
+	TimeInstants,
+	TimeValues,
+	Colour3DPlot,
+
+	// Layers with different initial parameters
+	MelodicRangeSpectrogram,
+
+	// Not-a-layer-type
+	UnknownLayer = 255
+    };
+
+    static LayerFactory *instance();
+    
+    virtual ~LayerFactory();
+
+    typedef std::set<LayerType> LayerTypeSet;
+    LayerTypeSet getValidLayerTypes(Model *model);
+
+    LayerType getLayerType(Layer *);
+
+    Layer *createLayer(LayerType type, View *view,
+		       Model *model = 0, int channel = -1);
+
+    QString getLayerPresentationName(LayerType type);
+
+    void setModel(Layer *layer, Model *model);
+
+protected:
+    template <typename LayerClass, typename ModelClass>
+    bool trySetModel(Layer *layerBase, Model *modelBase) {
+	LayerClass *layer = dynamic_cast<LayerClass *>(layerBase);
+	if (!layer) return false;
+	ModelClass *model = dynamic_cast<ModelClass *>(modelBase);
+	layer->setModel(model);
+	return true;
+    }
+
+    static LayerFactory *m_instance;
+};
+
+#endif
+
--- /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
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/SpectrogramLayer.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,215 @@
+/* -*- 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.
+*/
+
+#ifndef _SPECTROGRAM_VIEW_H_
+#define _SPECTROGRAM_VIEW_H_
+
+#include "base/Layer.h"
+#include "base/Window.h"
+#include "model/PowerOfSqrtTwoZoomConstraint.h"
+#include "model/DenseTimeValueModel.h"
+
+#include <QThread>
+#include <QMutex>
+#include <QWaitCondition>
+
+#include <fftw3.h>
+
+class View;
+class QPainter;
+class QImage;
+class QPixmap;
+class QTimer;
+class RealTime;
+
+/**
+ * SpectrogramLayer represents waveform data (obtained from a
+ * DenseTimeValueModel) in spectrogram form.
+ */
+
+class SpectrogramLayer : public Layer,
+			public PowerOfSqrtTwoZoomConstraint
+{
+    Q_OBJECT
+
+public:
+    enum Configuration { FullRangeDb, MelodicRange };
+    
+    SpectrogramLayer(View *w, Configuration = FullRangeDb);
+    ~SpectrogramLayer();
+
+    virtual const ZoomConstraint *getZoomConstraint() const { return this; }
+    virtual const Model *getModel() const { return m_model; }
+    virtual void paint(QPainter &paint, QRect rect) const;
+
+    virtual int getVerticalScaleWidth(QPainter &) const;
+    virtual void paintVerticalScale(QPainter &paint, QRect rect) const;
+
+    virtual QRect getFeatureDescriptionRect(QPainter &, QPoint) const;
+    virtual void paintLocalFeatureDescription(QPainter &, QRect, QPoint) const;
+
+    void setModel(const DenseTimeValueModel *model);
+
+    virtual PropertyList getProperties() const;
+    virtual PropertyType getPropertyType(const PropertyName &) const;
+    virtual QString getPropertyGroupName(const PropertyName &) const;
+    virtual int getPropertyRangeAndValue(const PropertyName &,
+					   int *min, int *max) const;
+    virtual QString getPropertyValueLabel(const PropertyName &,
+					  int value) const;
+    virtual void setProperty(const PropertyName &, int value);
+
+    /**
+     * Specify the channel to use from the source model.
+     * A value of -1 means to mix all available channels.
+     * The default is channel 0.
+     */
+    void setChannel(int);
+    int getChannel() const;
+
+    void setWindowSize(size_t);
+    size_t getWindowSize() const;
+    
+    void setWindowOverlap(size_t percent);
+    size_t getWindowOverlap() const;
+
+    void setWindowType(WindowType type);
+    WindowType getWindowType() const;
+
+    /**
+     * Set the gain multiplier for sample values in this view prior to
+     * FFT calculation.
+     *
+     * The default is 1.0.
+     */
+    void setGain(float gain);
+    float getGain() const;
+
+    void setMaxFrequency(size_t); // 0 -> no maximum
+    size_t getMaxFrequency() const;
+
+    enum ColourScale { LinearColourScale, MeterColourScale, dBColourScale,
+		       PhaseColourScale };
+
+    /**
+     * Specify the scale for sample levels.  See WaveformLayer for
+     * details of meter and dB scaling.  The default is dBColourScale.
+     */
+    void setColourScale(ColourScale);
+    ColourScale getColourScale() const;
+
+    enum FrequencyScale { LinearFrequencyScale, LogFrequencyScale };
+    
+    /**
+     * Specify the scale for the y axis.
+     */
+    void setFrequencyScale(FrequencyScale);
+    FrequencyScale getFrequencyScale() const;
+
+    enum ColourScheme { DefaultColours, WhiteOnBlack, BlackOnWhite,
+			RedOnBlue, YellowOnBlack, RedOnBlack };
+
+    void setColourScheme(ColourScheme scheme);
+    ColourScheme getColourScheme() const;
+
+    virtual VerticalPosition getPreferredFrameCountPosition() const {
+	return PositionTop;
+    }
+
+    virtual int getCompletion() const;
+
+    virtual QString getPropertyContainerIconName() const { return "spectrogram"; }
+
+protected slots:
+    void cacheInvalid();
+    void cacheInvalid(size_t startFrame, size_t endFrame);
+
+    void fillTimerTimedOut();
+
+protected:
+    const DenseTimeValueModel *m_model; // I do not own this
+    
+    int m_channel;
+    size_t m_windowSize;
+    WindowType m_windowType;
+    size_t m_windowOverlap;
+    float m_gain;
+    size_t m_maxFrequency;
+    ColourScale m_colourScale;
+    ColourScheme m_colourScheme;
+    FrequencyScale m_frequencyScale;
+
+    class CacheFillThread : public QThread
+    {
+    public:
+	CacheFillThread(SpectrogramLayer &layer) :
+	    m_layer(layer), m_fillExtent(0) { }
+
+	size_t getFillExtent() const { return m_fillExtent; }
+	size_t getFillCompletion() const { return m_fillCompletion; }
+	virtual void run();
+
+    protected:
+	SpectrogramLayer &m_layer;
+	size_t m_fillExtent;
+	size_t m_fillCompletion;
+    };
+
+    void fillCache();
+    
+    QImage *m_cache;
+    bool m_cacheInvalid;
+    size_t m_maxCachedFrequency;
+
+    mutable QPixmap *m_pixmapCache;
+    mutable bool m_pixmapCacheInvalid;
+    mutable long m_pixmapCacheStartFrame;
+    mutable size_t m_pixmapCacheZoomLevel;
+
+    QWaitCondition m_condition;
+    mutable QMutex m_mutex;
+
+    CacheFillThread *m_fillThread;
+    QTimer *m_updateTimer;
+    size_t m_lastFillExtent;
+    bool m_cachedInitialVisibleArea;
+    bool m_exiting;
+
+    void setCacheColourmap();
+
+    bool fillCacheColumn(int column,
+			 double *inputBuffer,
+			 fftw_complex *outputBuffer,
+			 fftw_plan plan,
+			 const Window<double> &windower,
+			 bool lock)
+	const;
+
+    bool getYBinRange(int y, float &freqBinMin, float &freqBinMax) const;
+
+    struct LayerRange {
+	long   startFrame;
+	int    zoomLevel;
+	size_t modelStart;
+	size_t modelEnd;
+    };
+    /// LayerRange is only passed in to save lookup time
+    bool getXBinRange(int x, float &windowMin, float &windowMax,
+		      LayerRange *range = 0) const;
+
+    bool getYBinSourceRange(int y, float &freqMin, float &freqMax) const;
+    bool getXBinSourceRange(int x, RealTime &timeMin, RealTime &timeMax) const;
+    bool getXYBinSourceRange(int x, int y, float &dbMin, float &dbMax) const;
+
+    size_t getWindowIncrement() const {
+	return m_windowSize - m_windowSize * m_windowOverlap / 100;
+    }
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/TimeInstantLayer.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,305 @@
+/* -*- 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 "TimeInstantLayer.h"
+
+#include "base/Model.h"
+#include "base/RealTime.h"
+#include "base/View.h"
+#include "base/Profiler.h"
+
+#include "model/SparseOneDimensionalModel.h"
+
+#include <QPainter>
+
+#include <iostream>
+
+TimeInstantLayer::TimeInstantLayer(View *w) :
+    Layer(w),
+    m_model(0),
+    m_colour(QColor(200, 50, 255))
+{
+    m_view->addLayer(this);
+}
+
+void
+TimeInstantLayer::setModel(SparseOneDimensionalModel *model)
+{
+    if (m_model == model) return;
+    m_model = model;
+
+    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()));
+
+    std::cerr << "TimeInstantLayer::setModel(" << model << ")" << std::endl;
+
+    emit modelReplaced();
+}
+
+Layer::PropertyList
+TimeInstantLayer::getProperties() const
+{
+    PropertyList list;
+    list.push_back(tr("Colour"));
+    return list;
+}
+
+Layer::PropertyType
+TimeInstantLayer::getPropertyType(const PropertyName &name) const
+{
+    return ValueProperty;
+}
+
+int
+TimeInstantLayer::getPropertyRangeAndValue(const PropertyName &name,
+					 int *min, int *max) const
+{
+    int deft = 0;
+
+    if (name == tr("Colour")) {
+
+	*min = 0;
+	*max = 5;
+
+	if (m_colour == Qt::black) deft = 0;
+	else if (m_colour == Qt::darkRed) deft = 1;
+	else if (m_colour == Qt::darkBlue) deft = 2;
+	else if (m_colour == Qt::darkGreen) deft = 3;
+	else if (m_colour == QColor(200, 50, 255)) deft = 4;
+	else if (m_colour == QColor(255, 150, 50)) deft = 5;
+
+    } else {
+	
+	deft = Layer::getPropertyRangeAndValue(name, min, max);
+    }
+
+    return deft;
+}
+
+QString
+TimeInstantLayer::getPropertyValueLabel(const PropertyName &name,
+				    int value) const
+{
+    if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0: return tr("Black");
+	case 1: return tr("Red");
+	case 2: return tr("Blue");
+	case 3: return tr("Green");
+	case 4: return tr("Purple");
+	case 5: return tr("Orange");
+	}
+    }
+    return tr("<unknown>");
+}
+
+void
+TimeInstantLayer::setProperty(const PropertyName &name, int value)
+{
+    if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0:	setBaseColour(Qt::black); break;
+	case 1: setBaseColour(Qt::darkRed); break;
+	case 2: setBaseColour(Qt::darkBlue); break;
+	case 3: setBaseColour(Qt::darkGreen); break;
+	case 4: setBaseColour(QColor(200, 50, 255)); break;
+	case 5: setBaseColour(QColor(255, 150, 50)); break;
+	}
+    }
+}
+
+void
+TimeInstantLayer::setBaseColour(QColor colour)
+{
+    if (m_colour == colour) return;
+    m_colour = colour;
+    emit layerParametersChanged();
+}
+
+bool
+TimeInstantLayer::isLayerScrollable() const
+{
+    QPoint discard;
+    return !m_view->shouldIlluminateLocalFeatures(this, discard);
+}
+
+QRect
+TimeInstantLayer::getFeatureDescriptionRect(QPainter &paint, QPoint pos) const
+{
+    return QRect(0, 0,
+		 std::max(100, paint.fontMetrics().width(tr("No local points"))),
+		 50); //!!! cruddy
+}
+
+SparseOneDimensionalModel::PointList
+TimeInstantLayer::getLocalPoints(int x) const
+{
+    if (!m_model) return SparseOneDimensionalModel::PointList();
+
+    long startFrame = m_view->getStartFrame();
+    long endFrame = m_view->getEndFrame();
+    int zoomLevel = m_view->getZoomLevel();
+    long frame = startFrame + x * zoomLevel;
+
+    SparseOneDimensionalModel::PointList onPoints =
+	m_model->getPoints(frame);
+
+    if (!onPoints.empty()) {
+	return onPoints;
+    }
+
+    SparseOneDimensionalModel::PointList prevPoints =
+	m_model->getPreviousPoints(frame);
+    SparseOneDimensionalModel::PointList nextPoints =
+	m_model->getNextPoints(frame);
+
+    SparseOneDimensionalModel::PointList usePoints = prevPoints;
+
+    if (prevPoints.empty()) {
+	usePoints = nextPoints;
+    } else if (prevPoints.begin()->frame < startFrame &&
+	       !(nextPoints.begin()->frame > endFrame)) {
+	usePoints = nextPoints;
+    } else if (nextPoints.begin()->frame - frame <
+	       frame - prevPoints.begin()->frame) {
+	usePoints = nextPoints;
+    }
+
+    return usePoints;
+}
+
+void
+TimeInstantLayer::paintLocalFeatureDescription(QPainter &paint, QRect rect,
+					       QPoint pos) const
+{
+    //!!! bleagh
+
+    int x = pos.x();
+    
+    if (!m_model || !m_model->getSampleRate()) return;
+
+    SparseOneDimensionalModel::PointList points = getLocalPoints(x);
+
+    QFontMetrics metrics = paint.fontMetrics();
+    int xbase = rect.x() + 5;
+    int ybase = rect.y() + 5;
+
+    if (points.empty()) {
+	QString label = tr("No local points");
+	if (!m_model->isReady()) {
+	    label = tr("In progress");
+	}
+	paint.drawText(xbase + 5, ybase + 5 + metrics.ascent(), label);
+	return;
+    }
+
+    long useFrame = points.begin()->frame;
+
+    RealTime rt = RealTime::frame2RealTime(useFrame, m_model->getSampleRate());
+    QString timeText = QString("%1").arg(rt.toText(true).c_str());
+
+    int timewidth = metrics.width(timeText);
+    int labelwidth = metrics.width(points.begin()->label);
+
+    int boxheight = metrics.height() * 2 + 3;
+    int boxwidth = std::max(timewidth, labelwidth);
+
+    paint.drawRect(xbase, ybase, boxwidth + 10,
+		   boxheight + 10 - metrics.descent() + 1);
+
+    paint.drawText(xbase + 5, ybase + 5 + metrics.ascent(), timeText);
+    paint.drawText(xbase + 5, ybase + 7 + metrics.ascent() + metrics.height(),
+		   points.begin()->label);
+}
+
+void
+TimeInstantLayer::paint(QPainter &paint, QRect rect) const
+{
+    if (!m_model || !m_model->isOK()) return;
+
+//    Profiler profiler("TimeInstantLayer::paint", true);
+
+    long startFrame = m_view->getStartFrame();
+    int zoomLevel = m_view->getZoomLevel();
+
+    int x0 = rect.left(), x1 = rect.right();
+    long frame0 = startFrame + x0 * zoomLevel;
+    long frame1 = startFrame + x1 * zoomLevel;
+
+    SparseOneDimensionalModel::PointList points(m_model->getPoints
+						(frame0, frame1));
+
+    paint.setPen(m_colour);
+
+    QColor brushColour(m_colour);
+    brushColour.setAlpha(100);
+    paint.setBrush(brushColour);
+
+//    std::cerr << "TimeInstantLayer::paint: resolution is "
+//	      << m_model->getResolution() << " frames" << std::endl;
+
+    QPoint localPos;
+    long illuminateFrame = -1;
+
+    if (m_view->shouldIlluminateLocalFeatures(this, localPos)) {
+	SparseOneDimensionalModel::PointList localPoints =
+	    getLocalPoints(localPos.x());
+	if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame;
+    }
+	
+    for (SparseOneDimensionalModel::PointList::const_iterator i = points.begin();
+	 i != points.end(); ++i) {
+
+	const SparseOneDimensionalModel::Point &p(*i);
+
+	int x = (p.frame - startFrame) / zoomLevel;
+	int w = m_model->getResolution() / zoomLevel;
+
+	if (w < 1) w = 1;
+	if (p.frame == illuminateFrame) {
+	    paint.setPen(Qt::black); //!!!
+	} else {
+	    paint.setPen(brushColour);
+	}
+	paint.drawRect(x, 0, w - 1, m_view->height() - 1);
+	paint.setPen(m_colour);
+	
+	if (p.label != "") {
+
+	    // only draw if there's enough room from here to the next point
+
+	    int lw = paint.fontMetrics().width(p.label);
+	    bool good = true;
+
+	    SparseOneDimensionalModel::PointList::const_iterator j = i;
+	    if (++j != points.end()) {
+		int nx = (j->frame - startFrame) / zoomLevel;
+		if (nx >= x && nx - x - w - 3 <= lw) good = false;
+	    }
+
+	    if (good) {
+		paint.drawText(x + w + 2,
+			       m_view->height() - paint.fontMetrics().height(),
+			       p.label);
+	    }
+	}
+    }
+}
+
+
+#ifdef INCLUDE_MOCFILES
+#include "TimeInstantLayer.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/TimeInstantLayer.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,62 @@
+/* -*- 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.
+*/
+
+#ifndef _TIME_INSTANT_VIEW_H_
+#define _TIME_INSTANT_VIEW_H_
+
+#include "base/Layer.h"
+#include "model/SparseOneDimensionalModel.h"
+
+#include <QObject>
+#include <QColor>
+
+class View;
+class QPainter;
+
+class TimeInstantLayer : public Layer
+{
+    Q_OBJECT
+
+public:
+    TimeInstantLayer(View *w);
+
+    virtual void paint(QPainter &paint, QRect rect) const;
+
+    virtual QRect getFeatureDescriptionRect(QPainter &, QPoint) const;
+    virtual void paintLocalFeatureDescription(QPainter &, QRect, QPoint) const;
+
+    virtual const Model *getModel() const { return m_model; }
+    void setModel(SparseOneDimensionalModel *model);
+
+    virtual PropertyList getProperties() const;
+    virtual PropertyType getPropertyType(const PropertyName &) const;
+    virtual int getPropertyRangeAndValue(const PropertyName &,
+					   int *min, int *max) const;
+    virtual QString getPropertyValueLabel(const PropertyName &,
+					  int value) const;
+    virtual void setProperty(const PropertyName &, int value);
+
+    void setBaseColour(QColor);
+    QColor getBaseColour() const { return m_colour; }
+
+    virtual QString getPropertyContainerIconName() const { return "instants"; }
+
+    virtual bool isLayerScrollable() const;
+
+    virtual int getCompletion() const { return m_model->getCompletion(); }
+
+protected:
+    SparseOneDimensionalModel::PointList getLocalPoints(int) const;
+
+    SparseOneDimensionalModel *m_model;
+    QColor m_colour;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/TimeRulerLayer.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,288 @@
+/* -*- 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 "TimeRulerLayer.h"
+
+#include "base/Model.h"
+#include "base/RealTime.h"
+#include "base/View.h"
+
+#include <QPainter>
+
+#include <iostream>
+
+using std::cerr;
+using std::endl;
+
+TimeRulerLayer::TimeRulerLayer(View *w) :
+    Layer(w),
+    m_model(0),
+    m_colour(Qt::black),
+    m_labelHeight(LabelTop)
+{
+    m_view->addLayer(this);
+}
+
+void
+TimeRulerLayer::setModel(Model *model)
+{
+    if (m_model == model) return;
+    m_model = model;
+    emit modelReplaced();
+}
+
+void
+TimeRulerLayer::setBaseColour(QColor colour)
+{
+    if (m_colour == colour) return;
+    m_colour = colour;
+    emit layerParametersChanged();
+}
+
+Layer::PropertyList
+TimeRulerLayer::getProperties() const
+{
+    PropertyList list;
+    list.push_back(tr("Colour"));
+    return list;
+}
+
+Layer::PropertyType
+TimeRulerLayer::getPropertyType(const PropertyName &name) const
+{
+    return ValueProperty;
+}
+
+int
+TimeRulerLayer::getPropertyRangeAndValue(const PropertyName &name,
+					 int *min, int *max) const
+{
+    int deft = 0;
+
+    if (name == tr("Colour")) {
+
+	*min = 0;
+	*max = 5;
+
+	if (m_colour == Qt::black) deft = 0;
+	else if (m_colour == Qt::darkRed) deft = 1;
+	else if (m_colour == Qt::darkBlue) deft = 2;
+	else if (m_colour == Qt::darkGreen) deft = 3;
+	else if (m_colour == QColor(200, 50, 255)) deft = 4;
+	else if (m_colour == QColor(255, 150, 50)) deft = 5;
+
+    } else {
+	
+	deft = Layer::getPropertyRangeAndValue(name, min, max);
+    }
+
+    return deft;
+}
+
+QString
+TimeRulerLayer::getPropertyValueLabel(const PropertyName &name,
+				    int value) const
+{
+    if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0: return tr("Black");
+	case 1: return tr("Red");
+	case 2: return tr("Blue");
+	case 3: return tr("Green");
+	case 4: return tr("Purple");
+	case 5: return tr("Orange");
+	}
+    }
+    return tr("<unknown>");
+}
+
+void
+TimeRulerLayer::setProperty(const PropertyName &name, int value)
+{
+    if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0:	setBaseColour(Qt::black); break;
+	case 1: setBaseColour(Qt::darkRed); break;
+	case 2: setBaseColour(Qt::darkBlue); break;
+	case 3: setBaseColour(Qt::darkGreen); break;
+	case 4: setBaseColour(QColor(200, 50, 255)); break;
+	case 5: setBaseColour(QColor(255, 150, 50)); break;
+	}
+    }
+}
+
+void
+TimeRulerLayer::paint(QPainter &paint, QRect rect) const
+{
+//    std::cerr << "TimeRulerLayer::paint (" << rect.x() << "," << rect.y()
+//	      << ") [" << rect.width() << "x" << rect.height() << "]" << std::endl;
+    
+    if (!m_model || !m_model->isOK()) return;
+
+    int sampleRate = m_model->getSampleRate();
+    if (!sampleRate) return;
+
+    long startFrame = m_view->getStartFrame();
+    long endFrame = m_view->getEndFrame();
+
+    int zoomLevel = m_view->getZoomLevel();
+
+    long rectStart = startFrame + (rect.x() - 100) * zoomLevel;
+    long rectEnd = startFrame + (rect.x() + rect.width() + 100) * zoomLevel;
+    if (rectStart < startFrame) rectStart = startFrame;
+    if (rectEnd > endFrame) rectEnd = endFrame;
+
+//    std::cerr << "TimeRulerLayer::paint: calling paint.save()" << std::endl;
+    paint.save();
+//!!!    paint.setClipRect(m_view->rect());
+
+    int minPixelSpacing = 50;
+
+    RealTime rtStart = RealTime::frame2RealTime(startFrame, sampleRate);
+    RealTime rtEnd = RealTime::frame2RealTime(endFrame, sampleRate);
+//    cerr << "startFrame " << startFrame << ", endFrame " << m_view->getEndFrame() << ", rtStart " << rtStart << ", rtEnd " << rtEnd << endl;
+    int count = m_view->width() / minPixelSpacing;
+    if (count < 1) count = 1;
+    RealTime rtGap = (rtEnd - rtStart) / count;
+//    cerr << "rtGap is " << rtGap << endl;
+
+    int incms;
+    bool quarter = false;
+
+    if (rtGap.sec > 0) {
+	incms = 1000;
+	int s = rtGap.sec;
+	if (s > 0) { incms *= 5; s /= 5; }
+	if (s > 0) { incms *= 2; s /= 2; }
+	if (s > 0) { incms *= 6; s /= 6; quarter = true; }
+	if (s > 0) { incms *= 5; s /= 5; quarter = false; }
+	if (s > 0) { incms *= 2; s /= 2; }
+	if (s > 0) { incms *= 6; s /= 6; quarter = true; }
+	while (s > 0) {
+	    incms *= 10;
+	    s /= 10;
+	    quarter = false;
+	}
+    } else {
+	incms = 1;
+	int ms = rtGap.msec();
+	if (ms > 0) { incms *= 10; ms /= 10; }
+	if (ms > 0) { incms *= 10; ms /= 10; }
+	if (ms > 0) { incms *= 5; ms /= 5; }
+	if (ms > 0) { incms *= 2; ms /= 2; }
+    }
+//    cerr << "incms is " << incms << endl;
+
+    RealTime rt = RealTime::frame2RealTime(rectStart, sampleRate);
+    long ms = rt.sec * 1000 + rt.msec();
+    ms = (ms / incms) * incms - incms;
+
+    RealTime incRt = RealTime(incms / 1000, (incms % 1000) * 1000000);
+    long incFrame = RealTime::realTime2Frame(incRt, sampleRate);
+    int incX = incFrame / zoomLevel;
+    int ticks = 10;
+    if (incX < minPixelSpacing * 2) {
+	ticks = quarter ? 4 : 5;
+    }
+
+    QRect oldClipRect = rect;
+    QRect newClipRect(oldClipRect.x() - 25, oldClipRect.y(),
+		      oldClipRect.width() + 50, oldClipRect.height());
+    paint.setClipRect(newClipRect);
+
+    QColor greyColour(m_colour);
+    if (m_colour == Qt::black) {
+	greyColour = QColor(200,200,200);
+    } else {
+	greyColour = m_colour.light(150);
+    }
+
+    while (1) {
+
+	rt = RealTime(ms / 1000, (ms % 1000) * 1000000);
+	ms += incms;
+
+	long frame = RealTime::realTime2Frame(rt, sampleRate);
+	if (frame >= rectEnd) break;
+
+	int x = (frame - startFrame) / zoomLevel;
+	if (x < rect.x() || x >= rect.x() + rect.width()) continue;
+
+	paint.setPen(greyColour);
+	paint.drawLine(x, 0, x, m_view->height());
+
+	paint.setPen(m_colour);
+	paint.drawLine(x, 0, x, 5);
+	paint.drawLine(x, m_view->height() - 6, x, m_view->height() - 1);
+
+	QString text(QString::fromStdString(rt.toText()));
+
+	int y;
+	QFontMetrics metrics = paint.fontMetrics();
+	switch (m_labelHeight) {
+	default:
+	case LabelTop:
+	    y = 6 + metrics.ascent();
+	    break;
+	case LabelMiddle:
+	    y = m_view->height() / 2 - metrics.height() / 2 + metrics.ascent();
+	    break;
+	case LabelBottom:
+	    y = m_view->height() - metrics.height() + metrics.ascent() - 6;
+	}
+
+	int tw = metrics.width(text);
+
+	paint.setPen(m_view->palette().background().color());
+
+	//!!! simple drawing function for this please
+	//!!! and need getContrastingColour() in widget, or use the
+	//palette properly -- get the base class able to draw text
+	//using the proper colour (or this technique) automatically
+	for (int dx = -1; dx <= 1; ++dx) {
+	    for (int dy = -1; dy <= 1; ++dy) {
+		if ((dx && dy) || !(dx || dy)) continue;
+		paint.drawText(x + 2 - tw / 2 + dx, y + dy, text);
+	    }
+	}
+	
+	paint.setPen(m_colour);
+	paint.drawText(x + 2 - tw / 2, y, text);
+		
+	paint.setPen(greyColour);
+
+	for (int i = 1; i < ticks; ++i) {
+	    rt = rt + (incRt / ticks);
+	    frame = RealTime::realTime2Frame(rt, sampleRate);
+	    x = (frame - startFrame) / zoomLevel;
+	    int sz = 5;
+	    if (ticks == 10) {
+		if ((i % 2) == 1) {
+		    if (i == 5) {
+			paint.drawLine(x, 0, x, m_view->height());
+		    } else sz = 3;
+		} else {
+		    sz = 7;
+		}
+	    }
+	    paint.drawLine(x, 0, x, sz);
+	    paint.drawLine(x, m_view->height() - sz - 1, x, m_view->height() - 1);
+	}
+    }
+
+    paint.restore();
+}
+    
+
+#ifdef INCLUDE_MOCFILES
+#include "TimeRulerLayer.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/TimeRulerLayer.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,57 @@
+/* -*- 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.
+*/
+
+#ifndef _TIME_RULER_H_
+#define _TIME_RULER_H_
+
+#include "base/Layer.h"
+
+#include <QRect>
+#include <QColor>
+
+class View;
+class Model;
+class QPainter;
+
+class TimeRulerLayer : public Layer
+{
+    Q_OBJECT
+
+public:
+    TimeRulerLayer(View *w);
+
+    virtual void paint(QPainter &paint, QRect rect) const;
+
+    void setModel(Model *);
+    virtual const Model *getModel() const { return m_model; }
+
+    void setBaseColour(QColor);
+    QColor getBaseColour() const { return m_colour; }
+
+    enum LabelHeight { LabelTop, LabelMiddle, LabelBottom };
+    void setLabelHeight(LabelHeight h) { m_labelHeight = h; }
+    LabelHeight getLabelHeight() const { return m_labelHeight; }
+
+    virtual PropertyList getProperties() const;
+    virtual PropertyType getPropertyType(const PropertyName &) const;
+    virtual int getPropertyRangeAndValue(const PropertyName &,
+					   int *min, int *max) const;
+    virtual QString getPropertyValueLabel(const PropertyName &,
+					  int value) const;
+    virtual void setProperty(const PropertyName &, int value);
+
+    virtual QString getPropertyContainerIconName() const { return "timeruler"; }
+
+protected:
+    Model *m_model;
+    QColor m_colour;
+    LabelHeight m_labelHeight;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/TimeValueLayer.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,378 @@
+/* -*- 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 "TimeValueLayer.h"
+
+#include "base/Model.h"
+#include "base/RealTime.h"
+#include "base/Profiler.h"
+#include "base/View.h"
+
+#include "model/SparseTimeValueModel.h"
+
+#include <QPainter>
+
+#include <iostream>
+#include <cmath>
+
+TimeValueLayer::TimeValueLayer(View *w) :
+    Layer(w),
+    m_model(0),
+    m_colour(Qt::black),
+    m_plotStyle(PlotLines)
+{
+    m_view->addLayer(this);
+}
+
+void
+TimeValueLayer::setModel(SparseTimeValueModel *model)
+{
+    if (m_model == model) return;
+    m_model = model;
+
+    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()));
+
+    std::cerr << "TimeValueLayer::setModel(" << model << ")" << std::endl;
+
+    emit modelReplaced();
+}
+
+Layer::PropertyList
+TimeValueLayer::getProperties() const
+{
+    PropertyList list;
+    list.push_back(tr("Colour"));
+    list.push_back(tr("Plot Type"));
+    return list;
+}
+
+Layer::PropertyType
+TimeValueLayer::getPropertyType(const PropertyName &name) const
+{
+    return ValueProperty;
+}
+
+int
+TimeValueLayer::getPropertyRangeAndValue(const PropertyName &name,
+					 int *min, int *max) const
+{
+    //!!! factor this colour handling stuff out into a colour manager class
+
+    int deft = 0;
+
+    if (name == tr("Colour")) {
+
+	*min = 0;
+	*max = 5;
+
+	if (m_colour == Qt::black) deft = 0;
+	else if (m_colour == Qt::darkRed) deft = 1;
+	else if (m_colour == Qt::darkBlue) deft = 2;
+	else if (m_colour == Qt::darkGreen) deft = 3;
+	else if (m_colour == QColor(200, 50, 255)) deft = 4;
+	else if (m_colour == QColor(255, 150, 50)) deft = 5;
+
+    } else if (name == tr("Plot Type")) {
+	
+	*min = 0;
+	*max = 2;
+	
+	deft = int(m_plotStyle);
+
+    } else {
+	
+	deft = Layer::getPropertyRangeAndValue(name, min, max);
+    }
+
+    return deft;
+}
+
+QString
+TimeValueLayer::getPropertyValueLabel(const PropertyName &name,
+				    int value) const
+{
+    if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0: return tr("Black");
+	case 1: return tr("Red");
+	case 2: return tr("Blue");
+	case 3: return tr("Green");
+	case 4: return tr("Purple");
+	case 5: return tr("Orange");
+	}
+    } else if (name == tr("Plot Type")) {
+	switch (value) {
+	default:
+	case 0: return tr("Points");
+	case 1: return tr("Stems");
+	case 2: return tr("Lines");
+	}
+    }
+    return tr("<unknown>");
+}
+
+void
+TimeValueLayer::setProperty(const PropertyName &name, int value)
+{
+    if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0:	setBaseColour(Qt::black); break;
+	case 1: setBaseColour(Qt::darkRed); break;
+	case 2: setBaseColour(Qt::darkBlue); break;
+	case 3: setBaseColour(Qt::darkGreen); break;
+	case 4: setBaseColour(QColor(200, 50, 255)); break;
+	case 5: setBaseColour(QColor(255, 150, 50)); break;
+	}
+    } else if (name == tr("Plot Type")) {
+	setPlotStyle(PlotStyle(value));
+    }
+}
+
+void
+TimeValueLayer::setBaseColour(QColor colour)
+{
+    if (m_colour == colour) return;
+    m_colour = colour;
+    emit layerParametersChanged();
+}
+
+void
+TimeValueLayer::setPlotStyle(PlotStyle style)
+{
+    if (m_plotStyle == style) return;
+    m_plotStyle = style;
+    emit layerParametersChanged();
+}
+
+bool
+TimeValueLayer::isLayerScrollable() const
+{
+    QPoint discard;
+    return !m_view->shouldIlluminateLocalFeatures(this, discard);
+}
+
+QRect
+TimeValueLayer::getFeatureDescriptionRect(QPainter &paint, QPoint pos) const
+{
+    return QRect(0, 0,
+		 std::max(100, paint.fontMetrics().width(tr("No local points"))),
+		 70); //!!!
+}
+
+//!!! too much in common with TimeInstantLayer
+
+SparseTimeValueModel::PointList
+TimeValueLayer::getLocalPoints(int x) const
+{
+    if (!m_model) return SparseTimeValueModel::PointList();
+
+    long startFrame = m_view->getStartFrame();
+    long endFrame = m_view->getEndFrame();
+    int zoomLevel = m_view->getZoomLevel();
+    long frame = startFrame + x * zoomLevel;
+
+    SparseTimeValueModel::PointList onPoints =
+	m_model->getPoints(frame);
+
+    if (!onPoints.empty()) {
+	return onPoints;
+    }
+
+    SparseTimeValueModel::PointList prevPoints =
+	m_model->getPreviousPoints(frame);
+    SparseTimeValueModel::PointList nextPoints =
+	m_model->getNextPoints(frame);
+
+    SparseTimeValueModel::PointList usePoints = prevPoints;
+
+    if (prevPoints.empty()) {
+	usePoints = nextPoints;
+    } else if (prevPoints.begin()->frame < startFrame &&
+	       !(nextPoints.begin()->frame > endFrame)) {
+	usePoints = nextPoints;
+    } else if (nextPoints.begin()->frame - frame <
+	       frame - prevPoints.begin()->frame) {
+	usePoints = nextPoints;
+    }
+
+    return usePoints;
+}
+
+void
+TimeValueLayer::paintLocalFeatureDescription(QPainter &paint, QRect rect,
+					     QPoint pos) const
+{
+    //!!! bleagh
+
+    int x = pos.x();
+    
+    if (!m_model || !m_model->getSampleRate()) return;
+
+    SparseTimeValueModel::PointList points = getLocalPoints(x);
+
+    QFontMetrics metrics = paint.fontMetrics();
+    int xbase = rect.x() + 5;
+    int ybase = rect.y() + 5;
+
+    if (points.empty()) {
+	QString label = tr("No local points");
+	if (!m_model->isReady()) {
+	    label = tr("In progress");
+	}
+	paint.drawText(xbase + 5, ybase + 5 + metrics.ascent(), label);
+	return;
+    }
+
+    long useFrame = points.begin()->frame;
+
+    RealTime rt = RealTime::frame2RealTime(useFrame, m_model->getSampleRate());
+    QString timeText = QString("%1").arg(rt.toText(true).c_str());
+    QString valueText = QString("%1").arg(points.begin()->value);
+
+    int timewidth = metrics.width(timeText);
+    int valuewidth = metrics.width(valueText);
+    int labelwidth = metrics.width(points.begin()->label);
+
+    int boxheight = metrics.height() * 3 + 4;
+    int boxwidth = std::max(std::max(timewidth, labelwidth), valuewidth);
+    
+    paint.drawRect(xbase, ybase, boxwidth + 10,
+		   boxheight + 10 - metrics.descent() + 1);
+
+    paint.drawText(xbase + 5, ybase + 5 + metrics.ascent(), timeText);
+    paint.drawText(xbase + 5, ybase + 7 + metrics.ascent() + metrics.height(),
+		   valueText);
+    paint.drawText(xbase + 5, ybase + 9 + metrics.ascent() + 2*metrics.height(),
+		   points.begin()->label);
+}
+
+void
+TimeValueLayer::paint(QPainter &paint, QRect rect) const
+{
+    if (!m_model || !m_model->isOK()) return;
+
+    int sampleRate = m_model->getSampleRate();
+    if (!sampleRate) return;
+
+//    Profiler profiler("TimeValueLayer::paint", true);
+
+    long startFrame = m_view->getStartFrame();
+    int zoomLevel = m_view->getZoomLevel();
+
+    int x0 = rect.left(), x1 = rect.right();
+    long frame0 = startFrame + x0 * zoomLevel;
+    long frame1 = startFrame + x1 * zoomLevel;
+
+    SparseTimeValueModel::PointList points(m_model->getPoints
+					   (frame0, frame1));
+
+    paint.setPen(m_colour);
+
+    QColor brushColour(m_colour);
+    brushColour.setAlpha(80);
+    paint.setBrush(brushColour);
+
+//    std::cerr << "TimeValueLayer::paint: resolution is "
+//	      << m_model->getResolution() << " frames" << std::endl;
+
+    float min = m_model->getValueMinimum();
+    float max = m_model->getValueMaximum();
+    if (max == min) max = min + 1.0;
+
+    int origin = int(nearbyint(m_view->height() -
+			       (-min * m_view->height()) / (max - min)));
+
+    QPoint localPos;
+    long illuminateFrame = -1;
+
+    if (m_view->shouldIlluminateLocalFeatures(this, localPos)) {
+	SparseTimeValueModel::PointList localPoints =
+	    getLocalPoints(localPos.x());
+	if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame;
+    }
+	
+    for (SparseTimeValueModel::PointList::const_iterator i = points.begin();
+	 i != points.end(); ++i) {
+
+	const SparseTimeValueModel::Point &p(*i);
+
+	int x = (p.frame - startFrame) / zoomLevel;
+	int y = int(nearbyint(m_view->height() -
+			      ((p.value - min) * m_view->height()) /
+			      (max - min)));
+	int w = m_model->getResolution() / zoomLevel;
+
+	if (w < 1) w = 1;
+
+	paint.setPen(m_colour);
+	paint.setBrush(brushColour);
+
+	if (m_plotStyle == PlotStems) {
+	    paint.setPen(brushColour);
+	    if (y < origin - 1) {
+		paint.drawRect(x + w/2, y + 1, 1, origin - y);
+	    } else if (y > origin + 1) {
+		paint.drawRect(x + w/2, origin, 1, y - origin - 1);
+	    }
+	    paint.setPen(m_colour);
+	}
+
+	if (illuminateFrame == p.frame) {
+	    //!!! aside from the problem of choosing a colour, it'd be
+	    //better to save the highlighted rects and draw them at
+	    //the end perhaps
+	    paint.setPen(Qt::black);//!!!
+	    paint.setBrush(Qt::black);//!!!
+	}
+
+	paint.drawRect(x, y - 1, w, 2);
+
+//	if (w > 1) {
+//	    paint.setPen(brushColour);
+//	    paint.drawRect(x, y - 1, w - 1, 2);
+//	    paint.setPen(m_colour);
+//	}
+//	paint.drawLine(x, 0, x, m_view->height());
+
+	if (m_plotStyle == PlotLines) {
+
+	    paint.setPen(brushColour);
+	    SparseTimeValueModel::PointList::const_iterator j = i;
+	    ++j;
+	    if (j != points.end()) {
+		const SparseTimeValueModel::Point &q(*j);
+		int nx = (q.frame - startFrame) / zoomLevel;
+		int ny = int(nearbyint(m_view->height() -
+				       ((q.value - min) * m_view->height()) /
+				       (max - min)));
+		paint.drawLine(x + w, y, nx, ny);
+	    }
+	}
+
+	
+///	if (p.label != "") {
+///	    paint.drawText(x + 5, y - paint.fontMetrics().height() + paint.fontMetrics().ascent(), p.label);
+///	}
+    }
+	
+	
+}
+
+
+#ifdef INCLUDE_MOCFILES
+#include "TimeValueLayer.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/TimeValueLayer.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,68 @@
+/* -*- 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.
+*/
+
+#ifndef _TIME_VALUE_VIEW_H_
+#define _TIME_VALUE_VIEW_H_
+
+#include "base/Layer.h"
+#include "model/SparseTimeValueModel.h"
+
+#include <QObject>
+#include <QColor>
+
+class View;
+class QPainter;
+
+class TimeValueLayer : public Layer
+{
+    Q_OBJECT
+
+public:
+    TimeValueLayer(View *w);
+
+    virtual void paint(QPainter &paint, QRect rect) const;
+
+    virtual QRect getFeatureDescriptionRect(QPainter &, QPoint) const;
+    virtual void paintLocalFeatureDescription(QPainter &, QRect, QPoint) const;
+
+    virtual const Model *getModel() const { return m_model; }
+    void setModel(SparseTimeValueModel *model);
+
+    virtual PropertyList getProperties() const;
+    virtual PropertyType getPropertyType(const PropertyName &) const;
+    virtual int getPropertyRangeAndValue(const PropertyName &,
+					   int *min, int *max) const;
+    virtual QString getPropertyValueLabel(const PropertyName &,
+					  int value) const;
+    virtual void setProperty(const PropertyName &, int value);
+
+    void setBaseColour(QColor);
+    QColor getBaseColour() const { return m_colour; }
+
+    enum PlotStyle { PlotPoints, PlotStems, PlotLines };
+
+    void setPlotStyle(PlotStyle style);
+    PlotStyle getPlotStyle() const { return m_plotStyle; }
+
+    virtual QString getPropertyContainerIconName() const { return "values"; }
+
+    virtual bool isLayerScrollable() const;
+
+    virtual int getCompletion() const { return m_model->getCompletion(); }
+
+protected:
+    SparseTimeValueModel::PointList getLocalPoints(int) const;
+
+    SparseTimeValueModel *m_model;
+    QColor m_colour;
+    PlotStyle m_plotStyle;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/WaveformLayer.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,741 @@
+/* -*- 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 "WaveformLayer.h"
+
+#include "base/AudioLevel.h"
+#include "base/View.h"
+#include "base/Profiler.h"
+
+#include <QPainter>
+#include <QPixmap>
+
+#include <iostream>
+#include <cmath>
+
+using std::cerr;
+using std::endl;
+
+WaveformLayer::WaveformLayer(View *w) :
+    Layer(w),
+    m_model(0),
+    m_gain(1.0f),
+    m_colour(Qt::black),
+    m_showMeans(true),
+    m_greyscale(true),
+    m_channelMode(SeparateChannels),
+    m_channel(-1),
+    m_scale(LinearScale),
+    m_aggressive(false),
+    m_cache(0),
+    m_cacheValid(false)
+{
+    m_view->addLayer(this);
+}
+
+WaveformLayer::~WaveformLayer()
+{
+    delete m_cache;
+}
+
+void
+WaveformLayer::setModel(const RangeSummarisableTimeValueModel *model)
+{
+    m_model = model;
+    m_cacheValid = false;
+    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()));
+
+    emit modelReplaced();
+}
+
+Layer::PropertyList
+WaveformLayer::getProperties() const
+{
+    PropertyList list;
+    list.push_back(tr("Colour"));
+    list.push_back(tr("Scale"));
+    list.push_back(tr("Gain"));
+    list.push_back(tr("Merge Channels"));
+    return list;
+}
+
+Layer::PropertyType
+WaveformLayer::getPropertyType(const PropertyName &name) const
+{
+    if (name == tr("Gain")) return RangeProperty;
+    if (name == tr("Colour")) return ValueProperty;
+    if (name == tr("Merge Channels")) return ToggleProperty;
+    if (name == tr("Scale")) return ValueProperty;
+    return InvalidProperty;
+}
+
+QString
+WaveformLayer::getPropertyGroupName(const PropertyName &name) const
+{
+    if (name == tr("Gain") ||
+	name == tr("Scale")) return tr("Scale");
+    return QString();
+}
+
+int
+WaveformLayer::getPropertyRangeAndValue(const PropertyName &name,
+					 int *min, int *max) const
+{
+    int deft = 0;
+
+    if (name == tr("Gain")) {
+
+	*min = -50;
+	*max = 50;
+
+	deft = int(nearbyint(log10(m_gain) * 20.0));
+	if (deft < *min) deft = *min;
+	if (deft > *max) deft = *max;
+
+    } else if (name == tr("Colour")) {
+
+	*min = 0;
+	*max = 5;
+
+	if (m_colour == Qt::black) deft = 0;
+	else if (m_colour == Qt::darkRed) deft = 1;
+	else if (m_colour == Qt::darkBlue) deft = 2;
+	else if (m_colour == Qt::darkGreen) deft = 3;
+	else if (m_colour == QColor(200, 50, 255)) deft = 4;
+	else if (m_colour == QColor(255, 150, 50)) deft = 5;
+
+    } else if (name == tr("Merge Channels")) {
+
+	deft = ((m_channelMode == MergeChannels) ? 1 : 0);
+
+    } else if (name == tr("Scale")) {
+
+	*min = 0;
+	*max = 2;
+
+	deft = (int)m_scale;
+
+    } else {
+	deft = Layer::getPropertyRangeAndValue(name, min, max);
+    }
+
+    return deft;
+}
+
+QString
+WaveformLayer::getPropertyValueLabel(const PropertyName &name,
+				    int value) const
+{
+    if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0: return tr("Black");
+	case 1: return tr("Red");
+	case 2: return tr("Blue");
+	case 3: return tr("Green");
+	case 4: return tr("Purple");
+	case 5: return tr("Orange");
+	}
+    }
+    if (name == tr("Scale")) {
+	switch (value) {
+	default:
+	case 0: return tr("Linear");
+	case 1: return tr("Meter");
+	case 2: return tr("dB");
+	}
+    }
+    return tr("<unknown>");
+}
+
+void
+WaveformLayer::setProperty(const PropertyName &name, int value)
+{
+    if (name == tr("Gain")) {
+	setGain(pow(10, float(value)/20.0));
+    } else if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0:	setBaseColour(Qt::black); break;
+	case 1: setBaseColour(Qt::darkRed); break;
+	case 2: setBaseColour(Qt::darkBlue); break;
+	case 3: setBaseColour(Qt::darkGreen); break;
+	case 4: setBaseColour(QColor(200, 50, 255)); break;
+	case 5: setBaseColour(QColor(255, 150, 50)); break;
+	}
+    } else if (name == tr("Merge Channels")) {
+	setChannelMode(value ? MergeChannels : SeparateChannels);
+    } else if (name == tr("Scale")) {
+	switch (value) {
+	default:
+	case 0: setScale(LinearScale); break;
+	case 1: setScale(MeterScale); break;
+	case 2: setScale(dBScale); break;
+	}
+    }
+}
+
+/*
+
+int
+WaveformLayer::getProperty(const PropertyName &name)
+{
+    if (name == "Gain") {
+	return int((getGain() - 1.0) * 10.0 + 0.01);
+    }
+    if (name == "Colour") {
+	
+*/
+
+void
+WaveformLayer::setGain(float gain) //!!! inadequate for floats!
+{
+    if (m_gain == gain) return;
+    m_gain = gain;
+    m_cacheValid = false;
+    emit layerParametersChanged();
+}
+
+void
+WaveformLayer::setBaseColour(QColor colour)
+{
+    if (m_colour == colour) return;
+    m_colour = colour;
+    m_cacheValid = false;
+    emit layerParametersChanged();
+}
+
+void
+WaveformLayer::setShowMeans(bool showMeans)
+{
+    if (m_showMeans == showMeans) return;
+    m_showMeans = showMeans;
+    m_cacheValid = false;
+    emit layerParametersChanged();
+}
+
+void
+WaveformLayer::setUseGreyscale(bool useGreyscale)
+{
+    if (m_greyscale == useGreyscale) return;
+    m_greyscale = useGreyscale;
+    m_cacheValid = false;
+    emit layerParametersChanged();
+}
+
+void
+WaveformLayer::setChannelMode(ChannelMode channelMode)
+{
+    if (m_channelMode == channelMode) return;
+    m_channelMode = channelMode;
+    m_cacheValid = false;
+    emit layerParametersChanged();
+}
+
+void
+WaveformLayer::setChannel(int channel)
+{
+    std::cerr << "WaveformLayer::setChannel(" << channel << ")" << std::endl;
+
+    if (m_channel == channel) return;
+    m_channel = channel;
+    m_cacheValid = false;
+    emit layerParametersChanged();
+}
+
+void
+WaveformLayer::setScale(Scale scale)
+{
+    if (m_scale == scale) return;
+    m_scale = scale;
+    m_cacheValid = false;
+    emit layerParametersChanged();
+}
+
+void
+WaveformLayer::setAggressiveCacheing(bool aggressive)
+{
+    if (m_aggressive == aggressive) return;
+    m_aggressive = aggressive;
+    m_cacheValid = false;
+    emit layerParametersChanged();
+}
+
+int
+WaveformLayer::getCompletion() const
+{
+    int completion = 100;
+    if (!m_model || !m_model->isOK()) return completion;
+    if (m_model->isReady(&completion)) return 100;
+    return completion;
+}
+
+int
+WaveformLayer::dBscale(float sample, int m) const
+{
+    if (sample < 0.0) return -dBscale(-sample, m);
+    float dB = AudioLevel::multiplier_to_dB(sample);
+    if (dB < -50.0) return 0;
+    if (dB > 0.0) return m;
+    return int(((dB + 50.0) * m) / 50.0 + 0.1);
+}
+
+size_t
+WaveformLayer::getChannelArrangement(size_t &min, size_t &max, bool &merging)
+    const
+{
+    if (!m_model || !m_model->isOK()) return 0;
+
+    size_t channels = m_model->getChannelCount();
+    if (channels == 0) return 0;
+
+    size_t rawChannels = channels;
+
+    if (m_channel == -1) {
+	min = 0;
+	if (m_channelMode == MergeChannels) {
+	    max = 0;
+	    channels = 1;
+	} else {
+	    max = channels - 1;
+	}
+    } else {
+	min = m_channel;
+	max = m_channel;
+	rawChannels = 1;
+	channels = 1;
+    }
+
+    merging = (m_channelMode == MergeChannels && rawChannels > 1);
+
+//    std::cerr << "WaveformLayer::getChannelArrangement: min " << min << ", max " << max << ", merging " << merging << ", channels " << channels << std::endl;
+
+    return channels;
+}    
+
+void
+WaveformLayer::paint(QPainter &viewPainter, QRect rect) const
+{
+    if (!m_model || !m_model->isOK()) {
+	return;
+    }
+  
+    long startFrame = m_view->getStartFrame();
+    int zoomLevel = m_view->getZoomLevel();
+
+
+    Profiler profiler("WaveformLayer::paint", true);
+    std::cerr << "WaveformLayer::paint (" << rect.x() << "," << rect.y()
+	      << ") [" << rect.width() << "x" << rect.height() << "]: zoom " << zoomLevel << ", start " << startFrame << std::endl;
+
+
+    size_t channels = 0, minChannel = 0, maxChannel = 0;
+    bool mergingChannels = false;
+
+    channels = getChannelArrangement(minChannel, maxChannel, mergingChannels);
+    if (channels == 0) return;
+
+    int w = m_view->width();
+    int h = m_view->height();
+
+    bool ready = m_model->isReady();
+    QPainter *paint;
+
+    if (m_aggressive) {
+
+	if (m_cacheValid && (zoomLevel != m_cacheZoomLevel)) {
+	    m_cacheValid = false;
+	}
+
+	if (m_cacheValid) {
+	    viewPainter.drawPixmap(rect, *m_cache, rect);
+	    return;
+	}
+
+	if (!m_cache || m_cache->width() != w || m_cache->height() != h) {
+	    delete m_cache;
+	    m_cache = new QPixmap(w, h);
+	}
+
+	paint = new QPainter(m_cache);
+
+	paint->setPen(Qt::NoPen);
+	paint->setBrush(m_view->palette().background());
+	paint->drawRect(rect);
+
+	paint->setPen(Qt::black);
+	paint->setBrush(Qt::NoBrush);
+
+    } else {
+	paint = &viewPainter;
+    }
+
+    int x0 = 0, x1 = w - 1;
+    int y0 = 0, y1 = h - 1;
+
+    x0 = rect.left();
+    x1 = rect.right();
+    y0 = rect.top();
+    y1 = rect.bottom();
+
+    long frame0 = startFrame + x0 * zoomLevel;
+    long frame1 = startFrame + (x1 + 1) * zoomLevel;
+     
+//    std::cerr << "Painting waveform from " << frame0 << " to " << frame1 << " (" << (x1-x0+1) << " pixels at zoom " << zoomLevel << ")" <<  std::endl;
+
+    RangeSummarisableTimeValueModel::RangeBlock ranges;
+    RangeSummarisableTimeValueModel::RangeBlock otherChannelRanges;
+    RangeSummarisableTimeValueModel::Range range;
+    
+    QColor greys[3];
+    if (m_colour == Qt::black) {
+	for (int i = 0; i < 3; ++i) {
+	    int level = 192 - 64 * i;
+	    greys[i] = QColor(level, level, level);
+	}
+    } else {
+	int factor = (m_view->hasLightBackground() ? 120 : 80);
+	greys[2] = m_colour.light(factor);
+	greys[1] = greys[2].light(factor);
+	greys[0] = greys[1].light(factor);
+    }
+        
+    QColor midColour = m_colour;
+    if (midColour == Qt::black) {
+	midColour = Qt::gray;
+    } else if (m_view->hasLightBackground()) {
+	midColour = midColour.light(150);
+    } else {
+	midColour = midColour.light(50);
+    }
+
+    for (size_t ch = minChannel; ch <= maxChannel; ++ch) {
+
+	int prevRangeBottom = -1, prevRangeTop = -1;
+
+	int m = (h / channels) / 2;
+	int my = m + (((ch - minChannel) * h) / channels);
+	
+//	std::cerr << "ch = " << ch << ", channels = " << channels << ", m = " << m << ", my = " << my << ", h = " << h << std::endl;
+
+	if (my - m > y1 || my + m < y0) continue;
+
+	paint->setPen(greys[0]);
+	paint->drawLine(x0, my, x1, my);
+
+	if (frame1 <= 0) continue;
+
+	size_t modelZoomLevel = zoomLevel;
+
+	ranges = m_model->getRanges
+	    (ch, frame0 < 0 ? 0 : frame0, frame1, modelZoomLevel);
+        
+	if (mergingChannels) {
+	    otherChannelRanges = m_model->getRanges
+		(1, frame0 < 0 ? 0 : frame0, frame1, modelZoomLevel);
+	}
+
+	for (int x = x0; x <= x1; ++x) {
+
+	    range = RangeSummarisableTimeValueModel::Range();
+	    size_t index = x - x0;
+	    size_t maxIndex = index;
+
+	    if (frame0 < 0) {
+		if (index < size_t(-frame0 / zoomLevel)) {
+		    continue;
+		} else {
+		    index -= -frame0 / zoomLevel;
+		    maxIndex = index;
+		}
+	    }
+            
+	    if (int(modelZoomLevel) != zoomLevel) {
+
+		index = size_t((double(index) * zoomLevel) / modelZoomLevel);
+
+		if (int(modelZoomLevel) < zoomLevel) {
+		    // Peaks may be missed!  The model should avoid
+		    // this by rounding zoom levels up rather than
+		    // down, but we'd better cope in case it doesn't
+		    maxIndex = index;
+		} else {
+		    maxIndex = size_t((double(index + 1) * zoomLevel)
+				      / modelZoomLevel) - 1;
+		}
+	    }
+
+	    if (index < ranges.size()) {
+
+		range = ranges[index];
+
+		if (maxIndex > index && maxIndex < ranges.size()) {
+		    range.max = std::max(range.max, ranges[maxIndex].max);
+		    range.min = std::min(range.min, ranges[maxIndex].min);
+		    range.absmean = (range.absmean +
+				     ranges[maxIndex].absmean) / 2;
+		}
+
+	    } else {
+		continue;
+	    }
+
+	    int rangeBottom = 0, rangeTop = 0, meanBottom = 0, meanTop = 0;
+
+	    if (mergingChannels) {
+
+		if (index < otherChannelRanges.size()) {
+
+		    range.max = fabsf(range.max);
+		    range.min = -fabsf(otherChannelRanges[index].max);
+		    range.absmean = (range.absmean +
+				     otherChannelRanges[index].absmean) / 2;
+
+		    if (maxIndex > index && maxIndex < ranges.size()) {
+			// let's not concern ourselves about the mean
+			range.min = std::min
+			    (range.min,
+			     -fabsf(otherChannelRanges[maxIndex].max));
+		    }
+		}
+	    }
+
+	    int greyLevels = 1;
+	    if (m_greyscale && (m_scale == LinearScale)) greyLevels = 4;
+
+	    switch (m_scale) {
+
+	    case LinearScale:
+		rangeBottom = int( m * greyLevels * range.min * m_gain);
+		rangeTop    = int( m * greyLevels * range.max * m_gain);
+		meanBottom  = int(-m * range.absmean * m_gain);
+		meanTop     = int( m * range.absmean * m_gain);
+		break;
+
+	    case dBScale:
+		rangeBottom =  dBscale(range.min * m_gain, m * greyLevels);
+		rangeTop    =  dBscale(range.max * m_gain, m * greyLevels);
+		meanBottom  = -dBscale(range.absmean * m_gain, m);
+		meanTop     =  dBscale(range.absmean * m_gain, m);
+		break;
+
+	    case MeterScale:
+		rangeBottom =  AudioLevel::multiplier_to_preview(range.min * m_gain, m * greyLevels);
+		rangeTop    =  AudioLevel::multiplier_to_preview(range.max * m_gain, m * greyLevels);
+		meanBottom  = -AudioLevel::multiplier_to_preview(range.absmean * m_gain, m);
+		meanTop     =  AudioLevel::multiplier_to_preview(range.absmean * m_gain, m);
+	    }
+
+	    int topFill = (rangeTop < 0 ? -rangeTop : rangeTop) % greyLevels;
+	    int bottomFill = (rangeBottom < 0 ? -rangeBottom : rangeBottom) % greyLevels;
+	    rangeTop = rangeTop / greyLevels;
+	    rangeBottom = rangeBottom / greyLevels;
+
+	    bool clipped = false;
+	    if (rangeTop < -m) { rangeTop = -m; clipped = true; }
+	    if (rangeTop >  m) { rangeTop =  m; clipped = true; }
+	    if (rangeBottom < -m) { rangeBottom = -m; clipped = true; }
+	    if (rangeBottom >  m) { rangeBottom =  m; clipped = true; }
+	    
+	    rangeBottom = my - rangeBottom;
+	    rangeTop    = my - rangeTop;
+	    meanBottom  = my - meanBottom;
+	    meanTop     = my - meanTop;
+
+	    if (meanBottom > rangeBottom) meanBottom = rangeBottom;
+	    if (meanTop < rangeTop) meanTop = rangeTop;
+
+	    bool drawMean = m_showMeans;
+	    if (meanTop == rangeTop) {
+		if (meanTop < meanBottom) ++meanTop;
+		else drawMean = false;
+	    }
+	    if (meanBottom == rangeBottom) {
+		if (meanBottom > meanTop) --meanBottom;
+		else drawMean = false;
+	    }
+
+	    if (x != x0 && prevRangeBottom != -1) {
+		if (prevRangeBottom > rangeBottom &&
+		    prevRangeTop    > rangeBottom) {
+		    paint->setPen(midColour);
+		    paint->drawLine(x-1, prevRangeTop, x, rangeBottom);
+		    paint->setPen(m_colour);
+		    paint->drawPoint(x-1, prevRangeTop);
+		} else if (prevRangeBottom < rangeTop &&
+			   prevRangeTop    < rangeTop) {
+		    paint->setPen(midColour);
+		    paint->drawLine(x-1, prevRangeBottom, x, rangeTop);
+		    paint->setPen(m_colour);
+		    paint->drawPoint(x-1, prevRangeBottom);
+		}
+	    }
+
+	    if (ready) {
+		if (clipped ||
+		    range.min * m_gain <= -1.0 ||
+		    range.max * m_gain >=  1.0) {
+		    paint->setPen(Qt::red);
+		} else {
+		    paint->setPen(m_colour);
+		}
+	    } else {
+		paint->setPen(midColour);
+	    }
+
+	    paint->drawLine(x, rangeBottom, x, rangeTop);
+
+	    if (m_greyscale && (m_scale == LinearScale) && ready) {
+		if (!clipped) {
+		    if (rangeTop < rangeBottom) {
+			if (topFill > 0 &&
+			    (!drawMean || (rangeTop < meanTop - 1))) {
+			    paint->setPen(greys[topFill - 1]);
+			    paint->drawPoint(x, rangeTop - 1);
+			}
+			if (bottomFill > 0 && 
+			    (!drawMean || (rangeBottom > meanBottom + 1))) {
+			    paint->setPen(greys[bottomFill - 1]);
+			    paint->drawPoint(x, rangeBottom + 1);
+			}
+		    }
+		}
+	    }
+        
+	    if (drawMean) {
+		paint->setPen(midColour);
+		paint->drawLine(x, meanBottom, x, meanTop);
+	    }
+        
+	    prevRangeBottom = rangeBottom;
+	    prevRangeTop = rangeTop;
+	}
+    }
+
+    if (m_aggressive) {
+	if (ready && rect == m_view->rect()) {
+	    m_cacheValid = true;
+	    m_cacheZoomLevel = zoomLevel;
+	}
+	paint->end();
+	delete paint;
+	viewPainter.drawPixmap(rect, *m_cache, rect);
+    }
+}
+
+int
+WaveformLayer::getVerticalScaleWidth(QPainter &paint) const
+{
+    if (m_scale == LinearScale) {
+	return paint.fontMetrics().width("0.0") + 13;
+    } else {
+	return std::max(paint.fontMetrics().width(tr("0dB")),
+			paint.fontMetrics().width(tr("-Inf"))) + 13;
+    }
+}
+
+void
+WaveformLayer::paintVerticalScale(QPainter &paint, QRect rect) const
+{
+    if (!m_model || !m_model->isOK()) {
+	return;
+    }
+
+    size_t channels = 0, minChannel = 0, maxChannel = 0;
+    bool mergingChannels = false;
+
+    channels = getChannelArrangement(minChannel, maxChannel, mergingChannels);
+    if (channels == 0) return;
+
+    int h = rect.height(), w = rect.width();
+    int textHeight = paint.fontMetrics().height();
+    int toff = -textHeight/2 + paint.fontMetrics().ascent() + 1;
+
+    for (size_t ch = minChannel; ch <= maxChannel; ++ch) {
+
+	int m = (h / channels) / 2;
+	int my = m + (((ch - minChannel) * h) / channels);
+	int py = -1;
+
+	for (int i = 0; i <= 10; ++i) {
+
+	    int vy = 0;
+	    QString text = "";
+
+	    if (m_scale == LinearScale) {
+
+		vy = int((m * i * m_gain) / 10);
+
+		text = QString("%1").arg(float(i) / 10.0);
+		if (i == 0) text = "0.0";
+		if (i == 10) text = "1.0";
+
+	    } else {
+
+		int db;
+		bool minvalue = false;
+		
+		if (m_scale == MeterScale) {
+		    static int dbs[] = { -50, -40, -30, -20, -15,
+					 -10, -5, -3, -2, -1, 0 };
+		    db = dbs[i];
+		    if (db == -50) minvalue = true;
+		    vy = AudioLevel::multiplier_to_preview
+			(AudioLevel::dB_to_multiplier(db) * m_gain, m);
+		} else {
+		    db = -100 + i * 10;
+		    if (db == -100) minvalue = true;
+		    vy = dBscale
+			(AudioLevel::dB_to_multiplier(db) * m_gain, m);
+		}
+
+		text = QString("%1").arg(db);
+		if (db == 0) text = tr("0dB");
+		if (minvalue) {
+		    text = tr("-Inf");
+		    vy = 0;
+		}
+	    }
+
+	    if (vy < 0) vy = -vy;
+	    if (vy >= m - 1) continue;
+
+	    if (py >= 0 && (vy - py) < textHeight - 1) {
+		paint.drawLine(w - 4, my - vy, w, my - vy);
+		if (vy > 0) paint.drawLine(w - 4, my + vy, w, my + vy);
+		continue;
+	    }
+
+	    paint.drawLine(w - 7, my - vy, w, my - vy);
+	    if (vy > 0) paint.drawLine(w - 7, my + vy, w, my + vy);
+
+	    int tx = 3;
+	    if (m_scale != LinearScale) {
+		tx = w - 10 - paint.fontMetrics().width(text);
+	    }
+	    
+	    paint.drawText(tx, my - vy + toff, text);
+	    if (vy > 0) paint.drawText(tx, my + vy + toff, text);
+		
+	    py = vy;
+	}
+    }
+}
+
+#ifdef INCLUDE_MOCFILES
+#include "WaveformLayer.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/WaveformLayer.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,182 @@
+/* -*- 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.
+*/
+
+#ifndef _WAVEFORM_VIEW_H_
+#define _WAVEFORM_VIEW_H_
+
+#include <QRect>
+#include <QColor>
+
+#include "base/Layer.h"
+
+#include "model/RangeSummarisableTimeValueModel.h"
+
+class View;
+class QPainter;
+class QPixmap;
+
+class WaveformLayer : public Layer
+{
+    Q_OBJECT
+
+public:
+    WaveformLayer(View *w);
+    ~WaveformLayer();
+
+    virtual const ZoomConstraint *getZoomConstraint() const { return m_model; }
+    virtual const Model *getModel() const { return m_model; }
+    virtual void paint(QPainter &paint, QRect rect) const;
+
+    virtual int getVerticalScaleWidth(QPainter &) const;
+    virtual void paintVerticalScale(QPainter &paint, QRect rect) const;
+
+    void setModel(const RangeSummarisableTimeValueModel *model);
+
+    virtual PropertyList getProperties() const;
+    virtual PropertyType getPropertyType(const PropertyName &) const;
+    virtual QString getPropertyGroupName(const PropertyName &) const;
+    virtual int getPropertyRangeAndValue(const PropertyName &,
+					   int *min, int *max) const;
+    virtual QString getPropertyValueLabel(const PropertyName &,
+					  int value) const;
+    virtual void setProperty(const PropertyName &, int value);
+
+    /**
+     * Set the gain multiplier for sample values in this view.
+     *
+     * The default is 1.0.
+     */
+    void setGain(float gain);
+    float getGain() const { return m_gain; }
+
+    /**
+     * Set the basic display colour for waveforms.
+     *
+     * The default is black.
+     *!!! NB should default to white if the associated View !hasLightBackground()
+     */
+    void setBaseColour(QColor);
+    QColor getBaseColour() const { return m_colour; }
+
+    /**
+     * Set whether to display mean values as a lighter-coloured area
+     * beneath the peaks.  Rendering will be slightly faster without
+     * but arguably prettier with.
+     *
+     * The default is to display means.
+     */
+    void setShowMeans(bool);
+    bool getShowMeans() const { return m_showMeans; }
+
+    /**
+     * Set whether to use shades of grey (or of the base colour) to
+     * provide additional perceived vertical resolution (i.e. using
+     * half-filled pixels to represent levels that only just meet the
+     * pixel unit boundary).  This provides a small improvement in
+     * waveform quality at a small cost in rendering speed.
+     * 
+     * The default is to use greyscale.
+     */
+    void setUseGreyscale(bool);
+    bool getUseGreyscale() const { return m_greyscale; }
+
+
+    enum ChannelMode { SeparateChannels, MergeChannels };
+
+    /**
+     * Specify whether multi-channel audio data should be displayed
+     * with a separate axis per channel (SeparateChannels), or with a
+     * single synthetic axis showing channel 0 above the axis and
+     * channel 1 below (MergeChannels).
+     * 
+     * MergeChannels does not work for files with more than 2
+     * channels.
+     * 
+     * The default is SeparateChannels.
+     */
+    void setChannelMode(ChannelMode);
+    ChannelMode getChannelMode() const { return m_channelMode; }
+
+
+    /**
+     * Specify the channel to use from the source model.  A value of
+     * -1 means to show all available channels (laid out to the
+     * channel mode). The default is -1.
+     */
+    void setChannel(int);
+    int getChannel() const { return m_channel; }
+
+
+    enum Scale { LinearScale, MeterScale, dBScale };
+
+    /**
+     * Specify the vertical scale for sample levels.  With LinearScale,
+     * the scale is directly proportional to the raw [-1, +1)
+     * floating-point audio sample values.  With dBScale the
+     * vertical scale is proportional to dB level (truncated at
+     * -50dB).  MeterScale provides a hybrid variable scale based on
+     * IEC meter scale, intended to provide a clear overview at
+     * relatively small heights.
+     *
+     * Note that the effective gain (see setGain()) is applied before
+     * vertical scaling.
+     *
+     * The default is LinearScale.
+     */
+    void setScale(Scale);
+    Scale getScale() const { return m_scale; }
+
+    /**
+     * Enable or disable aggressive pixmap cacheing.  If enabled,
+     * waveforms will be rendered to an off-screen pixmap and
+     * refreshed from there instead of being redrawn from the peak
+     * data each time.  This may be faster if the data and zoom level
+     * do not change often, but it may be slower for frequently zoomed
+     * data and it will only work if the waveform is the "bottom"
+     * layer on the displayed widget, as each refresh will erase
+     * anything beneath the waveform.
+     *
+     * This is intended specifically for a panner widget display in
+     * which the waveform never moves, zooms, or changes, but some
+     * graphic such as a panner outline is frequently redrawn over the
+     * waveform.  This situation would necessitate a lot of waveform
+     * refresh if the default cacheing strategy was used.
+     *
+     * The default is not to use aggressive cacheing.
+     */
+    void setAggressiveCacheing(bool);
+    bool getAggressiveCacheing() const { return m_aggressive; }
+
+    virtual int getCompletion() const;
+
+    virtual QString getPropertyContainerIconName() const { return "waveform"; }
+
+protected:
+    int dBscale(float sample, int m) const;
+
+    const RangeSummarisableTimeValueModel *m_model; // I do not own this
+
+    /// Return value is number of channels displayed
+    size_t getChannelArrangement(size_t &min, size_t &max, bool &merging) const;
+
+    float m_gain;
+    QColor m_colour;
+    bool m_showMeans;
+    bool m_greyscale;
+    ChannelMode m_channelMode;
+    int m_channel;
+    Scale m_scale;
+    bool m_aggressive;
+
+    mutable QPixmap *m_cache;
+    mutable bool m_cacheValid;
+    mutable int m_cacheZoomLevel;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/AudioDial.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,341 @@
+/* -*- 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.
+*/
+
+/**
+ * A rotary dial widget.
+ *
+ * Based on an original design by Thorsten Wilms.
+ *
+ * Implemented as a widget for the Rosegarden MIDI and audio sequencer
+ * and notation editor by Chris Cannam.
+ *
+ * Extracted into a standalone Qt3 widget by Pedro Lopez-Cabanillas
+ * and adapted for use in QSynth.
+ * 
+ * Ported to Qt4 by Chris Cannam.
+ *
+ * This file copyright 2003-2005 Chris Cannam, copyright 2005 Pedro
+ * Lopez-Cabanillas.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.  See the file
+ * COPYING included with this distribution for more information.
+ */
+
+#include "AudioDial.h"
+
+#include <cmath>
+#include <iostream>
+
+#include <QTimer>
+#include <QPainter>
+#include <QPixmap>
+#include <QImage>
+#include <QColormap>
+#include <QMouseEvent>
+#include <QPaintEvent>
+
+using std::endl;
+using std::cerr;
+
+
+//!!! Pedro updated his version to use my up/down response code from RG -- need to grab that code in preference to this version from Rui
+
+
+//-------------------------------------------------------------------------
+// AudioDial - Instance knob widget class.
+//
+
+#define AUDIO_DIAL_MIN (0.25 * M_PI)
+#define AUDIO_DIAL_MAX (1.75 * M_PI)
+#define AUDIO_DIAL_RANGE (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN)
+
+
+// Constructor.
+AudioDial::AudioDial(QWidget *parent) :
+    QDial(parent),
+    m_knobColor(Qt::black), m_meterColor(Qt::white)
+{
+    m_mouseDial = false;
+    m_mousePressed = false;
+}
+
+
+// Destructor.
+AudioDial::~AudioDial (void)
+{
+}
+
+
+void AudioDial::paintEvent(QPaintEvent *)
+{
+    QPainter paint;
+
+    float angle = AUDIO_DIAL_MIN // offset
+	+ (AUDIO_DIAL_RANGE *
+	   (float(QDial::value() - QDial::minimum()) /
+	    (float(QDial::maximum() - QDial::minimum()))));
+    int degrees = int(angle * 180.0 / M_PI);
+
+    int ns = notchSize();
+    int numTicks = 1 + (maximum() + ns - minimum()) / ns;
+	
+    QColor knobColor(m_knobColor);
+    if (knobColor == Qt::black)
+	knobColor = palette().mid().color();
+
+    QColor meterColor(m_meterColor);
+    if (!isEnabled())
+	meterColor = palette().mid().color();
+    else if (m_meterColor == Qt::white)
+	meterColor = palette().highlight().color();
+
+    int m_size = width() < height() ? width() : height();
+    int scale = 1;
+    int width = m_size - 2*scale, height = m_size - 2*scale;
+
+    paint.begin(this);
+    paint.setRenderHint(QPainter::Antialiasing, true);
+    paint.translate(1, 1);
+
+    QPen pen;
+    QColor c;
+
+    // Knob body and face...
+
+    c = knobColor;
+    pen.setColor(knobColor);
+    pen.setWidth(scale * 2);
+    pen.setCapStyle(Qt::FlatCap);
+	
+    paint.setPen(pen);
+    paint.setBrush(c);
+
+    int indent = (int)(width * 0.15 + 1);
+
+    paint.drawEllipse(indent-1, indent-1, width-2*indent, width-2*indent);
+
+    pen.setWidth(3 * scale);
+    int pos = indent-1 + (width-2*indent) / 20;
+    int darkWidth = (width-2*indent) * 3 / 4;
+    while (darkWidth) {
+	c = c.light(102);
+	pen.setColor(c);
+	paint.setPen(pen);
+	paint.drawEllipse(pos, pos, darkWidth, darkWidth);
+	if (!--darkWidth) break;
+	paint.drawEllipse(pos, pos, darkWidth, darkWidth);
+	if (!--darkWidth) break;
+	paint.drawEllipse(pos, pos, darkWidth, darkWidth);
+	++pos; --darkWidth;
+    }
+
+    // Tick notches...
+
+    if ( true/* notchesVisible() */) {
+//	std::cerr << "Notches visible" << std::endl;
+	pen.setColor(palette().dark().color());
+	pen.setWidth(scale);
+	paint.setPen(pen);
+	for (int i = 0; i < numTicks; ++i) {
+	    int div = numTicks;
+	    if (div > 1) --div;
+	    drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
+		     width, true);
+	}
+    }
+
+    // The bright metering bit...
+
+    c = meterColor;
+    pen.setColor(c);
+    pen.setWidth(indent);
+    paint.setPen(pen);
+
+//    std::cerr << "degrees " << degrees << ", gives us " << -(degrees - 45) * 16 << std::endl;
+
+    int arcLen = -(degrees - 45) * 16;
+    if (arcLen == 0) arcLen = -16;
+
+    paint.drawArc(indent/2, indent/2,
+		  width-indent, width-indent, (180 + 45) * 16, arcLen);
+
+    paint.setBrush(Qt::NoBrush);
+
+    // Shadowing...
+
+    pen.setWidth(scale);
+    paint.setPen(pen);
+
+    // Knob shadow...
+
+    int shadowAngle = -720;
+    c = knobColor.dark();
+    for (int arc = 120; arc < 2880; arc += 240) {
+	pen.setColor(c);
+	paint.setPen(pen);
+	paint.drawArc(indent, indent,
+		      width-2*indent, width-2*indent, shadowAngle + arc, 240);
+	paint.drawArc(indent, indent,
+		      width-2*indent, width-2*indent, shadowAngle - arc, 240);
+	c = c.light(110);
+    }
+
+    // Scale shadow...
+
+    shadowAngle = 2160;
+    c = palette().dark().color();
+    for (int arc = 120; arc < 2880; arc += 240) {
+	pen.setColor(c);
+	paint.setPen(pen);
+	paint.drawArc(scale/2, scale/2,
+		      width-scale, width-scale, shadowAngle + arc, 240);
+	paint.drawArc(scale/2, scale/2,
+		      width-scale, width-scale, shadowAngle - arc, 240);
+	c = c.light(108);
+    }
+
+    // Undraw the bottom part...
+
+    pen.setColor(palette().background().color());
+    pen.setWidth(scale * 4);
+    paint.setPen(pen);
+    paint.drawArc(scale/2, scale/2,
+		  width-scale, width-scale, -45 * 16, -92 * 16);
+
+    // Scale ends...
+
+    pen.setColor(palette().dark().color());
+    pen.setWidth(scale);
+    paint.setPen(pen);
+    for (int i = 0; i < numTicks; ++i) {
+	if (i != 0 && i != numTicks - 1) continue;
+	int div = numTicks;
+	if (div > 1) --div;
+	drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
+		 width, false);
+    }
+
+    // Pointer notch...
+
+    float hyp = float(width) / 2.0;
+    float len = hyp - indent;
+    --len;
+
+    float x0 = hyp;
+    float y0 = hyp;
+
+    float x = hyp - len * sin(angle);
+    float y = hyp + len * cos(angle);
+
+    c = palette().dark().color();
+    pen.setColor(isEnabled() ? c.dark(130) : c);
+    pen.setWidth(scale * 2);
+    paint.setPen(pen);
+    paint.drawLine(int(x0), int(y0), int(x), int(y));
+
+    paint.end();
+}
+
+
+void AudioDial::drawTick(QPainter &paint,
+			 float angle, int size, bool internal)
+{
+    float hyp = float(size) / 2.0;
+    float x0 = hyp - (hyp - 1) * sin(angle);
+    float y0 = hyp + (hyp - 1) * cos(angle);
+
+//    cerr << "drawTick: angle " << angle << ", size " << size << ", internal " << internal << endl;
+    
+    if (internal) {
+
+	float len = hyp / 4;
+	float x1 = hyp - (hyp - len) * sin(angle);
+	float y1 = hyp + (hyp - len) * cos(angle);
+		
+	paint.drawLine(int(x0), int(y0), int(x1), int(y1));
+
+    } else {
+
+	float len = hyp / 4;
+	float x1 = hyp - (hyp + len) * sin(angle);
+	float y1 = hyp + (hyp + len) * cos(angle);
+
+	paint.drawLine(int(x0), int(y0), int(x1), int(y1));
+    }
+}
+
+
+void AudioDial::setKnobColor(const QColor& color)
+{
+    m_knobColor = color;
+    update();
+}
+
+
+void AudioDial::setMeterColor(const QColor& color)
+{
+    m_meterColor = color;
+    update();
+}
+
+
+void AudioDial::setMouseDial(bool mouseDial)
+{
+    m_mouseDial = mouseDial;
+}
+
+
+// Alternate mouse behavior event handlers.
+void AudioDial::mousePressEvent(QMouseEvent *mouseEvent)
+{
+    if (m_mouseDial) {
+	QDial::mousePressEvent(mouseEvent);
+    } else if (mouseEvent->button() == Qt::LeftButton) {
+	m_mousePressed = true;
+	m_posMouse = mouseEvent->pos();
+    }
+}
+
+
+void AudioDial::mouseMoveEvent(QMouseEvent *mouseEvent)
+{
+    if (m_mouseDial) {
+	QDial::mouseMoveEvent(mouseEvent);
+    } else if (m_mousePressed) {
+	const QPoint& posMouse = mouseEvent->pos();
+	int v = QDial::value()
+	    + (posMouse.x() - m_posMouse.x())
+	    + (m_posMouse.y() - posMouse.y());
+	if (v > QDial::maximum())
+	    v = QDial::maximum();
+	else
+	    if (v < QDial::minimum())
+		v = QDial::minimum();
+	m_posMouse = posMouse;
+	QDial::setValue(v);
+    }
+}
+
+
+void AudioDial::mouseReleaseEvent(QMouseEvent *mouseEvent)
+{
+    if (m_mouseDial) {
+	QDial::mouseReleaseEvent(mouseEvent);
+    } else if (m_mousePressed) {
+	m_mousePressed = false;
+    }
+}
+
+#ifdef INCLUDE_MOCFILES
+#include "AudioDial.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/AudioDial.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,109 @@
+/* -*- 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.
+*/
+
+#ifndef _AUDIO_DIAL_H_
+#define _AUDIO_DIAL_H_
+
+/**
+ * A rotary dial widget.
+ *
+ * Based on an original design by Thorsten Wilms.
+ *
+ * Implemented as a widget for the Rosegarden MIDI and audio sequencer
+ * and notation editor by Chris Cannam.
+ *
+ * Extracted into a standalone Qt3 widget by Pedro Lopez-Cabanillas
+ * and adapted for use in QSynth.
+ * 
+ * Ported to Qt4 by Chris Cannam.
+ *
+ * This file copyright 2003-2005 Chris Cannam, copyright 2005 Pedro
+ * Lopez-Cabanillas.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.  See the file
+ * COPYING included with this distribution for more information.
+ */
+
+#include <QDial>
+#include <map>
+
+/**
+ * AudioDial is a nicer-looking QDial that by default reacts to mouse
+ * movement on horizontal and vertical axes instead of in a radial
+ * motion.  Move the mouse up or right to increment the value, down or
+ * left to decrement it.  AudioDial also responds to the mouse wheel.
+ *
+ * The programming interface for this widget is compatible with QDial,
+ * with the addition of properties for the knob colour and meter
+ * colour and a boolean property mouseDial that determines whether to
+ * respond to radial mouse motion in the same way as QDial (the
+ * default is no).
+ */
+
+class AudioDial : public QDial
+{
+    Q_OBJECT
+    Q_PROPERTY( QColor knobColor READ getKnobColor WRITE setKnobColor )
+    Q_PROPERTY( QColor meterColor READ getMeterColor WRITE setMeterColor )
+    Q_PROPERTY( bool mouseDial READ getMouseDial WRITE setMouseDial )
+
+public:
+    AudioDial(QWidget *parent = 0);
+    ~AudioDial();
+
+    const QColor& getKnobColor()  const { return m_knobColor;  }
+    const QColor& getMeterColor() const { return m_meterColor; }
+    bool getMouseDial() const { return m_mouseDial; }
+
+public slots:
+    /**
+     * Set the colour of the knob.  The default is to inherit the
+     * colour from the widget's palette.
+     */
+    void setKnobColor(const QColor &color);
+
+    /**
+     * Set the colour of the meter (the highlighted area around the
+     * knob that shows the current value).  The default is to inherit
+     * the colour from the widget's palette.
+     */
+    void setMeterColor(const QColor &color);
+    
+    /**
+     * Specify that the dial should respond to radial mouse movements
+     * in the same way as QDial.
+     */
+    void setMouseDial(bool mouseDial);
+
+protected:
+    void drawTick(QPainter &paint, float angle, int size, bool internal);
+    virtual void paintEvent(QPaintEvent *);
+
+    // Alternate mouse behavior event handlers.
+    virtual void mousePressEvent(QMouseEvent *pMouseEvent);
+    virtual void mouseMoveEvent(QMouseEvent *pMouseEvent);
+    virtual void mouseReleaseEvent(QMouseEvent *pMouseEvent);
+
+private:
+    QColor m_knobColor;
+    QColor m_meterColor;
+    
+    // Alternate mouse behavior tracking.
+    bool m_mouseDial;
+    bool m_mousePressed;
+    QPoint m_posMouse;
+};
+
+
+#endif  // __AudioDial_h
+
+// end of AudioDial.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/Fader.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,260 @@
+/* -*- 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.
+*/
+
+/**
+ * Horizontal audio fader and meter widget.
+ *
+ * Based on the vertical fader and meter widget from the Hydrogen drum
+ * machine.  (Any poor taste that has crept in during the
+ * modifications for this application is entirely my own, however.)
+ * The following copyright notice applies to code from this file, and
+ * also to the files in icons/fader_*.png (also modified by me). --cc
+ */
+
+/**
+ * Hydrogen
+ * Copyright(c) 2002-2005 by Alex >Comix< Cominu [comix@users.sourceforge.net]
+ *
+ * http://www.hydrogen-music.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY, without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+#include "Fader.h"
+
+#include "base/AudioLevel.h"
+
+#include <QMouseEvent>
+#include <QPixmap>
+#include <QWheelEvent>
+#include <QPaintEvent>
+#include <QPainter>
+
+Fader::Fader(QWidget *parent, bool withoutKnob) :
+    QWidget(parent),
+    m_withoutKnob(withoutKnob),
+    m_value(1.0),
+    m_peakLeft(0.0),
+    m_peakRight(0.0)
+{
+    setMinimumSize(116, 23);
+    setMaximumSize(116, 23);
+    resize(116, 23);
+
+    QString background_path = ":/icons/fader_background.png";
+    bool ok = m_back.load(background_path);
+    if (ok == false) {
+	std::cerr << "Fader: Error loading pixmap" << std::endl;
+    }
+
+    QString leds_path = ":/icons/fader_leds.png";
+    ok = m_leds.load(leds_path);
+    if (ok == false) {
+	std::cerr <<  "Error loading pixmap" << std::endl;
+    }
+
+    QString knob_path = ":/icons/fader_knob.png";
+    ok = m_knob.load(knob_path);
+    if (ok == false) {
+	std::cerr <<  "Error loading pixmap" << std::endl;
+    }
+
+    QString clip_path = ":/icons/fader_knob_red.png";
+    ok = m_clip.load(clip_path);
+    if (ok == false) {
+	std::cerr <<  "Error loading pixmap" << std::endl;
+    }
+}
+
+Fader::~Fader()
+{
+
+}
+
+void
+Fader::mouseMoveEvent(QMouseEvent *ev)
+{
+    int x = ev->x() - 6;
+    const int max_x = 116 - 12;
+
+    int value = x;
+
+    if (value > max_x) {
+	value = max_x;
+    } else if (value < 0) {
+	value = 0;
+    }
+
+//    float fval = float(value) / float(max_x);
+    float fval = AudioLevel::fader_to_multiplier
+	(value, max_x, AudioLevel::LongFader);
+
+    setValue(fval);
+    emit valueChanged(fval);
+
+    update();
+}
+
+
+void
+Fader::mouseDoubleClickEvent(QMouseEvent *)
+{
+    setValue(1.0);
+    emit valueChanged(1.0);
+    update();
+}
+
+void
+Fader::mousePressEvent(QMouseEvent *ev)
+{
+    int x = ev->x() - 6;
+    const int max_x = 116 - 12;
+
+    int value = x;
+
+    if (value > max_x) {
+	value = max_x;
+    } else if (value < 0) {
+	value = 0;
+    }
+
+    float fval = AudioLevel::fader_to_multiplier
+	(value, max_x, AudioLevel::LongFader);
+
+    setValue(fval);
+    emit valueChanged(fval);
+
+    update();
+}
+
+
+void
+Fader::wheelEvent(QWheelEvent *ev)
+{
+    ev->accept();
+
+    //!!! needs improvement
+
+    if (ev->delta() > 0) {
+	setValue(m_value * 1.1);
+    } else {
+	setValue(m_value / 1.1);
+    }
+
+    update();
+    emit valueChanged(getValue());
+}
+
+
+void
+Fader::setValue(float v)
+{
+    float max = AudioLevel::dB_to_multiplier(10.0);
+
+    if (v > max) {
+	v = max;
+    } else if (v < 0.0) {
+	v = 0.0;
+    }
+
+    if (m_value != v) {
+	m_value = v;
+	float db = AudioLevel::multiplier_to_dB(m_value);
+	if (db <= AudioLevel::DB_FLOOR) {
+	    setToolTip(tr("Level: Off"));
+	} else {
+	    setToolTip(tr("Level: %1%2.%3%4 dB")
+		       .arg(db < 0.0 ? "-" : "")
+		       .arg(abs(int(db)))
+		       .arg(abs(int(db * 10.0) % 10))
+		       .arg(abs(int(db * 100.0) % 10)));
+	}
+	update();
+    }
+}
+
+
+float
+Fader::getValue()
+{
+    return m_value;
+}
+
+
+
+void
+Fader::setPeakLeft(float peak)
+{
+    if (this->m_peakLeft != peak) {
+	this->m_peakLeft = peak;
+	update();
+    }
+}
+
+
+void
+Fader::setPeakRight(float peak) 
+{
+    if (this->m_peakRight != peak) {
+	this->m_peakRight = peak;
+	update();
+    }
+}
+
+
+void
+Fader::paintEvent(QPaintEvent *)
+{
+    QPainter painter(this);
+
+    // background
+    painter.drawPixmap(rect(), m_back, QRect(0, 0, 116, 23));
+
+    int offset_L = AudioLevel::multiplier_to_fader(m_peakLeft, 116,
+						   AudioLevel::IEC268LongMeter);
+
+    painter.drawPixmap(QRect(0, 0, offset_L, 11), m_leds,
+		       QRect(0, 0, offset_L, 11));
+
+    int offset_R = AudioLevel::multiplier_to_fader(m_peakRight, 116,
+						   AudioLevel::IEC268LongMeter);
+
+    painter.drawPixmap(QRect(0, 11, offset_R, 11), m_leds,
+		       QRect(0, 11, offset_R, 11));
+
+    if (m_withoutKnob == false) {
+
+	static const uint knob_width = 29;
+	static const uint knob_height = 9;
+
+	int x = AudioLevel::multiplier_to_fader(m_value, 116 - knob_width,
+						AudioLevel::LongFader);
+
+	bool clipping = (m_peakLeft > 1.0 || m_peakRight > 1.0);
+
+	painter.drawPixmap(QRect(x, 7, knob_width, knob_height),
+			   clipping ? m_clip : m_knob,
+			   QRect(0, 0, knob_width, knob_height));
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/Fader.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,87 @@
+/* -*- 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.
+*/
+
+#ifndef FADER_H
+#define FADER_H
+
+/**
+ * Horizontal audio fader and meter widget.
+ * Based on the vertical fader and meter widget from:
+ * 
+ * Hydrogen
+ * Copyright(c) 2002-2005 by Alex >Comix< Cominu [comix@users.sourceforge.net]
+ *
+ * http://www.hydrogen-music.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY, without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * $Id: Fader.h,v 1.14 2005/08/10 08:03:30 comix Exp $
+ */
+
+
+#include <string>
+#include <iostream>
+
+#include <QWidget>
+#include <QPixmap>
+#include <QMouseEvent>
+#include <QWheelEvent>
+#include <QPaintEvent>
+
+class Fader : public QWidget
+{
+    Q_OBJECT
+
+public:
+    Fader(QWidget *parent, bool withoutKnob = false);
+    ~Fader();
+
+    void setValue(float newValue);
+    float getValue();
+
+    void setPeakLeft(float);
+    float getPeakLeft() { return m_peakLeft; }
+
+    void setPeakRight(float);
+    float getPeakRight() { return m_peakRight; }
+
+    virtual void mousePressEvent(QMouseEvent *ev);
+    virtual void mouseDoubleClickEvent(QMouseEvent *ev);
+    virtual void mouseMoveEvent(QMouseEvent *ev);
+    virtual void wheelEvent( QWheelEvent *ev );
+    virtual void paintEvent(QPaintEvent *ev);
+
+signals:
+    void valueChanged(float); // 0.0 -> 1.0
+
+private:
+    bool m_withoutKnob;
+    float m_value;
+    float m_peakLeft;
+    float m_peakRight;
+
+    QPixmap m_back;
+    QPixmap m_leds;
+    QPixmap m_knob;
+    QPixmap m_clip;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/Pane.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,397 @@
+/* -*- 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 "widgets/Pane.h"
+#include "base/Layer.h"
+#include "base/Model.h"
+#include "base/ZoomConstraint.h"
+#include "base/RealTime.h"
+#include "base/Profiler.h"
+
+#include <QPaintEvent>
+#include <QPainter>
+#include <iostream>
+#include <cmath>
+
+using std::cerr;
+using std::endl;
+
+Pane::Pane(QWidget *w) :
+    View(w, true),
+    m_identifyFeatures(false),
+    m_clickedInRange(false),
+    m_shiftPressed(false),
+    m_centreLineVisible(true)
+{
+    setObjectName("Pane");
+    setMouseTracking(true);
+}
+
+bool
+Pane::shouldIlluminateLocalFeatures(const Layer *layer, QPoint &pos)
+{
+    for (LayerList::iterator vi = m_layers.end(); vi != m_layers.begin(); ) {
+	--vi;
+	if (layer != *vi) return false;
+	pos = m_identifyPoint;
+	return m_identifyFeatures;
+    }
+
+    return false;
+}
+
+void
+Pane::setCentreLineVisible(bool visible)
+{
+    m_centreLineVisible = visible;
+    update();
+}
+
+void
+Pane::paintEvent(QPaintEvent *e)
+{
+    QPainter paint;
+
+    QRect r(rect());
+
+    if (e) {
+	r = e->rect();
+    }
+/*
+    paint.begin(this);
+    paint.setClipRect(r);
+
+    if (hasLightBackground()) {
+	paint.setPen(Qt::white);
+	paint.setBrush(Qt::white);
+    } else {
+	paint.setPen(Qt::black);
+	paint.setBrush(Qt::black);
+    }
+    paint.drawRect(r);
+
+    paint.end();
+*/
+    View::paintEvent(e);
+
+    paint.begin(this);
+
+    if (e) {
+	paint.setClipRect(r);
+    }
+	
+    for (LayerList::iterator vi = m_layers.end(); vi != m_layers.begin(); ) {
+	--vi;
+
+	int sw = (*vi)->getVerticalScaleWidth(paint);
+
+	if (sw > 0 && r.left() < sw) {
+
+//	    Profiler profiler("Pane::paintEvent - painting vertical scale", true);
+
+//	    std::cerr << "Pane::paintEvent: calling paint.save() in vertical scale block" << std::endl;
+	    paint.save();
+
+	    paint.setPen(Qt::black);
+	    paint.setBrush(Qt::white);
+	    paint.drawRect(0, 0, sw, height());
+
+	    paint.setBrush(Qt::NoBrush);
+	    (*vi)->paintVerticalScale(paint, QRect(0, 0, sw, height()));
+
+	    paint.restore();
+	}
+
+	if (m_identifyFeatures) {
+	    QRect descRect = (*vi)->getFeatureDescriptionRect(paint,
+							      m_identifyPoint);
+	    if (descRect.width() > 0 && descRect.height() > 0 &&
+		r.left() + r.width() >= width() - descRect.width() &&
+		r.top() < descRect.height()) {
+		
+//		Profiler profiler("Pane::paintEvent - painting local feature description", true);
+		
+//		std::cerr << "Pane::paintEvent: calling paint.save() in feature description block" << std::endl;
+		paint.save();
+		
+		paint.setPen(Qt::black);
+		paint.setBrush(Qt::white);
+		
+		QRect rect(width() - descRect.width() - 1, 0,
+			   descRect.width(), descRect.height());
+		
+		paint.drawRect(rect);
+		
+		paint.setBrush(Qt::NoBrush);
+		(*vi)->paintLocalFeatureDescription(paint, rect, m_identifyPoint);
+		
+		paint.restore();
+	    }
+	}
+
+	break;
+    }
+    
+    if (m_centreLineVisible) {
+
+	if (hasLightBackground()) {
+	    paint.setPen(QColor(50, 50, 50));
+	} else {
+	    paint.setPen(QColor(200, 200, 200));
+	}	
+	paint.setBrush(Qt::NoBrush);
+	paint.drawLine(width() / 2, 0, width() / 2, height() - 1);
+	
+//    QFont font(paint.font());
+//    font.setBold(true);
+//    paint.setFont(font);
+
+	int sampleRate = getModelsSampleRate();
+	int y = height() - paint.fontMetrics().height()
+	    + paint.fontMetrics().ascent() - 6;
+	
+	LayerList::iterator vi = m_layers.end();
+	
+	if (vi != m_layers.begin()) {
+	    
+	    switch ((*--vi)->getPreferredFrameCountPosition()) {
+		
+	    case Layer::PositionTop:
+		y = paint.fontMetrics().ascent() + 6;
+		break;
+		
+	    case Layer::PositionMiddle:
+		y = (height() - paint.fontMetrics().height()) / 2
+		    + paint.fontMetrics().ascent();
+		break;
+
+	    case Layer::PositionBottom:
+		// y already set correctly
+		break;
+	    }
+	}
+
+	if (sampleRate) {
+
+	    QString text(QString::fromStdString
+			 (RealTime::frame2RealTime
+			  (m_centreFrame, sampleRate).toText(true)));
+
+	    int tw = paint.fontMetrics().width(text);
+	    int x = width()/2 - 4 - tw;
+
+	    if (hasLightBackground()) {
+		paint.setPen(palette().background().color());
+		for (int dx = -1; dx <= 1; ++dx) {
+		    for (int dy = -1; dy <= 1; ++dy) {
+			if ((dx && dy) || !(dx || dy)) continue;
+			paint.drawText(x + dx, y + dy, text);
+		    }
+		}
+		paint.setPen(QColor(50, 50, 50));
+	    } else {
+		paint.setPen(QColor(200, 200, 200));
+	    }
+
+	    paint.drawText(x, y, text);
+	}
+
+	QString text = QString("%1").arg(m_centreFrame);
+
+	int tw = paint.fontMetrics().width(text);
+	int x = width()/2 + 4;
+    
+	if (hasLightBackground()) {
+	    paint.setPen(palette().background().color());
+	    for (int dx = -1; dx <= 1; ++dx) {
+		for (int dy = -1; dy <= 1; ++dy) {
+		    if ((dx && dy) || !(dx || dy)) continue;
+		    paint.drawText(x + dx, y + dy, text);
+		}
+	    }
+	    paint.setPen(QColor(50, 50, 50));
+	} else {
+	    paint.setPen(QColor(200, 200, 200));
+	}
+	paint.drawText(x, y, text);
+    }
+
+    if (m_clickedInRange && m_shiftPressed) {
+	paint.setPen(Qt::blue);
+	paint.drawRect(m_clickPos.x(), m_clickPos.y(),
+		       m_mousePos.x() - m_clickPos.x(),
+		       m_mousePos.y() - m_clickPos.y());
+    }
+    
+    paint.end();
+}
+
+void
+Pane::mousePressEvent(QMouseEvent *e)
+{
+    m_clickPos = e->pos();
+    m_clickedInRange = true;
+    m_shiftPressed = (e->modifiers() & Qt::ShiftModifier);
+    m_dragCentreFrame = m_centreFrame;
+
+    emit paneInteractedWith();
+}
+
+void
+Pane::mouseReleaseEvent(QMouseEvent *e)
+{
+    if (m_clickedInRange) {
+	mouseMoveEvent(e);
+    }
+    if (m_shiftPressed) {
+
+	int x0 = std::min(m_clickPos.x(), m_mousePos.x());
+	int x1 = std::max(m_clickPos.x(), m_mousePos.x());
+	int w = x1 - x0;
+
+	long newStartFrame = getStartFrame() + m_zoomLevel * x0;
+
+	if (newStartFrame <= -long(width() * m_zoomLevel)) {
+	    newStartFrame  = -long(width() * m_zoomLevel) + 1;
+	}
+
+	if (newStartFrame >= long(getModelsEndFrame())) {
+	    newStartFrame  = getModelsEndFrame() - 1;
+	}
+
+	float ratio = float(w) / float(width());
+//	std::cerr << "ratio: " << ratio << std::endl;
+	size_t newZoomLevel = (size_t)nearbyint(m_zoomLevel * ratio);
+	if (newZoomLevel < 1) newZoomLevel = 1;
+
+//	std::cerr << "start: " << m_startFrame << ", level " << m_zoomLevel << std::endl;
+	setZoomLevel(getZoomConstraintBlockSize(newZoomLevel));
+	setStartFrame(newStartFrame);
+
+	//cerr << "mouseReleaseEvent: start frame now " << m_startFrame << endl;
+//	update();
+    }
+    m_clickedInRange = false;
+
+    emit paneInteractedWith();
+}
+
+void
+Pane::mouseMoveEvent(QMouseEvent *e)
+{
+    if (!m_clickedInRange) {
+	
+//	std::cerr << "Pane: calling identifyLocalFeatures" << std::endl;
+
+//!!!	identifyLocalFeatures(true, e->x(), e->y());
+
+	bool previouslyIdentifying = m_identifyFeatures;
+	QPoint prevPoint = m_identifyPoint;
+
+	m_identifyFeatures = true;
+	m_identifyPoint = e->pos();
+
+	if (m_identifyFeatures != previouslyIdentifying ||
+	    m_identifyPoint != prevPoint) {
+	    update();
+	}
+
+    } else if (m_shiftPressed) {
+
+	m_mousePos = e->pos();
+	update();
+
+    } else {
+
+	long xoff = int(e->x()) - int(m_clickPos.x());
+	long frameOff = xoff * m_zoomLevel;
+
+	size_t newCentreFrame = m_dragCentreFrame;
+	
+	if (frameOff < 0) {
+	    newCentreFrame -= frameOff;
+	} else if (newCentreFrame >= size_t(frameOff)) {
+	    newCentreFrame -= frameOff;
+	} else {
+	    newCentreFrame = 0;
+	}
+
+	if (newCentreFrame >= getModelsEndFrame()) {
+	    newCentreFrame = getModelsEndFrame();
+	    if (newCentreFrame > 0) --newCentreFrame;
+	}
+
+	if (std::max(m_centreFrame, newCentreFrame) -
+	    std::min(m_centreFrame, newCentreFrame) > size_t(m_zoomLevel)) {
+	    setCentreFrame(newCentreFrame);
+	}
+    }
+}
+
+void
+Pane::mouseDoubleClickEvent(QMouseEvent *e)
+{
+    std::cerr << "mouseDoubleClickEvent" << std::endl;
+}
+
+void
+Pane::leaveEvent(QEvent *)
+{
+    bool previouslyIdentifying = m_identifyFeatures;
+    m_identifyFeatures = false;
+    if (previouslyIdentifying) update();
+}
+
+void
+Pane::wheelEvent(QWheelEvent *e)
+{
+    //std::cerr << "wheelEvent, delta " << e->delta() << std::endl;
+
+    int newZoomLevel = m_zoomLevel;
+
+    int count = e->delta();
+
+    if (count > 0) {
+	if (count >= 120) count /= 120;
+	else count = 1;
+    } 
+
+    if (count < 0) {
+	if (count <= -120) count /= 120;
+	else count = -1;
+    }
+  
+    while (count > 0) {
+	if (newZoomLevel <= 2) {
+	    newZoomLevel = 1;
+	    break;
+	}
+	newZoomLevel = getZoomConstraintBlockSize(newZoomLevel - 1, 
+						  ZoomConstraint::RoundDown);
+	--count;
+    }
+
+    while (count < 0) {
+	newZoomLevel = getZoomConstraintBlockSize(newZoomLevel + 1,
+						  ZoomConstraint::RoundUp);
+	++count;
+    }
+
+    if (newZoomLevel != m_zoomLevel) {
+	setZoomLevel(newZoomLevel);
+    }
+
+    emit paneInteractedWith();
+}
+    
+
+#ifdef INCLUDE_MOCFILES
+#include "Pane.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/Pane.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,60 @@
+
+/* -*- 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.
+*/
+
+#ifndef _PANE_H_
+#define _PANE_H_
+
+#include <QFrame>
+#include <QPoint>
+
+#include "base/ZoomConstraint.h"
+#include "base/View.h"
+
+class QWidget;
+class QPaintEvent;
+class Layer;
+
+class Pane : public View
+{
+    Q_OBJECT
+
+public:
+    Pane(QWidget *parent = 0);
+    virtual QString getPropertyContainerIconName() const { return "pane"; }
+
+    virtual bool shouldIlluminateLocalFeatures(const Layer *layer, QPoint &pos);
+
+    void setCentreLineVisible(bool visible);
+    bool getCentreLineVisible() const { return m_centreLineVisible; }
+
+signals:
+    void paneInteractedWith();
+
+protected:
+    virtual void paintEvent(QPaintEvent *e);
+    virtual void mousePressEvent(QMouseEvent *e);
+    virtual void mouseReleaseEvent(QMouseEvent *e);
+    virtual void mouseMoveEvent(QMouseEvent *e);
+    virtual void mouseDoubleClickEvent(QMouseEvent *e);
+    virtual void leaveEvent(QEvent *e);
+    virtual void wheelEvent(QWheelEvent *e);
+
+    bool m_identifyFeatures;
+    QPoint m_identifyPoint;
+    QPoint m_clickPos;
+    QPoint m_mousePos;
+    bool m_clickedInRange;
+    bool m_shiftPressed;
+    size_t m_dragCentreFrame;
+    bool m_centreLineVisible;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/PaneStack.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,239 @@
+
+/* -*- 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 "PaneStack.h"
+
+#include "widgets/Pane.h"
+#include "widgets/PropertyStack.h"
+#include "base/Layer.h"
+#include "base/ViewManager.h"
+
+#include <QApplication>
+#include <QHBoxLayout>
+#include <QPainter>
+#include <QPalette>
+#include <QLabel>
+
+#include <iostream>
+
+PaneStack::PaneStack(QWidget *parent, ViewManager *viewManager) :
+    QSplitter(parent),
+    m_currentPane(0),
+    m_viewManager(viewManager)
+{
+    setOrientation(Qt::Vertical);
+    setOpaqueResize(false);
+}
+
+Pane *
+PaneStack::addPane(bool suppressPropertyBox)
+{
+    QFrame *frame = new QFrame;
+    QHBoxLayout *layout = new QHBoxLayout;
+    layout->setMargin(0);
+    layout->setSpacing(2);
+
+    QLabel *currentIndicator = new QLabel(frame);
+    currentIndicator->setFixedWidth(QPainter(this).fontMetrics().width("x"));
+    layout->addWidget(currentIndicator);
+    layout->setStretchFactor(currentIndicator, 1);
+    currentIndicator->setScaledContents(true);
+    m_currentIndicators.push_back(currentIndicator);
+
+    Pane *pane = new Pane(frame);
+    pane->setViewManager(m_viewManager);
+    layout->addWidget(pane);
+    layout->setStretchFactor(pane, 10);
+    m_panes.push_back(pane);
+
+    QWidget *properties = 0;
+    if (suppressPropertyBox) {
+	properties = new QFrame();
+    } else {
+	properties = new PropertyStack(frame, pane);
+	connect(properties, SIGNAL(propertyContainerSelected(PropertyContainer *)),
+		this, SLOT(propertyContainerSelected(PropertyContainer *)));
+    }
+    layout->addWidget(properties);
+    layout->setStretchFactor(properties, 1);
+    m_propertyStacks.push_back(properties);
+
+    frame->setLayout(layout);
+    addWidget(frame);
+
+    connect(pane, SIGNAL(propertyContainerAdded(PropertyContainer *)),
+	    this, SLOT(propertyContainerAdded(PropertyContainer *)));
+    connect(pane, SIGNAL(propertyContainerRemoved(PropertyContainer *)),
+	    this, SLOT(propertyContainerRemoved(PropertyContainer *)));
+    connect(pane, SIGNAL(paneInteractedWith()),
+	    this, SLOT(paneInteractedWith()));
+
+    if (!m_currentPane) {
+	setCurrentPane(pane);
+    }
+
+    return pane;
+}
+
+Pane *
+PaneStack::getPane(int n)
+{
+    return m_panes[n];
+}
+
+void
+PaneStack::deletePane(Pane *pane)
+{
+    int n = 0;
+    std::vector<Pane *>::iterator i = m_panes.begin();
+    std::vector<QWidget *>::iterator j = m_propertyStacks.begin();
+    std::vector<QLabel *>::iterator k = m_currentIndicators.begin();
+
+    while (i != m_panes.end()) {
+	if (*i == pane) break;
+	++i;
+	++j;
+	++k;
+	++n;
+    }
+    if (n >= int(m_panes.size())) return;
+
+    m_panes.erase(i);
+    m_propertyStacks.erase(j);
+    m_currentIndicators.erase(k);
+    delete widget(n);
+
+    if (m_currentPane == pane) {
+	if (m_panes.size() > 0) {
+	    setCurrentPane(m_panes[0]);
+	} else {
+	    setCurrentPane(0);
+	}
+    }
+}
+
+int
+PaneStack::getPaneCount() const
+{
+    return m_panes.size();
+}
+
+void
+PaneStack::setCurrentPane(Pane *pane) // may be null
+{
+    if (m_currentPane == pane) return;
+    
+    std::vector<Pane *>::iterator i = m_panes.begin();
+    std::vector<QLabel *>::iterator k = m_currentIndicators.begin();
+
+    // We used to do this by setting the foreground and background
+    // role, but it seems the background role is ignored and the
+    // background drawn transparent in Qt 4.1 -- I can't quite see why
+    
+    QPixmap selectedMap(1, 1);
+    selectedMap.fill(QApplication::palette().color(QPalette::Foreground));
+    
+    QPixmap unselectedMap(1, 1);
+    unselectedMap.fill(QApplication::palette().color(QPalette::Background));
+
+    while (i != m_panes.end()) {
+	if (*i == pane) {
+	    (*k)->setPixmap(selectedMap);
+	} else {
+	    (*k)->setPixmap(unselectedMap);
+	}
+	++i;
+	++k;
+    }
+    m_currentPane = pane;
+
+    emit currentPaneChanged(m_currentPane);
+}
+
+Pane *
+PaneStack::getCurrentPane() 
+{
+    return m_currentPane;
+}
+
+void
+PaneStack::propertyContainerAdded(PropertyContainer *)
+{
+    sizePropertyStacks();
+}
+
+void
+PaneStack::propertyContainerRemoved(PropertyContainer *)
+{
+    sizePropertyStacks();
+}
+
+void
+PaneStack::propertyContainerSelected(PropertyContainer *pc)
+{
+    std::vector<Pane *>::iterator i = m_panes.begin();
+    std::vector<QWidget *>::iterator j = m_propertyStacks.begin();
+
+    while (i != m_panes.end()) {
+	PropertyStack *stack = dynamic_cast<PropertyStack *>(*j);
+	if (stack && stack->containsContainer(pc)) {
+	    setCurrentPane(*i);
+	    break;
+	}
+	++i;
+	++j;
+    }
+}
+
+void
+PaneStack::paneInteractedWith()
+{
+    Pane *pane = dynamic_cast<Pane *>(sender());
+    if (!pane) return;
+    setCurrentPane(pane);
+}
+
+void
+PaneStack::sizePropertyStacks()
+{
+    int maxMinWidth = 0;
+
+    for (unsigned int i = 0; i < m_propertyStacks.size(); ++i) {
+	if (!m_propertyStacks[i]) continue;
+	std::cerr << "PaneStack::sizePropertyStacks: " << i << ": min " 
+		  << m_propertyStacks[i]->minimumSizeHint().width() << ", current "
+		  << m_propertyStacks[i]->width() << std::endl;
+
+	if (m_propertyStacks[i]->minimumSizeHint().width() > maxMinWidth) {
+	    maxMinWidth = m_propertyStacks[i]->minimumSizeHint().width();
+	}
+    }
+
+    std::cerr << "PaneStack::sizePropertyStacks: max min width " << maxMinWidth << std::endl;
+
+#ifdef Q_WS_MAC
+    // This is necessary to compensate for cb->setMinimumSize(10, 10)
+    // in PropertyBox in the Mac version (to avoid a mysterious crash)
+    int setWidth = maxMinWidth * 3 / 2;
+#else
+    int setWidth = maxMinWidth;
+#endif
+
+    for (unsigned int i = 0; i < m_propertyStacks.size(); ++i) {
+	if (!m_propertyStacks[i]) continue;
+	m_propertyStacks[i]->setMinimumWidth(setWidth);
+    }
+}
+    
+
+#ifdef INCLUDE_MOCFILES
+#include "PaneStack.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/PaneStack.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,59 @@
+
+/* -*- 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.
+*/
+
+#ifndef _PANESTACK_H_
+#define _PANESTACK_H_
+
+#include <QSplitter>
+
+class QWidget;
+class QLabel;
+class Pane;
+class ViewManager;
+class PropertyContainer;
+class PropertyStack;
+
+class PaneStack : public QSplitter
+{
+    Q_OBJECT
+
+public:
+    PaneStack(QWidget *parent, ViewManager *viewManager);
+
+    Pane *addPane(bool suppressPropertyBox = false); // I own the returned value
+    Pane *getPane(int n); // I own the returned value
+    void deletePane(Pane *pane); // Deletes the pane and all its views
+    int getPaneCount() const;
+
+    void setCurrentPane(Pane *pane);
+    Pane *getCurrentPane();
+
+signals:
+    void currentPaneChanged(Pane *pane);
+
+public slots:
+    void propertyContainerAdded(PropertyContainer *);
+    void propertyContainerRemoved(PropertyContainer *);
+    void propertyContainerSelected(PropertyContainer *);
+    void paneInteractedWith();
+
+protected:
+    Pane *m_currentPane;
+    //!!! should be a single vector of structs
+    std::vector<Pane *> m_panes; // I own these
+    std::vector<QWidget *> m_propertyStacks; // I own these
+    std::vector<QLabel *> m_currentIndicators; // I own these
+    ViewManager *m_viewManager; // I don't own this
+
+    void sizePropertyStacks();
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/Panner.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,236 @@
+/* -*- 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 "Panner.h"
+#include "base/Layer.h"
+#include "base/Model.h"
+#include "base/ZoomConstraint.h"
+
+#include <QPaintEvent>
+#include <QPainter>
+#include <iostream>
+
+using std::cerr;
+using std::endl;
+
+Panner::Panner(QWidget *w) :
+    View(w, false),
+    m_clickedInRange(false)
+{
+    setObjectName(tr("Panner"));
+    m_followPan = false;
+    m_followZoom = false;
+}
+
+void
+Panner::modelChanged(size_t startFrame, size_t endFrame)
+{
+    View::modelChanged(startFrame, endFrame);
+}
+
+void
+Panner::modelReplaced()
+{
+    View::modelReplaced();
+}
+
+void
+Panner::registerView(View *widget)
+{
+    m_widgets[widget] = WidgetRec(0, -1);
+    update(); 
+}
+
+void
+Panner::unregisterView(View *widget)
+{
+    m_widgets.erase(widget);
+    update();
+}
+
+void
+Panner::viewManagerCentreFrameChanged(void *p, unsigned long f, bool)
+{
+//    std::cerr << "Panner[" << this << "]::viewManagerCentreFrameChanged(" 
+//	      << p << ", " << f << ")" << std::endl;
+
+    if (p == this) return;
+    if (m_widgets.find(p) != m_widgets.end()) {
+	m_widgets[p].first = f;
+	update();
+    }
+}
+
+void
+Panner::viewManagerZoomLevelChanged(void *p, unsigned long z, bool)
+{
+    if (p == this) return;
+    if (m_widgets.find(p) != m_widgets.end()) {
+	m_widgets[p].second = z;
+	update();
+    }
+}
+
+void
+Panner::viewManagerPlaybackFrameChanged(unsigned long f)
+{
+    bool changed = false;
+
+    if (m_playPointerFrame / m_zoomLevel != f / m_zoomLevel) changed = true;
+    m_playPointerFrame = f;
+
+    for (WidgetMap::iterator i = m_widgets.begin(); i != m_widgets.end(); ++i) {
+	unsigned long of = i->second.first;
+	i->second.first = f;
+	if (of / m_zoomLevel != f / m_zoomLevel) changed = true;
+    }
+
+    if (changed) update();
+}
+
+void
+Panner::paintEvent(QPaintEvent *e)
+{
+/*!!!
+    // Force View to recalculate zoom in case the size of the
+    // widget has changed.  (We need a better name/mechanism for this)
+    m_newModel = true;
+*/
+
+    // Recalculate zoom in case the size of the widget has changed.
+
+    size_t startFrame = getModelsStartFrame();
+    size_t frameCount = getModelsEndFrame() - getModelsStartFrame();
+    int zoomLevel = frameCount / width();
+    if (zoomLevel < 1) zoomLevel = 1;
+    zoomLevel = getZoomConstraintBlockSize(zoomLevel,
+					   ZoomConstraint::RoundUp);
+    if (zoomLevel != m_zoomLevel) {
+	m_zoomLevel = zoomLevel;
+	emit zoomLevelChanged(this, m_zoomLevel, m_followZoom);
+    }
+    size_t centreFrame = startFrame + m_zoomLevel * (width() / 2);
+    if (centreFrame > (startFrame + getModelsEndFrame())/2) {
+	centreFrame = (startFrame + getModelsEndFrame())/2;
+    }
+    if (centreFrame != m_centreFrame) {
+	m_centreFrame = centreFrame;
+	emit centreFrameChanged(this, m_centreFrame, false);
+    }
+
+    View::paintEvent(e);
+
+    QPainter paint;
+    paint.begin(this);
+
+    QRect r(rect());
+
+    if (e) {
+	r = e->rect();
+	paint.setClipRect(r);
+    }
+
+    paint.setPen(Qt::black);
+
+    int y = 0;
+    long prevCentre = 0;
+    long prevZoom = -1;
+
+    for (WidgetMap::iterator i = m_widgets.begin(); i != m_widgets.end(); ++i) {
+	if (!i->first) continue;
+
+	View *w = (View *)i->first;
+	if (i->second.second < 0) i->second.second = w->getZoomLevel();
+	if (i->second.second < 0) continue;
+
+	long c = (long)i->second.first;
+	long z = (long)i->second.second;
+
+	long f0 = c - (w->width() / 2) * z;
+	long f1 = c + (w->width() / 2) * z;
+
+	int x0 = (f0 - long(getCentreFrame())) / getZoomLevel() + width()/2;
+	int x1 = (f1 - long(getCentreFrame())) / getZoomLevel() + width()/2 - 1;
+
+	if (c != prevCentre || z != prevZoom) {
+	    y += height() / 10 + 1;
+	    prevCentre = c;
+	    prevZoom = z;
+	}
+	
+	paint.drawRect(x0, y, x1 - x0, height() - 2 * y);
+    }
+
+    paint.end();
+}
+
+void
+Panner::mousePressEvent(QMouseEvent *e)
+{
+    m_clickPos = e->pos();
+    for (WidgetMap::iterator i = m_widgets.begin(); i != m_widgets.end(); ++i) {
+	if (i->first && i->second.second >= 0) {
+	    m_clickedInRange = true;
+	    m_dragCentreFrame = i->second.first;
+	}
+    }
+}
+
+void
+Panner::mouseReleaseEvent(QMouseEvent *e)
+{
+    if (m_clickedInRange) {
+	mouseMoveEvent(e);
+    }
+    m_clickedInRange = false;
+}
+
+void
+Panner::mouseMoveEvent(QMouseEvent *e)
+{
+    if (!m_clickedInRange) return;
+
+/*!!!
+    long newFrame = getStartFrame() + e->x() * m_zoomLevel;
+
+    if (newFrame < 0) newFrame = 0;
+    if (newFrame >= getModelsEndFrame()) {
+	newFrame = getModelsEndFrame();
+	if (newFrame > 0) --newFrame;
+    }
+    emit centreFrameChanged(this, newFrame, true);
+*/
+
+    long xoff = int(e->x()) - int(m_clickPos.x());
+    long frameOff = xoff * m_zoomLevel;
+    
+    size_t newCentreFrame = m_dragCentreFrame;
+    if (frameOff > 0) {
+	newCentreFrame += frameOff;
+    } else if (newCentreFrame >= size_t(-frameOff)) {
+	newCentreFrame += frameOff;
+    } else {
+	newCentreFrame = 0;
+    }
+
+    if (newCentreFrame >= getModelsEndFrame()) {
+	newCentreFrame = getModelsEndFrame();
+	if (newCentreFrame > 0) --newCentreFrame;
+    }
+    
+    if (std::max(m_centreFrame, newCentreFrame) -
+	std::min(m_centreFrame, newCentreFrame) > size_t(m_zoomLevel)) {
+	emit centreFrameChanged(this, newCentreFrame, true);
+    }
+}
+
+#ifdef INCLUDE_MOCFILES
+#include "Panner.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/Panner.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,61 @@
+/* -*- 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.
+*/
+
+#ifndef _PAN_WIDGET_H_
+#define _PAN_WIDGET_H_
+
+#include "base/View.h"
+
+#include <QPoint>
+
+class QWidget;
+class QPaintEvent;
+class Layer;
+class View;
+
+#include <map>
+
+class Panner : public View
+{
+    Q_OBJECT
+
+public:
+    Panner(QWidget *parent = 0);
+
+    void registerView(View *widget);
+    void unregisterView(View *widget);
+
+    virtual QString getPropertyContainerIconName() const { return "panner"; }
+
+public slots:
+    virtual void modelChanged(size_t startFrame, size_t endFrame);
+    virtual void modelReplaced();
+
+    virtual void viewManagerCentreFrameChanged(void *, unsigned long, bool);
+    virtual void viewManagerZoomLevelChanged(void *, unsigned long, bool);
+    virtual void viewManagerPlaybackFrameChanged(unsigned long);
+
+protected:
+    virtual void paintEvent(QPaintEvent *e);
+    virtual void mousePressEvent(QMouseEvent *e);
+    virtual void mouseReleaseEvent(QMouseEvent *e);
+    virtual void mouseMoveEvent(QMouseEvent *e);
+
+    QPoint m_clickPos;
+    QPoint m_mousePos;
+    bool m_clickedInRange;
+    size_t m_dragCentreFrame;
+    
+    typedef std::pair<size_t, int> WidgetRec; // centre, zoom (-1 = invalid)
+    typedef std::map<void *, WidgetRec> WidgetMap;
+    WidgetMap m_widgets;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/PropertyBox.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,272 @@
+/* -*- 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 "PropertyBox.h"
+
+#include "base/PropertyContainer.h"
+
+#include "AudioDial.h"
+
+#include <QGridLayout>
+#include <QHBoxLayout>
+#include <QCheckBox>
+#include <QComboBox>
+#include <QLabel>
+
+#include <cassert>
+#include <iostream>
+
+//#define DEBUG_PROPERTY_BOX 1
+
+PropertyBox::PropertyBox(PropertyContainer *container) :
+    m_container(container)
+{
+#ifdef DEBUG_PROPERTY_BOX
+    std::cerr << "PropertyBox[" << this << "(\"" <<
+	container->getPropertyContainerName().toStdString() << "\")]::PropertyBox" << std::endl;
+#endif
+
+    m_layout = new QGridLayout;
+    setLayout(m_layout);
+
+    PropertyContainer::PropertyList properties = container->getProperties();
+
+    blockSignals(true);
+
+    size_t i;
+
+    for (i = 0; i < properties.size(); ++i) {
+	updatePropertyEditor(properties[i]);
+    }
+
+    blockSignals(false);
+
+    m_layout->setRowStretch(m_layout->rowCount(), 10);
+
+#ifdef DEBUG_PROPERTY_BOX
+    std::cerr << "PropertyBox[" << this << "]::PropertyBox returning" << std::endl;
+#endif
+}
+
+PropertyBox::~PropertyBox()
+{
+#ifdef DEBUG_PROPERTY_BOX
+    std::cerr << "PropertyBox[" << this << "(\"" << m_container->getPropertyContainerName().toStdString() << "\")]::~PropertyBox" << std::endl;
+#endif
+}
+
+void
+PropertyBox::updatePropertyEditor(PropertyContainer::PropertyName name)
+{
+    PropertyContainer::PropertyType type = m_container->getPropertyType(name);
+    int row = m_layout->rowCount();
+
+    int min = 0, max = 0, value = 0;
+    value = m_container->getPropertyRangeAndValue(name, &min, &max);
+
+    bool have = (m_propertyControllers.find(name) !=
+		 m_propertyControllers.end());
+
+    QString groupName = m_container->getPropertyGroupName(name);
+
+#ifdef DEBUG_PROPERTY_BOX
+    std::cerr << "PropertyBox[" << this
+	      << "(\"" << m_container->getPropertyContainerName().toStdString()
+	      << "\")]";
+    std::cerr << "::updatePropertyEditor(\"" << name.toStdString() << "\"):";
+    std::cerr << " value " << value << ", have " << have << ", group \""
+	      << groupName.toStdString() << "\"" << std::endl;
+#endif
+
+    bool inGroup = (groupName != QString());
+
+    if (!have) {
+	if (inGroup) {
+	    if (m_groupLayouts.find(groupName) == m_groupLayouts.end()) {
+#ifdef DEBUG_PROPERTY_BOX
+		std::cerr << "PropertyBox: adding label \"" << groupName.toStdString() << "\" and frame for group for \"" << name.toStdString() << "\"" << std::endl;
+#endif
+		m_layout->addWidget(new QLabel(groupName, this), row, 0);
+		QFrame *frame = new QFrame(this);
+		m_layout->addWidget(frame, row, 1, 1, 2);
+		m_groupLayouts[groupName] = new QHBoxLayout;
+		m_groupLayouts[groupName]->setMargin(0);
+		frame->setLayout(m_groupLayouts[groupName]);
+	    }
+	} else {
+#ifdef DEBUG_PROPERTY_BOX 
+	    std::cerr << "PropertyBox: adding label \"" << name.toStdString() << "\"" << std::endl;
+#endif
+	    m_layout->addWidget(new QLabel(name, this), row, 0);
+	}
+    }
+
+    switch (type) {
+
+    case PropertyContainer::ToggleProperty:
+    {
+	QCheckBox *cb;
+
+	if (have) {
+	    cb = dynamic_cast<QCheckBox *>(m_propertyControllers[name]);
+	    assert(cb);
+	} else {
+#ifdef DEBUG_PROPERTY_BOX 
+	    std::cerr << "PropertyBox: creating new checkbox" << std::endl;
+#endif
+	    cb = new QCheckBox();
+	    cb->setObjectName(name);
+	    connect(cb, SIGNAL(stateChanged(int)),
+		    this, SLOT(propertyControllerChanged(int)));
+	    if (inGroup) {
+		cb->setToolTip(name);
+		m_groupLayouts[groupName]->addWidget(cb);
+	    } else {
+		m_layout->addWidget(cb, row, 1, 1, 2);
+	    }
+	    m_propertyControllers[name] = cb;
+	}
+
+	if (cb->isChecked() != (value > 0)) cb->setChecked(value > 0);
+	break;
+    }
+
+    case PropertyContainer::RangeProperty:
+    {
+	AudioDial *dial;
+
+	if (have) {
+	    dial = dynamic_cast<AudioDial *>(m_propertyControllers[name]);
+	    assert(dial);
+	} else {
+#ifdef DEBUG_PROPERTY_BOX 
+	    std::cerr << "PropertyBox: creating new dial" << std::endl;
+#endif
+	    dial = new AudioDial();
+	    dial->setObjectName(name);
+	    dial->setMinimum(min);
+	    dial->setMaximum(max);
+	    dial->setPageStep(1);
+	    dial->setNotchesVisible(true);
+	    connect(dial, SIGNAL(valueChanged(int)),
+		    this, SLOT(propertyControllerChanged(int)));
+
+	    if (inGroup) {
+		dial->setFixedWidth(24);
+		dial->setFixedHeight(24);
+		dial->setToolTip(name);
+		m_groupLayouts[groupName]->addWidget(dial);
+	    } else {
+		dial->setFixedWidth(32);
+		dial->setFixedHeight(32);
+		m_layout->addWidget(dial, row, 1);
+		QLabel *label = new QLabel(this);
+		connect(dial, SIGNAL(valueChanged(int)),
+			label, SLOT(setNum(int)));
+		label->setNum(value);
+		m_layout->addWidget(label, row, 2);
+	    }
+
+	    m_propertyControllers[name] = dial;
+	}
+
+	if (dial->value() != value) dial->setValue(value);
+	break;
+    }
+
+    case PropertyContainer::ValueProperty:
+    {
+	QComboBox *cb;
+
+	if (have) {
+	    cb = dynamic_cast<QComboBox *>(m_propertyControllers[name]);
+	    assert(cb);
+	} else {
+#ifdef DEBUG_PROPERTY_BOX 
+	    std::cerr << "PropertyBox: creating new combobox" << std::endl;
+#endif
+
+	    cb = new QComboBox();
+	    cb->setObjectName(name);
+	    for (int i = min; i <= max; ++i) {
+		cb->addItem(m_container->getPropertyValueLabel(name, i));
+	    }
+	    connect(cb, SIGNAL(activated(int)),
+		    this, SLOT(propertyControllerChanged(int)));
+	    if (inGroup) {
+		cb->setToolTip(name);
+		m_groupLayouts[groupName]->addWidget(cb);
+	    } else {
+		m_layout->addWidget(cb, row, 1, 1, 2);
+	    }
+	    m_propertyControllers[name] = cb;
+	}
+
+	if (cb->currentIndex() != value) cb->setCurrentIndex(value);
+
+#ifdef Q_WS_MAC
+	// Crashes on startup without this, for some reason
+	cb->setMinimumSize(QSize(10, 10));
+#endif
+
+	break;
+    }
+
+    default:
+	break;
+    }
+}
+
+void
+PropertyBox::propertyContainerPropertyChanged(PropertyContainer *pc)
+{
+    if (pc != m_container) return;
+    
+    PropertyContainer::PropertyList properties = m_container->getProperties();
+    size_t i;
+
+    blockSignals(true);
+
+    for (i = 0; i < properties.size(); ++i) {
+	updatePropertyEditor(properties[i]);
+    }
+
+    blockSignals(false);
+}
+
+void
+PropertyBox::propertyControllerChanged(int value)
+{
+    QObject *obj = sender();
+    QString name = obj->objectName();
+
+//    std::cerr << "PropertyBox::propertyControllerChanged(" << name.toStdString()
+//	      << ", " << value << ")" << std::endl;
+    
+    PropertyContainer::PropertyType type = m_container->getPropertyType(name);
+    
+    if (type != PropertyContainer::InvalidProperty) {
+	m_container->setProperty(name, value);
+    }
+
+    if (type == PropertyContainer::RangeProperty) {
+	AudioDial *dial = dynamic_cast<AudioDial *>(m_propertyControllers[name]);
+	if (dial) {
+	    dial->setToolTip(QString("%1: %2").arg(name).arg(value));
+	    //!!! unfortunately this doesn't update an already-visible tooltip
+	}
+    }
+}
+    
+	    
+
+#ifdef INCLUDE_MOCFILES
+#include "PropertyBox.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/PropertyBox.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,45 @@
+/* -*- 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.
+*/
+
+#ifndef _PROPERTY_BOX_H_
+#define _PROPERTY_BOX_H_
+
+#include "base/PropertyContainer.h"
+
+#include <QFrame>
+#include <map>
+
+class QGridLayout;
+
+class PropertyBox : public QFrame
+{
+    Q_OBJECT
+
+public:
+    PropertyBox(PropertyContainer *);
+    ~PropertyBox();
+
+    PropertyContainer *getContainer() { return m_container; }
+
+public slots:
+    void propertyContainerPropertyChanged(PropertyContainer *);
+
+protected slots:
+    void propertyControllerChanged(int);
+
+protected:
+    void updatePropertyEditor(PropertyContainer::PropertyName);
+
+    QGridLayout *m_layout;
+    PropertyContainer *m_container;
+    std::map<QString, QLayout *> m_groupLayouts;
+    std::map<QString, QWidget *> m_propertyControllers;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/PropertyStack.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,139 @@
+/* -*- 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 "PropertyStack.h"
+#include "PropertyBox.h"
+#include "base/PropertyContainer.h"
+#include "base/View.h"
+
+#include <QIcon>
+#include <QTabWidget>
+
+#include <iostream>
+
+#define DEBUG_PROPERTY_STACK 1
+
+PropertyStack::PropertyStack(QWidget *parent, View *client) :
+    QTabWidget(parent),
+    m_client(client)
+{
+    repopulate();
+
+    connect(this, SIGNAL(currentChanged(int)),
+	    this, SLOT(selectedContainerChanged(int)));
+
+    connect(m_client, SIGNAL(propertyContainerAdded(PropertyContainer *)),
+	    this, SLOT(propertyContainerAdded(PropertyContainer *)));
+
+    connect(m_client, SIGNAL(propertyContainerRemoved(PropertyContainer *)),
+	    this, SLOT(propertyContainerRemoved(PropertyContainer *)));
+
+    connect(m_client, SIGNAL(propertyContainerPropertyChanged(PropertyContainer *)),
+	    this, SLOT(propertyContainerPropertyChanged(PropertyContainer *)));
+
+    connect(m_client, SIGNAL(propertyContainerNameChanged(PropertyContainer *)),
+	    this, SLOT(propertyContainerNameChanged(PropertyContainer *)));
+
+    connect(this, SIGNAL(propertyContainerSelected(PropertyContainer *)),
+	    m_client, SLOT(propertyContainerSelected(PropertyContainer *)));
+}
+
+void
+PropertyStack::repopulate()
+{
+    blockSignals(true);
+
+#ifdef DEBUG_PROPERTY_STACK
+    std::cerr << "PropertyStack::repopulate" << std::endl;
+#endif
+    
+    while (count() > 0) {
+	removeTab(0);
+    }
+    for (size_t i = 0; i < m_boxes.size(); ++i) {
+	delete m_boxes[i];
+    }
+    m_boxes.clear();
+    
+    for (size_t i = 0; i < m_client->getPropertyContainerCount(); ++i) {
+
+	PropertyContainer *container = m_client->getPropertyContainer(i);
+	QString name = container->getPropertyContainerName();
+	
+	QString iconName = container->getPropertyContainerIconName();
+
+	PropertyBox *box = new PropertyBox(container);
+
+	QIcon icon(QString(":/icons/%1.png").arg(iconName));
+	if (icon.isNull()) {
+	    addTab(box, name);
+	} else {
+	    addTab(box, icon, QString("&%1").arg(i + 1));
+	    setTabToolTip(count() - 1, name);
+	}
+
+	m_boxes.push_back(box);
+    }    
+
+    blockSignals(false);
+}
+
+bool
+PropertyStack::containsContainer(PropertyContainer *pc) const
+{
+    for (size_t i = 0; i < m_client->getPropertyContainerCount(); ++i) {
+	PropertyContainer *container = m_client->getPropertyContainer(i);
+	if (pc == container) return true;
+    }
+
+    return false;
+}
+
+void
+PropertyStack::propertyContainerAdded(PropertyContainer *)
+{
+    if (sender() != m_client) return;
+    repopulate();
+}
+
+void
+PropertyStack::propertyContainerRemoved(PropertyContainer *)
+{
+    if (sender() != m_client) return;
+    repopulate();
+}
+
+void
+PropertyStack::propertyContainerPropertyChanged(PropertyContainer *pc)
+{
+    for (unsigned int i = 0; i < m_boxes.size(); ++i) {
+	if (pc == m_boxes[i]->getContainer()) {
+	    m_boxes[i]->propertyContainerPropertyChanged(pc);
+	}
+    }
+}
+
+void
+PropertyStack::propertyContainerNameChanged(PropertyContainer *pc)
+{
+    if (sender() != m_client) return;
+    repopulate();
+}
+
+void
+PropertyStack::selectedContainerChanged(int n)
+{
+    if (n >= int(m_boxes.size())) return;
+    emit propertyContainerSelected(m_boxes[n]->getContainer());
+}
+
+#ifdef INCLUDE_MOCFILES
+#include "PropertyStack.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/PropertyStack.h	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,51 @@
+/* -*- 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.
+*/
+
+#ifndef _PROPERTY_STACK_H_
+#define _PROPERTY_STACK_H_
+
+#include <QFrame>
+#include <QTabWidget>
+#include <vector>
+
+class Layer;
+class View;
+class PropertyBox;
+class PropertyContainer;
+
+class PropertyStack : public QTabWidget
+{
+    Q_OBJECT
+
+public:
+    PropertyStack(QWidget *parent, View *client);
+
+    bool containsContainer(PropertyContainer *container) const;
+
+signals:
+    void propertyContainerSelected(PropertyContainer *container);
+
+public slots:
+    void propertyContainerAdded(PropertyContainer *);
+    void propertyContainerRemoved(PropertyContainer *);
+    void propertyContainerPropertyChanged(PropertyContainer *);
+    void propertyContainerNameChanged(PropertyContainer *);
+
+protected slots:
+    void selectedContainerChanged(int);
+
+protected:
+    View *m_client;
+    std::vector<PropertyBox *> m_boxes;
+
+    void repopulate();
+    void updateValues(PropertyContainer *);
+};
+
+#endif