changeset 1453:4b496a258782

Merge from branch streaming-csv-writer
author Chris Cannam
date Tue, 17 Apr 2018 10:52:06 +0100
parents 48e9f538e6e9 (current diff) 6e9615bde1f9 (diff)
children 743c38b209d0
files
diffstat 11 files changed, 565 insertions(+), 63 deletions(-) [+]
line wrap: on
line diff
--- a/base/Selection.h	Thu Mar 01 18:02:22 2018 +0000
+++ b/base/Selection.h	Tue Apr 17 10:52:06 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _SELECTION_H_
-#define _SELECTION_H_
+#ifndef SV_SELECTION_H
+#define SV_SELECTION_H
 
 #include <cstddef>
 #include <set>
--- a/data/fileio/CSVFileWriter.cpp	Thu Mar 01 18:02:22 2018 +0000
+++ b/data/fileio/CSVFileWriter.cpp	Tue Apr 17 10:52:06 2018 +0100
@@ -14,6 +14,7 @@
 */
 
 #include "CSVFileWriter.h"
+#include "CSVStreamWriter.h"
 
 #include "model/Model.h"
 #include "model/SparseOneDimensionalModel.h"
@@ -27,6 +28,7 @@
 
 #include <QFile>
 #include <QTextStream>
+#include <exception>
 
 CSVFileWriter::CSVFileWriter(QString path,
                              Model *model,
@@ -59,30 +61,17 @@
 void
 CSVFileWriter::write()
 {
-    try {
-        TempWriteFile temp(m_path);
-
-        QFile file(temp.getTemporaryFilename());
-        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
-            m_error = tr("Failed to open file %1 for writing")
-                .arg(temp.getTemporaryFilename());
-            return;
-        }
-    
-        QTextStream out(&file);
-        out << m_model->toDelimitedDataStringWithOptions
-            (m_delimiter, m_options);
-
-        file.close();
-        temp.moveToTarget();
-
-    } catch (FileOperationFailed &f) {
-        m_error = f.what();
-    }
+    Selection all {
+        m_model->getStartFrame(),
+        m_model->getEndFrame()
+    };
+    MultiSelection selections;
+    selections.addSelection(all);
+    writeSelection(selections); 
 }
 
 void
-CSVFileWriter::writeSelection(MultiSelection *selection)
+CSVFileWriter::writeSelection(MultiSelection selection)
 {
     try {
         TempWriteFile temp(m_path);
@@ -96,22 +85,34 @@
     
         QTextStream out(&file);
 
-        for (MultiSelection::SelectionList::iterator i =
-                 selection->getSelections().begin();
-             i != selection->getSelections().end(); ++i) {
+        sv_frame_t blockSize = 65536;
+
+        if (m_model->isSparse()) {
+            // Write the whole in one go, as re-seeking for each block
+            // may be very costly otherwise
+            sv_frame_t startFrame, endFrame;
+            selection.getExtents(startFrame, endFrame);
+            blockSize = endFrame - startFrame;
+        }
         
-            sv_frame_t f0(i->getStartFrame()), f1(i->getEndFrame());
-            out << m_model->toDelimitedDataStringSubsetWithOptions
-                (m_delimiter, m_options, f0, f1);
-        }
+        bool completed = CSVStreamWriter::writeInChunks(
+            out,
+            *m_model,
+            selection,
+            m_reporter,
+            m_delimiter,
+            m_options,
+            blockSize
+        );
 
         file.close();
-        temp.moveToTarget();
+        if (completed) {
+            temp.moveToTarget();
+        }
 
     } catch (FileOperationFailed &f) {
         m_error = f.what();
+    } catch (const std::exception &e) { // ProgressReporter could throw
+        m_error = e.what();
     }
 }
