annotate runner/JAMSFeatureWriter.cpp @ 399:a3912193ce69 tip

Default branch is now named default on git as well as hg, in case we ever want to switch to mirroring in the other direction
author Chris Cannam
date Thu, 27 Aug 2020 15:57:37 +0100
parents ef03350baec7
children
rev   line source
Chris@145 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@145 2
Chris@145 3 /*
Chris@145 4 Sonic Annotator
Chris@145 5 A utility for batch feature extraction from audio files.
Chris@145 6 Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
Chris@145 7 Copyright 2007-2014 QMUL.
Chris@145 8
Chris@145 9 This program is free software; you can redistribute it and/or
Chris@145 10 modify it under the terms of the GNU General Public License as
Chris@145 11 published by the Free Software Foundation; either version 2 of the
Chris@145 12 License, or (at your option) any later version. See the file
Chris@145 13 COPYING included with this distribution for more information.
Chris@145 14 */
Chris@145 15
Chris@145 16 #include "JAMSFeatureWriter.h"
Chris@145 17
Chris@145 18 using namespace std;
Chris@145 19 using Vamp::Plugin;
Chris@145 20 using Vamp::PluginBase;
Chris@145 21
Chris@145 22 #include "base/Exceptions.h"
Chris@324 23 #include "base/Debug.h"
Chris@145 24 #include "rdf/PluginRDFIndexer.h"
Chris@145 25
Chris@166 26 #include <QFileInfo>
Chris@189 27 #include <QTextCodec>
Chris@166 28
Chris@162 29 #include "version.h"
Chris@162 30
Chris@145 31 JAMSFeatureWriter::JAMSFeatureWriter() :
Chris@145 32 FileFeatureWriter(SupportOneFilePerTrackTransform |
Chris@145 33 SupportOneFilePerTrack |
Chris@152 34 SupportOneFileTotal |
Chris@145 35 SupportStdOut,
Chris@200 36 "json"), // file extension is json even with jams writer
Chris@145 37 m_network(false),
Chris@169 38 m_networkRetrieved(false),
Chris@169 39 m_n(1),
Chris@204 40 m_m(1),
Chris@204 41 m_digits(6)
Chris@145 42 {
Chris@145 43 }
Chris@145 44
Chris@145 45 JAMSFeatureWriter::~JAMSFeatureWriter()
Chris@145 46 {
Chris@145 47 }
Chris@145 48
Chris@145 49 string
Chris@145 50 JAMSFeatureWriter::getDescription() const
Chris@145 51 {
Chris@170 52 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 53 }
Chris@145 54
Chris@145 55 JAMSFeatureWriter::ParameterList
Chris@145 56 JAMSFeatureWriter::getSupportedParameters() const
Chris@145 57 {
Chris@145 58 ParameterList pl = FileFeatureWriter::getSupportedParameters();
Chris@145 59 Parameter p;
Chris@145 60
Chris@204 61 p.name = "digits";
Chris@204 62 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 63 p.hasArg = true;
Chris@204 64 pl.push_back(p);
Chris@204 65
Chris@145 66 p.name = "network";
Chris@197 67 p.description = "Attempt to retrieve RDF descriptions of plugins from network, if not available locally.";
Chris@145 68 p.hasArg = false;
Chris@145 69 pl.push_back(p);
Chris@145 70
Chris@145 71 return pl;
Chris@145 72 }
Chris@145 73
Chris@145 74 void
Chris@145 75 JAMSFeatureWriter::setParameters(map<string, string> &params)
Chris@145 76 {
Chris@145 77 FileFeatureWriter::setParameters(params);
Chris@145 78
Chris@145 79 for (map<string, string>::iterator i = params.begin();
Chris@145 80 i != params.end(); ++i) {
Chris@145 81 if (i->first == "network") {
Chris@145 82 m_network = true;
Chris@204 83 } else if (i->first == "digits") {
Chris@204 84 int digits = atoi(i->second.c_str());
Chris@204 85 if (digits <= 0 || digits > 100) {
Chris@324 86 SVCERR << "JAMSFeatureWriter: ERROR: Invalid or out-of-range value for number of significant digits: " << i->second << endl;
Chris@324 87 SVCERR << "JAMSFeatureWriter: NOTE: Continuing with default settings" << endl;
Chris@204 88 } else {
Chris@204 89 m_digits = digits;
Chris@204 90 }
Chris@145 91 }
Chris@145 92 }
Chris@145 93 }
Chris@145 94
Chris@145 95 void
Chris@145 96 JAMSFeatureWriter::setTrackMetadata(QString trackId, TrackMetadata metadata)
Chris@145 97 {
Chris@167 98 m_trackMetadata[trackId] = metadata;
Chris@145 99 }
Chris@145 100
Chris@145 101 void
Chris@145 102 JAMSFeatureWriter::write(QString trackId,
Chris@145 103 const Transform &transform,
Chris@145 104 const Plugin::OutputDescriptor& ,
Chris@145 105 const Plugin::FeatureList& features,
Chris@145 106 std::string /* summaryType */)
Chris@145 107 {
Chris@145 108 QString transformId = transform.getIdentifier();
Chris@145 109
Chris@189 110 QTextStream *sptr = getOutputStream
Chris@189 111 (trackId, transformId, QTextCodec::codecForName("UTF-8"));
Chris@145 112 if (!sptr) {
Chris@145 113 throw FailedToOpenOutputStream(trackId, transformId);
Chris@145 114 }
Chris@145 115
Chris@167 116 DataId did(trackId, transform);
Chris@145 117
Chris@167 118 if (m_data.find(did) == m_data.end()) {
Chris@167 119 identifyTask(transform);
Chris@167 120 m_streamTracks[sptr].insert(trackId);
Chris@167 121 m_streamTasks[sptr].insert(m_tasks[transformId]);
Chris@167 122 m_streamData[sptr].insert(did);
Chris@152 123 }
Chris@152 124
Chris@167 125 QString d = m_data[did];
Chris@153 126
Chris@145 127 for (int i = 0; i < int(features.size()); ++i) {
Chris@153 128
Chris@167 129 if (d != "") {
Chris@153 130 d += ",\n";
Chris@153 131 }
Chris@153 132
Chris@153 133 d += " { ";
Chris@145 134
Chris@153 135 Plugin::Feature f(features[i]);
Chris@153 136
Chris@204 137 QString timestr = f.timestamp.toString().c_str();
Chris@204 138 timestr.replace(QRegExp("^ +"), "");
Chris@204 139
Chris@208 140 QString durstr = "0.0";
Chris@168 141 if (f.hasDuration) {
Chris@208 142 durstr = f.duration.toString().c_str();
Chris@208 143 durstr.replace(QRegExp("^ +"), "");
Chris@153 144 }
Chris@153 145
Chris@208 146 d += QString("\"time\": %1, \"duration\": %2, \"confidence\": 1.0")
Chris@208 147 .arg(timestr).arg(durstr);
Chris@208 148
Chris@208 149 // here we have to differ from the JAMS 0.2.0 spec. It allows
Chris@208 150 // a single "value" element which can be either a number or a
Chris@208 151 // string, depending on the selected task. But we may have
Chris@208 152 // many values and may have a label as well, and no way to
Chris@208 153 // know whether these can be made to conform to the JAMS task
Chris@208 154 // schema. We should just write what we have. If we only have
Chris@208 155 // a label, we can write that out as "value" as JAMS requests,
Chris@208 156 // but if we have a (numerical) value and a label, we really
Chris@208 157 // have to write them separately, and if we have multiple
Chris@208 158 // values we'll have to use an array. The chances of actually
Chris@208 159 // ending up with a schema-compliant JAMS format are quite
Chris@208 160 // small, which suggests JAMS isn't a great idea for this
Chris@208 161 // after all!
Chris@208 162
Chris@153 163 if (f.label != "") {
Chris@208 164 if (f.values.empty()) {
Chris@208 165 d += QString(", \"value\": \"%2\"").arg(f.label.c_str());
Chris@208 166 } else {
Chris@208 167 d += QString(", \"label\": \"%2\"").arg(f.label.c_str());
Chris@208 168 }
Chris@167 169 }
Chris@167 170
Chris@208 171 if (!f.values.empty()) {
Chris@208 172 d += QString(", \"value\": ");
Chris@208 173 if (f.values.size() > 1) {
Chris@208 174 d += "[ ";
Chris@208 175 }
Chris@167 176 for (int j = 0; j < int(f.values.size()); ++j) {
Chris@169 177 if (isnan(f.values[j])) {
Chris@192 178 d += "\"NaN\"";
Chris@169 179 } else if (isinf(f.values[j])) {
Chris@192 180 d += "\"Inf\"";
Chris@169 181 } else {
Chris@204 182 d += QString("%1").arg(f.values[j], 0, 'g', m_digits);
Chris@192 183 }
Chris@192 184 if (j + 1 < int(f.values.size())) {
Chris@192 185 d += ", ";
Chris@169 186 }
Chris@167 187 }
Chris@208 188 if (f.values.size() > 1) {
Chris@208 189 d += " ]";
Chris@208 190 }
Chris@153 191 }
Chris@153 192
Chris@153 193 d += " }";
Chris@145 194 }
Chris@153 195
Chris@167 196 m_data[did] = d;
Chris@145 197 }
Chris@145 198
Chris@145 199 void
Chris@169 200 JAMSFeatureWriter::setNofM(int n, int m)
Chris@169 201 {
Chris@169 202 if (m_singleFileName != "" || m_stdout) {
Chris@169 203 m_n = n;
Chris@169 204 m_m = m;
Chris@169 205 } else {
Chris@169 206 m_n = 1;
Chris@169 207 m_m = 1;
Chris@169 208 }
Chris@169 209 }
Chris@169 210
Chris@169 211 void
Chris@152 212 JAMSFeatureWriter::finish()
Chris@152 213 {
Chris@167 214 for (FileStreamMap::const_iterator stri = m_streams.begin();
Chris@167 215 stri != m_streams.end(); ++stri) {
Chris@152 216
Chris@167 217 QTextStream *sptr = stri->second;
Chris@167 218 QTextStream &stream = *sptr;
Chris@152 219
Chris@167 220 bool firstInStream = true;
Chris@167 221
Chris@167 222 for (TrackIds::const_iterator tri = m_streamTracks[sptr].begin();
Chris@167 223 tri != m_streamTracks[sptr].end(); ++tri) {
Chris@167 224
Chris@167 225 TrackId trackId = *tri;
Chris@167 226
Chris@169 227 if (firstInStream) {
Chris@169 228 if (m_streamTracks[sptr].size() > 1 || (m_m > 1 && m_n == 1)) {
Chris@169 229 stream << "[\n";
Chris@169 230 }
Chris@169 231 }
Chris@169 232
Chris@169 233 if (!firstInStream || (m_m > 1 && m_n > 1)) {
Chris@167 234 stream << ",\n";
Chris@167 235 }
Chris@167 236
Chris@167 237 stream << "{\n"
Chris@167 238 << QString("\"file_metadata\": {\n"
Chris@208 239 " \"jams_version\": \"0.2.0\",\n"
Chris@208 240 " \"identifiers\": { \"filename\": \"%1\" }")
Chris@167 241 .arg(QFileInfo(trackId).fileName());
Chris@167 242
Chris@167 243 if (m_trackMetadata.find(trackId) != m_trackMetadata.end()) {
Chris@208 244
Chris@208 245 QString durstr = m_trackMetadata[trackId].duration.toString().c_str();
Chris@208 246 durstr.replace(QRegExp("^ +"), "");
Chris@208 247 stream << QString(",\n \"duration\": %1").arg(durstr);
Chris@208 248
Chris@167 249 if (m_trackMetadata[trackId].maker != "") {
Chris@167 250 stream << QString(",\n \"artist\": \"%1\"")
Chris@167 251 .arg(m_trackMetadata[trackId].maker);
Chris@167 252 }
Chris@167 253 if (m_trackMetadata[trackId].title != "") {
Chris@167 254 stream << QString(",\n \"title\": \"%1\"")
Chris@167 255 .arg(m_trackMetadata[trackId].title);
Chris@167 256 }
Chris@167 257 }
Chris@167 258
Chris@167 259 stream << "\n},\n";
Chris@208 260 stream << "\"annotations\": [\n";
Chris@167 261
Chris@167 262 bool firstInTrack = true;
Chris@167 263
Chris@167 264 for (Tasks::const_iterator ti = m_streamTasks[sptr].begin();
Chris@167 265 ti != m_streamTasks[sptr].end(); ++ti) {
Chris@167 266
Chris@167 267 Task task = *ti;
Chris@167 268
Chris@167 269 for (DataIds::const_iterator di = m_streamData[sptr].begin();
Chris@167 270 di != m_streamData[sptr].end(); ++di) {
Chris@167 271
Chris@167 272 DataId did = *di;
Chris@167 273
Chris@167 274 QString trackId = did.first;
Chris@167 275 Transform transform = did.second;
Chris@167 276
Chris@167 277 if (m_tasks[transform.getIdentifier()] != task) continue;
Chris@167 278
Chris@167 279 QString data = m_data[did];
Chris@167 280
Chris@208 281 if (!firstInTrack) {
Chris@167 282 stream << ",\n";
Chris@167 283 }
Chris@167 284
Chris@208 285 stream << "{\n \"namespace\": \"" << getTaskKey(task) << "\",\n";
Chris@208 286
Chris@167 287 stream << QString
Chris@208 288 (" \"annotation_metadata\": {\n"
Chris@167 289 " \"annotation_tools\": \"Sonic Annotator v%2\",\n"
Chris@167 290 " \"data_source\": \"Automatic feature extraction\",\n"
Chris@167 291 " \"annotator\": {\n"
Chris@167 292 "%3"
Chris@167 293 " }\n"
Chris@167 294 " },\n"
Chris@167 295 " \"data\": [\n")
Chris@167 296 .arg(RUNNER_VERSION)
Chris@167 297 .arg(writeTransformToObjectContents(transform));
Chris@167 298
Chris@167 299 stream << data;
Chris@167 300
Chris@167 301 stream << "\n ]\n}";
Chris@208 302 firstInTrack = false;
Chris@167 303 }
Chris@208 304 }
Chris@167 305
Chris@208 306 stream << "\n]";
Chris@167 307
Chris@167 308 stream << "\n}";
Chris@167 309 firstInStream = false;
Chris@152 310 }
Chris@167 311
Chris@169 312 if (!firstInStream) {
Chris@169 313 if (m_streamTracks[sptr].size() > 1 || (m_m > 1 && m_n == m_m)) {
Chris@169 314 stream << "\n]";
Chris@169 315 }
Chris@169 316 stream << "\n";
Chris@167 317 }
Chris@152 318 }
Chris@152 319
Chris@167 320 m_streamTracks.clear();
Chris@167 321 m_streamTasks.clear();
Chris@167 322 m_streamData.clear();
Chris@152 323 m_data.clear();
Chris@152 324
Chris@152 325 FileFeatureWriter::finish();
Chris@152 326 }
Chris@152 327
Chris@152 328 void
Chris@145 329 JAMSFeatureWriter::loadRDFDescription(const Transform &transform)
Chris@145 330 {
Chris@145 331 QString pluginId = transform.getPluginIdentifier();
Chris@145 332 if (m_rdfDescriptions.find(pluginId) != m_rdfDescriptions.end()) return;
Chris@145 333
Chris@145 334 if (m_network && !m_networkRetrieved) {
Chris@145 335 PluginRDFIndexer::getInstance()->indexConfiguredURLs();
Chris@145 336 m_networkRetrieved = true;
Chris@145 337 }
Chris@145 338
Chris@145 339 m_rdfDescriptions[pluginId] = PluginRDFDescription(pluginId);
Chris@145 340
Chris@145 341 if (m_rdfDescriptions[pluginId].haveDescription()) {
Chris@324 342 SVCERR << "NOTE: Have RDF description for plugin ID \""
Chris@145 343 << pluginId << "\"" << endl;
Chris@145 344 } else {
Chris@324 345 SVCERR << "NOTE: No RDF description for plugin ID \""
Chris@145 346 << pluginId << "\"" << endl;
Chris@145 347 if (!m_network) {
Chris@324 348 SVCERR << " Consider using the --jams-network option to retrieve plugin descriptions" << endl;
Chris@324 349 SVCERR << " from the network where possible." << endl;
Chris@145 350 }
Chris@145 351 }
Chris@145 352 }
Chris@145 353
Chris@145 354 void
Chris@145 355 JAMSFeatureWriter::identifyTask(const Transform &transform)
Chris@145 356 {
Chris@145 357 QString transformId = transform.getIdentifier();
Chris@145 358 if (m_tasks.find(transformId) != m_tasks.end()) return;
Chris@145 359
Chris@145 360 loadRDFDescription(transform);
Chris@145 361
Chris@145 362 Task task = UnknownTask;
Chris@145 363
Chris@145 364 QString pluginId = transform.getPluginIdentifier();
Chris@145 365 QString outputId = transform.getOutput();
Chris@145 366
Chris@145 367 const PluginRDFDescription &desc = m_rdfDescriptions[pluginId];
Chris@145 368
Chris@145 369 if (desc.haveDescription()) {
Chris@145 370
Chris@145 371 PluginRDFDescription::OutputDisposition disp =
Chris@145 372 desc.getOutputDisposition(outputId);
Chris@145 373
Chris@145 374 QString af = "http://purl.org/ontology/af/";
Chris@145 375
Chris@145 376 if (disp == PluginRDFDescription::OutputSparse) {
Chris@145 377
Chris@145 378 QString eventUri = desc.getOutputEventTypeURI(outputId);
Chris@145 379
Chris@145 380 //!!! todo: allow user to prod writer for task type
Chris@145 381
Chris@145 382 if (eventUri == af + "Note") {
Chris@145 383 task = NoteTask;
Chris@145 384 } else if (eventUri == af + "Beat") {
Chris@145 385 task = BeatTask;
Chris@145 386 } else if (eventUri == af + "ChordSegment") {
Chris@145 387 task = ChordTask;
Chris@145 388 } else if (eventUri == af + "KeyChange") {
Chris@145 389 task = KeyTask;
Chris@145 390 } else if (eventUri == af + "KeySegment") {
Chris@145 391 task = KeyTask;
Chris@145 392 } else if (eventUri == af + "Onset") {
Chris@145 393 task = OnsetTask;
Chris@145 394 } else if (eventUri == af + "NonTonalOnset") {
Chris@145 395 task = OnsetTask;
Chris@145 396 } else if (eventUri == af + "Segment") {
Chris@145 397 task = SegmentTask;
Chris@145 398 } else if (eventUri == af + "SpeechSegment") {
Chris@145 399 task = SegmentTask;
Chris@145 400 } else if (eventUri == af + "StructuralSegment") {
Chris@145 401 task = SegmentTask;
Chris@145 402 } else {
Chris@324 403 SVCERR << "WARNING: Unsupported event type URI <"
Chris@145 404 << eventUri << ">, proceeding with UnknownTask type"
Chris@145 405 << endl;
Chris@145 406 }
Chris@145 407
Chris@145 408 } else {
Chris@145 409
Chris@324 410 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 411 }
Chris@145 412 }
Chris@145 413
Chris@145 414 m_tasks[transformId] = task;
Chris@145 415 }
Chris@145 416
Chris@145 417 QString
Chris@145 418 JAMSFeatureWriter::getTaskKey(Task task)
Chris@145 419 {
Chris@145 420 switch (task) {
Chris@145 421 case UnknownTask: return "unknown";
Chris@145 422 case BeatTask: return "beat";
Chris@145 423 case OnsetTask: return "onset";
Chris@145 424 case ChordTask: return "chord";
Chris@145 425 case SegmentTask: return "segment";
Chris@145 426 case KeyTask: return "key";
Chris@145 427 case NoteTask: return "note";
Chris@145 428 case MelodyTask: return "melody";
Chris@145 429 case PitchTask: return "pitch";
Chris@145 430 }
Chris@145 431 return "unknown";
Chris@145 432 }
Chris@165 433
Chris@165 434 QString
Chris@165 435 JAMSFeatureWriter::writeTransformToObjectContents(const Transform &t)
Chris@165 436 {
Chris@165 437 QString json;
Chris@165 438 QString stpl(" \"%1\": \"%2\",\n");
Chris@165 439 QString ntpl(" \"%1\": %2,\n");
Chris@165 440
Chris@165 441 json += stpl.arg("plugin_id").arg(t.getPluginIdentifier());
Chris@165 442 json += stpl.arg("output_id").arg(t.getOutput());
Chris@165 443
Chris@165 444 if (t.getSummaryType() != Transform::NoSummary) {
Chris@165 445 json += stpl.arg("summary_type")
Chris@165 446 .arg(Transform::summaryTypeToString(t.getSummaryType()));
Chris@165 447 }
Chris@165 448
Chris@165 449 if (t.getPluginVersion() != QString()) {
Chris@165 450 json += stpl.arg("plugin_version").arg(t.getPluginVersion());
Chris@165 451 }
Chris@165 452
Chris@165 453 if (t.getProgram() != QString()) {
Chris@165 454 json += stpl.arg("program").arg(t.getProgram());
Chris@165 455 }
Chris@165 456
Chris@165 457 if (t.getStepSize() != 0) {
Chris@165 458 json += ntpl.arg("step_size").arg(t.getStepSize());
Chris@165 459 }
Chris@165 460
Chris@165 461 if (t.getBlockSize() != 0) {
Chris@165 462 json += ntpl.arg("block_size").arg(t.getBlockSize());
Chris@165 463 }
Chris@165 464
Chris@165 465 if (t.getWindowType() != HanningWindow) {
Chris@165 466 json += stpl.arg("window_type")
Chris@165 467 .arg(Window<float>::getNameForType(t.getWindowType()).c_str());
Chris@165 468 }
Chris@165 469
Chris@165 470 if (t.getStartTime() != RealTime::zeroTime) {
Chris@204 471 json += ntpl.arg("start")
Chris@204 472 .arg(t.getStartTime().toDouble(), 0, 'g', 9);
Chris@165 473 }
Chris@165 474
Chris@165 475 if (t.getDuration() != RealTime::zeroTime) {
Chris@204 476 json += ntpl.arg("duration")
Chris@204 477 .arg(t.getDuration().toDouble(), 0, 'g', 9);
Chris@165 478 }
Chris@165 479
Chris@165 480 if (t.getSampleRate() != 0) {
Chris@165 481 json += ntpl.arg("sample_rate").arg(t.getSampleRate());
Chris@165 482 }
Chris@165 483
Chris@165 484 if (!t.getParameters().empty()) {
Chris@165 485 json += QString(" \"parameters\": {\n");
Chris@165 486 Transform::ParameterMap parameters = t.getParameters();
Chris@165 487 for (Transform::ParameterMap::const_iterator i = parameters.begin();
Chris@165 488 i != parameters.end(); ++i) {
Chris@167 489 if (i != parameters.begin()) {
Chris@167 490 json += ",\n";
Chris@167 491 }
Chris@165 492 QString name = i->first;
Chris@165 493 float value = i->second;
Chris@204 494 json += QString(" \"%1\": %2")
Chris@204 495 .arg(name)
Chris@204 496 .arg(value, 0, 'g', 8); // parameter values always to high precision
Chris@165 497 }
Chris@167 498 json += QString("\n },\n");
Chris@165 499 }
Chris@165 500
Chris@165 501 // no trailing comma on final property:
Chris@165 502 json += QString(" \"transform_id\": \"%1\"\n").arg(t.getIdentifier());
Chris@165 503
Chris@165 504 return json;
Chris@165 505 }
Chris@165 506