Chris@145: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@145: Chris@145: /* Chris@145: Sonic Annotator Chris@145: A utility for batch feature extraction from audio files. Chris@145: Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London. Chris@145: Copyright 2007-2014 QMUL. Chris@145: Chris@145: This program is free software; you can redistribute it and/or Chris@145: modify it under the terms of the GNU General Public License as Chris@145: published by the Free Software Foundation; either version 2 of the Chris@145: License, or (at your option) any later version. See the file Chris@145: COPYING included with this distribution for more information. Chris@145: */ Chris@145: Chris@145: #include "JAMSFeatureWriter.h" Chris@145: Chris@145: using namespace std; Chris@145: using Vamp::Plugin; Chris@145: using Vamp::PluginBase; Chris@145: Chris@145: #include "base/Exceptions.h" Chris@145: #include "rdf/PluginRDFIndexer.h" Chris@145: Chris@166: #include Chris@166: Chris@162: #include "version.h" Chris@162: Chris@145: JAMSFeatureWriter::JAMSFeatureWriter() : Chris@145: FileFeatureWriter(SupportOneFilePerTrackTransform | Chris@145: SupportOneFilePerTrack | Chris@152: SupportOneFileTotal | Chris@145: SupportStdOut, Chris@145: "json"), Chris@145: m_network(false), Chris@145: m_networkRetrieved(false) Chris@145: { Chris@145: } Chris@145: Chris@145: JAMSFeatureWriter::~JAMSFeatureWriter() Chris@145: { Chris@145: } Chris@145: Chris@145: string Chris@145: JAMSFeatureWriter::getDescription() const Chris@145: { Chris@145: return "Write features to JSON files in JAMS (JSON Annotated Music Specification) format."; Chris@145: } Chris@145: Chris@145: JAMSFeatureWriter::ParameterList Chris@145: JAMSFeatureWriter::getSupportedParameters() const Chris@145: { Chris@145: ParameterList pl = FileFeatureWriter::getSupportedParameters(); Chris@145: Parameter p; Chris@145: Chris@145: p.name = "network"; Chris@145: p.description = "Attempt to retrieve RDF descriptions of plugins from network, if not available locally"; Chris@145: p.hasArg = false; Chris@145: pl.push_back(p); Chris@145: Chris@145: return pl; Chris@145: } Chris@145: Chris@145: void Chris@145: JAMSFeatureWriter::setParameters(map ¶ms) Chris@145: { Chris@145: FileFeatureWriter::setParameters(params); Chris@145: Chris@145: for (map::iterator i = params.begin(); Chris@145: i != params.end(); ++i) { Chris@145: if (i->first == "network") { Chris@145: m_network = true; Chris@145: } Chris@145: } Chris@145: } Chris@145: Chris@145: void Chris@145: JAMSFeatureWriter::setTrackMetadata(QString trackId, TrackMetadata metadata) Chris@145: { Chris@166: m_metadata[trackId] = metadata; Chris@145: } Chris@145: Chris@153: static double Chris@153: realTime2Sec(const Vamp::RealTime &r) Chris@153: { Chris@153: return r / Vamp::RealTime(1, 0); Chris@153: } Chris@153: Chris@145: void Chris@145: JAMSFeatureWriter::write(QString trackId, Chris@145: const Transform &transform, Chris@145: const Plugin::OutputDescriptor& , Chris@145: const Plugin::FeatureList& features, Chris@145: std::string /* summaryType */) Chris@145: { Chris@145: QString transformId = transform.getIdentifier(); Chris@145: Chris@145: QTextStream *sptr = getOutputStream(trackId, transformId); Chris@145: if (!sptr) { Chris@145: throw FailedToOpenOutputStream(trackId, transformId); Chris@145: } Chris@145: Chris@145: QTextStream &stream = *sptr; Chris@145: Chris@152: TrackTransformPair tt(trackId, transformId); Chris@152: TrackTransformPair targetKey = getFilenameKey(trackId, transformId); Chris@152: Chris@152: if (m_startedTargets.find(targetKey) == m_startedTargets.end()) { Chris@152: // Need to write track-level preamble Chris@166: stream << "{\n"; Chris@166: stream << QString("\"file_metadata\": {\n" Chris@166: " \"filename\": \"%1\"") Chris@166: .arg(QFileInfo(trackId).fileName()); Chris@166: Chris@166: if (m_metadata.find(trackId) != m_metadata.end()) { Chris@166: if (m_metadata[trackId].maker != "") { Chris@166: stream << QString(",\n \"artist\": \"%1\"") Chris@166: .arg(m_metadata[trackId].maker); Chris@166: } Chris@166: if (m_metadata[trackId].title != "") { Chris@166: stream << QString(",\n \"title\": \"%1\"") Chris@166: .arg(m_metadata[trackId].title); Chris@166: } Chris@166: } Chris@166: Chris@166: stream << "\n},\n"; Chris@166: Chris@152: m_startedTargets.insert(targetKey); Chris@152: } Chris@152: Chris@153: bool justBegun = false; Chris@153: Chris@152: if (m_data.find(tt) == m_data.end()) { Chris@145: Chris@145: identifyTask(transform); Chris@145: Chris@162: QString json Chris@162: ("\"%1\": [ { \n" Chris@162: " \"annotation_metadata\": {\n" Chris@162: " \"annotation_tools\": \"Sonic Annotator v%2\",\n" Chris@162: " \"data_source\": \"Automatic feature extraction\",\n" Chris@165: " \"annotator\": {\n" Chris@165: "%3" Chris@165: " },\n" Chris@162: " },\n" Chris@162: " \"data\": ["); Chris@162: m_data[tt] = json Chris@162: .arg(getTaskKey(m_tasks[transformId])) Chris@162: .arg(RUNNER_VERSION) Chris@165: .arg(writeTransformToObjectContents(transform)); Chris@153: justBegun = true; Chris@145: } Chris@145: Chris@153: QString d = m_data[tt]; Chris@153: Chris@145: for (int i = 0; i < int(features.size()); ++i) { Chris@153: Chris@153: if (i > 0 || !justBegun) { Chris@153: d += ",\n"; Chris@153: } else { Chris@153: d += "\n"; Chris@153: } Chris@153: Chris@153: d += " { "; Chris@145: Chris@153: Plugin::Feature f(features[i]); Chris@153: Chris@153: switch (m_tasks[transformId]) { Chris@153: Chris@153: case ChordTask: Chris@153: case SegmentTask: Chris@153: case NoteTask: Chris@153: case UnknownTask: Chris@153: if (f.hasDuration) { Chris@153: d += QString Chris@153: ("\"start\": { \"value\": %1 }, " Chris@153: "\"end\": { \"value\": %2 }") Chris@153: .arg(realTime2Sec(f.timestamp)) Chris@153: .arg(realTime2Sec Chris@153: (f.timestamp + Chris@153: (f.hasDuration ? f.duration : Vamp::RealTime::zeroTime))); Chris@153: break; Chris@153: } else { Chris@153: // don't break; fall through to simpler no-duration case Chris@153: } Chris@153: Chris@153: case BeatTask: Chris@153: case KeyTask: Chris@153: case OnsetTask: Chris@153: d += QString("\"time\": { \"value\": %1 }") Chris@153: .arg(realTime2Sec(f.timestamp)); Chris@153: break; Chris@161: Chris@161: case MelodyTask: Chris@161: case PitchTask: Chris@161: //!!! Chris@161: break; Chris@153: } Chris@153: Chris@153: if (f.label != "") { Chris@153: d += QString(", \"label\": { \"value\": \"%2\" }") Chris@153: .arg(f.label.c_str()); Chris@153: } else if (f.values.size() > 0) { Chris@153: d += QString(", \"label\": { \"value\": \"%2\" }") Chris@153: .arg(f.values[0]); Chris@153: } Chris@153: Chris@153: d += " }"; Chris@145: } Chris@153: Chris@153: m_data[tt] = d; Chris@145: } Chris@145: Chris@145: void Chris@152: JAMSFeatureWriter::finish() Chris@152: { Chris@152: cerr << "Finish called on " << this << endl; Chris@152: Chris@152: set startedStreams; Chris@152: Chris@152: for (DataMap::const_iterator i = m_data.begin(); Chris@152: i != m_data.end(); ++i) { Chris@152: Chris@152: TrackTransformPair tt = i->first; Chris@152: QString data = i->second; Chris@152: Chris@152: QTextStream *sptr = getOutputStream(tt.first, tt.second); Chris@152: if (!sptr) { Chris@152: throw FailedToOpenOutputStream(tt.first, tt.second); Chris@152: } Chris@152: Chris@152: if (startedStreams.find(sptr) != startedStreams.end()) { Chris@152: *sptr << "," << endl; Chris@152: } Chris@152: startedStreams.insert(sptr); Chris@152: Chris@162: *sptr << data << "\n ]\n} ]"; Chris@152: } Chris@152: Chris@152: for (FileStreamMap::const_iterator i = m_streams.begin(); Chris@152: i != m_streams.end(); ++i) { Chris@152: *(i->second) << endl << "}" << endl; Chris@152: } Chris@152: Chris@152: m_data.clear(); Chris@152: m_startedTargets.clear(); Chris@152: Chris@152: FileFeatureWriter::finish(); Chris@152: } Chris@152: Chris@152: void Chris@145: JAMSFeatureWriter::loadRDFDescription(const Transform &transform) Chris@145: { Chris@145: QString pluginId = transform.getPluginIdentifier(); Chris@145: if (m_rdfDescriptions.find(pluginId) != m_rdfDescriptions.end()) return; Chris@145: Chris@145: if (m_network && !m_networkRetrieved) { Chris@145: PluginRDFIndexer::getInstance()->indexConfiguredURLs(); Chris@145: m_networkRetrieved = true; Chris@145: } Chris@145: Chris@145: m_rdfDescriptions[pluginId] = PluginRDFDescription(pluginId); Chris@145: Chris@145: if (m_rdfDescriptions[pluginId].haveDescription()) { Chris@145: cerr << "NOTE: Have RDF description for plugin ID \"" Chris@145: << pluginId << "\"" << endl; Chris@145: } else { Chris@145: cerr << "NOTE: No RDF description for plugin ID \"" Chris@145: << pluginId << "\"" << endl; Chris@145: if (!m_network) { Chris@145: cerr << " Consider using the --json-network option to retrieve plugin descriptions" << endl; Chris@145: cerr << " from the network where possible." << endl; Chris@145: } Chris@145: } Chris@145: } Chris@145: Chris@145: void Chris@145: JAMSFeatureWriter::identifyTask(const Transform &transform) Chris@145: { Chris@145: QString transformId = transform.getIdentifier(); Chris@145: if (m_tasks.find(transformId) != m_tasks.end()) return; Chris@145: Chris@145: loadRDFDescription(transform); Chris@145: Chris@145: Task task = UnknownTask; Chris@145: Chris@145: QString pluginId = transform.getPluginIdentifier(); Chris@145: QString outputId = transform.getOutput(); Chris@145: Chris@145: const PluginRDFDescription &desc = m_rdfDescriptions[pluginId]; Chris@145: Chris@145: if (desc.haveDescription()) { Chris@145: Chris@145: PluginRDFDescription::OutputDisposition disp = Chris@145: desc.getOutputDisposition(outputId); Chris@145: Chris@145: QString af = "http://purl.org/ontology/af/"; Chris@145: Chris@145: if (disp == PluginRDFDescription::OutputSparse) { Chris@145: Chris@145: QString eventUri = desc.getOutputEventTypeURI(outputId); Chris@145: Chris@145: //!!! todo: allow user to prod writer for task type Chris@145: Chris@145: if (eventUri == af + "Note") { Chris@145: task = NoteTask; Chris@145: } else if (eventUri == af + "Beat") { Chris@145: task = BeatTask; Chris@145: } else if (eventUri == af + "ChordSegment") { Chris@145: task = ChordTask; Chris@145: } else if (eventUri == af + "KeyChange") { Chris@145: task = KeyTask; Chris@145: } else if (eventUri == af + "KeySegment") { Chris@145: task = KeyTask; Chris@145: } else if (eventUri == af + "Onset") { Chris@145: task = OnsetTask; Chris@145: } else if (eventUri == af + "NonTonalOnset") { Chris@145: task = OnsetTask; Chris@145: } else if (eventUri == af + "Segment") { Chris@145: task = SegmentTask; Chris@145: } else if (eventUri == af + "SpeechSegment") { Chris@145: task = SegmentTask; Chris@145: } else if (eventUri == af + "StructuralSegment") { Chris@145: task = SegmentTask; Chris@145: } else { Chris@145: cerr << "WARNING: Unsupported event type URI <" Chris@145: << eventUri << ">, proceeding with UnknownTask type" Chris@145: << endl; Chris@145: } Chris@145: Chris@145: } else { Chris@145: Chris@145: 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; Chris@145: } Chris@145: } Chris@145: Chris@145: m_tasks[transformId] = task; Chris@145: } Chris@145: Chris@145: QString Chris@145: JAMSFeatureWriter::getTaskKey(Task task) Chris@145: { Chris@145: switch (task) { Chris@145: case UnknownTask: return "unknown"; Chris@145: case BeatTask: return "beat"; Chris@145: case OnsetTask: return "onset"; Chris@145: case ChordTask: return "chord"; Chris@145: case SegmentTask: return "segment"; Chris@145: case KeyTask: return "key"; Chris@145: case NoteTask: return "note"; Chris@145: case MelodyTask: return "melody"; Chris@145: case PitchTask: return "pitch"; Chris@145: } Chris@145: return "unknown"; Chris@145: } Chris@165: Chris@165: QString Chris@165: JAMSFeatureWriter::writeTransformToObjectContents(const Transform &t) Chris@165: { Chris@165: QString json; Chris@165: QString stpl(" \"%1\": \"%2\",\n"); Chris@165: QString ntpl(" \"%1\": %2,\n"); Chris@165: Chris@165: json += stpl.arg("plugin_id").arg(t.getPluginIdentifier()); Chris@165: json += stpl.arg("output_id").arg(t.getOutput()); Chris@165: Chris@165: if (t.getSummaryType() != Transform::NoSummary) { Chris@165: json += stpl.arg("summary_type") Chris@165: .arg(Transform::summaryTypeToString(t.getSummaryType())); Chris@165: } Chris@165: Chris@165: if (t.getPluginVersion() != QString()) { Chris@165: json += stpl.arg("plugin_version").arg(t.getPluginVersion()); Chris@165: } Chris@165: Chris@165: if (t.getProgram() != QString()) { Chris@165: json += stpl.arg("program").arg(t.getProgram()); Chris@165: } Chris@165: Chris@165: if (t.getStepSize() != 0) { Chris@165: json += ntpl.arg("step_size").arg(t.getStepSize()); Chris@165: } Chris@165: Chris@165: if (t.getBlockSize() != 0) { Chris@165: json += ntpl.arg("block_size").arg(t.getBlockSize()); Chris@165: } Chris@165: Chris@165: if (t.getWindowType() != HanningWindow) { Chris@165: json += stpl.arg("window_type") Chris@165: .arg(Window::getNameForType(t.getWindowType()).c_str()); Chris@165: } Chris@165: Chris@165: if (t.getStartTime() != RealTime::zeroTime) { Chris@165: json += ntpl.arg("start").arg(t.getStartTime().toDouble()); Chris@165: } Chris@165: Chris@165: if (t.getDuration() != RealTime::zeroTime) { Chris@165: json += ntpl.arg("duration").arg(t.getDuration().toDouble()); Chris@165: } Chris@165: Chris@165: if (t.getSampleRate() != 0) { Chris@165: json += ntpl.arg("sample_rate").arg(t.getSampleRate()); Chris@165: } Chris@165: Chris@165: if (!t.getParameters().empty()) { Chris@165: json += QString(" \"parameters\": {\n"); Chris@165: Transform::ParameterMap parameters = t.getParameters(); Chris@165: for (Transform::ParameterMap::const_iterator i = parameters.begin(); Chris@165: i != parameters.end(); ++i) { Chris@165: QString name = i->first; Chris@165: float value = i->second; Chris@165: json += QString(" \"%1\": %2\n").arg(name).arg(value); Chris@165: } Chris@165: json += QString(" },\n"); Chris@165: } Chris@165: Chris@165: // no trailing comma on final property: Chris@165: json += QString(" \"transform_id\": \"%1\"\n").arg(t.getIdentifier()); Chris@165: Chris@165: return json; Chris@165: } Chris@165: