changeset 0:cd5d7ff8ef38

* Reorganising code base. This revision will not compile.
author Chris Cannam
date Mon, 31 Jul 2006 12:03:45 +0000
parents
children 40116f709d3b
files audioio/AudioCallbackPlaySource.cpp audioio/AudioCallbackPlaySource.h audioio/AudioCallbackPlayTarget.cpp audioio/AudioCallbackPlayTarget.h audioio/AudioCoreAudioTarget.cpp audioio/AudioCoreAudioTarget.h audioio/AudioGenerator.cpp audioio/AudioGenerator.h audioio/AudioJACKTarget.cpp audioio/AudioJACKTarget.h audioio/AudioPortAudioTarget.cpp audioio/AudioPortAudioTarget.h audioio/AudioTargetFactory.cpp audioio/AudioTargetFactory.h audioio/IntegerTimeStretcher.cpp audioio/IntegerTimeStretcher.h document/Document.cpp document/Document.h document/SVFileReader.cpp document/SVFileReader.h main/MainWindow.cpp main/MainWindow.h main/PreferencesDialog.cpp main/PreferencesDialog.h main/main.cpp main/systeminit.cpp transform/FeatureExtractionPluginTransform.cpp transform/FeatureExtractionPluginTransform.h transform/RealTimePluginTransform.cpp transform/RealTimePluginTransform.h transform/Transform.cpp transform/Transform.h transform/TransformFactory.cpp transform/TransformFactory.h
diffstat 34 files changed, 11325 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioCallbackPlaySource.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,1257 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "AudioCallbackPlaySource.h"
+
+#include "AudioGenerator.h"
+
+#include "base/Model.h"
+#include "base/ViewManager.h"
+#include "base/PlayParameterRepository.h"
+#include "model/DenseTimeValueModel.h"
+#include "model/SparseOneDimensionalModel.h"
+#include "IntegerTimeStretcher.h"
+
+#include <iostream>
+#include <cassert>
+
+//#define DEBUG_AUDIO_PLAY_SOURCE 1
+//#define DEBUG_AUDIO_PLAY_SOURCE_PLAYING 1
+
+//const size_t AudioCallbackPlaySource::m_ringBufferSize = 102400;
+const size_t AudioCallbackPlaySource::m_ringBufferSize = 131071;
+
+AudioCallbackPlaySource::AudioCallbackPlaySource(ViewManager *manager) :
+    m_viewManager(manager),
+    m_audioGenerator(new AudioGenerator()),
+    m_readBuffers(0),
+    m_writeBuffers(0),
+    m_readBufferFill(0),
+    m_writeBufferFill(0),
+    m_bufferScavenger(1),
+    m_sourceChannelCount(0),
+    m_blockSize(1024),
+    m_sourceSampleRate(0),
+    m_targetSampleRate(0),
+    m_playLatency(0),
+    m_playing(false),
+    m_exiting(false),
+    m_lastModelEndFrame(0),
+    m_outputLeft(0.0),
+    m_outputRight(0.0),
+    m_slowdownCounter(0),
+    m_timeStretcher(0),
+    m_fillThread(0),
+    m_converter(0)
+{
+    m_viewManager->setAudioPlaySource(this);
+
+    connect(m_viewManager, SIGNAL(selectionChanged()),
+	    this, SLOT(selectionChanged()));
+    connect(m_viewManager, SIGNAL(playLoopModeChanged()),
+	    this, SLOT(playLoopModeChanged()));
+    connect(m_viewManager, SIGNAL(playSelectionModeChanged()),
+	    this, SLOT(playSelectionModeChanged()));
+
+    connect(PlayParameterRepository::getInstance(),
+	    SIGNAL(playParametersChanged(PlayParameters *)),
+	    this, SLOT(playParametersChanged(PlayParameters *)));
+}
+
+AudioCallbackPlaySource::~AudioCallbackPlaySource()
+{
+    m_exiting = true;
+
+    if (m_fillThread) {
+	m_condition.wakeAll();
+	m_fillThread->wait();
+	delete m_fillThread;
+    }
+
+    clearModels();
+    
+    if (m_readBuffers != m_writeBuffers) {
+	delete m_readBuffers;
+    }
+
+    delete m_writeBuffers;
+
+    delete m_audioGenerator;
+
+    m_bufferScavenger.scavenge(true);
+}
+
+void
+AudioCallbackPlaySource::addModel(Model *model)
+{
+    if (m_models.find(model) != m_models.end()) return;
+
+    bool canPlay = m_audioGenerator->addModel(model);
+
+    m_mutex.lock();
+
+    m_models.insert(model);
+    if (model->getEndFrame() > m_lastModelEndFrame) {
+	m_lastModelEndFrame = model->getEndFrame();
+    }
+
+    bool buffersChanged = false, srChanged = false;
+
+    size_t modelChannels = 1;
+    DenseTimeValueModel *dtvm = dynamic_cast<DenseTimeValueModel *>(model);
+    if (dtvm) modelChannels = dtvm->getChannelCount();
+    if (modelChannels > m_sourceChannelCount) {
+	m_sourceChannelCount = modelChannels;
+    }
+
+//    std::cerr << "Adding model with " << modelChannels << " channels " << std::endl;
+
+    if (m_sourceSampleRate == 0) {
+
+	m_sourceSampleRate = model->getSampleRate();
+	srChanged = true;
+
+    } else if (model->getSampleRate() != m_sourceSampleRate) {
+
+        // If this is a dense time-value model and we have no other, we
+        // can just switch to this model's sample rate
+
+        if (dtvm) {
+
+            bool conflicting = false;
+
+            for (std::set<Model *>::const_iterator i = m_models.begin();
+                 i != m_models.end(); ++i) {
+                if (*i != dtvm && dynamic_cast<DenseTimeValueModel *>(*i)) {
+                    std::cerr << "AudioCallbackPlaySource::addModel: Conflicting dense time-value model " << *i << " found" << std::endl;
+                    conflicting = true;
+                    break;
+                }
+            }
+
+            if (conflicting) {
+
+                std::cerr << "AudioCallbackPlaySource::addModel: ERROR: "
+                          << "New model sample rate does not match" << std::endl
+                          << "existing model(s) (new " << model->getSampleRate()
+                          << " vs " << m_sourceSampleRate
+                          << "), playback will be wrong"
+                          << std::endl;
+                
+                emit sampleRateMismatch(model->getSampleRate(), m_sourceSampleRate,
+                                        false);
+            } else {
+                m_sourceSampleRate = model->getSampleRate();
+                srChanged = true;
+            }
+        }
+    }
+
+    if (!m_writeBuffers || (m_writeBuffers->size() < getTargetChannelCount())) {
+	clearRingBuffers(true, getTargetChannelCount());
+	buffersChanged = true;
+    } else {
+	if (canPlay) clearRingBuffers(true);
+    }
+
+    if (buffersChanged || srChanged) {
+	if (m_converter) {
+	    src_delete(m_converter);
+	    m_converter = 0;
+	}
+    }
+
+    m_mutex.unlock();
+
+    m_audioGenerator->setTargetChannelCount(getTargetChannelCount());
+
+    if (!m_fillThread) {
+	m_fillThread = new AudioCallbackPlaySourceFillThread(*this);
+	m_fillThread->start();
+    }
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+    std::cerr << "AudioCallbackPlaySource::addModel: emitting modelReplaced" << std::endl;
+#endif
+
+    if (buffersChanged || srChanged) {
+	emit modelReplaced();
+    }
+
+    m_condition.wakeAll();
+}
+
+void
+AudioCallbackPlaySource::removeModel(Model *model)
+{
+    m_mutex.lock();
+
+    m_models.erase(model);
+
+    if (m_models.empty()) {
+	if (m_converter) {
+	    src_delete(m_converter);
+	    m_converter = 0;
+	}
+	m_sourceSampleRate = 0;
+    }
+
+    size_t lastEnd = 0;
+    for (std::set<Model *>::const_iterator i = m_models.begin();
+	 i != m_models.end(); ++i) {
+//	std::cerr << "AudioCallbackPlaySource::removeModel(" << model << "): checking end frame on model " << *i << std::endl;
+	if ((*i)->getEndFrame() > lastEnd) lastEnd = (*i)->getEndFrame();
+//	std::cerr << "(done, lastEnd now " << lastEnd << ")" << std::endl;
+    }
+    m_lastModelEndFrame = lastEnd;
+
+    m_mutex.unlock();
+
+    m_audioGenerator->removeModel(model);
+
+    clearRingBuffers();
+}
+
+void
+AudioCallbackPlaySource::clearModels()
+{
+    m_mutex.lock();
+
+    m_models.clear();
+
+    if (m_converter) {
+	src_delete(m_converter);
+	m_converter = 0;
+    }
+
+    m_lastModelEndFrame = 0;
+
+    m_sourceSampleRate = 0;
+
+    m_mutex.unlock();
+
+    m_audioGenerator->clearModels();
+}    
+
+void
+AudioCallbackPlaySource::clearRingBuffers(bool haveLock, size_t count)
+{
+    if (!haveLock) m_mutex.lock();
+
+    if (count == 0) {
+	if (m_writeBuffers) count = m_writeBuffers->size();
+    }
+
+    size_t sf = m_readBufferFill;
+    RingBuffer<float> *rb = getReadRingBuffer(0);
+    if (rb) {
+	//!!! This is incorrect if we're in a non-contiguous selection
+	//Same goes for all related code (subtracting the read space
+	//from the fill frame to try to establish where the effective
+	//pre-resample/timestretch read pointer is)
+	size_t rs = rb->getReadSpace();
+	if (rs < sf) sf -= rs;
+	else sf = 0;
+    }
+    m_writeBufferFill = sf;
+
+    if (m_readBuffers != m_writeBuffers) {
+	delete m_writeBuffers;
+    }
+
+    m_writeBuffers = new RingBufferVector;
+
+    for (size_t i = 0; i < count; ++i) {
+	m_writeBuffers->push_back(new RingBuffer<float>(m_ringBufferSize));
+    }
+
+//    std::cerr << "AudioCallbackPlaySource::clearRingBuffers: Created "
+//	      << count << " write buffers" << std::endl;
+
+    if (!haveLock) {
+	m_mutex.unlock();
+    }
+}
+
+void
+AudioCallbackPlaySource::play(size_t startFrame)
+{
+    if (m_viewManager->getPlaySelectionMode() &&
+	!m_viewManager->getSelections().empty()) {
+	MultiSelection::SelectionList selections = m_viewManager->getSelections();
+	MultiSelection::SelectionList::iterator i = selections.begin();
+	if (i != selections.end()) {
+	    if (startFrame < i->getStartFrame()) {
+		startFrame = i->getStartFrame();
+	    } else {
+		MultiSelection::SelectionList::iterator j = selections.end();
+		--j;
+		if (startFrame >= j->getEndFrame()) {
+		    startFrame = i->getStartFrame();
+		}
+	    }
+	}
+    } else {
+	if (startFrame >= m_lastModelEndFrame) {
+	    startFrame = 0;
+	}
+    }
+
+    // The fill thread will automatically empty its buffers before
+    // starting again if we have not so far been playing, but not if
+    // we're just re-seeking.
+
+    m_mutex.lock();
+    if (m_playing) {
+	m_readBufferFill = m_writeBufferFill = startFrame;
+	if (m_readBuffers) {
+	    for (size_t c = 0; c < getTargetChannelCount(); ++c) {
+		RingBuffer<float> *rb = getReadRingBuffer(c);
+		if (rb) rb->reset();
+	    }
+	}
+	if (m_converter) src_reset(m_converter);
+    } else {
+	if (m_converter) src_reset(m_converter);
+	m_readBufferFill = m_writeBufferFill = startFrame;
+    }
+    m_mutex.unlock();
+
+    m_audioGenerator->reset();
+
+    bool changed = !m_playing;
+    m_playing = true;
+    m_condition.wakeAll();
+    if (changed) emit playStatusChanged(m_playing);
+}
+
+void
+AudioCallbackPlaySource::stop()
+{
+    bool changed = m_playing;
+    m_playing = false;
+    m_condition.wakeAll();
+    if (changed) emit playStatusChanged(m_playing);
+}
+
+void
+AudioCallbackPlaySource::selectionChanged()
+{
+    if (m_viewManager->getPlaySelectionMode()) {
+	clearRingBuffers();
+    }
+}
+
+void
+AudioCallbackPlaySource::playLoopModeChanged()
+{
+    clearRingBuffers();
+}
+
+void
+AudioCallbackPlaySource::playSelectionModeChanged()
+{
+    if (!m_viewManager->getSelections().empty()) {
+	clearRingBuffers();
+    }
+}
+
+void
+AudioCallbackPlaySource::playParametersChanged(PlayParameters *params)
+{
+    clearRingBuffers();
+}
+
+void
+AudioCallbackPlaySource::setTargetBlockSize(size_t size)
+{
+//    std::cerr << "AudioCallbackPlaySource::setTargetBlockSize() -> " << size << std::endl;
+    assert(size < m_ringBufferSize);
+    m_blockSize = size;
+}
+
+size_t
+AudioCallbackPlaySource::getTargetBlockSize() const
+{
+//    std::cerr << "AudioCallbackPlaySource::getTargetBlockSize() -> " << m_blockSize << std::endl;
+    return m_blockSize;
+}
+
+void
+AudioCallbackPlaySource::setTargetPlayLatency(size_t latency)
+{
+    m_playLatency = latency;
+}
+
+size_t
+AudioCallbackPlaySource::getTargetPlayLatency() const
+{
+    return m_playLatency;
+}
+
+size_t
+AudioCallbackPlaySource::getCurrentPlayingFrame()
+{
+    bool resample = false;
+    double ratio = 1.0;
+
+    if (getSourceSampleRate() != getTargetSampleRate()) {
+	resample = true;
+	ratio = double(getSourceSampleRate()) / double(getTargetSampleRate());
+    }
+
+    size_t readSpace = 0;
+    for (size_t c = 0; c < getTargetChannelCount(); ++c) {
+	RingBuffer<float> *rb = getReadRingBuffer(c);
+	if (rb) {
+	    size_t spaceHere = rb->getReadSpace();
+	    if (c == 0 || spaceHere < readSpace) readSpace = spaceHere;
+	}
+    }
+
+    if (resample) {
+	readSpace = size_t(readSpace * ratio + 0.1);
+    }
+
+    size_t latency = m_playLatency;
+    if (resample) latency = size_t(m_playLatency * ratio + 0.1);
+    
+    TimeStretcherData *timeStretcher = m_timeStretcher;
+    if (timeStretcher) {
+	latency += timeStretcher->getStretcher(0)->getProcessingLatency();
+    }
+
+    latency += readSpace;
+    size_t bufferedFrame = m_readBufferFill;
+
+    bool looping = m_viewManager->getPlayLoopMode();
+    bool constrained = (m_viewManager->getPlaySelectionMode() &&
+			!m_viewManager->getSelections().empty());
+
+    size_t framePlaying = bufferedFrame;
+
+    if (looping && !constrained) {
+	while (framePlaying < latency) framePlaying += m_lastModelEndFrame;
+    }
+
+    if (framePlaying > latency) framePlaying -= latency;
+    else framePlaying = 0;
+
+    if (!constrained) {
+	if (!looping && framePlaying > m_lastModelEndFrame) {
+	    framePlaying = m_lastModelEndFrame;
+	    stop();
+	}
+	return framePlaying;
+    }
+
+    MultiSelection::SelectionList selections = m_viewManager->getSelections();
+    MultiSelection::SelectionList::const_iterator i;
+
+    i = selections.begin();
+    size_t rangeStart = i->getStartFrame();
+
+    i = selections.end();
+    --i;
+    size_t rangeEnd = i->getEndFrame();
+
+    for (i = selections.begin(); i != selections.end(); ++i) {
+	if (i->contains(bufferedFrame)) break;
+    }
+
+    size_t f = bufferedFrame;
+
+//    std::cerr << "getCurrentPlayingFrame: f=" << f << ", latency=" << latency << ", rangeEnd=" << rangeEnd << std::endl;
+
+    if (i == selections.end()) {
+	--i;
+	if (i->getEndFrame() + latency < f) {
+//    std::cerr << "framePlaying = " << framePlaying << ", rangeEnd = " << rangeEnd << std::endl;
+
+	    if (!looping && (framePlaying > rangeEnd)) {
+//		std::cerr << "STOPPING" << std::endl;
+		stop();
+		return rangeEnd;
+	    } else {
+		return framePlaying;
+	    }
+	} else {
+//	    std::cerr << "latency <- " << latency << "-(" << f << "-" << i->getEndFrame() << ")" << std::endl;
+	    latency -= (f - i->getEndFrame());
+	    f = i->getEndFrame();
+	}
+    }
+
+//    std::cerr << "i=(" << i->getStartFrame() << "," << i->getEndFrame() << ") f=" << f << ", latency=" << latency << std::endl;
+
+    while (latency > 0) {
+	size_t offset = f - i->getStartFrame();
+	if (offset >= latency) {
+	    if (f > latency) {
+		framePlaying = f - latency;
+	    } else {
+		framePlaying = 0;
+	    }
+	    break;
+	} else {
+	    if (i == selections.begin()) {
+		if (looping) {
+		    i = selections.end();
+		}
+	    }
+	    latency -= offset;
+	    --i;
+	    f = i->getEndFrame();
+	}
+    }
+
+    return framePlaying;
+}
+
+void
+AudioCallbackPlaySource::setOutputLevels(float left, float right)
+{
+    m_outputLeft = left;
+    m_outputRight = right;
+}
+
+bool
+AudioCallbackPlaySource::getOutputLevels(float &left, float &right)
+{
+    left = m_outputLeft;
+    right = m_outputRight;
+    return true;
+}
+
+void
+AudioCallbackPlaySource::setTargetSampleRate(size_t sr)
+{
+    m_targetSampleRate = sr;
+
+    if (getSourceSampleRate() != getTargetSampleRate()) {
+
+	int err = 0;
+	m_converter = src_new(SRC_SINC_BEST_QUALITY,
+			      getTargetChannelCount(), &err);
+	if (!m_converter) {
+	    std::cerr
+		<< "AudioCallbackPlaySource::setModel: ERROR in creating samplerate converter: "
+		<< src_strerror(err) << std::endl;
+
+            emit sampleRateMismatch(getSourceSampleRate(),
+                                    getTargetSampleRate(),
+                                    false);
+	} else {
+
+            emit sampleRateMismatch(getSourceSampleRate(),
+                                    getTargetSampleRate(),
+                                    true);
+        }
+    }
+}
+
+size_t
+AudioCallbackPlaySource::getTargetSampleRate() const
+{
+    if (m_targetSampleRate) return m_targetSampleRate;
+    else return getSourceSampleRate();
+}
+
+size_t
+AudioCallbackPlaySource::getSourceChannelCount() const
+{
+    return m_sourceChannelCount;
+}
+
+size_t
+AudioCallbackPlaySource::getTargetChannelCount() const
+{
+    if (m_sourceChannelCount < 2) return 2;
+    return m_sourceChannelCount;
+}
+
+size_t
+AudioCallbackPlaySource::getSourceSampleRate() const
+{
+    return m_sourceSampleRate;
+}
+
+AudioCallbackPlaySource::TimeStretcherData::TimeStretcherData(size_t channels,
+							      size_t factor,
+							      size_t blockSize) :
+    m_factor(factor),
+    m_blockSize(blockSize)
+{
+//    std::cerr << "TimeStretcherData::TimeStretcherData(" << channels << ", " << factor << ", " << blockSize << ")" << std::endl;
+
+    for (size_t ch = 0; ch < channels; ++ch) {
+	m_stretcher[ch] = StretcherBuffer
+	    //!!! We really need to measure performance and work out
+	    //what sort of quality level to use -- or at least to
+	    //allow the user to configure it
+	    (new IntegerTimeStretcher(factor, blockSize, 128),
+	     new float[blockSize * factor]);
+    }
+    m_stretchInputBuffer = new float[blockSize];
+}
+
+AudioCallbackPlaySource::TimeStretcherData::~TimeStretcherData()
+{
+//    std::cerr << "TimeStretcherData::~TimeStretcherData" << std::endl;
+
+    while (!m_stretcher.empty()) {
+	delete m_stretcher.begin()->second.first;
+	delete[] m_stretcher.begin()->second.second;
+	m_stretcher.erase(m_stretcher.begin());
+    }
+    delete m_stretchInputBuffer;
+}
+
+IntegerTimeStretcher *
+AudioCallbackPlaySource::TimeStretcherData::getStretcher(size_t channel)
+{
+    return m_stretcher[channel].first;
+}
+
+float *
+AudioCallbackPlaySource::TimeStretcherData::getOutputBuffer(size_t channel)
+{
+    return m_stretcher[channel].second;
+}
+
+float *
+AudioCallbackPlaySource::TimeStretcherData::getInputBuffer()
+{
+    return m_stretchInputBuffer;
+}
+
+void
+AudioCallbackPlaySource::TimeStretcherData::run(size_t channel)
+{
+    getStretcher(channel)->process(getInputBuffer(),
+				   getOutputBuffer(channel),
+				   m_blockSize);
+}
+
+void
+AudioCallbackPlaySource::setSlowdownFactor(size_t factor)
+{
+    // Avoid locks -- create, assign, mark old one for scavenging
+    // later (as a call to getSourceSamples may still be using it)
+
+    TimeStretcherData *existingStretcher = m_timeStretcher;
+
+    if (existingStretcher && existingStretcher->getFactor() == factor) {
+	return;
+    }
+
+    if (factor > 1) {
+	TimeStretcherData *newStretcher = new TimeStretcherData
+	    (getTargetChannelCount(), factor, getTargetBlockSize());
+	m_slowdownCounter = 0;
+	m_timeStretcher = newStretcher;
+    } else {
+	m_timeStretcher = 0;
+    }
+
+    if (existingStretcher) {
+	m_timeStretcherScavenger.claim(existingStretcher);
+    }
+}
+	    
+size_t
+AudioCallbackPlaySource::getSourceSamples(size_t count, float **buffer)
+{
+    if (!m_playing) {
+	for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) {
+	    for (size_t i = 0; i < count; ++i) {
+		buffer[ch][i] = 0.0;
+	    }
+	}
+	return 0;
+    }
+
+    TimeStretcherData *timeStretcher = m_timeStretcher;
+
+    if (!timeStretcher || timeStretcher->getFactor() == 1) {
+
+	size_t got = 0;
+
+	for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) {
+
+	    RingBuffer<float> *rb = getReadRingBuffer(ch);
+
+	    if (rb) {
+
+		// this is marginally more likely to leave our channels in
+		// sync after a processing failure than just passing "count":
+		size_t request = count;
+		if (ch > 0) request = got;
+
+		got = rb->read(buffer[ch], request);
+	    
+#ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING
+		std::cout << "AudioCallbackPlaySource::getSamples: got " << got << " samples on channel " << ch << ", signalling for more (possibly)" << std::endl;
+#endif
+	    }
+
+	    for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) {
+		for (size_t i = got; i < count; ++i) {
+		    buffer[ch][i] = 0.0;
+		}
+	    }
+	}
+
+        m_condition.wakeAll();
+	return got;
+    }
+
+    if (m_slowdownCounter == 0) {
+
+	size_t got = 0;
+	float *ib = timeStretcher->getInputBuffer();
+
+	for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) {
+
+	    RingBuffer<float> *rb = getReadRingBuffer(ch);
+
+	    if (rb) {
+
+		size_t request = count;
+		if (ch > 0) request = got; // see above
+		got = rb->read(buffer[ch], request);
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+		std::cout << "AudioCallbackPlaySource::getSamples: got " << got << " samples on channel " << ch << ", running time stretcher" << std::endl;
+#endif
+
+		for (size_t i = 0; i < count; ++i) {
+		    ib[i] = buffer[ch][i];
+		}
+	    
+		timeStretcher->run(ch);
+	    }
+	}
+
+    } else if (m_slowdownCounter >= timeStretcher->getFactor()) {
+	// reset this in case the factor has changed leaving the
+	// counter out of range
+	m_slowdownCounter = 0;
+    }
+
+    for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) {
+
+	float *ob = timeStretcher->getOutputBuffer(ch);
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+	std::cerr << "AudioCallbackPlaySource::getSamples: Copying from (" << (m_slowdownCounter * count) << "," << count << ") to buffer" << std::endl;
+#endif
+
+	for (size_t i = 0; i < count; ++i) {
+	    buffer[ch][i] = ob[m_slowdownCounter * count + i];
+	}
+    }
+
+//!!!    if (m_slowdownCounter == 0) m_condition.wakeAll();
+    m_slowdownCounter = (m_slowdownCounter + 1) % timeStretcher->getFactor();
+    return count;
+}
+
+// Called from fill thread, m_playing true, mutex held
+bool
+AudioCallbackPlaySource::fillBuffers()
+{
+    static float *tmp = 0;
+    static size_t tmpSize = 0;
+
+    size_t space = 0;
+    for (size_t c = 0; c < getTargetChannelCount(); ++c) {
+	RingBuffer<float> *wb = getWriteRingBuffer(c);
+	if (wb) {
+	    size_t spaceHere = wb->getWriteSpace();
+	    if (c == 0 || spaceHere < space) space = spaceHere;
+	}
+    }
+    
+    if (space == 0) return false;
+
+    size_t f = m_writeBufferFill;
+	
+    bool readWriteEqual = (m_readBuffers == m_writeBuffers);
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+    std::cout << "AudioCallbackPlaySourceFillThread: filling " << space << " frames" << std::endl;
+#endif
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+    std::cout << "buffered to " << f << " already" << std::endl;
+#endif
+
+    bool resample = (getSourceSampleRate() != getTargetSampleRate());
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+    std::cout << (resample ? "" : "not ") << "resampling (source " << getSourceSampleRate() << ", target " << getTargetSampleRate() << ")" << std::endl;
+#endif
+
+    size_t channels = getTargetChannelCount();
+
+    size_t orig = space;
+    size_t got = 0;
+
+    static float **bufferPtrs = 0;
+    static size_t bufferPtrCount = 0;
+
+    if (bufferPtrCount < channels) {
+	if (bufferPtrs) delete[] bufferPtrs;
+	bufferPtrs = new float *[channels];
+	bufferPtrCount = channels;
+    }
+
+    size_t generatorBlockSize = m_audioGenerator->getBlockSize();
+
+    if (resample && !m_converter) {
+	static bool warned = false;
+	if (!warned) {
+	    std::cerr << "WARNING: sample rates differ, but no converter available!" << std::endl;
+	    warned = true;
+	}
+    }
+
+    if (resample && m_converter) {
+
+	double ratio =
+	    double(getTargetSampleRate()) / double(getSourceSampleRate());
+	orig = size_t(orig / ratio + 0.1);
+
+	// orig must be a multiple of generatorBlockSize
+	orig = (orig / generatorBlockSize) * generatorBlockSize;
+	if (orig == 0) return false;
+
+	size_t work = std::max(orig, space);
+
+	// We only allocate one buffer, but we use it in two halves.
+	// We place the non-interleaved values in the second half of
+	// the buffer (orig samples for channel 0, orig samples for
+	// channel 1 etc), and then interleave them into the first
+	// half of the buffer.  Then we resample back into the second
+	// half (interleaved) and de-interleave the results back to
+	// the start of the buffer for insertion into the ringbuffers.
+	// What a faff -- especially as we've already de-interleaved
+	// the audio data from the source file elsewhere before we
+	// even reach this point.
+	
+	if (tmpSize < channels * work * 2) {
+	    delete[] tmp;
+	    tmp = new float[channels * work * 2];
+	    tmpSize = channels * work * 2;
+	}
+
+	float *nonintlv = tmp + channels * work;
+	float *intlv = tmp;
+	float *srcout = tmp + channels * work;
+	
+	for (size_t c = 0; c < channels; ++c) {
+	    for (size_t i = 0; i < orig; ++i) {
+		nonintlv[channels * i + c] = 0.0f;
+	    }
+	}
+
+	for (size_t c = 0; c < channels; ++c) {
+	    bufferPtrs[c] = nonintlv + c * orig;
+	}
+
+	got = mixModels(f, orig, bufferPtrs);
+
+	// and interleave into first half
+	for (size_t c = 0; c < channels; ++c) {
+	    for (size_t i = 0; i < got; ++i) {
+		float sample = nonintlv[c * got + i];
+		intlv[channels * i + c] = sample;
+	    }
+	}
+		
+	SRC_DATA data;
+	data.data_in = intlv;
+	data.data_out = srcout;
+	data.input_frames = got;
+	data.output_frames = work;
+	data.src_ratio = ratio;
+	data.end_of_input = 0;
+	
+	int err = src_process(m_converter, &data);
+//	size_t toCopy = size_t(work * ratio + 0.1);
+	size_t toCopy = size_t(got * ratio + 0.1);
+
+	if (err) {
+	    std::cerr
+		<< "AudioCallbackPlaySourceFillThread: ERROR in samplerate conversion: "
+		<< src_strerror(err) << std::endl;
+	    //!!! Then what?
+	} else {
+	    got = data.input_frames_used;
+	    toCopy = data.output_frames_gen;
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+	    std::cerr << "Resampled " << got << " frames to " << toCopy << " frames" << std::endl;
+#endif
+	}
+	
+	for (size_t c = 0; c < channels; ++c) {
+	    for (size_t i = 0; i < toCopy; ++i) {
+		tmp[i] = srcout[channels * i + c];
+	    }
+	    RingBuffer<float> *wb = getWriteRingBuffer(c);
+	    if (wb) wb->write(tmp, toCopy);
+	}
+
+	m_writeBufferFill = f;
+	if (readWriteEqual) m_readBufferFill = f;
+
+    } else {
+
+	// space must be a multiple of generatorBlockSize
+	space = (space / generatorBlockSize) * generatorBlockSize;
+	if (space == 0) return false;
+
+	if (tmpSize < channels * space) {
+	    delete[] tmp;
+	    tmp = new float[channels * space];
+	    tmpSize = channels * space;
+	}
+
+	for (size_t c = 0; c < channels; ++c) {
+
+	    bufferPtrs[c] = tmp + c * space;
+	    
+	    for (size_t i = 0; i < space; ++i) {
+		tmp[c * space + i] = 0.0f;
+	    }
+	}
+
+	size_t got = mixModels(f, space, bufferPtrs);
+
+	for (size_t c = 0; c < channels; ++c) {
+
+	    RingBuffer<float> *wb = getWriteRingBuffer(c);
+	    if (wb) wb->write(bufferPtrs[c], got);
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+	    if (wb)
+		std::cerr << "Wrote " << got << " frames for ch " << c << ", now "
+			  << wb->getReadSpace() << " to read" 
+			  << std::endl;
+#endif
+	}
+
+	m_writeBufferFill = f;
+	if (readWriteEqual) m_readBufferFill = f;
+
+	//!!! how do we know when ended? need to mark up a fully-buffered flag and check this if we find the buffers empty in getSourceSamples
+    }
+
+    return true;
+}    
+
+size_t
+AudioCallbackPlaySource::mixModels(size_t &frame, size_t count, float **buffers)
+{
+    size_t processed = 0;
+    size_t chunkStart = frame;
+    size_t chunkSize = count;
+    size_t selectionSize = 0;
+    size_t nextChunkStart = chunkStart + chunkSize;
+    
+    bool looping = m_viewManager->getPlayLoopMode();
+    bool constrained = (m_viewManager->getPlaySelectionMode() &&
+			!m_viewManager->getSelections().empty());
+
+    static float **chunkBufferPtrs = 0;
+    static size_t chunkBufferPtrCount = 0;
+    size_t channels = getTargetChannelCount();
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+    std::cerr << "Selection playback: start " << frame << ", size " << count <<", channels " << channels << std::endl;
+#endif
+
+    if (chunkBufferPtrCount < channels) {
+	if (chunkBufferPtrs) delete[] chunkBufferPtrs;
+	chunkBufferPtrs = new float *[channels];
+	chunkBufferPtrCount = channels;
+    }
+
+    for (size_t c = 0; c < channels; ++c) {
+	chunkBufferPtrs[c] = buffers[c];
+    }
+
+    while (processed < count) {
+	
+	chunkSize = count - processed;
+	nextChunkStart = chunkStart + chunkSize;
+	selectionSize = 0;
+
+	size_t fadeIn = 0, fadeOut = 0;
+
+	if (constrained) {
+	    
+	    Selection selection =
+		m_viewManager->getContainingSelection(chunkStart, true);
+	    
+	    if (selection.isEmpty()) {
+		if (looping) {
+		    selection = *m_viewManager->getSelections().begin();
+		    chunkStart = selection.getStartFrame();
+		    fadeIn = 50;
+		}
+	    }
+
+	    if (selection.isEmpty()) {
+
+		chunkSize = 0;
+		nextChunkStart = chunkStart;
+
+	    } else {
+
+		selectionSize =
+		    selection.getEndFrame() -
+		    selection.getStartFrame();
+
+		if (chunkStart < selection.getStartFrame()) {
+		    chunkStart = selection.getStartFrame();
+		    fadeIn = 50;
+		}
+
+		nextChunkStart = chunkStart + chunkSize;
+
+		if (nextChunkStart >= selection.getEndFrame()) {
+		    nextChunkStart = selection.getEndFrame();
+		    fadeOut = 50;
+		}
+
+		chunkSize = nextChunkStart - chunkStart;
+	    }
+	
+	} else if (looping && m_lastModelEndFrame > 0) {
+
+	    if (chunkStart >= m_lastModelEndFrame) {
+		chunkStart = 0;
+	    }
+	    if (chunkSize > m_lastModelEndFrame - chunkStart) {
+		chunkSize = m_lastModelEndFrame - chunkStart;
+	    }
+	    nextChunkStart = chunkStart + chunkSize;
+	}
+	
+//	std::cerr << "chunkStart " << chunkStart << ", chunkSize " << chunkSize << ", nextChunkStart " << nextChunkStart << ", frame " << frame << ", count " << count << ", processed " << processed << std::endl;
+
+	if (!chunkSize) {
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+	    std::cerr << "Ending selection playback at " << nextChunkStart << std::endl;
+#endif
+	    // We need to maintain full buffers so that the other
+	    // thread can tell where it's got to in the playback -- so
+	    // return the full amount here
+	    frame = frame + count;
+	    return count;
+	}
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+	std::cerr << "Selection playback: chunk at " << chunkStart << " -> " << nextChunkStart << " (size " << chunkSize << ")" << std::endl;
+#endif
+
+	size_t got = 0;
+
+	if (selectionSize < 100) {
+	    fadeIn = 0;
+	    fadeOut = 0;
+	} else if (selectionSize < 300) {
+	    if (fadeIn > 0) fadeIn = 10;
+	    if (fadeOut > 0) fadeOut = 10;
+	}
+
+	if (fadeIn > 0) {
+	    if (processed * 2 < fadeIn) {
+		fadeIn = processed * 2;
+	    }
+	}
+
+	if (fadeOut > 0) {
+	    if ((count - processed - chunkSize) * 2 < fadeOut) {
+		fadeOut = (count - processed - chunkSize) * 2;
+	    }
+	}
+
+	for (std::set<Model *>::iterator mi = m_models.begin();
+	     mi != m_models.end(); ++mi) {
+	    
+	    got = m_audioGenerator->mixModel(*mi, chunkStart, 
+					     chunkSize, chunkBufferPtrs,
+					     fadeIn, fadeOut);
+	}
+
+	for (size_t c = 0; c < channels; ++c) {
+	    chunkBufferPtrs[c] += chunkSize;
+	}
+
+	processed += chunkSize;
+	chunkStart = nextChunkStart;
+    }
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+    std::cerr << "Returning selection playback " << processed << " frames to " << nextChunkStart << std::endl;
+#endif
+
+    frame = nextChunkStart;
+    return processed;
+}
+
+void
+AudioCallbackPlaySource::unifyRingBuffers()
+{
+    if (m_readBuffers == m_writeBuffers) return;
+
+    // only unify if there will be something to read
+    for (size_t c = 0; c < getTargetChannelCount(); ++c) {
+	RingBuffer<float> *wb = getWriteRingBuffer(c);
+	if (wb) {
+	    if (wb->getReadSpace() < m_blockSize * 2) {
+		if ((m_writeBufferFill + m_blockSize * 2) < 
+		    m_lastModelEndFrame) {
+		    // OK, we don't have enough and there's more to
+		    // read -- don't unify until we can do better
+		    return;
+		}
+	    }
+	    break;
+	}
+    }
+
+    size_t rf = m_readBufferFill;
+    RingBuffer<float> *rb = getReadRingBuffer(0);
+    if (rb) {
+	size_t rs = rb->getReadSpace();
+	//!!! incorrect when in non-contiguous selection, see comments elsewhere
+//	std::cerr << "rs = " << rs << std::endl;
+	if (rs < rf) rf -= rs;
+	else rf = 0;
+    }
+    
+    //std::cerr << "m_readBufferFill = " << m_readBufferFill << ", rf = " << rf << ", m_writeBufferFill = " << m_writeBufferFill << std::endl;
+
+    size_t wf = m_writeBufferFill;
+    size_t skip = 0;
+    for (size_t c = 0; c < getTargetChannelCount(); ++c) {
+	RingBuffer<float> *wb = getWriteRingBuffer(c);
+	if (wb) {
+	    if (c == 0) {
+		
+		size_t wrs = wb->getReadSpace();
+//		std::cerr << "wrs = " << wrs << std::endl;
+
+		if (wrs < wf) wf -= wrs;
+		else wf = 0;
+//		std::cerr << "wf = " << wf << std::endl;
+		
+		if (wf < rf) skip = rf - wf;
+		if (skip == 0) break;
+	    }
+
+//	    std::cerr << "skipping " << skip << std::endl;
+	    wb->skip(skip);
+	}
+    }
+		    
+    m_bufferScavenger.claim(m_readBuffers);
+    m_readBuffers = m_writeBuffers;
+    m_readBufferFill = m_writeBufferFill;
+//    std::cerr << "unified" << std::endl;
+}
+
+void
+AudioCallbackPlaySource::AudioCallbackPlaySourceFillThread::run()
+{
+    AudioCallbackPlaySource &s(m_source);
+    
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+    std::cerr << "AudioCallbackPlaySourceFillThread starting" << std::endl;
+#endif
+
+    s.m_mutex.lock();
+
+    bool previouslyPlaying = s.m_playing;
+    bool work = false;
+
+    while (!s.m_exiting) {
+
+	s.unifyRingBuffers();
+	s.m_bufferScavenger.scavenge();
+	s.m_timeStretcherScavenger.scavenge();
+
+	if (work && s.m_playing && s.getSourceSampleRate()) {
+	    
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+	    std::cout << "AudioCallbackPlaySourceFillThread: not waiting" << std::endl;
+#endif
+
+	    s.m_mutex.unlock();
+	    s.m_mutex.lock();
+
+	} else {
+	    
+	    float ms = 100;
+	    if (s.getSourceSampleRate() > 0) {
+		ms = float(m_ringBufferSize) / float(s.getSourceSampleRate()) * 1000.0;
+	    }
+	    
+	    if (s.m_playing) ms /= 10;
+	    
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+	    std::cout << "AudioCallbackPlaySourceFillThread: waiting for " << ms << "ms..." << std::endl;
+#endif
+	    
+	    s.m_condition.wait(&s.m_mutex, size_t(ms));
+	}
+
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+	std::cout << "AudioCallbackPlaySourceFillThread: awoken" << std::endl;
+#endif
+
+	work = false;
+
+	if (!s.getSourceSampleRate()) continue;
+
+	bool playing = s.m_playing;
+
+	if (playing && !previouslyPlaying) {
+#ifdef DEBUG_AUDIO_PLAY_SOURCE
+	    std::cout << "AudioCallbackPlaySourceFillThread: playback state changed, resetting" << std::endl;
+#endif
+	    for (size_t c = 0; c < s.getTargetChannelCount(); ++c) {
+		RingBuffer<float> *rb = s.getReadRingBuffer(c);
+		if (rb) rb->reset();
+	    }
+	}
+	previouslyPlaying = playing;
+
+	work = s.fillBuffers();
+    }
+
+    s.m_mutex.unlock();
+}
+
+
+
+#ifdef INCLUDE_MOCFILES
+#include "AudioCallbackPlaySource.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioCallbackPlaySource.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,306 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _AUDIO_CALLBACK_PLAY_SOURCE_H_
+#define _AUDIO_CALLBACK_PLAY_SOURCE_H_
+
+#include "base/RingBuffer.h"
+#include "base/AudioPlaySource.h"
+#include "base/Scavenger.h"
+
+#include <QObject>
+#include <QMutex>
+#include <QWaitCondition>
+
+#include "base/Thread.h"
+
+#include <samplerate.h>
+
+#include <set>
+#include <map>
+
+class Model;
+class ViewManager;
+class AudioGenerator;
+class PlayParameters;
+class IntegerTimeStretcher;
+
+/**
+ * AudioCallbackPlaySource manages audio data supply to callback-based
+ * audio APIs such as JACK or CoreAudio.  It maintains one ring buffer
+ * per channel, filled during playback by a non-realtime thread, and
+ * provides a method for a realtime thread to pick up the latest
+ * available sample data from these buffers.
+ */
+class AudioCallbackPlaySource : public virtual QObject,
+				public AudioPlaySource
+{
+    Q_OBJECT
+
+public:
+    AudioCallbackPlaySource(ViewManager *);
+    virtual ~AudioCallbackPlaySource();
+    
+    /**
+     * Add a data model to be played from.  The source can mix
+     * playback from a number of sources including dense and sparse
+     * models.  The models must match in sample rate, but they don't
+     * have to have identical numbers of channels.
+     */
+    virtual void addModel(Model *model);
+
+    /**
+     * Remove a model.
+     */
+    virtual void removeModel(Model *model);
+
+    /**
+     * Remove all models.  (Silence will ensue.)
+     */
+    virtual void clearModels();
+
+    /**
+     * Start making data available in the ring buffers for playback,
+     * from the given frame.  If playback is already under way, reseek
+     * to the given frame and continue.
+     */
+    virtual void play(size_t startFrame);
+
+    /**
+     * Stop playback and ensure that no more data is returned.
+     */
+    virtual void stop();
+
+    /**
+     * Return whether playback is currently supposed to be happening.
+     */
+    virtual bool isPlaying() const { return m_playing; }
+
+    /**
+     * Return the frame number that is currently expected to be coming
+     * out of the speakers.  (i.e. compensating for playback latency.)
+     */
+    virtual size_t getCurrentPlayingFrame();
+
+    /**
+     * Set the block size of the target audio device.  This should
+     * be called by the target class.
+     */
+    void setTargetBlockSize(size_t);
+
+    /**
+     * Get the block size of the target audio device.
+     */
+    size_t getTargetBlockSize() const;
+
+    /**
+     * Set the playback latency of the target audio device, in frames
+     * at the target sample rate.  This is the difference between the
+     * frame currently "leaving the speakers" and the last frame (or
+     * highest last frame across all channels) requested via
+     * getSamples().  The default is zero.
+     */
+    void setTargetPlayLatency(size_t);
+
+    /**
+     * Get the playback latency of the target audio device.
+     */
+    size_t getTargetPlayLatency() const;
+
+    /**
+     * Specify that the target audio device has a fixed sample rate
+     * (i.e. cannot accommodate arbitrary sample rates based on the
+     * source).  If the target sets this to something other than the
+     * source sample rate, this class will resample automatically to
+     * fit.
+     */
+    void setTargetSampleRate(size_t);
+
+    /**
+     * Return the sample rate set by the target audio device (or the
+     * source sample rate if the target hasn't set one).
+     */
+    virtual size_t getTargetSampleRate() const;
+
+    /**
+     * Set the current output levels for metering (for call from the
+     * target)
+     */
+    void setOutputLevels(float left, float right);
+
+    /**
+     * Return the current (or thereabouts) output levels in the range
+     * 0.0 -> 1.0, for metering purposes.
+     */
+    virtual bool getOutputLevels(float &left, float &right);
+
+    /**
+     * Get the number of channels of audio that in the source models.
+     * This may safely be called from a realtime thread.  Returns 0 if
+     * there is no source yet available.
+     */
+    size_t getSourceChannelCount() const;
+
+    /**
+     * Get the number of channels of audio that will be provided
+     * to the play target.  This may be more than the source channel
+     * count: for example, a mono source will provide 2 channels
+     * after pan.
+     * This may safely be called from a realtime thread.  Returns 0 if
+     * there is no source yet available.
+     */
+    size_t getTargetChannelCount() const;
+
+    /**
+     * Get the actual sample rate of the source material.  This may
+     * safely be called from a realtime thread.  Returns 0 if there is
+     * no source yet available.
+     */
+    size_t getSourceSampleRate() const;
+
+    /**
+     * Get "count" samples (at the target sample rate) of the mixed
+     * audio data, in all channels.  This may safely be called from a
+     * realtime thread.
+     */
+    size_t getSourceSamples(size_t count, float **buffer);
+
+    void setSlowdownFactor(size_t factor);
+
+signals:
+    void modelReplaced();
+
+    void playStatusChanged(bool isPlaying);
+
+    void sampleRateMismatch(size_t requested, size_t available, bool willResample);
+
+protected slots:
+    void selectionChanged();
+    void playLoopModeChanged();
+    void playSelectionModeChanged();
+    void playParametersChanged(PlayParameters *);
+
+protected:
+    ViewManager                     *m_viewManager;
+    AudioGenerator                  *m_audioGenerator;
+
+    class RingBufferVector : public std::vector<RingBuffer<float> *> {
+    public:
+	virtual ~RingBufferVector() {
+	    while (!empty()) {
+		delete *begin();
+		erase(begin());
+	    }
+	}
+    };
+
+    std::set<Model *>                m_models;
+    RingBufferVector                *m_readBuffers;
+    RingBufferVector                *m_writeBuffers;
+    size_t                           m_readBufferFill;
+    size_t                           m_writeBufferFill;
+    Scavenger<RingBufferVector>      m_bufferScavenger;
+    size_t                           m_sourceChannelCount;
+    size_t                           m_blockSize;
+    size_t                           m_sourceSampleRate;
+    size_t                           m_targetSampleRate;
+    size_t                           m_playLatency;
+    bool                             m_playing;
+    bool                             m_exiting;
+    size_t                           m_lastModelEndFrame;
+    static const size_t              m_ringBufferSize;
+    float                            m_outputLeft;
+    float                            m_outputRight;
+
+    RingBuffer<float> *getWriteRingBuffer(size_t c) {
+	if (m_writeBuffers && c < m_writeBuffers->size()) {
+	    return (*m_writeBuffers)[c];
+	} else {
+	    return 0;
+	}
+    }
+
+    RingBuffer<float> *getReadRingBuffer(size_t c) {
+	RingBufferVector *rb = m_readBuffers;
+	if (rb && c < rb->size()) {
+	    return (*rb)[c];
+	} else {
+	    return 0;
+	}
+    }
+
+    void clearRingBuffers(bool haveLock = false, size_t count = 0);
+    void unifyRingBuffers();
+
+    class TimeStretcherData
+    {
+    public:
+	TimeStretcherData(size_t channels, size_t factor, size_t blockSize);
+	~TimeStretcherData();
+
+	size_t getFactor() const { return m_factor; }
+	IntegerTimeStretcher *getStretcher(size_t channel);
+	float *getOutputBuffer(size_t channel);
+	float *getInputBuffer();
+	
+	void run(size_t channel);
+
+    protected:
+	TimeStretcherData(const TimeStretcherData &); // not provided
+	TimeStretcherData &operator=(const TimeStretcherData &); // not provided
+
+	typedef std::pair<IntegerTimeStretcher *, float *> StretcherBuffer;
+	std::map<size_t, StretcherBuffer> m_stretcher;
+	float *m_stretchInputBuffer;
+	size_t m_factor;
+	size_t m_blockSize;
+    };
+
+    size_t m_slowdownCounter;
+    TimeStretcherData *m_timeStretcher;
+    Scavenger<TimeStretcherData> m_timeStretcherScavenger;
+
+    // Called from fill thread, m_playing true, mutex held
+    // Return true if work done
+    bool fillBuffers();
+    
+    // Called from fillBuffers.  Return the number of frames written,
+    // which will be count or fewer.  Return in the frame argument the
+    // new buffered frame position (which may be earlier than the
+    // frame argument passed in, in the case of looping).
+    size_t mixModels(size_t &frame, size_t count, float **buffers);
+
+    class AudioCallbackPlaySourceFillThread : public Thread
+    {
+    public:
+	AudioCallbackPlaySourceFillThread(AudioCallbackPlaySource &source) :
+            Thread(Thread::NonRTThread),
+	    m_source(source) { }
+
+	virtual void run();
+
+    protected:
+	AudioCallbackPlaySource &m_source;
+    };
+
+    QMutex m_mutex;
+    QWaitCondition m_condition;
+    AudioCallbackPlaySourceFillThread *m_fillThread;
+    SRC_STATE *m_converter;
+};
+
+#endif
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioCallbackPlayTarget.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,46 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "AudioCallbackPlayTarget.h"
+#include "AudioCallbackPlaySource.h"
+
+#include <iostream>
+
+AudioCallbackPlayTarget::AudioCallbackPlayTarget(AudioCallbackPlaySource *source) :
+    m_source(source),
+    m_outputGain(1.0)
+{
+    if (m_source) {
+	connect(m_source, SIGNAL(modelReplaced()),
+		this, SLOT(sourceModelReplaced()));
+    }
+}
+
+AudioCallbackPlayTarget::~AudioCallbackPlayTarget()
+{
+}
+
+void
+AudioCallbackPlayTarget::setOutputGain(float gain)
+{
+    m_outputGain = gain;
+}
+
+#ifdef INCLUDE_MOCFILES
+#ifdef INCLUDE_MOCFILES
+#include "AudioCallbackPlayTarget.moc.cpp"
+#endif
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioCallbackPlayTarget.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,59 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _AUDIO_CALLBACK_PLAY_TARGET_H_
+#define _AUDIO_CALLBACK_PLAY_TARGET_H_
+
+#include <QObject>
+
+class AudioCallbackPlaySource;
+
+class AudioCallbackPlayTarget : public QObject
+{
+    Q_OBJECT
+
+public:
+    AudioCallbackPlayTarget(AudioCallbackPlaySource *source);
+    virtual ~AudioCallbackPlayTarget();
+
+    virtual bool isOK() const = 0;
+
+    float getOutputGain() const {
+	return m_outputGain;
+    }
+
+public slots:
+    /**
+     * Set the playback gain (0.0 = silence, 1.0 = levels unmodified)
+     */
+    virtual void setOutputGain(float gain);
+
+    /**
+     * The main source model (providing the playback sample rate) has
+     * been changed.  The target should query the source's sample
+     * rate, set its output sample rate accordingly, and call back on
+     * the source's setTargetSampleRate to indicate what sample rate
+     * it succeeded in setting at the output.  If this differs from
+     * the model rate, the source will resample.
+     */
+    virtual void sourceModelReplaced() = 0;
+
+protected:
+    AudioCallbackPlaySource *m_source;
+    float m_outputGain;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioCoreAudioTarget.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,22 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifdef HAVE_COREAUDIO
+
+#include "AudioCoreAudioTarget.h"
+
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioCoreAudioTarget.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,64 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _AUDIO_CORE_AUDIO_TARGET_H_
+#define _AUDIO_CORE_AUDIO_TARGET_H_
+
+#ifdef HAVE_COREAUDIO
+
+#include <jack/jack.h>
+#include <vector>
+
+#include <CoreAudio/CoreAudio.h>
+#include <CoreAudio/CoreAudioTypes.h>
+#include <AudioUnit/AUComponent.h>
+#include <AudioUnit/AudioUnitProperties.h>
+#include <AudioUnit/AudioUnitParameters.h>
+#include <AudioUnit/AudioOutputUnit.h>
+
+#include "AudioCallbackPlayTarget.h"
+
+class AudioCallbackPlaySource;
+
+class AudioCoreAudioTarget : public AudioCallbackPlayTarget
+{
+    Q_OBJECT
+
+public:
+    AudioCoreAudioTarget(AudioCallbackPlaySource *source);
+    ~AudioCoreAudioTarget();
+
+    virtual bool isOK() const;
+
+public slots:
+    virtual void sourceModelReplaced();
+
+protected:
+    OSStatus process(void *data,
+		     AudioUnitRenderActionFlags *flags,
+		     const AudioTimeStamp *timestamp,
+		     unsigned int inbus,
+		     unsigned int inframes,
+		     AudioBufferList *ioData);
+
+    int m_bufferSize;
+    int m_sampleRate;
+    int m_latency;
+};
+
+#endif /* HAVE_COREAUDIO */
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioGenerator.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,764 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "AudioGenerator.h"
+
+#include "base/TempDirectory.h"
+#include "base/PlayParameters.h"
+#include "base/PlayParameterRepository.h"
+#include "base/Pitch.h"
+#include "base/Exceptions.h"
+
+#include "model/NoteModel.h"
+#include "model/DenseTimeValueModel.h"
+#include "model/SparseOneDimensionalModel.h"
+
+#include "plugin/RealTimePluginFactory.h"
+#include "plugin/RealTimePluginInstance.h"
+#include "plugin/PluginIdentifier.h"
+#include "plugin/PluginXml.h"
+#include "plugin/api/alsa/seq_event.h"
+
+#include <iostream>
+#include <math.h>
+
+#include <QDir>
+#include <QFile>
+
+const size_t
+AudioGenerator::m_pluginBlockSize = 2048;
+
+QString
+AudioGenerator::m_sampleDir = "";
+
+//#define DEBUG_AUDIO_GENERATOR 1
+
+AudioGenerator::AudioGenerator() :
+    m_sourceSampleRate(0),
+    m_targetChannelCount(1)
+{
+    connect(PlayParameterRepository::getInstance(),
+            SIGNAL(playPluginIdChanged(const Model *, QString)),
+            this,
+            SLOT(playPluginIdChanged(const Model *, QString)));
+
+    connect(PlayParameterRepository::getInstance(),
+            SIGNAL(playPluginConfigurationChanged(const Model *, QString)),
+            this,
+            SLOT(playPluginConfigurationChanged(const Model *, QString)));
+}
+
+AudioGenerator::~AudioGenerator()
+{
+}
+
+bool
+AudioGenerator::canPlay(const Model *model)
+{
+    if (dynamic_cast<const DenseTimeValueModel *>(model) ||
+	dynamic_cast<const SparseOneDimensionalModel *>(model) ||
+	dynamic_cast<const NoteModel *>(model)) {
+	return true;
+    } else {
+	return false;
+    }
+}
+
+bool
+AudioGenerator::addModel(Model *model)
+{
+    if (m_sourceSampleRate == 0) {
+
+	m_sourceSampleRate = model->getSampleRate();
+
+    } else {
+
+	DenseTimeValueModel *dtvm =
+	    dynamic_cast<DenseTimeValueModel *>(model);
+
+	if (dtvm) {
+	    m_sourceSampleRate = model->getSampleRate();
+	    return true;
+	}
+    }
+
+    RealTimePluginInstance *plugin = loadPluginFor(model);
+    if (plugin) {
+        QMutexLocker locker(&m_mutex);
+        m_synthMap[model] = plugin;
+        return true;
+    }
+
+    return false;
+}
+
+void
+AudioGenerator::playPluginIdChanged(const Model *model, QString)
+{
+    if (m_synthMap.find(model) == m_synthMap.end()) return;
+    
+    RealTimePluginInstance *plugin = loadPluginFor(model);
+    if (plugin) {
+        QMutexLocker locker(&m_mutex);
+        delete m_synthMap[model];
+        m_synthMap[model] = plugin;
+    }
+}
+
+void
+AudioGenerator::playPluginConfigurationChanged(const Model *model,
+                                               QString configurationXml)
+{
+//    std::cerr << "AudioGenerator::playPluginConfigurationChanged" << std::endl;
+
+    if (m_synthMap.find(model) == m_synthMap.end()) {
+        std::cerr << "AudioGenerator::playPluginConfigurationChanged: We don't know about this plugin" << std::endl;
+        return;
+    }
+
+    RealTimePluginInstance *plugin = m_synthMap[model];
+    if (plugin) {
+        PluginXml(plugin).setParametersFromXml(configurationXml);
+    }
+}
+
+QString
+AudioGenerator::getDefaultPlayPluginId(const Model *model)
+{
+    const SparseOneDimensionalModel *sodm =
+        dynamic_cast<const SparseOneDimensionalModel *>(model);
+    if (sodm) {
+        return QString("dssi:%1:sample_player").
+            arg(PluginIdentifier::BUILTIN_PLUGIN_SONAME);
+    }
+
+    const NoteModel *nm = dynamic_cast<const NoteModel *>(model);
+    if (nm) {
+        return QString("dssi:%1:sample_player").
+            arg(PluginIdentifier::BUILTIN_PLUGIN_SONAME);
+    }  
+    
+    return "";
+}
+
+QString
+AudioGenerator::getDefaultPlayPluginConfiguration(const Model *model)
+{
+    QString program = "";
+
+    const SparseOneDimensionalModel *sodm =
+        dynamic_cast<const SparseOneDimensionalModel *>(model);
+    if (sodm) {
+        program = "tap";
+    }
+
+    const NoteModel *nm = dynamic_cast<const NoteModel *>(model);
+    if (nm) {
+        program = "piano";
+    }
+
+    if (program == "") return "";
+
+    return
+        QString("<plugin configuration=\"%1\" program=\"%2\"/>")
+        .arg(XmlExportable::encodeEntities
+             (QString("sampledir=%1")
+              .arg(PluginXml::encodeConfigurationChars(getSampleDir()))))
+        .arg(XmlExportable::encodeEntities(program));
+}    
+
+QString
+AudioGenerator::getSampleDir()
+{
+    if (m_sampleDir != "") return m_sampleDir;
+
+    try {
+        m_sampleDir = TempDirectory::getInstance()->getSubDirectoryPath("samples");
+    } catch (DirectoryCreationFailed f) {
+        std::cerr << "WARNING: AudioGenerator::getSampleDir: Failed to create "
+                  << "temporary sample directory" << std::endl;
+        m_sampleDir = "";
+        return "";
+    }
+
+    QDir sampleResourceDir(":/samples", "*.wav");
+
+    for (unsigned int i = 0; i < sampleResourceDir.count(); ++i) {
+
+        QString fileName(sampleResourceDir[i]);
+        QFile file(sampleResourceDir.filePath(fileName));
+
+        if (!file.copy(QDir(m_sampleDir).filePath(fileName))) {
+            std::cerr << "WARNING: AudioGenerator::getSampleDir: "
+                      << "Unable to copy " << fileName.toStdString()
+                      << " into temporary directory \""
+                      << m_sampleDir.toStdString() << "\"" << std::endl;
+        }
+    }
+
+    return m_sampleDir;
+}
+
+void
+AudioGenerator::setSampleDir(RealTimePluginInstance *plugin)
+{
+    plugin->configure("sampledir", getSampleDir().toStdString());
+} 
+
+RealTimePluginInstance *
+AudioGenerator::loadPluginFor(const Model *model)
+{
+    QString pluginId, configurationXml;
+
+    PlayParameters *parameters =
+	PlayParameterRepository::getInstance()->getPlayParameters(model);
+    if (parameters) {
+        pluginId = parameters->getPlayPluginId();
+        configurationXml = parameters->getPlayPluginConfiguration();
+    }
+
+    if (pluginId == "") {
+        pluginId = getDefaultPlayPluginId(model);
+        configurationXml = getDefaultPlayPluginConfiguration(model);
+    }
+
+    if (pluginId == "") return 0;
+
+    RealTimePluginInstance *plugin = loadPlugin(pluginId, "");
+    if (!plugin) return 0;
+
+    if (configurationXml != "") {
+        PluginXml(plugin).setParametersFromXml(configurationXml);
+    }
+
+    if (parameters) {
+        parameters->setPlayPluginId(pluginId);
+        parameters->setPlayPluginConfiguration(configurationXml);
+    }
+
+    return plugin;
+}
+
+RealTimePluginInstance *
+AudioGenerator::loadPlugin(QString pluginId, QString program)
+{
+    RealTimePluginFactory *factory =
+	RealTimePluginFactory::instanceFor(pluginId);
+    
+    if (!factory) {
+	std::cerr << "Failed to get plugin factory" << std::endl;
+	return false;
+    }
+	
+    RealTimePluginInstance *instance =
+	factory->instantiatePlugin
+	(pluginId, 0, 0, m_sourceSampleRate, m_pluginBlockSize, m_targetChannelCount);
+
+    if (!instance) {
+	std::cerr << "Failed to instantiate plugin " << pluginId.toStdString() << std::endl;
+        return 0;
+    }
+
+    setSampleDir(instance);
+
+    for (unsigned int i = 0; i < instance->getParameterCount(); ++i) {
+        instance->setParameterValue(i, instance->getParameterDefault(i));
+    }
+    std::string defaultProgram = instance->getProgram(0, 0);
+    if (defaultProgram != "") {
+//        std::cerr << "first selecting default program " << defaultProgram << std::endl;
+        instance->selectProgram(defaultProgram);
+    }
+    if (program != "") {
+//        std::cerr << "now selecting desired program " << program.toStdString() << std::endl;
+        instance->selectProgram(program.toStdString());
+    }
+    instance->setIdealChannelCount(m_targetChannelCount); // reset!
+
+    return instance;
+}
+
+void
+AudioGenerator::removeModel(Model *model)
+{
+    SparseOneDimensionalModel *sodm =
+	dynamic_cast<SparseOneDimensionalModel *>(model);
+    if (!sodm) return; // nothing to do
+
+    QMutexLocker locker(&m_mutex);
+
+    if (m_synthMap.find(sodm) == m_synthMap.end()) return;
+
+    RealTimePluginInstance *instance = m_synthMap[sodm];
+    m_synthMap.erase(sodm);
+    delete instance;
+}
+
+void
+AudioGenerator::clearModels()
+{
+    QMutexLocker locker(&m_mutex);
+    while (!m_synthMap.empty()) {
+	RealTimePluginInstance *instance = m_synthMap.begin()->second;
+	m_synthMap.erase(m_synthMap.begin());
+	delete instance;
+    }
+}    
+
+void
+AudioGenerator::reset()
+{
+    QMutexLocker locker(&m_mutex);
+    for (PluginMap::iterator i = m_synthMap.begin(); i != m_synthMap.end(); ++i) {
+	if (i->second) {
+	    i->second->silence();
+	    i->second->discardEvents();
+	}
+    }
+
+    m_noteOffs.clear();
+}
+
+void
+AudioGenerator::setTargetChannelCount(size_t targetChannelCount)
+{
+    if (m_targetChannelCount == targetChannelCount) return;
+
+//    std::cerr << "AudioGenerator::setTargetChannelCount(" << targetChannelCount << ")" << std::endl;
+
+    QMutexLocker locker(&m_mutex);
+    m_targetChannelCount = targetChannelCount;
+
+    for (PluginMap::iterator i = m_synthMap.begin(); i != m_synthMap.end(); ++i) {
+	if (i->second) i->second->setIdealChannelCount(targetChannelCount);
+    }
+}
+
+size_t
+AudioGenerator::getBlockSize() const
+{
+    return m_pluginBlockSize;
+}
+
+size_t
+AudioGenerator::mixModel(Model *model, size_t startFrame, size_t frameCount,
+			 float **buffer, size_t fadeIn, size_t fadeOut)
+{
+    if (m_sourceSampleRate == 0) {
+	std::cerr << "WARNING: AudioGenerator::mixModel: No base source sample rate available" << std::endl;
+	return frameCount;
+    }
+
+    QMutexLocker locker(&m_mutex);
+
+    PlayParameters *parameters =
+	PlayParameterRepository::getInstance()->getPlayParameters(model);
+    if (!parameters) return frameCount;
+
+    bool playing = !parameters->isPlayMuted();
+    if (!playing) return frameCount;
+
+    float gain = parameters->getPlayGain();
+    float pan = parameters->getPlayPan();
+
+    DenseTimeValueModel *dtvm = dynamic_cast<DenseTimeValueModel *>(model);
+    if (dtvm) {
+	return mixDenseTimeValueModel(dtvm, startFrame, frameCount,
+				      buffer, gain, pan, fadeIn, fadeOut);
+    }
+
+    SparseOneDimensionalModel *sodm = dynamic_cast<SparseOneDimensionalModel *>
+	(model);
+    if (sodm) {
+	return mixSparseOneDimensionalModel(sodm, startFrame, frameCount,
+					    buffer, gain, pan, fadeIn, fadeOut);
+    }
+
+    NoteModel *nm = dynamic_cast<NoteModel *>(model);
+    if (nm) {
+	return mixNoteModel(nm, startFrame, frameCount,
+			    buffer, gain, pan, fadeIn, fadeOut);
+    }
+
+    return frameCount;
+}
+
+size_t
+AudioGenerator::mixDenseTimeValueModel(DenseTimeValueModel *dtvm,
+				       size_t startFrame, size_t frames,
+				       float **buffer, float gain, float pan,
+				       size_t fadeIn, size_t fadeOut)
+{
+    static float *channelBuffer = 0;
+    static size_t channelBufSiz = 0;
+
+    size_t totalFrames = frames + fadeIn/2 + fadeOut/2;
+
+    if (channelBufSiz < totalFrames) {
+	delete[] channelBuffer;
+	channelBuffer = new float[totalFrames];
+	channelBufSiz = totalFrames;
+    }
+    
+    size_t got = 0;
+    size_t prevChannel = 999;
+
+    for (size_t c = 0; c < m_targetChannelCount; ++c) {
+
+	size_t sourceChannel = (c % dtvm->getChannelCount());
+
+//	std::cerr << "mixing channel " << c << " from source channel " << sourceChannel << std::endl;
+
+	float channelGain = gain;
+	if (pan != 0.0) {
+	    if (c == 0) {
+		if (pan > 0.0) channelGain *= 1.0 - pan;
+	    } else {
+		if (pan < 0.0) channelGain *= pan + 1.0;
+	    }
+	}
+
+	if (prevChannel != sourceChannel) {
+	    if (startFrame >= fadeIn/2) {
+		got = dtvm->getValues
+		    (sourceChannel,
+		     startFrame - fadeIn/2, startFrame + frames + fadeOut/2,
+		     channelBuffer);
+	    } else {
+		size_t missing = fadeIn/2 - startFrame;
+		got = dtvm->getValues
+		    (sourceChannel,
+		     0, startFrame + frames + fadeOut/2,
+		     channelBuffer + missing);
+	    }	    
+	}
+	prevChannel = sourceChannel;
+
+	for (size_t i = 0; i < fadeIn/2; ++i) {
+	    float *back = buffer[c];
+	    back -= fadeIn/2;
+	    back[i] += (channelGain * channelBuffer[i] * i) / fadeIn;
+	}
+
+	for (size_t i = 0; i < frames + fadeOut/2; ++i) {
+	    float mult = channelGain;
+	    if (i < fadeIn/2) {
+		mult = (mult * i) / fadeIn;
+	    }
+	    if (i > frames - fadeOut/2) {
+		mult = (mult * ((frames + fadeOut/2) - i)) / fadeOut;
+	    }
+	    buffer[c][i] += mult * channelBuffer[i];
+	}
+    }
+
+    return got;
+}
+  
+size_t
+AudioGenerator::mixSparseOneDimensionalModel(SparseOneDimensionalModel *sodm,
+					     size_t startFrame, size_t frames,
+					     float **buffer, float gain, float pan,
+					     size_t /* fadeIn */,
+					     size_t /* fadeOut */)
+{
+    RealTimePluginInstance *plugin = m_synthMap[sodm];
+    if (!plugin) return 0;
+
+    size_t latency = plugin->getLatency();
+    size_t blocks = frames / m_pluginBlockSize;
+    
+    //!!! hang on -- the fact that the audio callback play source's
+    //buffer is a multiple of the plugin's buffer size doesn't mean
+    //that we always get called for a multiple of it here (because it
+    //also depends on the JACK block size).  how should we ensure that
+    //all models write the same amount in to the mix, and that we
+    //always have a multiple of the plugin buffer size?  I guess this
+    //class has to be queryable for the plugin buffer size & the
+    //callback play source has to use that as a multiple for all the
+    //calls to mixModel
+
+    size_t got = blocks * m_pluginBlockSize;
+
+#ifdef DEBUG_AUDIO_GENERATOR
+    std::cout << "mixModel [sparse]: frames " << frames
+	      << ", blocks " << blocks << std::endl;
+#endif
+
+    snd_seq_event_t onEv;
+    onEv.type = SND_SEQ_EVENT_NOTEON;
+    onEv.data.note.channel = 0;
+    onEv.data.note.note = 64;
+    onEv.data.note.velocity = 127;
+
+    snd_seq_event_t offEv;
+    offEv.type = SND_SEQ_EVENT_NOTEOFF;
+    offEv.data.note.channel = 0;
+    offEv.data.note.velocity = 0;
+    
+    NoteOffSet &noteOffs = m_noteOffs[sodm];
+
+    for (size_t i = 0; i < blocks; ++i) {
+
+	size_t reqStart = startFrame + i * m_pluginBlockSize;
+
+	SparseOneDimensionalModel::PointList points =
+	    sodm->getPoints(reqStart + latency,
+			    reqStart + latency + m_pluginBlockSize);
+
+        Vamp::RealTime blockTime = Vamp::RealTime::frame2RealTime
+	    (startFrame + i * m_pluginBlockSize, m_sourceSampleRate);
+
+	for (SparseOneDimensionalModel::PointList::iterator pli =
+		 points.begin(); pli != points.end(); ++pli) {
+
+	    size_t pliFrame = pli->frame;
+
+	    if (pliFrame >= latency) pliFrame -= latency;
+
+	    if (pliFrame < reqStart ||
+		pliFrame >= reqStart + m_pluginBlockSize) continue;
+
+	    while (noteOffs.begin() != noteOffs.end() &&
+		   noteOffs.begin()->frame <= pliFrame) {
+
+                Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime
+		    (noteOffs.begin()->frame, m_sourceSampleRate);
+
+		offEv.data.note.note = noteOffs.begin()->pitch;
+
+#ifdef DEBUG_AUDIO_GENERATOR
+		std::cerr << "mixModel [sparse]: sending note-off event at time " << eventTime << " frame " << noteOffs.begin()->frame << std::endl;
+#endif
+
+		plugin->sendEvent(eventTime, &offEv);
+		noteOffs.erase(noteOffs.begin());
+	    }
+
+            Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime
+		(pliFrame, m_sourceSampleRate);
+	    
+	    plugin->sendEvent(eventTime, &onEv);
+
+#ifdef DEBUG_AUDIO_GENERATOR
+	    std::cout << "mixModel [sparse]: point at frame " << pliFrame << ", block start " << (startFrame + i * m_pluginBlockSize) << ", resulting time " << eventTime << std::endl;
+#endif
+	    
+	    size_t duration = 7000; // frames [for now]
+	    NoteOff noff;
+	    noff.pitch = onEv.data.note.note;
+	    noff.frame = pliFrame + duration;
+	    noteOffs.insert(noff);
+	}
+
+	while (noteOffs.begin() != noteOffs.end() &&
+	       noteOffs.begin()->frame <=
+	       startFrame + i * m_pluginBlockSize + m_pluginBlockSize) {
+
+            Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime
+		(noteOffs.begin()->frame, m_sourceSampleRate);
+
+	    offEv.data.note.note = noteOffs.begin()->pitch;
+
+#ifdef DEBUG_AUDIO_GENERATOR
+		std::cerr << "mixModel [sparse]: sending leftover note-off event at time " << eventTime << " frame " << noteOffs.begin()->frame << std::endl;
+#endif
+
+	    plugin->sendEvent(eventTime, &offEv);
+	    noteOffs.erase(noteOffs.begin());
+	}
+	
+	plugin->run(blockTime);
+	float **outs = plugin->getAudioOutputBuffers();
+
+	for (size_t c = 0; c < m_targetChannelCount; ++c) {
+#ifdef DEBUG_AUDIO_GENERATOR
+	    std::cout << "mixModel [sparse]: adding " << m_pluginBlockSize << " samples from plugin output " << c << std::endl;
+#endif
+
+	    size_t sourceChannel = (c % plugin->getAudioOutputCount());
+
+	    float channelGain = gain;
+	    if (pan != 0.0) {
+		if (c == 0) {
+		    if (pan > 0.0) channelGain *= 1.0 - pan;
+		} else {
+		    if (pan < 0.0) channelGain *= pan + 1.0;
+		}
+	    }
+
+	    for (size_t j = 0; j < m_pluginBlockSize; ++j) {
+		buffer[c][i * m_pluginBlockSize + j] +=
+		    channelGain * outs[sourceChannel][j];
+	    }
+	}
+    }
+
+    return got;
+}
+
+    
+//!!! mucho duplication with above -- refactor
+size_t
+AudioGenerator::mixNoteModel(NoteModel *nm,
+			     size_t startFrame, size_t frames,
+			     float **buffer, float gain, float pan,
+			     size_t /* fadeIn */,
+			     size_t /* fadeOut */)
+{
+    RealTimePluginInstance *plugin = m_synthMap[nm];
+    if (!plugin) return 0;
+
+    size_t latency = plugin->getLatency();
+    size_t blocks = frames / m_pluginBlockSize;
+    
+    //!!! hang on -- the fact that the audio callback play source's
+    //buffer is a multiple of the plugin's buffer size doesn't mean
+    //that we always get called for a multiple of it here (because it
+    //also depends on the JACK block size).  how should we ensure that
+    //all models write the same amount in to the mix, and that we
+    //always have a multiple of the plugin buffer size?  I guess this
+    //class has to be queryable for the plugin buffer size & the
+    //callback play source has to use that as a multiple for all the
+    //calls to mixModel
+
+    size_t got = blocks * m_pluginBlockSize;
+
+#ifdef DEBUG_AUDIO_GENERATOR
+    std::cout << "mixModel [note]: frames " << frames
+	      << ", blocks " << blocks << std::endl;
+#endif
+
+    snd_seq_event_t onEv;
+    onEv.type = SND_SEQ_EVENT_NOTEON;
+    onEv.data.note.channel = 0;
+    onEv.data.note.note = 64;
+    onEv.data.note.velocity = 127;
+
+    snd_seq_event_t offEv;
+    offEv.type = SND_SEQ_EVENT_NOTEOFF;
+    offEv.data.note.channel = 0;
+    offEv.data.note.velocity = 0;
+    
+    NoteOffSet &noteOffs = m_noteOffs[nm];
+
+    for (size_t i = 0; i < blocks; ++i) {
+
+	size_t reqStart = startFrame + i * m_pluginBlockSize;
+
+	NoteModel::PointList points =
+	    nm->getPoints(reqStart + latency,
+			    reqStart + latency + m_pluginBlockSize);
+
+        Vamp::RealTime blockTime = Vamp::RealTime::frame2RealTime
+	    (startFrame + i * m_pluginBlockSize, m_sourceSampleRate);
+
+	for (NoteModel::PointList::iterator pli =
+		 points.begin(); pli != points.end(); ++pli) {
+
+	    size_t pliFrame = pli->frame;
+
+	    if (pliFrame >= latency) pliFrame -= latency;
+
+	    if (pliFrame < reqStart ||
+		pliFrame >= reqStart + m_pluginBlockSize) continue;
+
+	    while (noteOffs.begin() != noteOffs.end() &&
+		   noteOffs.begin()->frame <= pliFrame) {
+
+                Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime
+		    (noteOffs.begin()->frame, m_sourceSampleRate);
+
+		offEv.data.note.note = noteOffs.begin()->pitch;
+
+#ifdef DEBUG_AUDIO_GENERATOR
+		std::cerr << "mixModel [note]: sending note-off event at time " << eventTime << " frame " << noteOffs.begin()->frame << std::endl;
+#endif
+
+		plugin->sendEvent(eventTime, &offEv);
+		noteOffs.erase(noteOffs.begin());
+	    }
+
+            Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime
+		(pliFrame, m_sourceSampleRate);
+	    
+            if (nm->getScaleUnits() == "Hz") {
+                onEv.data.note.note = Pitch::getPitchForFrequency(pli->value);
+            } else {
+                onEv.data.note.note = lrintf(pli->value);
+            }
+
+	    plugin->sendEvent(eventTime, &onEv);
+
+#ifdef DEBUG_AUDIO_GENERATOR
+	    std::cout << "mixModel [note]: point at frame " << pliFrame << ", block start " << (startFrame + i * m_pluginBlockSize) << ", resulting time " << eventTime << std::endl;
+#endif
+	    
+	    size_t duration = pli->duration;
+            if (duration == 0 || duration == 1) {
+                duration = m_sourceSampleRate / 20;
+            }
+	    NoteOff noff;
+	    noff.pitch = onEv.data.note.note;
+	    noff.frame = pliFrame + duration;
+	    noteOffs.insert(noff);
+	}
+
+	while (noteOffs.begin() != noteOffs.end() &&
+	       noteOffs.begin()->frame <=
+	       startFrame + i * m_pluginBlockSize + m_pluginBlockSize) {
+
+            Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime
+		(noteOffs.begin()->frame, m_sourceSampleRate);
+
+	    offEv.data.note.note = noteOffs.begin()->pitch;
+
+#ifdef DEBUG_AUDIO_GENERATOR
+		std::cerr << "mixModel [note]: sending leftover note-off event at time " << eventTime << " frame " << noteOffs.begin()->frame << std::endl;
+#endif
+
+	    plugin->sendEvent(eventTime, &offEv);
+	    noteOffs.erase(noteOffs.begin());
+	}
+	
+	plugin->run(blockTime);
+	float **outs = plugin->getAudioOutputBuffers();
+
+	for (size_t c = 0; c < m_targetChannelCount; ++c) {
+#ifdef DEBUG_AUDIO_GENERATOR
+	    std::cout << "mixModel [note]: adding " << m_pluginBlockSize << " samples from plugin output " << c << std::endl;
+#endif
+
+	    size_t sourceChannel = (c % plugin->getAudioOutputCount());
+
+	    float channelGain = gain;
+	    if (pan != 0.0) {
+		if (c == 0) {
+		    if (pan > 0.0) channelGain *= 1.0 - pan;
+		} else {
+		    if (pan < 0.0) channelGain *= pan + 1.0;
+		}
+	    }
+
+	    for (size_t j = 0; j < m_pluginBlockSize; ++j) {
+		buffer[c][i * m_pluginBlockSize + j] += 
+		    channelGain * outs[sourceChannel][j];
+	    }
+	}
+    }
+
+    return got;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioGenerator.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,144 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _AUDIO_GENERATOR_H_
+#define _AUDIO_GENERATOR_H_
+
+class Model;
+class NoteModel;
+class DenseTimeValueModel;
+class SparseOneDimensionalModel;
+class RealTimePluginInstance;
+
+#include <QObject>
+#include <QMutex>
+
+#include <set>
+#include <map>
+
+class AudioGenerator : public QObject
+{
+    Q_OBJECT
+
+public:
+    AudioGenerator();
+    virtual ~AudioGenerator();
+
+    /**
+     * Return true if the given model is of a type that we generally
+     * know how to play.  This doesn't guarantee that a specific
+     * AudioGenerator will actually produce sounds for it (for
+     * example, it may turn out that a vital plugin is missing).
+     */
+    static bool canPlay(const Model *model);
+
+    static QString getDefaultPlayPluginId(const Model *model);
+    static QString getDefaultPlayPluginConfiguration(const Model *model);
+
+    /**
+     * Add a data model to be played from and initialise any necessary
+     * audio generation code.  Returns true if the model will be
+     * played.  (The return value test here is stricter than that for
+     * canPlay, above.)  The model will be added regardless of the
+     * return value.
+     */
+    virtual bool addModel(Model *model);
+
+    /**
+     * Remove a model.
+     */
+    virtual void removeModel(Model *model);
+
+    /**
+     * Remove all models.
+     */
+    virtual void clearModels();
+
+    /**
+     * Reset playback, clearing plugins and the like.
+     */
+    virtual void reset();
+
+    /**
+     * Set the target channel count.  The buffer parameter to mixModel
+     * must always point to at least this number of arrays.
+     */
+    virtual void setTargetChannelCount(size_t channelCount);
+
+    /**
+     * Return the internal processing block size.  The frameCount
+     * argument to all mixModel calls must be a multiple of this
+     * value.
+     */
+    virtual size_t getBlockSize() const;
+
+    /**
+     * Mix a single model into an output buffer.
+     */
+    virtual size_t mixModel(Model *model, size_t startFrame, size_t frameCount,
+			    float **buffer, size_t fadeIn = 0, size_t fadeOut = 0);
+
+protected slots:
+    void playPluginIdChanged(const Model *, QString);
+    void playPluginConfigurationChanged(const Model *, QString);
+
+protected:
+    size_t       m_sourceSampleRate;
+    size_t       m_targetChannelCount;
+
+    struct NoteOff {
+
+	int pitch;
+	size_t frame;
+
+	struct Comparator {
+	    bool operator()(const NoteOff &n1, const NoteOff &n2) const {
+		return n1.frame < n2.frame;
+	    }
+	};
+    };
+
+    typedef std::map<const Model *, RealTimePluginInstance *> PluginMap;
+
+    typedef std::set<NoteOff, NoteOff::Comparator> NoteOffSet;
+    typedef std::map<const Model *, NoteOffSet> NoteOffMap;
+
+    QMutex m_mutex;
+    PluginMap m_synthMap;
+    NoteOffMap m_noteOffs;
+    static QString m_sampleDir;
+
+    virtual RealTimePluginInstance *loadPluginFor(const Model *model);
+    virtual RealTimePluginInstance *loadPlugin(QString id, QString program);
+    static QString getSampleDir();
+    static void setSampleDir(RealTimePluginInstance *plugin);
+
+    virtual size_t mixDenseTimeValueModel
+    (DenseTimeValueModel *model, size_t startFrame, size_t frameCount,
+     float **buffer, float gain, float pan, size_t fadeIn, size_t fadeOut);
+
+    virtual size_t mixSparseOneDimensionalModel
+    (SparseOneDimensionalModel *model, size_t startFrame, size_t frameCount,
+     float **buffer, float gain, float pan, size_t fadeIn, size_t fadeOut);
+
+    virtual size_t mixNoteModel
+    (NoteModel *model, size_t startFrame, size_t frameCount,
+     float **buffer, float gain, float pan, size_t fadeIn, size_t fadeOut);
+
+    static const size_t m_pluginBlockSize;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioJACKTarget.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,218 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifdef HAVE_JACK
+
+#include "AudioJACKTarget.h"
+#include "AudioCallbackPlaySource.h"
+
+#include <iostream>
+#include <cmath>
+
+//#define DEBUG_AUDIO_JACK_TARGET 1
+
+AudioJACKTarget::AudioJACKTarget(AudioCallbackPlaySource *source) :
+    AudioCallbackPlayTarget(source),
+    m_client(0),
+    m_bufferSize(0),
+    m_sampleRate(0)
+{
+    char name[20];
+    strcpy(name, "Sonic Visualiser");
+    m_client = jack_client_new(name);
+
+    if (!m_client) {
+	sprintf(name, "Sonic Visualiser (%d)", (int)getpid());
+	m_client = jack_client_new(name);
+	if (!m_client) {
+	    std::cerr
+		<< "ERROR: AudioJACKTarget: Failed to connect to JACK server"
+		<< std::endl;
+	}
+    }
+
+    if (!m_client) return;
+
+    m_bufferSize = jack_get_buffer_size(m_client);
+    m_sampleRate = jack_get_sample_rate(m_client);
+
+    jack_set_process_callback(m_client, processStatic, this);
+
+    if (jack_activate(m_client)) {
+	std::cerr << "ERROR: AudioJACKTarget: Failed to activate JACK client"
+		  << std::endl;
+    }
+
+    if (m_source) {
+	sourceModelReplaced();
+    }
+}
+
+AudioJACKTarget::~AudioJACKTarget()
+{
+    if (m_client) {
+	jack_deactivate(m_client);
+	jack_client_close(m_client);
+    }
+}
+
+bool
+AudioJACKTarget::isOK() const
+{
+    return (m_client != 0);
+}
+
+int
+AudioJACKTarget::processStatic(jack_nframes_t nframes, void *arg)
+{
+    return ((AudioJACKTarget *)arg)->process(nframes);
+}
+
+void
+AudioJACKTarget::sourceModelReplaced()
+{
+    m_mutex.lock();
+
+    m_source->setTargetBlockSize(m_bufferSize);
+    m_source->setTargetSampleRate(m_sampleRate);
+
+    size_t channels = m_source->getSourceChannelCount();
+
+    // Because we offer pan, we always want at least 2 channels
+    if (channels < 2) channels = 2;
+
+    if (channels == m_outputs.size() || !m_client) {
+	m_mutex.unlock();
+	return;
+    }
+
+    const char **ports =
+	jack_get_ports(m_client, NULL, NULL,
+		       JackPortIsPhysical | JackPortIsInput);
+    size_t physicalPortCount = 0;
+    while (ports[physicalPortCount]) ++physicalPortCount;
+
+#ifdef DEBUG_AUDIO_JACK_TARGET    
+    std::cerr << "AudioJACKTarget::sourceModelReplaced: have " << channels << " channels and " << physicalPortCount << " physical ports" << std::endl;
+#endif
+
+    while (m_outputs.size() < channels) {
+	
+	char name[20];
+	jack_port_t *port;
+
+	sprintf(name, "out %d", m_outputs.size() + 1);
+
+	port = jack_port_register(m_client,
+				  name,
+				  JACK_DEFAULT_AUDIO_TYPE,
+				  JackPortIsOutput,
+				  0);
+
+	if (!port) {
+	    std::cerr
+		<< "ERROR: AudioJACKTarget: Failed to create JACK output port "
+		<< m_outputs.size() << std::endl;
+	} else {
+	    m_source->setTargetPlayLatency(jack_port_get_latency(port));
+	}
+
+	if (m_outputs.size() < physicalPortCount) {
+	    jack_connect(m_client, jack_port_name(port), ports[m_outputs.size()]);
+	}
+
+	m_outputs.push_back(port);
+    }
+
+    while (m_outputs.size() > channels) {
+	std::vector<jack_port_t *>::iterator itr = m_outputs.end();
+	--itr;
+	jack_port_t *port = *itr;
+	if (port) jack_port_unregister(m_client, port);
+	m_outputs.erase(itr);
+    }
+
+    m_mutex.unlock();
+}
+
+int
+AudioJACKTarget::process(jack_nframes_t nframes)
+{
+    if (!m_mutex.tryLock()) {
+	return 0;
+    }
+
+    if (m_outputs.empty()) {
+	m_mutex.unlock();
+	return 0;
+    }
+
+#ifdef DEBUG_AUDIO_JACK_TARGET    
+    std::cout << "AudioJACKTarget::process(" << nframes << "): have a source" << std::endl;
+#endif
+
+#ifdef DEBUG_AUDIO_JACK_TARGET    
+    if (m_bufferSize != nframes) {
+	std::cerr << "WARNING: m_bufferSize != nframes (" << m_bufferSize << " != " << nframes << ")" << std::endl;
+    }
+#endif
+
+    float **buffers = (float **)alloca(m_outputs.size() * sizeof(float *));
+
+    for (size_t ch = 0; ch < m_outputs.size(); ++ch) {
+	buffers[ch] = (float *)jack_port_get_buffer(m_outputs[ch], nframes);
+    }
+
+    if (m_source) {
+	m_source->getSourceSamples(nframes, buffers);
+    } else {
+	for (size_t ch = 0; ch < m_outputs.size(); ++ch) {
+	    for (size_t i = 0; i < nframes; ++i) {
+		buffers[ch][i] = 0.0;
+	    }
+	}
+    }
+
+    float peakLeft = 0.0, peakRight = 0.0;
+
+    for (size_t ch = 0; ch < m_outputs.size(); ++ch) {
+
+	float peak = 0.0;
+
+	for (size_t i = 0; i < nframes; ++i) {
+	    buffers[ch][i] *= m_outputGain;
+	    float sample = fabsf(buffers[ch][i]);
+	    if (sample > peak) peak = sample;
+	}
+
+	if (ch == 0) peakLeft = peak;
+	if (ch > 0 || m_outputs.size() == 1) peakRight = peak;
+    }
+	    
+    if (m_source) {
+	m_source->setOutputLevels(peakLeft, peakRight);
+    }
+
+    m_mutex.unlock();
+    return 0;
+}
+
+
+#ifdef INCLUDE_MOCFILES
+#include "AudioJACKTarget.moc.cpp"
+#endif
+
+#endif /* HAVE_JACK */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioJACKTarget.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,58 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _AUDIO_JACK_TARGET_H_
+#define _AUDIO_JACK_TARGET_H_
+
+#ifdef HAVE_JACK
+
+#include <jack/jack.h>
+#include <vector>
+
+#include "AudioCallbackPlayTarget.h"
+
+#include <QMutex>
+
+class AudioCallbackPlaySource;
+
+class AudioJACKTarget : public AudioCallbackPlayTarget
+{
+    Q_OBJECT
+
+public:
+    AudioJACKTarget(AudioCallbackPlaySource *source);
+    virtual ~AudioJACKTarget();
+
+    virtual bool isOK() const;
+
+public slots:
+    virtual void sourceModelReplaced();
+
+protected:
+    int process(jack_nframes_t nframes);
+
+    static int processStatic(jack_nframes_t, void *);
+
+    jack_client_t              *m_client;
+    std::vector<jack_port_t *>  m_outputs;
+    jack_nframes_t              m_bufferSize;
+    jack_nframes_t              m_sampleRate;
+    QMutex                      m_mutex;
+};
+
+#endif /* HAVE_JACK */
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioPortAudioTarget.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,201 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifdef HAVE_PORTAUDIO
+
+#include "AudioPortAudioTarget.h"
+#include "AudioCallbackPlaySource.h"
+
+#include <iostream>
+#include <cassert>
+#include <cmath>
+
+//#define DEBUG_AUDIO_PORT_AUDIO_TARGET 1
+
+AudioPortAudioTarget::AudioPortAudioTarget(AudioCallbackPlaySource *source) :
+    AudioCallbackPlayTarget(source),
+    m_stream(0),
+    m_bufferSize(0),
+    m_sampleRate(0),
+    m_latency(0)
+{
+    PaError err;
+
+    err = Pa_Initialize();
+    if (err != paNoError) {
+	std::cerr << "ERROR: AudioPortAudioTarget: Failed to initialize PortAudio" << std::endl;
+	return;
+    }
+
+    m_bufferSize = 1024;
+    m_sampleRate = 44100;
+    if (m_source && (m_source->getSourceSampleRate() != 0)) {
+	m_sampleRate = m_source->getSourceSampleRate();
+    }
+
+    m_latency = Pa_GetMinNumBuffers(m_bufferSize, m_sampleRate) * m_bufferSize;
+
+    std::cerr << "\n\n\nLATENCY= " << m_latency << std::endl;
+
+    err = Pa_OpenDefaultStream(&m_stream, 0, 2, paFloat32,
+			       m_sampleRate, m_bufferSize, 0,
+			       processStatic, this);
+
+    if (err != paNoError) {
+	std::cerr << "ERROR: AudioPortAudioTarget: Failed to open PortAudio stream" << std::endl;
+	m_stream = 0;
+	Pa_Terminate();
+	return;
+    }
+
+    err = Pa_StartStream(m_stream);
+
+    if (err != paNoError) {
+	std::cerr << "ERROR: AudioPortAudioTarget: Failed to start PortAudio stream" << std::endl;
+	Pa_CloseStream(m_stream);
+	m_stream = 0;
+	Pa_Terminate();
+	return;
+    }
+
+    if (m_source) {
+	std::cerr << "AudioPortAudioTarget: block size " << m_bufferSize << std::endl;
+	m_source->setTargetBlockSize(m_bufferSize);
+	m_source->setTargetSampleRate(m_sampleRate);
+	m_source->setTargetPlayLatency(m_latency);
+    }
+}
+
+AudioPortAudioTarget::~AudioPortAudioTarget()
+{
+    if (m_stream) {
+	PaError err;
+	err = Pa_CloseStream(m_stream);
+	if (err != paNoError) {
+	    std::cerr << "ERROR: AudioPortAudioTarget: Failed to close PortAudio stream" << std::endl;
+	}
+	Pa_Terminate();
+    }
+}
+
+bool
+AudioPortAudioTarget::isOK() const
+{
+    return (m_stream != 0);
+}
+
+int
+AudioPortAudioTarget::processStatic(void *input, void *output,
+				    unsigned long nframes,
+				    PaTimestamp outTime, void *data)
+{
+    return ((AudioPortAudioTarget *)data)->process(input, output,
+						   nframes, outTime);
+}
+
+void
+AudioPortAudioTarget::sourceModelReplaced()
+{
+    m_source->setTargetSampleRate(m_sampleRate);
+}
+
+int
+AudioPortAudioTarget::process(void *inputBuffer, void *outputBuffer,
+			      unsigned long nframes,
+			      PaTimestamp)
+{
+#ifdef DEBUG_AUDIO_PORT_AUDIO_TARGET    
+    std::cout << "AudioPortAudioTarget::process(" << nframes << ")" << std::endl;
+#endif
+
+    if (!m_source) return 0;
+
+    float *output = (float *)outputBuffer;
+
+    assert(nframes <= m_bufferSize);
+
+    static float **tmpbuf = 0;
+    static size_t tmpbufch = 0;
+    static size_t tmpbufsz = 0;
+
+    size_t sourceChannels = m_source->getSourceChannelCount();
+
+    // Because we offer pan, we always want at least 2 channels
+    if (sourceChannels < 2) sourceChannels = 2;
+
+    if (!tmpbuf || tmpbufch != sourceChannels || tmpbufsz < m_bufferSize) {
+
+	if (tmpbuf) {
+	    for (size_t i = 0; i < tmpbufch; ++i) {
+		delete[] tmpbuf[i];
+	    }
+	    delete[] tmpbuf;
+	}
+
+	tmpbufch = sourceChannels;
+	tmpbufsz = m_bufferSize;
+	tmpbuf = new float *[tmpbufch];
+
+	for (size_t i = 0; i < tmpbufch; ++i) {
+	    tmpbuf[i] = new float[tmpbufsz];
+	}
+    }
+	
+    m_source->getSourceSamples(nframes, tmpbuf);
+
+    float peakLeft = 0.0, peakRight = 0.0;
+
+    for (size_t ch = 0; ch < 2; ++ch) {
+	
+	float peak = 0.0;
+
+	if (ch < sourceChannels) {
+
+	    // PortAudio samples are interleaved
+	    for (size_t i = 0; i < nframes; ++i) {
+		output[i * 2 + ch] = tmpbuf[ch][i] * m_outputGain;
+		float sample = fabsf(output[i * 2 + ch]);
+		if (sample > peak) peak = sample;
+	    }
+
+	} else if (ch == 1 && sourceChannels == 1) {
+
+	    for (size_t i = 0; i < nframes; ++i) {
+		output[i * 2 + ch] = tmpbuf[0][i] * m_outputGain;
+		float sample = fabsf(output[i * 2 + ch]);
+		if (sample > peak) peak = sample;
+	    }
+
+	} else {
+	    for (size_t i = 0; i < nframes; ++i) {
+		output[i * 2 + ch] = 0;
+	    }
+	}
+
+	if (ch == 0) peakLeft = peak;
+	if (ch > 0 || sourceChannels == 1) peakRight = peak;
+    }
+
+    m_source->setOutputLevels(peakLeft, peakRight);
+
+    return 0;
+}
+
+#ifdef INCLUDE_MOCFILES
+#include "AudioPortAudioTarget.moc.cpp"
+#endif
+
+#endif /* HAVE_PORTAUDIO */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioPortAudioTarget.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,58 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _AUDIO_PORT_AUDIO_TARGET_H_
+#define _AUDIO_PORT_AUDIO_TARGET_H_
+
+#ifdef HAVE_PORTAUDIO
+
+#include <portaudio.h>
+#include <vector>
+
+#include "AudioCallbackPlayTarget.h"
+
+class AudioCallbackPlaySource;
+
+class AudioPortAudioTarget : public AudioCallbackPlayTarget
+{
+    Q_OBJECT
+
+public:
+    AudioPortAudioTarget(AudioCallbackPlaySource *source);
+    virtual ~AudioPortAudioTarget();
+
+    virtual bool isOK() const;
+
+public slots:
+    virtual void sourceModelReplaced();
+
+protected:
+    int process(void *input, void *output, unsigned long frames,
+		PaTimestamp outTime);
+
+    static int processStatic(void *, void *, unsigned long,
+			     PaTimestamp, void *);
+
+    PortAudioStream *m_stream;
+
+    int m_bufferSize;
+    int m_sampleRate;
+    int m_latency;
+};
+
+#endif /* HAVE_PORTAUDIO */
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioTargetFactory.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,69 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "AudioTargetFactory.h"
+
+#include "AudioJACKTarget.h"
+#include "AudioCoreAudioTarget.h"
+#include "AudioPortAudioTarget.h"
+
+#include <iostream>
+
+AudioCallbackPlayTarget *
+AudioTargetFactory::createCallbackTarget(AudioCallbackPlaySource *source)
+{
+    AudioCallbackPlayTarget *target = 0;
+
+#ifdef HAVE_JACK
+    target = new AudioJACKTarget(source);
+    if (target->isOK()) return target;
+    else {
+	std::cerr << "WARNING: AudioTargetFactory::createCallbackTarget: Failed to open JACK target" << std::endl;
+	delete target;
+    }
+#endif
+
+#ifdef HAVE_COREAUDIO
+    target = new AudioCoreAudioTarget(source);
+    if (target->isOK()) return target;
+    else {
+	std::cerr << "WARNING: AudioTargetFactory::createCallbackTarget: Failed to open CoreAudio target" << std::endl;
+	delete target;
+    }
+#endif
+
+#ifdef HAVE_DIRECTSOUND
+    target = new AudioDirectSoundTarget(source);
+    if (target->isOK()) return target;
+    else {
+	std::cerr << "WARNING: AudioTargetFactory::createCallbackTarget: Failed to open DirectSound target" << std::endl;
+	delete target;
+    }
+#endif
+
+#ifdef HAVE_PORTAUDIO
+    target = new AudioPortAudioTarget(source);
+    if (target->isOK()) return target;
+    else {
+	std::cerr << "WARNING: AudioTargetFactory::createCallbackTarget: Failed to open PortAudio target" << std::endl;
+	delete target;
+    }
+#endif
+
+    std::cerr << "WARNING: AudioTargetFactory::createCallbackTarget: No suitable targets available" << std::endl;
+    return 0;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/AudioTargetFactory.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,29 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _AUDIO_TARGET_FACTORY_H_
+#define _AUDIO_TARGET_FACTORY_H_
+
+class AudioCallbackPlaySource;
+class AudioCallbackPlayTarget;
+
+class AudioTargetFactory 
+{
+public:
+    static AudioCallbackPlayTarget *createCallbackTarget(AudioCallbackPlaySource *);
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/IntegerTimeStretcher.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,226 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "IntegerTimeStretcher.h"
+
+#include <iostream>
+#include <cassert>
+
+//#define DEBUG_INTEGER_TIME_STRETCHER 1
+
+IntegerTimeStretcher::IntegerTimeStretcher(size_t ratio,
+					   size_t maxProcessInputBlockSize,
+					   size_t inputIncrement,
+					   size_t windowSize,
+					   WindowType windowType) :
+    m_ratio(ratio),
+    m_n1(inputIncrement),
+    m_n2(m_n1 * ratio),
+    m_wlen(std::max(windowSize, m_n2 * 2)),
+    m_inbuf(m_wlen),
+    m_outbuf(maxProcessInputBlockSize * ratio)
+{
+    m_window = new Window<float>(windowType, m_wlen),
+
+    m_time = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * m_wlen);
+    m_freq = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * m_wlen);
+    m_dbuf = (float *)fftwf_malloc(sizeof(float) * m_wlen);
+
+    m_plan = fftwf_plan_dft_1d(m_wlen, m_time, m_freq, FFTW_FORWARD, FFTW_ESTIMATE);
+    m_iplan = fftwf_plan_dft_c2r_1d(m_wlen, m_freq, m_dbuf, FFTW_ESTIMATE);
+
+    m_mashbuf = new float[m_wlen];
+    for (int i = 0; i < m_wlen; ++i) {
+	m_mashbuf[i] = 0.0;
+    }
+}
+
+IntegerTimeStretcher::~IntegerTimeStretcher()
+{
+    std::cerr << "IntegerTimeStretcher::~IntegerTimeStretcher" << std::endl;
+
+    fftwf_destroy_plan(m_plan);
+    fftwf_destroy_plan(m_iplan);
+
+    fftwf_free(m_time);
+    fftwf_free(m_freq);
+    fftwf_free(m_dbuf);
+
+    delete m_window;
+    delete m_mashbuf;
+}	
+
+size_t
+IntegerTimeStretcher::getProcessingLatency() const
+{
+    return getWindowSize() - getInputIncrement();
+}
+
+void
+IntegerTimeStretcher::process(float *input, float *output, size_t samples)
+{
+    // We need to add samples from input to our internal buffer.  When
+    // we have m_windowSize samples in the buffer, we can process it,
+    // move the samples back by m_n1 and write the output onto our
+    // internal output buffer.  If we have (samples * ratio) samples
+    // in that, we can write m_n2 of them back to output and return
+    // (otherwise we have to write zeroes).
+
+    // When we process, we write m_wlen to our fixed output buffer
+    // (m_mashbuf).  We then pull out the first m_n2 samples from that
+    // buffer, push them into the output ring buffer, and shift
+    // m_mashbuf left by that amount.
+
+    // The processing latency is then m_wlen - m_n2.
+
+    size_t consumed = 0;
+
+#ifdef DEBUG_INTEGER_TIME_STRETCHER
+    std::cerr << "IntegerTimeStretcher::process(" << samples << ", consumed = " << consumed << "), writable " << m_inbuf.getWriteSpace() <<", readable "<< m_outbuf.getReadSpace() << std::endl;
+#endif
+
+    while (consumed < samples) {
+
+	size_t writable = m_inbuf.getWriteSpace();
+	writable = std::min(writable, samples - consumed);
+
+	if (writable == 0) {
+	    //!!! then what? I don't think this should happen, but
+	    std::cerr << "WARNING: IntegerTimeStretcher::process: writable == 0" << std::endl;
+	    break;
+	}
+
+#ifdef DEBUG_INTEGER_TIME_STRETCHER
+	std::cerr << "writing " << writable << " from index " << consumed << " to inbuf, consumed will be " << consumed + writable << std::endl;
+#endif
+	m_inbuf.write(input + consumed, writable);
+	consumed += writable;
+
+	while (m_inbuf.getReadSpace() >= m_wlen &&
+	       m_outbuf.getWriteSpace() >= m_n2) {
+
+	    // We know we have at least m_wlen samples available
+	    // in m_inbuf.  We need to peek m_wlen of them for
+	    // processing, and then read m_n1 to advance the read
+	    // pointer.
+
+	    size_t got = m_inbuf.peek(m_dbuf, m_wlen);
+	    assert(got == m_wlen);
+		
+	    processBlock(m_dbuf, m_mashbuf);
+
+#ifdef DEBUG_INTEGER_TIME_STRETCHER
+	    std::cerr << "writing first " << m_n2 << " from mashbuf, skipping " << m_n1 << " on inbuf " << std::endl;
+#endif
+	    m_inbuf.skip(m_n1);
+	    m_outbuf.write(m_mashbuf, m_n2);
+
+	    for (size_t i = 0; i < m_wlen - m_n2; ++i) {
+		m_mashbuf[i] = m_mashbuf[i + m_n2];
+	    }
+	    for (size_t i = m_wlen - m_n2; i < m_wlen; ++i) {
+		m_mashbuf[i] = 0.0f;
+	    }
+	}
+
+//	std::cerr << "WARNING: IntegerTimeStretcher::process: writespace not enough for output increment (" << m_outbuf.getWriteSpace() << " < " << m_n2 << ")" << std::endl;
+//	}
+
+#ifdef DEBUG_INTEGER_TIME_STRETCHER
+	std::cerr << "loop ended: inbuf read space " << m_inbuf.getReadSpace() << ", outbuf write space " << m_outbuf.getWriteSpace() << std::endl;
+#endif
+    }
+
+    if (m_outbuf.getReadSpace() < samples * m_ratio) {
+	std::cerr << "WARNING: IntegerTimeStretcher::process: not enough data (yet?) (" << m_outbuf.getReadSpace() << " < " << (samples * m_ratio) << ")" << std::endl;
+	size_t fill = samples * m_ratio - m_outbuf.getReadSpace();
+	for (size_t i = 0; i < fill; ++i) {
+	    output[i] = 0.0;
+	}
+	m_outbuf.read(output + fill, m_outbuf.getReadSpace());
+    } else {
+#ifdef DEBUG_INTEGER_TIME_STRETCHER
+	std::cerr << "enough data - writing " << samples * m_ratio << " from outbuf" << std::endl;
+#endif
+	m_outbuf.read(output, samples * m_ratio);
+    }
+
+#ifdef DEBUG_INTEGER_TIME_STRETCHER
+    std::cerr << "IntegerTimeStretcher::process returning" << std::endl;
+#endif
+}
+
+void
+IntegerTimeStretcher::processBlock(float *buf, float *out)
+{
+    size_t i;
+
+    // buf contains m_wlen samples; out contains enough space for
+    // m_wlen * ratio samples (we mix into out, rather than replacing)
+
+#ifdef DEBUG_INTEGER_TIME_STRETCHER
+    std::cerr << "IntegerTimeStretcher::processBlock" << std::endl;
+#endif
+
+    m_window->cut(buf);
+
+    for (i = 0; i < m_wlen/2; ++i) {
+	float temp = buf[i];
+	buf[i] = buf[i + m_wlen/2];
+	buf[i + m_wlen/2] = temp;
+    }
+    
+    for (i = 0; i < m_wlen; ++i) {
+	m_time[i][0] = buf[i];
+	m_time[i][1] = 0.0;
+    }
+
+    fftwf_execute(m_plan); // m_time -> m_freq
+
+    for (i = 0; i < m_wlen; ++i) {
+	
+	float mag = sqrtf(m_freq[i][0] * m_freq[i][0] +
+			  m_freq[i][1] * m_freq[i][1]);
+		
+	float phase = atan2f(m_freq[i][1], m_freq[i][0]);
+	
+	phase = phase * m_ratio;
+	
+	float real = mag * cosf(phase);
+	float imag = mag * sinf(phase);
+	m_freq[i][0] = real;
+	m_freq[i][1] = imag;
+    }
+    
+    fftwf_execute(m_iplan); // m_freq -> in, inverse fft
+    
+    for (i = 0; i < m_wlen/2; ++i) {
+	float temp = buf[i] / m_wlen;
+	buf[i] = buf[i + m_wlen/2] / m_wlen;
+	buf[i + m_wlen/2] = temp;
+    }
+    
+    m_window->cut(buf);
+    
+    int div = m_wlen / m_n2;
+    if (div > 1) div /= 2;
+    for (i = 0; i < m_wlen; ++i) {
+	buf[i] /= div;
+    }
+
+    for (i = 0; i < m_wlen; ++i) {
+	out[i] += buf[i];
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/audioio/IntegerTimeStretcher.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,91 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _INTEGER_TIME_STRETCHER_H_
+#define _INTEGER_TIME_STRETCHER_H_
+
+#include "base/Window.h"
+#include "base/RingBuffer.h"
+
+#include <fftw3.h>
+
+/**
+ * A time stretcher that slows down audio by an integer multiple of
+ * its original duration, preserving pitch.  This uses the simple
+ * phase vocoder technique from DAFX pp275-276, adding a block-based
+ * stream oriented API.
+ *
+ * Causes significant transient smearing, but sounds good for steady
+ * notes and is generally predictable.
+ */
+
+class IntegerTimeStretcher
+{
+public:
+    IntegerTimeStretcher(size_t ratio,
+			 size_t maxProcessInputBlockSize,
+			 size_t inputIncrement = 64,
+			 size_t windowSize = 2048,
+			 WindowType windowType = HanningWindow);
+    virtual ~IntegerTimeStretcher();
+
+    void process(float *input, float *output, size_t samples);
+
+    /**
+     * Get the hop size for input.  Smaller values may produce better
+     * results, at a cost in processing time.  Larger values are
+     * faster but increase the likelihood of echo-like effects.  The
+     * default is 64, which is usually pretty good, though heavy on
+     * processor power.
+     */
+    size_t getInputIncrement() const { return m_n1; }
+
+    /**
+     * Get the window size for FFT processing.  Must be larger than
+     * the input and output increments.  The default is 2048.
+     */
+    size_t getWindowSize() const { return m_wlen; }
+
+    /**
+     * Get the window type.  The default is a Hanning window.
+     */
+    WindowType getWindowType() const { return m_window->getType(); }
+
+    size_t getRatio() const { return m_ratio; }
+    size_t getOutputIncrement() const { return getInputIncrement() * getRatio(); }
+    size_t getProcessingLatency() const;
+
+protected:
+    void processBlock(float *in, float *out);
+
+    size_t m_ratio;
+    size_t m_n1;
+    size_t m_n2;
+    size_t m_wlen;
+    Window<float> *m_window;
+
+    fftwf_complex *m_time;
+    fftwf_complex *m_freq;
+    float *m_dbuf;
+
+    fftwf_plan m_plan;
+    fftwf_plan m_iplan;
+    
+    RingBuffer<float> m_inbuf;
+    RingBuffer<float> m_outbuf;
+    float *m_mashbuf;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/document/Document.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,746 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "Document.h"
+
+#include "model/WaveFileModel.h"
+#include "base/Layer.h"
+#include "base/CommandHistory.h"
+#include "base/Command.h"
+#include "base/View.h"
+#include "base/PlayParameterRepository.h"
+#include "base/PlayParameters.h"
+#include "transform/TransformFactory.h"
+#include <iostream>
+
+//!!! still need to handle command history, documentRestored/documentModified
+
+Document::Document() :
+    m_mainModel(0)
+{
+}
+
+Document::~Document()
+{
+    //!!! Document should really own the command history.  atm we
+    //still refer to it in various places that don't have access to
+    //the document, be nice to fix that
+
+//    std::cerr << "\n\nDocument::~Document: about to clear command history" << std::endl;
+    CommandHistory::getInstance()->clear();
+    
+//    std::cerr << "Document::~Document: about to delete layers" << std::endl;
+    while (!m_layers.empty()) {
+	deleteLayer(*m_layers.begin(), true);
+    }
+
+    if (!m_models.empty()) {
+	std::cerr << "Document::~Document: WARNING: " 
+		  << m_models.size() << " model(s) still remain -- "
+		  << "should have been garbage collected when deleting layers"
+		  << std::endl;
+	while (!m_models.empty()) {
+	    if (m_models.begin()->first == m_mainModel) {
+		// just in case!
+		std::cerr << "Document::~Document: WARNING: Main model is also"
+			  << " in models list!" << std::endl;
+	    } else {
+		emit modelAboutToBeDeleted(m_models.begin()->first);
+		delete m_models.begin()->first;
+	    }
+	    m_models.erase(m_models.begin());
+	}
+    }
+
+//    std::cerr << "Document::~Document: About to get rid of main model"
+//	      << std::endl;
+    emit modelAboutToBeDeleted(m_mainModel);
+    emit mainModelChanged(0);
+    delete m_mainModel;
+
+}
+
+Layer *
+Document::createLayer(LayerFactory::LayerType type)
+{
+    Layer *newLayer = LayerFactory::getInstance()->createLayer(type);
+    if (!newLayer) return 0;
+
+    newLayer->setObjectName(getUniqueLayerName(newLayer->objectName()));
+
+    m_layers.insert(newLayer);
+    emit layerAdded(newLayer);
+
+    return newLayer;
+}
+
+Layer *
+Document::createMainModelLayer(LayerFactory::LayerType type)
+{
+    Layer *newLayer = createLayer(type);
+    if (!newLayer) return 0;
+    setModel(newLayer, m_mainModel);
+    return newLayer;
+}
+
+Layer *
+Document::createImportedLayer(Model *model)
+{
+    LayerFactory::LayerTypeSet types =
+	LayerFactory::getInstance()->getValidLayerTypes(model);
+
+    if (types.empty()) {
+	std::cerr << "WARNING: Document::importLayer: no valid display layer for model" << std::endl;
+	return 0;
+    }
+
+    //!!! for now, just use the first suitable layer type
+    LayerFactory::LayerType type = *types.begin();
+
+    Layer *newLayer = LayerFactory::getInstance()->createLayer(type);
+    if (!newLayer) return 0;
+
+    newLayer->setObjectName(getUniqueLayerName(newLayer->objectName()));
+
+    addImportedModel(model);
+    setModel(newLayer, model);
+
+    //!!! and all channels
+    setChannel(newLayer, -1);
+
+    m_layers.insert(newLayer);
+    emit layerAdded(newLayer);
+    return newLayer;
+}
+
+Layer *
+Document::createEmptyLayer(LayerFactory::LayerType type)
+{
+    Model *newModel =
+	LayerFactory::getInstance()->createEmptyModel(type, m_mainModel);
+    if (!newModel) return 0;
+
+    Layer *newLayer = createLayer(type);
+    if (!newLayer) {
+	delete newModel;
+	return 0;
+    }
+
+    addImportedModel(newModel);
+    setModel(newLayer, newModel);
+
+    return newLayer;
+}
+
+Layer *
+Document::createDerivedLayer(LayerFactory::LayerType type,
+			     TransformName transform)
+{
+    Layer *newLayer = createLayer(type);
+    if (!newLayer) return 0;
+
+    newLayer->setObjectName(getUniqueLayerName
+                            (TransformFactory::getInstance()->
+                             getTransformFriendlyName(transform)));
+
+    return newLayer;
+}
+
+Layer *
+Document::createDerivedLayer(TransformName transform,
+                             Model *inputModel, 
+                             int channel,
+                             QString configurationXml)
+{
+    Model *newModel = createModelForTransform(transform, inputModel,
+                                              channel, configurationXml);
+    if (!newModel) {
+        // error already printed to stderr by createModelForTransform
+        emit modelGenerationFailed(transform);
+        return 0;
+    }
+
+    LayerFactory::LayerTypeSet types =
+	LayerFactory::getInstance()->getValidLayerTypes(newModel);
+
+    if (types.empty()) {
+	std::cerr << "WARNING: Document::createLayerForTransform: no valid display layer for output of transform " << transform.toStdString() << std::endl;
+	delete newModel;
+	return 0;
+    }
+
+    //!!! for now, just use the first suitable layer type
+
+    Layer *newLayer = createLayer(*types.begin());
+    setModel(newLayer, newModel);
+
+    //!!! We need to clone the model when adding the layer, so that it
+    //can be edited without affecting other layers that are based on
+    //the same model.  Unfortunately we can't just clone it now,
+    //because it probably hasn't been completed yet -- the transform
+    //runs in the background.  Maybe the transform has to handle
+    //cloning and cacheing models itself.
+    //
+    // Once we do clone models here, of course, we'll have to avoid
+    // leaking them too.
+    //
+    // We want the user to be able to add a model to a second layer
+    // _while it's still being calculated in the first_ and have it
+    // work quickly.  That means we need to put the same physical
+    // model pointer in both layers, so they can't actually be cloned.
+    
+    if (newLayer) {
+	newLayer->setObjectName(getUniqueLayerName
+                                (TransformFactory::getInstance()->
+                                 getTransformFriendlyName(transform)));
+    }
+
+    emit layerAdded(newLayer);
+    return newLayer;
+}
+
+void
+Document::setMainModel(WaveFileModel *model)
+{
+    Model *oldMainModel = m_mainModel;
+    m_mainModel = model;
+
+    emit modelAdded(m_mainModel);
+
+    std::vector<Layer *> obsoleteLayers;
+    std::set<QString> failedTransforms;
+
+    // We need to ensure that no layer is left using oldMainModel or
+    // any of the old derived models as its model.  Either replace the
+    // model, or delete the layer for each layer that is currently
+    // using one of these.  Carry out this replacement before we
+    // delete any of the models.
+
+    for (LayerSet::iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+
+	Layer *layer = *i;
+	Model *model = layer->getModel();
+
+	if (model == oldMainModel) {
+	    LayerFactory::getInstance()->setModel(layer, m_mainModel);
+	    continue;
+	}
+
+	if (m_models.find(model) == m_models.end()) {
+	    std::cerr << "WARNING: Document::setMainModel: Unknown model "
+		      << model << " in layer " << layer << std::endl;
+	    // get rid of this hideous degenerate
+	    obsoleteLayers.push_back(layer);
+	    continue;
+	}
+	    
+	if (m_models[model].source == oldMainModel) {
+
+	    // This model was derived from the previous main
+	    // model: regenerate it.
+	    
+	    TransformName transform = m_models[model].transform;
+            int channel = m_models[model].channel;
+	    
+	    Model *replacementModel =
+                createModelForTransform(transform,
+                                        m_mainModel,
+                                        channel,
+                                        m_models[model].configurationXml);
+	    
+	    if (!replacementModel) {
+		std::cerr << "WARNING: Document::setMainModel: Failed to regenerate model for transform \""
+			  << transform.toStdString() << "\"" << " in layer " << layer << std::endl;
+                if (failedTransforms.find(transform) == failedTransforms.end()) {
+                    emit modelRegenerationFailed(layer->objectName(),
+                                                 transform);
+                    failedTransforms.insert(transform);
+                }
+		obsoleteLayers.push_back(layer);
+	    } else {
+		setModel(layer, replacementModel);
+	    }
+	}	    
+    }
+
+    for (size_t k = 0; k < obsoleteLayers.size(); ++k) {
+	deleteLayer(obsoleteLayers[k], true);
+    }
+
+    emit mainModelChanged(m_mainModel);
+
+    // we already emitted modelAboutToBeDeleted for this
+    delete oldMainModel;
+}
+
+void
+Document::addDerivedModel(TransformName transform,
+                          Model *inputModel,
+                          int channel,
+                          Model *outputModelToAdd,
+                          QString configurationXml)
+{
+    if (m_models.find(outputModelToAdd) != m_models.end()) {
+	std::cerr << "WARNING: Document::addDerivedModel: Model already added"
+		  << std::endl;
+	return;
+    }
+
+    ModelRecord rec;
+    rec.source = inputModel;
+    rec.transform = transform;
+    rec.channel = channel;
+    rec.configurationXml = configurationXml;
+    rec.refcount = 0;
+
+    m_models[outputModelToAdd] = rec;
+
+    emit modelAdded(outputModelToAdd);
+}
+
+
+void
+Document::addImportedModel(Model *model)
+{
+    if (m_models.find(model) != m_models.end()) {
+	std::cerr << "WARNING: Document::addImportedModel: Model already added"
+		  << std::endl;
+	return;
+    }
+
+    ModelRecord rec;
+    rec.source = 0;
+    rec.transform = "";
+    rec.channel = -1;
+    rec.refcount = 0;
+
+    m_models[model] = rec;
+
+    emit modelAdded(model);
+}
+
+Model *
+Document::createModelForTransform(TransformName transform,
+                                  Model *inputModel,
+                                  int channel,
+                                  QString configurationXml)
+{
+    Model *model = 0;
+
+    for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
+	if (i->second.transform == transform &&
+	    i->second.source == inputModel && 
+            i->second.channel == channel &&
+            i->second.configurationXml == configurationXml) {
+	    return i->first;
+	}
+    }
+
+    model = TransformFactory::getInstance()->transform
+	(transform, inputModel, channel, configurationXml);
+
+    if (!model) {
+	std::cerr << "WARNING: Document::createModelForTransform: no output model for transform " << transform.toStdString() << std::endl;
+    } else {
+	addDerivedModel(transform, inputModel, channel, model, configurationXml);
+    }
+
+    return model;
+}
+
+void
+Document::releaseModel(Model *model) // Will _not_ release main model!
+{
+    if (model == 0) {
+	return;
+    }
+
+    if (model == m_mainModel) {
+	return;
+    }
+
+    bool toDelete = false;
+
+    if (m_models.find(model) != m_models.end()) {
+	
+	if (m_models[model].refcount == 0) {
+	    std::cerr << "WARNING: Document::releaseModel: model " << model
+		      << " reference count is zero already!" << std::endl;
+	} else {
+	    if (--m_models[model].refcount == 0) {
+		toDelete = true;
+	    }
+	}
+    } else { 
+	std::cerr << "WARNING: Document::releaseModel: Unfound model "
+		  << model << std::endl;
+	toDelete = true;
+    }
+
+    if (toDelete) {
+
+	int sourceCount = 0;
+
+	for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
+	    if (i->second.source == model) {
+		++sourceCount;
+		i->second.source = 0;
+	    }
+	}
+
+	if (sourceCount > 0) {
+	    std::cerr << "Document::releaseModel: Deleting model "
+		      << model << " even though it is source for "
+		      << sourceCount << " other derived model(s) -- resetting "
+		      << "their source fields appropriately" << std::endl;
+	}
+
+	emit modelAboutToBeDeleted(model);
+	m_models.erase(model);
+	delete model;
+    }
+}
+
+void
+Document::deleteLayer(Layer *layer, bool force)
+{
+    if (m_layerViewMap.find(layer) != m_layerViewMap.end() &&
+	m_layerViewMap[layer].size() > 0) {
+
+	std::cerr << "WARNING: Document::deleteLayer: Layer "
+		  << layer << " [" << layer->objectName().toStdString() << "]"
+		  << " is still used in " << m_layerViewMap[layer].size()
+		  << " views!" << std::endl;
+
+	if (force) {
+
+	    std::cerr << "(force flag set -- deleting from all views)" << std::endl;
+
+	    for (std::set<View *>::iterator j = m_layerViewMap[layer].begin();
+		 j != m_layerViewMap[layer].end(); ++j) {
+		// don't use removeLayerFromView, as it issues a command
+		layer->setLayerDormant(*j, true);
+		(*j)->removeLayer(layer);
+	    }
+	    
+	    m_layerViewMap.erase(layer);
+
+	} else {
+	    return;
+	}
+    }
+
+    if (m_layers.find(layer) == m_layers.end()) {
+	std::cerr << "Document::deleteLayer: Layer "
+		  << layer << " does not exist, or has already been deleted "
+		  << "(this may not be as serious as it sounds)" << std::endl;
+	return;
+    }
+
+    m_layers.erase(layer);
+
+    releaseModel(layer->getModel());
+    emit layerRemoved(layer);
+    emit layerAboutToBeDeleted(layer);
+    delete layer;
+}
+
+void
+Document::setModel(Layer *layer, Model *model)
+{
+    if (model && 
+	model != m_mainModel &&
+	m_models.find(model) == m_models.end()) {
+	std::cerr << "ERROR: Document::setModel: Layer " << layer
+		  << " is using unregistered model " << model
+		  << ": register the layer's model before setting it!"
+		  << std::endl;
+	return;
+    }
+
+    if (layer->getModel()) {
+	if (layer->getModel() == model) {
+	    std::cerr << "WARNING: Document::setModel: Layer is already set to this model" << std::endl;
+	    return;
+	}
+	releaseModel(layer->getModel());
+    }
+
+    if (model && model != m_mainModel) {
+	m_models[model].refcount ++;
+    }
+
+    LayerFactory::getInstance()->setModel(layer, model);
+}
+
+void
+Document::setChannel(Layer *layer, int channel)
+{
+    LayerFactory::getInstance()->setChannel(layer, channel);
+}
+
+void
+Document::addLayerToView(View *view, Layer *layer)
+{
+    Model *model = layer->getModel();
+    if (!model) {
+	std::cerr << "Document::addLayerToView: Layer with no model being added to view: normally you want to set the model first" << std::endl;
+    } else {
+	if (model != m_mainModel &&
+	    m_models.find(model) == m_models.end()) {
+	    std::cerr << "ERROR: Document::addLayerToView: Layer " << layer
+		      << " has unregistered model " << model
+		      << " -- register the layer's model before adding the layer!" << std::endl;
+	    return;
+	}
+    }
+
+    CommandHistory::getInstance()->addCommand
+	(new Document::AddLayerCommand(this, view, layer));
+}
+
+void
+Document::removeLayerFromView(View *view, Layer *layer)
+{
+    CommandHistory::getInstance()->addCommand
+	(new Document::RemoveLayerCommand(this, view, layer));
+}
+
+void
+Document::addToLayerViewMap(Layer *layer, View *view)
+{
+    bool firstView = (m_layerViewMap.find(layer) == m_layerViewMap.end() ||
+                      m_layerViewMap[layer].empty());
+
+    if (m_layerViewMap[layer].find(view) !=
+	m_layerViewMap[layer].end()) {
+	std::cerr << "WARNING: Document::addToLayerViewMap:"
+		  << " Layer " << layer << " -> view " << view << " already in"
+		  << " layer view map -- internal inconsistency" << std::endl;
+    }
+
+    m_layerViewMap[layer].insert(view);
+
+    if (firstView) emit layerInAView(layer, true);
+}
+    
+void
+Document::removeFromLayerViewMap(Layer *layer, View *view)
+{
+    if (m_layerViewMap[layer].find(view) ==
+	m_layerViewMap[layer].end()) {
+	std::cerr << "WARNING: Document::removeFromLayerViewMap:"
+		  << " Layer " << layer << " -> view " << view << " not in"
+		  << " layer view map -- internal inconsistency" << std::endl;
+    }
+
+    m_layerViewMap[layer].erase(view);
+
+    if (m_layerViewMap[layer].empty()) {
+        m_layerViewMap.erase(layer);
+        emit layerInAView(layer, false);
+    }
+}
+
+QString
+Document::getUniqueLayerName(QString candidate)
+{
+    for (int count = 1; ; ++count) {
+
+        QString adjusted =
+            (count > 1 ? QString("%1 <%2>").arg(candidate).arg(count) :
+             candidate);
+        
+        bool duplicate = false;
+
+        for (LayerSet::iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+            if ((*i)->objectName() == adjusted) {
+                duplicate = true;
+                break;
+            }
+        }
+
+        if (!duplicate) return adjusted;
+    }
+}
+
+Document::AddLayerCommand::AddLayerCommand(Document *d,
+					   View *view,
+					   Layer *layer) :
+    m_d(d),
+    m_view(view),
+    m_layer(layer),
+    m_name(d->tr("Add %1 Layer").arg(layer->objectName())),
+    m_added(false)
+{
+}
+
+Document::AddLayerCommand::~AddLayerCommand()
+{
+//    std::cerr << "Document::AddLayerCommand::~AddLayerCommand" << std::endl;
+    if (!m_added) {
+	m_d->deleteLayer(m_layer);
+    }
+}
+
+void
+Document::AddLayerCommand::execute()
+{
+    for (int i = 0; i < m_view->getLayerCount(); ++i) {
+	if (m_view->getLayer(i) == m_layer) {
+	    // already there
+	    m_layer->setLayerDormant(m_view, false);
+	    m_added = true;
+	    return;
+	}
+    }
+
+    m_view->addLayer(m_layer);
+    m_layer->setLayerDormant(m_view, false);
+
+    m_d->addToLayerViewMap(m_layer, m_view);
+    m_added = true;
+}
+
+void
+Document::AddLayerCommand::unexecute()
+{
+    m_view->removeLayer(m_layer);
+    m_layer->setLayerDormant(m_view, true);
+
+    m_d->removeFromLayerViewMap(m_layer, m_view);
+    m_added = false;
+}
+
+Document::RemoveLayerCommand::RemoveLayerCommand(Document *d,
+						 View *view,
+						 Layer *layer) :
+    m_d(d),
+    m_view(view),
+    m_layer(layer),
+    m_name(d->tr("Delete %1 Layer").arg(layer->objectName())),
+    m_added(true)
+{
+}
+
+Document::RemoveLayerCommand::~RemoveLayerCommand()
+{
+//    std::cerr << "Document::RemoveLayerCommand::~RemoveLayerCommand" << std::endl;
+    if (!m_added) {
+	m_d->deleteLayer(m_layer);
+    }
+}
+
+void
+Document::RemoveLayerCommand::execute()
+{
+    bool have = false;
+    for (int i = 0; i < m_view->getLayerCount(); ++i) {
+	if (m_view->getLayer(i) == m_layer) {
+	    have = true;
+	    break;
+	}
+    }
+
+    if (!have) { // not there!
+	m_layer->setLayerDormant(m_view, true);
+	m_added = false;
+	return;
+    }
+
+    m_view->removeLayer(m_layer);
+    m_layer->setLayerDormant(m_view, true);
+
+    m_d->removeFromLayerViewMap(m_layer, m_view);
+    m_added = false;
+}
+
+void
+Document::RemoveLayerCommand::unexecute()
+{
+    m_view->addLayer(m_layer);
+    m_layer->setLayerDormant(m_view, false);
+
+    m_d->addToLayerViewMap(m_layer, m_view);
+    m_added = true;
+}
+
+void
+Document::toXml(QTextStream &out, QString indent, QString extraAttributes) const
+{
+    out << indent + QString("<data%1%2>\n")
+        .arg(extraAttributes == "" ? "" : " ").arg(extraAttributes);
+
+    if (m_mainModel) {
+	m_mainModel->toXml(out, indent + "  ", "mainModel=\"true\"");
+    }
+
+    for (ModelMap::const_iterator i = m_models.begin();
+	 i != m_models.end(); ++i) {
+
+	i->first->toXml(out, indent + "  ");
+	
+	const ModelRecord &rec = i->second;
+
+	if (rec.source && rec.transform != "") {
+	    
+	    out << indent;
+	    out << QString("  <derivation source=\"%1\" model=\"%2\" channel=\"%3\" transform=\"%4\"")
+		.arg(XmlExportable::getObjectExportId(rec.source))
+		.arg(XmlExportable::getObjectExportId(i->first))
+                .arg(rec.channel)
+		.arg(XmlExportable::encodeEntities(rec.transform));
+
+            if (rec.configurationXml != "") {
+                out << ">\n    " + indent + rec.configurationXml
+                    + "\n" + indent + "  </derivation>\n";
+            } else {
+                out << "/>\n";
+            }
+	}
+
+        //!!! We should probably own the PlayParameterRepository
+        PlayParameters *playParameters =
+            PlayParameterRepository::getInstance()->getPlayParameters(i->first);
+        if (playParameters) {
+            playParameters->toXml
+                (out, indent + "  ",
+                 QString("model=\"%1\"")
+                 .arg(XmlExportable::getObjectExportId(i->first)));
+        }
+    }
+	    
+    for (LayerSet::const_iterator i = m_layers.begin();
+	 i != m_layers.end(); ++i) {
+
+	(*i)->toXml(out, indent + "  ");
+    }
+
+    out << indent + "</data>\n";
+}
+
+QString
+Document::toXmlString(QString indent, QString extraAttributes) const
+{
+    QString s;
+
+    {
+        QTextStream out(&s);
+        toXml(out, indent, extraAttributes);
+    }
+
+    return s;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/document/Document.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,295 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _DOCUMENT_H_
+#define _DOCUMENT_H_
+
+#include "layer/LayerFactory.h"
+#include "transform/Transform.h"
+#include "base/Command.h"
+
+#include <map>
+#include <set>
+
+class Model;
+class Layer;
+class View;
+class WaveFileModel;
+
+/**
+ * A Sonic Visualiser document consists of a set of data models, and
+ * also the visualisation layers used to display them.  Changes to the
+ * layers and their layout need to be stored and managed in much the
+ * same way as changes to the underlying data.
+ * 
+ * The document manages:
+ * 
+ * -- A main data model, which provides the underlying sample rate and
+ * such like.  This must be a wave file model.
+ * 
+ * -- Any number of imported models, which contain data without any
+ * requirement to remember where the data came from or how to
+ * regenerate it.
+ * 
+ * -- Any number of models generated by transforms such as feature
+ * extraction plugins.  For these, we also record the source model and
+ * the name of the transform used to generate the model so that we can
+ * regenerate it (potentially from a different source) on demand.
+ *
+ * -- A flat list of layers.  Elsewhere, the GUI may distribute these
+ * across any number of view widgets.  A layer may be viewable on more
+ * than one view at once, in principle.  A layer refers to one model,
+ * but the same model can be in use in more than one layer.
+ *
+ * The document does *not* manage the existence or structure of Pane
+ * and other view widgets.  However, it does provide convenience
+ * methods for reference-counted command-based management of the
+ * association between layers and views (addLayerToView,
+ * removeLayerFromView).
+ */
+
+class Document : public QObject,
+		 public XmlExportable
+{
+    Q_OBJECT
+
+public:
+    Document();
+    virtual ~Document();
+
+    /**
+     * Create and return a new layer of the given type, associated
+     * with no model.  The caller may set any model on this layer, but
+     * the model must also be registered with the document via the
+     * add-model methods below.
+     */
+    Layer *createLayer(LayerFactory::LayerType);
+
+    /**
+     * Create and return a new layer of the given type, associated
+     * with the current main model (if appropriate to the layer type).
+     */
+    Layer *createMainModelLayer(LayerFactory::LayerType);
+
+    /**
+     * Create and return a new layer associated with the given model,
+     * and register the model as an imported model.
+     */
+    Layer *createImportedLayer(Model *);
+
+    /**
+     * Create and return a new layer of the given type, with an
+     * appropriate empty model.  If the given type is not one for
+     * which an empty model can meaningfully be created, return 0.
+     */
+    Layer *createEmptyLayer(LayerFactory::LayerType);
+
+    /**
+     * Create and return a new layer of the given type, associated
+     * with the given transform name.  This method does not run the
+     * transform itself, nor create a model.  The caller can safely
+     * add a model to the layer later, but note that all models used
+     * by a transform layer _must_ be registered with the document
+     * using addDerivedModel below.
+     */
+    Layer *createDerivedLayer(LayerFactory::LayerType, TransformName);
+
+    /**
+     * Create and return a suitable layer for the given transform,
+     * running the transform and associating the resulting model with
+     * the new layer.
+     */
+    Layer *createDerivedLayer(TransformName,
+                              Model *inputModel, 
+                              int inputChannel, // -1 -> all
+                              QString configurationXml);
+
+    /**
+     * Set the main model (the source for playback sample rate, etc)
+     * to the given wave file model.  This will regenerate any derived
+     * models that were based on the previous main model.
+     */
+    void setMainModel(WaveFileModel *);
+
+    /**
+     * Get the main model (the source for playback sample rate, etc).
+     */
+    WaveFileModel *getMainModel() { return m_mainModel; }
+
+    /**
+     * Add a derived model associated with the given transform name.
+     * This is necessary to register any derived model that was not
+     * created by the document using
+     * e.g. createDerivedLayer(TransformName) above.
+     */
+    void addDerivedModel(TransformName,
+                         Model *inputModel,
+                         int inputChannel, // -1 -> all
+                         Model *outputModelToAdd,
+                         QString configurationXml);
+
+    /**
+     * Add an imported (non-derived, non-main) model.  This is
+     * necessary to register any imported model that is associated
+     * with a layer.
+     */
+    void addImportedModel(Model *);
+
+    /**
+     * Associate the given model with the given layer.  The model must
+     * have already been registered using one of the addXXModel
+     * methods above.
+     */
+    void setModel(Layer *, Model *);
+
+    /**
+     * Set the given layer to use the given channel of its model (-1
+     * means all available channels).
+     */
+    void setChannel(Layer *, int);
+
+    /**
+     * Add the given layer to the given view.  If the layer is
+     * intended to show a particular model, the model should normally
+     * be set using setModel before this method is called.
+     */
+    void addLayerToView(View *, Layer *);
+
+    /**
+     * Remove the given layer from the given view.
+     */
+    void removeLayerFromView(View *, Layer *);
+
+    void toXml(QTextStream &, QString indent, QString extraAttributes) const;
+    QString toXmlString(QString indent, QString extraAttributes) const;
+
+signals:
+    void layerAdded(Layer *);
+    void layerRemoved(Layer *);
+    void layerAboutToBeDeleted(Layer *);
+
+    // Emitted when a layer is first added to a view, or when it is
+    // last removed from a view
+    void layerInAView(Layer *, bool);
+
+    void modelAdded(Model *);
+    void mainModelChanged(WaveFileModel *); // emitted after modelAdded
+    void modelAboutToBeDeleted(Model *);
+
+    void modelGenerationFailed(QString transformName);
+    void modelRegenerationFailed(QString layerName, QString transformName);
+
+protected:
+    Model *createModelForTransform(TransformName transform,
+                                   Model *inputModel,
+                                   int channel,
+                                   QString configurationXml);
+    void releaseModel(Model *model);
+
+    /**
+     * Delete the given layer, and also its associated model if no
+     * longer used by any other layer.  In general, this should be the
+     * only method used to delete layers -- doing so directly is a bit
+     * of a social gaffe.
+     */
+    void deleteLayer(Layer *, bool force = false);
+
+    /*
+     * Every model that is in use by a layer in the document must be
+     * found in either m_mainModel, m_derivedModels or
+     * m_importedModels.  We own and control the lifespan of all of
+     * these models.
+     */
+
+    /**
+     * The model that provides the underlying sample rate, etc.  This
+     * model is not reference counted for layers, and is not freed
+     * unless it is replaced or the document is deleted.
+     */
+    WaveFileModel *m_mainModel;
+
+    struct ModelRecord
+    {
+	// Information associated with a non-main model.  If this
+	// model is derived from another, then source will be non-NULL
+	// and the transform name will be set appropriately.  If the
+	// transform name is set but source is NULL, then there was a
+	// transform involved but the (target) model has been modified
+	// since being generated from it.
+	const Model *source;
+	TransformName transform;
+        int channel;
+        QString configurationXml;
+
+	// Count of the number of layers using this model.
+	int refcount;
+    };
+
+    typedef std::map<Model *, ModelRecord> ModelMap;
+    ModelMap m_models;
+
+    class AddLayerCommand : public Command
+    {
+    public:
+	AddLayerCommand(Document *d, View *view, Layer *layer);
+	virtual ~AddLayerCommand();
+	
+	virtual void execute();
+	virtual void unexecute();
+	virtual QString getName() const { return m_name; }
+
+    protected:
+	Document *m_d;
+	View *m_view; // I don't own this
+	Layer *m_layer; // Document owns this, but I determine its lifespans
+	QString m_name;
+	bool m_added;
+    };
+
+    class RemoveLayerCommand : public Command
+    {
+    public:
+	RemoveLayerCommand(Document *d, View *view, Layer *layer);
+	virtual ~RemoveLayerCommand();
+	
+	virtual void execute();
+	virtual void unexecute();
+	virtual QString getName() const { return m_name; }
+
+    protected:
+	Document *m_d;
+	View *m_view; // I don't own this
+	Layer *m_layer; // Document owns this, but I determine its lifespan
+	QString m_name;
+	bool m_added;
+    };
+
+    typedef std::map<Layer *, std::set<View *> > LayerViewMap;
+    LayerViewMap m_layerViewMap;
+
+    void addToLayerViewMap(Layer *, View *);
+    void removeFromLayerViewMap(Layer *, View *);
+
+    QString getUniqueLayerName(QString candidate);
+    
+    /**
+     * And these are the layers.  We also control the lifespans of
+     * these (usually through the commands used to add and remove them).
+     */
+    typedef std::set<Layer *> LayerSet;
+    LayerSet m_layers;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/document/SVFileReader.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,1005 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "SVFileReader.h"
+
+#include "base/Layer.h"
+#include "base/View.h"
+#include "base/PlayParameters.h"
+#include "base/PlayParameterRepository.h"
+
+#include "AudioFileReaderFactory.h"
+
+#include "model/WaveFileModel.h"
+#include "model/DenseThreeDimensionalModel.h"
+#include "model/SparseOneDimensionalModel.h"
+#include "model/SparseTimeValueModel.h"
+#include "model/NoteModel.h"
+#include "model/TextModel.h"
+
+#include "widgets/Pane.h"
+
+#include "main/Document.h"
+
+#include <QString>
+#include <QMessageBox>
+#include <QFileDialog>
+
+#include <iostream>
+
+SVFileReader::SVFileReader(Document *document,
+			   SVFileReaderPaneCallback &callback) :
+    m_document(document),
+    m_paneCallback(callback),
+    m_currentPane(0),
+    m_currentDataset(0),
+    m_currentDerivedModel(0),
+    m_currentPlayParameters(0),
+    m_datasetSeparator(" "),
+    m_inRow(false),
+    m_rowNumber(0),
+    m_ok(false)
+{
+}
+
+void
+SVFileReader::parse(const QString &xmlData)
+{
+    QXmlInputSource inputSource;
+    inputSource.setData(xmlData);
+    parse(inputSource);
+}
+
+void
+SVFileReader::parse(QXmlInputSource &inputSource)
+{
+    QXmlSimpleReader reader;
+    reader.setContentHandler(this);
+    reader.setErrorHandler(this);
+    m_ok = reader.parse(inputSource);
+}    
+
+bool
+SVFileReader::isOK()
+{
+    return m_ok;
+}
+	
+SVFileReader::~SVFileReader()
+{
+    if (!m_awaitingDatasets.empty()) {
+	std::cerr << "WARNING: SV-XML: File ended with "
+		  << m_awaitingDatasets.size() << " unfilled model dataset(s)"
+		  << std::endl;
+    }
+
+    std::set<Model *> unaddedModels;
+
+    for (std::map<int, Model *>::iterator i = m_models.begin();
+	 i != m_models.end(); ++i) {
+	if (m_addedModels.find(i->second) == m_addedModels.end()) {
+	    unaddedModels.insert(i->second);
+	}
+    }
+
+    if (!unaddedModels.empty()) {
+	std::cerr << "WARNING: SV-XML: File contained "
+		  << unaddedModels.size() << " unused models"
+		  << std::endl;
+	while (!unaddedModels.empty()) {
+	    delete *unaddedModels.begin();
+	    unaddedModels.erase(unaddedModels.begin());
+	}
+    }	
+}
+
+bool
+SVFileReader::startElement(const QString &, const QString &,
+			   const QString &qName,
+			   const QXmlAttributes &attributes)
+{
+    QString name = qName.toLower();
+
+    bool ok = false;
+
+    // Valid element names:
+    //
+    // sv
+    // data
+    // dataset
+    // display
+    // derivation
+    // playparameters
+    // layer
+    // model
+    // point
+    // row
+    // view
+    // window
+
+    if (name == "sv") {
+
+	// nothing needed
+	ok = true;
+
+    } else if (name == "data") {
+
+	// nothing needed
+	m_inData = true;
+	ok = true;
+
+    } else if (name == "display") {
+
+	// nothing needed
+	ok = true;
+
+    } else if (name == "window") {
+
+	ok = readWindow(attributes);
+
+    } else if (name == "model") {
+
+	ok = readModel(attributes);
+    
+    } else if (name == "dataset") {
+	
+	ok = readDatasetStart(attributes);
+
+    } else if (name == "bin") {
+	
+	ok = addBinToDataset(attributes);
+    
+    } else if (name == "point") {
+	
+	ok = addPointToDataset(attributes);
+
+    } else if (name == "row") {
+
+	ok = addRowToDataset(attributes);
+
+    } else if (name == "layer") {
+
+        addUnaddedModels(); // all models must be specified before first layer
+	ok = readLayer(attributes);
+
+    } else if (name == "view") {
+
+	m_inView = true;
+	ok = readView(attributes);
+
+    } else if (name == "derivation") {
+
+	ok = readDerivation(attributes);
+
+    } else if (name == "playparameters") {
+        
+        ok = readPlayParameters(attributes);
+
+    } else if (name == "plugin") {
+
+	ok = readPlugin(attributes);
+
+    } else if (name == "selections") {
+
+	m_inSelections = true;
+	ok = true;
+
+    } else if (name == "selection") {
+
+	ok = readSelection(attributes);
+    }
+
+    if (!ok) {
+	std::cerr << "WARNING: SV-XML: Failed to completely process element \""
+		  << name.toLocal8Bit().data() << "\"" << std::endl;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::characters(const QString &text)
+{
+    bool ok = false;
+
+    if (m_inRow) {
+	ok = readRowData(text);
+	if (!ok) {
+	    std::cerr << "WARNING: SV-XML: Failed to read row data content for row " << m_rowNumber << std::endl;
+	}
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::endElement(const QString &, const QString &,
+			 const QString &qName)
+{
+    QString name = qName.toLower();
+
+    if (name == "dataset") {
+
+	if (m_currentDataset) {
+	    
+	    bool foundInAwaiting = false;
+
+	    for (std::map<int, int>::iterator i = m_awaitingDatasets.begin();
+		 i != m_awaitingDatasets.end(); ++i) {
+		if (m_models[i->second] == m_currentDataset) {
+		    m_awaitingDatasets.erase(i);
+		    foundInAwaiting = true;
+		    break;
+		}
+	    }
+
+	    if (!foundInAwaiting) {
+		std::cerr << "WARNING: SV-XML: Dataset precedes model, or no model uses dataset" << std::endl;
+	    }
+	}
+
+	m_currentDataset = 0;
+
+    } else if (name == "data") {
+
+        addUnaddedModels();
+	m_inData = false;
+
+    } else if (name == "derivation") {
+        
+        if (m_currentDerivedModel) {
+            m_document->addDerivedModel(m_currentTransform,
+                                        m_document->getMainModel(), //!!!
+                                        m_currentTransformChannel,
+                                        m_currentDerivedModel,
+                                        m_currentTransformConfiguration);
+            m_addedModels.insert(m_currentDerivedModel);
+            m_currentDerivedModel = 0;
+            m_currentTransform = "";
+            m_currentTransformConfiguration = "";
+        }
+
+    } else if (name == "row") {
+	m_inRow = false;
+    } else if (name == "view") {
+	m_inView = false;
+    } else if (name == "selections") {
+	m_inSelections = false;
+    } else if (name == "playparameters") {
+        m_currentPlayParameters = 0;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::error(const QXmlParseException &exception)
+{
+    m_errorString =
+	QString("ERROR: SV-XML: %1 at line %2, column %3")
+	.arg(exception.message())
+	.arg(exception.lineNumber())
+	.arg(exception.columnNumber());
+    std::cerr << m_errorString.toLocal8Bit().data() << std::endl;
+    return QXmlDefaultHandler::error(exception);
+}
+
+bool
+SVFileReader::fatalError(const QXmlParseException &exception)
+{
+    m_errorString =
+	QString("FATAL ERROR: SV-XML: %1 at line %2, column %3")
+	.arg(exception.message())
+	.arg(exception.lineNumber())
+	.arg(exception.columnNumber());
+    std::cerr << m_errorString.toLocal8Bit().data() << std::endl;
+    return QXmlDefaultHandler::fatalError(exception);
+}
+
+
+#define READ_MANDATORY(TYPE, NAME, CONVERSION)		      \
+    TYPE NAME = attributes.value(#NAME).trimmed().CONVERSION(&ok); \
+    if (!ok) { \
+	std::cerr << "WARNING: SV-XML: Missing or invalid mandatory " #TYPE " attribute \"" #NAME "\"" << std::endl; \
+	return false; \
+    }
+
+bool
+SVFileReader::readWindow(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, width, toInt);
+    READ_MANDATORY(int, height, toInt);
+
+    m_paneCallback.setWindowSize(width, height);
+    return true;
+}
+
+void
+SVFileReader::addUnaddedModels()
+{
+    std::set<Model *> unaddedModels;
+    
+    for (std::map<int, Model *>::iterator i = m_models.begin();
+         i != m_models.end(); ++i) {
+        if (m_addedModels.find(i->second) == m_addedModels.end()) {
+            unaddedModels.insert(i->second);
+        }
+    }
+    
+    for (std::set<Model *>::iterator i = unaddedModels.begin();
+         i != unaddedModels.end(); ++i) {
+        m_document->addImportedModel(*i);
+        m_addedModels.insert(*i);
+    }
+}
+
+bool
+SVFileReader::readModel(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, id, toInt);
+
+    if (m_models.find(id) != m_models.end()) {
+	std::cerr << "WARNING: SV-XML: Ignoring duplicate model id " << id
+		  << std::endl;
+	return false;
+    }
+
+    QString name = attributes.value("name");
+
+    READ_MANDATORY(int, sampleRate, toInt);
+
+    QString type = attributes.value("type").trimmed();
+    bool mainModel = (attributes.value("mainModel").trimmed() == "true");
+    
+    if (type == "wavefile") {
+	
+	QString file = attributes.value("file");
+	WaveFileModel *model = new WaveFileModel(file);
+
+	while (!model->isOK()) {
+
+	    delete model;
+	    model = 0;
+
+	    if (QMessageBox::question(0,
+				      QMessageBox::tr("Failed to open file"),
+				      QMessageBox::tr("Audio file \"%1\" could not be opened.\nLocate it?").arg(file),
+				      QMessageBox::Ok,
+				      QMessageBox::Cancel) == QMessageBox::Ok) {
+
+		QString path = QFileDialog::getOpenFileName
+		    (0, QFileDialog::tr("Locate file \"%1\"").arg(QFileInfo(file).fileName()), file,
+		     QFileDialog::tr("Audio files (%1)\nAll files (*.*)")
+		     .arg(AudioFileReaderFactory::getKnownExtensions()));
+
+		if (path != "") {
+		    model = new WaveFileModel(path);
+		} else {
+		    return false;
+		}
+	    } else {
+		return false;
+	    }
+	}
+
+	m_models[id] = model;
+	if (mainModel) {
+	    m_document->setMainModel(model);
+	    m_addedModels.insert(model);
+	}
+	// Derived models will be added when their derivation
+	// is found.
+
+	return true;
+
+    } else if (type == "dense") {
+	
+	READ_MANDATORY(int, dimensions, toInt);
+		    
+	// Currently the only dense model we support here
+	// is the dense 3d model.  Dense time-value models
+	// are always file-backed waveform data, at this
+	// point, and they come in as the wavefile model
+	// type above.
+	
+	if (dimensions == 3) {
+	    
+	    READ_MANDATORY(int, windowSize, toInt);
+	    READ_MANDATORY(int, yBinCount, toInt);
+	    
+	    DenseThreeDimensionalModel *model =
+		new DenseThreeDimensionalModel(sampleRate, windowSize, yBinCount);
+	    
+	    float minimum = attributes.value("minimum").trimmed().toFloat(&ok);
+	    if (ok) model->setMinimumLevel(minimum);
+	    
+	    float maximum = attributes.value("maximum").trimmed().toFloat(&ok);
+	    if (ok) model->setMaximumLevel(maximum);
+
+	    int dataset = attributes.value("dataset").trimmed().toInt(&ok);
+	    if (ok) m_awaitingDatasets[dataset] = id;
+
+	    m_models[id] = model;
+	    return true;
+
+	} else {
+
+	    std::cerr << "WARNING: SV-XML: Unexpected dense model dimension ("
+		      << dimensions << ")" << std::endl;
+	}
+    } else if (type == "sparse") {
+
+	READ_MANDATORY(int, dimensions, toInt);
+		  
+	if (dimensions == 1) {
+	    
+	    READ_MANDATORY(int, resolution, toInt);
+
+	    SparseOneDimensionalModel *model = new SparseOneDimensionalModel
+		(sampleRate, resolution);
+	    m_models[id] = model;
+
+	    int dataset = attributes.value("dataset").trimmed().toInt(&ok);
+	    if (ok) m_awaitingDatasets[dataset] = id;
+
+	    return true;
+
+	} else if (dimensions == 2 || dimensions == 3) {
+	    
+	    READ_MANDATORY(int, resolution, toInt);
+
+	    float minimum = attributes.value("minimum").trimmed().toFloat(&ok);
+	    float maximum = attributes.value("maximum").trimmed().toFloat(&ok);
+	    float valueQuantization =
+		attributes.value("valueQuantization").trimmed().toFloat(&ok);
+
+	    bool notifyOnAdd = (attributes.value("notifyOnAdd") == "true");
+
+            QString units = attributes.value("units");
+
+	    if (dimensions == 2) {
+		if (attributes.value("subtype") == "text") {
+		    TextModel *model = new TextModel
+			(sampleRate, resolution, notifyOnAdd);
+		    m_models[id] = model;
+		} else {
+		    SparseTimeValueModel *model = new SparseTimeValueModel
+			(sampleRate, resolution, minimum, maximum, notifyOnAdd);
+                    model->setScaleUnits(units);
+		    m_models[id] = model;
+		}
+	    } else {
+		NoteModel *model = new NoteModel
+		    (sampleRate, resolution, minimum, maximum, notifyOnAdd);
+		model->setValueQuantization(valueQuantization);
+                model->setScaleUnits(units);
+		m_models[id] = model;
+	    }
+
+	    int dataset = attributes.value("dataset").trimmed().toInt(&ok);
+	    if (ok) m_awaitingDatasets[dataset] = id;
+
+	    return true;
+
+	} else {
+
+	    std::cerr << "WARNING: SV-XML: Unexpected sparse model dimension ("
+		      << dimensions << ")" << std::endl;
+	}
+    } else {
+
+	std::cerr << "WARNING: SV-XML: Unexpected model type \""
+		  << type.toLocal8Bit().data() << "\" for model id" << id << std::endl;
+    }
+
+    return false;
+}
+
+bool
+SVFileReader::readView(const QXmlAttributes &attributes)
+{
+    QString type = attributes.value("type");
+    m_currentPane = 0;
+    
+    if (type != "pane") {
+	std::cerr << "WARNING: SV-XML: Unexpected view type \""
+		  << type.toLocal8Bit().data() << "\"" << std::endl;
+	return false;
+    }
+
+    m_currentPane = m_paneCallback.addPane();
+
+    if (!m_currentPane) {
+	std::cerr << "WARNING: SV-XML: Internal error: Failed to add pane!"
+		  << std::endl;
+	return false;
+    }
+
+    bool ok = false;
+
+    View *view = m_currentPane;
+
+    // The view properties first
+
+    READ_MANDATORY(size_t, centre, toUInt);
+    READ_MANDATORY(size_t, zoom, toUInt);
+    READ_MANDATORY(int, followPan, toInt);
+    READ_MANDATORY(int, followZoom, toInt);
+    READ_MANDATORY(int, light, toInt);
+    QString tracking = attributes.value("tracking");
+
+    // Specify the follow modes before we set the actual values
+    view->setFollowGlobalPan(followPan);
+    view->setFollowGlobalZoom(followZoom);
+    view->setPlaybackFollow(tracking == "scroll" ? View::PlaybackScrollContinuous :
+			    tracking == "page" ? View::PlaybackScrollPage
+			    : View::PlaybackIgnore);
+
+    // Then set these values
+    view->setCentreFrame(centre);
+    view->setZoomLevel(zoom);
+    view->setLightBackground(light);
+
+    // And pane properties
+    READ_MANDATORY(int, centreLineVisible, toInt);
+    m_currentPane->setCentreLineVisible(centreLineVisible);
+
+    int height = attributes.value("height").toInt(&ok);
+    if (ok) {
+	m_currentPane->resize(m_currentPane->width(), height);
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readLayer(const QXmlAttributes &attributes)
+{
+    QString type = attributes.value("type");
+
+    int id;
+    bool ok = false;
+    id = attributes.value("id").trimmed().toInt(&ok);
+
+    if (!ok) {
+	std::cerr << "WARNING: SV-XML: No layer id for layer of type \""
+		  << type.toLocal8Bit().data()
+		  << "\"" << std::endl;
+	return false;
+    }
+
+    Layer *layer = 0;
+    bool isNewLayer = false;
+
+    // Layers are expected to be defined in layer elements in the data
+    // section, and referred to in layer elements in the view
+    // sections.  So if we're in the data section, we expect this
+    // layer not to exist already; if we're in the view section, we
+    // expect it to exist.
+
+    if (m_inData) {
+
+	if (m_layers.find(id) != m_layers.end()) {
+	    std::cerr << "WARNING: SV-XML: Ignoring duplicate layer id " << id
+		      << " in data section" << std::endl;
+	    return false;
+	}
+
+	layer = m_layers[id] = m_document->createLayer
+	    (LayerFactory::getInstance()->getLayerTypeForName(type));
+
+	if (layer) {
+	    m_layers[id] = layer;
+	    isNewLayer = true;
+	}
+
+    } else {
+
+	if (!m_currentPane) {
+	    std::cerr << "WARNING: SV-XML: No current pane for layer " << id
+		      << " in view section" << std::endl;
+	    return false;
+	}
+
+	if (m_layers.find(id) != m_layers.end()) {
+	    
+	    layer = m_layers[id];
+	
+	} else {
+	    std::cerr << "WARNING: SV-XML: Layer id " << id 
+		      << " in view section has not been defined -- defining it here"
+		      << std::endl;
+
+	    layer = m_document->createLayer
+		(LayerFactory::getInstance()->getLayerTypeForName(type));
+
+	    if (layer) {
+		m_layers[id] = layer;
+		isNewLayer = true;
+	    }
+	}
+    }
+	    
+    if (!layer) {
+	std::cerr << "WARNING: SV-XML: Failed to add layer of type \""
+		  << type.toLocal8Bit().data()
+		  << "\"" << std::endl;
+	return false;
+    }
+
+    if (isNewLayer) {
+
+	QString name = attributes.value("name");
+	layer->setObjectName(name);
+
+	int modelId;
+	bool modelOk = false;
+	modelId = attributes.value("model").trimmed().toInt(&modelOk);
+
+	if (modelOk) {
+	    if (m_models.find(modelId) != m_models.end()) {
+		Model *model = m_models[modelId];
+		m_document->setModel(layer, model);
+	    } else {
+		std::cerr << "WARNING: SV-XML: Unknown model id " << modelId
+			  << " in layer definition" << std::endl;
+	    }
+	}
+
+	layer->setProperties(attributes);
+    }
+
+    if (!m_inData && m_currentPane) {
+	m_document->addLayerToView(m_currentPane, layer);
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readDatasetStart(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, id, toInt);
+    READ_MANDATORY(int, dimensions, toInt);
+    
+    if (m_awaitingDatasets.find(id) == m_awaitingDatasets.end()) {
+	std::cerr << "WARNING: SV-XML: Unwanted dataset " << id << std::endl;
+	return false;
+    }
+    
+    int modelId = m_awaitingDatasets[id];
+    
+    Model *model = 0;
+    if (m_models.find(modelId) != m_models.end()) {
+	model = m_models[modelId];
+    } else {
+	std::cerr << "WARNING: SV-XML: Internal error: Unknown model " << modelId
+		  << " expecting dataset " << id << std::endl;
+	return false;
+    }
+
+    bool good = false;
+
+    switch (dimensions) {
+    case 1:
+	if (dynamic_cast<SparseOneDimensionalModel *>(model)) good = true;
+	break;
+
+    case 2:
+	if (dynamic_cast<SparseTimeValueModel *>(model)) good = true;
+	else if (dynamic_cast<TextModel *>(model)) good = true;
+	break;
+
+    case 3:
+	if (dynamic_cast<NoteModel *>(model)) good = true;
+	else if (dynamic_cast<DenseThreeDimensionalModel *>(model)) {
+	    m_datasetSeparator = attributes.value("separator");
+	    good = true;
+	}
+	break;
+    }
+
+    if (!good) {
+	std::cerr << "WARNING: SV-XML: Model id " << modelId << " has wrong number of dimensions for " << dimensions << "-D dataset " << id << std::endl;
+	m_currentDataset = 0;
+	return false;
+    }
+
+    m_currentDataset = model;
+    return true;
+}
+
+bool
+SVFileReader::addPointToDataset(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, frame, toInt);
+
+    SparseOneDimensionalModel *sodm = dynamic_cast<SparseOneDimensionalModel *>
+	(m_currentDataset);
+
+    if (sodm) {
+	QString label = attributes.value("label");
+	sodm->addPoint(SparseOneDimensionalModel::Point(frame, label));
+	return true;
+    }
+
+    SparseTimeValueModel *stvm = dynamic_cast<SparseTimeValueModel *>
+	(m_currentDataset);
+
+    if (stvm) {
+	float value = 0.0;
+	value = attributes.value("value").trimmed().toFloat(&ok);
+	QString label = attributes.value("label");
+	stvm->addPoint(SparseTimeValueModel::Point(frame, value, label));
+	return ok;
+    }
+	
+    NoteModel *nm = dynamic_cast<NoteModel *>(m_currentDataset);
+
+    if (nm) {
+	float value = 0.0;
+	value = attributes.value("value").trimmed().toFloat(&ok);
+	float duration = 0.0;
+	duration = attributes.value("duration").trimmed().toFloat(&ok);
+	QString label = attributes.value("label");
+	nm->addPoint(NoteModel::Point(frame, value, duration, label));
+	return ok;
+    }
+
+    TextModel *tm = dynamic_cast<TextModel *>(m_currentDataset);
+
+    if (tm) {
+	float height = 0.0;
+	height = attributes.value("height").trimmed().toFloat(&ok);
+	QString label = attributes.value("label");
+	tm->addPoint(TextModel::Point(frame, height, label));
+	return ok;
+    }
+
+    std::cerr << "WARNING: SV-XML: Point element found in non-point dataset" << std::endl;
+
+    return false;
+}
+
+bool
+SVFileReader::addBinToDataset(const QXmlAttributes &attributes)
+{
+    DenseThreeDimensionalModel *dtdm = dynamic_cast<DenseThreeDimensionalModel *>
+	(m_currentDataset);
+
+    if (dtdm) {
+
+	bool ok = false;
+	int n = attributes.value("number").trimmed().toInt(&ok);
+	if (!ok) {
+	    std::cerr << "WARNING: SV-XML: Missing or invalid bin number"
+		      << std::endl;
+	    return false;
+	}
+
+	QString name = attributes.value("name");
+
+	dtdm->setBinName(n, name);
+	return true;
+    }
+
+    std::cerr << "WARNING: SV-XML: Bin definition found in incompatible dataset" << std::endl;
+
+    return false;
+}
+
+
+bool
+SVFileReader::addRowToDataset(const QXmlAttributes &attributes)
+{
+    m_inRow = false;
+
+    bool ok = false;
+    m_rowNumber = attributes.value("n").trimmed().toInt(&ok);
+    if (!ok) {
+	std::cerr << "WARNING: SV-XML: Missing or invalid row number"
+		  << std::endl;
+	return false;
+    }
+    
+    m_inRow = true;
+
+//    std::cerr << "SV-XML: In row " << m_rowNumber << std::endl;
+    
+    return true;
+}
+
+bool
+SVFileReader::readRowData(const QString &text)
+{
+    DenseThreeDimensionalModel *dtdm = dynamic_cast<DenseThreeDimensionalModel *>
+	(m_currentDataset);
+
+    bool warned = false;
+
+    if (dtdm) {
+	QStringList data = text.split(m_datasetSeparator);
+
+	DenseThreeDimensionalModel::BinValueSet values;
+
+	for (QStringList::iterator i = data.begin(); i != data.end(); ++i) {
+
+	    if (values.size() == dtdm->getYBinCount()) {
+		if (!warned) {
+		    std::cerr << "WARNING: SV-XML: Too many y-bins in 3-D dataset row "
+			      << m_rowNumber << std::endl;
+		    warned = true;
+		}
+	    }
+
+	    bool ok;
+	    float value = i->toFloat(&ok);
+	    if (!ok) {
+		std::cerr << "WARNING: SV-XML: Bad floating-point value "
+			  << i->toLocal8Bit().data()
+			  << " in row data" << std::endl;
+	    } else {
+		values.push_back(value);
+	    }
+	}
+
+	size_t windowStartFrame = m_rowNumber * dtdm->getWindowSize();
+
+	dtdm->setBinValues(windowStartFrame, values);
+	return true;
+    }
+
+    std::cerr << "WARNING: SV-XML: Row data found in non-row dataset" << std::endl;
+
+    return false;
+}
+
+bool
+SVFileReader::readDerivation(const QXmlAttributes &attributes)
+{
+    int modelId = 0;
+    bool modelOk = false;
+    modelId = attributes.value("model").trimmed().toInt(&modelOk);
+
+    if (!modelOk) {
+	std::cerr << "WARNING: SV-XML: No model id specified for derivation" << std::endl;
+	return false;
+    }
+    
+    QString transform = attributes.value("transform");
+    
+    if (m_models.find(modelId) != m_models.end()) {
+
+        m_currentDerivedModel = m_models[modelId];
+        m_currentTransform = transform;
+        m_currentTransformConfiguration = "";
+
+        bool ok = false;
+        int channel = attributes.value("channel").trimmed().toInt(&ok);
+        if (ok) m_currentTransformChannel = channel;
+        else m_currentTransformChannel = -1;
+
+    } else {
+	std::cerr << "WARNING: SV-XML: Unknown derived model " << modelId
+		  << " for transform \"" << transform.toLocal8Bit().data() << "\""
+		  << std::endl;
+        return false;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readPlayParameters(const QXmlAttributes &attributes)
+{
+    m_currentPlayParameters = 0;
+
+    int modelId = 0;
+    bool modelOk = false;
+    modelId = attributes.value("model").trimmed().toInt(&modelOk);
+
+    if (!modelOk) {
+	std::cerr << "WARNING: SV-XML: No model id specified for play parameters" << std::endl;
+	return false;
+    }
+
+    if (m_models.find(modelId) != m_models.end()) {
+
+        bool ok = false;
+
+        PlayParameters *parameters = PlayParameterRepository::getInstance()->
+            getPlayParameters(m_models[modelId]);
+
+        if (!parameters) {
+            std::cerr << "WARNING: SV-XML: Play parameters for model "
+                      << modelId
+                      << " not found - has model been added to document?"
+                      << std::endl;
+            return false;
+        }
+        
+        bool muted = (attributes.value("mute").trimmed() == "true");
+        if (ok) parameters->setPlayMuted(muted);
+        
+        float pan = attributes.value("pan").toFloat(&ok);
+        if (ok) parameters->setPlayPan(pan);
+        
+        float gain = attributes.value("gain").toFloat(&ok);
+        if (ok) parameters->setPlayGain(gain);
+        
+        QString pluginId = attributes.value("pluginId");
+        if (pluginId != "") parameters->setPlayPluginId(pluginId);
+        
+        m_currentPlayParameters = parameters;
+
+//        std::cerr << "Current play parameters for model: " << m_models[modelId] << ": " << m_currentPlayParameters << std::endl;
+
+    } else {
+
+	std::cerr << "WARNING: SV-XML: Unknown model " << modelId
+		  << " for play parameters" << std::endl;
+        return false;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readPlugin(const QXmlAttributes &attributes)
+{
+    if (!m_currentDerivedModel && !m_currentPlayParameters) {
+        std::cerr << "WARNING: SV-XML: Plugin found outside derivation or play parameters" << std::endl;
+        return false;
+    }
+
+    QString configurationXml = "<plugin";
+    
+    for (int i = 0; i < attributes.length(); ++i) {
+        configurationXml += QString(" %1=\"%2\"")
+            .arg(attributes.qName(i)).arg(attributes.value(i));
+    }
+
+    configurationXml += "/>";
+
+    if (m_currentPlayParameters) {
+        m_currentPlayParameters->setPlayPluginConfiguration(configurationXml);
+    } else {
+        m_currentTransformConfiguration += configurationXml;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readSelection(const QXmlAttributes &attributes)
+{
+    bool ok;
+
+    READ_MANDATORY(int, start, toInt);
+    READ_MANDATORY(int, end, toInt);
+
+    m_paneCallback.addSelection(start, end);
+
+    return true;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/document/SVFileReader.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,108 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _SV_FILE_READER_H_
+#define _SV_FILE_READER_H_
+
+#include "layer/LayerFactory.h"
+#include "transform/Transform.h"
+
+#include <QXmlDefaultHandler>
+
+#include <map>
+
+class Pane;
+class Model;
+class Document;
+class PlayParameters;
+
+class SVFileReaderPaneCallback
+{
+public:
+    virtual Pane *addPane() = 0;
+    virtual void setWindowSize(int width, int height) = 0;
+    virtual void addSelection(int start, int end) = 0;
+};
+
+class SVFileReader : public QXmlDefaultHandler
+{
+public:
+    SVFileReader(Document *document,
+		 SVFileReaderPaneCallback &callback);
+    virtual ~SVFileReader();
+
+    void parse(const QString &xmlData);
+    void parse(QXmlInputSource &source);
+
+    bool isOK();
+    QString getErrorString() const { return m_errorString; }
+
+    // For loading a single layer onto an existing pane
+    void setCurrentPane(Pane *pane) { m_currentPane = pane; }
+    
+    virtual bool startElement(const QString &namespaceURI,
+			      const QString &localName,
+			      const QString &qName,
+			      const QXmlAttributes& atts);
+
+    virtual bool characters(const QString &);
+
+    virtual bool endElement(const QString &namespaceURI,
+			    const QString &localName,
+			    const QString &qName);
+
+    bool error(const QXmlParseException &exception);
+    bool fatalError(const QXmlParseException &exception);
+
+protected:
+    bool readWindow(const QXmlAttributes &);
+    bool readModel(const QXmlAttributes &);
+    bool readView(const QXmlAttributes &);
+    bool readLayer(const QXmlAttributes &);
+    bool readDatasetStart(const QXmlAttributes &);
+    bool addBinToDataset(const QXmlAttributes &);
+    bool addPointToDataset(const QXmlAttributes &);
+    bool addRowToDataset(const QXmlAttributes &);
+    bool readRowData(const QString &);
+    bool readDerivation(const QXmlAttributes &);
+    bool readPlayParameters(const QXmlAttributes &);
+    bool readPlugin(const QXmlAttributes &);
+    bool readSelection(const QXmlAttributes &);
+    void addUnaddedModels();
+
+    Document *m_document;
+    SVFileReaderPaneCallback &m_paneCallback;
+    Pane *m_currentPane;
+    std::map<int, Layer *> m_layers;
+    std::map<int, Model *> m_models;
+    std::set<Model *> m_addedModels;
+    std::map<int, int> m_awaitingDatasets; // map dataset id -> model id
+    Model *m_currentDataset;
+    Model *m_currentDerivedModel;
+    PlayParameters *m_currentPlayParameters;
+    QString m_currentTransform;
+    int m_currentTransformChannel;
+    QString m_currentTransformConfiguration;
+    QString m_datasetSeparator;
+    bool m_inRow;
+    bool m_inView;
+    bool m_inData;
+    bool m_inSelections;
+    int m_rowNumber;
+    QString m_errorString;
+    bool m_ok;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/MainWindow.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,3097 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "../version.h"
+
+#include "MainWindow.h"
+#include "Document.h"
+#include "PreferencesDialog.h"
+
+#include "widgets/Pane.h"
+#include "widgets/PaneStack.h"
+#include "model/WaveFileModel.h"
+#include "model/SparseOneDimensionalModel.h"
+#include "base/ViewManager.h"
+#include "base/Preferences.h"
+#include "layer/WaveformLayer.h"
+#include "layer/TimeRulerLayer.h"
+#include "layer/TimeInstantLayer.h"
+#include "layer/TimeValueLayer.h"
+#include "layer/Colour3DPlotLayer.h"
+#include "widgets/Fader.h"
+#include "widgets/Panner.h"
+#include "widgets/PropertyBox.h"
+#include "widgets/PropertyStack.h"
+#include "widgets/AudioDial.h"
+#include "widgets/LayerTree.h"
+#include "widgets/ListInputDialog.h"
+#include "audioio/AudioCallbackPlaySource.h"
+#include "audioio/AudioCallbackPlayTarget.h"
+#include "audioio/AudioTargetFactory.h"
+#include "fileio/AudioFileReaderFactory.h"
+#include "fileio/DataFileReaderFactory.h"
+#include "fileio/WavFileWriter.h"
+#include "fileio/CSVFileWriter.h"
+#include "fileio/BZipFileDevice.h"
+#include "fileio/RecentFiles.h"
+#include "transform/TransformFactory.h"
+#include "base/PlayParameterRepository.h"
+#include "base/XmlExportable.h"
+#include "base/CommandHistory.h"
+#include "base/Profiler.h"
+#include "base/Clipboard.h"
+
+// For version information
+#include "vamp/vamp.h"
+#include "vamp-sdk/PluginBase.h"
+#include "plugin/api/ladspa.h"
+#include "plugin/api/dssi.h"
+
+#include <QApplication>
+#include <QPushButton>
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QGridLayout>
+#include <QLabel>
+#include <QAction>
+#include <QMenuBar>
+#include <QToolBar>
+#include <QInputDialog>
+#include <QStatusBar>
+#include <QTreeView>
+#include <QFile>
+#include <QTextStream>
+#include <QProcess>
+
+#include <iostream>
+#include <cstdio>
+#include <errno.h>
+
+using std::cerr;
+using std::endl;
+
+
+MainWindow::MainWindow() :
+    m_document(0),
+    m_paneStack(0),
+    m_viewManager(0),
+    m_panner(0),
+    m_timeRulerLayer(0),
+    m_playSource(0),
+    m_playTarget(0),
+    m_mainMenusCreated(false),
+    m_paneMenu(0),
+    m_layerMenu(0),
+    m_existingLayersMenu(0),
+    m_rightButtonMenu(0),
+    m_rightButtonLayerMenu(0),
+    m_documentModified(false),
+    m_preferencesDialog(0)
+{
+    setWindowTitle(tr("Sonic Visualiser"));
+
+    UnitDatabase::getInstance()->registerUnit("Hz");
+    UnitDatabase::getInstance()->registerUnit("dB");
+
+    connect(CommandHistory::getInstance(), SIGNAL(commandExecuted()),
+	    this, SLOT(documentModified()));
+    connect(CommandHistory::getInstance(), SIGNAL(documentRestored()),
+	    this, SLOT(documentRestored()));
+
+    QFrame *frame = new QFrame;
+    setCentralWidget(frame);
+
+    QGridLayout *layout = new QGridLayout;
+    
+    m_viewManager = new ViewManager();
+    connect(m_viewManager, SIGNAL(selectionChanged()),
+	    this, SLOT(updateMenuStates()));
+
+    m_descriptionLabel = new QLabel;
+
+    m_paneStack = new PaneStack(frame, m_viewManager);
+    connect(m_paneStack, SIGNAL(currentPaneChanged(Pane *)),
+	    this, SLOT(currentPaneChanged(Pane *)));
+    connect(m_paneStack, SIGNAL(currentLayerChanged(Pane *, Layer *)),
+	    this, SLOT(currentLayerChanged(Pane *, Layer *)));
+    connect(m_paneStack, SIGNAL(rightButtonMenuRequested(Pane *, QPoint)),
+            this, SLOT(rightButtonMenuRequested(Pane *, QPoint)));
+
+    m_panner = new Panner(frame);
+    m_panner->setViewManager(m_viewManager);
+    m_panner->setFixedHeight(40);
+
+    m_panLayer = new WaveformLayer;
+    m_panLayer->setChannelMode(WaveformLayer::MergeChannels);
+//    m_panLayer->setScale(WaveformLayer::MeterScale);
+    m_panLayer->setAutoNormalize(true);
+    m_panLayer->setBaseColour(Qt::darkGreen);
+    m_panLayer->setAggressiveCacheing(true);
+    m_panner->addLayer(m_panLayer);
+
+    m_playSource = new AudioCallbackPlaySource(m_viewManager);
+
+    connect(m_playSource, SIGNAL(sampleRateMismatch(size_t, size_t, bool)),
+	    this,           SLOT(sampleRateMismatch(size_t, size_t, bool)));
+
+    m_fader = new Fader(frame, false);
+
+    m_playSpeed = new AudioDial(frame);
+    m_playSpeed->setMinimum(1);
+    m_playSpeed->setMaximum(10);
+    m_playSpeed->setValue(10);
+    m_playSpeed->setFixedWidth(24);
+    m_playSpeed->setFixedHeight(24);
+    m_playSpeed->setNotchesVisible(true);
+    m_playSpeed->setPageStep(1);
+    m_playSpeed->setToolTip(tr("Playback speed: Full"));
+    m_playSpeed->setDefaultValue(10);
+    connect(m_playSpeed, SIGNAL(valueChanged(int)),
+	    this, SLOT(playSpeedChanged(int)));
+
+    layout->addWidget(m_paneStack, 0, 0, 1, 3);
+    layout->addWidget(m_panner, 1, 0);
+    layout->addWidget(m_fader, 1, 1);
+    layout->addWidget(m_playSpeed, 1, 2);
+    frame->setLayout(layout);
+
+    connect(m_viewManager, SIGNAL(outputLevelsChanged(float, float)),
+	    this, SLOT(outputLevelsChanged(float, float)));
+
+    connect(Preferences::getInstance(),
+            SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
+            this,
+            SLOT(preferenceChanged(PropertyContainer::PropertyName)));
+
+    setupMenus();
+    setupToolbars();
+
+//    statusBar()->addWidget(m_descriptionLabel);
+
+    newSession();
+}
+
+MainWindow::~MainWindow()
+{
+    closeSession();
+    delete m_playTarget;
+    delete m_playSource;
+    delete m_viewManager;
+    Profiles::getInstance()->dump();
+}
+
+void
+MainWindow::setupMenus()
+{
+    QAction *action = 0;
+    QMenu *menu = 0;
+    QToolBar *toolbar = 0;
+
+    if (!m_mainMenusCreated) {
+        m_rightButtonMenu = new QMenu();
+    }
+
+    if (m_rightButtonLayerMenu) {
+        m_rightButtonLayerMenu->clear();
+    } else {
+        m_rightButtonLayerMenu = m_rightButtonMenu->addMenu(tr("&Layer"));
+        m_rightButtonMenu->addSeparator();
+    }
+
+    if (!m_mainMenusCreated) {
+
+        CommandHistory::getInstance()->registerMenu(m_rightButtonMenu);
+        m_rightButtonMenu->addSeparator();
+
+	menu = menuBar()->addMenu(tr("&File"));
+        toolbar = addToolBar(tr("File Toolbar"));
+
+        QIcon icon(":icons/filenew.png");
+        icon.addFile(":icons/filenew-22.png");
+	action = new QAction(icon, tr("&New Session"), this);
+	action->setShortcut(tr("Ctrl+N"));
+	action->setStatusTip(tr("Clear the current Sonic Visualiser session and start a new one"));
+	connect(action, SIGNAL(triggered()), this, SLOT(newSession()));
+	menu->addAction(action);
+        toolbar->addAction(action);
+	
+        icon = QIcon(":icons/fileopen.png");
+        icon.addFile(":icons/fileopen-22.png");
+
+	action = new QAction(icon, tr("&Open Session..."), this);
+	action->setShortcut(tr("Ctrl+O"));
+	action->setStatusTip(tr("Open a previously saved Sonic Visualiser session file"));
+	connect(action, SIGNAL(triggered()), this, SLOT(openSession()));
+	menu->addAction(action);
+
+	action = new QAction(icon, tr("&Open..."), this);
+	action->setStatusTip(tr("Open a session file, audio file, or layer"));
+	connect(action, SIGNAL(triggered()), this, SLOT(openSomething()));
+        toolbar->addAction(action);
+
+        icon = QIcon(":icons/filesave.png");
+        icon.addFile(":icons/filesave-22.png");
+	action = new QAction(icon, tr("&Save Session"), this);
+	action->setShortcut(tr("Ctrl+S"));
+	action->setStatusTip(tr("Save the current session into a Sonic Visualiser session file"));
+	connect(action, SIGNAL(triggered()), this, SLOT(saveSession()));
+	connect(this, SIGNAL(canSave(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+        toolbar->addAction(action);
+	
+        icon = QIcon(":icons/filesaveas.png");
+        icon.addFile(":icons/filesaveas-22.png");
+	action = new QAction(icon, tr("Save Session &As..."), this);
+	action->setStatusTip(tr("Save the current session into a new Sonic Visualiser session file"));
+	connect(action, SIGNAL(triggered()), this, SLOT(saveSessionAs()));
+	menu->addAction(action);
+        toolbar->addAction(action);
+
+	menu->addSeparator();
+
+	action = new QAction(tr("&Import Audio File..."), this);
+	action->setShortcut(tr("Ctrl+I"));
+	action->setStatusTip(tr("Import an existing audio file"));
+	connect(action, SIGNAL(triggered()), this, SLOT(importAudio()));
+	menu->addAction(action);
+
+	action = new QAction(tr("Import Secondary Audio File..."), this);
+	action->setShortcut(tr("Ctrl+Shift+I"));
+	action->setStatusTip(tr("Import an extra audio file as a separate layer"));
+	connect(action, SIGNAL(triggered()), this, SLOT(importMoreAudio()));
+	connect(this, SIGNAL(canImportMoreAudio(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+
+	action = new QAction(tr("&Export Audio File..."), this);
+	action->setStatusTip(tr("Export selection as an audio file"));
+	connect(action, SIGNAL(triggered()), this, SLOT(exportAudio()));
+	connect(this, SIGNAL(canExportAudio(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+
+	menu->addSeparator();
+
+	action = new QAction(tr("Import Annotation &Layer..."), this);
+	action->setShortcut(tr("Ctrl+L"));
+	action->setStatusTip(tr("Import layer data from an existing file"));
+	connect(action, SIGNAL(triggered()), this, SLOT(importLayer()));
+	connect(this, SIGNAL(canImportLayer(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+
+	action = new QAction(tr("Export Annotation Layer..."), this);
+	action->setStatusTip(tr("Export layer data to a file"));
+	connect(action, SIGNAL(triggered()), this, SLOT(exportLayer()));
+	connect(this, SIGNAL(canExportLayer(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+
+	menu->addSeparator();
+        m_recentFilesMenu = menu->addMenu(tr("&Recent Files"));
+        menu->addMenu(m_recentFilesMenu);
+        setupRecentFilesMenu();
+        connect(RecentFiles::getInstance(), SIGNAL(recentFilesChanged()),
+                this, SLOT(setupRecentFilesMenu()));
+
+	menu->addSeparator();
+	action = new QAction(tr("&Preferences..."), this);
+	action->setStatusTip(tr("Adjust the application preferences"));
+	connect(action, SIGNAL(triggered()), this, SLOT(preferences()));
+	menu->addAction(action);
+	
+	/*!!!
+	menu->addSeparator();
+	
+	action = new QAction(tr("Play / Pause"), this);
+	action->setShortcut(tr("Space"));
+	action->setStatusTip(tr("Start or stop playback from the current position"));
+	connect(action, SIGNAL(triggered()), this, SLOT(play()));
+	menu->addAction(action);
+	*/
+
+	menu->addSeparator();
+	action = new QAction(QIcon(":/icons/exit.png"),
+			     tr("&Quit"), this);
+	action->setShortcut(tr("Ctrl+Q"));
+	connect(action, SIGNAL(triggered()), this, SLOT(close()));
+	menu->addAction(action);
+
+	menu = menuBar()->addMenu(tr("&Edit"));
+	CommandHistory::getInstance()->registerMenu(menu);
+
+	menu->addSeparator();
+
+	action = new QAction(QIcon(":/icons/editcut.png"),
+			     tr("Cu&t"), this);
+	action->setShortcut(tr("Ctrl+X"));
+	connect(action, SIGNAL(triggered()), this, SLOT(cut()));
+	connect(this, SIGNAL(canEditSelection(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+        m_rightButtonMenu->addAction(action);
+
+	action = new QAction(QIcon(":/icons/editcopy.png"),
+			     tr("&Copy"), this);
+	action->setShortcut(tr("Ctrl+C"));
+	connect(action, SIGNAL(triggered()), this, SLOT(copy()));
+	connect(this, SIGNAL(canEditSelection(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+        m_rightButtonMenu->addAction(action);
+
+	action = new QAction(QIcon(":/icons/editpaste.png"),
+			     tr("&Paste"), this);
+	action->setShortcut(tr("Ctrl+V"));
+	connect(action, SIGNAL(triggered()), this, SLOT(paste()));
+	connect(this, SIGNAL(canPaste(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+        m_rightButtonMenu->addAction(action);
+
+	action = new QAction(tr("&Delete Selected Items"), this);
+	action->setShortcut(tr("Del"));
+	connect(action, SIGNAL(triggered()), this, SLOT(deleteSelected()));
+	connect(this, SIGNAL(canEditSelection(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+        m_rightButtonMenu->addAction(action);
+
+	menu->addSeparator();
+        m_rightButtonMenu->addSeparator();
+	
+	action = new QAction(tr("Select &All"), this);
+	action->setShortcut(tr("Ctrl+A"));
+	connect(action, SIGNAL(triggered()), this, SLOT(selectAll()));
+	connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+        m_rightButtonMenu->addAction(action);
+	
+	action = new QAction(tr("Select &Visible Range"), this);
+	action->setShortcut(tr("Ctrl+Shift+A"));
+	connect(action, SIGNAL(triggered()), this, SLOT(selectVisible()));
+	connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+	
+	action = new QAction(tr("Select to &Start"), this);
+	action->setShortcut(tr("Shift+Left"));
+	connect(action, SIGNAL(triggered()), this, SLOT(selectToStart()));
+	connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+	
+	action = new QAction(tr("Select to &End"), this);
+	action->setShortcut(tr("Shift+Right"));
+	connect(action, SIGNAL(triggered()), this, SLOT(selectToEnd()));
+	connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+
+	action = new QAction(tr("C&lear Selection"), this);
+	action->setShortcut(tr("Esc"));
+	connect(action, SIGNAL(triggered()), this, SLOT(clearSelection()));
+	connect(this, SIGNAL(canClearSelection(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+        m_rightButtonMenu->addAction(action);
+
+	menu->addSeparator();
+
+	action = new QAction(tr("&Insert Instant at Playback Position"), this);
+	action->setShortcut(tr("Enter"));
+	connect(action, SIGNAL(triggered()), this, SLOT(insertInstant()));
+	connect(this, SIGNAL(canInsertInstant(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+
+	menu = menuBar()->addMenu(tr("&View"));
+
+        QActionGroup *overlayGroup = new QActionGroup(this);
+        
+        action = new QAction(tr("&No Text Overlays"), this);
+	action->setShortcut(tr("0"));
+	action->setStatusTip(tr("Show no texts for frame times, layer names etc"));
+	connect(action, SIGNAL(triggered()), this, SLOT(showNoOverlays()));
+        action->setCheckable(true);
+        action->setChecked(false);
+        overlayGroup->addAction(action);
+	menu->addAction(action);
+        
+        action = new QAction(tr("Basic &Text Overlays"), this);
+	action->setShortcut(tr("9"));
+	action->setStatusTip(tr("Show texts for frame times etc, but not layer names etc"));
+	connect(action, SIGNAL(triggered()), this, SLOT(showBasicOverlays()));
+        action->setCheckable(true);
+        action->setChecked(true);
+        overlayGroup->addAction(action);
+	menu->addAction(action);
+        
+        action = new QAction(tr("&All Text Overlays"), this);
+	action->setShortcut(tr("8"));
+	action->setStatusTip(tr("Show texts for frame times, layer names etc"));
+	connect(action, SIGNAL(triggered()), this, SLOT(showAllOverlays()));
+        action->setCheckable(true);
+        action->setChecked(false);
+        overlayGroup->addAction(action);
+	menu->addAction(action);
+
+	menu->addSeparator();
+	
+	action = new QAction(tr("Scroll &Left"), this);
+	action->setShortcut(tr("Left"));
+	action->setStatusTip(tr("Scroll the current pane to the left"));
+	connect(action, SIGNAL(triggered()), this, SLOT(scrollLeft()));
+	connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+	
+	action = new QAction(tr("Scroll &Right"), this);
+	action->setShortcut(tr("Right"));
+	action->setStatusTip(tr("Scroll the current pane to the right"));
+	connect(action, SIGNAL(triggered()), this, SLOT(scrollRight()));
+	connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+	
+	action = new QAction(tr("Jump Left"), this);
+	action->setShortcut(tr("Ctrl+Left"));
+	action->setStatusTip(tr("Scroll the current pane a big step to the left"));
+	connect(action, SIGNAL(triggered()), this, SLOT(jumpLeft()));
+	connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+	
+	action = new QAction(tr("Jump Right"), this);
+	action->setShortcut(tr("Ctrl+Right"));
+	action->setStatusTip(tr("Scroll the current pane a big step to the right"));
+	connect(action, SIGNAL(triggered()), this, SLOT(jumpRight()));
+	connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+
+	menu->addSeparator();
+
+	action = new QAction(QIcon(":/icons/zoom-in.png"),
+			     tr("Zoom &In"), this);
+	action->setShortcut(tr("Up"));
+	action->setStatusTip(tr("Increase the zoom level"));
+	connect(action, SIGNAL(triggered()), this, SLOT(zoomIn()));
+	connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+	
+	action = new QAction(QIcon(":/icons/zoom-out.png"),
+			     tr("Zoom &Out"), this);
+	action->setShortcut(tr("Down"));
+	action->setStatusTip(tr("Decrease the zoom level"));
+	connect(action, SIGNAL(triggered()), this, SLOT(zoomOut()));
+	connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+	
+	action = new QAction(tr("Restore &Default Zoom"), this);
+	connect(action, SIGNAL(triggered()), this, SLOT(zoomDefault()));
+	connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+
+        action = new QAction(tr("Zoom to &Fit"), this);
+	action->setStatusTip(tr("Zoom to show the whole file"));
+	connect(action, SIGNAL(triggered()), this, SLOT(zoomToFit()));
+	connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool)));
+	menu->addAction(action);
+
+/*!!! This one doesn't work properly yet
+
+	menu->addSeparator();
+
+	action = new QAction(tr("Show &Layer Hierarchy"), this);
+	action->setShortcut(tr("Alt+L"));
+	connect(action, SIGNAL(triggered()), this, SLOT(showLayerTree()));
+	menu->addAction(action);
+*/
+    }
+
+    if (m_paneMenu) {
+	m_paneActions.clear();
+	m_paneMenu->clear();
+    } else {
+	m_paneMenu = menuBar()->addMenu(tr("&Pane"));
+    }
+
+    if (m_layerMenu) {
+	m_layerTransformActions.clear();
+	m_layerActions.clear();
+	m_layerMenu->clear();
+    } else {
+	m_layerMenu = menuBar()->addMenu(tr("&Layer"));
+    }
+
+    TransformFactory::TransformList transforms =
+	TransformFactory::getInstance()->getAllTransforms();
+
+    std::vector<QString> types =
+        TransformFactory::getInstance()->getAllTransformTypes();
+
+    std::map<QString, QMenu *> transformMenus;
+
+    for (std::vector<QString>::iterator i = types.begin(); i != types.end(); ++i) {
+        transformMenus[*i] = m_layerMenu->addMenu(*i);
+        m_rightButtonLayerMenu->addMenu(transformMenus[*i]);
+    }
+
+    for (unsigned int i = 0; i < transforms.size(); ++i) {
+	
+	QString description = transforms[i].description;
+	if (description == "") description = transforms[i].name;
+
+	QString actionText = description;
+        if (transforms[i].configurable) {
+            actionText = QString("%1...").arg(actionText);
+        }
+
+	action = new QAction(actionText, this);
+	connect(action, SIGNAL(triggered()), this, SLOT(addLayer()));
+	m_layerTransformActions[action] = transforms[i].name;
+	connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool)));
+	transformMenus[transforms[i].type]->addAction(action);
+    }
+
+    m_rightButtonLayerMenu->addSeparator();
+
+    menu = m_paneMenu;
+
+    action = new QAction(QIcon(":/icons/pane.png"), tr("Add &New Pane"), this);
+    action->setShortcut(tr("Alt+N"));
+    action->setStatusTip(tr("Add a new pane containing only a time ruler"));
+    connect(action, SIGNAL(triggered()), this, SLOT(addPane()));
+    connect(this, SIGNAL(canAddPane(bool)), action, SLOT(setEnabled(bool)));
+    m_paneActions[action] = PaneConfiguration(LayerFactory::TimeRuler);
+    menu->addAction(action);
+
+    menu->addSeparator();
+
+    menu = m_layerMenu;
+
+    menu->addSeparator();
+
+    LayerFactory::LayerTypeSet emptyLayerTypes =
+	LayerFactory::getInstance()->getValidEmptyLayerTypes();
+
+    for (LayerFactory::LayerTypeSet::iterator i = emptyLayerTypes.begin();
+	 i != emptyLayerTypes.end(); ++i) {
+	
+	QIcon icon;
+	QString mainText, tipText, channelText;
+	LayerFactory::LayerType type = *i;
+	QString name = LayerFactory::getInstance()->getLayerPresentationName(type);
+	
+	icon = QIcon(QString(":/icons/%1.png")
+		     .arg(LayerFactory::getInstance()->getLayerIconName(type)));
+
+	mainText = tr("Add New %1 Layer").arg(name);
+	tipText = tr("Add a new empty layer of type %1").arg(name);
+
+	action = new QAction(icon, mainText, this);
+	action->setStatusTip(tipText);
+
+	if (type == LayerFactory::Text) {
+	    action->setShortcut(tr("Alt+T"));
+	}
+
+	connect(action, SIGNAL(triggered()), this, SLOT(addLayer()));
+	connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool)));
+	m_layerActions[action] = type;
+	menu->addAction(action);
+        m_rightButtonLayerMenu->addAction(action);
+    }
+    
+    m_rightButtonLayerMenu->addSeparator();
+    menu->addSeparator();
+
+    int channels = 1;
+    if (getMainModel()) channels = getMainModel()->getChannelCount();
+
+    if (channels < 1) channels = 1;
+
+    LayerFactory::LayerType backgroundTypes[] = {
+	LayerFactory::Waveform,
+	LayerFactory::Spectrogram,
+	LayerFactory::MelodicRangeSpectrogram,
+	LayerFactory::PeakFrequencySpectrogram
+    };
+
+    for (unsigned int i = 0;
+	 i < sizeof(backgroundTypes)/sizeof(backgroundTypes[0]); ++i) {
+
+	for (int menuType = 0; menuType <= 1; ++menuType) { // pane, layer
+
+	    if (menuType == 0) menu = m_paneMenu;
+	    else menu = m_layerMenu;
+
+	    QMenu *submenu = 0;
+
+	    for (int c = 0; c <= channels; ++c) {
+
+		if (c == 1 && channels == 1) continue;
+		bool isDefault = (c == 0);
+		bool isOnly = (isDefault && (channels == 1));
+
+		if (menuType == 1) {
+		    if (isDefault) isOnly = true;
+		    else continue;
+		}
+
+		QIcon icon;
+		QString mainText, shortcutText, tipText, channelText;
+		LayerFactory::LayerType type = backgroundTypes[i];
+		bool mono = true;
+
+		switch (type) {
+
+		case LayerFactory::Waveform:
+		    icon = QIcon(":/icons/waveform.png");
+		    mainText = tr("Add &Waveform");
+		    if (menuType == 0) {
+			shortcutText = tr("Alt+W");
+			tipText = tr("Add a new pane showing a waveform view");
+		    } else {
+			tipText = tr("Add a new layer showing a waveform view");
+		    }
+		    mono = false;
+		    break;
+		    
+		case LayerFactory::Spectrogram:
+		    mainText = tr("Add &Spectrogram");
+		    if (menuType == 0) {
+			shortcutText = tr("Alt+S");
+			tipText = tr("Add a new pane showing a dB spectrogram");
+		    } else {
+			tipText = tr("Add a new layer showing a dB spectrogram");
+		    }
+		    break;
+		
+		case LayerFactory::MelodicRangeSpectrogram:
+		    mainText = tr("Add &Melodic Range Spectrogram");
+		    if (menuType == 0) {
+			shortcutText = tr("Alt+M");
+			tipText = tr("Add a new pane showing a spectrogram set up for a pitch overview");
+		    } else {
+			tipText = tr("Add a new layer showing a spectrogram set up for a pitch overview");
+		    }
+		    break;
+		
+		case LayerFactory::PeakFrequencySpectrogram:
+		    mainText = tr("Add &Peak Frequency Spectrogram");
+		    if (menuType == 0) {
+			shortcutText = tr("Alt+P");
+			tipText = tr("Add a new pane showing a spectrogram set up for tracking frequencies");
+		    } else {
+			tipText = tr("Add a new layer showing a spectrogram set up for tracking frequencies");
+		    }
+		    break;
+
+		default: break;
+		}
+
+		if (isOnly) {
+
+		    action = new QAction(icon, mainText, this);
+		    action->setShortcut(shortcutText);
+		    action->setStatusTip(tipText);
+		    if (menuType == 0) {
+			connect(action, SIGNAL(triggered()), this, SLOT(addPane()));
+			connect(this, SIGNAL(canAddPane(bool)), action, SLOT(setEnabled(bool)));
+			m_paneActions[action] = PaneConfiguration(type);
+		    } else {
+			connect(action, SIGNAL(triggered()), this, SLOT(addLayer()));
+			connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool)));
+			m_layerActions[action] = type;
+		    }
+		    menu->addAction(action);
+
+		} else {
+
+		    QString actionText;
+		    if (c == 0) 
+			if (mono) actionText = tr("&All Channels Mixed");
+			else actionText = tr("&All Channels");
+		    else actionText = tr("Channel &%1").arg(c);
+
+		    if (!submenu) {
+			submenu = menu->addMenu(mainText);
+		    }
+		 
+		    action = new QAction(icon, actionText, this);
+		    if (isDefault) action->setShortcut(shortcutText);
+		    action->setStatusTip(tipText);
+		    if (menuType == 0) {
+			connect(action, SIGNAL(triggered()), this, SLOT(addPane()));
+			connect(this, SIGNAL(canAddPane(bool)), action, SLOT(setEnabled(bool)));
+			m_paneActions[action] = PaneConfiguration(type, c - 1);
+		    } else {
+			connect(action, SIGNAL(triggered()), this, SLOT(addLayer()));
+			connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool)));
+			m_layerActions[action] = type;
+		    }
+		    submenu->addAction(action);
+		}
+	    }
+	}
+    }
+
+    menu = m_paneMenu;
+
+    menu->addSeparator();
+
+    action = new QAction(QIcon(":/icons/editdelete.png"), tr("&Delete Pane"), this);
+    action->setShortcut(tr("Alt+D"));
+    action->setStatusTip(tr("Delete the currently selected pane"));
+    connect(action, SIGNAL(triggered()), this, SLOT(deleteCurrentPane()));
+    connect(this, SIGNAL(canDeleteCurrentPane(bool)), action, SLOT(setEnabled(bool)));
+    menu->addAction(action);
+
+    menu = m_layerMenu;
+
+    action = new QAction(QIcon(":/icons/timeruler.png"), tr("Add &Time Ruler"), this);
+    action->setStatusTip(tr("Add a new layer showing a time ruler"));
+    connect(action, SIGNAL(triggered()), this, SLOT(addLayer()));
+    connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool)));
+    m_layerActions[action] = LayerFactory::TimeRuler;
+    menu->addAction(action);
+
+    menu->addSeparator();
+
+    m_existingLayersMenu = menu->addMenu(tr("Add &Existing Layer"));
+    m_rightButtonLayerMenu->addMenu(m_existingLayersMenu);
+    setupExistingLayersMenu();
+
+    m_rightButtonLayerMenu->addSeparator();
+    menu->addSeparator();
+
+    action = new QAction(tr("&Rename Layer..."), this);
+    action->setShortcut(tr("Alt+R"));
+    action->setStatusTip(tr("Rename the currently active layer"));
+    connect(action, SIGNAL(triggered()), this, SLOT(renameCurrentLayer()));
+    connect(this, SIGNAL(canRenameLayer(bool)), action, SLOT(setEnabled(bool)));
+    menu->addAction(action);
+    m_rightButtonLayerMenu->addAction(action);
+
+    action = new QAction(QIcon(":/icons/editdelete.png"), tr("&Delete Layer"), this);
+    action->setShortcut(tr("Alt+Shift+D"));
+    action->setStatusTip(tr("Delete the currently active layer"));
+    connect(action, SIGNAL(triggered()), this, SLOT(deleteCurrentLayer()));
+    connect(this, SIGNAL(canDeleteCurrentLayer(bool)), action, SLOT(setEnabled(bool)));
+    menu->addAction(action);
+    m_rightButtonLayerMenu->addAction(action);
+
+    if (!m_mainMenusCreated) {
+	
+	menu = menuBar()->addMenu(tr("&Help"));
+
+	action = new QAction(tr("&Help Reference"), this); 
+	action->setStatusTip(tr("Open the Sonic Visualiser reference manual")); 
+	connect(action, SIGNAL(triggered()), this, SLOT(help()));
+	menu->addAction(action);
+
+	action = new QAction(tr("Sonic Visualiser on the &Web"), this); 
+	action->setStatusTip(tr("Open the Sonic Visualiser website")); 
+	connect(action, SIGNAL(triggered()), this, SLOT(website()));
+	menu->addAction(action);
+
+	action = new QAction(tr("&About Sonic Visualiser"), this); 
+	action->setStatusTip(tr("Show information about Sonic Visualiser")); 
+	connect(action, SIGNAL(triggered()), this, SLOT(about()));
+	menu->addAction(action);
+/*
+	action = new QAction(tr("About &Qt"), this);
+	action->setStatusTip(tr("Show information about Qt"));
+	connect(action, SIGNAL(triggered()),
+		QApplication::getInstance(), SLOT(aboutQt()));
+	menu->addAction(action);
+*/
+    }
+
+    m_mainMenusCreated = true;
+}
+
+void
+MainWindow::setupRecentFilesMenu()
+{
+    m_recentFilesMenu->clear();
+    std::vector<QString> files = RecentFiles::getInstance()->getRecentFiles();
+    for (size_t i = 0; i < files.size(); ++i) {
+	QAction *action = new QAction(files[i], this);
+	connect(action, SIGNAL(triggered()), this, SLOT(openRecentFile()));
+	m_recentFilesMenu->addAction(action);
+    }
+}
+
+void
+MainWindow::setupExistingLayersMenu()
+{
+    if (!m_existingLayersMenu) return; // should have been created by setupMenus
+
+//    std::cerr << "MainWindow::setupExistingLayersMenu" << std::endl;
+
+    m_existingLayersMenu->clear();
+    m_existingLayerActions.clear();
+
+    std::vector<Layer *> orderedLayers;
+    std::set<Layer *> observedLayers;
+
+    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
+
+	Pane *pane = m_paneStack->getPane(i);
+	if (!pane) continue;
+
+	for (int j = 0; j < pane->getLayerCount(); ++j) {
+
+	    Layer *layer = pane->getLayer(j);
+	    if (!layer) continue;
+	    if (observedLayers.find(layer) != observedLayers.end()) {
+		std::cerr << "found duplicate layer " << layer << std::endl;
+		continue;
+	    }
+
+//	    std::cerr << "found new layer " << layer << " (name = " 
+//		      << layer->getLayerPresentationName().toStdString() << ")" << std::endl;
+
+	    orderedLayers.push_back(layer);
+	    observedLayers.insert(layer);
+	}
+    }
+
+    std::map<QString, int> observedNames;
+
+    for (int i = 0; i < orderedLayers.size(); ++i) {
+	
+	QString name = orderedLayers[i]->getLayerPresentationName();
+	int n = ++observedNames[name];
+	if (n > 1) name = QString("%1 <%2>").arg(name).arg(n);
+
+	QAction *action = new QAction(name, this);
+	connect(action, SIGNAL(triggered()), this, SLOT(addLayer()));
+	connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool)));
+	m_existingLayerActions[action] = orderedLayers[i];
+
+	m_existingLayersMenu->addAction(action);
+    }
+}
+
+void
+MainWindow::setupToolbars()
+{
+    QToolBar *toolbar = addToolBar(tr("Transport Toolbar"));
+
+    QAction *action = toolbar->addAction(QIcon(":/icons/rewind-start.png"),
+					 tr("Rewind to Start"));
+    action->setShortcut(tr("Home"));
+    action->setStatusTip(tr("Rewind to the start"));
+    connect(action, SIGNAL(triggered()), this, SLOT(rewindStart()));
+    connect(this, SIGNAL(canPlay(bool)), action, SLOT(setEnabled(bool)));
+
+    action = toolbar->addAction(QIcon(":/icons/rewind.png"),
+				tr("Rewind"));
+    action->setShortcut(tr("PageUp"));
+    action->setStatusTip(tr("Rewind to the previous time instant in the current layer"));
+    connect(action, SIGNAL(triggered()), this, SLOT(rewind()));
+    connect(this, SIGNAL(canRewind(bool)), action, SLOT(setEnabled(bool)));
+
+    action = toolbar->addAction(QIcon(":/icons/playpause.png"),
+				tr("Play / Pause"));
+    action->setCheckable(true);
+    action->setShortcut(tr("Space"));
+    action->setStatusTip(tr("Start or stop playback from the current position"));
+    connect(action, SIGNAL(triggered()), this, SLOT(play()));
+    connect(m_playSource, SIGNAL(playStatusChanged(bool)),
+	    action, SLOT(setChecked(bool)));
+    connect(this, SIGNAL(canPlay(bool)), action, SLOT(setEnabled(bool)));
+
+    action = toolbar->addAction(QIcon(":/icons/ffwd.png"),
+				tr("Fast Forward"));
+    action->setShortcut(tr("PageDown"));
+    action->setStatusTip(tr("Fast forward to the next time instant in the current layer"));
+    connect(action, SIGNAL(triggered()), this, SLOT(ffwd()));
+    connect(this, SIGNAL(canFfwd(bool)), action, SLOT(setEnabled(bool)));
+
+    action = toolbar->addAction(QIcon(":/icons/ffwd-end.png"),
+				tr("Fast Forward to End"));
+    action->setShortcut(tr("End"));
+    action->setStatusTip(tr("Fast-forward to the end"));
+    connect(action, SIGNAL(triggered()), this, SLOT(ffwdEnd()));
+    connect(this, SIGNAL(canPlay(bool)), action, SLOT(setEnabled(bool)));
+
+    toolbar = addToolBar(tr("Play Mode Toolbar"));
+
+    action = toolbar->addAction(QIcon(":/icons/playselection.png"),
+				tr("Constrain Playback to Selection"));
+    action->setCheckable(true);
+    action->setChecked(m_viewManager->getPlaySelectionMode());
+    action->setShortcut(tr("s"));
+    action->setStatusTip(tr("Constrain playback to the selected area"));
+    connect(action, SIGNAL(triggered()), this, SLOT(playSelectionToggled()));
+    connect(this, SIGNAL(canPlaySelection(bool)), action, SLOT(setEnabled(bool)));
+
+    action = toolbar->addAction(QIcon(":/icons/playloop.png"),
+				tr("Loop Playback"));
+    action->setCheckable(true);
+    action->setChecked(m_viewManager->getPlayLoopMode());
+    action->setShortcut(tr("l"));
+    action->setStatusTip(tr("Loop playback"));
+    connect(action, SIGNAL(triggered()), this, SLOT(playLoopToggled()));
+    connect(this, SIGNAL(canPlay(bool)), action, SLOT(setEnabled(bool)));
+
+    toolbar = addToolBar(tr("Edit Toolbar"));
+    CommandHistory::getInstance()->registerToolbar(toolbar);
+
+    toolbar = addToolBar(tr("Tools Toolbar"));
+    QActionGroup *group = new QActionGroup(this);
+
+    action = toolbar->addAction(QIcon(":/icons/navigate.png"),
+				tr("Navigate"));
+    action->setCheckable(true);
+    action->setChecked(true);
+    action->setShortcut(tr("1"));
+    connect(action, SIGNAL(triggered()), this, SLOT(toolNavigateSelected()));
+    group->addAction(action);
+    m_toolActions[ViewManager::NavigateMode] = action;
+
+    action = toolbar->addAction(QIcon(":/icons/select.png"),
+				tr("Select"));
+    action->setCheckable(true);
+    action->setShortcut(tr("2"));
+    connect(action, SIGNAL(triggered()), this, SLOT(toolSelectSelected()));
+    group->addAction(action);
+    m_toolActions[ViewManager::SelectMode] = action;
+
+    action = toolbar->addAction(QIcon(":/icons/move.png"),
+				tr("Edit"));
+    action->setCheckable(true);
+    action->setShortcut(tr("3"));
+    connect(action, SIGNAL(triggered()), this, SLOT(toolEditSelected()));
+    connect(this, SIGNAL(canEditLayer(bool)), action, SLOT(setEnabled(bool)));
+    group->addAction(action);
+    m_toolActions[ViewManager::EditMode] = action;
+
+    action = toolbar->addAction(QIcon(":/icons/draw.png"),
+				tr("Draw"));
+    action->setCheckable(true);
+    action->setShortcut(tr("4"));
+    connect(action, SIGNAL(triggered()), this, SLOT(toolDrawSelected()));
+    connect(this, SIGNAL(canEditLayer(bool)), action, SLOT(setEnabled(bool)));
+    group->addAction(action);
+    m_toolActions[ViewManager::DrawMode] = action;
+
+//    action = toolbar->addAction(QIcon(":/icons/text.png"),
+//				tr("Text"));
+//    action->setCheckable(true);
+//    action->setShortcut(tr("5"));
+//    connect(action, SIGNAL(triggered()), this, SLOT(toolTextSelected()));
+//    group->addAction(action);
+//    m_toolActions[ViewManager::TextMode] = action;
+
+    toolNavigateSelected();
+}
+
+void
+MainWindow::updateMenuStates()
+{
+    bool haveCurrentPane =
+	(m_paneStack &&
+	 (m_paneStack->getCurrentPane() != 0));
+    bool haveCurrentLayer =
+	(haveCurrentPane &&
+	 (m_paneStack->getCurrentPane()->getSelectedLayer()));
+    bool haveMainModel =
+	(getMainModel() != 0);
+    bool havePlayTarget =
+	(m_playTarget != 0);
+    bool haveSelection = 
+	(m_viewManager &&
+	 !m_viewManager->getSelections().empty());
+    bool haveCurrentEditableLayer =
+	(haveCurrentLayer &&
+	 m_paneStack->getCurrentPane()->getSelectedLayer()->
+	 isLayerEditable());
+    bool haveCurrentTimeInstantsLayer = 
+	(haveCurrentLayer &&
+	 dynamic_cast<TimeInstantLayer *>
+	 (m_paneStack->getCurrentPane()->getSelectedLayer()));
+    bool haveCurrentTimeValueLayer = 
+	(haveCurrentLayer &&
+	 dynamic_cast<TimeValueLayer *>
+	 (m_paneStack->getCurrentPane()->getSelectedLayer()));
+    bool haveCurrentColour3DPlot =
+        (haveCurrentLayer &&
+         dynamic_cast<Colour3DPlotLayer *>
+         (m_paneStack->getCurrentPane()->getSelectedLayer()));
+    bool haveClipboardContents =
+        (m_viewManager &&
+         !m_viewManager->getClipboard().empty());
+
+    emit canAddPane(haveMainModel);
+    emit canDeleteCurrentPane(haveCurrentPane);
+    emit canZoom(haveMainModel && haveCurrentPane);
+    emit canScroll(haveMainModel && haveCurrentPane);
+    emit canAddLayer(haveMainModel && haveCurrentPane);
+    emit canImportMoreAudio(haveMainModel);
+    emit canImportLayer(haveMainModel && haveCurrentPane);
+    emit canExportAudio(haveMainModel);
+    emit canExportLayer(haveMainModel &&
+                        (haveCurrentEditableLayer || haveCurrentColour3DPlot));
+    emit canDeleteCurrentLayer(haveCurrentLayer);
+    emit canRenameLayer(haveCurrentLayer);
+    emit canEditLayer(haveCurrentEditableLayer);
+    emit canSelect(haveMainModel && haveCurrentPane);
+    emit canPlay(/*!!! haveMainModel && */ havePlayTarget);
+    emit canFfwd(haveCurrentTimeInstantsLayer || haveCurrentTimeValueLayer);
+    emit canRewind(haveCurrentTimeInstantsLayer || haveCurrentTimeValueLayer);
+    emit canPaste(haveCurrentEditableLayer && haveClipboardContents);
+    emit canInsertInstant(haveCurrentPane);
+    emit canPlaySelection(haveMainModel && havePlayTarget && haveSelection);
+    emit canClearSelection(haveSelection);
+    emit canEditSelection(haveSelection && haveCurrentEditableLayer);
+    emit canSave(m_sessionFile != "" && m_documentModified);
+}
+
+void
+MainWindow::updateDescriptionLabel()
+{
+    if (!getMainModel()) {
+	m_descriptionLabel->setText(tr("No audio file loaded."));
+	return;
+    }
+
+    QString description;
+
+    size_t ssr = getMainModel()->getSampleRate();
+    size_t tsr = ssr;
+    if (m_playSource) tsr = m_playSource->getTargetSampleRate();
+
+    if (ssr != tsr) {
+	description = tr("%1Hz (resampling to %2Hz)").arg(ssr).arg(tsr);
+    } else {
+	description = QString("%1Hz").arg(ssr);
+    }
+
+    description = QString("%1 - %2")
+	.arg(RealTime::frame2RealTime(getMainModel()->getEndFrame(), ssr)
+	     .toText(false).c_str())
+	.arg(description);
+
+    m_descriptionLabel->setText(description);
+}
+
+void
+MainWindow::documentModified()
+{
+//    std::cerr << "MainWindow::documentModified" << std::endl;
+
+    if (!m_documentModified) {
+	setWindowTitle(tr("%1 (modified)").arg(windowTitle()));
+    }
+
+    m_documentModified = true;
+    updateMenuStates();
+}
+
+void
+MainWindow::documentRestored()
+{
+//    std::cerr << "MainWindow::documentRestored" << std::endl;
+
+    if (m_documentModified) {
+	QString wt(windowTitle());
+	wt.replace(tr(" (modified)"), "");
+	setWindowTitle(wt);
+    }
+
+    m_documentModified = false;
+    updateMenuStates();
+}
+
+void
+MainWindow::playLoopToggled()
+{
+    QAction *action = dynamic_cast<QAction *>(sender());
+    
+    if (action) {
+	m_viewManager->setPlayLoopMode(action->isChecked());
+    } else {
+	m_viewManager->setPlayLoopMode(!m_viewManager->getPlayLoopMode());
+    }
+}
+
+void
+MainWindow::playSelectionToggled()
+{
+    QAction *action = dynamic_cast<QAction *>(sender());
+    
+    if (action) {
+	m_viewManager->setPlaySelectionMode(action->isChecked());
+    } else {
+	m_viewManager->setPlaySelectionMode(!m_viewManager->getPlaySelectionMode());
+    }
+}
+
+void
+MainWindow::currentPaneChanged(Pane *)
+{
+    updateMenuStates();
+}
+
+void
+MainWindow::currentLayerChanged(Pane *, Layer *)
+{
+    updateMenuStates();
+}
+
+void
+MainWindow::toolNavigateSelected()
+{
+    m_viewManager->setToolMode(ViewManager::NavigateMode);
+}
+
+void
+MainWindow::toolSelectSelected()
+{
+    m_viewManager->setToolMode(ViewManager::SelectMode);
+}
+
+void
+MainWindow::toolEditSelected()
+{
+    m_viewManager->setToolMode(ViewManager::EditMode);
+}
+
+void
+MainWindow::toolDrawSelected()
+{
+    m_viewManager->setToolMode(ViewManager::DrawMode);
+}
+
+//void
+//MainWindow::toolTextSelected()
+//{
+//    m_viewManager->setToolMode(ViewManager::TextMode);
+//}
+
+void
+MainWindow::selectAll()
+{
+    if (!getMainModel()) return;
+    m_viewManager->setSelection(Selection(getMainModel()->getStartFrame(),
+					  getMainModel()->getEndFrame()));
+}
+
+void
+MainWindow::selectToStart()
+{
+    if (!getMainModel()) return;
+    m_viewManager->setSelection(Selection(getMainModel()->getStartFrame(),
+					  m_viewManager->getGlobalCentreFrame()));
+}
+
+void
+MainWindow::selectToEnd()
+{
+    if (!getMainModel()) return;
+    m_viewManager->setSelection(Selection(m_viewManager->getGlobalCentreFrame(),
+					  getMainModel()->getEndFrame()));
+}
+
+void
+MainWindow::selectVisible()
+{
+    Model *model = getMainModel();
+    if (!model) return;
+
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (!currentPane) return;
+
+    size_t startFrame, endFrame;
+
+    if (currentPane->getStartFrame() < 0) startFrame = 0;
+    else startFrame = currentPane->getStartFrame();
+
+    if (currentPane->getEndFrame() > model->getEndFrame()) endFrame = model->getEndFrame();
+    else endFrame = currentPane->getEndFrame();
+
+    m_viewManager->setSelection(Selection(startFrame, endFrame));
+}
+
+void
+MainWindow::clearSelection()
+{
+    m_viewManager->clearSelections();
+}
+
+void
+MainWindow::cut()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (!currentPane) return;
+
+    Layer *layer = currentPane->getSelectedLayer();
+    if (!layer) return;
+
+    Clipboard &clipboard = m_viewManager->getClipboard();
+    clipboard.clear();
+
+    MultiSelection::SelectionList selections = m_viewManager->getSelections();
+
+    CommandHistory::getInstance()->startCompoundOperation(tr("Cut"), true);
+
+    for (MultiSelection::SelectionList::iterator i = selections.begin();
+         i != selections.end(); ++i) {
+        layer->copy(*i, clipboard);
+        layer->deleteSelection(*i);
+    }
+
+    CommandHistory::getInstance()->endCompoundOperation();
+}
+
+void
+MainWindow::copy()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (!currentPane) return;
+
+    Layer *layer = currentPane->getSelectedLayer();
+    if (!layer) return;
+
+    Clipboard &clipboard = m_viewManager->getClipboard();
+    clipboard.clear();
+
+    MultiSelection::SelectionList selections = m_viewManager->getSelections();
+
+    for (MultiSelection::SelectionList::iterator i = selections.begin();
+         i != selections.end(); ++i) {
+        layer->copy(*i, clipboard);
+    }
+}
+
+void
+MainWindow::paste()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (!currentPane) return;
+
+    //!!! if we have no current layer, we should create one of the most
+    // appropriate type
+
+    Layer *layer = currentPane->getSelectedLayer();
+    if (!layer) return;
+
+    Clipboard &clipboard = m_viewManager->getClipboard();
+    Clipboard::PointList contents = clipboard.getPoints();
+/*
+    long minFrame = 0;
+    bool have = false;
+    for (int i = 0; i < contents.size(); ++i) {
+        if (!contents[i].haveFrame()) continue;
+        if (!have || contents[i].getFrame() < minFrame) {
+            minFrame = contents[i].getFrame();
+            have = true;
+        }
+    }
+
+    long frameOffset = long(m_viewManager->getGlobalCentreFrame()) - minFrame;
+
+    layer->paste(clipboard, frameOffset);
+*/
+    layer->paste(clipboard, 0, true);
+}
+
+void
+MainWindow::deleteSelected()
+{
+    if (m_paneStack->getCurrentPane() &&
+	m_paneStack->getCurrentPane()->getSelectedLayer()) {
+
+	MultiSelection::SelectionList selections =
+	    m_viewManager->getSelections();
+
+	for (MultiSelection::SelectionList::iterator i = selections.begin();
+	     i != selections.end(); ++i) {
+
+	    m_paneStack->getCurrentPane()->getSelectedLayer()->deleteSelection(*i);
+	}
+    }
+}
+
+void
+MainWindow::insertInstant()
+{
+    int frame = m_viewManager->getPlaybackFrame();
+            
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (!pane) {
+        return;
+    }
+
+    Layer *layer = dynamic_cast<TimeInstantLayer *>
+        (pane->getSelectedLayer());
+
+    if (!layer) {
+        for (int i = pane->getLayerCount(); i > 0; --i) {
+            layer = dynamic_cast<TimeInstantLayer *>(pane->getLayer(i - 1));
+            if (layer) break;
+        }
+
+        if (!layer) {
+            CommandHistory::getInstance()->startCompoundOperation
+                (tr("Add Point"), true);
+            layer = m_document->createEmptyLayer(LayerFactory::TimeInstants);
+            if (layer) {
+                m_document->addLayerToView(pane, layer);
+                m_paneStack->setCurrentLayer(pane, layer);
+            }
+            CommandHistory::getInstance()->endCompoundOperation();
+        }
+    }
+
+    if (layer) {
+    
+        Model *model = layer->getModel();
+        SparseOneDimensionalModel *sodm = dynamic_cast<SparseOneDimensionalModel *>
+            (model);
+
+        if (sodm) {
+            SparseOneDimensionalModel::Point point
+                (frame, QString("%1").arg(sodm->getPointCount() + 1));
+            CommandHistory::getInstance()->addCommand
+                (new SparseOneDimensionalModel::AddPointCommand(sodm, point,
+                                                                tr("Add Points")),
+                 true, true); // bundled
+        }
+    }
+}
+
+void
+MainWindow::importAudio()
+{
+    QString orig = m_audioFile;
+
+//    std::cerr << "orig = " << orig.toStdString() << std::endl;
+    
+    if (orig == "") orig = ".";
+
+    QString path = QFileDialog::getOpenFileName
+	(this, tr("Select an audio file"), orig,
+	 tr("Audio files (%1)\nAll files (*.*)")
+	 .arg(AudioFileReaderFactory::getKnownExtensions()));
+
+    if (path != "") {
+	if (!openAudioFile(path, ReplaceMainModel)) {
+	    QMessageBox::critical(this, tr("Failed to open file"),
+				  tr("Audio file \"%1\" could not be opened").arg(path));
+	}
+    }
+}
+
+void
+MainWindow::importMoreAudio()
+{
+    QString orig = m_audioFile;
+
+//    std::cerr << "orig = " << orig.toStdString() << std::endl;
+    
+    if (orig == "") orig = ".";
+
+    QString path = QFileDialog::getOpenFileName
+	(this, tr("Select an audio file"), orig,
+	 tr("Audio files (%1)\nAll files (*.*)")
+	 .arg(AudioFileReaderFactory::getKnownExtensions()));
+
+    if (path != "") {
+	if (!openAudioFile(path, CreateAdditionalModel)) {
+	    QMessageBox::critical(this, tr("Failed to open file"),
+				  tr("Audio file \"%1\" could not be opened").arg(path));
+	}
+    }
+}
+
+void
+MainWindow::exportAudio()
+{
+    if (!getMainModel()) return;
+
+    QString path = QFileDialog::getSaveFileName
+	(this, tr("Select a file to export to"), ".",
+	 tr("WAV audio files (*.wav)\nAll files (*.*)"));
+    
+    if (path == "") return;
+
+    if (!path.endsWith(".wav")) path = path + ".wav";
+
+    bool ok = false;
+    QString error;
+
+    WavFileWriter *writer = 0;
+    MultiSelection ms = m_viewManager->getSelection();
+    MultiSelection::SelectionList selections = m_viewManager->getSelections();
+
+    bool multiple = false;
+
+    if (selections.empty()) {
+
+	writer = new WavFileWriter(path, getMainModel()->getSampleRate(),
+				   getMainModel(), 0);
+    
+    } else if (selections.size() == 1) {
+
+	QStringList items;
+	items << tr("Export the selected region only")
+	      << tr("Export the whole audio file");
+	
+	bool ok = false;
+	QString item = ListInputDialog::getItem
+	    (this, tr("Select region to export"),
+	     tr("Which region from the original audio file do you want to export?"),
+	     items, 0, &ok);
+	
+	if (!ok || item.isEmpty()) return;
+	
+	if (item == items[0]) {
+
+	    writer = new WavFileWriter(path, getMainModel()->getSampleRate(),
+				       getMainModel(), &ms);
+
+	} else {
+
+	    writer = new WavFileWriter(path, getMainModel()->getSampleRate(),
+				       getMainModel(), 0);
+	}
+    } else {
+
+	QStringList items;
+	items << tr("Export the selected regions into a single audio file")
+	      << tr("Export the selected regions into separate files")
+	      << tr("Export the whole audio file");
+
+	bool ok = false;
+	QString item = ListInputDialog::getItem
+	    (this, tr("Select region to export"),
+	     tr("Multiple regions of the original audio file are selected.\nWhat do you want to export?"),
+	     items, 0, &ok);
+	    
+	if (!ok || item.isEmpty()) return;
+
+	if (item == items[0]) {
+
+	    writer = new WavFileWriter(path, getMainModel()->getSampleRate(),
+				       getMainModel(), &ms);
+
+	} else if (item == items[2]) {
+
+	    writer = new WavFileWriter(path, getMainModel()->getSampleRate(),
+				       getMainModel(), 0);
+
+	} else {
+
+            multiple = true;
+
+	    int n = 1;
+	    QString base = path;
+	    base.replace(".wav", "");
+
+	    for (MultiSelection::SelectionList::iterator i = selections.begin();
+		 i != selections.end(); ++i) {
+
+		MultiSelection subms;
+		subms.setSelection(*i);
+
+		QString subpath = QString("%1.%2.wav").arg(base).arg(n);
+		++n;
+
+		if (QFileInfo(subpath).exists()) {
+		    error = tr("Fragment file %1 already exists, aborting").arg(subpath);
+		    break;
+		}
+
+		WavFileWriter subwriter(subpath, getMainModel()->getSampleRate(),
+					getMainModel(), &subms);
+		subwriter.write();
+		ok = subwriter.isOK();
+
+		if (!ok) {
+		    error = subwriter.getError();
+		    break;
+		}
+	    }
+	}
+    }
+
+    if (writer) {
+	writer->write();
+	ok = writer->isOK();
+	error = writer->getError();
+	delete writer;
+    }
+
+    if (ok) {
+        if (!multiple) {
+            RecentFiles::getInstance()->addFile(path);
+        }
+    } else {
+	QMessageBox::critical(this, tr("Failed to write file"), error);
+    }
+}
+
+void
+MainWindow::importLayer()
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    
+    if (!pane) {
+	// shouldn't happen, as the menu action should have been disabled
+	std::cerr << "WARNING: MainWindow::importLayer: no current pane" << std::endl;
+	return;
+    }
+
+    if (!getMainModel()) {
+	// shouldn't happen, as the menu action should have been disabled
+	std::cerr << "WARNING: MainWindow::importLayer: No main model -- hence no default sample rate available" << std::endl;
+	return;
+    }
+
+    QString path = QFileDialog::getOpenFileName
+	(this, tr("Select file"), ".",
+	 tr("All supported files (%1)\nSonic Visualiser Layer XML files (*.svl)\nComma-separated data files (*.csv)\nSpace-separated .lab files (*.lab)\nMIDI files (*.mid)\nText files (*.txt)\nAll files (*.*)").arg(DataFileReaderFactory::getKnownExtensions()));
+
+    if (path != "") {
+
+        if (!openLayerFile(path)) {
+            QMessageBox::critical(this, tr("Failed to open file"),
+                                  tr("File %1 could not be opened.").arg(path));
+            return;
+        }
+    }
+}
+
+bool
+MainWindow::openLayerFile(QString path)
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    
+    if (!pane) {
+	// shouldn't happen, as the menu action should have been disabled
+	std::cerr << "WARNING: MainWindow::openLayerFile: no current pane" << std::endl;
+	return false;
+    }
+
+    if (!getMainModel()) {
+	// shouldn't happen, as the menu action should have been disabled
+	std::cerr << "WARNING: MainWindow::openLayerFile: No main model -- hence no default sample rate available" << std::endl;
+	return false;
+    }
+
+    if (path.endsWith(".svl") || path.endsWith(".xml")) {
+
+        PaneCallback callback(this);
+        QFile file(path);
+        
+        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+            std::cerr << "ERROR: MainWindow::openLayerFile("
+                      << path.toStdString()
+                      << "): Failed to open file for reading" << std::endl;
+            return false;
+        }
+        
+        SVFileReader reader(m_document, callback);
+        reader.setCurrentPane(pane);
+        
+        QXmlInputSource inputSource(&file);
+        reader.parse(inputSource);
+        
+        if (!reader.isOK()) {
+            std::cerr << "ERROR: MainWindow::openLayerFile("
+                      << path.toStdString()
+                      << "): Failed to read XML file: "
+                      << reader.getErrorString().toStdString() << std::endl;
+            return false;
+        }
+
+        RecentFiles::getInstance()->addFile(path);
+        return true;
+        
+    } else {
+        
+        Model *model = DataFileReaderFactory::load(path, getMainModel()->getSampleRate());
+        
+        if (model) {
+            Layer *newLayer = m_document->createImportedLayer(model);
+            if (newLayer) {
+                m_document->addLayerToView(pane, newLayer);
+                RecentFiles::getInstance()->addFile(path);
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+void
+MainWindow::exportLayer()
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (!pane) return;
+
+    Layer *layer = pane->getSelectedLayer();
+    if (!layer) return;
+
+    Model *model = layer->getModel();
+    if (!model) return;
+
+    QString path = QFileDialog::getSaveFileName
+	(this, tr("Select a file to export to"), ".",
+	 tr("Sonic Visualiser Layer XML files (*.svl)\nComma-separated data files (*.csv)\nText files (*.txt)\nAll files (*.*)"));
+
+    if (path == "") return;
+
+    if (QFileInfo(path).suffix() == "") path += ".svl";
+
+    QString error;
+
+    if (path.endsWith(".xml") || path.endsWith(".svl")) {
+
+        QFile file(path);
+        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+            error = tr("Failed to open file %1 for writing").arg(path);
+        } else {
+            QTextStream out(&file);
+            out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+                << "<!DOCTYPE sonic-visualiser>\n"
+                << "<sv>\n"
+                << "  <data>\n";
+
+            model->toXml(out, "    ");
+
+            out << "  </data>\n"
+                << "  <display>\n";
+
+            layer->toXml(out, "    ");
+
+            out << "  </display>\n"
+                << "</sv>\n";
+        }
+
+    } else {
+
+        CSVFileWriter writer(path, model,
+                             (path.endsWith(".csv") ? "," : "\t"));
+        writer.write();
+
+        if (!writer.isOK()) {
+            error = writer.getError();
+        }
+    }
+
+    if (error != "") {
+        QMessageBox::critical(this, tr("Failed to write file"), error);
+    } else {
+        RecentFiles::getInstance()->addFile(path);
+    }
+}
+
+bool
+MainWindow::openAudioFile(QString path, AudioFileOpenMode mode)
+{
+    if (!(QFileInfo(path).exists() &&
+	  QFileInfo(path).isFile() &&
+	  QFileInfo(path).isReadable())) {
+	return false;
+    }
+
+    WaveFileModel *newModel = new WaveFileModel(path);
+
+    if (!newModel->isOK()) {
+	delete newModel;
+	return false;
+    }
+
+    bool setAsMain = true;
+    static bool prevSetAsMain = true;
+
+    if (mode == CreateAdditionalModel) setAsMain = false;
+    else if (mode == AskUser) {
+        if (m_document->getMainModel()) {
+
+            QStringList items;
+            items << tr("Replace the existing main waveform")
+                  << tr("Load this file into a new waveform pane");
+
+            bool ok = false;
+            QString item = ListInputDialog::getItem
+                (this, tr("Select target for import"),
+                 tr("You already have an audio waveform loaded.\nWhat would you like to do with the new audio file?"),
+                 items, prevSetAsMain ? 0 : 1, &ok);
+            
+            if (!ok || item.isEmpty()) {
+                delete newModel;
+                return false;
+            }
+            
+            setAsMain = (item == items[0]);
+            prevSetAsMain = setAsMain;
+        }
+    }
+
+    if (setAsMain) {
+
+        Model *prevMain = getMainModel();
+        if (prevMain) m_playSource->removeModel(prevMain);
+
+	PlayParameterRepository::getInstance()->clear();
+
+	// The clear() call will have removed the parameters for the
+	// main model.  Re-add them with the new one.
+	PlayParameterRepository::getInstance()->addModel(newModel);
+
+	m_document->setMainModel(newModel);
+	setupMenus();
+
+	if (m_sessionFile == "") {
+	    setWindowTitle(tr("Sonic Visualiser: %1")
+			   .arg(QFileInfo(path).fileName()));
+	    CommandHistory::getInstance()->clear();
+	    CommandHistory::getInstance()->documentSaved();
+	    m_documentModified = false;
+	} else {
+	    setWindowTitle(tr("Sonic Visualiser: %1 [%2]")
+			   .arg(QFileInfo(m_sessionFile).fileName())
+			   .arg(QFileInfo(path).fileName()));
+	    if (m_documentModified) {
+		m_documentModified = false;
+		documentModified(); // so as to restore "(modified)" window title
+	    }
+	}
+
+	m_audioFile = path;
+
+    } else { // !setAsMain
+
+	CommandHistory::getInstance()->startCompoundOperation
+	    (tr("Import \"%1\"").arg(QFileInfo(path).fileName()), true);
+
+	m_document->addImportedModel(newModel);
+
+	AddPaneCommand *command = new AddPaneCommand(this);
+	CommandHistory::getInstance()->addCommand(command);
+
+	Pane *pane = command->getPane();
+
+	if (!m_timeRulerLayer) {
+	    m_timeRulerLayer = m_document->createMainModelLayer
+		(LayerFactory::TimeRuler);
+	}
+
+	m_document->addLayerToView(pane, m_timeRulerLayer);
+
+	Layer *newLayer = m_document->createImportedLayer(newModel);
+
+	if (newLayer) {
+	    m_document->addLayerToView(pane, newLayer);
+	}
+	
+	CommandHistory::getInstance()->endCompoundOperation();
+    }
+
+    updateMenuStates();
+    RecentFiles::getInstance()->addFile(path);
+
+    return true;
+}
+
+void
+MainWindow::createPlayTarget()
+{
+    if (m_playTarget) return;
+
+    m_playTarget = AudioTargetFactory::createCallbackTarget(m_playSource);
+    if (!m_playTarget) {
+	QMessageBox::warning
+	    (this, tr("Couldn't open audio device"),
+	     tr("Could not open an audio device for playback.\nAudio playback will not be available during this session.\n"),
+	     QMessageBox::Ok, 0);
+    }
+    connect(m_fader, SIGNAL(valueChanged(float)),
+	    m_playTarget, SLOT(setOutputGain(float)));
+}
+
+WaveFileModel *
+MainWindow::getMainModel()
+{
+    if (!m_document) return 0;
+    return m_document->getMainModel();
+}
+
+void
+MainWindow::newSession()
+{
+    if (!checkSaveModified()) return;
+
+    closeSession();
+    createDocument();
+
+    Pane *pane = m_paneStack->addPane();
+
+    if (!m_timeRulerLayer) {
+	m_timeRulerLayer = m_document->createMainModelLayer
+	    (LayerFactory::TimeRuler);
+    }
+
+    m_document->addLayerToView(pane, m_timeRulerLayer);
+
+    Layer *waveform = m_document->createMainModelLayer(LayerFactory::Waveform);
+    m_document->addLayerToView(pane, waveform);
+
+    m_panner->registerView(pane);
+
+    CommandHistory::getInstance()->clear();
+    CommandHistory::getInstance()->documentSaved();
+    documentRestored();
+    updateMenuStates();
+}
+
+void
+MainWindow::createDocument()
+{
+    m_document = new Document;
+
+    connect(m_document, SIGNAL(layerAdded(Layer *)),
+	    this, SLOT(layerAdded(Layer *)));
+    connect(m_document, SIGNAL(layerRemoved(Layer *)),
+	    this, SLOT(layerRemoved(Layer *)));
+    connect(m_document, SIGNAL(layerAboutToBeDeleted(Layer *)),
+	    this, SLOT(layerAboutToBeDeleted(Layer *)));
+    connect(m_document, SIGNAL(layerInAView(Layer *, bool)),
+	    this, SLOT(layerInAView(Layer *, bool)));
+
+    connect(m_document, SIGNAL(modelAdded(Model *)),
+	    this, SLOT(modelAdded(Model *)));
+    connect(m_document, SIGNAL(mainModelChanged(WaveFileModel *)),
+	    this, SLOT(mainModelChanged(WaveFileModel *)));
+    connect(m_document, SIGNAL(modelAboutToBeDeleted(Model *)),
+	    this, SLOT(modelAboutToBeDeleted(Model *)));
+
+    connect(m_document, SIGNAL(modelGenerationFailed(QString)),
+            this, SLOT(modelGenerationFailed(QString)));
+    connect(m_document, SIGNAL(modelRegenerationFailed(QString)),
+            this, SLOT(modelRegenerationFailed(QString)));
+}
+
+void
+MainWindow::closeSession()
+{
+    if (!checkSaveModified()) return;
+
+    while (m_paneStack->getPaneCount() > 0) {
+
+	Pane *pane = m_paneStack->getPane(m_paneStack->getPaneCount() - 1);
+
+	while (pane->getLayerCount() > 0) {
+	    m_document->removeLayerFromView
+		(pane, pane->getLayer(pane->getLayerCount() - 1));
+	}
+
+	m_panner->unregisterView(pane);
+	m_paneStack->deletePane(pane);
+    }
+
+    while (m_paneStack->getHiddenPaneCount() > 0) {
+
+	Pane *pane = m_paneStack->getHiddenPane
+	    (m_paneStack->getHiddenPaneCount() - 1);
+
+	while (pane->getLayerCount() > 0) {
+	    m_document->removeLayerFromView
+		(pane, pane->getLayer(pane->getLayerCount() - 1));
+	}
+
+	m_panner->unregisterView(pane);
+	m_paneStack->deletePane(pane);
+    }
+
+    delete m_document;
+    m_document = 0;
+    m_viewManager->clearSelections();
+    m_timeRulerLayer = 0; // document owned this
+
+    m_sessionFile = "";
+    setWindowTitle(tr("Sonic Visualiser"));
+
+    CommandHistory::getInstance()->clear();
+    CommandHistory::getInstance()->documentSaved();
+    documentRestored();
+}
+
+void
+MainWindow::openSession()
+{
+    if (!checkSaveModified()) return;
+
+    QString orig = m_audioFile;
+    if (orig == "") orig = ".";
+    else orig = QFileInfo(orig).absoluteDir().canonicalPath();
+
+    QString path = QFileDialog::getOpenFileName
+	(this, tr("Select a session file"), orig,
+	 tr("Sonic Visualiser session files (*.sv)\nAll files (*.*)"));
+
+    if (path.isEmpty()) return;
+
+    if (!(QFileInfo(path).exists() &&
+	  QFileInfo(path).isFile() &&
+	  QFileInfo(path).isReadable())) {
+	QMessageBox::critical(this, tr("Failed to open file"),
+			      tr("File \"%1\" does not exist or is not a readable file").arg(path));
+	return;
+    }
+
+    if (!openSessionFile(path)) {
+	QMessageBox::critical(this, tr("Failed to open file"),
+			      tr("Session file \"%1\" could not be opened").arg(path));
+    }
+}
+
+void
+MainWindow::openSomething()
+{
+    QString orig = m_audioFile;
+    if (orig == "") orig = ".";
+    else orig = QFileInfo(orig).absoluteDir().canonicalPath();
+
+    bool canImportLayer = (getMainModel() != 0 &&
+                           m_paneStack != 0 &&
+                           m_paneStack->getCurrentPane() != 0);
+
+    QString importSpec;
+
+    if (canImportLayer) {
+        importSpec = tr("All supported files (*.sv %1 %2)\nSonic Visualiser session files (*.sv)\nAudio files (%1)\nLayer files (%2)\nAll files (*.*)")
+            .arg(AudioFileReaderFactory::getKnownExtensions())
+            .arg(DataFileReaderFactory::getKnownExtensions());
+    } else {
+        importSpec = tr("All supported files (*.sv %1)\nSonic Visualiser session files (*.sv)\nAudio files (%1)\nAll files (*.*)")
+            .arg(AudioFileReaderFactory::getKnownExtensions());
+    }
+
+    QString path = QFileDialog::getOpenFileName
+	(this, tr("Select a file to open"), orig, importSpec);
+
+    if (path.isEmpty()) return;
+
+    if (!(QFileInfo(path).exists() &&
+	  QFileInfo(path).isFile() &&
+	  QFileInfo(path).isReadable())) {
+	QMessageBox::critical(this, tr("Failed to open file"),
+			      tr("File \"%1\" does not exist or is not a readable file").arg(path));
+	return;
+    }
+
+    if (path.endsWith(".sv")) {
+
+        if (!checkSaveModified()) return;
+
+        if (!openSessionFile(path)) {
+            QMessageBox::critical(this, tr("Failed to open file"),
+                                  tr("Session file \"%1\" could not be opened").arg(path));
+        }
+
+    } else {
+
+        if (!openAudioFile(path, AskUser)) {
+
+            if (!canImportLayer || !openLayerFile(path)) {
+
+                QMessageBox::critical(this, tr("Failed to open file"),
+                                      tr("File \"%1\" could not be opened").arg(path));
+            }
+        }
+    }
+}
+
+void
+MainWindow::openRecentFile()
+{
+    QObject *obj = sender();
+    QAction *action = dynamic_cast<QAction *>(obj);
+    
+    if (!action) {
+	std::cerr << "WARNING: MainWindow::openRecentFile: sender is not an action"
+		  << std::endl;
+	return;
+    }
+
+    QString path = action->text();
+    if (path == "") return;
+
+    if (path.endsWith("sv")) {
+
+        if (!checkSaveModified()) return ;
+
+        if (!openSessionFile(path)) {
+            QMessageBox::critical(this, tr("Failed to open file"),
+                                  tr("Session file \"%1\" could not be opened").arg(path));
+        }
+
+    } else {
+
+        if (!openAudioFile(path, AskUser)) {
+
+            bool canImportLayer = (getMainModel() != 0 &&
+                                   m_paneStack != 0 &&
+                                   m_paneStack->getCurrentPane() != 0);
+
+            if (!canImportLayer || !openLayerFile(path)) {
+
+                QMessageBox::critical(this, tr("Failed to open file"),
+                                      tr("File \"%1\" could not be opened").arg(path));
+            }
+        }
+    }
+}
+
+bool
+MainWindow::openSomeFile(QString path)
+{
+    if (openAudioFile(path)) {
+	return true;
+    } else if (openSessionFile(path)) {
+	return true;
+    } else {
+	return false;
+    }
+}
+
+bool
+MainWindow::openSessionFile(QString path)
+{
+    BZipFileDevice bzFile(path);
+    if (!bzFile.open(QIODevice::ReadOnly)) {
+        std::cerr << "Failed to open session file \"" << path.toStdString()
+                  << "\": " << bzFile.errorString().toStdString() << std::endl;
+        return false;
+    }
+
+    QString error;
+    closeSession();
+    createDocument();
+
+    PaneCallback callback(this);
+    m_viewManager->clearSelections();
+
+    SVFileReader reader(m_document, callback);
+    QXmlInputSource inputSource(&bzFile);
+    reader.parse(inputSource);
+    
+    if (!reader.isOK()) {
+        error = tr("SV XML file read error:\n%1").arg(reader.getErrorString());
+    }
+    
+    bzFile.close();
+
+    bool ok = (error == "");
+    
+    if (ok) {
+	setWindowTitle(tr("Sonic Visualiser: %1")
+		       .arg(QFileInfo(path).fileName()));
+	m_sessionFile = path;
+	setupMenus();
+	CommandHistory::getInstance()->clear();
+	CommandHistory::getInstance()->documentSaved();
+	m_documentModified = false;
+	updateMenuStates();
+        RecentFiles::getInstance()->addFile(path);
+    } else {
+	setWindowTitle(tr("Sonic Visualiser"));
+    }
+
+    return ok;
+}
+
+void
+MainWindow::closeEvent(QCloseEvent *e)
+{
+    if (!checkSaveModified()) {
+	e->ignore();
+	return;
+    }
+
+    e->accept();
+    return;
+}
+
+bool
+MainWindow::checkSaveModified()
+{
+    // Called before some destructive operation (e.g. new session,
+    // exit program).  Return true if we can safely proceed, false to
+    // cancel.
+
+    if (!m_documentModified) return true;
+
+    int button = 
+	QMessageBox::warning(this,
+			     tr("Session modified"),
+			     tr("The current session has been modified.\nDo you want to save it?"),
+			     QMessageBox::Yes,
+			     QMessageBox::No,
+			     QMessageBox::Cancel);
+
+    if (button == QMessageBox::Yes) {
+	saveSession();
+	if (m_documentModified) { // save failed -- don't proceed!
+	    return false;
+	} else {
+            return true; // saved, so it's safe to continue now
+        }
+    } else if (button == QMessageBox::No) {
+	m_documentModified = false; // so we know to abandon it
+	return true;
+    }
+
+    // else cancel
+    return false;
+}
+
+void
+MainWindow::saveSession()
+{
+    if (m_sessionFile != "") {
+	if (!saveSessionFile(m_sessionFile)) {
+	    QMessageBox::critical(this, tr("Failed to save file"),
+				  tr("Session file \"%1\" could not be saved.").arg(m_sessionFile));
+	} else {
+	    CommandHistory::getInstance()->documentSaved();
+	    documentRestored();
+	}
+    } else {
+	saveSessionAs();
+    }
+}
+
+void
+MainWindow::saveSessionAs()
+{
+    QString orig = m_audioFile;
+    if (orig == "") orig = ".";
+    else orig = QFileInfo(orig).absoluteDir().canonicalPath();
+
+    QString path;
+    bool good = false;
+
+    while (!good) {
+
+	path = QFileDialog::getSaveFileName
+	    (this, tr("Select a file to save to"), orig,
+	     tr("Sonic Visualiser session files (*.sv)\nAll files (*.*)"), 0,
+	     QFileDialog::DontConfirmOverwrite); // we'll do that
+
+	if (path.isEmpty()) return;
+
+	if (!path.endsWith(".sv")) path = path + ".sv";
+
+	QFileInfo fi(path);
+
+	if (fi.isDir()) {
+	    QMessageBox::critical(this, tr("Directory selected"),
+				  tr("File \"%1\" is a directory").arg(path));
+	    continue;
+	}
+
+	if (fi.exists()) {
+	    if (QMessageBox::question(this, tr("File exists"),
+				      tr("The file \"%1\" already exists.\nDo you want to overwrite it?").arg(path),
+				      QMessageBox::Ok,
+				      QMessageBox::Cancel) == QMessageBox::Ok) {
+		good = true;
+	    } else {
+		continue;
+	    }
+	}
+
+	good = true;
+    }
+
+    if (!saveSessionFile(path)) {
+	QMessageBox::critical(this, tr("Failed to save file"),
+			      tr("Session file \"%1\" could not be saved.").arg(m_sessionFile));
+    } else {
+	setWindowTitle(tr("Sonic Visualiser: %1")
+		       .arg(QFileInfo(path).fileName()));
+	m_sessionFile = path;
+	CommandHistory::getInstance()->documentSaved();
+	documentRestored();
+        RecentFiles::getInstance()->addFile(path);
+    }
+}
+
+bool
+MainWindow::saveSessionFile(QString path)
+{
+    BZipFileDevice bzFile(path);
+    if (!bzFile.open(QIODevice::WriteOnly)) {
+        std::cerr << "Failed to open session file \"" << path.toStdString()
+                  << "\" for writing: "
+                  << bzFile.errorString().toStdString() << std::endl;
+        return false;
+    }
+
+    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
+
+    QTextStream out(&bzFile);
+    toXml(out);
+    out.flush();
+
+    QApplication::restoreOverrideCursor();
+
+    if (bzFile.errorString() != "") {
+	QMessageBox::critical(this, tr("Failed to write file"),
+			      tr("Failed to write to file \"%1\": %2")
+			      .arg(path).arg(bzFile.errorString()));
+        bzFile.close();
+	return false;
+    }
+
+    bzFile.close();
+    return true;
+}
+
+void
+MainWindow::toXml(QTextStream &out)
+{
+    QString indent("  ");
+
+    out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
+    out << "<!DOCTYPE sonic-visualiser>\n";
+    out << "<sv>\n";
+
+    m_document->toXml(out, "", "");
+
+    out << "<display>\n";
+
+    out << QString("  <window width=\"%1\" height=\"%2\"/>\n")
+	.arg(width()).arg(height());
+
+    for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
+
+	Pane *pane = m_paneStack->getPane(i);
+
+	if (pane) {
+            pane->toXml(out, indent);
+	}
+    }
+
+    out << "</display>\n";
+
+    m_viewManager->getSelection().toXml(out);
+
+    out << "</sv>\n";
+}
+
+Pane *
+MainWindow::addPaneToStack()
+{
+    AddPaneCommand *command = new AddPaneCommand(this);
+    CommandHistory::getInstance()->addCommand(command);
+    return command->getPane();
+}
+
+void
+MainWindow::zoomIn()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->zoom(true);
+}
+
+void
+MainWindow::zoomOut()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->zoom(false);
+}
+
+void
+MainWindow::zoomToFit()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (!currentPane) return;
+
+    Model *model = getMainModel();
+    if (!model) return;
+    
+    size_t start = model->getStartFrame();
+    size_t end = model->getEndFrame();
+    size_t pixels = currentPane->width();
+    size_t zoomLevel = (end - start) / pixels;
+
+    currentPane->setZoomLevel(zoomLevel);
+    currentPane->setStartFrame(start);
+}
+
+void
+MainWindow::zoomDefault()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->setZoomLevel(1024);
+}
+
+void
+MainWindow::scrollLeft()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->scroll(false, false);
+}
+
+void
+MainWindow::jumpLeft()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->scroll(false, true);
+}
+
+void
+MainWindow::scrollRight()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->scroll(true, false);
+}
+
+void
+MainWindow::jumpRight()
+{
+    Pane *currentPane = m_paneStack->getCurrentPane();
+    if (currentPane) currentPane->scroll(true, true);
+}
+
+void
+MainWindow::showNoOverlays()
+{
+    m_viewManager->setOverlayMode(ViewManager::NoOverlays);
+}
+
+void
+MainWindow::showBasicOverlays()
+{
+    m_viewManager->setOverlayMode(ViewManager::BasicOverlays);
+}
+
+void
+MainWindow::showAllOverlays()
+{
+    m_viewManager->setOverlayMode(ViewManager::AllOverlays);
+}
+
+void
+MainWindow::play()
+{
+    if (m_playSource->isPlaying()) {
+	m_playSource->stop();
+    } else {
+	m_playSource->play(m_viewManager->getPlaybackFrame());
+    }
+}
+
+void
+MainWindow::ffwd()
+{
+    if (!getMainModel()) return;
+
+    int frame = m_viewManager->getPlaybackFrame();
+    ++frame;
+
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (!pane) return;
+
+    Layer *layer = pane->getSelectedLayer();
+
+    if (!dynamic_cast<TimeInstantLayer *>(layer) &&
+        !dynamic_cast<TimeValueLayer *>(layer)) return;
+
+    size_t resolution = 0;
+    if (!layer->snapToFeatureFrame(pane, frame, resolution, Layer::SnapRight)) {
+        frame = getMainModel()->getEndFrame();
+    }
+    
+    m_viewManager->setPlaybackFrame(frame);
+}
+
+void
+MainWindow::ffwdEnd()
+{
+    if (!getMainModel()) return;
+    m_viewManager->setPlaybackFrame(getMainModel()->getEndFrame());
+}
+
+void
+MainWindow::rewind()
+{
+    if (!getMainModel()) return;
+
+    int frame = m_viewManager->getPlaybackFrame();
+    if (frame > 0) --frame;
+
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (!pane) return;
+
+    Layer *layer = pane->getSelectedLayer();
+
+    if (!dynamic_cast<TimeInstantLayer *>(layer) &&
+        !dynamic_cast<TimeValueLayer *>(layer)) return;
+
+    size_t resolution = 0;
+    if (!layer->snapToFeatureFrame(pane, frame, resolution, Layer::SnapLeft)) {
+        frame = getMainModel()->getEndFrame();
+    }
+    
+    m_viewManager->setPlaybackFrame(frame);
+}
+
+void
+MainWindow::rewindStart()
+{
+    if (!getMainModel()) return;
+    m_viewManager->setPlaybackFrame(getMainModel()->getStartFrame());
+}
+
+void
+MainWindow::stop()
+{
+    m_playSource->stop();
+}
+
+void
+MainWindow::addPane()
+{
+    QObject *s = sender();
+    QAction *action = dynamic_cast<QAction *>(s);
+    
+    if (!action) {
+	std::cerr << "WARNING: MainWindow::addPane: sender is not an action"
+		  << std::endl;
+	return;
+    }
+
+    PaneActionMap::iterator i = m_paneActions.find(action);
+
+    if (i == m_paneActions.end()) {
+	std::cerr << "WARNING: MainWindow::addPane: unknown action "
+		  << action->objectName().toStdString() << std::endl;
+	return;
+    }
+
+    CommandHistory::getInstance()->startCompoundOperation
+	(action->text(), true);
+
+    AddPaneCommand *command = new AddPaneCommand(this);
+    CommandHistory::getInstance()->addCommand(command);
+
+    Pane *pane = command->getPane();
+
+    if (i->second.layer != LayerFactory::TimeRuler) {
+	if (!m_timeRulerLayer) {
+//	    std::cerr << "no time ruler layer, creating one" << std::endl;
+	    m_timeRulerLayer = m_document->createMainModelLayer
+		(LayerFactory::TimeRuler);
+	}
+
+//	std::cerr << "adding time ruler layer " << m_timeRulerLayer << std::endl;
+
+	m_document->addLayerToView(pane, m_timeRulerLayer);
+    }
+
+    Layer *newLayer = m_document->createLayer(i->second.layer);
+    m_document->setModel(newLayer, m_document->getMainModel());
+    m_document->setChannel(newLayer, i->second.channel);
+    m_document->addLayerToView(pane, newLayer);
+
+    m_paneStack->setCurrentPane(pane);
+
+    CommandHistory::getInstance()->endCompoundOperation();
+
+    updateMenuStates();
+}
+
+MainWindow::AddPaneCommand::AddPaneCommand(MainWindow *mw) :
+    m_mw(mw),
+    m_pane(0),
+    m_prevCurrentPane(0),
+    m_added(false)
+{
+}
+
+MainWindow::AddPaneCommand::~AddPaneCommand()
+{
+    if (m_pane && !m_added) {
+	m_mw->m_paneStack->deletePane(m_pane);
+    }
+}
+
+QString
+MainWindow::AddPaneCommand::getName() const
+{
+    return tr("Add Pane");
+}
+
+void
+MainWindow::AddPaneCommand::execute()
+{
+    if (!m_pane) {
+	m_prevCurrentPane = m_mw->m_paneStack->getCurrentPane();
+	m_pane = m_mw->m_paneStack->addPane();
+    } else {
+	m_mw->m_paneStack->showPane(m_pane);
+    }
+
+    m_mw->m_paneStack->setCurrentPane(m_pane);
+    m_mw->m_panner->registerView(m_pane);
+    m_added = true;
+}
+
+void
+MainWindow::AddPaneCommand::unexecute()
+{
+    m_mw->m_paneStack->hidePane(m_pane);
+    m_mw->m_paneStack->setCurrentPane(m_prevCurrentPane);
+    m_mw->m_panner->unregisterView(m_pane); 
+    m_added = false;
+}
+
+MainWindow::RemovePaneCommand::RemovePaneCommand(MainWindow *mw, Pane *pane) :
+    m_mw(mw),
+    m_pane(pane),
+    m_added(true)
+{
+}
+
+MainWindow::RemovePaneCommand::~RemovePaneCommand()
+{
+    if (m_pane && !m_added) {
+	m_mw->m_paneStack->deletePane(m_pane);
+    }
+}
+
+QString
+MainWindow::RemovePaneCommand::getName() const
+{
+    return tr("Remove Pane");
+}
+
+void
+MainWindow::RemovePaneCommand::execute()
+{
+    m_prevCurrentPane = m_mw->m_paneStack->getCurrentPane();
+    m_mw->m_paneStack->hidePane(m_pane);
+    m_mw->m_panner->unregisterView(m_pane);
+    m_added = false;
+}
+
+void
+MainWindow::RemovePaneCommand::unexecute()
+{
+    m_mw->m_paneStack->showPane(m_pane);
+    m_mw->m_paneStack->setCurrentPane(m_prevCurrentPane);
+    m_mw->m_panner->registerView(m_pane);
+    m_added = true;
+}
+
+void
+MainWindow::addLayer()
+{
+    QObject *s = sender();
+    QAction *action = dynamic_cast<QAction *>(s);
+    
+    if (!action) {
+	std::cerr << "WARNING: MainWindow::addLayer: sender is not an action"
+		  << std::endl;
+	return;
+    }
+
+    Pane *pane = m_paneStack->getCurrentPane();
+    
+    if (!pane) {
+	std::cerr << "WARNING: MainWindow::addLayer: no current pane" << std::endl;
+	return;
+    }
+
+    ExistingLayerActionMap::iterator ei = m_existingLayerActions.find(action);
+
+    if (ei != m_existingLayerActions.end()) {
+	Layer *newLayer = ei->second;
+	m_document->addLayerToView(pane, newLayer);
+	m_paneStack->setCurrentLayer(pane, newLayer);
+	return;
+    }
+
+    TransformActionMap::iterator i = m_layerTransformActions.find(action);
+
+    if (i == m_layerTransformActions.end()) {
+
+	LayerActionMap::iterator i = m_layerActions.find(action);
+	
+	if (i == m_layerActions.end()) {
+	    std::cerr << "WARNING: MainWindow::addLayer: unknown action "
+		      << action->objectName().toStdString() << std::endl;
+	    return;
+	}
+
+	LayerFactory::LayerType type = i->second;
+	
+	LayerFactory::LayerTypeSet emptyTypes =
+	    LayerFactory::getInstance()->getValidEmptyLayerTypes();
+
+	Layer *newLayer;
+
+	if (emptyTypes.find(type) != emptyTypes.end()) {
+
+	    newLayer = m_document->createEmptyLayer(type);
+	    m_toolActions[ViewManager::DrawMode]->trigger();
+
+	} else {
+
+	    newLayer = m_document->createMainModelLayer(type);
+	}
+
+	m_document->addLayerToView(pane, newLayer);
+	m_paneStack->setCurrentLayer(pane, newLayer);
+
+	return;
+    }
+
+    TransformName transform = i->second;
+    TransformFactory *factory = TransformFactory::getInstance();
+
+    QString configurationXml;
+
+    int channel = -1;
+    // pick up the default channel from any existing layers on the same pane
+    for (int j = 0; j < pane->getLayerCount(); ++j) {
+	int c = LayerFactory::getInstance()->getChannel(pane->getLayer(j));
+	if (c != -1) {
+	    channel = c;
+	    break;
+	}
+    }
+
+    bool needConfiguration = false;
+
+    if (factory->isTransformConfigurable(transform)) {
+        needConfiguration = true;
+    } else {
+        int minChannels, maxChannels;
+        int myChannels = m_document->getMainModel()->getChannelCount();
+        if (factory->getTransformChannelRange(transform,
+                                              minChannels,
+                                              maxChannels)) {
+//            std::cerr << "myChannels: " << myChannels << ", minChannels: " << minChannels << ", maxChannels: " << maxChannels << std::endl;
+            needConfiguration = (myChannels > maxChannels && maxChannels == 1);
+        }
+    }
+
+    if (needConfiguration) {
+        bool ok =
+            factory->getConfigurationForTransform
+            (transform, m_document->getMainModel(), channel, configurationXml);
+        if (!ok) return;
+    }
+
+    Layer *newLayer = m_document->createDerivedLayer(transform,
+                                                     m_document->getMainModel(),
+                                                     channel,
+                                                     configurationXml);
+
+    if (newLayer) {
+        m_document->addLayerToView(pane, newLayer);
+        m_document->setChannel(newLayer, channel);
+    }
+
+    updateMenuStates();
+}
+
+void
+MainWindow::deleteCurrentPane()
+{
+    CommandHistory::getInstance()->startCompoundOperation
+	(tr("Delete Pane"), true);
+
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (pane) {
+	while (pane->getLayerCount() > 0) {
+	    Layer *layer = pane->getLayer(0);
+	    if (layer) {
+		m_document->removeLayerFromView(pane, layer);
+	    } else {
+		break;
+	    }
+	}
+
+	RemovePaneCommand *command = new RemovePaneCommand(this, pane);
+	CommandHistory::getInstance()->addCommand(command);
+    }
+
+    CommandHistory::getInstance()->endCompoundOperation();
+
+    updateMenuStates();
+}
+
+void
+MainWindow::deleteCurrentLayer()
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (pane) {
+	Layer *layer = pane->getSelectedLayer();
+	if (layer) {
+	    m_document->removeLayerFromView(pane, layer);
+	}
+    }
+    updateMenuStates();
+}
+
+void
+MainWindow::renameCurrentLayer()
+{
+    Pane *pane = m_paneStack->getCurrentPane();
+    if (pane) {
+	Layer *layer = pane->getSelectedLayer();
+	if (layer) {
+	    bool ok = false;
+	    QString newName = QInputDialog::getText
+		(this, tr("Rename Layer"),
+		 tr("New name for this layer:"),
+		 QLineEdit::Normal, layer->objectName(), &ok);
+	    if (ok) {
+		layer->setObjectName(newName);
+		setupExistingLayersMenu();
+	    }
+	}
+    }
+}
+
+void
+MainWindow::playSpeedChanged(int speed)
+{
+    int factor = 11 - speed;
+    m_playSpeed->setToolTip(tr("Playback speed: %1")
+			    .arg(factor > 1 ?
+				 QString("1/%1").arg(factor) :
+				 tr("Full")));
+    m_playSource->setSlowdownFactor(factor);
+}
+
+void
+MainWindow::outputLevelsChanged(float left, float right)
+{
+    m_fader->setPeakLeft(left);
+    m_fader->setPeakRight(right);
+}
+
+void
+MainWindow::sampleRateMismatch(size_t requested, size_t actual,
+                               bool willResample)
+{
+    if (!willResample) {
+        //!!! more helpful message needed
+        QMessageBox::information
+            (this, tr("Sample rate mismatch"),
+             tr("The sample rate of this audio file (%1 Hz) does not match\nthe current playback rate (%2 Hz).\n\nThe file will play at the wrong speed.")
+             .arg(requested).arg(actual));
+    }        
+
+/*!!! Let's not do this for now, and see how we go -- now that we're putting
+      sample rate information in the status bar
+
+    QMessageBox::information
+	(this, tr("Sample rate mismatch"),
+	 tr("The sample rate of this audio file (%1 Hz) does not match\nthat of the output audio device (%2 Hz).\n\nThe file will be resampled automatically during playback.")
+	 .arg(requested).arg(actual));
+*/
+
+    updateDescriptionLabel();
+}
+
+void
+MainWindow::layerAdded(Layer *layer)
+{
+//    std::cerr << "MainWindow::layerAdded(" << layer << ")" << std::endl;
+//    setupExistingLayersMenu();
+    updateMenuStates();
+}
+
+void
+MainWindow::layerRemoved(Layer *layer)
+{
+//    std::cerr << "MainWindow::layerRemoved(" << layer << ")" << std::endl;
+    setupExistingLayersMenu();
+    updateMenuStates();
+}
+
+void
+MainWindow::layerAboutToBeDeleted(Layer *layer)
+{
+//    std::cerr << "MainWindow::layerAboutToBeDeleted(" << layer << ")" << std::endl;
+    if (layer == m_timeRulerLayer) {
+//	std::cerr << "(this is the time ruler layer)" << std::endl;
+	m_timeRulerLayer = 0;
+    }
+}
+
+void
+MainWindow::layerInAView(Layer *layer, bool inAView)
+{
+//    std::cerr << "MainWindow::layerInAView(" << layer << "," << inAView << ")" << std::endl;
+
+    // Check whether we need to add or remove model from play source
+    Model *model = layer->getModel();
+    if (model) {
+        if (inAView) {
+            m_playSource->addModel(model);
+        } else {
+            bool found = false;
+            for (int i = 0; i < m_paneStack->getPaneCount(); ++i) {
+                Pane *pane = m_paneStack->getPane(i);
+                if (!pane) continue;
+                for (int j = 0; j < pane->getLayerCount(); ++j) {
+                    Layer *pl = pane->getLayer(j);
+                    if (pl && pl->getModel() == model) {
+                        found = true;
+                        break;
+                    }
+                }
+                if (found) break;
+            }
+            if (!found) m_playSource->removeModel(model);
+        }
+    }
+
+    setupExistingLayersMenu();
+    updateMenuStates();
+}
+
+void
+MainWindow::modelAdded(Model *model)
+{
+//    std::cerr << "MainWindow::modelAdded(" << model << ")" << std::endl;
+    m_playSource->addModel(model);
+}
+
+void
+MainWindow::mainModelChanged(WaveFileModel *model)
+{
+//    std::cerr << "MainWindow::mainModelChanged(" << model << ")" << std::endl;
+    updateDescriptionLabel();
+    m_panLayer->setModel(model);
+    if (model) m_viewManager->setMainModelSampleRate(model->getSampleRate());
+    if (model && !m_playTarget) createPlayTarget();
+}
+
+void
+MainWindow::modelAboutToBeDeleted(Model *model)
+{
+//    std::cerr << "MainWindow::modelAboutToBeDeleted(" << model << ")" << std::endl;
+    m_playSource->removeModel(model);
+}
+
+void
+MainWindow::modelGenerationFailed(QString transformName)
+{
+    QMessageBox::warning
+        (this,
+         tr("Failed to generate layer"),
+         tr("The layer transform \"%1\" failed to run.\nThis probably means that a plugin failed to initialise.")
+         .arg(transformName),
+         QMessageBox::Ok, 0);
+}
+
+void
+MainWindow::modelRegenerationFailed(QString layerName, QString transformName)
+{
+    QMessageBox::warning
+        (this,
+         tr("Failed to regenerate layer"),
+         tr("Failed to regenerate derived layer \"%1\".\nThe layer transform \"%2\" failed to run.\nThis probably means the layer used a plugin that is not currently available.")
+         .arg(layerName).arg(transformName),
+         QMessageBox::Ok, 0);
+}
+
+void
+MainWindow::rightButtonMenuRequested(Pane *pane, QPoint position)
+{
+//    std::cerr << "MainWindow::rightButtonMenuRequested(" << pane << ", " << position.x() << ", " << position.y() << ")" << std::endl;
+    m_paneStack->setCurrentPane(pane);
+    m_rightButtonMenu->popup(position);
+}
+
+void
+MainWindow::showLayerTree()
+{
+    QTreeView *view = new QTreeView();
+    LayerTreeModel *tree = new LayerTreeModel(m_paneStack);
+    view->expand(tree->index(0, 0, QModelIndex()));
+    view->setModel(tree);
+    view->show();
+}
+
+void
+MainWindow::preferenceChanged(PropertyContainer::PropertyName name)
+{
+    if (name == "Property Box Layout") {
+        if (Preferences::getInstance()->getPropertyBoxLayout() ==
+            Preferences::VerticallyStacked) {
+            m_paneStack->setLayoutStyle(PaneStack::PropertyStackPerPaneLayout);
+        } else {
+            m_paneStack->setLayoutStyle(PaneStack::SinglePropertyStackLayout);
+        }
+    }
+}
+
+void
+MainWindow::preferences()
+{
+    if (!m_preferencesDialog.isNull()) {
+        m_preferencesDialog->show();
+        m_preferencesDialog->raise();
+        return;
+    }
+
+    m_preferencesDialog = new PreferencesDialog(this);
+
+    // DeleteOnClose is safe here, because m_preferencesDialog is a
+    // QPointer that will be zeroed when the dialog is deleted.  We
+    // use it in preference to leaving the dialog lying around because
+    // if you Cancel the dialog, it resets the preferences state
+    // without resetting its own widgets, so its state will be
+    // incorrect when next shown unless we construct it afresh
+    m_preferencesDialog->setAttribute(Qt::WA_DeleteOnClose);
+
+    m_preferencesDialog->show();
+}
+
+void
+MainWindow::website()
+{
+    openHelpUrl(tr("http://www.sonicvisualiser.org/"));
+}
+
+void
+MainWindow::help()
+{
+    openHelpUrl(tr("http://www.sonicvisualiser.org/doc/reference/en/"));
+}
+
+void
+MainWindow::openHelpUrl(QString url)
+{
+    // This method mostly lifted from Qt Assistant source code
+
+    QProcess *process = new QProcess(this);
+    connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
+
+    QStringList args;
+
+#ifdef Q_OS_MAC
+    args.append(url);
+    process->start("open", args);
+#else
+#ifdef Q_OS_WIN32
+
+	QString pf(getenv("ProgramFiles"));
+	QString command = pf + QString("\\Internet Explorer\\IEXPLORE.EXE");
+
+	args.append(url);
+	process->start(command, args);
+
+#else
+#ifdef Q_WS_X11
+    if (!qgetenv("KDE_FULL_SESSION").isEmpty()) {
+        args.append("exec");
+        args.append(url);
+        process->start("kfmclient", args);
+    } else if (!qgetenv("BROWSER").isEmpty()) {
+        args.append(url);
+        process->start(qgetenv("BROWSER"), args);
+    } else {
+        args.append(url);
+        process->start("firefox", args);
+    }
+#endif
+#endif
+#endif
+}
+
+void
+MainWindow::about()
+{
+    bool debug = false;
+    QString version = "(unknown version)";
+
+#ifdef BUILD_DEBUG
+    debug = true;
+#endif
+#ifdef SV_VERSION
+#ifdef SVNREV
+    version = tr("Release %1 : Revision %2").arg(SV_VERSION).arg(SVNREV);
+#else
+    version = tr("Release %1").arg(SV_VERSION);
+#endif
+#else
+#ifdef SVNREV
+    version = tr("Unreleased : Revision %1").arg(SVNREV);
+#endif
+#endif
+
+    QString aboutText;
+
+    aboutText += tr("<h3>About Sonic Visualiser</h3>");
+    aboutText += tr("<p>Sonic Visualiser is a program for viewing and exploring audio data for semantic music analysis and annotation.</p>");
+    aboutText += tr("<p>%1 : %2 build</p>")
+        .arg(version)
+        .arg(debug ? tr("Debug") : tr("Release"));
+
+#ifdef BUILD_STATIC
+    aboutText += tr("<p>Statically linked");
+#ifndef QT_SHARED
+    aboutText += tr("<br>With Qt (v%1) &copy; Trolltech AS").arg(QT_VERSION_STR);
+#endif
+#ifdef HAVE_JACK
+    aboutText += tr("<br>With JACK audio output (v%1) &copy; Paul Davis and Jack O'Quin").arg(JACK_VERSION);
+#endif
+#ifdef HAVE_PORTAUDIO
+    aboutText += tr("<br>With PortAudio audio output &copy; Ross Bencina and Phil Burk");
+#endif
+#ifdef HAVE_OGGZ
+    aboutText += tr("<br>With Ogg file decoder (oggz v%1, fishsound v%2) &copy; CSIRO Australia").arg(OGGZ_VERSION).arg(FISHSOUND_VERSION);
+#endif
+#ifdef HAVE_MAD
+    aboutText += tr("<br>With MAD mp3 decoder (v%1) &copy; Underbit Technologies Inc").arg(MAD_VERSION);
+#endif
+#ifdef HAVE_SAMPLERATE
+    aboutText += tr("<br>With libsamplerate (v%1) &copy; Erik de Castro Lopo").arg(SAMPLERATE_VERSION);
+#endif
+#ifdef HAVE_SNDFILE
+    aboutText += tr("<br>With libsndfile (v%1) &copy; Erik de Castro Lopo").arg(SNDFILE_VERSION);
+#endif
+#ifdef HAVE_FFTW3
+    aboutText += tr("<br>With FFTW3 (v%1) &copy; Matteo Frigo and MIT").arg(FFTW3_VERSION);
+#endif
+#ifdef HAVE_VAMP
+    aboutText += tr("<br>With Vamp plugin support (API v%1, SDK v%2) &copy; Chris Cannam").arg(VAMP_API_VERSION).arg(VAMP_SDK_VERSION);
+#endif
+    aboutText += tr("<br>With LADSPA plugin support (API v%1) &copy; Richard Furse, Paul Davis, Stefan Westerfeld").arg(LADSPA_VERSION);
+    aboutText += tr("<br>With DSSI plugin support (API v%1) &copy; Chris Cannam, Steve Harris, Sean Bolton").arg(DSSI_VERSION);
+    aboutText += "</p>";
+#endif
+
+    aboutText += 
+        "<p>Sonic Visualiser Copyright &copy; 2005 - 2006 Chris Cannam<br>"
+        "Centre for Digital Music, Queen Mary, University of London.</p>"
+        "<p>This program is free software; you can redistribute it and/or<br>"
+        "modify it under the terms of the GNU General Public License as<br>"
+        "published by the Free Software Foundation; either version 2 of the<br>"
+        "License, or (at your option) any later version.<br>See the file "
+        "COPYING included with this distribution for more information.</p>";
+    
+    QMessageBox::about(this, tr("About Sonic Visualiser"), aboutText);
+}
+
+
+#ifdef INCLUDE_MOCFILES
+#include "MainWindow.moc.cpp"
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/MainWindow.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,315 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _MAIN_WINDOW_H_
+#define _MAIN_WINDOW_H_
+
+#include <QFrame>
+#include <QString>
+#include <QMainWindow>
+#include <QPointer>
+
+#include "base/Command.h"
+#include "base/ViewManager.h"
+#include "base/PropertyContainer.h"
+#include "layer/LayerFactory.h"
+#include "transform/Transform.h"
+#include "fileio/SVFileReader.h"
+#include <map>
+
+class Document;
+class PaneStack;
+class Pane;
+class View;
+class Fader;
+class Panner;
+class Layer;
+class WaveformLayer;
+class WaveFileModel;
+class AudioCallbackPlaySource;
+class AudioCallbackPlayTarget;
+class CommandHistory;
+class QMenu;
+class AudioDial;
+class QLabel;
+class PreferencesDialog;
+
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow();
+    virtual ~MainWindow();
+    
+    enum AudioFileOpenMode {
+        ReplaceMainModel,
+        CreateAdditionalModel,
+        AskUser
+    };
+
+    bool openSomeFile(QString path);
+    bool openAudioFile(QString path, AudioFileOpenMode = AskUser);
+    bool openLayerFile(QString path);
+    bool openSessionFile(QString path);
+    bool saveSessionFile(QString path);
+
+signals:
+    // Used to toggle the availability of menu actions
+    void canAddPane(bool);
+    void canDeleteCurrentPane(bool);
+    void canAddLayer(bool);
+    void canImportMoreAudio(bool);
+    void canImportLayer(bool);
+    void canExportAudio(bool);
+    void canExportLayer(bool);
+    void canRenameLayer(bool);
+    void canEditLayer(bool);
+    void canSelect(bool);
+    void canClearSelection(bool);
+    void canEditSelection(bool);
+    void canPaste(bool);
+    void canInsertInstant(bool);
+    void canDeleteCurrentLayer(bool);
+    void canZoom(bool);
+    void canScroll(bool);
+    void canPlay(bool);
+    void canFfwd(bool);
+    void canRewind(bool);
+    void canPlaySelection(bool);
+    void canSave(bool);
+
+protected slots:
+    void openSession();
+    void importAudio();
+    void importMoreAudio();
+    void openSomething();
+    void openRecentFile();
+    void exportAudio();
+    void importLayer();
+    void exportLayer();
+    void saveSession();
+    void saveSessionAs();
+    void newSession();
+    void closeSession();
+    void preferences();
+
+    void zoomIn();
+    void zoomOut();
+    void zoomToFit();
+    void zoomDefault();
+    void scrollLeft();
+    void scrollRight();
+    void jumpLeft();
+    void jumpRight();
+
+    void showNoOverlays();
+    void showBasicOverlays();
+    void showAllOverlays();
+
+    void play();
+    void ffwd();
+    void ffwdEnd();
+    void rewind();
+    void rewindStart();
+    void stop();
+
+    void addPane();
+    void addLayer();
+    void deleteCurrentPane();
+    void renameCurrentLayer();
+    void deleteCurrentLayer();
+
+    void playLoopToggled();
+    void playSelectionToggled();
+    void playSpeedChanged(int);
+    void sampleRateMismatch(size_t, size_t, bool);
+
+    void outputLevelsChanged(float, float);
+
+    void currentPaneChanged(Pane *);
+    void currentLayerChanged(Pane *, Layer *);
+
+    void toolNavigateSelected();
+    void toolSelectSelected();
+    void toolEditSelected();
+    void toolDrawSelected();
+
+    void selectAll();
+    void selectToStart();
+    void selectToEnd();
+    void selectVisible();
+    void clearSelection();
+    void cut();
+    void copy();
+    void paste();
+    void deleteSelected();
+    void insertInstant();
+
+    void documentModified();
+    void documentRestored();
+
+    void updateMenuStates();
+    void updateDescriptionLabel();
+
+    void layerAdded(Layer *);
+    void layerRemoved(Layer *);
+    void layerAboutToBeDeleted(Layer *);
+    void layerInAView(Layer *, bool);
+
+    void mainModelChanged(WaveFileModel *);
+    void modelAdded(Model *);
+    void modelAboutToBeDeleted(Model *);
+
+    void modelGenerationFailed(QString);
+    void modelRegenerationFailed(QString, QString);
+
+    void rightButtonMenuRequested(Pane *, QPoint point);
+
+    void preferenceChanged(PropertyContainer::PropertyName);
+
+    void setupRecentFilesMenu();
+
+    void showLayerTree();
+
+    void website();
+    void help();
+    void about();
+
+protected:
+    QString                  m_sessionFile;
+    QString                  m_audioFile;
+    Document                *m_document;
+
+    QLabel                  *m_descriptionLabel;
+    PaneStack               *m_paneStack;
+    ViewManager             *m_viewManager;
+    Panner                  *m_panner;
+    Fader                   *m_fader;
+    AudioDial               *m_playSpeed;
+    WaveformLayer           *m_panLayer;
+    Layer                   *m_timeRulerLayer;
+
+    AudioCallbackPlaySource *m_playSource;
+    AudioCallbackPlayTarget *m_playTarget;
+
+    bool                     m_mainMenusCreated;
+    QMenu                   *m_paneMenu;
+    QMenu                   *m_layerMenu;
+    QMenu                   *m_existingLayersMenu;
+    QMenu                   *m_recentFilesMenu;
+    QMenu                   *m_rightButtonMenu;
+    QMenu                   *m_rightButtonLayerMenu;
+
+    bool                     m_documentModified;
+
+    QPointer<PreferencesDialog> m_preferencesDialog;
+
+    WaveFileModel *getMainModel();
+    void createDocument();
+
+    struct PaneConfiguration {
+	PaneConfiguration(LayerFactory::LayerType _layer
+			                       = LayerFactory::TimeRuler,
+			  int _channel = -1) :
+	    layer(_layer), channel(_channel) { }
+	LayerFactory::LayerType layer;
+	int channel;
+    };
+
+    typedef std::map<QAction *, PaneConfiguration> PaneActionMap;
+    PaneActionMap m_paneActions;
+
+    typedef std::map<QAction *, TransformName> TransformActionMap;
+    TransformActionMap m_layerTransformActions;
+
+    typedef std::map<QAction *, LayerFactory::LayerType> LayerActionMap;
+    LayerActionMap m_layerActions;
+
+    typedef std::map<QAction *, Layer *> ExistingLayerActionMap;
+    ExistingLayerActionMap m_existingLayerActions;
+
+    typedef std::map<ViewManager::ToolMode, QAction *> ToolActionMap;
+    ToolActionMap m_toolActions;
+
+    void setupMenus();
+    void setupExistingLayersMenu();
+    void setupToolbars();
+    Pane *addPaneToStack();
+
+    class PaneCallback : public SVFileReaderPaneCallback
+    {
+    public:
+	PaneCallback(MainWindow *mw) : m_mw(mw) { }
+	virtual Pane *addPane() { return m_mw->addPaneToStack(); }
+	virtual void setWindowSize(int width, int height) {
+	    m_mw->resize(width, height);
+	}
+	virtual void addSelection(int start, int end) {
+	    m_mw->m_viewManager->addSelection(Selection(start, end));
+	}
+    protected:
+	MainWindow *m_mw;
+    };
+
+    class AddPaneCommand : public Command
+    {
+    public:
+	AddPaneCommand(MainWindow *mw);
+	virtual ~AddPaneCommand();
+	
+	virtual void execute();
+	virtual void unexecute();
+	virtual QString getName() const;
+
+	Pane *getPane() { return m_pane; }
+
+    protected:
+	MainWindow *m_mw;
+	Pane *m_pane; // Main window owns this, but I determine its lifespan
+	Pane *m_prevCurrentPane; // I don't own this
+	bool m_added;
+    };
+
+    class RemovePaneCommand : public Command
+    {
+    public:
+	RemovePaneCommand(MainWindow *mw, Pane *pane);
+	virtual ~RemovePaneCommand();
+	
+	virtual void execute();
+	virtual void unexecute();
+	virtual QString getName() const;
+
+    protected:
+	MainWindow *m_mw;
+	Pane *m_pane; // Main window owns this, but I determine its lifespan
+	Pane *m_prevCurrentPane; // I don't own this
+	bool m_added;
+    };
+
+    virtual void closeEvent(QCloseEvent *e);
+    bool checkSaveModified();
+
+    void createPlayTarget();
+
+    void openHelpUrl(QString url);
+
+    void toXml(QTextStream &stream);
+};
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/PreferencesDialog.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,376 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "PreferencesDialog.h"
+
+#include <QGridLayout>
+#include <QComboBox>
+#include <QCheckBox>
+#include <QGroupBox>
+#include <QDoubleSpinBox>
+#include <QLabel>
+#include <QPushButton>
+#include <QHBoxLayout>
+#include <QPainter>
+#include <QPainterPath>
+#include <QFont>
+#include <QString>
+
+#include <fftw3.h>
+
+#include "base/Preferences.h"
+#include "fileio/ConfigFile.h"
+
+PreferencesDialog::PreferencesDialog(QWidget *parent, Qt::WFlags flags) :
+    QDialog(parent, flags)
+{
+    setWindowTitle(tr("Application Preferences"));
+
+    Preferences *prefs = Preferences::getInstance();
+
+    QGridLayout *grid = new QGridLayout;
+    setLayout(grid);
+    
+    QGroupBox *groupBox = new QGroupBox;
+    groupBox->setTitle(tr("Sonic Visualiser Application Preferences"));
+    grid->addWidget(groupBox, 0, 0);
+    
+    QGridLayout *subgrid = new QGridLayout;
+    groupBox->setLayout(subgrid);
+
+    // Create this first, as slots that get called from the ctor will
+    // refer to it
+    m_applyButton = new QPushButton(tr("Apply"));
+
+    // The WindowType enum is in rather a ragbag order -- reorder it here
+    // in a more sensible order
+    m_windows = new WindowType[9];
+    m_windows[0] = HanningWindow;
+    m_windows[1] = HammingWindow;
+    m_windows[2] = BlackmanWindow;
+    m_windows[3] = BlackmanHarrisWindow;
+    m_windows[4] = NuttallWindow;
+    m_windows[5] = GaussianWindow;
+    m_windows[6] = ParzenWindow;
+    m_windows[7] = BartlettWindow;
+    m_windows[8] = RectangularWindow;
+
+    QComboBox *windowCombo = new QComboBox;
+    int min, max, i;
+    int window = prefs->getPropertyRangeAndValue("Window Type", &min, &max);
+    m_windowType = window;
+    int index = 0;
+    
+    for (i = 0; i <= 8; ++i) {
+        windowCombo->addItem(prefs->getPropertyValueLabel("Window Type",
+                                                          m_windows[i]));
+        if (m_windows[i] == window) index = i;
+    }
+
+    windowCombo->setCurrentIndex(index);
+
+    m_windowTimeExampleLabel = new QLabel;
+    m_windowFreqExampleLabel = new QLabel;
+
+    connect(windowCombo, SIGNAL(currentIndexChanged(int)),
+            this, SLOT(windowTypeChanged(int)));
+    windowTypeChanged(index);
+
+    QCheckBox *smoothing = new QCheckBox;
+    m_smoothSpectrogram = prefs->getSmoothSpectrogram();
+    smoothing->setCheckState(m_smoothSpectrogram ?
+                             Qt::Checked : Qt::Unchecked);
+
+    connect(smoothing, SIGNAL(stateChanged(int)),
+            this, SLOT(smoothSpectrogramChanged(int)));
+
+    QComboBox *propertyLayout = new QComboBox;
+    int pl = prefs->getPropertyRangeAndValue("Property Box Layout", &min, &max);
+    m_propertyLayout = pl;
+
+    for (i = min; i <= max; ++i) {
+        propertyLayout->addItem(prefs->getPropertyValueLabel("Property Box Layout", i));
+    }
+
+    propertyLayout->setCurrentIndex(pl);
+
+    connect(propertyLayout, SIGNAL(currentIndexChanged(int)),
+            this, SLOT(propertyLayoutChanged(int)));
+
+    m_tuningFrequency = prefs->getTuningFrequency();
+
+    QDoubleSpinBox *frequency = new QDoubleSpinBox;
+    frequency->setMinimum(100.0);
+    frequency->setMaximum(5000.0);
+    frequency->setSuffix(" Hz");
+    frequency->setSingleStep(1);
+    frequency->setValue(m_tuningFrequency);
+    frequency->setDecimals(2);
+
+    connect(frequency, SIGNAL(valueChanged(double)),
+            this, SLOT(tuningFrequencyChanged(double)));
+
+    int row = 0;
+
+    subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel
+                                                ("Property Box Layout"))),
+                       row, 0);
+    subgrid->addWidget(propertyLayout, row++, 1, 1, 2);
+
+    subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel
+                                                ("Tuning Frequency"))),
+                       row, 0);
+    subgrid->addWidget(frequency, row++, 1, 1, 2);
+
+    subgrid->addWidget(new QLabel(prefs->getPropertyLabel
+                                  ("Smooth Spectrogram")),
+                       row, 0, 1, 2);
+    subgrid->addWidget(smoothing, row++, 2);
+
+    subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel
+                                                ("Window Type"))),
+                       row, 0);
+    subgrid->addWidget(windowCombo, row++, 1, 1, 2);
+
+    subgrid->addWidget(m_windowTimeExampleLabel, row, 1);
+    subgrid->addWidget(m_windowFreqExampleLabel, row, 2);
+    
+    QHBoxLayout *hbox = new QHBoxLayout;
+    grid->addLayout(hbox, 1, 0);
+    
+    QPushButton *ok = new QPushButton(tr("OK"));
+    QPushButton *cancel = new QPushButton(tr("Cancel"));
+    hbox->addStretch(10);
+    hbox->addWidget(ok);
+    hbox->addWidget(m_applyButton);
+    hbox->addWidget(cancel);
+    connect(ok, SIGNAL(clicked()), this, SLOT(okClicked()));
+    connect(m_applyButton, SIGNAL(clicked()), this, SLOT(applyClicked()));
+    connect(cancel, SIGNAL(clicked()), this, SLOT(cancelClicked()));
+
+    m_applyButton->setEnabled(false);
+}
+
+PreferencesDialog::~PreferencesDialog()
+{
+    std::cerr << "PreferencesDialog::~PreferencesDialog()" << std::endl;
+
+    delete[] m_windows;
+}
+
+void
+PreferencesDialog::windowTypeChanged(int value)
+{
+    int step = 24;
+    int peak = 48;
+    int w = step * 4, h = 64;
+    WindowType type = m_windows[value];
+    Window<float> windower = Window<float>(type, step * 2);
+
+    QPixmap timeLabel(w, h + 1);
+    timeLabel.fill(Qt::white);
+    QPainter timePainter(&timeLabel);
+
+    QPainterPath path;
+
+    path.moveTo(0, h - peak + 1);
+    path.lineTo(w, h - peak + 1);
+
+    timePainter.setPen(Qt::gray);
+    timePainter.setRenderHint(QPainter::Antialiasing, true);
+    timePainter.drawPath(path);
+    
+    path = QPainterPath();
+
+    float acc[w];
+    for (int i = 0; i < w; ++i) acc[i] = 0.f;
+    for (int j = 0; j < 3; ++j) {
+        for (int i = 0; i < step * 2; ++i) {
+            acc[j * step + i] += windower.getValue(i);
+        }
+    }
+    for (int i = 0; i < w; ++i) {
+        int y = h - int(peak * acc[i] + 0.001) + 1;
+        if (i == 0) path.moveTo(i, y);
+        else path.lineTo(i, y);
+    }
+
+    timePainter.drawPath(path);
+    timePainter.setRenderHint(QPainter::Antialiasing, false);
+
+    path = QPainterPath();
+
+    timePainter.setPen(Qt::black);
+    
+    for (int i = 0; i < step * 2; ++i) {
+        int y = h - int(peak * windower.getValue(i) + 0.001) + 1;
+        if (i == 0) path.moveTo(i + step, float(y));
+        else path.lineTo(i + step, float(y));
+    }
+
+    if (type == RectangularWindow) {
+        timePainter.drawPath(path);
+        path = QPainterPath();
+    }
+
+    timePainter.setRenderHint(QPainter::Antialiasing, true);
+    path.addRect(0, 0, w, h + 1);
+    timePainter.drawPath(path);
+
+    QFont font;
+    font.setPixelSize(10);
+    font.setItalic(true);
+    timePainter.setFont(font);
+    QString label = tr("V / time");
+    timePainter.drawText(w - timePainter.fontMetrics().width(label) - 4,
+                         timePainter.fontMetrics().ascent() + 1, label);
+
+    m_windowTimeExampleLabel->setPixmap(timeLabel);
+    
+    int fw = 100;
+
+    QPixmap freqLabel(fw, h + 1);
+    freqLabel.fill(Qt::white);
+    QPainter freqPainter(&freqLabel);
+    path = QPainterPath();
+
+    size_t fftsize = 512;
+
+    float *input = (float *)fftwf_malloc(fftsize * sizeof(float));
+    fftwf_complex *output =
+        (fftwf_complex *)fftwf_malloc(fftsize * sizeof(fftwf_complex));
+    fftwf_plan plan = fftwf_plan_dft_r2c_1d(fftsize, input, output,
+                                            FFTW_ESTIMATE);
+    for (int i = 0; i < fftsize; ++i) input[i] = 0.f;
+    for (int i = 0; i < step * 2; ++i) {
+        input[fftsize/2 - step + i] = windower.getValue(i);
+    }
+    
+    fftwf_execute(plan);
+    fftwf_destroy_plan(plan);
+
+    float maxdb = 0.f;
+    float mindb = 0.f;
+    bool first = true;
+    for (int i = 0; i < fftsize/2; ++i) {
+        float power = output[i][0] * output[i][0] + output[i][1] * output[i][1];
+        float db = mindb;
+        if (power > 0) {
+            db = 20 * log10(power);
+            if (first || db > maxdb) maxdb = db;
+            if (first || db < mindb) mindb = db;
+            first = false;
+        }
+    }
+
+    if (mindb > -80.f) mindb = -80.f;
+
+    // -- no, don't use the actual mindb -- it's easier to compare
+    // plots with a fixed min value
+    mindb = -170.f;
+
+    float maxval = maxdb + -mindb;
+
+    float ly = h - ((-80.f + -mindb) / maxval) * peak + 1;
+
+    path.moveTo(0, h - peak + 1);
+    path.lineTo(fw, h - peak + 1);
+
+    freqPainter.setPen(Qt::gray);
+    freqPainter.setRenderHint(QPainter::Antialiasing, true);
+    freqPainter.drawPath(path);
+    
+    path = QPainterPath();
+    freqPainter.setPen(Qt::black);
+
+//    std::cerr << "maxdb = " << maxdb << ", mindb = " << mindb << ", maxval = " <<maxval << std::endl;
+
+    for (int i = 0; i < fftsize/2; ++i) {
+        float power = output[i][0] * output[i][0] + output[i][1] * output[i][1];
+        float db = 20 * log10(power);
+        float val = db + -mindb;
+        if (val < 0) val = 0;
+        float norm = val / maxval;
+        float x = (fw / float(fftsize/2)) * i;
+        float y = h - norm * peak + 1;
+        if (i == 0) path.moveTo(x, y);
+        else path.lineTo(x, y);
+    }
+
+    freqPainter.setRenderHint(QPainter::Antialiasing, true);
+    path.addRect(0, 0, fw, h + 1);
+    freqPainter.drawPath(path);
+
+    fftwf_free(input);
+    fftwf_free(output);
+
+    freqPainter.setFont(font);
+    label = tr("dB / freq");
+    freqPainter.drawText(fw - freqPainter.fontMetrics().width(label) - 4,
+                         freqPainter.fontMetrics().ascent() + 1, label);
+
+    m_windowFreqExampleLabel->setPixmap(freqLabel);
+
+    m_windowType = type;
+    m_applyButton->setEnabled(true);
+}
+
+void
+PreferencesDialog::smoothSpectrogramChanged(int state)
+{
+    m_smoothSpectrogram = (state == Qt::Checked);
+    m_applyButton->setEnabled(true);
+}
+
+void
+PreferencesDialog::propertyLayoutChanged(int layout)
+{
+    m_propertyLayout = layout;
+    m_applyButton->setEnabled(true);
+}
+
+void
+PreferencesDialog::tuningFrequencyChanged(double freq)
+{
+    m_tuningFrequency = freq;
+    m_applyButton->setEnabled(true);
+}
+
+void
+PreferencesDialog::okClicked()
+{
+    applyClicked();
+    Preferences::getInstance()->getConfigFile()->commit();
+    accept();
+}
+
+void
+PreferencesDialog::applyClicked()
+{
+    Preferences *prefs = Preferences::getInstance();
+    prefs->setWindowType(WindowType(m_windowType));
+    prefs->setSmoothSpectrogram(m_smoothSpectrogram);
+    prefs->setPropertyBoxLayout(Preferences::PropertyBoxLayout
+                                (m_propertyLayout));
+    prefs->setTuningFrequency(m_tuningFrequency);
+    m_applyButton->setEnabled(false);
+}    
+
+void
+PreferencesDialog::cancelClicked()
+{
+    reject();
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/PreferencesDialog.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,58 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _PREFERENCES_DIALOG_H_
+#define _PREFERENCES_DIALOG_H_
+
+#include <QDialog>
+
+#include "base/Window.h"
+
+class QLabel;
+class QPushButton;
+
+class PreferencesDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    PreferencesDialog(QWidget *parent = 0, Qt::WFlags flags = 0);
+    ~PreferencesDialog();
+
+protected slots:
+    void windowTypeChanged(int type);
+    void smoothSpectrogramChanged(int state);
+    void propertyLayoutChanged(int layout);
+    void tuningFrequencyChanged(double freq);
+
+    void okClicked();
+    void applyClicked();
+    void cancelClicked();
+
+protected:
+    QLabel *m_windowTimeExampleLabel;
+    QLabel *m_windowFreqExampleLabel;
+
+    WindowType *m_windows;
+
+    QPushButton *m_applyButton;
+    
+    int   m_windowType;
+    bool  m_smoothSpectrogram;
+    int   m_propertyLayout;
+    float m_tuningFrequency;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/main.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,120 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "MainWindow.h"
+
+#include "base/System.h"
+#include "base/TempDirectory.h"
+#include "base/PropertyContainer.h"
+#include "base/Preferences.h"
+#include "fileio/ConfigFile.h"
+
+#include <QMetaType>
+#include <QApplication>
+#include <QDesktopWidget>
+#include <QMessageBox>
+#include <QTranslator>
+#include <QLocale>
+
+#include <iostream>
+#include <signal.h>
+
+//!!! catch trappable signals, cleanup temporary directory etc
+//!!! check for crap left over from previous run
+
+static QMutex cleanupMutex;
+
+static void
+signalHandler(int /* signal */)
+{
+    // Avoid this happening more than once across threads
+
+    cleanupMutex.lock();
+    std::cerr << "signalHandler: cleaning up and exiting" << std::endl;
+    TempDirectory::getInstance()->cleanup();
+    exit(0); // without releasing mutex
+}
+
+extern void svSystemSpecificInitialisation();
+
+int
+main(int argc, char **argv)
+{
+    QApplication application(argc, argv);
+
+    signal(SIGINT,  signalHandler);
+    signal(SIGTERM, signalHandler);
+
+#ifndef Q_WS_WIN32
+    signal(SIGHUP,  signalHandler);
+    signal(SIGQUIT, signalHandler);
+#endif
+
+    svSystemSpecificInitialisation();
+
+    QString language = QLocale::system().name();
+
+    QTranslator qtTranslator;
+    QString qtTrName = QString("qt_%1").arg(language);
+    std::cerr << "Loading " << qtTrName.toStdString() << "..." << std::endl;
+    qtTranslator.load(qtTrName);
+    application.installTranslator(&qtTranslator);
+
+    QTranslator svTranslator;
+    QString svTrName = QString("sonic-visualiser_%1").arg(language);
+    std::cerr << "Loading " << svTrName.toStdString() << "..." << std::endl;
+    svTranslator.load(svTrName, ":i18n");
+    application.installTranslator(&svTranslator);
+
+    // Permit size_t and PropertyName to be used as args in queued signal calls
+    qRegisterMetaType<size_t>("size_t");
+    qRegisterMetaType<PropertyContainer::PropertyName>("PropertyContainer::PropertyName");
+
+    MainWindow gui;
+
+    QDesktopWidget *desktop = QApplication::desktop();
+    QRect available = desktop->availableGeometry();
+
+    int width = available.width() * 2 / 3;
+    int height = available.height() / 2;
+    if (height < 450) height = available.height() * 2 / 3;
+    if (width > height * 2) width = height * 2;
+
+    gui.resize(width, height);
+    gui.show();
+
+    if (argc > 1) {
+	QString path = argv[1];
+        bool success = false;
+        if (path.endsWith(".sv")) {
+            success = gui.openSessionFile(path);
+        }
+        if (!success) {
+            success = gui.openSomeFile(path);
+        }
+        if (!success) {
+	    QMessageBox::critical(&gui, QMessageBox::tr("Failed to open file"),
+				  QMessageBox::tr("File \"%1\" could not be opened").arg(path));
+	}
+    }
+
+    int rv = application.exec();
+    std::cerr << "application.exec() returned " << rv << std::endl;
+
+    cleanupMutex.lock();
+    TempDirectory::getInstance()->cleanup();
+    Preferences::getInstance()->getConfigFile()->commit();
+    return rv;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main/systeminit.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,92 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 <QApplication>
+#include <QFont>
+
+#include <iostream>
+
+#ifdef Q_WS_X11
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xatom.h>
+#include <X11/SM/SMlib.h>
+
+static int handle_x11_error(Display *dpy, XErrorEvent *err)
+{
+    char errstr[256];
+    XGetErrorText(dpy, err->error_code, errstr, 256);
+    if (err->error_code != BadWindow) {
+	std::cerr << "waveform: X Error: "
+		  << errstr << " " << int(err->error_code)
+		  << "\nin major opcode:  "
+		  << int(err->request_code) << std::endl;
+    }
+    return 0;
+}
+#endif
+
+#ifdef Q_WS_WIN32
+
+#include <fcntl.h>
+
+// Set default file open mode to binary
+#undef _fmode
+int _fmode = _O_BINARY;
+
+void redirectStderr()
+{
+    HANDLE stderrHandle = GetStdHandle(STD_ERROR_HANDLE);
+    if (!stderrHandle) return;
+
+    AllocConsole();
+
+    CONSOLE_SCREEN_BUFFER_INFO info;
+    GetConsoleScreenBufferInfo(stderrHandle, &info);
+    info.dwSize.Y = 1000;
+    SetConsoleScreenBufferSize(stderrHandle, info.dwSize);
+
+    int h = _open_osfhandle((long)stderrHandle, _O_TEXT);
+    if (h) {
+        FILE *fd = _fdopen(h, "w");
+        if (fd) {
+            *stderr = *fd;
+            setvbuf(stderr, NULL, _IONBF, 0);
+        }
+    }
+}
+
+#endif
+
+extern void svSystemSpecificInitialisation()
+{
+#ifdef Q_WS_X11
+    XSetErrorHandler(handle_x11_error);
+#endif
+
+#ifdef Q_WS_WIN32
+    redirectStderr();
+    QFont fn = qApp->font();
+    fn.setFamily("Tahoma");
+    qApp->setFont(fn);
+#else
+#ifdef Q_WS_X11
+    QFont fn = qApp->font();
+    fn.setPointSize(fn.pointSize() + 2);
+    qApp->setFont(fn);
+#endif
+#endif
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/FeatureExtractionPluginTransform.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,494 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "FeatureExtractionPluginTransform.h"
+
+#include "plugin/FeatureExtractionPluginFactory.h"
+#include "plugin/PluginXml.h"
+#include "vamp-sdk/Plugin.h"
+
+#include "base/Model.h"
+#include "base/Window.h"
+#include "model/SparseOneDimensionalModel.h"
+#include "model/SparseTimeValueModel.h"
+#include "model/DenseThreeDimensionalModel.h"
+#include "model/DenseTimeValueModel.h"
+#include "model/NoteModel.h"
+#include "fileio/FFTFuzzyAdapter.h"
+
+#include <fftw3.h>
+
+#include <iostream>
+
+FeatureExtractionPluginTransform::FeatureExtractionPluginTransform(Model *inputModel,
+								   QString pluginId,
+                                                                   int channel,
+                                                                   QString configurationXml,
+								   QString outputName) :
+    Transform(inputModel),
+    m_plugin(0),
+    m_channel(channel),
+    m_stepSize(0),
+    m_blockSize(0),
+    m_descriptor(0),
+    m_outputFeatureNo(0)
+{
+//    std::cerr << "FeatureExtractionPluginTransform::FeatureExtractionPluginTransform: plugin " << pluginId.toStdString() << ", outputName " << outputName.toStdString() << std::endl;
+
+    FeatureExtractionPluginFactory *factory =
+	FeatureExtractionPluginFactory::instanceFor(pluginId);
+
+    if (!factory) {
+	std::cerr << "FeatureExtractionPluginTransform: No factory available for plugin id \""
+		  << pluginId.toStdString() << "\"" << std::endl;
+	return;
+    }
+
+    m_plugin = factory->instantiatePlugin(pluginId, m_input->getSampleRate());
+
+    if (!m_plugin) {
+	std::cerr << "FeatureExtractionPluginTransform: Failed to instantiate plugin \""
+		  << pluginId.toStdString() << "\"" << std::endl;
+	return;
+    }
+
+    if (configurationXml != "") {
+        PluginXml(m_plugin).setParametersFromXml(configurationXml);
+    }
+
+    m_blockSize = m_plugin->getPreferredBlockSize();
+    m_stepSize = m_plugin->getPreferredStepSize();
+
+    if (m_blockSize == 0) m_blockSize = 1024; //!!! todo: ask user
+    if (m_stepSize == 0) m_stepSize = m_blockSize; //!!! likewise
+
+    DenseTimeValueModel *input = getInput();
+    if (!input) return;
+
+    size_t channelCount = input->getChannelCount();
+    if (m_plugin->getMaxChannelCount() < channelCount) {
+	channelCount = 1;
+    }
+    if (m_plugin->getMinChannelCount() > channelCount) {
+	std::cerr << "FeatureExtractionPluginTransform:: "
+		  << "Can't provide enough channels to plugin (plugin min "
+		  << m_plugin->getMinChannelCount() << ", max "
+		  << m_plugin->getMaxChannelCount() << ", input model has "
+		  << input->getChannelCount() << ")" << std::endl;
+	return;
+    }
+
+    if (!m_plugin->initialise(channelCount, m_stepSize, m_blockSize)) {
+        std::cerr << "FeatureExtractionPluginTransform: Plugin "
+                  << m_plugin->getName() << " failed to initialise!" << std::endl;
+        return;
+    }
+
+    Vamp::Plugin::OutputList outputs = m_plugin->getOutputDescriptors();
+
+    if (outputs.empty()) {
+	std::cerr << "FeatureExtractionPluginTransform: Plugin \""
+		  << pluginId.toStdString() << "\" has no outputs" << std::endl;
+	return;
+    }
+    
+    for (size_t i = 0; i < outputs.size(); ++i) {
+	if (outputName == "" || outputs[i].name == outputName.toStdString()) {
+	    m_outputFeatureNo = i;
+	    m_descriptor = new Vamp::Plugin::OutputDescriptor
+		(outputs[i]);
+	    break;
+	}
+    }
+
+    if (!m_descriptor) {
+	std::cerr << "FeatureExtractionPluginTransform: Plugin \""
+		  << pluginId.toStdString() << "\" has no output named \""
+		  << outputName.toStdString() << "\"" << std::endl;
+	return;
+    }
+
+//    std::cerr << "FeatureExtractionPluginTransform: output sample type "
+//	      << m_descriptor->sampleType << std::endl;
+
+    int binCount = 1;
+    float minValue = 0.0, maxValue = 0.0;
+    
+    if (m_descriptor->hasFixedBinCount) {
+	binCount = m_descriptor->binCount;
+    }
+
+//    std::cerr << "FeatureExtractionPluginTransform: output bin count "
+//	      << binCount << std::endl;
+
+    if (binCount > 0 && m_descriptor->hasKnownExtents) {
+	minValue = m_descriptor->minValue;
+	maxValue = m_descriptor->maxValue;
+    }
+
+    size_t modelRate = m_input->getSampleRate();
+    size_t modelResolution = 1;
+    
+    switch (m_descriptor->sampleType) {
+
+    case Vamp::Plugin::OutputDescriptor::VariableSampleRate:
+	if (m_descriptor->sampleRate != 0.0) {
+	    modelResolution = size_t(modelRate / m_descriptor->sampleRate + 0.001);
+	}
+	break;
+
+    case Vamp::Plugin::OutputDescriptor::OneSamplePerStep:
+	modelResolution = m_stepSize;
+	break;
+
+    case Vamp::Plugin::OutputDescriptor::FixedSampleRate:
+	modelRate = size_t(m_descriptor->sampleRate + 0.001);
+	break;
+    }
+
+    if (binCount == 0) {
+
+	m_output = new SparseOneDimensionalModel(modelRate, modelResolution,
+						 false);
+
+    } else if (binCount == 1) {
+
+        SparseTimeValueModel *model = new SparseTimeValueModel
+            (modelRate, modelResolution, minValue, maxValue, false);
+        model->setScaleUnits(outputs[m_outputFeatureNo].unit.c_str());
+
+        m_output = model;
+
+    } else if (m_descriptor->sampleType ==
+	       Vamp::Plugin::OutputDescriptor::VariableSampleRate) {
+
+        // We don't have a sparse 3D model, so interpret this as a
+        // note model.  There's nothing to define which values to use
+        // as which parameters of the note -- for the moment let's
+        // treat the first as pitch, second as duration in frames,
+        // third (if present) as velocity. (Our note model doesn't
+        // yet store velocity.)
+        //!!! todo: ask the user!
+	
+        NoteModel *model = new NoteModel
+            (modelRate, modelResolution, minValue, maxValue, false);
+        model->setScaleUnits(outputs[m_outputFeatureNo].unit.c_str());
+
+        m_output = model;
+
+    } else {
+	
+	m_output = new DenseThreeDimensionalModel(modelRate, modelResolution,
+						  binCount, false);
+
+	if (!m_descriptor->binNames.empty()) {
+	    std::vector<QString> names;
+	    for (size_t i = 0; i < m_descriptor->binNames.size(); ++i) {
+		names.push_back(m_descriptor->binNames[i].c_str());
+	    }
+	    (dynamic_cast<DenseThreeDimensionalModel *>(m_output))
+		->setBinNames(names);
+	}
+    }
+}
+
+FeatureExtractionPluginTransform::~FeatureExtractionPluginTransform()
+{
+    delete m_plugin;
+    delete m_descriptor;
+}
+
+DenseTimeValueModel *
+FeatureExtractionPluginTransform::getInput()
+{
+    DenseTimeValueModel *dtvm =
+	dynamic_cast<DenseTimeValueModel *>(getInputModel());
+    if (!dtvm) {
+	std::cerr << "FeatureExtractionPluginTransform::getInput: WARNING: Input model is not conformable to DenseTimeValueModel" << std::endl;
+    }
+    return dtvm;
+}
+
+void
+FeatureExtractionPluginTransform::run()
+{
+    DenseTimeValueModel *input = getInput();
+    if (!input) return;
+
+    if (!m_output) return;
+
+    size_t sampleRate = m_input->getSampleRate();
+
+    size_t channelCount = input->getChannelCount();
+    if (m_plugin->getMaxChannelCount() < channelCount) {
+	channelCount = 1;
+    }
+
+    float **buffers = new float*[channelCount];
+    for (size_t ch = 0; ch < channelCount; ++ch) {
+	buffers[ch] = new float[m_blockSize];
+    }
+
+    bool frequencyDomain = (m_plugin->getInputDomain() ==
+                            Vamp::Plugin::FrequencyDomain);
+    std::vector<FFTFuzzyAdapter *> fftAdapters;
+
+    if (frequencyDomain) {
+        for (size_t ch = 0; ch < channelCount; ++ch) {
+            fftAdapters.push_back(new FFTFuzzyAdapter
+                                  (getInput(),
+                                   channelCount == 1 ? m_channel : ch,
+                                   HanningWindow,
+                                   m_blockSize,
+                                   m_stepSize,
+                                   m_blockSize,
+                                   false));
+        }
+    }
+
+    long startFrame = m_input->getStartFrame();
+    long   endFrame = m_input->getEndFrame();
+    long blockFrame = startFrame;
+
+    long prevCompletion = 0;
+
+    while (1) {
+
+        if (frequencyDomain) {
+            if (blockFrame - int(m_blockSize)/2 > endFrame) break;
+        } else {
+            if (blockFrame >= endFrame) break;
+        }
+
+//	std::cerr << "FeatureExtractionPluginTransform::run: blockFrame "
+//		  << blockFrame << std::endl;
+
+	long completion =
+	    (((blockFrame - startFrame) / m_stepSize) * 99) /
+	    (   (endFrame - startFrame) / m_stepSize);
+
+	// channelCount is either m_input->channelCount or 1
+
+        for (size_t ch = 0; ch < channelCount; ++ch) {
+            if (frequencyDomain) {
+                int column = (blockFrame - startFrame) / m_stepSize;
+                for (size_t i = 0; i < m_blockSize/2; ++i) {
+                    fftAdapters[ch]->getValuesAt
+                        (column, i, buffers[ch][i*2], buffers[ch][i*2+1]);
+                }
+/*!!!
+                float sum = 0.0;
+                for (size_t i = 0; i < m_blockSize/2; ++i) {
+                    sum += buffers[ch][i*2];
+                }
+                if (fabs(sum) < 0.0001) {
+                    std::cerr << "WARNING: small sum for column " << column << " (sum is " << sum << ")" << std::endl;
+                }
+*/
+            } else {
+                getFrames(ch, channelCount, 
+                          blockFrame, m_blockSize, buffers[ch]);
+            }                
+        }
+
+	Vamp::Plugin::FeatureSet features = m_plugin->process
+	    (buffers, Vamp::RealTime::frame2RealTime(blockFrame, sampleRate));
+
+	for (size_t fi = 0; fi < features[m_outputFeatureNo].size(); ++fi) {
+	    Vamp::Plugin::Feature feature =
+		features[m_outputFeatureNo][fi];
+	    addFeature(blockFrame, feature);
+	}
+
+	if (blockFrame == startFrame || completion > prevCompletion) {
+	    setCompletion(completion);
+	    prevCompletion = completion;
+	}
+
+	blockFrame += m_stepSize;
+    }
+
+    Vamp::Plugin::FeatureSet features = m_plugin->getRemainingFeatures();
+
+    for (size_t fi = 0; fi < features[m_outputFeatureNo].size(); ++fi) {
+	Vamp::Plugin::Feature feature =
+	    features[m_outputFeatureNo][fi];
+	addFeature(blockFrame, feature);
+    }
+
+    if (frequencyDomain) {
+        for (size_t ch = 0; ch < channelCount; ++ch) {
+            delete fftAdapters[ch];
+        }
+    }
+
+    setCompletion(100);
+}
+
+void
+FeatureExtractionPluginTransform::getFrames(int channel, int channelCount,
+                                            long startFrame, long size,
+                                            float *buffer)
+{
+    long offset = 0;
+
+    if (startFrame < 0) {
+        for (int i = 0; i < size && startFrame + i < 0; ++i) {
+            buffer[i] = 0.0f;
+        }
+        offset = -startFrame;
+        size -= offset;
+        if (size <= 0) return;
+        startFrame = 0;
+    }
+
+    long got = getInput()->getValues
+        ((channelCount == 1 ? m_channel : channel),
+         startFrame, startFrame + size, buffer + offset);
+
+    while (got < size) {
+        buffer[offset + got] = 0.0;
+        ++got;
+    }
+
+    if (m_channel == -1 && channelCount == 1 &&
+        getInput()->getChannelCount() > 1) {
+        // use mean instead of sum, as plugin input
+        int cc = getInput()->getChannelCount();
+        for (long i = 0; i < size; ++i) {
+            buffer[i] /= cc;
+        }
+    }
+}
+
+void
+FeatureExtractionPluginTransform::addFeature(size_t blockFrame,
+					     const Vamp::Plugin::Feature &feature)
+{
+    size_t inputRate = m_input->getSampleRate();
+
+//    std::cerr << "FeatureExtractionPluginTransform::addFeature("
+//	      << blockFrame << ")" << std::endl;
+
+    int binCount = 1;
+    if (m_descriptor->hasFixedBinCount) {
+	binCount = m_descriptor->binCount;
+    }
+
+    size_t frame = blockFrame;
+
+    if (m_descriptor->sampleType ==
+	Vamp::Plugin::OutputDescriptor::VariableSampleRate) {
+
+	if (!feature.hasTimestamp) {
+	    std::cerr
+		<< "WARNING: FeatureExtractionPluginTransform::addFeature: "
+		<< "Feature has variable sample rate but no timestamp!"
+		<< std::endl;
+	    return;
+	} else {
+	    frame = Vamp::RealTime::realTime2Frame(feature.timestamp, inputRate);
+	}
+
+    } else if (m_descriptor->sampleType ==
+	       Vamp::Plugin::OutputDescriptor::FixedSampleRate) {
+
+	if (feature.hasTimestamp) {
+	    //!!! warning: sampleRate may be non-integral
+	    frame = Vamp::RealTime::realTime2Frame(feature.timestamp,
+                                                   m_descriptor->sampleRate);
+	} else {
+	    frame = m_output->getEndFrame() + 1;
+	}
+    }
+	
+    if (binCount == 0) {
+
+	SparseOneDimensionalModel *model = getOutput<SparseOneDimensionalModel>();
+	if (!model) return;
+	model->addPoint(SparseOneDimensionalModel::Point(frame, feature.label.c_str()));
+	
+    } else if (binCount == 1) {
+
+	float value = 0.0;
+	if (feature.values.size() > 0) value = feature.values[0];
+
+	SparseTimeValueModel *model = getOutput<SparseTimeValueModel>();
+	if (!model) return;
+	model->addPoint(SparseTimeValueModel::Point(frame, value, feature.label.c_str()));
+
+    } else if (m_descriptor->sampleType == 
+	       Vamp::Plugin::OutputDescriptor::VariableSampleRate) {
+
+        float pitch = 0.0;
+        if (feature.values.size() > 0) pitch = feature.values[0];
+
+        float duration = 1;
+        if (feature.values.size() > 1) duration = feature.values[1];
+        
+        float velocity = 100;
+        if (feature.values.size() > 2) velocity = feature.values[2];
+
+        NoteModel *model = getOutput<NoteModel>();
+        if (!model) return;
+
+        model->addPoint(NoteModel::Point(frame, pitch, duration, feature.label.c_str()));
+	
+    } else {
+	
+	DenseThreeDimensionalModel::BinValueSet values = feature.values;
+	
+	DenseThreeDimensionalModel *model = getOutput<DenseThreeDimensionalModel>();
+	if (!model) return;
+
+	model->setBinValues(frame, values);
+    }
+}
+
+void
+FeatureExtractionPluginTransform::setCompletion(int completion)
+{
+    int binCount = 1;
+    if (m_descriptor->hasFixedBinCount) {
+	binCount = m_descriptor->binCount;
+    }
+
+    if (binCount == 0) {
+
+	SparseOneDimensionalModel *model = getOutput<SparseOneDimensionalModel>();
+	if (!model) return;
+	model->setCompletion(completion);
+
+    } else if (binCount == 1) {
+
+	SparseTimeValueModel *model = getOutput<SparseTimeValueModel>();
+	if (!model) return;
+	model->setCompletion(completion);
+
+    } else if (m_descriptor->sampleType ==
+	       Vamp::Plugin::OutputDescriptor::VariableSampleRate) {
+
+	NoteModel *model = getOutput<NoteModel>();
+	if (!model) return;
+	model->setCompletion(completion);
+
+    } else {
+
+	DenseThreeDimensionalModel *model = getOutput<DenseThreeDimensionalModel>();
+	if (!model) return;
+	model->setCompletion(completion);
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/FeatureExtractionPluginTransform.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,65 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _FEATURE_EXTRACTION_PLUGIN_TRANSFORM_H_
+#define _FEATURE_EXTRACTION_PLUGIN_TRANSFORM_H_
+
+#include "Transform.h"
+
+#include "vamp-sdk/Plugin.h"
+
+class DenseTimeValueModel;
+
+class FeatureExtractionPluginTransform : public Transform
+{
+public:
+    FeatureExtractionPluginTransform(Model *inputModel,
+				     QString plugin,
+                                     int channel,
+                                     QString configurationXml = "",
+				     QString outputName = "");
+    virtual ~FeatureExtractionPluginTransform();
+
+protected:
+    virtual void run();
+
+    Vamp::Plugin *m_plugin;
+    int m_channel;
+    size_t m_stepSize;
+    size_t m_blockSize;
+    Vamp::Plugin::OutputDescriptor *m_descriptor;
+    int m_outputFeatureNo;
+
+    void addFeature(size_t blockFrame,
+		    const Vamp::Plugin::Feature &feature);
+
+    void setCompletion(int);
+
+    void getFrames(int channel, int channelCount,
+                   long startFrame, long size, float *buffer);
+
+    // just casts
+    DenseTimeValueModel *getInput();
+    template <typename ModelClass> ModelClass *getOutput() {
+	ModelClass *mc = dynamic_cast<ModelClass *>(m_output);
+	if (!mc) {
+	    std::cerr << "FeatureExtractionPluginTransform::getOutput: Output model not conformable" << std::endl;
+	}
+	return mc;
+    }
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/RealTimePluginTransform.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,172 @@
+
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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 "RealTimePluginTransform.h"
+
+#include "plugin/RealTimePluginFactory.h"
+#include "plugin/RealTimePluginInstance.h"
+#include "plugin/PluginXml.h"
+
+#include "base/Model.h"
+#include "model/SparseTimeValueModel.h"
+#include "model/DenseTimeValueModel.h"
+
+#include <iostream>
+
+RealTimePluginTransform::RealTimePluginTransform(Model *inputModel,
+                                                 QString pluginId,
+                                                 int channel,
+                                                 QString configurationXml,
+                                                 QString units,
+                                                 int output) :
+    Transform(inputModel),
+    m_plugin(0),
+    m_channel(channel),
+    m_outputNo(output)
+{
+    std::cerr << "RealTimePluginTransform::RealTimePluginTransform: plugin " << pluginId.toStdString() << ", output " << output << std::endl;
+
+    RealTimePluginFactory *factory =
+	RealTimePluginFactory::instanceFor(pluginId);
+
+    if (!factory) {
+	std::cerr << "RealTimePluginTransform: No factory available for plugin id \""
+		  << pluginId.toStdString() << "\"" << std::endl;
+	return;
+    }
+
+    DenseTimeValueModel *input = getInput();
+    if (!input) return;
+
+    m_plugin = factory->instantiatePlugin(pluginId, 0, 0, m_input->getSampleRate(),
+                                          1024, //!!! wants to be configurable
+                                          input->getChannelCount());
+
+    if (!m_plugin) {
+	std::cerr << "RealTimePluginTransform: Failed to instantiate plugin \""
+		  << pluginId.toStdString() << "\"" << std::endl;
+	return;
+    }
+
+    if (configurationXml != "") {
+        PluginXml(m_plugin).setParametersFromXml(configurationXml);
+    }
+
+    if (m_outputNo >= m_plugin->getControlOutputCount()) {
+        std::cerr << "RealTimePluginTransform: Plugin has fewer than desired " << m_outputNo << " control outputs" << std::endl;
+        return;
+    }
+	
+    SparseTimeValueModel *model = new SparseTimeValueModel
+        (input->getSampleRate(), 1024, //!!!
+         0.0, 0.0, false);
+
+    if (units != "") model->setScaleUnits(units);
+
+    m_output = model;
+}
+
+RealTimePluginTransform::~RealTimePluginTransform()
+{
+    delete m_plugin;
+}
+
+DenseTimeValueModel *
+RealTimePluginTransform::getInput()
+{
+    DenseTimeValueModel *dtvm =
+	dynamic_cast<DenseTimeValueModel *>(getInputModel());
+    if (!dtvm) {
+	std::cerr << "RealTimePluginTransform::getInput: WARNING: Input model is not conformable to DenseTimeValueModel" << std::endl;
+    }
+    return dtvm;
+}
+
+void
+RealTimePluginTransform::run()
+{
+    DenseTimeValueModel *input = getInput();
+    if (!input) return;
+
+    SparseTimeValueModel *model = dynamic_cast<SparseTimeValueModel *>(m_output);
+    if (!model) return;
+
+    if (m_outputNo >= m_plugin->getControlOutputCount()) return;
+
+    size_t sampleRate = input->getSampleRate();
+    int channelCount = input->getChannelCount();
+    if (m_channel != -1) channelCount = 1;
+
+    size_t blockSize = m_plugin->getBufferSize();
+
+    float **buffers = m_plugin->getAudioInputBuffers();
+
+    size_t startFrame = m_input->getStartFrame();
+    size_t   endFrame = m_input->getEndFrame();
+    size_t blockFrame = startFrame;
+
+    size_t prevCompletion = 0;
+
+    int i = 0;
+
+    while (blockFrame < endFrame) {
+
+	size_t completion =
+	    (((blockFrame - startFrame) / blockSize) * 99) /
+	    (   (endFrame - startFrame) / blockSize);
+
+	size_t got = 0;
+
+	if (channelCount == 1) {
+	    got = input->getValues
+		(m_channel, blockFrame, blockFrame + blockSize, buffers[0]);
+	    while (got < blockSize) {
+		buffers[0][got++] = 0.0;
+	    }
+            if (m_channel == -1 && channelCount > 1) {
+                // use mean instead of sum, as plugin input
+                for (size_t i = 0; i < got; ++i) {
+                    buffers[0][i] /= channelCount;
+                }
+            }                
+	} else {
+	    for (size_t ch = 0; ch < channelCount; ++ch) {
+		got = input->getValues
+		    (ch, blockFrame, blockFrame + blockSize, buffers[ch]);
+		while (got < blockSize) {
+		    buffers[ch][got++] = 0.0;
+		}
+	    }
+	}
+
+        m_plugin->run(Vamp::RealTime::frame2RealTime(blockFrame, sampleRate));
+
+        float value = m_plugin->getControlOutputValue(m_outputNo);
+
+	model->addPoint(SparseTimeValueModel::Point
+                        (blockFrame - m_plugin->getLatency(), value, ""));
+
+	if (blockFrame == startFrame || completion > prevCompletion) {
+	    model->setCompletion(completion);
+	    prevCompletion = completion;
+	}
+        
+	blockFrame += blockSize;
+    }
+    
+    model->setCompletion(100);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/RealTimePluginTransform.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,47 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    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.
+*/
+
+#ifndef _REAL_TIME_PLUGIN_TRANSFORM_H_
+#define _REAL_TIME_PLUGIN_TRANSFORM_H_
+
+#include "Transform.h"
+#include "RealTimePluginInstance.h"
+
+class DenseTimeValueModel;
+
+class RealTimePluginTransform : public Transform
+{
+public:
+    RealTimePluginTransform(Model *inputModel,
+			    QString plugin,
+                            int channel,
+			    QString configurationXml = "",
+                            QString units = "",
+			    int output = 0);
+    virtual ~RealTimePluginTransform();
+
+protected:
+    virtual void run();
+
+    RealTimePluginInstance *m_plugin;
+    int m_channel;
+    int m_outputNo;
+
+    // just casts
+    DenseTimeValueModel *getInput();
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/Transform.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,32 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+   
+    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 "Transform.h"
+
+Transform::Transform(Model *m) :
+    m_input(m),
+    m_output(0),
+    m_detached(false),
+    m_deleting(false)
+{
+}
+
+Transform::~Transform()
+{
+    m_deleting = true;
+    wait();
+    if (!m_detached) delete m_output;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/Transform.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,56 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+   
+    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.
+*/
+
+#ifndef _TRANSFORM_H_
+#define _TRANSFORM_H_
+
+#include "Thread.h"
+
+#include "base/Model.h"
+
+typedef QString TransformName;
+
+/**
+ * A Transform turns one data model into another.
+ *
+ * Typically in this application, a Transform might have a
+ * DenseTimeValueModel as its input (e.g. an audio waveform) and a
+ * SparseOneDimensionalModel (e.g. detected beats) as its output.
+ *
+ * The Transform typically runs in the background, as a separate
+ * thread populating the output model.  The model is available to the
+ * user of the Transform immediately, but may be initially empty until
+ * the background thread has populated it.
+ */
+
+class Transform : public Thread
+{
+public:
+    virtual ~Transform();
+
+    Model *getInputModel()  { return m_input; }
+    Model *getOutputModel() { return m_output; }
+    Model *detachOutputModel() { m_detached = true; return m_output; }
+
+protected:
+    Transform(Model *m);
+
+    Model *m_input; // I don't own this
+    Model *m_output; // I own this, unless...
+    bool m_detached; // ... this is true.
+    bool m_deleting;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/TransformFactory.cpp	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,480 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+   
+    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 "TransformFactory.h"
+
+#include "FeatureExtractionPluginTransform.h"
+#include "RealTimePluginTransform.h"
+
+#include "plugin/FeatureExtractionPluginFactory.h"
+#include "plugin/RealTimePluginFactory.h"
+#include "plugin/PluginXml.h"
+
+#include "widgets/PluginParameterDialog.h"
+
+#include "model/DenseTimeValueModel.h"
+
+#include <iostream>
+#include <set>
+
+#include <QRegExp>
+
+TransformFactory *
+TransformFactory::m_instance = new TransformFactory;
+
+TransformFactory *
+TransformFactory::getInstance()
+{
+    return m_instance;
+}
+
+TransformFactory::~TransformFactory()
+{
+}
+
+TransformFactory::TransformList
+TransformFactory::getAllTransforms()
+{
+    if (m_transforms.empty()) populateTransforms();
+
+    TransformList list;
+    for (TransformDescriptionMap::const_iterator i = m_transforms.begin();
+	 i != m_transforms.end(); ++i) {
+	list.push_back(i->second);
+    }
+
+    return list;
+}
+
+std::vector<QString>
+TransformFactory::getAllTransformTypes()
+{
+    if (m_transforms.empty()) populateTransforms();
+
+    std::set<QString> types;
+    for (TransformDescriptionMap::const_iterator i = m_transforms.begin();
+	 i != m_transforms.end(); ++i) {
+        types.insert(i->second.type);
+    }
+
+    std::vector<QString> rv;
+    for (std::set<QString>::iterator i = types.begin(); i != types.end(); ++i) {
+        rv.push_back(*i);
+    }
+
+    return rv;
+}
+
+void
+TransformFactory::populateTransforms()
+{
+    TransformDescriptionMap transforms;
+
+    populateFeatureExtractionPlugins(transforms);
+    populateRealTimePlugins(transforms);
+
+    // disambiguate plugins with similar descriptions
+
+    std::map<QString, int> descriptions;
+
+    for (TransformDescriptionMap::iterator i = transforms.begin();
+         i != transforms.end(); ++i) {
+
+        TransformDesc desc = i->second;
+
+	++descriptions[desc.description];
+	++descriptions[QString("%1 [%2]").arg(desc.description).arg(desc.maker)];
+    }
+
+    std::map<QString, int> counts;
+    m_transforms.clear();
+
+    for (TransformDescriptionMap::iterator i = transforms.begin();
+         i != transforms.end(); ++i) {
+
+        TransformDesc desc = i->second;
+	QString name = desc.name;
+        QString description = desc.description;
+        QString maker = desc.maker;
+
+	if (descriptions[description] > 1) {
+	    description = QString("%1 [%2]").arg(description).arg(maker);
+	    if (descriptions[description] > 1) {
+		description = QString("%1 <%2>")
+		    .arg(description).arg(++counts[description]);
+	    }
+	}
+
+        desc.description = description;
+	m_transforms[name] = desc;
+    }	    
+}
+
+void
+TransformFactory::populateFeatureExtractionPlugins(TransformDescriptionMap &transforms)
+{
+    std::vector<QString> plugs =
+	FeatureExtractionPluginFactory::getAllPluginIdentifiers();
+
+    for (size_t i = 0; i < plugs.size(); ++i) {
+
+	QString pluginId = plugs[i];
+
+	FeatureExtractionPluginFactory *factory =
+	    FeatureExtractionPluginFactory::instanceFor(pluginId);
+
+	if (!factory) {
+	    std::cerr << "WARNING: TransformFactory::populateTransforms: No feature extraction plugin factory for instance " << pluginId.toLocal8Bit().data() << std::endl;
+	    continue;
+	}
+
+	Vamp::Plugin *plugin = 
+	    factory->instantiatePlugin(pluginId, 48000);
+
+	if (!plugin) {
+	    std::cerr << "WARNING: TransformFactory::populateTransforms: Failed to instantiate plugin " << pluginId.toLocal8Bit().data() << std::endl;
+	    continue;
+	}
+		
+	QString pluginDescription = plugin->getDescription().c_str();
+	Vamp::Plugin::OutputList outputs =
+	    plugin->getOutputDescriptors();
+
+	for (size_t j = 0; j < outputs.size(); ++j) {
+
+	    QString transformName = QString("%1:%2")
+		    .arg(pluginId).arg(outputs[j].name.c_str());
+
+	    QString userDescription;
+            QString friendlyName;
+            QString units = outputs[j].unit.c_str();
+
+	    if (outputs.size() == 1) {
+		userDescription = pluginDescription;
+                friendlyName = pluginDescription;
+	    } else {
+		userDescription = QString("%1: %2")
+		    .arg(pluginDescription)
+		    .arg(outputs[j].description.c_str());
+                friendlyName = outputs[j].description.c_str();
+	    }
+
+            bool configurable = (!plugin->getPrograms().empty() ||
+                                 !plugin->getParameterDescriptors().empty());
+
+	    transforms[transformName] = 
+                TransformDesc(tr("Analysis Plugins"),
+                              transformName,
+                              userDescription,
+                              friendlyName,
+                              plugin->getMaker().c_str(),
+                              units,
+                              configurable);
+	}
+    }
+}
+
+void
+TransformFactory::populateRealTimePlugins(TransformDescriptionMap &transforms)
+{
+    std::vector<QString> plugs =
+	RealTimePluginFactory::getAllPluginIdentifiers();
+
+    QRegExp unitRE("[\\[\\(]([A-Za-z0-9/]+)[\\)\\]]$");
+
+    for (size_t i = 0; i < plugs.size(); ++i) {
+        
+	QString pluginId = plugs[i];
+
+        RealTimePluginFactory *factory =
+            RealTimePluginFactory::instanceFor(pluginId);
+
+	if (!factory) {
+	    std::cerr << "WARNING: TransformFactory::populateTransforms: No real time plugin factory for instance " << pluginId.toLocal8Bit().data() << std::endl;
+	    continue;
+	}
+
+        const RealTimePluginDescriptor *descriptor =
+            factory->getPluginDescriptor(pluginId);
+
+        if (!descriptor) {
+	    std::cerr << "WARNING: TransformFactory::populateTransforms: Failed to query plugin " << pluginId.toLocal8Bit().data() << std::endl;
+	    continue;
+	}
+	
+        if (descriptor->controlOutputPortCount == 0 ||
+            descriptor->audioInputPortCount == 0) continue;
+
+//        std::cout << "TransformFactory::populateRealTimePlugins: plugin " << pluginId.toStdString() << " has " << descriptor->controlOutputPortCount << " output ports" << std::endl;
+	
+	QString pluginDescription = descriptor->name.c_str();
+
+	for (size_t j = 0; j < descriptor->controlOutputPortCount; ++j) {
+
+	    QString transformName = QString("%1:%2").arg(pluginId).arg(j);
+	    QString userDescription;
+            QString units;
+
+	    if (j < descriptor->controlOutputPortNames.size() &&
+                descriptor->controlOutputPortNames[j] != "") {
+
+                QString portName = descriptor->controlOutputPortNames[j].c_str();
+
+		userDescription = tr("%1: %2")
+                    .arg(pluginDescription)
+                    .arg(portName);
+
+                if (unitRE.indexIn(portName) >= 0) {
+                    units = unitRE.cap(1);
+                }
+
+	    } else if (descriptor->controlOutputPortCount > 1) {
+
+		userDescription = tr("%1: Output %2")
+		    .arg(pluginDescription)
+		    .arg(j + 1);
+
+	    } else {
+
+                userDescription = pluginDescription;
+            }
+
+
+            bool configurable = (descriptor->parameterCount > 0);
+
+	    transforms[transformName] = 
+                TransformDesc(tr("Other Plugins"),
+                              transformName,
+                              userDescription,
+                              userDescription,
+                              descriptor->maker.c_str(),
+                              units,
+                              configurable);
+	}
+    }
+}
+
+QString
+TransformFactory::getTransformDescription(TransformName name)
+{
+    if (m_transforms.find(name) != m_transforms.end()) {
+	return m_transforms[name].description;
+    } else return "";
+}
+
+QString
+TransformFactory::getTransformFriendlyName(TransformName name)
+{
+    if (m_transforms.find(name) != m_transforms.end()) {
+	return m_transforms[name].friendlyName;
+    } else return "";
+}
+
+QString
+TransformFactory::getTransformUnits(TransformName name)
+{
+    if (m_transforms.find(name) != m_transforms.end()) {
+	return m_transforms[name].units;
+    } else return "";
+}
+
+bool
+TransformFactory::isTransformConfigurable(TransformName name)
+{
+    if (m_transforms.find(name) != m_transforms.end()) {
+	return m_transforms[name].configurable;
+    } else return false;
+}
+
+bool
+TransformFactory::getTransformChannelRange(TransformName name,
+                                           int &min, int &max)
+{
+    QString id = name.section(':', 0, 2);
+
+    if (FeatureExtractionPluginFactory::instanceFor(id)) {
+
+        Vamp::Plugin *plugin = 
+            FeatureExtractionPluginFactory::instanceFor(id)->
+            instantiatePlugin(id, 48000);
+        if (!plugin) return false;
+
+        min = plugin->getMinChannelCount();
+        max = plugin->getMaxChannelCount();
+        delete plugin;
+
+        return true;
+
+    } else if (RealTimePluginFactory::instanceFor(id)) {
+
+        const RealTimePluginDescriptor *descriptor = 
+            RealTimePluginFactory::instanceFor(id)->
+            getPluginDescriptor(id);
+        if (!descriptor) return false;
+
+        min = descriptor->audioInputPortCount;
+        max = descriptor->audioInputPortCount;
+
+        return true;
+    }
+
+    return false;
+}
+
+bool
+TransformFactory::getChannelRange(TransformName name, Vamp::PluginBase *plugin,
+                                  int &minChannels, int &maxChannels)
+{
+    Vamp::Plugin *vp = 0;
+    if ((vp = dynamic_cast<Vamp::Plugin *>(plugin))) {
+        minChannels = vp->getMinChannelCount();
+        maxChannels = vp->getMaxChannelCount();
+        return true;
+    } else {
+        return getTransformChannelRange(name, minChannels, maxChannels);
+    }
+}
+
+bool
+TransformFactory::getConfigurationForTransform(TransformName name,
+                                               Model *inputModel,
+                                               int &channel,
+                                               QString &configurationXml)
+{
+    QString id = name.section(':', 0, 2);
+    QString output = name.section(':', 3);
+    
+    bool ok = false;
+    configurationXml = m_lastConfigurations[name];
+
+//    std::cerr << "last configuration: " << configurationXml.toStdString() << std::endl;
+
+    Vamp::PluginBase *plugin = 0;
+
+    if (FeatureExtractionPluginFactory::instanceFor(id)) {
+
+        plugin = FeatureExtractionPluginFactory::instanceFor(id)->instantiatePlugin
+            (id, inputModel->getSampleRate());
+
+    } else if (RealTimePluginFactory::instanceFor(id)) {
+
+        plugin = RealTimePluginFactory::instanceFor(id)->instantiatePlugin
+            (id, 0, 0, inputModel->getSampleRate(), 1024, 1);
+    }
+
+    if (plugin) {
+        if (configurationXml != "") {
+            PluginXml(plugin).setParametersFromXml(configurationXml);
+        }
+
+        int sourceChannels = 1;
+        if (dynamic_cast<DenseTimeValueModel *>(inputModel)) {
+            sourceChannels = dynamic_cast<DenseTimeValueModel *>(inputModel)
+                ->getChannelCount();
+        }
+
+        int minChannels = 1, maxChannels = sourceChannels;
+        getChannelRange(name, plugin, minChannels, maxChannels);
+
+        int targetChannels = sourceChannels;
+        if (sourceChannels < minChannels) targetChannels = minChannels;
+        if (sourceChannels > maxChannels) targetChannels = maxChannels;
+
+        int defaultChannel = channel;
+
+        PluginParameterDialog *dialog = new PluginParameterDialog(plugin,
+                                                                  sourceChannels,
+                                                                  targetChannels,
+                                                                  defaultChannel,
+                                                                  output);
+        if (dialog->exec() == QDialog::Accepted) {
+            ok = true;
+        }
+        configurationXml = PluginXml(plugin).toXmlString();
+        channel = dialog->getChannel();
+        delete dialog;
+        delete plugin;
+    }
+
+    if (ok) m_lastConfigurations[name] = configurationXml;
+
+    return ok;
+}
+
+Transform *
+TransformFactory::createTransform(TransformName name, Model *inputModel,
+                                  int channel, QString configurationXml, bool start)
+{
+    Transform *transform = 0;
+
+    //!!! use channel
+    
+    QString id = name.section(':', 0, 2);
+    QString output = name.section(':', 3);
+
+    if (FeatureExtractionPluginFactory::instanceFor(id)) {
+        transform = new FeatureExtractionPluginTransform(inputModel,
+                                                         id,
+                                                         channel,
+                                                         configurationXml,
+                                                         output);
+    } else if (RealTimePluginFactory::instanceFor(id)) {
+        transform = new RealTimePluginTransform(inputModel,
+                                                id,
+                                                channel,
+                                                configurationXml,
+                                                getTransformUnits(name),
+                                                output.toInt());
+    } else {
+        std::cerr << "TransformFactory::createTransform: Unknown transform \""
+                  << name.toStdString() << "\"" << std::endl;
+        return transform;
+    }
+
+    if (start && transform) transform->start();
+    transform->setObjectName(name);
+    return transform;
+}
+
+Model *
+TransformFactory::transform(TransformName name, Model *inputModel,
+                            int channel, QString configurationXml)
+{
+    Transform *t = createTransform(name, inputModel, channel,
+                                   configurationXml, false);
+
+    if (!t) return 0;
+
+    connect(t, SIGNAL(finished()), this, SLOT(transformFinished()));
+
+    t->start();
+    return t->detachOutputModel();
+}
+
+void
+TransformFactory::transformFinished()
+{
+    QObject *s = sender();
+    Transform *transform = dynamic_cast<Transform *>(s);
+    
+    if (!transform) {
+	std::cerr << "WARNING: TransformFactory::transformFinished: sender is not a transform" << std::endl;
+	return;
+    }
+
+    transform->wait(); // unnecessary but reassuring
+    delete transform;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/TransformFactory.h	Mon Jul 31 12:03:45 2006 +0000
@@ -0,0 +1,155 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+   
+    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.
+*/
+
+#ifndef _TRANSFORM_FACTORY_H_
+#define _TRANSFORM_FACTORY_H_
+
+#include "Transform.h"
+
+#include <map>
+
+namespace Vamp { class PluginBase; }
+
+class TransformFactory : public QObject
+{
+    Q_OBJECT
+
+public:
+    virtual ~TransformFactory();
+
+    static TransformFactory *getInstance();
+
+    // The name is intended to be computer-referenceable, and unique
+    // within the application.  The description is intended to be
+    // human readable.  In principle it doesn't have to be unique, but
+    // the factory will add suffixes to ensure that it is, all the
+    // same (just to avoid user confusion).  The friendly name is a
+    // shorter version of the description.  The type is also intended
+    // to be user-readable, for use in menus.
+
+    struct TransformDesc {
+        TransformDesc() { }
+	TransformDesc(QString _type, TransformName _name, QString _description,
+                      QString _friendlyName, QString _maker,
+                      QString _units, bool _configurable) :
+	    type(_type), name(_name), description(_description),
+            friendlyName(_friendlyName),
+            maker(_maker), units(_units), configurable(_configurable) { }
+        QString type;
+	TransformName name;
+	QString description;
+        QString friendlyName;
+        QString maker;
+        QString units;
+        bool configurable;
+    };
+    typedef std::vector<TransformDesc> TransformList;
+
+    TransformList getAllTransforms();
+
+    std::vector<QString> getAllTransformTypes();
+
+    /**
+     * Get a configuration XML string for the given transform (by
+     * asking the user, most likely).  Returns true if the transform
+     * is acceptable, false if the operation should be cancelled.
+     */
+    bool getConfigurationForTransform(TransformName name, Model *inputModel,
+                                      int &channel,
+                                      QString &configurationXml);
+
+    /**
+     * Return the output model resulting from applying the named
+     * transform to the given input model.  The transform may still be
+     * working in the background when the model is returned; check the
+     * output model's isReady completion status for more details.
+     *
+     * If the transform is unknown or the input model is not an
+     * appropriate type for the given transform, or if some other
+     * problem occurs, return 0.
+     * 
+     * The returned model is owned by the caller and must be deleted
+     * when no longer needed.
+     */
+    Model *transform(TransformName name, Model *inputModel,
+                     int channel, QString configurationXml = "");
+
+    /**
+     * Full description of a transform, suitable for putting on a menu.
+     */
+    QString getTransformDescription(TransformName name);
+
+    /**
+     * Brief but friendly description of a transform, suitable for use
+     * as the name of the output layer.
+     */
+    QString getTransformFriendlyName(TransformName name);
+
+    QString getTransformUnits(TransformName name);
+
+    /**
+     * Return true if the transform has any configurable parameters,
+     * i.e. if getConfigurationForTransform can ever return a non-trivial
+     * (not equivalent to empty) configuration string.
+     */
+    bool isTransformConfigurable(TransformName name);
+
+    /**
+     * If the transform has a prescribed number or range of channel
+     * inputs, return true and set minChannels and maxChannels to the
+     * minimum and maximum number of channel inputs the transform can
+     * accept.
+     */
+    bool getTransformChannelRange(TransformName name,
+                                  int &minChannels, int &maxChannels);
+
+    //!!! Need some way to indicate that the input model has changed /
+    //been deleted so as not to blow up backgrounded transform!  -- Or
+    //indeed, if the output model has been deleted -- could equally
+    //well happen!
+
+    //!!! Need transform category!
+	
+protected slots:
+    void transformFinished();
+
+protected:
+    Transform *createTransform(TransformName name, Model *inputModel,
+                               int channel, QString configurationXml, bool start);
+
+    struct TransformIdent
+    {
+        TransformName name;
+        QString configurationXml;
+    };
+
+    typedef std::map<TransformName, QString> TransformConfigurationMap;
+    TransformConfigurationMap m_lastConfigurations;
+
+    typedef std::map<TransformName, TransformDesc> TransformDescriptionMap;
+    TransformDescriptionMap m_transforms;
+
+    void populateTransforms();
+    void populateFeatureExtractionPlugins(TransformDescriptionMap &);
+    void populateRealTimePlugins(TransformDescriptionMap &);
+
+    bool getChannelRange(TransformName name,
+                         Vamp::PluginBase *plugin, int &min, int &max);
+
+    static TransformFactory *m_instance;
+};
+
+
+#endif