diff data/model/ReadOnlyWaveFileModel.cpp @ 1122:b9faee02afa5 recording

Make WritableWaveFileModel a true WaveFileModel (and ReadOnlyWaveFileModel the other sort of it). Enable recording from an empty session using that.
author Chris Cannam
date Wed, 19 Aug 2015 17:03:31 +0100
parents data/model/WaveFileModel.cpp@9f4505ac9072
children efea94b04d5a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/ReadOnlyWaveFileModel.cpp	Wed Aug 19 17:03:31 2015 +0100
@@ -0,0 +1,752 @@
+/* -*- 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 and QMUL.
+    
+    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 "ReadOnlyWaveFileModel.h"
+
+#include "fileio/AudioFileReader.h"
+#include "fileio/AudioFileReaderFactory.h"
+
+#include "system/System.h"
+
+#include "base/Preferences.h"
+
+#include <QFileInfo>
+#include <QTextStream>
+
+#include <iostream>
+#include <unistd.h>
+#include <cmath>
+#include <sndfile.h>
+
+#include <cassert>
+
+//#define DEBUG_WAVE_FILE_MODEL 1
+
+PowerOfSqrtTwoZoomConstraint
+ReadOnlyWaveFileModel::m_zoomConstraint;
+
+ReadOnlyWaveFileModel::ReadOnlyWaveFileModel(FileSource source, sv_samplerate_t targetRate) :
+    m_source(source),
+    m_path(source.getLocation()),
+    m_reader(0),
+    m_myReader(true),
+    m_startFrame(0),
+    m_fillThread(0),
+    m_updateTimer(0),
+    m_lastFillExtent(0),
+    m_exiting(false),
+    m_lastDirectReadStart(0),
+    m_lastDirectReadCount(0)
+{
+    m_source.waitForData();
+    if (m_source.isOK()) {
+        bool normalise = Preferences::getInstance()->getNormaliseAudio();
+        m_reader = AudioFileReaderFactory::createThreadingReader
+            (m_source, targetRate, normalise);
+        if (m_reader) {
+            SVDEBUG << "ReadOnlyWaveFileModel::ReadOnlyWaveFileModel: reader rate: "
+                      << m_reader->getSampleRate() << endl;
+        }
+    }
+    if (m_reader) setObjectName(m_reader->getTitle());
+    if (objectName() == "") setObjectName(QFileInfo(m_path).fileName());
+    if (isOK()) fillCache();
+}
+
+ReadOnlyWaveFileModel::ReadOnlyWaveFileModel(FileSource source, AudioFileReader *reader) :
+    m_source(source),
+    m_path(source.getLocation()),
+    m_reader(0),
+    m_myReader(false),
+    m_startFrame(0),
+    m_fillThread(0),
+    m_updateTimer(0),
+    m_lastFillExtent(0),
+    m_exiting(false)
+{
+    m_reader = reader;
+    if (m_reader) setObjectName(m_reader->getTitle());
+    if (objectName() == "") setObjectName(QFileInfo(m_path).fileName());
+    fillCache();
+}
+
+ReadOnlyWaveFileModel::~ReadOnlyWaveFileModel()
+{
+    m_exiting = true;
+    if (m_fillThread) m_fillThread->wait();
+    if (m_myReader) delete m_reader;
+    m_reader = 0;
+}
+
+bool
+ReadOnlyWaveFileModel::isOK() const
+{
+    return m_reader && m_reader->isOK();
+}
+
+bool
+ReadOnlyWaveFileModel::isReady(int *completion) const
+{
+    bool ready = (isOK() && (m_fillThread == 0));
+    double c = double(m_lastFillExtent) / double(getEndFrame() - getStartFrame());
+    static int prevCompletion = 0;
+    if (completion) {
+        *completion = int(c * 100.0 + 0.01);
+        if (m_reader) {
+            int decodeCompletion = m_reader->getDecodeCompletion();
+            if (decodeCompletion < 90) *completion = decodeCompletion;
+            else *completion = std::min(*completion, decodeCompletion);
+        }
+        if (*completion != 0 &&
+            *completion != 100 &&
+            prevCompletion != 0 &&
+            prevCompletion > *completion) {
+            // just to avoid completion going backwards
+            *completion = prevCompletion;
+        }
+        prevCompletion = *completion;
+    }
+#ifdef DEBUG_WAVE_FILE_MODEL
+    SVDEBUG << "ReadOnlyWaveFileModel::isReady(): ready = " << ready << ", completion = " << (completion ? *completion : -1) << endl;
+#endif
+    return ready;
+}
+
+sv_frame_t
+ReadOnlyWaveFileModel::getFrameCount() const
+{
+    if (!m_reader) return 0;
+    return m_reader->getFrameCount();
+}
+
+int
+ReadOnlyWaveFileModel::getChannelCount() const
+{
+    if (!m_reader) return 0;
+    return m_reader->getChannelCount();
+}
+
+sv_samplerate_t
+ReadOnlyWaveFileModel::getSampleRate() const 
+{
+    if (!m_reader) return 0;
+    return m_reader->getSampleRate();
+}
+
+sv_samplerate_t
+ReadOnlyWaveFileModel::getNativeRate() const 
+{
+    if (!m_reader) return 0;
+    sv_samplerate_t rate = m_reader->getNativeRate();
+    if (rate == 0) rate = getSampleRate();
+    return rate;
+}
+
+QString
+ReadOnlyWaveFileModel::getTitle() const
+{
+    QString title;
+    if (m_reader) title = m_reader->getTitle();
+    if (title == "") title = objectName();
+    return title;
+}
+
+QString
+ReadOnlyWaveFileModel::getMaker() const
+{
+    if (m_reader) return m_reader->getMaker();
+    return "";
+}
+
+QString
+ReadOnlyWaveFileModel::getLocation() const
+{
+    if (m_reader) return m_reader->getLocation();
+    return "";
+}
+
+QString
+ReadOnlyWaveFileModel::getLocalFilename() const
+{
+    if (m_reader) return m_reader->getLocalFilename();
+    return "";
+}
+    
+sv_frame_t
+ReadOnlyWaveFileModel::getData(int channel, sv_frame_t start, sv_frame_t count,
+                       float *buffer) const
+{
+    // Always read these directly from the file. 
+    // This is used for e.g. audio playback.
+    // Could be much more efficient (although compiler optimisation will help)
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+    cout << "ReadOnlyWaveFileModel::getData[" << this << "]: " << channel << ", " << start << ", " << count << ", " << buffer << endl;
+#endif
+
+    if (start >= m_startFrame) {
+        start -= m_startFrame;
+    } else {
+        for (sv_frame_t i = 0; i < count; ++i) {
+            buffer[i] = 0.f;
+        }
+        if (count <= m_startFrame - start) {
+            return 0;
+        } else {
+            count -= (m_startFrame - start);
+            start = 0;
+        }
+    }
+
+    if (!m_reader || !m_reader->isOK() || count == 0) {
+        for (sv_frame_t i = 0; i < count; ++i) buffer[i] = 0.f;
+        return 0;
+    }
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+//    SVDEBUG << "ReadOnlyWaveFileModel::getValues(" << channel << ", "
+//              << start << ", " << end << "): calling reader" << endl;
+#endif
+
+    int channels = getChannelCount();
+
+    SampleBlock frames = m_reader->getInterleavedFrames(start, count);
+
+    sv_frame_t i = 0;
+
+    int ch0 = channel, ch1 = channel;
+    if (channel == -1) {
+	ch0 = 0;
+	ch1 = channels - 1;
+    }
+    
+    while (i < count) {
+
+	buffer[i] = 0.0;
+
+	for (int ch = ch0; ch <= ch1; ++ch) {
+
+	    sv_frame_t index = i * channels + ch;
+	    if (index >= (sv_frame_t)frames.size()) break;
+            
+	    float sample = frames[index];
+	    buffer[i] += sample;
+	}
+
+	++i;
+    }
+
+    return i;
+}
+
+sv_frame_t
+ReadOnlyWaveFileModel::getMultiChannelData(int fromchannel, int tochannel,
+                                   sv_frame_t start, sv_frame_t count,
+                                   float **buffer) const
+{
+#ifdef DEBUG_WAVE_FILE_MODEL
+    cout << "ReadOnlyWaveFileModel::getData[" << this << "]: " << fromchannel << "," << tochannel << ", " << start << ", " << count << ", " << buffer << endl;
+#endif
+
+    int channels = getChannelCount();
+
+    if (fromchannel > tochannel) {
+        cerr << "ERROR: ReadOnlyWaveFileModel::getData: fromchannel ("
+                  << fromchannel << ") > tochannel (" << tochannel << ")"
+                  << endl;
+        return 0;
+    }
+
+    if (tochannel >= channels) {
+        cerr << "ERROR: ReadOnlyWaveFileModel::getData: tochannel ("
+                  << tochannel << ") >= channel count (" << channels << ")"
+                  << endl;
+        return 0;
+    }
+
+    if (fromchannel == tochannel) {
+        return getData(fromchannel, start, count, buffer[0]);
+    }
+
+    int reqchannels = (tochannel - fromchannel) + 1;
+
+    // Always read these directly from the file. 
+    // This is used for e.g. audio playback.
+    // Could be much more efficient (although compiler optimisation will help)
+
+    if (start >= m_startFrame) {
+        start -= m_startFrame;
+    } else {
+        for (int c = 0; c < reqchannels; ++c) {
+            for (sv_frame_t i = 0; i < count; ++i) buffer[c][i] = 0.f;
+        }
+        if (count <= m_startFrame - start) {
+            return 0;
+        } else {
+            count -= (m_startFrame - start);
+            start = 0;
+        }
+    }
+
+    if (!m_reader || !m_reader->isOK() || count == 0) {
+        for (int c = 0; c < reqchannels; ++c) {
+            for (sv_frame_t i = 0; i < count; ++i) buffer[c][i] = 0.f;
+        }
+        return 0;
+    }
+
+    SampleBlock frames = m_reader->getInterleavedFrames(start, count);
+
+    sv_frame_t i = 0;
+
+    sv_frame_t index = 0, available = frames.size();
+
+    while (i < count) {
+
+        if (index >= available) break;
+
+        int destc = 0;
+
+        for (int c = 0; c < channels; ++c) {
+            
+            if (c >= fromchannel && c <= tochannel) {
+                buffer[destc][i] = frames[index];
+                ++destc;
+            }
+
+            ++index;
+        }
+
+        ++i;
+    }
+
+    return i;
+}
+
+int
+ReadOnlyWaveFileModel::getSummaryBlockSize(int desired) const
+{
+    int cacheType = 0;
+    int power = m_zoomConstraint.getMinCachePower();
+    int roundedBlockSize = m_zoomConstraint.getNearestBlockSize
+        (desired, cacheType, power, ZoomConstraint::RoundDown);
+    if (cacheType != 0 && cacheType != 1) {
+        // We will be reading directly from file, so can satisfy any
+        // blocksize requirement
+        return desired;
+    } else {
+        return roundedBlockSize;
+    }
+}    
+
+void
+ReadOnlyWaveFileModel::getSummaries(int channel, sv_frame_t start, sv_frame_t count,
+                            RangeBlock &ranges, int &blockSize) const
+{
+    ranges.clear();
+    if (!isOK()) return;
+    ranges.reserve((count / blockSize) + 1);
+
+    if (start > m_startFrame) start -= m_startFrame;
+    else if (count <= m_startFrame - start) return;
+    else {
+        count -= (m_startFrame - start);
+        start = 0;
+    }
+
+    int cacheType = 0;
+    int power = m_zoomConstraint.getMinCachePower();
+    int roundedBlockSize = m_zoomConstraint.getNearestBlockSize
+        (blockSize, cacheType, power, ZoomConstraint::RoundDown);
+
+    int channels = getChannelCount();
+
+    if (cacheType != 0 && cacheType != 1) {
+
+	// We need to read directly from the file.  We haven't got
+	// this cached.  Hope the requested area is small.  This is
+	// not optimal -- we'll end up reading the same frames twice
+	// for stereo files, in two separate calls to this method.
+	// We could fairly trivially handle this for most cases that
+	// matter by putting a single cache in getInterleavedFrames
+	// for short queries.
+
+        m_directReadMutex.lock();
+
+        if (m_lastDirectReadStart != start ||
+            m_lastDirectReadCount != count ||
+            m_directRead.empty()) {
+
+            m_directRead = m_reader->getInterleavedFrames(start, count);
+            m_lastDirectReadStart = start;
+            m_lastDirectReadCount = count;
+        }
+
+	float max = 0.0, min = 0.0, total = 0.0;
+	sv_frame_t i = 0, got = 0;
+
+	while (i < count) {
+
+	    sv_frame_t index = i * channels + channel;
+	    if (index >= (sv_frame_t)m_directRead.size()) break;
+            
+	    float sample = m_directRead[index];
+            if (sample > max || got == 0) max = sample;
+	    if (sample < min || got == 0) min = sample;
+            total += fabsf(sample);
+
+	    ++i;
+            ++got;
+            
+            if (got == blockSize) {
+                ranges.push_back(Range(min, max, total / float(got)));
+                min = max = total = 0.0f;
+                got = 0;
+	    }
+	}
+
+        m_directReadMutex.unlock();
+
+	if (got > 0) {
+            ranges.push_back(Range(min, max, total / float(got)));
+	}
+
+	return;
+
+    } else {
+
+	QMutexLocker locker(&m_mutex);
+    
+	const RangeBlock &cache = m_cache[cacheType];
+
+        blockSize = roundedBlockSize;
+
+	sv_frame_t cacheBlock, div;
+        
+	if (cacheType == 0) {
+	    cacheBlock = (1 << m_zoomConstraint.getMinCachePower());
+            div = (1 << power) / cacheBlock;
+	} else {
+	    cacheBlock = sv_frame_t((1 << m_zoomConstraint.getMinCachePower()) * sqrt(2.) + 0.01);
+            div = sv_frame_t(((1 << power) * sqrt(2.) + 0.01) / double(cacheBlock));
+	}
+
+	sv_frame_t startIndex = start / cacheBlock;
+	sv_frame_t endIndex = (start + count) / cacheBlock;
+
+	float max = 0.0, min = 0.0, total = 0.0;
+	sv_frame_t i = 0, got = 0;
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+	cerr << "blockSize is " << blockSize << ", cacheBlock " << cacheBlock << ", start " << start << ", count " << count << " (frame count " << getFrameCount() << "), power is " << power << ", div is " << div << ", startIndex " << startIndex << ", endIndex " << endIndex << endl;
+#endif
+
+	for (i = 0; i <= endIndex - startIndex; ) {
+        
+	    sv_frame_t index = (i + startIndex) * channels + channel;
+	    if (index >= (sv_frame_t)cache.size()) break;
+            
+            const Range &range = cache[index];
+            if (range.max() > max || got == 0) max = range.max();
+            if (range.min() < min || got == 0) min = range.min();
+            total += range.absmean();
+            
+	    ++i;
+            ++got;
+            
+	    if (got == div) {
+		ranges.push_back(Range(min, max, total / float(got)));
+                min = max = total = 0.0f;
+                got = 0;
+	    }
+	}
+		
+	if (got > 0) {
+            ranges.push_back(Range(min, max, total / float(got)));
+	}
+    }
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+    SVDEBUG << "returning " << ranges.size() << " ranges" << endl;
+#endif
+    return;
+}
+
+ReadOnlyWaveFileModel::Range
+ReadOnlyWaveFileModel::getSummary(int channel, sv_frame_t start, sv_frame_t count) const
+{
+    Range range;
+    if (!isOK()) return range;
+
+    if (start > m_startFrame) start -= m_startFrame;
+    else if (count <= m_startFrame - start) return range;
+    else {
+        count -= (m_startFrame - start);
+        start = 0;
+    }
+
+    int blockSize;
+    for (blockSize = 1; blockSize <= count; blockSize *= 2);
+    if (blockSize > 1) blockSize /= 2;
+
+    bool first = false;
+
+    sv_frame_t blockStart = (start / blockSize) * blockSize;
+    sv_frame_t blockEnd = ((start + count) / blockSize) * blockSize;
+
+    if (blockStart < start) blockStart += blockSize;
+        
+    if (blockEnd > blockStart) {
+        RangeBlock ranges;
+        getSummaries(channel, blockStart, blockEnd - blockStart, ranges, blockSize);
+        for (int i = 0; i < (int)ranges.size(); ++i) {
+            if (first || ranges[i].min() < range.min()) range.setMin(ranges[i].min());
+            if (first || ranges[i].max() > range.max()) range.setMax(ranges[i].max());
+            if (first || ranges[i].absmean() < range.absmean()) range.setAbsmean(ranges[i].absmean());
+            first = false;
+        }
+    }
+
+    if (blockStart > start) {
+        Range startRange = getSummary(channel, start, blockStart - start);
+        range.setMin(std::min(range.min(), startRange.min()));
+        range.setMax(std::max(range.max(), startRange.max()));
+        range.setAbsmean(std::min(range.absmean(), startRange.absmean()));
+    }
+
+    if (blockEnd < start + count) {
+        Range endRange = getSummary(channel, blockEnd, start + count - blockEnd);
+        range.setMin(std::min(range.min(), endRange.min()));
+        range.setMax(std::max(range.max(), endRange.max()));
+        range.setAbsmean(std::min(range.absmean(), endRange.absmean()));
+    }
+
+    return range;
+}
+
+void
+ReadOnlyWaveFileModel::fillCache()
+{
+    m_mutex.lock();
+
+    m_updateTimer = new QTimer(this);
+    connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(fillTimerTimedOut()));
+    m_updateTimer->start(100);
+
+    m_fillThread = new RangeCacheFillThread(*this);
+    connect(m_fillThread, SIGNAL(finished()), this, SLOT(cacheFilled()));
+
+    m_mutex.unlock();
+    m_fillThread->start();
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+    SVDEBUG << "ReadOnlyWaveFileModel::fillCache: started fill thread" << endl;
+#endif
+}   
+
+void
+ReadOnlyWaveFileModel::fillTimerTimedOut()
+{
+    if (m_fillThread) {
+	sv_frame_t fillExtent = m_fillThread->getFillExtent();
+#ifdef DEBUG_WAVE_FILE_MODEL
+        SVDEBUG << "ReadOnlyWaveFileModel::fillTimerTimedOut: extent = " << fillExtent << endl;
+#endif
+	if (fillExtent > m_lastFillExtent) {
+	    emit modelChangedWithin(m_lastFillExtent, fillExtent);
+	    m_lastFillExtent = fillExtent;
+	}
+    } else {
+#ifdef DEBUG_WAVE_FILE_MODEL
+        SVDEBUG << "ReadOnlyWaveFileModel::fillTimerTimedOut: no thread" << endl;
+#endif
+	emit modelChanged();
+    }
+}
+
+void
+ReadOnlyWaveFileModel::cacheFilled()
+{
+    m_mutex.lock();
+    delete m_fillThread;
+    m_fillThread = 0;
+    delete m_updateTimer;
+    m_updateTimer = 0;
+    m_mutex.unlock();
+    if (getEndFrame() > m_lastFillExtent) {
+        emit modelChangedWithin(m_lastFillExtent, getEndFrame());
+    }
+    emit modelChanged();
+    emit ready();
+#ifdef DEBUG_WAVE_FILE_MODEL
+    SVDEBUG << "ReadOnlyWaveFileModel::cacheFilled" << endl;
+#endif
+}
+
+void
+ReadOnlyWaveFileModel::RangeCacheFillThread::run()
+{
+    int cacheBlockSize[2];
+    cacheBlockSize[0] = (1 << m_model.m_zoomConstraint.getMinCachePower());
+    cacheBlockSize[1] = (int((1 << m_model.m_zoomConstraint.getMinCachePower()) *
+                                        sqrt(2.) + 0.01));
+    
+    sv_frame_t frame = 0;
+    const sv_frame_t readBlockSize = 16384;
+    SampleBlock block;
+
+    if (!m_model.isOK()) return;
+    
+    int channels = m_model.getChannelCount();
+    bool updating = m_model.m_reader->isUpdating();
+
+    if (updating) {
+        while (channels == 0 && !m_model.m_exiting) {
+//            SVDEBUG << "ReadOnlyWaveFileModel::fill: Waiting for channels..." << endl;
+            sleep(1);
+            channels = m_model.getChannelCount();
+        }
+    }
+
+    Range *range = new Range[2 * channels];
+    float *means = new float[2 * channels];
+    int count[2];
+    count[0] = count[1] = 0;
+    for (int i = 0; i < 2 * channels; ++i) {
+        means[i] = 0.f;
+    }
+
+    bool first = true;
+
+    while (first || updating) {
+
+        updating = m_model.m_reader->isUpdating();
+        m_frameCount = m_model.getFrameCount();
+
+//        SVDEBUG << "ReadOnlyWaveFileModel::fill: frame = " << frame << ", count = " << m_frameCount << endl;
+
+        while (frame < m_frameCount) {
+
+//            SVDEBUG << "ReadOnlyWaveFileModel::fill inner loop: frame = " << frame << ", count = " << m_frameCount << ", blocksize " << readBlockSize << endl;
+
+            if (updating && (frame + readBlockSize > m_frameCount)) break;
+
+            block = m_model.m_reader->getInterleavedFrames(frame, readBlockSize);
+
+//            cerr << "block is " << block.size() << endl;
+
+            for (sv_frame_t i = 0; i < readBlockSize; ++i) {
+		
+                if (channels * i + channels > (int)block.size()) break;
+
+                for (int ch = 0; ch < channels; ++ch) {
+
+                    sv_frame_t index = channels * i + ch;
+                    float sample = block[index];
+                    
+                    for (int cacheType = 0; cacheType < 2; ++cacheType) { // cache type
+                        
+                        sv_frame_t rangeIndex = ch * 2 + cacheType;
+                        range[rangeIndex].sample(sample);
+                        means[rangeIndex] += fabsf(sample);
+                    }
+                }
+
+                //!!! this looks like a ludicrous way to do synchronisation
+                QMutexLocker locker(&m_model.m_mutex);
+
+                for (int cacheType = 0; cacheType < 2; ++cacheType) {
+
+                    if (++count[cacheType] == cacheBlockSize[cacheType]) {
+                        
+                        for (int ch = 0; ch < int(channels); ++ch) {
+                            int rangeIndex = ch * 2 + cacheType;
+                            means[rangeIndex] = means[rangeIndex] / float(count[cacheType]);
+                            range[rangeIndex].setAbsmean(means[rangeIndex]);
+                            m_model.m_cache[cacheType].push_back(range[rangeIndex]);
+                            range[rangeIndex] = Range();
+                            means[rangeIndex] = 0.f;
+                        }
+
+                        count[cacheType] = 0;
+                    }
+                }
+                
+                ++frame;
+            }
+            
+            if (m_model.m_exiting) break;
+            
+            m_fillExtent = frame;
+        }
+
+//        cerr << "ReadOnlyWaveFileModel: inner loop ended" << endl;
+
+        first = false;
+        if (m_model.m_exiting) break;
+        if (updating) {
+//            cerr << "sleeping..." << endl;
+            sleep(1);
+        }
+    }
+
+    if (!m_model.m_exiting) {
+
+        QMutexLocker locker(&m_model.m_mutex);
+
+        for (int cacheType = 0; cacheType < 2; ++cacheType) {
+
+            if (count[cacheType] > 0) {
+
+                for (int ch = 0; ch < int(channels); ++ch) {
+                    int rangeIndex = ch * 2 + cacheType;
+                    means[rangeIndex] = means[rangeIndex] / float(count[cacheType]);
+                    range[rangeIndex].setAbsmean(means[rangeIndex]);
+                    m_model.m_cache[cacheType].push_back(range[rangeIndex]);
+                    range[rangeIndex] = Range();
+                    means[rangeIndex] = 0.f;
+                }
+
+                count[cacheType] = 0;
+            }
+            
+            const Range &rr = *m_model.m_cache[cacheType].begin();
+            MUNLOCK(&rr, m_model.m_cache[cacheType].capacity() * sizeof(Range));
+        }
+    }
+    
+    delete[] means;
+    delete[] range;
+
+    m_fillExtent = m_frameCount;
+
+#ifdef DEBUG_WAVE_FILE_MODEL        
+    for (int cacheType = 0; cacheType < 2; ++cacheType) {
+        cerr << "Cache type " << cacheType << " now contains " << m_model.m_cache[cacheType].size() << " ranges" << endl;
+    }
+#endif
+}
+
+void
+ReadOnlyWaveFileModel::toXml(QTextStream &out,
+                     QString indent,
+                     QString extraAttributes) const
+{
+    Model::toXml(out, indent,
+                 QString("type=\"wavefile\" file=\"%1\" %2")
+                 .arg(encodeEntities(m_path)).arg(extraAttributes));
+}
+
+