-
-
-
--- a/data/fileio/CSVFileWriter.h	Thu Mar 01 18:02:22 2018 +0000
+++ b/data/fileio/CSVFileWriter.h	Tue Apr 17 10:52:06 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _CSV_FILE_WRITER_H_
-#define _CSV_FILE_WRITER_H_
+#ifndef SV_CSV_FILE_WRITER_H
+#define SV_CSV_FILE_WRITER_H
 
 #include <QObject>
 #include <QString>
@@ -23,6 +23,7 @@
 
 class Model;
 class MultiSelection;
+class ProgressReporter;
 
 class CSVFileWriter : public QObject
 {
@@ -33,13 +34,23 @@
                   Model *model,
                   QString delimiter = ",",
                   DataExportOptions options = DataExportDefaults);
+
+    CSVFileWriter(QString path,
+                  Model *model,
+                  ProgressReporter *reporter,
+                  QString delimiter = ",",
+                  DataExportOptions options = DataExportDefaults) 
+    : CSVFileWriter(path, model, delimiter, options)
+    {
+        m_reporter = reporter;
+    }
     virtual ~CSVFileWriter();
 
     virtual bool isOK() const;
     virtual QString getError() const;
 
     virtual void write();
-    virtual void writeSelection(MultiSelection *selection);
+    virtual void writeSelection(MultiSelection selection);
 
 protected:
     QString m_path;
@@ -47,6 +58,7 @@
     QString m_error;
     QString m_delimiter;
     DataExportOptions m_options;
