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@324: #include "base/Debug.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@200: "json"), // file extension is json even with jams writer Chris@145: m_network(false), Chris@169: m_networkRetrieved(false), Chris@169: m_n(1), Chris@204: m_m(1), Chris@204: m_digits(6) 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@204: p.name = "digits"; Chris@204: p.description = "Specify the number of significant digits to use when printing transform outputs. Outputs are represented internally using single-precision floating-point, so digits beyond the 8th or 9th place are usually meaningless. The default is 6."; Chris@204: p.hasArg = true; Chris@204: pl.push_back(p); Chris@204: Chris@145: p.name = "network"; Chris@197: 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@204: } else if (i->first == "digits") { Chris@204: int digits = atoi(i->second.c_str()); Chris@204: if (digits <= 0 || digits > 100) { Chris@324: SVCERR << "JAMSFeatureWriter: ERROR: Invalid or out-of-range value for number of significant digits: " << i->second << endl; Chris@324: SVCERR << "JAMSFeatureWriter: NOTE: Continuing with default settings" << endl; Chris@204: } else { Chris@204: m_digits = digits; Chris@204: } 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@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@204: QString timestr = f.timestamp.toString().c_str(); Chris@204: timestr.replace(QRegExp("^ +"), ""); Chris@204: Chris@208: QString durstr = "0.0"; Chris@168: if (f.hasDuration) { Chris@208: durstr = f.duration.toString().c_str(); Chris@208: durstr.replace(QRegExp("^ +"), ""); Chris@153: } Chris@153: Chris@208: d += QString("\"time\": %1, \"duration\": %2, \"confidence\": 1.0") Chris@208: .arg(timestr).arg(durstr); Chris@208: Chris@208: // here we have to differ from the JAMS 0.2.0 spec. It allows Chris@208: // a single "value" element which can be either a number or a Chris@208: // string, depending on the selected task. But we may have Chris@208: // many values and may have a label as well, and no way to Chris@208: // know whether these can be made to conform to the JAMS task Chris@208: // schema. We should just write what we have. If we only have Chris@208: // a label, we can write that out as "value" as JAMS requests, Chris@208: // but if we have a (numerical) value and a label, we really Chris@208: // have to write them separately, and if we have multiple Chris@208: // values we'll have to use an array. The chances of actually Chris@208: // ending up with a schema-compliant JAMS format are quite Chris@208: // small, which suggests JAMS isn't a great idea for this Chris@208: // after all! Chris@208: Chris@153: if (f.label != "") { Chris@208: if (f.values.empty()) { Chris@208: d += QString(", \"value\": \"%2\"").arg(f.label.c_str()); Chris@208: } else { Chris@208: d += QString(", \"label\": \"%2\"").arg(f.label.c_str()); Chris@208: } Chris@167: } Chris@167: Chris@208: if (!f.values.empty()) { Chris@208: d += QString(", \"value\": "); Chris@208: if (f.values.size() > 1) { Chris@208: d += "[ "; Chris@208: } 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@204: d += QString("%1").arg(f.values[j], 0, 'g', m_digits); Chris@192: } Chris@192: if (j + 1 < int(f.values.size())) { Chris@192: d += ", "; Chris@169: } Chris@167: } Chris@208: if (f.values.size() > 1) { Chris@208: d += " ]"; Chris@208: } 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@208: " \"jams_version\": \"0.2.0\",\n" Chris@208: " \"identifiers\": { \"filename\": \"%1\" }") Chris@167: .arg(QFileInfo(trackId).fileName()); Chris@167: Chris@167: if (m_trackMetadata.find(trackId) != m_trackMetadata.end()) { Chris@208: Chris@208: QString durstr = m_trackMetadata[trackId].duration.toString().c_str(); Chris@208: durstr.replace(QRegExp("^ +"), ""); Chris@208: stream << QString(",\n \"duration\": %1").arg(durstr); Chris@208: 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@208: stream << "\"annotations\": [\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: 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@208: if (!firstInTrack) { Chris@167: stream << ",\n"; Chris@167: } Chris@167: Chris@208: stream << "{\n \"namespace\": \"" << getTaskKey(task) << "\",\n"; Chris@208: Chris@167: stream << QString Chris@208: (" \"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@208: firstInTrack = false; Chris@167: } Chris@208: } Chris@167: Chris@208: stream << "\n]"; 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@324: SVCERR << "NOTE: Have RDF description for plugin ID \"" Chris@145: << pluginId << "\"" << endl; Chris@145: } else { Chris@324: SVCERR << "NOTE: No RDF description for plugin ID \"" Chris@145: << pluginId << "\"" << endl; Chris@145: if (!m_network) { Chris@324: SVCERR << " Consider using the --jams-network option to retrieve plugin descriptions" << endl; Chris@324: SVCERR << " 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@324: SVCERR << "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@324: SVCERR << "WARNING: Cannot currently write dense or track-level outputs to JAMS 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@204: json += ntpl.arg("start") Chris@204: .arg(t.getStartTime().toDouble(), 0, 'g', 9); Chris@165: } Chris@165: Chris@165: if (t.getDuration() != RealTime::zeroTime) { Chris@204: json += ntpl.arg("duration") Chris@204: .arg(t.getDuration().toDouble(), 0, 'g', 9); 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@204: json += QString(" \"%1\": %2") Chris@204: .arg(name) Chris@204: .arg(value, 0, 'g', 8); // parameter values always to high precision 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: