annotate runner/FeatureExtractionManager.cpp @ 152:db83ea0e102d jams

Make the JSON well-formed (though still empty), except in the case where we are asked to write more than one file's features to the same output file or stdout
author Chris Cannam
date Tue, 14 Oct 2014 12:35:19 +0100
parents ee56e3e9eeb5
children 447230267c0d
rev   line source
Chris@0 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@0 2
Chris@0 3 /*
Chris@0 4 Sonic Annotator
Chris@0 5 A utility for batch feature extraction from audio files.
Chris@0 6 Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
Chris@0 7 Copyright 2007-2008 QMUL.
Chris@0 8
Chris@0 9 This program is free software; you can redistribute it and/or
Chris@0 10 modify it under the terms of the GNU General Public License as
Chris@0 11 published by the Free Software Foundation; either version 2 of the
Chris@0 12 License, or (at your option) any later version. See the file
Chris@0 13 COPYING included with this distribution for more information.
Chris@0 14 */
Chris@0 15
Chris@0 16 #include "FeatureExtractionManager.h"
Chris@106 17 #include "MultiplexedReader.h"
Chris@0 18
Chris@0 19 #include <vamp-hostsdk/PluginChannelAdapter.h>
Chris@0 20 #include <vamp-hostsdk/PluginBufferingAdapter.h>
Chris@0 21 #include <vamp-hostsdk/PluginInputDomainAdapter.h>
Chris@0 22 #include <vamp-hostsdk/PluginSummarisingAdapter.h>
Chris@8 23 #include <vamp-hostsdk/PluginWrapper.h>
Chris@0 24 #include <vamp-hostsdk/PluginLoader.h>
Chris@0 25
Chris@21 26 #include "base/Exceptions.h"
Chris@21 27
Chris@0 28 #include <iostream>
Chris@0 29
Chris@0 30 using namespace std;
Chris@0 31
Chris@0 32 using Vamp::Plugin;
Chris@0 33 using Vamp::PluginBase;
Chris@0 34 using Vamp::HostExt::PluginLoader;
Chris@0 35 using Vamp::HostExt::PluginChannelAdapter;
Chris@0 36 using Vamp::HostExt::PluginBufferingAdapter;
Chris@0 37 using Vamp::HostExt::PluginInputDomainAdapter;
Chris@0 38 using Vamp::HostExt::PluginSummarisingAdapter;
Chris@8 39 using Vamp::HostExt::PluginWrapper;
Chris@0 40
Chris@0 41 #include "data/fileio/FileSource.h"
Chris@0 42 #include "data/fileio/AudioFileReader.h"
Chris@0 43 #include "data/fileio/AudioFileReaderFactory.h"
Chris@0 44 #include "base/TempDirectory.h"
Chris@0 45 #include "base/ProgressPrinter.h"
Chris@0 46 #include "transform/TransformFactory.h"
Chris@0 47 #include "rdf/RDFTransformFactory.h"
Chris@0 48 #include "transform/FeatureWriter.h"
Chris@0 49
Chris@0 50 #include <QTextStream>
Chris@0 51 #include <QFile>
Chris@0 52 #include <QFileInfo>
Chris@0 53
Chris@0 54 FeatureExtractionManager::FeatureExtractionManager() :
Chris@0 55 m_summariesOnly(false),
Chris@0 56 // We can read using an arbitrary fixed block size --
Chris@0 57 // PluginBufferingAdapter handles this for us. It's likely to be
Chris@0 58 // quicker to use larger sizes than smallish ones like 1024
Chris@0 59 m_blockSize(16384),
Chris@0 60 m_defaultSampleRate(0),
Chris@0 61 m_sampleRate(0),
Chris@116 62 m_channels(0),
Chris@116 63 m_normalise(false)
Chris@0 64 {
Chris@0 65 }
Chris@0 66
Chris@0 67 FeatureExtractionManager::~FeatureExtractionManager()
Chris@0 68 {
Chris@0 69 for (PluginMap::iterator pi = m_plugins.begin();
Chris@0 70 pi != m_plugins.end(); ++pi) {
Chris@0 71 delete pi->first;
Chris@0 72 }
Chris@45 73 foreach (AudioFileReader *r, m_readyReaders) {
Chris@45 74 delete r;
Chris@45 75 }
Chris@0 76 }
Chris@0 77
Chris@0 78 void FeatureExtractionManager::setChannels(int channels)
Chris@0 79 {
Chris@0 80 m_channels = channels;
Chris@0 81 }
Chris@0 82
Chris@0 83 void FeatureExtractionManager::setDefaultSampleRate(int sampleRate)
Chris@0 84 {
Chris@0 85 m_defaultSampleRate = sampleRate;
Chris@0 86 }
Chris@0 87
Chris@116 88 void FeatureExtractionManager::setNormalise(bool normalise)
Chris@116 89 {
Chris@116 90 m_normalise = normalise;
Chris@116 91 }
Chris@116 92
Chris@0 93 static PluginSummarisingAdapter::SummaryType
Chris@0 94 getSummaryType(string name)
Chris@0 95 {
Chris@0 96 if (name == "min") return PluginSummarisingAdapter::Minimum;
Chris@0 97 if (name == "max") return PluginSummarisingAdapter::Maximum;
Chris@0 98 if (name == "mean") return PluginSummarisingAdapter::Mean;
Chris@0 99 if (name == "median") return PluginSummarisingAdapter::Median;
Chris@0 100 if (name == "mode") return PluginSummarisingAdapter::Mode;
Chris@0 101 if (name == "sum") return PluginSummarisingAdapter::Sum;
Chris@0 102 if (name == "variance") return PluginSummarisingAdapter::Variance;
Chris@0 103 if (name == "sd") return PluginSummarisingAdapter::StandardDeviation;
Chris@0 104 if (name == "count") return PluginSummarisingAdapter::Count;
Chris@0 105 return PluginSummarisingAdapter::UnknownSummaryType;
Chris@0 106 }
Chris@0 107
Chris@102 108 bool
Chris@102 109 FeatureExtractionManager::setSummaryTypes(const set<string> &names,
Chris@102 110 const PluginSummarisingAdapter::SegmentBoundaries &boundaries)
Chris@0 111 {
Chris@0 112 for (SummaryNameSet::const_iterator i = names.begin();
Chris@0 113 i != names.end(); ++i) {
Chris@0 114 if (getSummaryType(*i) == PluginSummarisingAdapter::UnknownSummaryType) {
Chris@0 115 cerr << "ERROR: Unknown summary type \"" << *i << "\"" << endl;
Chris@0 116 return false;
Chris@0 117 }
Chris@0 118 }
Chris@0 119 m_summaries = names;
Chris@0 120 m_boundaries = boundaries;
Chris@0 121 return true;
Chris@0 122 }
Chris@0 123
Chris@102 124 void
Chris@102 125 FeatureExtractionManager::setSummariesOnly(bool summariesOnly)
Chris@102 126 {
Chris@102 127 m_summariesOnly = summariesOnly;
Chris@102 128 }
Chris@102 129
Chris@51 130 static PluginInputDomainAdapter::WindowType
Chris@51 131 convertWindowType(WindowType t)
Chris@51 132 {
Chris@51 133 switch (t) {
Chris@51 134 case RectangularWindow:
Chris@51 135 return PluginInputDomainAdapter::RectangularWindow;
Chris@51 136 case BartlettWindow:
Chris@51 137 return PluginInputDomainAdapter::BartlettWindow;
Chris@51 138 case HammingWindow:
Chris@51 139 return PluginInputDomainAdapter::HammingWindow;
Chris@51 140 case HanningWindow:
Chris@51 141 return PluginInputDomainAdapter::HanningWindow;
Chris@51 142 case BlackmanWindow:
Chris@51 143 return PluginInputDomainAdapter::BlackmanWindow;
Chris@51 144 case NuttallWindow:
Chris@51 145 return PluginInputDomainAdapter::NuttallWindow;
Chris@51 146 case BlackmanHarrisWindow:
Chris@51 147 return PluginInputDomainAdapter::BlackmanHarrisWindow;
Chris@138 148 case GaussianWindow:
Chris@138 149 case ParzenWindow:
Chris@138 150 // Not supported in Vamp SDK, fall through
Chris@51 151 default:
Chris@51 152 cerr << "ERROR: Unknown or unsupported window type \"" << t << "\", using Hann (\"" << HanningWindow << "\")" << endl;
Chris@51 153 return PluginInputDomainAdapter::HanningWindow;
Chris@51 154 }
Chris@51 155 }
Chris@51 156
Chris@0 157 bool FeatureExtractionManager::addFeatureExtractor
Chris@0 158 (Transform transform, const vector<FeatureWriter*> &writers)
Chris@0 159 {
Chris@0 160 //!!! exceptions rather than return values?
Chris@0 161
Chris@0 162 if (transform.getSampleRate() == 0) {
Chris@0 163 if (m_sampleRate == 0) {
Chris@0 164 cerr << "NOTE: Transform does not specify a sample rate, using default rate of " << m_defaultSampleRate << endl;
Chris@0 165 transform.setSampleRate(m_defaultSampleRate);
Chris@0 166 m_sampleRate = m_defaultSampleRate;
Chris@0 167 } else {
Chris@0 168 cerr << "NOTE: Transform does not specify a sample rate, using previous transform's rate of " << m_sampleRate << endl;
Chris@0 169 transform.setSampleRate(m_sampleRate);
Chris@0 170 }
Chris@0 171 }
Chris@0 172
Chris@0 173 if (m_sampleRate == 0) {
Chris@0 174 m_sampleRate = transform.getSampleRate();
Chris@0 175 }
Chris@0 176
Chris@0 177 if (transform.getSampleRate() != m_sampleRate) {
Chris@0 178 cerr << "WARNING: Transform sample rate " << transform.getSampleRate() << " does not match previously specified transform rate of " << m_sampleRate << " -- only a single rate is supported for each run" << endl;
Chris@0 179 cerr << "WARNING: Using previous rate of " << m_sampleRate << " for this transform as well" << endl;
Chris@0 180 transform.setSampleRate(m_sampleRate);
Chris@0 181 }
Chris@0 182
Chris@0 183 Plugin *plugin = 0;
Chris@0 184
Chris@0 185 // Remember what the original transform looked like, and index
Chris@0 186 // based on this -- because we may be about to fill in the zeros
Chris@0 187 // for step and block size, but we want any further copies with
Chris@0 188 // the same zeros to match this one
Chris@0 189 Transform originalTransform = transform;
Chris@0 190
Chris@0 191 if (m_transformPluginMap.find(transform) == m_transformPluginMap.end()) {
Chris@0 192
Chris@0 193 // Test whether we already have a transform that is identical
Chris@0 194 // to this, except for the output requested and/or the summary
Chris@0 195 // type -- if so, they should share plugin instances (a vital
Chris@0 196 // optimisation)
Chris@0 197
Chris@0 198 for (TransformPluginMap::iterator i = m_transformPluginMap.begin();
Chris@0 199 i != m_transformPluginMap.end(); ++i) {
Chris@0 200 Transform test = i->first;
Chris@0 201 test.setOutput(transform.getOutput());
Chris@0 202 test.setSummaryType(transform.getSummaryType());
Chris@0 203 if (transform == test) {
Chris@0 204 cerr << "NOTE: Already have transform identical to this one (for \""
Chris@0 205 << transform.getIdentifier().toStdString()
Chris@0 206 << "\") in every detail except output identifier and/or "
Chris@0 207 << "summary type; sharing its plugin instance" << endl;
Chris@0 208 plugin = i->second;
Chris@0 209 if (transform.getSummaryType() != Transform::NoSummary &&
Chris@0 210 !dynamic_cast<PluginSummarisingAdapter *>(plugin)) {
Chris@0 211 plugin = new PluginSummarisingAdapter(plugin);
Chris@0 212 i->second = plugin;
Chris@0 213 }
Chris@0 214 break;
Chris@0 215 }
Chris@0 216 }
Chris@0 217
Chris@0 218 if (!plugin) {
Chris@0 219
Chris@0 220 TransformFactory *tf = TransformFactory::getInstance();
Chris@0 221
Chris@0 222 PluginBase *pb = tf->instantiatePluginFor(transform);
Chris@0 223 plugin = tf->downcastVampPlugin(pb);
Chris@0 224 if (!plugin) {
Chris@0 225 //!!! todo: handle non-Vamp plugins too, or make the main --list
Chris@0 226 // option print out only Vamp transforms
Chris@0 227 cerr << "ERROR: Failed to load plugin for transform \""
Chris@0 228 << transform.getIdentifier().toStdString() << "\"" << endl;
Chris@0 229 delete pb;
Chris@0 230 return false;
Chris@0 231 }
Chris@0 232
Chris@0 233 // We will provide the plugin with arbitrary step and
Chris@0 234 // block sizes (so that we can use the same read/write
Chris@0 235 // block size for all transforms), and to that end we use
Chris@0 236 // a PluginBufferingAdapter. However, we need to know the
Chris@0 237 // underlying step size so that we can provide the right
Chris@0 238 // context for dense outputs. (Although, don't forget
Chris@0 239 // that the PluginBufferingAdapter rewrites
Chris@0 240 // OneSamplePerStep outputs so as to use FixedSampleRate
Chris@0 241 // -- so it supplies the sample rate in the output
Chris@0 242 // feature. I'm not sure whether we can easily use that.)
Chris@0 243
Chris@0 244 size_t pluginStepSize = plugin->getPreferredStepSize();
Chris@0 245 size_t pluginBlockSize = plugin->getPreferredBlockSize();
Chris@0 246
Chris@25 247 PluginInputDomainAdapter *pida = 0;
Chris@25 248
Chris@0 249 // adapt the plugin for buffering, channels, etc.
Chris@0 250 if (plugin->getInputDomain() == Plugin::FrequencyDomain) {
Chris@51 251
Chris@25 252 pida = new PluginInputDomainAdapter(plugin);
Chris@26 253 pida->setProcessTimestampMethod(PluginInputDomainAdapter::ShiftData);
Chris@51 254
Chris@51 255 PluginInputDomainAdapter::WindowType wtype =
Chris@51 256 convertWindowType(transform.getWindowType());
Chris@51 257 pida->setWindowType(wtype);
Chris@25 258 plugin = pida;
Chris@0 259 }
Chris@0 260
Chris@0 261 PluginBufferingAdapter *pba = new PluginBufferingAdapter(plugin);
Chris@0 262 plugin = pba;
Chris@0 263
Chris@0 264 if (transform.getStepSize() != 0) {
Chris@0 265 pba->setPluginStepSize(transform.getStepSize());
Chris@0 266 } else {
Chris@0 267 transform.setStepSize(pluginStepSize);
Chris@0 268 }
Chris@0 269
Chris@0 270 if (transform.getBlockSize() != 0) {
Chris@0 271 pba->setPluginBlockSize(transform.getBlockSize());
Chris@0 272 } else {
Chris@0 273 transform.setBlockSize(pluginBlockSize);
Chris@0 274 }
Chris@0 275
Chris@0 276 plugin = new PluginChannelAdapter(plugin);
Chris@0 277
Chris@0 278 if (!m_summaries.empty() ||
Chris@0 279 transform.getSummaryType() != Transform::NoSummary) {
Chris@0 280 PluginSummarisingAdapter *adapter =
Chris@0 281 new PluginSummarisingAdapter(plugin);
Chris@0 282 adapter->setSummarySegmentBoundaries(m_boundaries);
Chris@0 283 plugin = adapter;
Chris@0 284 }
Chris@0 285
Chris@0 286 if (!plugin->initialise(m_channels, m_blockSize, m_blockSize)) {
Chris@0 287 cerr << "ERROR: Plugin initialise (channels = " << m_channels << ", stepSize = " << m_blockSize << ", blockSize = " << m_blockSize << ") failed." << endl;
Chris@0 288 delete plugin;
Chris@0 289 return false;
Chris@0 290 }
Chris@0 291
Chris@0 292 // cerr << "Initialised plugin" << endl;
Chris@0 293
Chris@0 294 size_t actualStepSize = 0;
Chris@0 295 size_t actualBlockSize = 0;
Chris@0 296 pba->getActualStepAndBlockSizes(actualStepSize, actualBlockSize);
Chris@0 297 transform.setStepSize(actualStepSize);
Chris@0 298 transform.setBlockSize(actualBlockSize);
Chris@0 299
Chris@0 300 Plugin::OutputList outputs = plugin->getOutputDescriptors();
Chris@0 301 for (int i = 0; i < (int)outputs.size(); ++i) {
Chris@0 302
Chris@0 303 // cerr << "Newly initialised plugin output " << i << " has bin count " << outputs[i].binCount << endl;
Chris@0 304
Chris@0 305 m_pluginOutputs[plugin][outputs[i].identifier] = outputs[i];
Chris@0 306 m_pluginOutputIndices[outputs[i].identifier] = i;
Chris@0 307 }
Chris@0 308
Chris@10 309 cerr << "NOTE: Loaded and initialised plugin for transform \""
Chris@25 310 << transform.getIdentifier().toStdString()
Chris@25 311 << "\" with plugin step size " << actualStepSize
Chris@25 312 << " and block size " << actualBlockSize
Chris@25 313 << " (adapter step and block size " << m_blockSize << ")"
Chris@25 314 << endl;
Chris@25 315
Chris@25 316 if (pida) {
Chris@25 317 cerr << "NOTE: PluginInputDomainAdapter timestamp adjustment is "
Chris@25 318
Chris@25 319 << pida->getTimestampAdjustment() << endl;
Chris@25 320 }
Chris@8 321
Chris@8 322 } else {
Chris@8 323
Chris@8 324 if (transform.getStepSize() == 0 || transform.getBlockSize() == 0) {
Chris@8 325
Chris@8 326 PluginWrapper *pw = dynamic_cast<PluginWrapper *>(plugin);
Chris@8 327 if (pw) {
Chris@8 328 PluginBufferingAdapter *pba =
Chris@8 329 pw->getWrapper<PluginBufferingAdapter>();
Chris@8 330 if (pba) {
Chris@8 331 size_t actualStepSize = 0;
Chris@8 332 size_t actualBlockSize = 0;
Chris@8 333 pba->getActualStepAndBlockSizes(actualStepSize,
Chris@8 334 actualBlockSize);
Chris@8 335 if (transform.getStepSize() == 0) {
Chris@8 336 transform.setStepSize(actualStepSize);
Chris@8 337 }
Chris@8 338 if (transform.getBlockSize() == 0) {
Chris@8 339 transform.setBlockSize(actualBlockSize);
Chris@8 340 }
Chris@8 341 }
Chris@8 342 }
Chris@8 343 }
Chris@0 344 }
Chris@0 345
Chris@130 346 if (transform.getPluginVersion() != "") {
Chris@130 347 if (QString("%1").arg(plugin->getPluginVersion())
Chris@130 348 != transform.getPluginVersion()) {
Chris@130 349 cerr << "ERROR: Transform specifies version "
Chris@130 350 << transform.getPluginVersion()
Chris@130 351 << " of plugin \"" << plugin->getIdentifier()
Chris@130 352 << "\", but installed plugin is version "
Chris@130 353 << plugin->getPluginVersion()
Chris@130 354 << endl;
Chris@130 355 return false;
Chris@130 356 }
Chris@130 357 }
Chris@130 358
Chris@0 359 if (transform.getOutput() == "") {
Chris@0 360 transform.setOutput
Chris@0 361 (plugin->getOutputDescriptors()[0].identifier.c_str());
Chris@129 362 } else {
Chris@129 363 if (m_pluginOutputs[plugin].find
Chris@129 364 (transform.getOutput().toLocal8Bit().data()) ==
Chris@129 365 m_pluginOutputs[plugin].end()) {
Chris@129 366 cerr << "ERROR: Transform requests nonexistent plugin output \""
Chris@129 367 << transform.getOutput()
Chris@129 368 << "\"" << endl;
Chris@129 369 return false;
Chris@129 370 }
Chris@0 371 }
Chris@0 372
Chris@0 373 m_transformPluginMap[transform] = plugin;
Chris@0 374
Chris@0 375 if (!(originalTransform == transform)) {
Chris@0 376 m_transformPluginMap[originalTransform] = plugin;
Chris@0 377 }
Chris@0 378
Chris@0 379 } else {
Chris@0 380
Chris@0 381 plugin = m_transformPluginMap[transform];
Chris@0 382 }
Chris@0 383
Chris@109 384 if (m_plugins.find(plugin) == m_plugins.end()) {
Chris@109 385 m_orderedPlugins.push_back(plugin);
Chris@109 386 }
Chris@109 387
Chris@0 388 m_plugins[plugin][transform] = writers;
Chris@0 389
Chris@0 390 return true;
Chris@0 391 }
Chris@0 392
Chris@0 393 bool FeatureExtractionManager::addDefaultFeatureExtractor
Chris@0 394 (TransformId transformId, const vector<FeatureWriter*> &writers)
Chris@0 395 {
Chris@0 396 TransformFactory *tf = TransformFactory::getInstance();
Chris@0 397
Chris@0 398 if (m_sampleRate == 0) {
Chris@0 399 if (m_defaultSampleRate == 0) {
Chris@0 400 cerr << "ERROR: Default transform requested, but no default sample rate available" << endl;
Chris@0 401 return false;
Chris@0 402 } else {
Chris@0 403 cerr << "NOTE: Using default sample rate of " << m_defaultSampleRate << " for default transform" << endl;
Chris@0 404 m_sampleRate = m_defaultSampleRate;
Chris@0 405 }
Chris@0 406 }
Chris@0 407
Chris@0 408 Transform transform = tf->getDefaultTransformFor(transformId, m_sampleRate);
Chris@0 409
Chris@0 410 return addFeatureExtractor(transform, writers);
Chris@0 411 }
Chris@0 412
Chris@0 413 bool FeatureExtractionManager::addFeatureExtractorFromFile
Chris@0 414 (QString transformXmlFile, const vector<FeatureWriter*> &writers)
Chris@0 415 {
Chris@99 416 bool tryRdf = true;
Chris@99 417
Chris@99 418 if (transformXmlFile.endsWith(".xml") || transformXmlFile.endsWith(".XML")) {
Chris@99 419 // We don't support RDF-XML (and nor does the underlying
Chris@99 420 // parser library) so skip the RDF parse if the filename
Chris@99 421 // suggests XML, to avoid puking out a load of errors from
Chris@99 422 // feeding XML to a Turtle parser
Chris@99 423 tryRdf = false;
Chris@0 424 }
Chris@99 425
Chris@99 426 if (tryRdf) {
Chris@99 427 RDFTransformFactory factory
Chris@99 428 (QUrl::fromLocalFile(QFileInfo(transformXmlFile).absoluteFilePath())
Chris@99 429 .toString());
Chris@99 430 ProgressPrinter printer("Parsing transforms RDF file");
Chris@99 431 std::vector<Transform> transforms = factory.getTransforms(&printer);
Chris@99 432 if (!factory.isOK()) {
Chris@99 433 cerr << "WARNING: FeatureExtractionManager::addFeatureExtractorFromFile: Failed to parse transforms file: " << factory.getErrorString().toStdString() << endl;
Chris@99 434 if (factory.isRDF()) {
Chris@99 435 return false; // no point trying it as XML
Chris@0 436 }
Chris@0 437 }
Chris@99 438 if (!transforms.empty()) {
Chris@99 439 bool success = true;
Chris@99 440 for (int i = 0; i < (int)transforms.size(); ++i) {
Chris@99 441 if (!addFeatureExtractor(transforms[i], writers)) {
Chris@99 442 success = false;
Chris@99 443 }
Chris@99 444 }
Chris@99 445 return success;
Chris@99 446 }
Chris@0 447 }
Chris@0 448
Chris@0 449 QFile file(transformXmlFile);
Chris@0 450 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
Chris@0 451 cerr << "ERROR: Failed to open transform XML file \""
Chris@0 452 << transformXmlFile.toStdString() << "\" for reading" << endl;
Chris@0 453 return false;
Chris@0 454 }
Chris@0 455
Chris@0 456 QTextStream *qts = new QTextStream(&file);
Chris@0 457 QString qs = qts->readAll();
Chris@0 458 delete qts;
Chris@0 459 file.close();
Chris@0 460
Chris@0 461 Transform transform(qs);
Chris@0 462
Chris@0 463 return addFeatureExtractor(transform, writers);
Chris@0 464 }
Chris@0 465
Chris@106 466 void FeatureExtractionManager::addSource(QString audioSource, bool willMultiplex)
Chris@0 467 {
Chris@45 468 std::cerr << "Have audio source: \"" << audioSource.toStdString() << "\"" << std::endl;
Chris@45 469
Chris@45 470 // We don't actually do anything with it here, unless it's the
Chris@45 471 // first audio source and we need it to establish default channel
Chris@45 472 // count and sample rate
Chris@45 473
Chris@45 474 if (m_channels == 0 || m_defaultSampleRate == 0) {
Chris@45 475
Chris@45 476 ProgressPrinter retrievalProgress("Determining default rate and channel count from first input file...");
Chris@45 477
Chris@45 478 FileSource source(audioSource, &retrievalProgress);
Chris@45 479 if (!source.isAvailable()) {
Chris@45 480 cerr << "ERROR: File or URL \"" << audioSource.toStdString()
Chris@117 481 << "\" could not be located";
Chris@117 482 if (source.getErrorString() != "") {
Chris@117 483 cerr << ": " << source.getErrorString();
Chris@117 484 }
Chris@117 485 cerr << endl;
Chris@45 486 throw FileNotFound(audioSource);
Chris@45 487 }
Chris@45 488
Chris@45 489 source.waitForData();
Chris@45 490
Chris@45 491 // Open to determine validity, channel count, sample rate only
Chris@45 492 // (then close, and open again later with actual desired rate &c)
Chris@45 493
Chris@45 494 AudioFileReader *reader =
Chris@116 495 AudioFileReaderFactory::createReader(source, 0,
Chris@116 496 m_normalise,
Chris@95 497 &retrievalProgress);
Chris@45 498
Chris@45 499 if (!reader) {
Chris@45 500 throw FailedToOpenFile(audioSource);
Chris@45 501 }
Chris@45 502
Chris@45 503 retrievalProgress.done();
Chris@45 504
Chris@45 505 cerr << "File or URL \"" << audioSource.toStdString() << "\" opened successfully" << endl;
Chris@45 506
Chris@113 507 if (!willMultiplex) {
Chris@106 508 if (m_channels == 0) {
Chris@106 509 m_channels = reader->getChannelCount();
Chris@106 510 cerr << "Taking default channel count of "
Chris@106 511 << reader->getChannelCount() << " from file" << endl;
Chris@106 512 }
Chris@45 513 }
Chris@45 514
Chris@45 515 if (m_defaultSampleRate == 0) {
Chris@45 516 m_defaultSampleRate = reader->getNativeRate();
Chris@45 517 cerr << "Taking default sample rate of "
Chris@45 518 << reader->getNativeRate() << "Hz from file" << endl;
Chris@45 519 cerr << "(Note: Default may be overridden by transforms)" << endl;
Chris@45 520 }
Chris@45 521
Chris@45 522 m_readyReaders[audioSource] = reader;
Chris@45 523 }
Chris@113 524
Chris@113 525 if (willMultiplex) {
Chris@113 526 ++m_channels; // channel count is simply number of sources
Chris@113 527 cerr << "Multiplexing, incremented target channel count to "
Chris@113 528 << m_channels << endl;
Chris@113 529 }
Chris@45 530 }
Chris@45 531
Chris@112 532 void FeatureExtractionManager::extractFeatures(QString audioSource)
Chris@45 533 {
Chris@45 534 if (m_plugins.empty()) return;
Chris@45 535
Chris@45 536 testOutputFiles(audioSource);
Chris@45 537
Chris@0 538 if (m_sampleRate == 0) {
Chris@45 539 throw FileOperationFailed
Chris@45 540 (audioSource, "internal error: have sources and plugins, but no sample rate");
Chris@45 541 }
Chris@45 542 if (m_channels == 0) {
Chris@45 543 throw FileOperationFailed
Chris@45 544 (audioSource, "internal error: have sources and plugins, but no channel count");
Chris@0 545 }
Chris@0 546
Chris@106 547 AudioFileReader *reader = prepareReader(audioSource);
Chris@106 548 extractFeaturesFor(reader, audioSource); // Note this also deletes reader
Chris@106 549 }
Chris@102 550
Chris@106 551 void FeatureExtractionManager::extractFeaturesMultiplexed(QStringList sources)
Chris@106 552 {
Chris@106 553 if (m_plugins.empty() || sources.empty()) return;
Chris@106 554
Chris@106 555 QString nominalSource = sources[0];
Chris@106 556
Chris@106 557 testOutputFiles(nominalSource);
Chris@106 558
Chris@106 559 if (m_sampleRate == 0) {
Chris@106 560 throw FileOperationFailed
Chris@106 561 (nominalSource, "internal error: have sources and plugins, but no sample rate");
Chris@106 562 }
Chris@106 563 if (m_channels == 0) {
Chris@106 564 throw FileOperationFailed
Chris@106 565 (nominalSource, "internal error: have sources and plugins, but no channel count");
Chris@106 566 }
Chris@106 567
Chris@106 568 QList<AudioFileReader *> readers;
Chris@106 569 foreach (QString source, sources) {
Chris@106 570 AudioFileReader *reader = prepareReader(source);
Chris@106 571 readers.push_back(reader);
Chris@106 572 }
Chris@106 573
Chris@106 574 AudioFileReader *reader = new MultiplexedReader(readers);
Chris@106 575 extractFeaturesFor(reader, nominalSource); // Note this also deletes reader
Chris@106 576 }
Chris@106 577
Chris@106 578 AudioFileReader *
Chris@106 579 FeatureExtractionManager::prepareReader(QString source)
Chris@106 580 {
Chris@45 581 AudioFileReader *reader = 0;
Chris@106 582 if (m_readyReaders.contains(source)) {
Chris@106 583 reader = m_readyReaders[source];
Chris@106 584 m_readyReaders.remove(source);
Chris@106 585 if (reader->getSampleRate() != m_sampleRate) {
Chris@45 586 // can't use this; open it again
Chris@45 587 delete reader;
Chris@45 588 reader = 0;
Chris@45 589 }
Chris@45 590 }
Chris@45 591 if (!reader) {
Chris@45 592 ProgressPrinter retrievalProgress("Retrieving audio data...");
Chris@106 593 FileSource fs(source, &retrievalProgress);
Chris@106 594 fs.waitForData();
Chris@116 595 reader = AudioFileReaderFactory::createReader(fs, m_sampleRate,
Chris@116 596 m_normalise,
Chris@116 597 &retrievalProgress);
Chris@45 598 retrievalProgress.done();
Chris@45 599 }
Chris@102 600 if (!reader) {
Chris@107 601 throw FailedToOpenFile(source);
Chris@102 602 }
Chris@106 603 return reader;
Chris@106 604 }
Chris@45 605
Chris@106 606 void
Chris@106 607 FeatureExtractionManager::extractFeaturesFor(AudioFileReader *reader,
Chris@106 608 QString audioSource)
Chris@106 609 {
Chris@106 610 // Note: This also deletes reader
Chris@0 611
Chris@45 612 cerr << "Audio file \"" << audioSource.toStdString() << "\": "
Chris@45 613 << reader->getChannelCount() << "ch at "
Chris@45 614 << reader->getNativeRate() << "Hz" << endl;
Chris@45 615 if (reader->getChannelCount() != m_channels ||
Chris@45 616 reader->getNativeRate() != m_sampleRate) {
Chris@113 617 cerr << "NOTE: File will be mixed or resampled for processing, to: "
Chris@45 618 << m_channels << "ch at "
Chris@45 619 << m_sampleRate << "Hz" << endl;
Chris@45 620 }
Chris@11 621
Chris@0 622 // allocate audio buffers
Chris@0 623 float **data = new float *[m_channels];
Chris@0 624 for (int c = 0; c < m_channels; ++c) {
Chris@0 625 data[c] = new float[m_blockSize];
Chris@0 626 }
Chris@31 627
Chris@31 628 struct LifespanMgr { // unintrusive hack introduced to ensure
Chris@31 629 // destruction on exceptions
Chris@31 630 AudioFileReader *m_r;
Chris@31 631 int m_c;
Chris@31 632 float **m_d;
Chris@31 633 LifespanMgr(AudioFileReader *r, int c, float **d) :
Chris@31 634 m_r(r), m_c(c), m_d(d) { }
Chris@31 635 ~LifespanMgr() { destroy(); }
Chris@31 636 void destroy() {
Chris@31 637 if (!m_r) return;
Chris@31 638 delete m_r;
Chris@31 639 for (int i = 0; i < m_c; ++i) delete[] m_d[i];
Chris@31 640 delete[] m_d;
Chris@31 641 m_r = 0;
Chris@31 642 }
Chris@31 643 };
Chris@31 644 LifespanMgr lifemgr(reader, m_channels, data);
Chris@0 645
Chris@0 646 size_t frameCount = reader->getFrameCount();
Chris@0 647
Chris@0 648 // cerr << "file has " << frameCount << " frames" << endl;
Chris@0 649
Chris@99 650 int earliestStartFrame = 0;
Chris@99 651 int latestEndFrame = frameCount;
Chris@99 652 bool haveExtents = false;
Chris@99 653
Chris@109 654 foreach (Plugin *plugin, m_orderedPlugins) {
Chris@0 655
Chris@109 656 PluginMap::iterator pi = m_plugins.find(plugin);
Chris@0 657
Chris@109 658 std::cerr << "Calling reset on " << plugin << std::endl;
Chris@0 659 plugin->reset();
Chris@0 660
Chris@0 661 for (TransformWriterMap::iterator ti = pi->second.begin();
Chris@0 662 ti != pi->second.end(); ++ti) {
Chris@0 663
Chris@0 664 const Transform &transform = ti->first;
Chris@0 665
Chris@99 666 int startFrame = RealTime::realTime2Frame
Chris@99 667 (transform.getStartTime(), m_sampleRate);
Chris@99 668 int duration = RealTime::realTime2Frame
Chris@99 669 (transform.getDuration(), m_sampleRate);
Chris@99 670 if (duration == 0) {
Chris@99 671 duration = frameCount - startFrame;
Chris@99 672 }
Chris@99 673
Chris@99 674 if (!haveExtents || startFrame < earliestStartFrame) {
Chris@99 675 earliestStartFrame = startFrame;
Chris@99 676 }
Chris@99 677 if (!haveExtents || startFrame + duration > latestEndFrame) {
Chris@99 678 latestEndFrame = startFrame + duration;
Chris@99 679 }
Chris@131 680 /*
Chris@109 681 cerr << "startFrame for transform " << startFrame << endl;
Chris@109 682 cerr << "duration for transform " << duration << endl;
Chris@109 683 cerr << "earliestStartFrame becomes " << earliestStartFrame << endl;
Chris@109 684 cerr << "latestEndFrame becomes " << latestEndFrame << endl;
Chris@131 685 */
Chris@99 686 haveExtents = true;
Chris@0 687
Chris@0 688 string outputId = transform.getOutput().toStdString();
Chris@0 689 if (m_pluginOutputs[plugin].find(outputId) ==
Chris@0 690 m_pluginOutputs[plugin].end()) {
Chris@129 691 // We shouldn't actually reach this point:
Chris@129 692 // addFeatureExtractor tests whether the output exists
Chris@129 693 cerr << "ERROR: Nonexistent plugin output \"" << outputId << "\" requested for transform \""
Chris@0 694 << transform.getIdentifier().toStdString() << "\", ignoring this transform"
Chris@0 695 << endl;
Chris@0 696 /*
Chris@0 697 cerr << "Known outputs for all plugins are as follows:" << endl;
Chris@0 698 for (PluginOutputMap::const_iterator k = m_pluginOutputs.begin();
Chris@0 699 k != m_pluginOutputs.end(); ++k) {
Chris@0 700 cerr << "Plugin " << k->first << ": ";
Chris@0 701 if (k->second.empty()) {
Chris@0 702 cerr << "(none)";
Chris@0 703 }
Chris@0 704 for (OutputMap::const_iterator i = k->second.begin();
Chris@0 705 i != k->second.end(); ++i) {
Chris@0 706 cerr << "\"" << i->first << "\" ";
Chris@0 707 }
Chris@0 708 cerr << endl;
Chris@0 709 }
Chris@0 710 */
Chris@0 711 }
Chris@0 712 }
Chris@0 713 }
Chris@0 714
Chris@99 715 int startFrame = earliestStartFrame;
Chris@99 716 int endFrame = latestEndFrame;
Chris@0 717
Chris@109 718 foreach (Plugin *plugin, m_orderedPlugins) {
Chris@109 719
Chris@109 720 PluginMap::iterator pi = m_plugins.find(plugin);
Chris@0 721
Chris@0 722 for (TransformWriterMap::const_iterator ti = pi->second.begin();
Chris@0 723 ti != pi->second.end(); ++ti) {
Chris@0 724
Chris@0 725 const vector<FeatureWriter *> &writers = ti->second;
Chris@0 726
Chris@0 727 for (int j = 0; j < (int)writers.size(); ++j) {
Chris@0 728 FeatureWriter::TrackMetadata m;
Chris@0 729 m.title = reader->getTitle();
Chris@0 730 m.maker = reader->getMaker();
Chris@19 731 if (m.title != "" && m.maker != "") {
Chris@19 732 writers[j]->setTrackMetadata(audioSource, m);
Chris@19 733 }
Chris@0 734 }
Chris@0 735 }
Chris@0 736 }
Chris@0 737
Chris@0 738 ProgressPrinter extractionProgress("Extracting and writing features...");
Chris@0 739 int progress = 0;
Chris@0 740
Chris@99 741 for (int i = startFrame; i < endFrame; i += m_blockSize) {
Chris@0 742
Chris@0 743 //!!! inefficient, although much of the inefficiency may be
Chris@99 744 // susceptible to compiler optimisation
Chris@0 745
Chris@0 746 SampleBlock frames;
Chris@0 747 reader->getInterleavedFrames(i, m_blockSize, frames);
Chris@0 748
Chris@0 749 // We have to do our own channel handling here; we can't just
Chris@0 750 // leave it to the plugin adapter because the same plugin
Chris@0 751 // adapter may have to serve for input files with various
Chris@0 752 // numbers of channels (so the adapter is simply configured
Chris@34 753 // with a fixed channel count).
Chris@0 754
Chris@0 755 int rc = reader->getChannelCount();
Chris@0 756
Chris@34 757 // m_channels is the number of channels we need for the plugin
Chris@34 758
Chris@34 759 int index;
Chris@34 760 int fc = (int)frames.size();
Chris@46 761
Chris@34 762 if (m_channels == 1) { // only case in which we can sensibly mix down
Chris@34 763 for (int j = 0; j < m_blockSize; ++j) {
Chris@34 764 data[0][j] = 0.f;
Chris@34 765 }
Chris@34 766 for (int c = 0; c < rc; ++c) {
Chris@34 767 for (int j = 0; j < m_blockSize; ++j) {
Chris@0 768 index = j * rc + c;
Chris@34 769 if (index < fc) data[0][j] += frames[index];
Chris@0 770 }
Chris@0 771 }
Chris@34 772 for (int j = 0; j < m_blockSize; ++j) {
Chris@34 773 data[0][j] /= rc;
Chris@34 774 }
Chris@34 775 } else {
Chris@34 776 for (int c = 0; c < m_channels; ++c) {
Chris@34 777 for (int j = 0; j < m_blockSize; ++j) {
Chris@34 778 data[c][j] = 0.f;
Chris@34 779 }
Chris@34 780 if (c < rc) {
Chris@34 781 for (int j = 0; j < m_blockSize; ++j) {
Chris@34 782 index = j * rc + c;
Chris@34 783 if (index < fc) data[c][j] += frames[index];
Chris@34 784 }
Chris@34 785 }
Chris@34 786 }
Chris@34 787 }
Chris@0 788
Chris@0 789 Vamp::RealTime timestamp = Vamp::RealTime::frame2RealTime
Chris@0 790 (i, m_sampleRate);
Chris@0 791
Chris@109 792 foreach (Plugin *plugin, m_orderedPlugins) {
Chris@0 793
Chris@109 794 PluginMap::iterator pi = m_plugins.find(plugin);
Chris@123 795
Chris@123 796 // Skip any plugin none of whose transforms have come
Chris@123 797 // around yet. (Though actually, all transforms for a
Chris@123 798 // given plugin must have the same start time -- they can
Chris@123 799 // only differ in output and summary type.)
Chris@123 800 bool inRange = false;
Chris@123 801 for (TransformWriterMap::const_iterator ti = pi->second.begin();
Chris@123 802 ti != pi->second.end(); ++ti) {
Chris@123 803 int startFrame = RealTime::realTime2Frame
Chris@123 804 (ti->first.getStartTime(), m_sampleRate);
Chris@123 805 if (i >= startFrame || i + m_blockSize > startFrame) {
Chris@123 806 inRange = true;
Chris@123 807 break;
Chris@123 808 }
Chris@123 809 }
Chris@123 810 if (!inRange) {
Chris@123 811 continue;
Chris@123 812 }
Chris@123 813
Chris@0 814 Plugin::FeatureSet featureSet = plugin->process(data, timestamp);
Chris@0 815
Chris@0 816 if (!m_summariesOnly) {
Chris@0 817 writeFeatures(audioSource, plugin, featureSet);
Chris@0 818 }
Chris@0 819 }
Chris@0 820
Chris@0 821 int pp = progress;
Chris@6 822 progress = int(((i - startFrame) * 100.0) / (endFrame - startFrame) + 0.1);
Chris@0 823 if (progress > pp) extractionProgress.setProgress(progress);
Chris@0 824 }
Chris@10 825
Chris@22 826 // std::cerr << "FeatureExtractionManager: deleting audio file reader" << std::endl;
Chris@12 827
Chris@31 828 lifemgr.destroy(); // deletes reader, data
Chris@57 829
Chris@109 830 foreach (Plugin *plugin, m_orderedPlugins) {
Chris@57 831
Chris@109 832 PluginMap::iterator pi = m_plugins.find(plugin);
Chris@0 833 Plugin::FeatureSet featureSet = plugin->getRemainingFeatures();
Chris@0 834
Chris@0 835 if (!m_summariesOnly) {
Chris@0 836 writeFeatures(audioSource, plugin, featureSet);
Chris@0 837 }
Chris@0 838
Chris@0 839 if (!m_summaries.empty()) {
Chris@0 840 PluginSummarisingAdapter *adapter =
Chris@0 841 dynamic_cast<PluginSummarisingAdapter *>(plugin);
Chris@0 842 if (!adapter) {
Chris@0 843 cerr << "WARNING: Summaries requested, but plugin is not a summarising adapter" << endl;
Chris@0 844 } else {
Chris@0 845 for (SummaryNameSet::const_iterator sni = m_summaries.begin();
Chris@0 846 sni != m_summaries.end(); ++sni) {
Chris@0 847 featureSet.clear();
Chris@0 848 //!!! problem here -- we are requesting summaries
Chris@0 849 //!!! for all outputs, but they in principle have
Chris@0 850 //!!! different averaging requirements depending
Chris@0 851 //!!! on whether their features have duration or
Chris@0 852 //!!! not
Chris@0 853 featureSet = adapter->getSummaryForAllOutputs
Chris@0 854 (getSummaryType(*sni),
Chris@0 855 PluginSummarisingAdapter::ContinuousTimeAverage);
Chris@0 856 writeFeatures(audioSource, plugin, featureSet,//!!! *sni);
Chris@0 857 Transform::stringToSummaryType(sni->c_str()));
Chris@0 858 }
Chris@0 859 }
Chris@0 860 }
Chris@0 861
Chris@0 862 writeSummaries(audioSource, plugin);
Chris@0 863 }
Chris@0 864
Chris@3 865 extractionProgress.done();
Chris@3 866
Chris@0 867 finish();
Chris@0 868
Chris@0 869 TempDirectory::getInstance()->cleanup();
Chris@0 870 }
Chris@0 871
Chris@0 872 void
Chris@0 873 FeatureExtractionManager::writeSummaries(QString audioSource, Plugin *plugin)
Chris@0 874 {
Chris@0 875 // caller should have ensured plugin is in m_plugins
Chris@0 876 PluginMap::iterator pi = m_plugins.find(plugin);
Chris@0 877
Chris@0 878 for (TransformWriterMap::const_iterator ti = pi->second.begin();
Chris@0 879 ti != pi->second.end(); ++ti) {
Chris@0 880
Chris@0 881 const Transform &transform = ti->first;
Chris@0 882 const vector<FeatureWriter *> &writers = ti->second;
Chris@0 883
Chris@0 884 Transform::SummaryType summaryType = transform.getSummaryType();
Chris@0 885 PluginSummarisingAdapter::SummaryType pType =
Chris@0 886 (PluginSummarisingAdapter::SummaryType)summaryType;
Chris@0 887
Chris@0 888 if (transform.getSummaryType() == Transform::NoSummary) {
Chris@0 889 continue;
Chris@0 890 }
Chris@0 891
Chris@0 892 PluginSummarisingAdapter *adapter =
Chris@0 893 dynamic_cast<PluginSummarisingAdapter *>(plugin);
Chris@0 894 if (!adapter) {
Chris@0 895 cerr << "FeatureExtractionManager::writeSummaries: INTERNAL ERROR: Summary requested for transform, but plugin is not a summarising adapter" << endl;
Chris@0 896 continue;
Chris@0 897 }
Chris@0 898
Chris@0 899 Plugin::FeatureSet featureSet = adapter->getSummaryForAllOutputs
Chris@0 900 (pType, PluginSummarisingAdapter::ContinuousTimeAverage);
Chris@0 901
Chris@0 902 // cout << "summary type " << int(pType) << " for transform:" << endl << transform.toXmlString().toStdString()<< endl << "... feature set with " << featureSet.size() << " elts" << endl;
Chris@0 903
Chris@0 904 writeFeatures(audioSource, plugin, featureSet, summaryType);
Chris@0 905 }
Chris@0 906 }
Chris@0 907
Chris@0 908 void FeatureExtractionManager::writeFeatures(QString audioSource,
Chris@0 909 Plugin *plugin,
Chris@0 910 const Plugin::FeatureSet &features,
Chris@0 911 Transform::SummaryType summaryType)
Chris@0 912 {
Chris@0 913 // caller should have ensured plugin is in m_plugins
Chris@0 914 PluginMap::iterator pi = m_plugins.find(plugin);
Chris@0 915
Chris@0 916 for (TransformWriterMap::const_iterator ti = pi->second.begin();
Chris@0 917 ti != pi->second.end(); ++ti) {
Chris@0 918
Chris@0 919 const Transform &transform = ti->first;
Chris@0 920 const vector<FeatureWriter *> &writers = ti->second;
Chris@0 921
Chris@0 922 if (transform.getSummaryType() != Transform::NoSummary &&
Chris@0 923 m_summaries.empty() &&
Chris@0 924 summaryType == Transform::NoSummary) {
Chris@0 925 continue;
Chris@0 926 }
Chris@0 927
Chris@0 928 if (transform.getSummaryType() != Transform::NoSummary &&
Chris@0 929 summaryType != Transform::NoSummary &&
Chris@0 930 transform.getSummaryType() != summaryType) {
Chris@0 931 continue;
Chris@0 932 }
Chris@0 933
Chris@0 934 string outputId = transform.getOutput().toStdString();
Chris@0 935
Chris@0 936 if (m_pluginOutputs[plugin].find(outputId) ==
Chris@0 937 m_pluginOutputs[plugin].end()) {
Chris@0 938 continue;
Chris@0 939 }
Chris@0 940
Chris@0 941 const Plugin::OutputDescriptor &desc =
Chris@0 942 m_pluginOutputs[plugin][outputId];
Chris@0 943
Chris@0 944 int outputIndex = m_pluginOutputIndices[outputId];
Chris@0 945 Plugin::FeatureSet::const_iterator fsi = features.find(outputIndex);
Chris@0 946 if (fsi == features.end()) continue;
Chris@0 947
Chris@0 948 for (int j = 0; j < (int)writers.size(); ++j) {
Chris@0 949 writers[j]->write
Chris@0 950 (audioSource, transform, desc, fsi->second,
Chris@0 951 Transform::summaryTypeToString(summaryType).toStdString());
Chris@0 952 }
Chris@0 953 }
Chris@0 954 }
Chris@0 955
Chris@31 956 void FeatureExtractionManager::testOutputFiles(QString audioSource)
Chris@31 957 {
Chris@31 958 for (PluginMap::iterator pi = m_plugins.begin();
Chris@31 959 pi != m_plugins.end(); ++pi) {
Chris@31 960
Chris@31 961 for (TransformWriterMap::iterator ti = pi->second.begin();
Chris@31 962 ti != pi->second.end(); ++ti) {
Chris@31 963
Chris@31 964 vector<FeatureWriter *> &writers = ti->second;
Chris@31 965
Chris@31 966 for (int i = 0; i < (int)writers.size(); ++i) {
Chris@31 967 writers[i]->testOutputFile(audioSource, ti->first.getIdentifier());
Chris@31 968 }
Chris@31 969 }
Chris@31 970 }
Chris@31 971 }
Chris@31 972
Chris@0 973 void FeatureExtractionManager::finish()
Chris@0 974 {
Chris@109 975 foreach (Plugin *plugin, m_orderedPlugins) {
Chris@109 976
Chris@109 977 PluginMap::iterator pi = m_plugins.find(plugin);
Chris@0 978
Chris@0 979 for (TransformWriterMap::iterator ti = pi->second.begin();
Chris@0 980 ti != pi->second.end(); ++ti) {
Chris@0 981
Chris@0 982 vector<FeatureWriter *> &writers = ti->second;
Chris@0 983
Chris@0 984 for (int i = 0; i < (int)writers.size(); ++i) {
Chris@0 985 writers[i]->flush();
Chris@0 986 writers[i]->finish();
Chris@0 987 }
Chris@0 988 }
Chris@0 989 }
Chris@0 990 }
Chris@0 991
Chris@0 992 void FeatureExtractionManager::print(Transform transform) const
Chris@0 993 {
Chris@0 994 QString qs;
Chris@0 995 QTextStream qts(&qs);
Chris@0 996 transform.toXml(qts);
Chris@0 997 cerr << qs.toStdString() << endl;
Chris@0 998 }