annotate runner/JAMSFeatureWriter.cpp @ 281:c8162c2992f8 piper-nopiper

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