changeset 1013:6370575a812c

Merge
author Chris Cannam
date Mon, 17 Nov 2014 17:10:29 +0000
parents ee9f4477f65b (current diff) 36f79bc5c3d7 (diff)
children ace22dccde8f
files
diffstat 22 files changed, 416 insertions(+), 118 deletions(-) [+]
line wrap: on
line diff
--- a/data/fileio/AudioFileReader.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/fileio/AudioFileReader.h	Mon Nov 17 17:10:29 2014 +0000
@@ -62,6 +62,16 @@
      */
     virtual QString getMaker() const { return ""; }
 
+    /**
+     * Return the local file path of the audio data. This is the
+     * location most likely to contain readable audio data: it may be
+     * in a different place or format from the originally specified
+     * location, for example if the file has been retrieved and
+     * decoded. In some cases there may be no local file path, and
+     * this will return "" if there is none.
+     */
+    virtual QString getLocalFilename() const { return ""; }
+    
     typedef std::map<QString, QString> TagMap;
     virtual TagMap getTags() const { return TagMap(); }
 
@@ -75,7 +85,9 @@
     /** 
      * Return interleaved samples for count frames from index start.
      * The resulting sample block will contain count *
-     * getChannelCount() samples (or fewer if end of file is reached).
+     * getChannelCount() samples (or fewer if end of file is
+     * reached). The caller does not need to allocate space and any
+     * existing content in the SampleBlock will be erased.
      *
      * The subclass implementations of this function must be
      * thread-safe -- that is, safe to call from multiple threads with
--- a/data/fileio/CSVFileReader.cpp	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/fileio/CSVFileReader.cpp	Mon Nov 17 17:10:29 2014 +0000
@@ -37,42 +37,54 @@
 CSVFileReader::CSVFileReader(QString path, CSVFormat format,
                              int mainModelSampleRate) :
     m_format(format),
-    m_file(0),
+    m_device(0),
+    m_ownDevice(true),
     m_warnings(0),
     m_mainModelSampleRate(mainModelSampleRate)
 {
-    m_file = new QFile(path);
+    QFile *file = new QFile(path);
     bool good = false;
     
-    if (!m_file->exists()) {
+    if (!file->exists()) {
 	m_error = QFile::tr("File \"%1\" does not exist").arg(path);
-    } else if (!m_file->open(QIODevice::ReadOnly | QIODevice::Text)) {
+    } else if (!file->open(QIODevice::ReadOnly | QIODevice::Text)) {
 	m_error = QFile::tr("Failed to open file \"%1\"").arg(path);
     } else {
 	good = true;
     }
 
-    if (!good) {
-	delete m_file;
-	m_file = 0;
+    if (good) {
+        m_device = file;
+    } else {
+	delete file;
     }
 }
 
+CSVFileReader::CSVFileReader(QIODevice *device, CSVFormat format,
+                             int mainModelSampleRate) :
+    m_format(format),
+    m_device(device),
+    m_ownDevice(false),
+    m_warnings(0),
+    m_mainModelSampleRate(mainModelSampleRate)
+{
+}
+
 CSVFileReader::~CSVFileReader()
 {
-    SVDEBUG << "CSVFileReader::~CSVFileReader: file is " << m_file << endl;
+    SVDEBUG << "CSVFileReader::~CSVFileReader: device is " << m_device << endl;
 
-    if (m_file) {
-        SVDEBUG << "CSVFileReader::CSVFileReader: Closing file" << endl;
-        m_file->close();
+    if (m_device && m_ownDevice) {
+        SVDEBUG << "CSVFileReader::CSVFileReader: Closing device" << endl;
+        m_device->close();
+        delete m_device;
     }
-    delete m_file;
 }
 
 bool
 CSVFileReader::isOK() const
 {
-    return (m_file != 0);
+    return (m_device != 0);
 }
 
 QString
@@ -136,7 +148,7 @@
 Model *
 CSVFileReader::load() const
 {
-    if (!m_file) return 0;
+    if (!m_device) return 0;
 
     CSVFormat::ModelType modelType = m_format.getModelType();
     CSVFormat::TimingType timingType = m_format.getTimingType();
@@ -168,8 +180,7 @@
     EditableDenseThreeDimensionalModel *model3 = 0;
     Model *model = 0;
 
-    QTextStream in(m_file);
-    in.seek(0);
+    QTextStream in(m_device);
 
     unsigned int warnings = 0, warnLimit = 10;
     unsigned int lineno = 0;
@@ -215,7 +226,7 @@
         for (int li = 0; li < lines.size(); ++li) {
 
             QString line = lines[li];
-
+            
             if (line.startsWith("#")) continue;
 
             QStringList list = StringBits::split(line, separator, allowQuoting);
--- a/data/fileio/CSVFileReader.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/fileio/CSVFileReader.h	Mon Nov 17 17:10:29 2014 +0000
@@ -28,16 +28,31 @@
 class CSVFileReader : public DataFileReader
 {
 public:
+    /**
+     * Construct a CSVFileReader to read the CSV file at the given
+     * path, with the given format.
+     */
     CSVFileReader(QString path, CSVFormat format, int mainModelSampleRate);
