# HG changeset patch # User Chris Cannam # Date 1413369194 -3600 # Node ID 4b19d824a2137d925404b9e8977e44ccee599703 # Parent ad96fd5f9cd76ee1e3f0b3959247bb0b16a0ad8e# Parent 2a399bed0184502c2429672ca635ab0c249ec38e Merge from default branch diff -r 2a399bed0184 -r 4b19d824a213 runner.pro --- a/runner.pro Wed Oct 15 11:23:05 2014 +0100 +++ b/runner.pro Wed Oct 15 11:33:14 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 diff -r 2a399bed0184 -r 4b19d824a213 runner/FeatureWriterFactory.cpp --- a/runner/FeatureWriterFactory.cpp Wed Oct 15 11:23:05 2014 +0100 +++ b/runner/FeatureWriterFactory.cpp Wed Oct 15 11:33:14 2014 +0100 @@ -23,6 +23,7 @@ #include "AudioDBFeatureWriter.h" #include "MIDIFeatureWriter.h" +#include "JAMSFeatureWriter.h" #include "LabFeatureWriter.h" set @@ -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; diff -r 2a399bed0184 -r 4b19d824a213 runner/JAMSFeatureWriter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runner/JAMSFeatureWriter.cpp Wed Oct 15 11:33:14 2014 +0100 @@ -0,0 +1,327 @@ +/* -*- 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" + +JAMSFeatureWriter::JAMSFeatureWriter() : + FileFeatureWriter(SupportOneFilePerTrackTransform | + SupportOneFilePerTrack | + SupportOneFileTotal | + SupportStdOut, + "json"), + m_network(false), + m_networkRetrieved(false) +{ +} + +JAMSFeatureWriter::~JAMSFeatureWriter() +{ +} + +string +JAMSFeatureWriter::getDescription() const +{ + return "Write features to JSON files in JAMS (JSON Annotated Music Specification) format."; +} + +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 ¶ms) +{ + FileFeatureWriter::setParameters(params); + + for (map::iterator i = params.begin(); + i != params.end(); ++i) { + if (i->first == "network") { + m_network = true; + } + } +} + +void +JAMSFeatureWriter::setTrackMetadata(QString trackId, TrackMetadata metadata) +{ + QString json + ("\n\"file_metadata\":\n" + " { \"artist\": \"%1\",\n" + " \"title\": \"%2\" },\n"); + m_metadata[trackId] = json.arg(metadata.maker).arg(metadata.title); + cerr << "setTrackMetadata: metadata is: " << m_metadata[trackId] << endl; +} + +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); + } + + QTextStream &stream = *sptr; + + TrackTransformPair tt(trackId, transformId); + TrackTransformPair targetKey = getFilenameKey(trackId, transformId); + + if (m_startedTargets.find(targetKey) == m_startedTargets.end()) { + // Need to write track-level preamble + stream << "{" << m_metadata[trackId] << endl; + m_startedTargets.insert(targetKey); + } + + bool justBegun = false; + + if (m_data.find(tt) == m_data.end()) { + + identifyTask(transform); + + QString json("\"%1\": [ "); + m_data[tt] = json.arg(getTaskKey(m_tasks[transformId])); + justBegun = true; + } + + QString d = m_data[tt]; + + for (int i = 0; i < int(features.size()); ++i) { + + if (i > 0 || !justBegun) { + d += ",\n"; + } else { + d += "\n"; + } + + d += " { "; + + Plugin::Feature f(features[i]); + + switch (m_tasks[transformId]) { + + case ChordTask: + case SegmentTask: + case NoteTask: + case UnknownTask: + 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))); + break; + } else { + // don't break; fall through to simpler no-duration case + } + + case BeatTask: + case KeyTask: + case OnsetTask: + d += QString("\"time\": { \"value\": %1 }") + .arg(realTime2Sec(f.timestamp)); + break; + + case MelodyTask: + case PitchTask: + //!!! + break; + } + + if (f.label != "") { + d += QString(", \"label\": { \"value\": \"%2\" }") + .arg(f.label.c_str()); + } else if (f.values.size() > 0) { + d += QString(", \"label\": { \"value\": \"%2\" }") + .arg(f.values[0]); + } + + d += " }"; + } + + m_data[tt] = d; +} + +void +JAMSFeatureWriter::finish() +{ + cerr << "Finish called on " << this << endl; + + set startedStreams; + + for (DataMap::const_iterator i = m_data.begin(); + i != m_data.end(); ++i) { + + TrackTransformPair tt = i->first; + QString data = i->second; + + QTextStream *sptr = getOutputStream(tt.first, tt.second); + if (!sptr) { + throw FailedToOpenOutputStream(tt.first, tt.second); + } + + if (startedStreams.find(sptr) != startedStreams.end()) { + *sptr << "," << endl; + } + startedStreams.insert(sptr); + + *sptr << data << "\n ]"; + } + + for (FileStreamMap::const_iterator i = m_streams.begin(); + i != m_streams.end(); ++i) { + *(i->second) << endl << "}" << endl; + } + + m_data.clear(); + m_startedTargets.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"; +} diff -r 2a399bed0184 -r 4b19d824a213 runner/JAMSFeatureWriter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runner/JAMSFeatureWriter.h Wed Oct 15 11:33:14 2014 +0100 @@ -0,0 +1,87 @@ +/* -*- 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 ¶ms); + + virtual void setTrackMetadata(QString trackid, TrackMetadata metadata); + + 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 RDFDescriptionMap; // by plugin id + RDFDescriptionMap m_rdfDescriptions; + + typedef map TrackMetadataMap; // track id -> json object + TrackMetadataMap m_metadata; + + typedef map DataMap; + DataMap m_data; + + typedef map TaskMap; // by transform id + TaskMap m_tasks; + + typedef set StartedSet; + StartedSet m_startedTargets; + + void loadRDFDescription(const Transform &); + void identifyTask(const Transform &); + + QString getTaskKey(Task); + + bool m_network; + bool m_networkRetrieved; +}; + +#endif + diff -r 2a399bed0184 -r 4b19d824a213 runner/MIDIFeatureWriter.cpp --- a/runner/MIDIFeatureWriter.cpp Wed Oct 15 11:23:05 2014 +0100 +++ b/runner/MIDIFeatureWriter.cpp Wed Oct 15 11:33:14 2014 +0100 @@ -165,5 +165,9 @@ } } } + + m_notes.clear(); + + FileFeatureWriter::finish(); } diff -r 2a399bed0184 -r 4b19d824a213 tests/test-json-destinations/test-json-destinations.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-json-destinations/test-json-destinations.sh Wed Oct 15 11:33:14 2014 +0100 @@ -0,0 +1,181 @@ +#!/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 + +check_json() { + test -f $1 || \ + fail "Fails to write output to expected location $1 for $2" + cat $1 | json_verify || \ + fail "Writes invalid JSON to location $1 for $2" + 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 diff -r 2a399bed0184 -r 4b19d824a213 tests/test-json-destinations/transforms/detectionfunction.n3 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-json-destinations/transforms/detectionfunction.n3 Wed Oct 15 11:33:14 2014 +0100 @@ -0,0 +1,11 @@ +@prefix rdf: . +@prefix vamp: . +@prefix examples: . +@prefix : <#>. + +:transform0 a vamp:Transform; + vamp:plugin examples:percussiononsets ; + vamp:output examples:percussiononsets_output_detectionfunction . + + + diff -r 2a399bed0184 -r 4b19d824a213 tests/test-json-destinations/transforms/onsets.n3 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-json-destinations/transforms/onsets.n3 Wed Oct 15 11:33:14 2014 +0100 @@ -0,0 +1,10 @@ +@prefix rdf: . +@prefix vamp: . +@prefix examples: . +@prefix : <#>. + +:transform0 a vamp:Transform; + vamp:plugin examples:percussiononsets. + + + diff -r 2a399bed0184 -r 4b19d824a213 tests/test-supportprogs/test-supportprogs.sh --- a/tests/test-supportprogs/test-supportprogs.sh Wed Oct 15 11:23:05 2014 +0100 +++ b/tests/test-supportprogs/test-supportprogs.sh Wed Oct 15 11:33:14 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 diff -r 2a399bed0184 -r 4b19d824a213 tests/test.sh --- a/tests/test.sh Wed Oct 15 11:23:05 2014 +0100 +++ b/tests/test.sh Wed Oct 15 11:33:14 2014 +0100 @@ -16,6 +16,7 @@ lab-writer \ lab-destinations \ midi-destinations \ + json-destinations \ summaries \ multiple-audio \ ; do