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@189: #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@169: m_networkRetrieved(false), Chris@169: m_n(1), Chris@169: m_m(1) 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@170: 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."; 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@167: m_trackMetadata[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@189: QTextStream *sptr = getOutputStream Chris@189: (trackId, transformId, QTextCodec::codecForName("UTF-8")); Chris@145: if (!sptr) { Chris@145: throw FailedToOpenOutputStream(trackId, transformId); Chris@145: } Chris@145: Chris@167: DataId did(trackId, transform); Chris@145: Chris@167: if (m_data.find(did) == m_data.end()) { Chris@167: identifyTask(transform); Chris@167: m_streamTracks[sptr].insert(trackId); Chris@167: m_streamTasks[sptr].insert(m_tasks[transformId]); Chris@167: m_streamData[sptr].insert(did); Chris@152: } Chris@152: Chris@167: QString d = m_data[did]; Chris@153: Chris@145: for (int i = 0; i < int(features.size()); ++i) { Chris@153: Chris@167: if (d != "") { Chris@153: d += ",\n"; Chris@153: } Chris@153: Chris@153: d += " { "; Chris@145: Chris@153: Plugin::Feature f(features[i]); Chris@153: Chris@168: if (f.hasDuration) { Chris@168: d += QString Chris@168: ("\"start\": { \"value\": %1 }, " Chris@168: "\"end\": { \"value\": %2 }") Chris@168: .arg(realTime2Sec(f.timestamp)) Chris@168: .arg(realTime2Sec Chris@168: (f.timestamp + Chris@168: (f.hasDuration ? f.duration : Vamp::RealTime::zeroTime))); Chris@168: } else { Chris@153: d += QString("\"time\": { \"value\": %1 }") Chris@153: .arg(realTime2Sec(f.timestamp)); Chris@153: } Chris@153: Chris@153: if (f.label != "") { Chris@153: d += QString(", \"label\": { \"value\": \"%2\" }") Chris@153: .arg(f.label.c_str()); Chris@167: } Chris@167: Chris@167: if (f.values.size() > 0) { Chris@167: d += QString(", \"value\": [ "); Chris@167: for (int j = 0; j < int(f.values.size()); ++j) { Chris@169: if (isnan(f.values[j])) { Chris@192: d += "\"NaN\""; Chris@169: } else if (isinf(f.values[j])) { Chris@192: d += "\"Inf\""; Chris@169: } else { Chris@192: d += QString("%1").arg(f.values[j]); Chris@192: } Chris@192: if (j + 1 < int(f.values.size())) { Chris@192: d += ", "; Chris@169: } Chris@167: } Chris@192: d += " ]"; Chris@153: } Chris@153: Chris@153: d += " }"; Chris@145: } Chris@153: Chris@167: m_data[did] = d; Chris@145: } Chris@145: Chris@145: void Chris@169: JAMSFeatureWriter::setNofM(int n, int m) Chris@169: { Chris@169: if (m_singleFileName != "" || m_stdout) { Chris@169: m_n = n; Chris@169: m_m = m; Chris@169: } else { Chris@169: m_n = 1; Chris@169: m_m = 1; Chris@169: } Chris@169: } Chris@169: Chris@169: void Chris@152: JAMSFeatureWriter::finish() Chris@152: { Chris@167: for (FileStreamMap::const_iterator stri = m_streams.begin(); Chris@167: stri != m_streams.end(); ++stri) { Chris@152: Chris@167: QTextStream *sptr = stri->second; Chris@167: QTextStream &stream = *sptr; Chris@152: Chris@167: bool firstInStream = true; Chris@167: Chris@167: for (TrackIds::const_iterator tri = m_streamTracks[sptr].begin(); Chris@167: tri != m_streamTracks[sptr].end(); ++tri) { Chris@167: Chris@167: TrackId trackId = *tri; Chris@167: Chris@169: if (firstInStream) { Chris@169: if (m_streamTracks[sptr].size() > 1 || (m_m > 1 && m_n == 1)) { Chris@169: stream << "[\n"; Chris@169: } Chris@169: } Chris@169: Chris@169: if (!firstInStream || (m_m > 1 && m_n > 1)) { Chris@167: stream << ",\n"; Chris@167: } Chris@167: Chris@167: stream << "{\n" Chris@167: << QString("\"file_metadata\": {\n" Chris@167: " \"filename\": \"%1\"") Chris@167: .arg(QFileInfo(trackId).fileName()); Chris@167: Chris@167: if (m_trackMetadata.find(trackId) != m_trackMetadata.end()) { Chris@167: if (m_trackMetadata[trackId].maker != "") { Chris@167: stream << QString(",\n \"artist\": \"%1\"") Chris@167: .arg(m_trackMetadata[trackId].maker); Chris@167: } Chris@167: if (m_trackMetadata[trackId].title != "") { Chris@167: stream << QString(",\n \"title\": \"%1\"") Chris@167: .arg(m_trackMetadata[trackId].title); Chris@167: } Chris@167: } Chris@167: Chris@167: stream << "\n},\n"; Chris@167: Chris@167: bool firstInTrack = true; Chris@167: Chris@167: for (Tasks::const_iterator ti = m_streamTasks[sptr].begin(); Chris@167: ti != m_streamTasks[sptr].end(); ++ti) { Chris@167: Chris@167: Task task = *ti; Chris@167: Chris@167: if (!firstInTrack) { Chris@167: stream << ",\n"; Chris@167: } Chris@167: Chris@167: stream << "\"" << getTaskKey(task) << "\": [\n"; Chris@167: Chris@167: bool firstInTask = true; Chris@167: Chris@167: for (DataIds::const_iterator di = m_streamData[sptr].begin(); Chris@167: di != m_streamData[sptr].end(); ++di) { Chris@167: Chris@167: DataId did = *di; Chris@167: Chris@167: QString trackId = did.first; Chris@167: Transform transform = did.second; Chris@167: Chris@167: if (m_tasks[transform.getIdentifier()] != task) continue; Chris@167: Chris@167: QString data = m_data[did]; Chris@167: Chris@167: if (!firstInTask) { Chris@167: stream << ",\n"; Chris@167: } Chris@167: Chris@167: stream << QString Chris@167: ("{ \n" Chris@167: " \"annotation_metadata\": {\n" Chris@167: " \"annotation_tools\": \"Sonic Annotator v%2\",\n" Chris@167: " \"data_source\": \"Automatic feature extraction\",\n" Chris@167: " \"annotator\": {\n" Chris@167: "%3" Chris@167: " }\n" Chris@167: " },\n" Chris@167: " \"data\": [\n") Chris@167: .arg(RUNNER_VERSION) Chris@167: .arg(writeTransformToObjectContents(transform)); Chris@167: Chris@167: stream << data; Chris@167: Chris@167: stream << "\n ]\n}"; Chris@167: firstInTask = false; Chris@167: } Chris@167: Chris@167: stream << "\n]"; Chris@167: firstInTrack = false; Chris@167: } Chris@167: Chris@167: stream << "\n}"; Chris@167: firstInStream = false; Chris@152: } Chris@167: Chris@169: if (!firstInStream) { Chris@169: if (m_streamTracks[sptr].size() > 1 || (m_m > 1 && m_n == m_m)) { Chris@169: stream << "\n]"; Chris@169: } Chris@169: stream << "\n"; Chris@167: } Chris@152: } Chris@152: Chris@167: m_streamTracks.clear(); Chris@167: m_streamTasks.clear(); Chris@167: m_streamData.clear(); Chris@152: m_data.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@167: if (i != parameters.begin()) { Chris@167: json += ",\n"; Chris@167: } Chris@165: QString name = i->first; Chris@165: float value = i->second; Chris@167: json += QString(" \"%1\": %2").arg(name).arg(value); Chris@165: } Chris@167: json += QString("\n },\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: