changeset 171:c1834a31029c

Merge from branch "jams"
author Chris Cannam
date Wed, 15 Oct 2014 16:09:47 +0100
parents f4f770b4356b (current diff) 3536342ac088 (diff)
children 64a7faf9a122
files
diffstat 13 files changed, 810 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/.hgsubstate	Wed Oct 15 13:06:27 2014 +0100
+++ b/.hgsubstate	Wed Oct 15 16:09:47 2014 +0100
@@ -1,3 +1,3 @@
 d16f0fd6db6104d87882bc43788a3bb1b0f8c528 dataquay
 879bdc878826bebec67130326f99397c430419b1 sv-dependency-builds
-274c4362bda661a7ef7706cf6b160777b88b3abe svcore
+6b2a8b34e9d3d9a573af2ae303f33362e4c17784 svcore
--- a/runner.pro	Wed Oct 15 13:06:27 2014 +0100
+++ b/runner.pro	Wed Oct 15 16:09:47 2014 +0100
@@ -84,6 +84,7 @@
         runner/FeatureWriterFactory.h  \
         runner/DefaultFeatureWriter.h \
         runner/FeatureExtractionManager.h \
+        runner/JAMSFeatureWriter.h \
         runner/LabFeatureWriter.h \
         runner/MIDIFeatureWriter.h \
         runner/MultiplexedReader.h
@@ -94,6 +95,7 @@
 	runner/FeatureExtractionManager.cpp \
         runner/AudioDBFeatureWriter.cpp \
         runner/FeatureWriterFactory.cpp \
+        runner/JAMSFeatureWriter.cpp \
         runner/LabFeatureWriter.cpp \
         runner/MIDIFeatureWriter.cpp \
         runner/MultiplexedReader.cpp
--- a/runner/FeatureExtractionManager.cpp	Wed Oct 15 13:06:27 2014 +0100
+++ b/runner/FeatureExtractionManager.cpp	Wed Oct 15 16:09:47 2014 +0100
@@ -728,7 +728,7 @@
                 FeatureWriter::TrackMetadata m;
                 m.title = reader->getTitle();
                 m.maker = reader->getMaker();
-                if (m.title != "" && m.maker != "") {
+                if (m.title != "" || m.maker != "") {
                     writers[j]->setTrackMetadata(audioSource, m);
                 }
             }
--- a/runner/FeatureWriterFactory.cpp	Wed Oct 15 13:06:27 2014 +0100
+++ b/runner/FeatureWriterFactory.cpp	Wed Oct 15 16:09:47 2014 +0100
@@ -23,6 +23,7 @@
 
 #include "AudioDBFeatureWriter.h"
 #include "MIDIFeatureWriter.h"
+#include "JAMSFeatureWriter.h"
 #include "LabFeatureWriter.h"
 
 set<string>
@@ -35,6 +36,7 @@
     tags.insert("csv");
     tags.insert("lab");
     tags.insert("midi");
+    tags.insert("json");
     return tags;
 }
 
@@ -53,6 +55,8 @@
         return new LabFeatureWriter();
     } else if (tag == "midi") {
         return new MIDIFeatureWriter();
+    } else if (tag == "json") {
+        return new JAMSFeatureWriter();
     }
 
     return 0;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runner/JAMSFeatureWriter.cpp	Wed Oct 15 16:09:47 2014 +0100