+
+    /**
+     * Construct a CSVFileReader to read from the given
+     * QIODevice. Caller retains ownership of the QIODevice: the
+     * CSVFileReader will not close or delete it and it must outlive
+     * the CSVFileReader.
+     */
+    CSVFileReader(QIODevice *device, CSVFormat format, int mainModelSampleRate);
+
     virtual ~CSVFileReader();
 
     virtual bool isOK() const;
     virtual QString getError() const;
+
     virtual Model *load() const;
 
 protected:
     CSVFormat m_format;
-    QFile *m_file;
+    QIODevice *m_device;
+    bool m_ownDevice;
     QString m_error;
     mutable int m_warnings;
     int m_mainModelSampleRate;
--- a/data/fileio/CSVFormat.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/fileio/CSVFormat.h	Mon Nov 17 17:10:29 2014 +0000
@@ -100,8 +100,8 @@
     void setTimingType(TimingType t)      { m_timingType   = t; }
     void setTimeUnits(TimeUnits t)        { m_timeUnits    = t; }
     void setSeparator(QChar s)            { m_separator    = s; }
-    void setSampleRate(int r)          { m_sampleRate   = r; }
-    void setWindowSize(int s)          { m_windowSize   = s; }
+    void setSampleRate(int r)             { m_sampleRate   = r; }
+    void setWindowSize(int s)             { m_windowSize   = s; }
     void setColumnCount(int c)            { m_columnCount  = c; }
     void setAllowQuoting(bool q)          { m_allowQuoting = q; }
 
--- a/data/fileio/CodedAudioFileReader.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/fileio/CodedAudioFileReader.h	Mon Nov 17 17:10:29 2014 +0000
@@ -43,6 +43,8 @@
 
     virtual int getNativeRate() const { return m_fileRate; }
 
+    virtual QString getLocalFilename() const { return m_cacheFileName; }
+    
     /// Intermediate cache means all CodedAudioFileReaders are quickly seekable
     virtual bool isQuicklySeekable() const { return true; }
 