+    ProgressReporter *m_reporter = nullptr;
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/CSVStreamWriter.h	Tue Apr 17 10:52:06 2018 +0100
@@ -0,0 +1,149 @@
+/* -*- 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 2017 Queen Mary, University of London.
+    
+    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_CSV_STREAM_WRITER_H
+#define SV_CSV_STREAM_WRITER_H
+
+#include "base/BaseTypes.h"
+#include "base/Selection.h"
+#include "base/ProgressReporter.h"
+#include "base/DataExportOptions.h"
+#include "data/model/Model.h"
+#include <QString>
+#include <algorithm>
+#include <numeric>
+
+namespace CSVStreamWriter
+{
+
+template <class OutStream>
+bool
+writeInChunks(OutStream& oss,
+              const Model& model,
+              const MultiSelection& regions,
+              ProgressReporter* reporter = nullptr,
+              QString delimiter = ",",
+              DataExportOptions options = DataExportDefaults,
+              const sv_frame_t blockSize = 16384)
+{
+    const auto selections = regions.getSelections();
+    if (blockSize <= 0 || selections.empty()) return false;
+
+    // TODO, some form of checking validity of selections?
+    const auto nFramesToWrite = std::accumulate(
+        selections.begin(),
+        selections.end(),
+        0,
+        [](sv_frame_t acc, const Selection& current) -> sv_frame_t {
+            return acc + (current.getEndFrame() - current.getStartFrame());
+        }
+    );
+    const auto finalFrameOfLastRegion = (*selections.crbegin()).getEndFrame();
+
+    const auto wasCancelled = [&reporter]() { 
+        return reporter && reporter->wasCancelled(); 
+    };
+
+    sv_frame_t nFramesWritten = 0;
+    int previousProgress = 0;
+
+    for (const auto& extents : selections) {
+        const auto startFrame = extents.getStartFrame();
+        const auto endFrame = extents.getEndFrame();
+        auto readPtr = startFrame;
+        while (readPtr < endFrame) {
+            if (wasCancelled()) return false;
+
+            const auto start = readPtr;
+            const auto end = std::min(start + blockSize, endFrame);
+            const auto data = model.toDelimitedDataStringSubsetWithOptions(
+                delimiter,
+                options,
+                start,
+                end
+            ).trimmed();
+
+            if ( data != "" ) {
+                oss << data << (end < finalFrameOfLastRegion ? "\n" : "");
+            }
+
+            nFramesWritten += end - start;
+            const auto currentProgress = 100 * nFramesWritten / nFramesToWrite;
+            const bool hasIncreased = currentProgress > previousProgress;
+            if (hasIncreased) {
+                if (reporter) reporter->setProgress(currentProgress);
+                previousProgress = currentProgress;
+            }
+            readPtr = end;
+        }
+    }
+    return !wasCancelled(); // setProgress could process event loop
+}
+
+template <class OutStream>
+bool 
+writeInChunks(OutStream& oss,
+              const Model& model,
+              const Selection& extents,
+              ProgressReporter* reporter = nullptr,
+              QString delimiter = ",",
+              DataExportOptions options = DataExportDefaults,
+              const sv_frame_t blockSize = 16384)
+{
+    const auto startFrame = extents.isEmpty() ?
+        model.getStartFrame() : extents.getStartFrame();
+    const auto endFrame = extents.isEmpty() ?
+        model.getEndFrame() : extents.getEndFrame();
+    const auto hasValidExtents = startFrame >= 0 && endFrame > startFrame;
+    if (!hasValidExtents) return false;
+    Selection all {
+        startFrame,
+        endFrame
+    };
+    MultiSelection regions;
+    regions.addSelection(all);
+    return CSVStreamWriter::writeInChunks(
+        oss,
+        model,
+        regions,
+        reporter,
+        delimiter,
+        options,
+        blockSize
+    );
+}
+
+template <class OutStream>
+bool 
+writeInChunks(OutStream& oss,
+              const Model& model,
+              ProgressReporter* reporter = nullptr,
+              QString delimiter = ",",
+              DataExportOptions options = DataExportDefaults,
+              const sv_frame_t blockSize = 16384)
+{
+    const Selection empty;
+    return CSVStreamWriter::writeInChunks(
+        oss,
+        model,
+        empty,
+        reporter,
+        delimiter,
+        options,
+        blockSize
+    );
+}
+} // namespace CSVStreamWriter
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/CSVStreamWriterTest.h	Tue Apr 17 10:52:06 2018 +0100
@@ -0,0 +1,328 @@
+/* -*- 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 2017 Queen Mary, University of London.
+    
+    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 TEST_CSV_STREAM_H
+#define TEST_CSV_STREAM_H
+
+#include <QtTest>
+#include <QObject>
+#include <sstream>
+#include <functional>
+
+#include "base/ProgressReporter.h"
+#include "base/DataExportOptions.h"
+#include "base/Selection.h"
+#include "data/model/NoteModel.h"
+#include "../CSVStreamWriter.h"
+#include "../../model/test/MockWaveModel.h"
+
+class StubReporter : public ProgressReporter
+{
+public:
+    StubReporter( std::function<bool()> isCancelled )
+        : m_isCancelled(isCancelled) {}
+    bool isDefinite() const override { return true; }
+    void setDefinite(bool) override {}
+    bool wasCancelled() const override { return m_isCancelled(); }
+    void setMessage(QString) override {}
+    void setProgress(int p) override
+    { 
+        ++m_calls;
+        m_percentageLog.push_back(p);
+    }
+
+    size_t getCallCount() const { return m_calls; }
+    std::vector<int> getPercentageLog() const { return m_percentageLog; }
+    void reset() { m_calls = 0; }
+private:
+    size_t m_calls = 0;
+    std::function<bool()> m_isCancelled;
+    std::vector<int> m_percentageLog;
+};
+
+class CSVStreamWriterTest : public QObject
+{
+    Q_OBJECT
+public:
+    std::string getExpectedString()
+    {
+        return
+        {
+          "0,0,0\n"
+          "1,0,0\n"
+          "2,0,0\n"
+          "3,0,0\n"
+          "4,1,1\n"
+          "5,1,1\n"
+          "6,1,1\n"
+          "7,1,1\n"
+          "8,1,1\n"
+          "9,1,1\n"
+          "10,1,1\n"
+          "11,1,1\n"
+          "12,1,1\n"
+          "13,1,1\n"
+          "14,1,1\n"
+          "15,1,1\n"
+          "16,1,1\n"
+          "17,1,1\n"
+          "18,1,1\n"
+          "19,1,1\n"
+          "20,0,0\n"
+          "21,0,0\n"
+          "22,0,0\n"
+          "23,0,0"
+        };
+    }
+
+private slots:
+    void simpleValidOutput()
+    {
+        MockWaveModel mwm({ DC, DC }, 16, 4);
+
+        std::ostringstream oss;
+        const auto result = CSVStreamWriter::writeInChunks(oss, mwm);
+        QVERIFY( oss.str() == getExpectedString() );
+        QVERIFY( result );
+    }
+
+    void callsReporterCorrectTimes()
+    {
+        MockWaveModel mwm({ DC, DC }, 16, 4);
+        StubReporter reporter { []() -> bool { return false; } };
+        const auto expected = getExpectedString();
+
+        std::ostringstream oss;
+        const auto writeStreamWithBlockSize = [&](int blockSize) {
+            return CSVStreamWriter::writeInChunks(
+                oss,
+                mwm,
+                &reporter,
+                ",",
+                DataExportDefaults,
+                blockSize
+            );
+        };
+
+        const auto reset = [&]() {
+            oss.str({});
+            reporter.reset();
+        };
+
+        const auto nonIntegerMultipleResult = writeStreamWithBlockSize(5);
+        QVERIFY( nonIntegerMultipleResult );
+        QVERIFY( reporter.getCallCount() == 5 /* 4.8 rounded up */ );
+        QVERIFY( oss.str() == expected );
+        reset();
+
+        const auto integerMultiple = writeStreamWithBlockSize(2);
+        QVERIFY( integerMultiple );
+        QVERIFY( reporter.getCallCount() == 12 );
+        QVERIFY( oss.str() == expected );
+        reset();
+
+        const auto largerThanNumberOfSamples = writeStreamWithBlockSize(100);
+        QVERIFY( largerThanNumberOfSamples );
+        QVERIFY( reporter.getCallCount() == 1 );
+        QVERIFY( oss.str() == expected );
+        reset();
+
+        const auto zero = writeStreamWithBlockSize(0);
+        QVERIFY( zero == false );
+        QVERIFY( reporter.getCallCount() == 0 );
+    }
+
+    void isCancellable()
+    {
+        MockWaveModel mwm({ DC, DC }, 16, 4);
+        StubReporter reporter { []() -> bool { return true; } };
+
+        std::ostringstream oss;
+        const auto cancelImmediately = CSVStreamWriter::writeInChunks(
+            oss,
+            mwm,
+            &reporter,
+            ",",
+            DataExportDefaults,
+            4
+        );
+        QVERIFY( cancelImmediately == false );
+        QVERIFY( reporter.getCallCount() == 0 );
+
+        StubReporter cancelMidway { 
+            [&]() { return cancelMidway.getCallCount() == 3; } 
+        };
+        const auto cancelledMidway = CSVStreamWriter::writeInChunks(
+            oss,
+            mwm,
+            &cancelMidway,
+            ",",
+            DataExportDefaults,
+            4
+        );
+        QVERIFY( cancelMidway.getCallCount() == 3 );
+        QVERIFY( cancelledMidway == false );
+    }
+
+    void zeroStartTimeReportsPercentageCorrectly()
+    {
+        MockWaveModel mwm({ DC, DC }, 16, 4);
+        StubReporter reporter { []() -> bool { return false; } };
+        std::ostringstream oss;
+        const auto succeeded = CSVStreamWriter::writeInChunks(
+            oss,
+            mwm,
+            &reporter,
+            ",",
+            DataExportDefaults,
+            4
+        );
+        QVERIFY( succeeded == true );
+        QVERIFY( reporter.getCallCount() == 6 );
+        const std::vector<int> expectedCallLog {
+            16,
+            33,
+            50,
+            66,
+            83,
+            100
+        };
+        QVERIFY( reporter.getPercentageLog() == expectedCallLog );
+        QVERIFY( oss.str() == getExpectedString() );
+    }
+
+    void nonZeroStartTimeReportsPercentageCorrectly()
+    {
+        MockWaveModel mwm({ DC, DC }, 16, 4);
+        StubReporter reporter { []() -> bool { return false; } };
+        std::ostringstream oss;
+        const auto writeSubSection = CSVStreamWriter::writeInChunks(
+            oss,
+            mwm,
+            {4, 20},
+            &reporter,
+            ",",
+            DataExportDefaults,
+            4
+        );
+        QVERIFY( reporter.getCallCount() == 4 );
+        const std::vector<int> expectedCallLog {
+            25,
+            50,
+            75,
+            100
+        };
+        QVERIFY( reporter.getPercentageLog() == expectedCallLog );
+        QVERIFY( writeSubSection == true );
+        const std::string expectedOutput {
+          "4,1,1\n"
+          "5,1,1\n"
+          "6,1,1\n"
+          "7,1,1\n"
+          "8,1,1\n"
+          "9,1,1\n"
+          "10,1,1\n"
+          "11,1,1\n"
+          "12,1,1\n"
+          "13,1,1\n"
+          "14,1,1\n"
+          "15,1,1\n"
+          "16,1,1\n"
+          "17,1,1\n"
+          "18,1,1\n"
+          "19,1,1"
+        };
+        QVERIFY( oss.str() == expectedOutput );
+    }
+
+    void multipleSelectionOutput()
+    {
+        MockWaveModel mwm({ DC, DC }, 16, 4);
+        StubReporter reporter { []() -> bool { return false; } };
+        std::ostringstream oss;
+        MultiSelection regions;
+        regions.addSelection({0, 2});
+        regions.addSelection({4, 6});
+        regions.addSelection({16, 18});
+        qDebug("End frame: %lld", mwm.getEndFrame());
+        const std::string expectedOutput {
+          "0,0,0\n"
+          "1,0,0\n"
+          "4,1,1\n"
+          "5,1,1\n"
+          "16,1,1\n"
+          "17,1,1"
+        };
+        const auto wroteMultiSection = CSVStreamWriter::writeInChunks(
+            oss,
+            mwm,
+            regions,
+            &reporter,
+            ",",
+            DataExportDefaults,
+            2
+        );
+        QVERIFY( wroteMultiSection == true );
+        QVERIFY( reporter.getCallCount() == 3 );
+        const std::vector<int> expectedCallLog { 33, 66, 100 };
+        QVERIFY( reporter.getPercentageLog() == expectedCallLog );
+        qDebug("%s", oss.str().c_str());
+        QVERIFY( oss.str() == expectedOutput );
+    }
+
+    void writeSparseModel()
+    {
+        const auto pentatonicFromRoot = [](float midiPitch) {
+            return std::vector<float> {
+                0 + midiPitch,
+                2 + midiPitch,
+                4 + midiPitch,
+                7 + midiPitch,
+                9 + midiPitch
+            };
+        };
+        const auto cMajorPentatonic = pentatonicFromRoot(60.0);
+        NoteModel notes(8 /* sampleRate */, 4 /* resolution */);
+        sv_frame_t startFrame = 0;
+        for (const auto& note : cMajorPentatonic) {
+            notes.addPoint({startFrame, note, 4, 1.f, ""});
+            startFrame += 8;
+        }
+        qDebug("Create Expected Output\n");
+
+        // NB. removed end line break
+        const auto expectedOutput = notes.toDelimitedDataString(",").trimmed();
+
+        StubReporter reporter { []() -> bool { return false; } };
+        std::ostringstream oss;
+        qDebug("End frame: %lld", notes.getEndFrame());
+        qDebug("Write streaming\n");
+        const auto wroteSparseModel = CSVStreamWriter::writeInChunks(
+            oss,
+            notes,
+            &reporter,
+            ",",
+            DataExportDefaults,
+            2
+        );
+
+        qDebug("\n%s\n", expectedOutput.toLocal8Bit().data());
+        qDebug("\n%s\n", oss.str().c_str());
+        QVERIFY( wroteSparseModel == true );
+        QVERIFY( oss.str() == expectedOutput.toStdString() );
+    }
+};
+
+#endif
\ No newline at end of file
--- a/data/fileio/test/files.pri	Thu Mar 01 18:02:22 2018 +0000
+++ b/data/fileio/test/files.pri	Tue Apr 17 10:52:06 2018 +0100
@@ -1,10 +1,13 @@
 
 TEST_HEADERS += \