@@ -0,0 +1,468 @@
+/* -*- 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-2014 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 "JAMSFeatureWriter.h"
+
+using namespace std;
+using Vamp::Plugin;
+using Vamp::PluginBase;
+
+#include "base/Exceptions.h"
+#include "rdf/PluginRDFIndexer.h"
+
+#include <QFileInfo>
+
+#include "version.h"
+
+JAMSFeatureWriter::JAMSFeatureWriter() :
+    FileFeatureWriter(SupportOneFilePerTrackTransform |
+                      SupportOneFilePerTrack |
+                      SupportOneFileTotal |
+		      SupportStdOut,
+                      "json"),
+    m_network(false),
+    m_networkRetrieved(false),
+    m_n(1),
+    m_m(1)
+{
+}
+
+JAMSFeatureWriter::~JAMSFeatureWriter()
+{
+}
+
+string
+JAMSFeatureWriter::getDescription() const
+{
+    return "Write features to JSON files in JAMS (JSON Annotated Music Specification) format. WARNING: This is a provisional implementation! The output format may change in future releases to comply more effectively with the specification. Please report any problems you find with the current implementation.";
+}
+
+JAMSFeatureWriter::ParameterList
+JAMSFeatureWriter::getSupportedParameters() const
+{
+    ParameterList pl = FileFeatureWriter::getSupportedParameters();
+    Parameter p;
+
+    p.name = "network";
+    p.description = "Attempt to retrieve RDF descriptions of plugins from network, if not available locally";
+    p.hasArg = false;
+    pl.push_back(p);
+
+    return pl;
+}
+
+void
+JAMSFeatureWriter::setParameters(map<string, string> &params)
+{
+    FileFeatureWriter::setParameters(params);
+
+    for (map<string, string>::iterator i = params.begin();
+         i != params.end(); ++i) {
+        if (i->first == "network") {
+            m_network = true;
+        }
+    }
+}
+
+void
+JAMSFeatureWriter::setTrackMetadata(QString trackId, TrackMetadata metadata)
+{
+    m_trackMetadata[trackId] = metadata;
+}
+
+static double
+realTime2Sec(const Vamp::RealTime &r)
+{
+    return r / Vamp::RealTime(1, 0);
+}
+
+void
+JAMSFeatureWriter::write(QString trackId,
+			 const Transform &transform,
+			 const Plugin::OutputDescriptor& ,
+			 const Plugin::FeatureList& features,
+			 std::string /* summaryType */)
+{
+    QString transformId = transform.getIdentifier();
+
+    QTextStream *sptr = getOutputStream(trackId, transformId);
+    if (!sptr) {
+        throw FailedToOpenOutputStream(trackId, transformId);
+    }
+
+    DataId did(trackId, transform);
+
+    if (m_data.find(did) == m_data.end()) {
+	identifyTask(transform);
+        m_streamTracks[sptr].insert(trackId);
+        m_streamTasks[sptr].insert(m_tasks[transformId]);
+        m_streamData[sptr].insert(did);
+    }
+
+    QString d = m_data[did];
+
+    for (int i = 0; i < int(features.size()); ++i) {
+
+        if (d != "") {
+            d += ",\n";
+        }
+        
+        d += "    { ";
+	
+        Plugin::Feature f(features[i]);
+
+        if (f.hasDuration) {
+            d += QString
+                ("\"start\": { \"value\": %1 }, "
+                 "\"end\": { \"value\": %2 }")
+                .arg(realTime2Sec(f.timestamp))
+                .arg(realTime2Sec
+                     (f.timestamp +
+                      (f.hasDuration ? f.duration : Vamp::RealTime::zeroTime)));
+        } else {
+            d += QString("\"time\": { \"value\": %1 }")
+                .arg(realTime2Sec(f.timestamp));
+        }
+        
+        if (f.label != "") {
+            d += QString(", \"label\": { \"value\": \"%2\" }")
+                .arg(f.label.c_str());
+        }
+
+        if (f.values.size() > 0) {
+            d += QString(", \"value\": [ ");
+            for (int j = 0; j < int(f.values.size()); ++j) {
+                if (isnan(f.values[j])) {
+                    d += "\"NaN\" ";
+                } else if (isinf(f.values[j])) {
+                    d += "\"Inf\" ";
+                } else {
+                    d += QString("%1 ").arg(f.values[j]);
+                }
+            }
+            d += "]";
+        }
+            
+        d += " }";
+    }	
+
+    m_data[did] = d;
+}
+
+void
+JAMSFeatureWriter::setNofM(int n, int m)
+{
+    if (m_singleFileName != "" || m_stdout) {
+        m_n = n;
+        m_m = m;
+    } else {
+        m_n = 1;
+        m_m = 1;
+    }
+}
+
+void
+JAMSFeatureWriter::finish()
+{
+    for (FileStreamMap::const_iterator stri = m_streams.begin();
+	 stri != m_streams.end(); ++stri) {
+
+        QTextStream *sptr = stri->second;
+        QTextStream &stream = *sptr;
+
+        bool firstInStream = true;
+
+        for (TrackIds::const_iterator tri = m_streamTracks[sptr].begin();
+             tri != m_streamTracks[sptr].end(); ++tri) {
+
+            TrackId trackId = *tri;
+
+            if (firstInStream) {
+                if (m_streamTracks[sptr].size() > 1 || (m_m > 1 && m_n == 1)) {
+                    stream << "[\n";
+                }
+            }
+
+            if (!firstInStream || (m_m > 1 && m_n > 1)) {
+                stream << ",\n";
+            }
+
+            stream << "{\n"
+                   << QString("\"file_metadata\": {\n"
+                              "  \"filename\": \"%1\"")
+                .arg(QFileInfo(trackId).fileName());
+
+            if (m_trackMetadata.find(trackId) != m_trackMetadata.end()) {
+                if (m_trackMetadata[trackId].maker != "") {
+                    stream << QString(",\n  \"artist\": \"%1\"")
+                        .arg(m_trackMetadata[trackId].maker);
+                }
+                if (m_trackMetadata[trackId].title != "") {
+                    stream << QString(",\n  \"title\": \"%1\"")
+                        .arg(m_trackMetadata[trackId].title);
+                }
+            }
+
+            stream << "\n},\n";
+
+            bool firstInTrack = true;
+
+            for (Tasks::const_iterator ti = m_streamTasks[sptr].begin();
+                 ti != m_streamTasks[sptr].end(); ++ti) {
+                
+                Task task = *ti;
+
+                if (!firstInTrack) {
+                    stream << ",\n";
+                }
+
+                stream << "\"" << getTaskKey(task) << "\": [\n";
+                
+                bool firstInTask = true;
+
+                for (DataIds::const_iterator di = m_streamData[sptr].begin();
+                     di != m_streamData[sptr].end(); ++di) {
+                    
+                    DataId did = *di;
+
+                    QString trackId = did.first;
+                    Transform transform = did.second;
+
+                    if (m_tasks[transform.getIdentifier()] != task) continue;
+
+                    QString data = m_data[did];
+
+                    if (!firstInTask) {
+                        stream << ",\n";
+                    }
+
+                    stream << QString
+                        ("{ \n"
+                         "  \"annotation_metadata\": {\n"
+                         "    \"annotation_tools\": \"Sonic Annotator v%2\",\n"
+                         "    \"data_source\": \"Automatic feature extraction\",\n"
+                         "    \"annotator\": {\n"
+                         "%3"
+                         "    }\n"
+                         "  },\n"
+                         "  \"data\": [\n")
+                        .arg(RUNNER_VERSION)
+                        .arg(writeTransformToObjectContents(transform));
+
+                    stream << data;
+
+                    stream << "\n  ]\n}";
+                    firstInTask = false;
+                }
+
+                stream << "\n]";
+                firstInTrack = false;
+            }
+
+            stream << "\n}";
+            firstInStream = false;
+        }
+
+        if (!firstInStream) {
+            if (m_streamTracks[sptr].size() > 1 || (m_m > 1 && m_n == m_m)) {
+                stream << "\n]";
+            }
+            stream << "\n";
+        }
+    }
+        
+    m_streamTracks.clear();
+    m_streamTasks.clear();
+    m_streamData.clear();
+    m_data.clear();
+
+    FileFeatureWriter::finish();
+}
+
+void
+JAMSFeatureWriter::loadRDFDescription(const Transform &transform)
+{
+    QString pluginId = transform.getPluginIdentifier();
+    if (m_rdfDescriptions.find(pluginId) != m_rdfDescriptions.end()) return;
+
+    if (m_network && !m_networkRetrieved) {
+	PluginRDFIndexer::getInstance()->indexConfiguredURLs();
+	m_networkRetrieved = true;
+    }
+
+    m_rdfDescriptions[pluginId] = PluginRDFDescription(pluginId);
+    
+    if (m_rdfDescriptions[pluginId].haveDescription()) {
+	cerr << "NOTE: Have RDF description for plugin ID \""
+	     << pluginId << "\"" << endl;
+    } else {
+	cerr << "NOTE: No RDF description for plugin ID \""
+	     << pluginId << "\"" << endl;
+	if (!m_network) {
+	    cerr << "      Consider using the --json-network option to retrieve plugin descriptions"  << endl;
+	    cerr << "      from the network where possible." << endl;
+	}
+    }
+}
+
+void
+JAMSFeatureWriter::identifyTask(const Transform &transform)
+{
+    QString transformId = transform.getIdentifier();
+    if (m_tasks.find(transformId) != m_tasks.end()) return;
+
+    loadRDFDescription(transform);
+    
+    Task task = UnknownTask;
+
+    QString pluginId = transform.getPluginIdentifier();
+    QString outputId = transform.getOutput();
+
+    const PluginRDFDescription &desc = m_rdfDescriptions[pluginId];
+    
+    if (desc.haveDescription()) {
+
+	PluginRDFDescription::OutputDisposition disp = 
+	    desc.getOutputDisposition(outputId);
+
+	QString af = "http://purl.org/ontology/af/";
+	    
+	if (disp == PluginRDFDescription::OutputSparse) {
+
+	    QString eventUri = desc.getOutputEventTypeURI(outputId);
+
+	    //!!! todo: allow user to prod writer for task type
+
+	    if (eventUri == af + "Note") {
+		task = NoteTask;
+	    } else if (eventUri == af + "Beat") {
+		task = BeatTask;
+	    } else if (eventUri == af + "ChordSegment") {
+		task = ChordTask;
+	    } else if (eventUri == af + "KeyChange") {
+		task = KeyTask;
+	    } else if (eventUri == af + "KeySegment") {
+		task = KeyTask;
+	    } else if (eventUri == af + "Onset") {
+		task = OnsetTask;
+	    } else if (eventUri == af + "NonTonalOnset") {
+		task = OnsetTask;
+	    } else if (eventUri == af + "Segment") {
+		task = SegmentTask;
+	    } else if (eventUri == af + "SpeechSegment") {
+		task = SegmentTask;
+	    } else if (eventUri == af + "StructuralSegment") {
+		task = SegmentTask;
+	    } else {
+		cerr << "WARNING: Unsupported event type URI <" 
+		     << eventUri << ">, proceeding with UnknownTask type"
+		     << endl;
+	    }
+
+	} else {
+
+	    cerr << "WARNING: Cannot currently write dense or track-level outputs to JSON format (only sparse ones). Will proceed using UnknownTask type, but this probably isn't going to work" << endl;
+	}
+    }	    
+
+    m_tasks[transformId] = task;
+}
+
+QString
+JAMSFeatureWriter::getTaskKey(Task task) 
+{
+    switch (task) {
+    case UnknownTask: return "unknown";
+    case BeatTask: return "beat";
+    case OnsetTask: return "onset";
+    case ChordTask: return "chord";
+    case SegmentTask: return "segment";
+    case KeyTask: return "key";
+    case NoteTask: return "note";
+    case MelodyTask: return "melody";
+    case PitchTask: return "pitch";
+    }
+    return "unknown";
+}
+
+QString
+JAMSFeatureWriter::writeTransformToObjectContents(const Transform &t)
+{
+    QString json;
+    QString stpl("      \"%1\": \"%2\",\n");
+    QString ntpl("      \"%1\": %2,\n");
+
+    json += stpl.arg("plugin_id").arg(t.getPluginIdentifier());
+    json += stpl.arg("output_id").arg(t.getOutput());
+
+    if (t.getSummaryType() != Transform::NoSummary) {
+        json += stpl.arg("summary_type")
+            .arg(Transform::summaryTypeToString(t.getSummaryType()));
+    }
+
+    if (t.getPluginVersion() != QString()) {
+        json += stpl.arg("plugin_version").arg(t.getPluginVersion());
+    }
+
+    if (t.getProgram() != QString()) {
+        json += stpl.arg("program").arg(t.getProgram());
+    }
+
+    if (t.getStepSize() != 0) {
+        json += ntpl.arg("step_size").arg(t.getStepSize());
+    }
+
+    if (t.getBlockSize() != 0) {
+        json += ntpl.arg("block_size").arg(t.getBlockSize());
+    }
+
+    if (t.getWindowType() != HanningWindow) {
+        json += stpl.arg("window_type")
+            .arg(Window<float>::getNameForType(t.getWindowType()).c_str());
+    }
+
+    if (t.getStartTime() != RealTime::zeroTime) {
+        json += ntpl.arg("start").arg(t.getStartTime().toDouble());
+    }
+
+    if (t.getDuration() != RealTime::zeroTime) {
+        json += ntpl.arg("duration").arg(t.getDuration().toDouble());
+    }
+
+    if (t.getSampleRate() != 0) {
+        json += ntpl.arg("sample_rate").arg(t.getSampleRate());
+    }
+
+    if (!t.getParameters().empty()) {
+        json += QString("      \"parameters\": {\n");
+        Transform::ParameterMap parameters = t.getParameters();
+        for (Transform::ParameterMap::const_iterator i = parameters.begin();
+             i != parameters.end(); ++i) {
+            if (i != parameters.begin()) {
+                json += ",\n";
+            }
+            QString name = i->first;
+            float value = i->second;
+            json += QString("        \"%1\": %2").arg(name).arg(value);
+        }
+        json += QString("\n      },\n");
+    }
+
+    // no trailing comma on final property:
+    json += QString("      \"transform_id\": \"%1\"\n").arg(t.getIdentifier());
+
+    return json;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runner/JAMSFeatureWriter.h	Wed Oct 15 16:09:47 2014 +0100
@@ -0,0 +1,105 @@
+/* -*- 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-2014 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 _JAMS_FEATURE_WRITER_H_
+#define _JAMS_FEATURE_WRITER_H_
+
+#include "transform/FileFeatureWriter.h"
+
+#include "rdf/PluginRDFDescription.h"
+
+class JAMSFileWriter;
+
+class JAMSFeatureWriter : public FileFeatureWriter
+{
+public:
+    JAMSFeatureWriter();
+    virtual ~JAMSFeatureWriter();
+
+    string getDescription() const;
+
+    virtual ParameterList getSupportedParameters() const;
+    virtual void setParameters(map<string, string> &params);
+
+    virtual void setTrackMetadata(QString trackid, TrackMetadata metadata);
+
+    virtual void setNofM(int, int);
+    
+    virtual void write(QString trackid,
+                       const Transform &transform,
+                       const Vamp::Plugin::OutputDescriptor &output,
+                       const Vamp::Plugin::FeatureList &features,
+                       std::string summaryType = "");
+
+    virtual void finish();
+
+    virtual QString getWriterTag() const { return "json"; }
+
+private:
+    enum Task {
+	UnknownTask,
+	BeatTask,
+	OnsetTask,
+	ChordTask,
+	SegmentTask,
+	KeyTask,
+	NoteTask,
+	MelodyTask,
+	PitchTask,
+    };
+
+    typedef map<QString, PluginRDFDescription> RDFDescriptionMap; // by plugin id
+    RDFDescriptionMap m_rdfDescriptions;
+
+    typedef QString TrackId;
+    typedef pair<TrackId, Transform> DataId;
+
+    typedef map<TrackId, TrackMetadata> TrackMetadataMap;
+    TrackMetadataMap m_trackMetadata;
+
+    typedef set<TrackId> TrackIds;
+    typedef map<QTextStream *, TrackIds> StreamTrackMap;
+    StreamTrackMap m_streamTracks;
+
+    typedef set<Task> Tasks;
+    typedef map<QTextStream *, Tasks> StreamTaskMap;
+    StreamTaskMap m_streamTasks;
+
+    typedef set<DataId> DataIds;
+    typedef map<QTextStream *, DataIds> StreamDataMap;
+    StreamDataMap m_streamData;
+
+    typedef map<DataId, QString> DataMap;
+    DataMap m_data;
+
+    typedef map<TransformId, Task> TaskMap;
+    TaskMap m_tasks;
+
+    void loadRDFDescription(const Transform &);
+    void identifyTask(const Transform &);
+
+    QString getTaskKey(Task);
+
+    QString writeTransformToObjectContents(const Transform &);
+
+    bool m_network;
+    bool m_networkRetrieved;
+    int m_n;
+    int m_m;
+};
+
+#endif
+
--- a/runner/MIDIFeatureWriter.cpp	Wed Oct 15 13:06:27 2014 +0100
+++ b/runner/MIDIFeatureWriter.cpp	Wed Oct 15 16:09:47 2014 +0100
@@ -165,5 +165,9 @@
             }
 	}
     }
+
+    m_notes.clear();
+
+    FileFeatureWriter::finish();
 }
 
--- a/runner/main.cpp	Wed Oct 15 13:06:27 2014 +0100
+++ b/runner/main.cpp	Wed Oct 15 16:09:47 2014 +0100
@@ -939,17 +939,25 @@
         }
         if (multiplex) {
             try {
+                for (int i = 0; i < (int)writers.size(); ++i) {
+                    writers[i]->setNofM(1, 1);
+                }
                 manager.extractFeaturesMultiplexed(goodSources);
             } catch (const std::exception &e) {
                 cerr << "ERROR: Feature extraction failed: "
                      << e.what() << endl;
             }
         } else {
+            int n = 0;
             for (QStringList::const_iterator i = goodSources.begin();
                  i != goodSources.end(); ++i) {
                 std::cerr << "Extracting features for: \"" << i->toStdString()
                           << "\"" << std::endl;
+                ++n;
                 try {
+                    for (int j = 0; j < (int)writers.size(); ++j) {
+                        writers[j]->setNofM(n, goodSources.size());
+                    }
                     manager.extractFeatures(*i);
                 } catch (const std::exception &e) {
                     cerr << "ERROR: Feature extraction failed for \""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-json-destinations/test-json-destinations.sh	Wed Oct 15 16:09:47 2014 +0100
@@ -0,0 +1,192 @@
+#!/bin/bash
+
+. ../include.sh
+
+infile1=$audiopath/3clicks8.wav
+infile2=$audiopath/6clicks8.wav
+
+infile1dot=$audiopath/3.clicks.8.wav
+
+outfile1=3clicks8.json
+outfile2=6clicks8.json
+
+outfile3=3clicks8_vamp_vamp-example-plugins_percussiononsets_onsets.json
+outfile4=3clicks8_vamp_vamp-example-plugins_percussiononsets_detectionfunction.json
+outfile5=6clicks8_vamp_vamp-example-plugins_percussiononsets_onsets.json
+outfile6=6clicks8_vamp_vamp-example-plugins_percussiononsets_detectionfunction.json
+
+outfile1dot=3.clicks.8.json
+
+tmpjson=$mypath/tmp_1_$$.json
+
+trap "rm -f $tmpjson $outfile1 $outfile2 $outfile3 $outfile4 $outfile5 $outfile6 $infile1dot $outfile1dot $audiopath/$outfile1 $audiopath/$outfile2 $audiopath/$outfile3 $audiopath/$outfile4 $audiopath/$outfile5 $audiopath/$outfile6 $audiopath/$outfile1dot" 0
+
+transformdir=$mypath/transforms
+
+failshow() {
+    echo "Test failed: $1"
+    if [ -n "$2" ]; then
+	echo "Output follows:"
+	echo "--"
+	cat $2
+	echo "--"
+    fi
+    exit 1
+}	
+
+check_json() {
+    test -f $1 || \
+	fail "Fails to write output to expected location $1 for $2"
+    cat $1 | json_verify -q || \
+	failshow "Writes invalid JSON to location $1 for $2" $1
+    rm -f $1
+}    
+
+
+ctx="onsets transform, one audio file, default JSON writer destination"
+
+rm -f $audiopath/$outfile1
+
+$r -t $transformdir/onsets.n3 -w json $infile1 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile1 "$ctx"
+
+
+ctx="onsets transform, one audio file with dots in filename, default JSON writer destination"
+
+rm -f $audiopath/$outfile1
+
+cp $infile1 $infile1dot
+
+$r -t $transformdir/onsets.n3 -w json $infile1dot 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile1dot "$ctx"
+
+rm -f $infile1dot $audiopath/$outfile1dot
+
+
+ctx="onsets and df transforms, one audio file, default JSON writer destination"
+
+rm -f $audiopath/$outfile1
+
+$r -t $transformdir/onsets.n3 -t $transformdir/detectionfunction.n3 -w json $infile1 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile1 "$ctx"
+
+
+ctx="onsets transform, two audio files, default JSON writer destination"
+
+rm -f $audiopath/$outfile1
+rm -f $audiopath/$outfile2
+
+$r -t $transformdir/onsets.n3 -w json $infile1 $infile2 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile1 "$ctx"
+check_json $audiopath/$outfile2 "$ctx"
+
+
+ctx="onsets transform, two audio files, one-file JSON writer"
+
+$r -t $transformdir/onsets.n3 -w json --json-one-file $tmpjson $infile1 $infile2 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $tmpjson "$ctx"
+
+
+ctx="onsets transform, two audio files, stdout JSON writer"
+
+$r -t $transformdir/onsets.n3 -w json --json-stdout $infile1 $infile2 2>/dev/null >$tmpjson || \
+    fail "Fails to run with $ctx"
+
+check_json $tmpjson "$ctx"
+
+
+ctx="onsets transform, one audio file, many-files JSON writer"
+
+rm -f $audiopath/$outfile3
+
+$r -t $transformdir/onsets.n3 -w json --json-many-files $infile1 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile3 "$ctx"
+
+
+ctx="onsets transform, two audio files, many-files JSON writer"
+
+rm -f $audiopath/$outfile3
+rm -f $audiopath/$outfile5
+
+$r -t $transformdir/onsets.n3 -w json --json-many-files $infile1 $infile2 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile3 "$ctx"
+check_json $audiopath/$outfile5 "$ctx"
+
+
+ctx="onsets and df transforms, two audio files, many-files JSON writer"
+
+rm -f $audiopath/$outfile3
+rm -f $audiopath/$outfile4
+rm -f $audiopath/$outfile5
+rm -f $audiopath/$outfile6
+
+$r -t $transformdir/onsets.n3 -t $transformdir/detectionfunction.n3 -w json --json-many-files $infile1 $infile2 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile3 "$ctx"
+check_json $audiopath/$outfile4 "$ctx"
+check_json $audiopath/$outfile5 "$ctx"
+check_json $audiopath/$outfile6 "$ctx"
+
+
+ctx="output base directory"
+
+rm -f ./$outfile1
+
+$r -t $transformdir/onsets.n3 -t $transformdir/detectionfunction.n3 -w json --json-basedir . $infile1 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json ./$outfile1 "$ctx"
+
+
+ctx="output base directory and many-files"
+
+rm -f ./$outfile3
+rm -f ./$outfile5
+
+$r -t $transformdir/onsets.n3 -w json --json-basedir . --json-many-files $infile1 $infile2 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json ./$outfile3 "$ctx"
+check_json ./$outfile5 "$ctx"
+
+
+ctx="nonexistent output base directory"
+
+$r -t $transformdir/onsets.n3 -w json --json-basedir ./DOES_NOT_EXIST $infile1 2>/dev/null && \
+    fail "Fails with $ctx by completing successfully (should refuse and bail out)"
+
+
+ctx="existing output file and no --json-force"
+
+touch $audiopath/$outfile1
+
+$r -t $transformdir/onsets.n3 -w json $infile1 2>/dev/null && \
+    fail "Fails by completing successfully when output file already exists (should refuse and bail out)"
+
+
+ctx="existing output file and --json-force"
+
+touch $audiopath/$outfile1
+
+$r -t $transformdir/onsets.n3 -w json --json-force $infile1 2>/dev/null || \
+    fail "Fails to run with $ctx"
+
+check_json $audiopath/$outfile1 "$ctx"
+
+
+exit 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-json-destinations/transforms/detectionfunction.n3	Wed Oct 15 16:09:47 2014 +0100
@@ -0,0 +1,11 @@
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
+@prefix vamp: <http://purl.org/ontology/vamp/>.
+@prefix examples: <http://vamp-plugins.org/rdf/plugins/vamp-example-plugins#>.
+@prefix : <#>.
+
+:transform0 a vamp:Transform;
+	vamp:plugin examples:percussiononsets ;
+	vamp:output examples:percussiononsets_output_detectionfunction .
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-json-destinations/transforms/onsets.n3	Wed Oct 15 16:09:47 2014 +0100
@@ -0,0 +1,10 @@
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
+@prefix vamp: <http://purl.org/ontology/vamp/>.
+@prefix examples: <http://vamp-plugins.org/rdf/plugins/vamp-example-plugins#>.
+@prefix : <#>.
+
+:transform0 a vamp:Transform;
+	vamp:plugin examples:percussiononsets.
+
+
+
--- a/tests/test-supportprogs/test-supportprogs.sh	Wed Oct 15 13:06:27 2014 +0100
+++ b/tests/test-supportprogs/test-supportprogs.sh	Wed Oct 15 16:09:47 2014 +0100
@@ -11,5 +11,8 @@
 rapper --version >/dev/null || \
     fail "Can't find required rapper program"
 
+echo '{}' | json_verify >/dev/null || \
+    fail "Can't find required json_verify program, or it doesn't seem to work"
+
 exit 0
 
--- a/tests/test.sh	Wed Oct 15 13:06:27 2014 +0100
+++ b/tests/test.sh	Wed Oct 15 16:09:47 2014 +0100
@@ -16,6 +16,7 @@
     lab-writer \
     lab-destinations \
     midi-destinations \
+    json-destinations \
     summaries \
     multiple-audio \
     ; do