--- a/data/fileio/FileSource.cpp	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/fileio/FileSource.cpp	Mon Nov 17 17:10:29 2014 +0000
@@ -487,6 +487,7 @@
     m_done = true;
     if (m_reply) {
         QNetworkReply *r = m_reply;
+        disconnect(r, 0, this, 0);
         m_reply = 0;
         // Can only call abort() when there are no errors.
         if (r->error() == QNetworkReply::NoError) {
--- a/data/fileio/MIDIFileWriter.cpp	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/fileio/MIDIFileWriter.cpp	Mon Nov 17 17:10:29 2014 +0000
@@ -317,7 +317,6 @@
     m_numberOfTracks = 1;
 
     int track = 0;
-    int midiChannel = 0;
 
     MIDIEvent *event;
 
@@ -349,10 +348,14 @@
         int duration = i->duration;
         int pitch = i->midiPitch;
         int velocity = i->velocity;
+        int channel = i->channel;
 
         if (pitch < 0) pitch = 0;
         if (pitch > 127) pitch = 127;
 
+        if (channel < 0) channel = 0;
+        if (channel > 15) channel = 0;
+
         // Convert frame to MIDI time
 
         double seconds = double(frame) / double(m_sampleRate);
@@ -370,13 +373,13 @@
         // in place).
 
         event = new MIDIEvent(midiTime,
-                              MIDI_NOTE_ON | midiChannel,
+                              MIDI_NOTE_ON | channel,
                               pitch,
                               velocity);
         m_midiComposition[track].push_back(event);
 
         event = new MIDIEvent(endTime,
-                              MIDI_NOTE_OFF | midiChannel,
+                              MIDI_NOTE_OFF | channel,
                               pitch,
                               127); // loudest silence you can muster
 
--- a/data/fileio/WavFileReader.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/fileio/WavFileReader.h	Mon Nov 17 17:10:29 2014 +0000
@@ -42,6 +42,8 @@
     virtual QString getLocation() const { return m_source.getLocation(); }
     virtual QString getError() const { return m_error; }
 
+    virtual QString getLocalFilename() const { return m_path; }
+    
     virtual bool isQuicklySeekable() const { return m_seekable; }
     
     /** 
--- a/data/model/AggregateWaveModel.cpp	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/model/AggregateWaveModel.cpp	Mon Nov 17 17:10:29 2014 +0000
@@ -118,14 +118,21 @@
         }
     }
 
-    int sz = count;
-
+    int longest = 0;
+    
     for (int c = ch0; c <= ch1; ++c) {
-        int szHere = 
+        int here = 
             m_components[c].model->getData(m_components[c].channel,
                                            start, count,
                                            readbuf);
-        if (szHere < sz) sz = szHere;
+        if (here > longest) {
+            longest = here;
+        }
+        if (here < count) {
+            for (int i = here; i < count; ++i) {
+                readbuf[i] = 0.f;
+            }
+        }
         if (mixing) {
             for (int i = 0; i < count; ++i) {
                 buffer[i] += readbuf[i];
@@ -134,7 +141,7 @@
     }
 
     if (mixing) delete[] readbuf;
-    return sz;
+    return longest;
 }
          
 int
@@ -157,14 +164,21 @@
         }
     }
 
-    int sz = count;
+    int longest = 0;
     
     for (int c = ch0; c <= ch1; ++c) {
-        int szHere = 
+        int here = 
             m_components[c].model->getData(m_components[c].channel,
                                            start, count,
                                            readbuf);
-        if (szHere < sz) sz = szHere;
+        if (here > longest) {
+            longest = here;
+        }
+        if (here < count) {
+            for (int i = here; i < count; ++i) {
+                readbuf[i] = 0.;
+            }
+        }
         if (mixing) {
             for (int i = 0; i < count; ++i) {
                 buffer[i] += readbuf[i];
@@ -173,7 +187,7 @@
     }
     
     if (mixing) delete[] readbuf;
-    return sz;
+    return longest;
 }
 
 int
--- a/data/model/AlignmentModel.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/model/AlignmentModel.h	Mon Nov 17 17:10:29 2014 +0000
@@ -32,7 +32,7 @@
 public:
     AlignmentModel(Model *reference,
                    Model *aligned,
-                   Model *inputModel, // probably an AggregateWaveModel; I take ownership
+                   Model *inputModel, // probably an AggregateWaveModel; may be null; I take ownership
                    SparseTimeValueModel *path); // I take ownership
     ~AlignmentModel();
 
--- a/data/model/NoteData.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/model/NoteData.h	Mon Nov 17 17:10:29 2014 +0000
@@ -23,14 +23,15 @@
 {
     NoteData(int _start, int _dur, int _mp, int _vel) :
 	start(_start), duration(_dur), midiPitch(_mp), frequency(0),
-	isMidiPitchQuantized(true), velocity(_vel) { };
+	isMidiPitchQuantized(true), velocity(_vel), channel(0) { };
             
-    int start;     // audio sample frame
-    int duration;  // in audio sample frames
-    int midiPitch; // 0-127
+    int start;       // audio sample frame
+    int duration;    // in audio sample frames
+    int midiPitch;   // 0-127
     float frequency; // Hz, to be used if isMidiPitchQuantized false
     bool isMidiPitchQuantized;
-    int velocity;  // MIDI-style 0-127
+    int velocity;    // MIDI-style 0-127
+    int channel;     // MIDI 0-15
 
     float getFrequency() const {
         if (isMidiPitchQuantized) {
--- a/data/model/WaveFileModel.cpp	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/model/WaveFileModel.cpp	Mon Nov 17 17:10:29 2014 +0000
@@ -183,6 +183,13 @@
     if (m_reader) return m_reader->getLocation();
     return "";
 }
+
+QString
+WaveFileModel::getLocalFilename() const
+{
+    if (m_reader) return m_reader->getLocalFilename();
+    return "";
+}
     
 int
 WaveFileModel::getData(int channel, int start, int count,
--- a/data/model/WaveFileModel.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/data/model/WaveFileModel.h	Mon Nov 17 17:10:29 2014 +0000
@@ -52,6 +52,8 @@
     QString getMaker() const;
     QString getLocation() const;
 
+    QString getLocalFilename() const;
+
     virtual Model *clone() const;
 
     float getValueMinimum() const { return -1.0f; }
--- a/rdf/RDFFeatureWriter.cpp	Mon Nov 17 17:09:32 2014 +0000
+++ b/rdf/RDFFeatureWriter.cpp	Mon Nov 17 17:10:29 2014 +0000
@@ -15,9 +15,6 @@
 
 #include <fstream>
 
-#include "vamp-hostsdk/PluginHostAdapter.h"
-#include "vamp-hostsdk/PluginLoader.h"
-
 #include "base/Exceptions.h"
 
 #include "RDFFeatureWriter.h"
@@ -36,7 +33,8 @@
 RDFFeatureWriter::RDFFeatureWriter() :
     FileFeatureWriter(SupportOneFilePerTrackTransform |
                       SupportOneFilePerTrack |
-                      SupportOneFileTotal,
+                      SupportOneFileTotal |
+                      SupportStdOut,
                       "n3"),
     m_plain(false),
     m_network(false),
@@ -49,6 +47,12 @@
 {
 }
 
+string
+RDFFeatureWriter::getDescription() const
+{
+    return "Write output in Audio Features Ontology RDF/Turtle format.";
+}
+
 RDFFeatureWriter::ParameterList
 RDFFeatureWriter::getSupportedParameters() const
 {
@@ -679,29 +683,45 @@
 
         stream << "\n:feature_timeline_" << featureNumber << " a tl:DiscreteTimeLine .\n\n";
 
-        int stepSize = transform.getStepSize();
-        if (stepSize == 0) {
-            cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the step size properly!" << endl;
-            return;
-        }
+        float sampleRate;
+        int stepSize, blockSize;
 
-        int blockSize = transform.getBlockSize();
-        if (blockSize == 0) {
-            cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the block size properly!" << endl;
-            return;
-        }
+        // If the output is FixedSampleRate, we need to draw the
+        // sample rate and step size from the output descriptor;
+        // otherwise they come from the transform
 
-        float sampleRate = transform.getSampleRate();
-        if (sampleRate == 0.f) {
-            cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the sample rate properly!" << endl;
-            return;
+        if (od.sampleType == Plugin::OutputDescriptor::FixedSampleRate) {
+
+            sampleRate = od.sampleRate;
+            stepSize = 1;
+            blockSize = 1;
+
+        } else {
+
+            sampleRate = transform.getSampleRate();
+            if (sampleRate == 0.f) {
+                cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the sample rate properly!" << endl;
+                return;
+            }
+
+            stepSize = transform.getStepSize();
+            if (stepSize == 0) {
+                cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the step size properly!" << endl;
+                return;
+            }
+
+            blockSize = transform.getBlockSize();
+            if (blockSize == 0) {
+                cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the block size properly!" << endl;
+                return;
+            }
         }
 
         stream << ":feature_timeline_map_" << featureNumber
                << " a tl:UniformSamplingWindowingMap ;\n"
                << "    tl:rangeTimeLine :feature_timeline_" << featureNumber << " ;\n"
                << "    tl:domainTimeLine " << timelineURI << " ;\n"
-               << "    tl:sampleRate \"" << int(sampleRate) << "\"^^xsd:int ;\n"
+               << "    tl:sampleRate \"" << sampleRate << "\"^^xsd:float ;\n"
                << "    tl:windowLength \"" << blockSize << "\"^^xsd:int ;\n"
                << "    tl:hopSize \"" << stepSize << "\"^^xsd:int .\n\n";
 
--- a/rdf/RDFFeatureWriter.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/rdf/RDFFeatureWriter.h	Mon Nov 17 17:10:29 2014 +0000
@@ -44,6 +44,8 @@
     RDFFeatureWriter();
     virtual ~RDFFeatureWriter();
 
+    virtual string getDescription() const;
+
     virtual ParameterList getSupportedParameters() const;
     virtual void setParameters(map<string, string> &params);
 
--- a/rdf/RDFTransformFactory.cpp	Mon Nov 17 17:09:32 2014 +0000
+++ b/rdf/RDFTransformFactory.cpp	Mon Nov 17 17:10:29 2014 +0000
@@ -212,7 +212,8 @@
             "window_type",
             "sample_rate",
             "start", 
-            "duration"
+            "duration",
+            "plugin_version"
         };
         
         for (int j = 0; j < int(sizeof(optionals)/sizeof(optionals[0])); ++j) {
@@ -249,6 +250,8 @@
                 if (duration == RealTime::zeroTime) {
                     cerr << "\nRDFTransformFactory: WARNING: Duration is specified as \"" << onode.value << "\" in RDF file,\n    but this evaluates to zero when parsed as an xsd:duration datatype.\n    The duration property will therefore be ignored.\n    To specify start time and duration use the xsd:duration format,\n    for example \"PT2.5S\"^^xsd:duration (for 2.5 seconds).\n\n";
                 }
+            } else if (optional == "plugin_version") {
+                transform.setPluginVersion(onode.value);
             } else {
                 cerr << "RDFTransformFactory: ERROR: Inconsistent optionals lists (unexpected optional \"" << optional << "\"" << endl;
             }
@@ -397,6 +400,11 @@
     if (transform.getBlockSize() != 0) {
         s << "    vamp:block_size \"" << transform.getBlockSize() << "\"^^xsd:int ; " << endl;
     }
+    if (transform.getWindowType() != HanningWindow) {
+        s << "    vamp:window_type \"" <<
+            Window<float>::getNameForType(transform.getWindowType()).c_str()
+          << "\" ; " << endl;
+    }
     if (transform.getStartTime() != RealTime::zeroTime) {
         s << "    vamp:start \"" << transform.getStartTime().toXsdDuration().c_str() << "\"^^xsd:duration ; " << endl;
     }
@@ -406,6 +414,9 @@
     if (transform.getSampleRate() != 0) {
         s << "    vamp:sample_rate \"" << transform.getSampleRate() << "\"^^xsd:float ; " << endl;
     }
+    if (transform.getPluginVersion() != "") {
+        s << "    vamp:plugin_version \"\"\"" << transform.getPluginVersion() << "\"\"\" ; " << endl;
+    }
     
     QString program = transform.getProgram();
     if (program != "") {
--- a/transform/CSVFeatureWriter.cpp	Mon Nov 17 17:09:32 2014 +0000
+++ b/transform/CSVFeatureWriter.cpp	Mon Nov 17 17:10:29 2014 +0000
@@ -29,10 +29,14 @@
 
 CSVFeatureWriter::CSVFeatureWriter() :
     FileFeatureWriter(SupportOneFilePerTrackTransform |
-                      SupportOneFileTotal,
+                      SupportOneFileTotal |
+                      SupportStdOut,
                       "csv"),
     m_separator(","),
-    m_sampleTiming(false)
+    m_sampleTiming(false),
+    m_endTimes(false),
+    m_forceEnd(false),
+    m_omitFilename(false)
 {
 }
 
@@ -40,6 +44,12 @@
 {
 }
 
+string
+CSVFeatureWriter::getDescription() const
+{
+    return "Write features in comma-separated (CSV) format. If transforms are being written to a single file or to stdout, the first column in the output will contain the input audio filename, or an empty string if the feature hails from the same audio file as its predecessor. If transforms are being written to multiple files, the audio filename column will be omitted. Subsequent columns will contain the feature timestamp, then any or all of duration, values, and label.";
+}
+
 CSVFeatureWriter::ParameterList
 CSVFeatureWriter::getSupportedParameters() const
 {
@@ -51,10 +61,25 @@
     p.hasArg = true;
     pl.push_back(p);
     
+    p.name = "omit-filename";
+    p.description = "Omit the filename column. May result in confusion if sending more than one audio file's features to the same CSV output.";
+    p.hasArg = false;
+    pl.push_back(p);
+    
     p.name = "sample-timing";
     p.description = "Show timings as sample frame counts instead of in seconds.";
     p.hasArg = false;
     pl.push_back(p);
+    
+    p.name = "end-times";
+    p.description = "Show start and end time instead of start and duration, for features with duration.";
+    p.hasArg = false;
+    pl.push_back(p);
+
+    p.name = "fill-ends";
+    p.description = "Include durations (or end times) even for features without duration, by using the gap to the next feature instead.";
+    p.hasArg = false;
+    pl.push_back(p);
 
     return pl;
 }
@@ -70,8 +95,18 @@
         cerr << i->first << " -> " << i->second << endl;
         if (i->first == "separator") {
             m_separator = i->second.c_str();
+            cerr << "m_separator = " << m_separator << endl;
+            if (m_separator == "\\t") {
+                m_separator = QChar::Tabulation;
+            }
         } else if (i->first == "sample-timing") {
             m_sampleTiming = true;
+        } else if (i->first == "end-times") {
+            m_endTimes = true;
+        } else if (i->first == "fill-ends") {
+            m_forceEnd = true;
+        } else if (i->first == "omit-filename") {
+            m_omitFilename = true;
         }
     }
 }
@@ -83,18 +118,83 @@
                         const Plugin::FeatureList& features,
                         std::string summaryType)
 {
+    TransformId transformId = transform.getIdentifier();
+
     // Select appropriate output file for our track/transform
     // combination
 
-    QTextStream *sptr = getOutputStream(trackId, transform.getIdentifier());
+    QTextStream *sptr = getOutputStream(trackId, transformId);
     if (!sptr) {
-        throw FailedToOpenOutputStream(trackId, transform.getIdentifier());
+        throw FailedToOpenOutputStream(trackId, transformId);
     }
 
     QTextStream &stream = *sptr;
 
-    for (unsigned int i = 0; i < features.size(); ++i) {
+    int n = features.size();
 
+    if (n == 0) return;
+
+    DataId tt(trackId, transform);
+
+    if (m_pending.find(tt) != m_pending.end()) {
+        writeFeature(tt,
+                     stream,
+                     m_pending[tt],
+                     &features[0],
+                     m_pendingSummaryTypes[tt]);
+        m_pending.erase(tt);
+        m_pendingSummaryTypes.erase(tt);
+    }
+
+    if (m_forceEnd) {
+        // can't write final feature until we know its end time
+        --n;
+        m_pending[tt] = features[n];
+        m_pendingSummaryTypes[tt] = summaryType;
+    }
+
+    for (int i = 0; i < n; ++i) {
+        writeFeature(tt, 
+                     stream,
+                     features[i], 
+                     m_forceEnd ? &features[i+1] : 0,
+                     summaryType);
+    }
+}
+
+void
+CSVFeatureWriter::finish()
+{
+    for (PendingFeatures::const_iterator i = m_pending.begin();
+         i != m_pending.end(); ++i) {
+        DataId tt = i->first;
+        Plugin::Feature f = i->second;
+        QTextStream *sptr = getOutputStream(tt.first, tt.second.getIdentifier());
+        if (!sptr) {
+            throw FailedToOpenOutputStream(tt.first, tt.second.getIdentifier());
+        }
+        QTextStream &stream = *sptr;
+        // final feature has its own time as end time (we can't
+        // reliably determine the end of audio file, and because of
+        // the nature of block processing, the feature could even
+        // start beyond that anyway)
+        writeFeature(tt, stream, f, &f, m_pendingSummaryTypes[tt]);
+    }
+
+    m_pending.clear();
+}
+
+void
+CSVFeatureWriter::writeFeature(DataId tt,
+                               QTextStream &stream,
+                               const Plugin::Feature &f,
+                               const Plugin::Feature *optionalNextFeature,
+                               std::string summaryType)
+{
+    QString trackId = tt.first;
+    Transform transform = tt.second;
+
+    if (!m_omitFilename) {
         if (m_stdout || m_singleFileName != "") {
             if (trackId != m_prevPrintedTrackId) {
                 stream << "\"" << trackId << "\"" << m_separator;
@@ -103,45 +203,68 @@
                 stream << m_separator;
             }
         }
+    }
 
-        if (m_sampleTiming) {
+    Vamp::RealTime duration;
+    bool haveDuration = true;
+    
+    if (f.hasDuration) {
+        duration = f.duration;
+    } else if (optionalNextFeature) {
+        duration = optionalNextFeature->timestamp - f.timestamp;
+    } else {
+        haveDuration = false;
+    }
 
-            stream << Vamp::RealTime::realTime2Frame
-                (features[i].timestamp, transform.getSampleRate());
+    if (m_sampleTiming) {
 
-            if (features[i].hasDuration) {
-                stream << m_separator;
+        float rate = transform.getSampleRate();
+
+        stream << Vamp::RealTime::realTime2Frame(f.timestamp, rate);
+
+        if (haveDuration) {
+            stream << m_separator;
+            if (m_endTimes) {
                 stream << Vamp::RealTime::realTime2Frame
-                    (features[i].duration, transform.getSampleRate());
+                    (f.timestamp + duration, rate);
+            } else {
+                stream << Vamp::RealTime::realTime2Frame(duration, rate);
             }
-
-        } else {
-
-            QString timestamp = features[i].timestamp.toString().c_str();
-            timestamp.replace(QRegExp("^ +"), "");
-            stream << timestamp;
-
-            if (features[i].hasDuration) {
-                QString duration = features[i].duration.toString().c_str();
-                duration.replace(QRegExp("^ +"), "");
-                stream << m_separator << duration;
-            }            
         }
 
-        if (summaryType != "") {
-            stream << m_separator << summaryType.c_str();
-        }
+    } else {
 
-        for (unsigned int j = 0; j < features[i].values.size(); ++j) {
-            stream << m_separator << features[i].values[j];
-        }
+        QString timestamp = f.timestamp.toString().c_str();
+        timestamp.replace(QRegExp("^ +"), "");
+        stream << timestamp;
 
-        if (features[i].label != "") {
-            stream << m_separator << "\"" << features[i].label.c_str() << "\"";
-        }
+        if (haveDuration) {
+            if (m_endTimes) {
+                QString endtime =
+                    (f.timestamp + duration).toString().c_str();
+                endtime.replace(QRegExp("^ +"), "");
+                stream << m_separator << endtime;
+            } else {
+                QString d = duration.toString().c_str();
+                d.replace(QRegExp("^ +"), "");
+                stream << m_separator << d;
+            }
+        }            
+    }
 
-        stream << "\n";
+    if (summaryType != "") {
+        stream << m_separator << summaryType.c_str();
     }
+    
+    for (unsigned int j = 0; j < f.values.size(); ++j) {
+        stream << m_separator << f.values[j];
+    }
+    
+    if (f.label != "") {
+        stream << m_separator << "\"" << f.label.c_str() << "\"";
+    }
+    
+    stream << "\n";
 }
 
 
--- a/transform/CSVFeatureWriter.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/transform/CSVFeatureWriter.h	Mon Nov 17 17:10:29 2014 +0000
@@ -40,6 +40,8 @@
     CSVFeatureWriter();
     virtual ~CSVFeatureWriter();
 
+    virtual string getDescription() const;
+
     virtual ParameterList getSupportedParameters() const;
     virtual void setParameters(map<string, string> &params);
 
@@ -49,12 +51,29 @@
                        const Vamp::Plugin::FeatureList &features,
                        std::string summaryType = "");
 
+    virtual void finish();
+
     virtual QString getWriterTag() const { return "csv"; }
 
 private:
     QString m_separator;
     bool m_sampleTiming;
+    bool m_endTimes;
+    bool m_forceEnd;
+    bool m_omitFilename;
     QString m_prevPrintedTrackId;
+
+    typedef pair<QString, Transform> DataId; // track id, transform
+    typedef map<DataId, Vamp::Plugin::Feature> PendingFeatures;
+    typedef map<DataId, std::string> PendingSummaryTypes;
+    PendingFeatures m_pending;
+    PendingSummaryTypes m_pendingSummaryTypes;
+
+    void writeFeature(DataId,
+                      QTextStream &,
+                      const Vamp::Plugin::Feature &f,
+                      const Vamp::Plugin::Feature *optionalNextFeature,
+                      std::string summaryType);
 };
 
 #endif
--- a/transform/FeatureExtractionModelTransformer.cpp	Mon Nov 17 17:09:32 2014 +0000
+++ b/transform/FeatureExtractionModelTransformer.cpp	Mon Nov 17 17:10:29 2014 +0000
@@ -679,11 +679,17 @@
         if (frequencyDomain) {
             for (int ch = 0; ch < channelCount; ++ch) {
                 int column = (blockFrame - startFrame) / stepSize;
-                fftModels[ch]->getValuesAt(column, reals, imaginaries);
-                for (int i = 0; i <= blockSize/2; ++i) {
-                    buffers[ch][i*2] = reals[i];
-                    buffers[ch][i*2+1] = imaginaries[i];
-                }
+                if (fftModels[ch]->getValuesAt(column, reals, imaginaries)) {
+                    for (int i = 0; i <= blockSize/2; ++i) {
+                        buffers[ch][i*2] = reals[i];
+                        buffers[ch][i*2+1] = imaginaries[i];
+                    }
+                } else {
+                    for (int i = 0; i <= blockSize/2; ++i) {
+                        buffers[ch][i*2] = 0.f;
+                        buffers[ch][i*2+1] = 0.f;
+                    }
+                }                    
                 error = fftModels[ch]->getError();
                 if (error != "") {
                     cerr << "FeatureExtractionModelTransformer::run: Abandoning, error is " << error << endl;
--- a/transform/FeatureWriter.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/transform/FeatureWriter.h	Mon Nov 17 17:10:29 2014 +0000
@@ -39,6 +39,8 @@
 public:
     virtual ~FeatureWriter() { }
 
+    virtual string getDescription() const = 0;
+
     struct Parameter { // parameter of the writer, not the plugin
         string name;
         string description;
@@ -77,8 +79,15 @@
         QString m_transformId;
     };
 
+    /**
+     * Notify the writer that we are about to start extraction for
+     * input file N of M (where N is 1..M). May be useful when writing
+     * multiple outputs into a single file where some syntactic
+     * element is needed to connect them.
+     */
+    virtual void setNofM(int /* N */, int /* M */) { }
+
     // may throw FailedToOpenFile or other exceptions
-
     virtual void write(QString trackid,
                        const Transform &transform,
                        const Vamp::Plugin::OutputDescriptor &output,
--- a/transform/FileFeatureWriter.cpp	Mon Nov 17 17:09:32 2014 +0000
+++ b/transform/FileFeatureWriter.cpp	Mon Nov 17 17:10:29 2014 +0000
@@ -75,14 +75,14 @@
     Parameter p;
 
     p.name = "basedir";
-    p.description = "Base output directory path.  (The default is the same directory as the input file.)";
+    p.description = "Base output directory path. (The default is the same directory as the input file.) The directory must exist already.";
     p.hasArg = true;
     pl.push_back(p);
 
     if (m_support & SupportOneFilePerTrackTransform &&
         m_support & SupportOneFilePerTrack) {
         p.name = "many-files";
-        p.description = "Create a separate output file for every combination of input file and transform.  The output file names will be based on the input file names.  (The default is to create one output file per input audio file, and write all transform results for that input into it.)";
+        p.description = "Create a separate output file for every combination of input file and transform. The output file names will be based on the input file names. (The default is to create one output file per input audio file, and write all transform results for that input into it.)";
         p.hasArg = false;
         pl.push_back(p);
     }
@@ -91,13 +91,15 @@
         if (m_support & ~SupportOneFileTotal) { // not only option
             p.name = "one-file";
             if (m_support & SupportOneFilePerTrack) {
-                p.description = "Write all transform results for all input files into the single named output file.  (The default is to create one output file per input audio file, and write all transform results for that input into it.)";
+                p.description = "Write all transform results for all input files into the single named output file. (The default is to create one output file per input audio file, and write all transform results for that input into it.)";
             } else {
-                p.description = "Write all transform results for all input files into the single named output file.  (The default is to create a separate output file for each combination of input audio file and transform.)";
+                p.description = "Write all transform results for all input files into the single named output file. (The default is to create a separate output file for each combination of input audio file and transform.)";
             }                
             p.hasArg = true;
             pl.push_back(p);
         }
+    }
+    if (m_support & SupportStdOut) {
         p.name = "stdout";
         p.description = "Write all transform results directly to standard output.";
         p.hasArg = false;
@@ -149,7 +151,7 @@
                 }
             }
         } else if (i->first == "stdout") {
-            if (m_support & SupportOneFileTotal) {
+            if (m_support & SupportStdOut) {
                 if (m_singleFileName != "") {
                     SVDEBUG << "FileFeatureWriter::setParameters: WARNING: Both stdout and one-file provided, ignoring stdout" << endl;
                 } else {
@@ -165,8 +167,8 @@
 }
 
 QString
-FileFeatureWriter::getOutputFilename(QString trackId,
-                                     TransformId transformId)
+FileFeatureWriter::createOutputFilename(QString trackId,
+                                        TransformId transformId)
 {
     if (m_singleFileName != "") {
         if (QFileInfo(m_singleFileName).exists() && !(m_force || m_append)) {
@@ -177,7 +179,9 @@
         return m_singleFileName;
     }
 
-    if (m_stdout) return "";
+    if (m_stdout) {
+        return "";
+    }
     
     QUrl url(trackId, QUrl::StrictMode);
     QString scheme = url.scheme().toLower();
@@ -235,29 +239,49 @@
     // leave it to it
     if (m_stdout || m_singleFileName != "") return;
 
-    QString filename = getOutputFilename(trackId, transformId);
+    QString filename = createOutputFilename(trackId, transformId);
     if (filename == "") {
         throw FailedToOpenOutputStream(trackId, transformId);
     }
 }
 
+FileFeatureWriter::TrackTransformPair
+FileFeatureWriter::getFilenameKey(QString trackId,
+                                  TransformId transformId)
+{
+    TrackTransformPair key;
+
+    if (m_singleFileName != "") {
+        key = TrackTransformPair("", "");
+    } else if (m_manyFiles) {
+        key = TrackTransformPair(trackId, transformId);
+    } else {
+        key = TrackTransformPair(trackId, "");
+    }
+
+    return key;
+}    
+
+QString
+FileFeatureWriter::getOutputFilename(QString trackId,
+                                     TransformId transformId)
+{
+    TrackTransformPair key = getFilenameKey(trackId, transformId);
+    if (m_filenames.find(key) == m_filenames.end()) {
+        m_filenames[key] = createOutputFilename(trackId, transformId);
+    }
+    return m_filenames[key];
+}
+
 QFile *
 FileFeatureWriter::getOutputFile(QString trackId,
                                  TransformId transformId)
 {
-    pair<QString, TransformId> key;
-
-    if (m_singleFileName != "") {
-        key = pair<QString, TransformId>("", "");
-    } else if (m_manyFiles) {
-        key = pair<QString, TransformId>(trackId, transformId);
-    } else {
-        key = pair<QString, TransformId>(trackId, "");
-    }
+    TrackTransformPair key = getFilenameKey(trackId, transformId);
 
     if (m_files.find(key) == m_files.end()) {
 
-        QString filename = getOutputFilename(trackId, transformId);
+        QString filename = createOutputFilename(trackId, transformId);
 
         if (filename == "") { // stdout or failure
             return 0;
--- a/transform/FileFeatureWriter.h	Mon Nov 17 17:09:32 2014 +0000
+++ b/transform/FileFeatureWriter.h	Mon Nov 17 17:10:29 2014 +0000
@@ -50,20 +50,34 @@
     enum FileWriteSupport {
         SupportOneFilePerTrackTransform = 1,
         SupportOneFilePerTrack = 2,
-        SupportOneFileTotal = 4
+        SupportOneFileTotal = 4,
+        SupportStdOut = 8
     };
 
     FileFeatureWriter(int support, QString extension);
     QTextStream *getOutputStream(QString, TransformId);
 
     typedef pair<QString, TransformId> TrackTransformPair;
+    typedef map<TrackTransformPair, QString> FileNameMap;
     typedef map<TrackTransformPair, QFile *> FileMap;
     typedef map<QFile *, QTextStream *> FileStreamMap;
     FileMap m_files;
+    FileNameMap m_filenames;
     FileStreamMap m_streams;
     QTextStream *m_prevstream;
 
+    TrackTransformPair getFilenameKey(QString, TransformId);
+
+    // Come up with a suitable output filename for the given track ID - 
+    // transform ID combo. Fail if it already exists, etc.
+    QString createOutputFilename(QString, TransformId);
+
+    // Look up and return the output filename for the given track ID -
+    // transform ID combo.
     QString getOutputFilename(QString, TransformId);
+
+    // Look up and return the output file handle for the given track
+    // ID - transform ID combo. Return 0 if it could not be opened.
     QFile *getOutputFile(QString, TransformId);
     
     // subclass can implement this to be called before file is opened for append