-	     AudioFileReaderTest.h \
-	     AudioFileWriterTest.h \
-	     AudioTestData.h \
-             EncodingTest.h \
-             MIDIFileReaderTest.h
-	     
+	../../model/test/MockWaveModel.h \
+	AudioFileReaderTest.h \
+	AudioFileWriterTest.h \
+	AudioTestData.h \
+	EncodingTest.h \
+	MIDIFileReaderTest.h \
+	CSVStreamWriterTest.h
+     
 TEST_SOURCES += \
-	     svcore-data-fileio-test.cpp
+	../../model/test/MockWaveModel.cpp \
+	svcore-data-fileio-test.cpp
--- a/data/fileio/test/svcore-data-fileio-test.cpp	Thu Mar 01 18:02:22 2018 +0000
+++ b/data/fileio/test/svcore-data-fileio-test.cpp	Tue Apr 17 10:52:06 2018 +0100
@@ -16,6 +16,7 @@
 #include "AudioFileWriterTest.h"
 #include "EncodingTest.h"
 #include "MIDIFileReaderTest.h"
+#include "CSVStreamWriterTest.h"
 
 #include <QtTest>
 
@@ -70,6 +71,12 @@
         else ++bad;
     }
 
+    {
+        CSVStreamWriterTest t;
+        if (QTest::qExec(&t, argc, argv) == 0) ++good;
+        else ++bad;
+    }
+
     if (bad > 0) {
     SVCERR << "\n********* " << bad << " test suite(s) failed!\n" << endl;
         return 1;
@@ -78,4 +85,3 @@
         return 0;
     }
 }
