changeset 498:fdf5930b7ccc

* Bring FeatureWriter and RDFFeatureWriter into the fold (from Runner) so that we can use them to export features from SV as well
author Chris Cannam
date Fri, 28 Nov 2008 13:47:11 +0000
parents b6dc6c7f402c
children b71116d3c180
files rdf/RDFFeatureWriter.cpp rdf/RDFFeatureWriter.h rdf/rdf.pro transform/CSVFeatureWriter.cpp transform/CSVFeatureWriter.h transform/FeatureWriter.h transform/FileFeatureWriter.cpp transform/FileFeatureWriter.h transform/transform.pro
diffstat 9 files changed, 1225 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rdf/RDFFeatureWriter.cpp	Fri Nov 28 13:47:11 2008 +0000
@@ -0,0 +1,545 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Annotator
+    A utility for batch feature extraction from audio files.
+    Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
+    Copyright 2007-2008 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 <fstream>
+
+#include "vamp-hostsdk/PluginHostAdapter.h"
+#include "vamp-hostsdk/PluginLoader.h"
+
+#include "RDFFeatureWriter.h"
+#include "RDFTransformFactory.h"
+
+#include <QTextStream>
+#include <QUrl>
+#include <QRegExp>
+
+using namespace std;
+using Vamp::Plugin;
+using Vamp::PluginBase;
+
+RDFFeatureWriter::RDFFeatureWriter() :
+    FileFeatureWriter(SupportOneFilePerTrackTransform |
+                      SupportOneFilePerTrack |
+                      SupportOneFileTotal,
+                      "n3"),
+    m_plain(false),
+    m_count(0)
+{
+}
+
+RDFFeatureWriter::~RDFFeatureWriter()
+{
+}
+
+RDFFeatureWriter::ParameterList
+RDFFeatureWriter::getSupportedParameters() const
+{
+    ParameterList pl = FileFeatureWriter::getSupportedParameters();
+    Parameter p;
+
+    p.name = "plain";
+    p.description = "Use \"plain\" RDF even if transform metadata is available.";
+    p.hasArg = false;
+    pl.push_back(p);
+
+    p.name = "signal-uri";
+    p.description = "Link the output RDF to the given signal URI.";
+    p.hasArg = true;
+    pl.push_back(p);
+    
+    return pl;
+}
+
+void
+RDFFeatureWriter::setParameters(map<string, string> &params)
+{
+    FileFeatureWriter::setParameters(params);
+
+    for (map<string, string>::iterator i = params.begin();
+         i != params.end(); ++i) {
+        if (i->first == "plain") {
+            m_plain = true;
+        }
+        if (i->first == "signal-uri") {
+            m_suri = i->second.c_str();
+        }
+    }
+}
+
+void RDFFeatureWriter::write(QString trackId,
+                             const Transform &transform,
+                             const Plugin::OutputDescriptor& output,
+                             const Plugin::FeatureList& features,
+                             std::string summaryType)
+{
+    QString pluginId = transform.getPluginIdentifier();
+
+    if (m_rdfDescriptions.find(pluginId) == m_rdfDescriptions.end()) {
+
+        m_rdfDescriptions[pluginId] = PluginRDFDescription(pluginId);
+
+        if (m_rdfDescriptions[pluginId].haveDescription()) {
+            cerr << "NOTE: Have RDF description for plugin ID \""
+                 << pluginId.toStdString() << "\"" << endl;
+        } else {
+            cerr << "NOTE: Do not have RDF description for plugin ID \""
+                 << pluginId.toStdString() << "\"" << endl;
+        }
+    }
+
+    // Need to select appropriate output file for our track/transform
+    // combination
+
+    QTextStream *stream = getOutputStream(trackId, transform.getIdentifier());
+    if (!stream) return; //!!! this is probably better handled with an exception
+
+    if (m_startedStreamTransforms.find(stream) ==
+        m_startedStreamTransforms.end()) {
+        cerr << "This stream is new, writing prefixes" << endl;
+        writePrefixes(stream);
+        if (m_singleFileName == "" && !m_stdout) {
+            writeSignalDescription(stream, trackId);
+        }
+    }
+
+    if (m_startedStreamTransforms[stream].find(transform) ==
+        m_startedStreamTransforms[stream].end()) {
+        m_startedStreamTransforms[stream].insert(transform);
+        writeLocalFeatureTypes
+            (stream, transform, output, m_rdfDescriptions[pluginId]);
+    }
+
+    if (m_singleFileName != "" || m_stdout) {
+        if (m_startedTrackIds.find(trackId) == m_startedTrackIds.end()) {
+            writeSignalDescription(stream, trackId);
+            m_startedTrackIds.insert(trackId);
+        }
+    }
+
+    QString timelineURI = m_trackTimelineURIs[trackId];
+    
+    if (timelineURI == "") {
+        cerr << "RDFFeatureWriter: INTERNAL ERROR: writing features without having established a timeline URI!" << endl;
+        exit(1);
+    }
+
+    if (summaryType != "") {
+
+        writeSparseRDF(stream, transform, output, features,
+                       m_rdfDescriptions[pluginId], timelineURI);
+
+    } else if (m_rdfDescriptions[pluginId].haveDescription() &&
+               m_rdfDescriptions[pluginId].getOutputDisposition
+               (output.identifier.c_str()) == 
+               PluginRDFDescription::OutputDense) {
+
+        QString signalURI = m_trackSignalURIs[trackId];
+
+        if (signalURI == "") {
+            cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having established a signal URI!" << endl;
+            exit(1);
+        }
+
+        writeDenseRDF(stream, transform, output, features,
+                      m_rdfDescriptions[pluginId], signalURI, timelineURI);
+
+    } else {
+
+        writeSparseRDF(stream, transform, output, features,
+                       m_rdfDescriptions[pluginId], timelineURI);
+    }
+}
+
+void
+RDFFeatureWriter::writePrefixes(QTextStream *sptr)
+{
+    QTextStream &stream = *sptr;
+
+    stream << "@prefix dc: <http://purl.org/dc/elements/1.1/> .\n"
+           << "@prefix mo: <http://purl.org/ontology/mo/> .\n"
+           << "@prefix af: <http://purl.org/ontology/af/> .\n"
+           << "@prefix event: <http://purl.org/NET/c4dm/event.owl#> .\n"
+           << "@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n"
+           << "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n"
+           << "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n"
+           << "@prefix tl: <http://purl.org/NET/c4dm/timeline.owl#> .\n"
+           << "@prefix vamp: <http://purl.org/ontology/vamp/> .\n"
+           << "@prefix : <#> .\n\n";
+}
+
+void
+RDFFeatureWriter::writeSignalDescription(QTextStream *sptr,
+                                         QString trackId)
+{
+    QTextStream &stream = *sptr;
+
+    /*
+     * Describe signal we're analysing (AudioFile, Signal, TimeLine, etc.)
+     */
+    
+    QUrl url(trackId);
+    QString scheme = url.scheme().toLower();
+    bool local = (scheme == "" || scheme == "file" || scheme.length() == 1);
+
+    if (local) {
+        if (scheme == "") {
+            url.setScheme("file");
+        } else if (scheme.length() == 1) { // DOS drive letter!
+            url.setScheme("file");
+            url.setPath(scheme + ":" + url.path());
+        }
+    }
+
+    //!!! FIX: If we are appending, we need to start counting after
+    //all of the existing counts that are already in the file!
+
+    uint64_t signalCount = m_count++;
+
+    if (m_trackSignalURIs.find(trackId) == m_trackSignalURIs.end()) {
+        m_trackSignalURIs[trackId] = QString(":signal_%1").arg(signalCount);
+    }
+    
+    if (m_suri != NULL) {
+        m_trackSignalURIs[trackId] = "<" + m_suri + ">";
+    }
+    QString signalURI = m_trackSignalURIs[trackId];
+   
+    if (m_trackTimelineURIs.find(trackId) == m_trackTimelineURIs.end()) {
+        m_trackTimelineURIs[trackId] = QString(":signal_timeline_%1").arg(signalCount);
+    }
+    QString timelineURI = m_trackTimelineURIs[trackId];
+
+    stream << "\n<" << url.toEncoded().data() << "> a mo:AudioFile .\n\n"
+           << signalURI << " a mo:Signal ;\n"
+           << "    mo:available_as <" << url.toEncoded().data()
+           << "> ;\n"
+           << "    mo:time [\n"
+           << "        a tl:Interval ;\n"
+           << "        tl:onTimeLine "
+           << timelineURI << "\n    ] .\n\n";
+} 
+
+void
+RDFFeatureWriter::writeLocalFeatureTypes(QTextStream *sptr,
+                                         const Transform &transform,
+                                         const Plugin::OutputDescriptor &od,
+                                         PluginRDFDescription &desc)
+{
+    QString outputId = od.identifier.c_str();
+    QTextStream &stream = *sptr;
+
+    bool needEventType = false;
+    bool needSignalType = false;
+
+    //!!! feature attribute type is not yet supported
+
+    //!!! bin names, extents and so on can be written out using e.g. vamp:bin_names ( "a" "b" "c" ) 
+
+    if (desc.getOutputDisposition(outputId) == 
+        PluginRDFDescription::OutputDense) {
+
+        // no feature events, so may need signal type but won't need
+        // event type
+
+        if (m_plain) {
+
+            needSignalType = true;
+
+        } else if (desc.getOutputSignalTypeURI(outputId) == "") {
+            
+            needSignalType = true;
+        }
+
+    } else {
+
+        // may need event type but won't need signal type
+
+        if (m_plain) {
+        
+            needEventType = true;
+    
+        } else if (desc.getOutputEventTypeURI(outputId) == "") {
+
+            needEventType = true;
+        }
+    }
+
+    QString transformUri;
+    if (m_transformURIs.find(transform) != m_transformURIs.end()) {
+        transformUri = m_transformURIs[transform];
+    } else {
+        transformUri = QString(":transform_%1_%2").arg(m_count++).arg(outputId);
+        m_transformURIs[transform] = transformUri;
+    }
+
+    stream << RDFTransformFactory::writeTransformToRDF(transform, transformUri)
+           << endl;
+
+    if (needEventType) {
+
+        QString uri;
+        if (m_syntheticEventTypeURIs.find(transform) !=
+            m_syntheticEventTypeURIs.end()) {
+            uri = m_syntheticEventTypeURIs[transform];
+        } else {
+            uri = QString(":event_type_%1").arg(m_count++);
+            m_syntheticEventTypeURIs[transform] = uri;
+        }
+
+        stream << uri
+               << " rdfs:subClassOf event:Event ;" << endl
+               << "    dc:title \"" << od.name.c_str() << "\" ;" << endl
+               << "    dc:format \"" << od.unit.c_str() << "\" ;" << endl
+               << "    dc:description \"" << od.description.c_str() << "\" ."
+               << endl << endl;
+    }
+
+    if (needSignalType) {
+
+        QString uri;
+        if (m_syntheticSignalTypeURIs.find(transform) !=
+            m_syntheticSignalTypeURIs.end()) {
+            uri = m_syntheticSignalTypeURIs[transform];
+        } else {
+            uri = QString(":signal_type_%1").arg(m_count++);
+            m_syntheticSignalTypeURIs[transform] = uri;
+        }
+
+        stream << uri
+               << " rdfs:subClassOf af:Signal ;" << endl
+               << "    dc:title \"" << od.name.c_str() << "\" ;" << endl
+               << "    dc:format \"" << od.unit.c_str() << "\" ;" << endl
+               << "    dc:description \"" << od.description.c_str() << "\" ."
+               << endl << endl;
+    }
+}
+
+void
+RDFFeatureWriter::writeSparseRDF(QTextStream *sptr,
+                                 const Transform &transform,
+                                 const Plugin::OutputDescriptor& od,
+                                 const Plugin::FeatureList& featureList,
+                                 PluginRDFDescription &desc,
+                                 QString timelineURI)
+{
+    if (featureList.empty()) return;
+    QTextStream &stream = *sptr;
+        
+    bool plain = (m_plain || !desc.haveDescription());
+
+    QString outputId = od.identifier.c_str();
+
+    // iterate through FeatureLists
+        
+    for (int i = 0; i < featureList.size(); ++i) {
+
+        const Plugin::Feature &feature = featureList[i];
+        uint64_t featureNumber = m_count++;
+
+        stream << ":event_" << featureNumber << " a ";
+
+        QString eventTypeURI = desc.getOutputEventTypeURI(outputId);
+        if (plain || eventTypeURI == "") {
+            if (m_syntheticEventTypeURIs.find(transform) != 
+                m_syntheticEventTypeURIs.end()) {
+                stream << m_syntheticEventTypeURIs[transform] << " ;\n";
+            } else {
+                stream << ":event_type_" << outputId << " ;\n";
+            }
+        } else {
+            stream << "<" << eventTypeURI << "> ;\n";
+        }
+
+        QString timestamp = feature.timestamp.toString().c_str();
+        timestamp.replace(QRegExp("^ +"), "");
+
+        if (feature.hasDuration && feature.duration > Vamp::RealTime::zeroTime) {
+
+            QString duration = feature.duration.toString().c_str();
+            duration.replace(QRegExp("^ +"), "");
+
+            stream << "    event:time [ \n"
+                   << "        a tl:Interval ;\n"
+                   << "        tl:onTimeLine " << timelineURI << " ;\n"
+                   << "        tl:beginsAt \"PT" << timestamp
+                   << "S\"^^xsd:duration ;\n"
+                   << "        tl:duration \"PT" << duration
+                   << "S\"^^xsd:duration ;\n"
+                   << "    ] ";
+
+        } else {
+
+            stream << "    event:time [ \n"
+                   << "        a tl:Instant ;\n" //location of the event in time
+                   << "        tl:onTimeLine " << timelineURI << " ;\n"
+                   << "        tl:at \"PT" << timestamp
+                   << "S\"^^xsd:duration ;\n    ] ";
+        }
+
+        stream << ";\n";
+        stream << "    vamp:computed_by " << m_transformURIs[transform] << " ";
+
+        if (feature.label.length() > 0) {
+            stream << ";\n";
+            stream << "    rdfs:label \"" << feature.label.c_str() << "\" ";
+        }
+
+        if (!feature.values.empty()) {
+            stream << ";\n";
+            //!!! named bins?
+            stream << "    af:feature \"" << feature.values[0];
+            for (int j = 1; j < feature.values.size(); ++j) {
+                stream << " " << feature.values[j];
+            }
+            stream << "\" ";
+        }
+
+        stream << ".\n";
+    }
+}
+
+void
+RDFFeatureWriter::writeDenseRDF(QTextStream *sptr,
+                                const Transform &transform,
+                                const Plugin::OutputDescriptor& od,
+                                const Plugin::FeatureList& featureList,
+                                PluginRDFDescription &desc,
+                                QString signalURI, 
+                                QString timelineURI)
+{
+    if (featureList.empty()) return;
+
+    StringTransformPair sp(signalURI, transform);
+
+    if (m_openDenseFeatures.find(sp) == m_openDenseFeatures.end()) {
+
+        StreamBuffer b(sptr, "");
+        m_openDenseFeatures[sp] = b;
+        
+        QString &str(m_openDenseFeatures[sp].second);
+        QTextStream stream(&str);
+
+        bool plain = (m_plain || !desc.haveDescription());
+        QString outputId = od.identifier.c_str();
+
+        uint64_t featureNumber = m_count++;
+
+        // need to write out feature timeline map -- for this we need
+        // the sample rate, window length and hop size from the
+        // transform
+
+        stream << "\n:feature_timeline_" << featureNumber << " a tl:DiscreteTimeLine .\n\n";
+
+        size_t stepSize = transform.getStepSize();
+        if (stepSize == 0) {
+            cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the step size properly!" << endl;
+            return;
+        }
+
+        size_t blockSize = transform.getBlockSize();
+        if (blockSize == 0) {
+            cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the block size properly!" << endl;
+            return;
+        }
+
+        float sampleRate = transform.getSampleRate();
+        if (sampleRate == 0.f) {
+            cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the sample rate 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:windowLength \"" << blockSize << "\"^^xsd:int ;\n"
+               << "    tl:hopSize \"" << stepSize << "\"^^xsd:int .\n\n";
+
+        stream << signalURI << " af:signal_feature :feature_"
+               << featureNumber << " ." << endl << endl;
+
+        stream << ":feature_" << featureNumber << " a ";
+
+        QString signalTypeURI = desc.getOutputSignalTypeURI(outputId);
+        if (plain || signalTypeURI == "") {
+            if (m_syntheticSignalTypeURIs.find(transform) !=
+                m_syntheticSignalTypeURIs.end()) {
+                stream << m_syntheticSignalTypeURIs[transform] << " ;\n";
+            } else {
+                stream << ":signal_type_" << outputId << " ;\n";
+            }
+        } else {
+            stream << signalTypeURI << " ;\n";
+        }
+
+        stream << "    mo:time ["
+               << "\n        a tl:Interval ;"
+               << "\n        tl:onTimeLine :feature_timeline_" << featureNumber << " ;";
+
+        RealTime startrt = transform.getStartTime();
+        RealTime durationrt = transform.getDuration();
+
+        int start = RealTime::realTime2Frame(startrt, sampleRate) / stepSize;
+        int duration = RealTime::realTime2Frame(durationrt, sampleRate) / stepSize;
+
+        if (start != 0) {
+            stream << "\n        tl:start \"" << start << "\"^^xsd:int ;";
+        }
+        if (duration != 0) {
+            stream << "\n        tl:duration \"" << duration << "\"^^xsd:int ;";
+        }
+
+        stream << "\n    ] ;\n";
+
+        if (od.hasFixedBinCount) {
+            // We only know the height, so write the width as zero
+            stream << "    af:dimensions \"" << od.binCount << " 0\" ;\n";
+        }
+
+        stream << "    af:value \"";
+    }
+
+    QString &str = m_openDenseFeatures[sp].second;
+    QTextStream stream(&str);
+
+    for (int i = 0; i < featureList.size(); ++i) {
+
+        const Plugin::Feature &feature = featureList[i];
+
+        for (int j = 0; j < feature.values.size(); ++j) {
+            stream << feature.values[j] << " ";
+        }
+    }
+}
+
+void RDFFeatureWriter::finish()
+{
+//    cerr << "RDFFeatureWriter::finish()" << endl;
+
+    // close any open dense feature literals
+
+    for (map<StringTransformPair, StreamBuffer>::iterator i =
+             m_openDenseFeatures.begin();
+         i != m_openDenseFeatures.end(); ++i) {
+        cerr << "closing a stream" << endl;
+        StreamBuffer &b = i->second;
+        *(b.first) << b.second << "\" ." << endl;
+    }
+
+    m_openDenseFeatures.clear();
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rdf/RDFFeatureWriter.h	Fri Nov 28 13:47:11 2008 +0000
@@ -0,0 +1,105 @@
+/* -*- 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.
+
+    Sonic Annotator
+    A utility for batch feature extraction from audio files.
+
+    Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
+    Copyright 2007-2008 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.
+*/
+
+#ifndef _RDF_FEATURE_WRITER_H_
+#define _RDF_FEATURE_WRITER_H_
+
+#include <string>
+#include <map>
+#include <set>
+
+#include <QString>
+
+#include "transform/FileFeatureWriter.h"
+
+#include "PluginRDFDescription.h"
+
+using std::string;
+using std::map;
+using std::set;
+using std::pair;
+
+class QTextStream;
+class QFile;
+
+class RDFFeatureWriter : public FileFeatureWriter
+{
+public:
+    RDFFeatureWriter();
+    virtual ~RDFFeatureWriter();
+
+    virtual ParameterList getSupportedParameters() const;
+    virtual void setParameters(map<string, string> &params);
+
+    virtual void write(QString trackid,
+                       const Transform &transform,
+                       const Vamp::Plugin::OutputDescriptor &output,
+                       const Vamp::Plugin::FeatureList &features,
+                       std::string summaryType = "");
+
+    virtual void finish();
+
+private:
+    typedef map<QString, PluginRDFDescription> RDFDescriptionMap; // by plugin id
+    RDFDescriptionMap m_rdfDescriptions;
+
+    void writePrefixes(QTextStream *);
+    void writeSignalDescription(QTextStream *, QString);
+    void writeLocalFeatureTypes(QTextStream *,
+                                const Transform &,
+                                const Vamp::Plugin::OutputDescriptor &,
+                                PluginRDFDescription &);
+
+    void writeSparseRDF(QTextStream *stream,
+                        const Transform &transform,
+                        const Vamp::Plugin::OutputDescriptor &output,
+                        const Vamp::Plugin::FeatureList &features,
+                        PluginRDFDescription &desc,
+                        QString timelineURI);
+
+    void writeDenseRDF(QTextStream *stream,
+                       const Transform &transform,
+                       const Vamp::Plugin::OutputDescriptor &output,
+                       const Vamp::Plugin::FeatureList &features,
+                       PluginRDFDescription &desc,
+                       QString signalURI,
+                       QString timelineURI);
+
+    set<QString> m_startedTrackIds;
+
+    map<QTextStream *, set<Transform> > m_startedStreamTransforms;
+
+    map<QString, QString> m_trackTimelineURIs;
+    map<QString, QString> m_trackSignalURIs;
+
+    map<Transform, QString> m_transformURIs;
+    map<Transform, QString> m_syntheticEventTypeURIs;
+    map<Transform, QString> m_syntheticSignalTypeURIs;
+
+    typedef pair<QString, Transform> StringTransformPair;
+    typedef pair<QTextStream *, QString> StreamBuffer;
+    map<StringTransformPair, StreamBuffer> m_openDenseFeatures; // signal URI + transform -> stream + text
+    QString m_suri;
+
+    bool m_plain;
+
+    uint64_t m_count;
+};
+
+#endif
--- a/rdf/rdf.pro	Fri Nov 28 13:36:13 2008 +0000
+++ b/rdf/rdf.pro	Fri Nov 28 13:47:11 2008 +0000
@@ -15,11 +15,13 @@
 # Input
 HEADERS += PluginRDFDescription.h \
            PluginRDFIndexer.h \
+           RDFFeatureWriter.h \
            RDFImporter.h \
 	   RDFTransformFactory.h \
            SimpleSPARQLQuery.h
 SOURCES += PluginRDFDescription.cpp \
            PluginRDFIndexer.cpp \
+           RDFFeatureWriter.cpp \
            RDFImporter.cpp \
            RDFTransformFactory.cpp \
            SimpleSPARQLQuery.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/CSVFeatureWriter.cpp	Fri Nov 28 13:47:11 2008 +0000
@@ -0,0 +1,105 @@
+/* -*- 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.
+
+    Sonic Annotator
+    A utility for batch feature extraction from audio files.
+
+    Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
+    Copyright 2007-2008 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 "CSVFeatureWriter.h"
+
+#include <iostream>
+
+#include <QRegExp>
+#include <QTextStream>
+
+using namespace std;
+using namespace Vamp;
+
+CSVFeatureWriter::CSVFeatureWriter() :
+    FileFeatureWriter(SupportOneFilePerTrackTransform |
+                      SupportOneFileTotal,
+                      "csv"),
+    m_separator(",")
+{
+}
+
+CSVFeatureWriter::~CSVFeatureWriter()
+{
+}
+
+CSVFeatureWriter::ParameterList
+CSVFeatureWriter::getSupportedParameters() const
+{
+    ParameterList pl = FileFeatureWriter::getSupportedParameters();
+    Parameter p;
+    
+    p.name = "separator";
+    p.description = "Column separator for output.  Default is \",\" (comma).";
+    p.hasArg = true;
+    pl.push_back(p);
+
+    return pl;
+}
+
+void
+CSVFeatureWriter::setParameters(map<string, string> &params)
+{
+    FileFeatureWriter::setParameters(params);
+
+    cerr << "CSVFeatureWriter::setParameters" << endl;
+    for (map<string, string>::iterator i = params.begin();
+         i != params.end(); ++i) {
+        cerr << i->first << " -> " << i->second << endl;
+        if (i->first == "separator") {
+            m_separator = i->second.c_str();
+        }
+    }
+}
+
+void
+CSVFeatureWriter::write(QString trackId,
+                        const Transform &transform,
+                        const Plugin::OutputDescriptor& output,
+                        const Plugin::FeatureList& features,
+                        std::string summaryType)
+{
+    // Select appropriate output file for our track/transform
+    // combination
+
+    QTextStream *sptr = getOutputStream(trackId, transform.getIdentifier());
+    if (!sptr) return; //!!! this is probably better handled with an exception
+
+    QTextStream &stream = *sptr;
+
+    for (unsigned int i = 0; i < features.size(); ++i) {
+
+        QString timestamp = features[i].timestamp.toString().c_str();
+        timestamp.replace(QRegExp("^ +"), "");
+
+        stream << timestamp;
+
+        if (summaryType != "") {
+            stream << m_separator << summaryType.c_str();
+        }
+
+        for (unsigned int j = 0; j < features[i].values.size(); ++j) {
+            stream << m_separator << features[i].values[j];
+        }
+
+        stream << "\n";
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/CSVFeatureWriter.h	Fri Nov 28 13:47:11 2008 +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.
+
+    Sonic Annotator
+    A utility for batch feature extraction from audio files.
+
+    Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
+    Copyright 2007-2008 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.
+*/
+
+#ifndef _CSV_FEATURE_WRITER_H_
+#define _CSV_FEATURE_WRITER_H_
+
+#include <string>
+#include <map>
+#include <set>
+
+#include <QString>
+
+#include "FileFeatureWriter.h"
+
+using std::string;
+using std::map;
+
+class QTextStream;
+class QFile;
+
+class CSVFeatureWriter : public FileFeatureWriter
+{
+public:
+    CSVFeatureWriter();
+    virtual ~CSVFeatureWriter();
+
+    virtual ParameterList getSupportedParameters() const;
+    virtual void setParameters(map<string, string> &params);
+
+    virtual void write(QString trackid,
+                       const Transform &transform,
+                       const Vamp::Plugin::OutputDescriptor &output,
+                       const Vamp::Plugin::FeatureList &features,
+                       std::string summaryType = "");
+
+    virtual void finish() { }
+
+private:
+    QString m_separator;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/FeatureWriter.h	Fri Nov 28 13:47:11 2008 +0000
@@ -0,0 +1,67 @@
+/* -*- 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.
+
+    Sonic Annotator
+    A utility for batch feature extraction from audio files.
+
+    Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
+    Copyright 2007-2008 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.
+*/
+
+#ifndef _FEATURE_WRITER_H_
+#define _FEATURE_WRITER_H_
+
+#include <string>
+#include <map>
+#include <vector>
+
+#include <QString>
+
+#include "Transform.h"
+
+#include <vamp-hostsdk/Plugin.h>
+
+using std::string;
+using std::map;
+using std::vector;
+
+class FeatureWriter
+{
+public:
+    virtual ~FeatureWriter() { }
+
+    struct Parameter { // parameter of the writer, not the plugin
+        string name;
+        string description;
+        bool hasArg;
+    };
+    typedef vector<Parameter> ParameterList;
+    virtual ParameterList getSupportedParameters() const {
+        return ParameterList();
+    }
+
+    virtual void setParameters(map<string, string> &params) {
+        return;
+    }
+
+    // may throw FailedToOpenFile or other exceptions
+
+    virtual void write(QString trackid,
+                       const Transform &transform,
+                       const Vamp::Plugin::OutputDescriptor &output,
+                       const Vamp::Plugin::FeatureList &features,
+                       std::string summaryType = "") = 0;
+
+    virtual void finish() = 0;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/FileFeatureWriter.cpp	Fri Nov 28 13:47:11 2008 +0000
@@ -0,0 +1,262 @@
+/* -*- 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.
+
+    Sonic Annotator
+    A utility for batch feature extraction from audio files.
+
+    Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
+    Copyright 2007-2008 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 "FileFeatureWriter.h"
+
+#include "base/Exceptions.h"
+
+#include <QTextStream>
+#include <QFile>
+#include <QFileInfo>
+#include <QUrl>
+#include <QDir>
+
+using namespace std;
+using namespace Vamp;
+
+FileFeatureWriter::FileFeatureWriter(int support,
+                                     QString extension) :
+    m_support(support),
+    m_extension(extension),
+    m_manyFiles(false),
+    m_stdout(false),
+    m_append(false),
+    m_force(false)
+{
+    if (!(m_support & SupportOneFilePerTrack)) {
+        if (m_support & SupportOneFilePerTrackTransform) {
+            m_manyFiles = true;
+        } else if (m_support & SupportOneFileTotal) {
+            m_singleFileName = QString("output.%1").arg(m_extension);
+        } else {
+            cerr << "FileFeatureWriter::FileFeatureWriter: ERROR: Invalid support specification " << support << endl;
+        }
+    }
+}
+
+FileFeatureWriter::~FileFeatureWriter()
+{
+    while (!m_streams.empty()) {
+        m_streams.begin()->second->flush();
+        delete m_streams.begin()->second;
+        m_streams.erase(m_streams.begin());
+    }
+    while (!m_files.empty()) {
+        delete m_files.begin()->second;
+        m_files.erase(m_files.begin());
+    }
+}
+
+FileFeatureWriter::ParameterList
+FileFeatureWriter::getSupportedParameters() const
+{
+    ParameterList pl;
+    Parameter p;
+
+    p.name = "basedir";
+    p.description = "Base output directory path.  (The default is the same directory as the input file.)";
+    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.hasArg = false;
+        pl.push_back(p);
+    }
+
+    if (m_support & SupportOneFileTotal) {
+        if (m_support & ~SupportOneFileTotal) { // not only option
+            p.name = "one-file";
+            p.description = "Write all transform results for all input files into the single named output file.";
+            p.hasArg = true;
+            pl.push_back(p);
+        }
+        p.name = "stdout";
+        p.description = "Write all transform results directly to standard output.";
+        p.hasArg = false;
+        pl.push_back(p);
+    }
+
+    p.name = "force";
+    p.description = "If an output file already exists, overwrite it.";
+    p.hasArg = false;
+    pl.push_back(p);
+
+    p.name = "append";
+    p.description = "If an output file already exists, append data to it.";
+    p.hasArg = false;
+    pl.push_back(p);
+
+    return pl;
+}
+
+void
+FileFeatureWriter::setParameters(map<string, string> &params)
+{
+    for (map<string, string>::iterator i = params.begin();
+         i != params.end(); ++i) {
+        if (i->first == "basedir") {
+            m_baseDir = i->second.c_str();
+        } else if (i->first == "many-files") {
+            if (m_support & SupportOneFilePerTrackTransform &&
+                m_support & SupportOneFilePerTrack) {
+                if (m_singleFileName != "") {
+                    cerr << "FileFeatureWriter::setParameters: WARNING: Both one-file and many-files parameters provided, ignoring many-files" << endl;
+                } else {
+                    m_manyFiles = true;
+                }
+            }
+        } else if (i->first == "one-file") {
+            if (m_support & SupportOneFileTotal) {
+                if (m_support & ~SupportOneFileTotal) { // not only option
+                    if (m_manyFiles) {
+                        cerr << "FileFeatureWriter::setParameters: WARNING: Both many-files and one-file parameters provided, ignoring one-file" << endl;
+                    } else {
+                        m_singleFileName = i->second.c_str();
+                    }
+                }
+            }
+        } else if (i->first == "stdout") {
+            if (m_support & SupportOneFileTotal) {
+                if (m_singleFileName != "") {
+                    cerr << "FileFeatureWriter::setParameters: WARNING: Both stdout and one-file provided, ignoring stdout" << endl;
+                } else {
+                    m_stdout = true;
+                }
+            }
+        } else if (i->first == "append") {
+            m_append = true;
+        } else if (i->first == "force") {
+            m_force = true;
+        }
+    }
+}
+
+QString FileFeatureWriter::getOutputFilename(QString trackId,
+                                             TransformId transformId)
+{
+    if (m_singleFileName != "") {
+        if (QFileInfo(m_singleFileName).exists() && !(m_force || m_append)) {
+            cerr << "FileFeatureWriter: ERROR: Specified output file \"" << m_singleFileName.toStdString() << "\" exists and neither force nor append flag is specified -- not overwriting" << endl;
+            return "";
+        }
+        return m_singleFileName;
+    }
+
+    if (m_stdout) return "";
+    
+    QUrl url(trackId);
+    QString scheme = url.scheme().toLower();
+    bool local = (scheme == "" || scheme == "file" || scheme.length() == 1);
+
+    QString dirname, basename;
+    QString infilename = url.toLocalFile();
+    if (infilename == "") infilename = url.path();
+    basename = QFileInfo(infilename).baseName();
+//    cerr << "url = " << url.toString().toStdString() << ", infilename = "
+//         << infilename.toStdString() << ", basename = " << basename.toStdString() << endl;
+
+
+    if (m_baseDir != "") dirname = QFileInfo(m_baseDir).absoluteFilePath();
+    else if (local) dirname = QFileInfo(infilename).absolutePath();
+    else dirname = QDir::currentPath();
+
+    QString filename;
+
+    if (m_manyFiles && transformId != "") {
+        filename = QString("%1:%2.%3").arg(basename).arg(transformId).arg(m_extension);
+    } else {
+        filename = QString("%1.%2").arg(basename).arg(m_extension);
+    }
+
+    filename = QDir(dirname).filePath(filename);
+
+    if (QFileInfo(filename).exists() && !(m_force || m_append)) {
+        cerr << "FileFeatureWriter: ERROR: Output file \"" << filename.toStdString() << "\" exists (for input file or URL \"" << trackId.toStdString() << "\" and transform \"" << transformId.toStdString() << "\") and neither force nor append is specified -- not overwriting" << endl;
+        return "";
+    }
+    
+    return filename;
+}
+
+
+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, "");
+    }
+
+    if (m_files.find(key) == m_files.end()) {
+
+        QString filename = getOutputFilename(trackId, transformId);
+
+        if (filename == "") { // stdout
+            return 0;
+        }
+
+        cerr << "FileFeatureWriter: NOTE: Using output filename \""
+             << filename.toStdString() << "\"" << endl;
+
+        QFile *file = new QFile(filename);
+        QIODevice::OpenMode mode = (QIODevice::WriteOnly);
+        if (m_append) mode |= QIODevice::Append;
+                       
+        if (!file->open(mode)) {
+            cerr << "FileFeatureWriter: ERROR: Failed to open output file \"" << filename.toStdString()
+                 << "\" for writing" << endl;
+            delete file;
+            m_files[key] = 0;
+            throw FailedToOpenFile(filename);
+        }
+
+        m_files[key] = file;
+    }
+
+    return m_files[key];
+}
+
+
+QTextStream *FileFeatureWriter::getOutputStream(QString trackId,
+                                               TransformId transformId)
+{
+    QFile *file = getOutputFile(trackId, transformId);
+    if (!file && !m_stdout) {
+        return 0;
+    }
+
+    if (m_streams.find(file) == m_streams.end()) {
+        if (m_stdout) {
+            m_streams[file] = new QTextStream(stdout);
+        } else {
+            m_streams[file] = new QTextStream(file);
+        }
+    }
+
+    return m_streams[file];
+}
+            
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/transform/FileFeatureWriter.h	Fri Nov 28 13:47:11 2008 +0000
@@ -0,0 +1,74 @@
+/* -*- 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.
+
+    Sonic Annotator
+    A utility for batch feature extraction from audio files.
+
+    Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
+    Copyright 2007-2008 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.
+*/
+
+#ifndef _FILE_FEATURE_WRITER_H_
+#define _FILE_FEATURE_WRITER_H_
+
+#include <string>
+#include <map>
+#include <set>
+
+#include "FeatureWriter.h"
+
+using std::string;
+using std::map;
+using std::set;
+using std::pair;
+
+class QTextStream;
+class QFile;
+
+class FileFeatureWriter : public FeatureWriter
+{
+public:
+    virtual ~FileFeatureWriter();
+
+    virtual ParameterList getSupportedParameters() const;
+    virtual void setParameters(map<string, string> &params);
+
+protected:
+    enum FileWriteSupport {
+        SupportOneFilePerTrackTransform = 1,
+        SupportOneFilePerTrack = 2,
+        SupportOneFileTotal = 4
+    };
+
+    FileFeatureWriter(int support, QString extension);
+    QTextStream *getOutputStream(QString, TransformId);
+
+    typedef pair<QString, TransformId> TrackTransformPair;
+    typedef map<TrackTransformPair, QFile *> FileMap;
+    typedef map<QFile *, QTextStream *> FileStreamMap;
+    FileMap m_files;
+    FileStreamMap m_streams;
+
+    QString getOutputFilename(QString, TransformId);
+    QFile *getOutputFile(QString, TransformId);
+
+    int m_support;
+    QString m_extension;
+    QString m_baseDir;
+    bool m_manyFiles;
+    QString m_singleFileName;
+    bool m_stdout;
+    bool m_append;
+    bool m_force;
+};
+
+#endif
--- a/transform/transform.pro	Fri Nov 28 13:36:13 2008 +0000
+++ b/transform/transform.pro	Fri Nov 28 13:47:11 2008 +0000
@@ -14,14 +14,19 @@
 MOC_DIR = tmp_moc
 
 # Input
-HEADERS += FeatureExtractionModelTransformer.h \
+HEADERS += CSVFeatureWriter.h \
+           FeatureExtractionModelTransformer.h \
+           FeatureWriter.h \
+           FileFeatureWriter.h \
            RealTimeEffectModelTransformer.h \
            Transform.h \
            TransformDescription.h \
            TransformFactory.h \
            ModelTransformer.h \
            ModelTransformerFactory.h
-SOURCES += FeatureExtractionModelTransformer.cpp \
+SOURCES += CSVFeatureWriter.cpp \
+           FeatureExtractionModelTransformer.cpp \
+           FileFeatureWriter.cpp \
            RealTimeEffectModelTransformer.cpp \
            Transform.cpp \
            TransformFactory.cpp \