-
--- a/data/model/Model.h	Thu Mar 01 18:02:22 2018 +0000
+++ b/data/model/Model.h	Tue Apr 17 10:52:06 2018 +0100
@@ -53,7 +53,11 @@
     virtual sv_frame_t getStartFrame() const = 0;
 
     /**
-     * Return the last audio frame spanned by the model.
+     * Return the audio frame at the end of the model, i.e. 1 more
+     * than the final frame contained within the model. The end frame
+     * minus the start frame should yield the total duration in frames
+     * spanned by the model. This is consistent with the definition of
+     * the end frame of a Selection object.
      */
     virtual sv_frame_t getEndFrame() const = 0;
 
@@ -91,6 +95,11 @@
     virtual QString getTypeName() const = 0;
 
     /**
+     * Return true if this is a sparse model.
+     */
+    virtual bool isSparse() const { return false; }
+    
+    /**
      * Mark the model as abandoning. This means that the application
      * no longer needs it, so it can stop doing any background
      * calculations it may be involved in. Note that as far as the
@@ -220,11 +229,11 @@
 
     virtual QString toDelimitedDataString(QString delimiter) const {
         return toDelimitedDataStringSubset
-            (delimiter, getStartFrame(), getEndFrame() + 1);
+            (delimiter, getStartFrame(), getEndFrame());
     }
     virtual QString toDelimitedDataStringWithOptions(QString delimiter, DataExportOptions opts) const {
         return toDelimitedDataStringSubsetWithOptions
-            (delimiter, opts, getStartFrame(), getEndFrame() + 1);
+            (delimiter, opts, getStartFrame(), getEndFrame());
     }
     virtual QString toDelimitedDataStringSubset(QString, sv_frame_t /* f0 */, sv_frame_t /* f1 */) const {
         return "";
--- a/data/model/SparseModel.h	Thu Mar 01 18:02:22 2018 +0000
+++ b/data/model/SparseModel.h	Tue Apr 17 10:52:06 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _SPARSE_MODEL_H_
-#define _SPARSE_MODEL_H_
+#ifndef SV_SPARSE_MODEL_H
+#define SV_SPARSE_MODEL_H
 
 #include "Model.h"
 #include "TabularModel.h"
@@ -62,12 +62,6 @@
         return m_resolution ? m_resolution : 1;
     }
     virtual void setResolution(int resolution);
-
-    // Extend the end of the model. If this is set to something beyond
-    // the end of the final point in the model, then getEndFrame()
-    // will return this value. Otherwise getEndFrame() will return the
-    // end of the final point.
-    virtual void extendEndFrame(sv_frame_t to) { m_extendTo = to; }
     
     typedef PointType Point;
     typedef std::multiset<PointType,
@@ -151,6 +145,8 @@
 
     virtual bool hasTextLabels() const { return m_hasTextLabels; }
 
+    virtual bool isSparse() const { return true; }
+
     QString getTypeName() const { return tr("Sparse"); }
 
     virtual QString getXmlOutputType() const { return "sparse"; }
@@ -168,7 +164,7 @@
                                                      DataExportOptions opts) const {
         return toDelimitedDataStringSubsetWithOptions
             (delimiter, opts,
-             std::min(getStartFrame(), sv_frame_t(0)), getEndFrame() + 1);
+             std::min(getStartFrame(), sv_frame_t(0)), getEndFrame());
     }
 
     virtual QString toDelimitedDataStringSubset(QString delimiter, sv_frame_t f0, sv_frame_t f1) const {
@@ -395,7 +391,6 @@
 protected:
     sv_samplerate_t m_sampleRate;
     int m_resolution;
-    sv_frame_t m_extendTo;
     bool m_notifyOnAdd;
     sv_frame_t m_sinceLastNotifyMin;
     sv_frame_t m_sinceLastNotifyMax;
@@ -541,7 +536,6 @@
                                     bool notifyOnAdd) :
     m_sampleRate(sampleRate),
     m_resolution(resolution),
-    m_extendTo(0),
     m_notifyOnAdd(notifyOnAdd),
     m_sinceLastNotifyMin(-1),
     m_sinceLastNotifyMax(-1),
@@ -571,10 +565,9 @@
     sv_frame_t f = 0;
     if (!m_points.empty()) {
         PointListConstIterator i(m_points.end());
-        f = (--i)->frame;
+        f = (--i)->frame + 1;
     }
-    if (m_extendTo > f) return m_extendTo;
-    else return f;
+    return f;
 }
 
 template <typename PointType>
--- a/data/model/test/MockWaveModel.cpp	Thu Mar 01 18:02:22 2018 +0000
+++ b/data/model/test/MockWaveModel.cpp	Tue Apr 17 10:52:06 2018 +0100
@@ -55,7 +55,7 @@
     vector<floatvec_t> data(tochannel - fromchannel + 1);
     
     for (int c = fromchannel; c <= tochannel; ++c) {
-        data.push_back(getData(c, start, count));
+        data[c] = getData(c, start, count);
     }
 
     return data;
--- a/files.pri	Thu Mar 01 18:02:22 2018 +0000
+++ b/files.pri	Tue Apr 17 10:52:06 2018 +0100
@@ -51,6 +51,7 @@
            data/fileio/CSVFileReader.h \
            data/fileio/CSVFileWriter.h \
            data/fileio/CSVFormat.h \
+           data/fileio/CSVStreamWriter.h \
            data/fileio/DataFileReader.h \
            data/fileio/DataFileReaderFactory.h \
            data/fileio/FileFinder.h \