view runner/FeatureExtractionManager.cpp @ 395:dec56c3e793b

Add a few more files to .hgignore
author Chris Cannam
date Tue, 09 Jun 2020 17:01:54 +0100
parents 9f7297c47850
children
line wrap: on
line source
/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */

/*
    Sonic Annotator
    A utility for batch feature extraction from audio files.
    Mark Levy, Chris Sutton and Chris Cannam, Queen Mary, University of London.
    Copyright 2007-2008 QMUL.

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License as
    published by the Free Software Foundation; either version 2 of the
    License, or (at your option) any later version.  See the file
    COPYING included with this distribution for more information.
*/

#include "FeatureExtractionManager.h"
#include "MultiplexedReader.h"

#include <vamp-hostsdk/PluginChannelAdapter.h>
#include <vamp-hostsdk/PluginBufferingAdapter.h>
#include <vamp-hostsdk/PluginInputDomainAdapter.h>
#include <vamp-hostsdk/PluginSummarisingAdapter.h>
#include <vamp-hostsdk/PluginWrapper.h>
#include <vamp-hostsdk/PluginLoader.h>

#include "base/Debug.h"
#include "base/Exceptions.h"

#include <iostream>

using namespace std;

using Vamp::Plugin;
using Vamp::PluginBase;
using Vamp::HostExt::PluginLoader;
using Vamp::HostExt::PluginChannelAdapter;
using Vamp::HostExt::PluginBufferingAdapter;
using Vamp::HostExt::PluginInputDomainAdapter;
using Vamp::HostExt::PluginSummarisingAdapter;
using Vamp::HostExt::PluginWrapper;

#include "data/fileio/FileSource.h"
#include "data/fileio/AudioFileReader.h"
#include "data/fileio/AudioFileReaderFactory.h"
#include "base/TempDirectory.h"
#include "base/ProgressPrinter.h"
#include "transform/TransformFactory.h"
#include "rdf/RDFTransformFactory.h"
#include "transform/FeatureWriter.h"

#include <QTextStream>
#include <QFile>
#include <QFileInfo>

FeatureExtractionManager::FeatureExtractionManager(bool verbose) :
    m_verbose(verbose),
    m_summariesOnly(false),
    // We can read using an arbitrary fixed block size --
    // PluginBufferingAdapter handles this for us. But while this
    // doesn't affect the step and block size actually passed to the
    // plugin, it does affect the overall time range of the audio
    // input (which gets rounded to the nearest block boundary). So
    // although a larger blocksize will normally run faster, and we
    // used a blocksize of 16384 in earlier releases of Sonic
    // Annotator for that reason, a smaller blocksize produces
    // "better" results and this is particularly relevant now we
    // support the start and duration flags for a transform.
    m_blockSize(1024),
    m_defaultSampleRate(0),
    m_sampleRate(0),
    m_channels(0),
    m_normalise(false)
{
}

FeatureExtractionManager::~FeatureExtractionManager()
{
    SVDEBUG << "FeatureExtractionManager::~FeatureExtractionManager: cleaning up"
            << endl;
    
    foreach (AudioFileReader *r, m_readyReaders) {
        delete r;
    }

    // We need to ensure m_allLoadedPlugins outlives anything that
    // holds a shared_ptr to a plugin adapter built from one of the
    // raw plugin pointers. So clear these explicitly, in this order,
    // instead of allowing it to happen automatically

    m_pluginOutputs.clear();
    m_transformPluginMap.clear();
    m_orderedPlugins.clear();
    m_plugins.clear();

    // and last
    
    m_allAdapters.clear();
    m_allLoadedPlugins.clear();
    
    SVDEBUG << "FeatureExtractionManager::~FeatureExtractionManager: done" << endl;
}

void FeatureExtractionManager::setChannels(int channels)
{
    m_channels = channels;
}

void FeatureExtractionManager::setDefaultSampleRate(sv_samplerate_t sampleRate)
{
    m_defaultSampleRate = sampleRate;
}

void FeatureExtractionManager::setNormalise(bool normalise)
{
    m_normalise = normalise;
}

static PluginSummarisingAdapter::SummaryType
getSummaryType(string name)
{
    if (name == "min")      return PluginSummarisingAdapter::Minimum;
    if (name == "max")      return PluginSummarisingAdapter::Maximum;
    if (name == "mean")     return PluginSummarisingAdapter::Mean;
    if (name == "median")   return PluginSummarisingAdapter::Median;
    if (name == "mode")     return PluginSummarisingAdapter::Mode;
    if (name == "sum")      return PluginSummarisingAdapter::Sum;
    if (name == "variance") return PluginSummarisingAdapter::Variance;
    if (name == "sd")       return PluginSummarisingAdapter::StandardDeviation;
    if (name == "count")    return PluginSummarisingAdapter::Count;
    return PluginSummarisingAdapter::UnknownSummaryType;
}

bool
FeatureExtractionManager::setSummaryTypes(const set<string> &names,
                                          const PluginSummarisingAdapter::SegmentBoundaries &boundaries)
{
    for (SummaryNameSet::const_iterator i = names.begin();
         i != names.end(); ++i) {
        if (getSummaryType(*i) == PluginSummarisingAdapter::UnknownSummaryType) {
            SVCERR << "ERROR: Unknown summary type \"" << *i << "\"" << endl;
            return false;
        }
    }
    m_summaries = names;
    m_boundaries = boundaries;
    return true;
}

void
FeatureExtractionManager::setSummariesOnly(bool summariesOnly)
{
    m_summariesOnly = summariesOnly;
}

static PluginInputDomainAdapter::WindowType
convertWindowType(WindowType t)
{
    switch (t) {
    case RectangularWindow:
        return PluginInputDomainAdapter::RectangularWindow;
    case BartlettWindow:
        return PluginInputDomainAdapter::BartlettWindow;
    case HammingWindow:
        return PluginInputDomainAdapter::HammingWindow;
    case HanningWindow:
        return PluginInputDomainAdapter::HanningWindow;
    case BlackmanWindow:
        return PluginInputDomainAdapter::BlackmanWindow;
    case NuttallWindow:
        return PluginInputDomainAdapter::NuttallWindow;
    case BlackmanHarrisWindow:
        return PluginInputDomainAdapter::BlackmanHarrisWindow;
    case GaussianWindow:
    case ParzenWindow:
        // Not supported in Vamp SDK, fall through
    default:
        SVCERR << "ERROR: Unknown or unsupported window type \"" << t << "\", using Hann (\"" << HanningWindow << "\")" << endl;
        return PluginInputDomainAdapter::HanningWindow;
    }
}

bool FeatureExtractionManager::addFeatureExtractor
(Transform transform, const vector<FeatureWriter*> &writers)
{
    //!!! exceptions rather than return values?

    if (transform.getSampleRate() == 0) {
        if (m_sampleRate == 0) {
            SVCERR << "NOTE: Transform does not specify a sample rate, using default rate of " << m_defaultSampleRate << endl;
            transform.setSampleRate(m_defaultSampleRate);
            m_sampleRate = m_defaultSampleRate;
        } else {
            SVCERR << "NOTE: Transform does not specify a sample rate, using previous transform's rate of " << m_sampleRate << endl;
            transform.setSampleRate(m_sampleRate);
        }
    }

    if (m_sampleRate == 0) {
        m_sampleRate = transform.getSampleRate();
    }

    if (transform.getSampleRate() != m_sampleRate) {
        SVCERR << "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;
        SVCERR << "WARNING: Using previous rate of " << m_sampleRate << " for this transform as well" << endl;
        transform.setSampleRate(m_sampleRate);
    }

    shared_ptr<Plugin> plugin = nullptr;

    // Remember what the original transform looked like, and index
    // based on this -- because we may be about to fill in the zeros
    // for step and block size, but we want any further copies with
    // the same zeros to match this one
    Transform originalTransform = transform;

    // In a few cases here, after loading the plugin, we create an
    // adapter to wrap it. We always give a raw pointer to the adapter
    // (that's what the API requires). It is safe to use the raw
    // pointer obtained from .get() on the originally loaded
    // shared_ptr, so long as we also stash the shared_ptr somewhere
    // so that it doesn't go out of scope before the adapter is
    // deleted, and we call disownPlugin() on the adapter to prevent
    // the adapter from trying to delete it. We have
    // m_allLoadedPlugins and m_allAdapters as our stashes of
    // shared_ptrs, which share the lifetime of this manager object.
    
    if (m_transformPluginMap.find(transform) == m_transformPluginMap.end()) {

        // Test whether we already have a transform that is identical
        // to this, except for the output requested and/or the summary
        // type -- if so, they should share plugin instances (a vital
        // optimisation)

        for (TransformPluginMap::iterator i = m_transformPluginMap.begin();
             i != m_transformPluginMap.end(); ++i) {
            Transform test = i->first;
            test.setOutput(transform.getOutput());
            test.setSummaryType(transform.getSummaryType());
            if (transform == test) {
                SVCERR << "NOTE: Already have transform identical to this one (for \""
                     << transform.getIdentifier().toStdString()
                     << "\") in every detail except output identifier and/or "
                     << "summary type; sharing its plugin instance" << endl;
                plugin = i->second;
                if (transform.getSummaryType() != Transform::NoSummary &&
                    !std::dynamic_pointer_cast<PluginSummarisingAdapter>(plugin)) {
                    // See comment above about safety of raw pointer here
                    auto psa =
                        make_shared<PluginSummarisingAdapter>(plugin.get());
                    psa->disownPlugin();
                    psa->setSummarySegmentBoundaries(m_boundaries);
                    m_allAdapters.insert(psa);
                    plugin = psa;
                    i->second = plugin;
                }
                break;
            }
        }

        if (!plugin) {

            TransformFactory *tf = TransformFactory::getInstance();

            shared_ptr<PluginBase> pb = tf->instantiatePluginFor(transform);
            plugin = dynamic_pointer_cast<Vamp::Plugin>(pb);
                
            if (!plugin) {
                //!!! todo: handle non-Vamp plugins too, or make the main --list
                // option print out only Vamp transforms
                SVCERR << "ERROR: Failed to load plugin for transform \""
                     << transform.getIdentifier().toStdString() << "\"" << endl;
                if (pb) {
                    SVCERR << "NOTE: (A plugin was loaded, but apparently not a Vamp plugin)" << endl;
                }
                return false;
            }

            m_allLoadedPlugins.insert(pb);
            
            // We will provide the plugin with arbitrary step and
            // block sizes (so that we can use the same read/write
            // block size for all transforms), and to that end we use
            // a PluginBufferingAdapter.  However, we need to know the
            // underlying step size so that we can provide the right
            // context for dense outputs.  (Although, don't forget
            // that the PluginBufferingAdapter rewrites
            // OneSamplePerStep outputs so as to use FixedSampleRate
            // -- so it supplies the sample rate in the output
            // feature.  I'm not sure whether we can easily use that.)

            size_t pluginStepSize = plugin->getPreferredStepSize();
            size_t pluginBlockSize = plugin->getPreferredBlockSize();

            shared_ptr<PluginInputDomainAdapter> pida = nullptr;

            // adapt the plugin for buffering, channels, etc.
            if (plugin->getInputDomain() == Plugin::FrequencyDomain) {

                // See comment up top about safety of raw pointer here
                pida = make_shared<PluginInputDomainAdapter>(plugin.get());
                pida->disownPlugin();
                pida->setProcessTimestampMethod
                    (PluginInputDomainAdapter::ShiftData);

                PluginInputDomainAdapter::WindowType wtype =
                    convertWindowType(transform.getWindowType());
                pida->setWindowType(wtype);

                m_allAdapters.insert(pida);
                plugin = pida;
            }

            auto pba = make_shared<PluginBufferingAdapter>(plugin.get());
            pba->disownPlugin();

            m_allAdapters.insert(pba);
            plugin = pba;

            if (transform.getStepSize() != 0) {
                pba->setPluginStepSize(transform.getStepSize());
            } else {
                transform.setStepSize(int(pluginStepSize));
            }

            if (transform.getBlockSize() != 0) {
                pba->setPluginBlockSize(transform.getBlockSize());
            } else {
                transform.setBlockSize(int(pluginBlockSize));
            }

            auto pca = make_shared<PluginChannelAdapter>(plugin.get());
            pca->disownPlugin();

            m_allAdapters.insert(pca);
            plugin = pca;

            if (!m_summaries.empty() ||
                transform.getSummaryType() != Transform::NoSummary) {
                auto psa = make_shared<PluginSummarisingAdapter>(plugin.get());
                psa->disownPlugin();
                psa->setSummarySegmentBoundaries(m_boundaries);
                m_allAdapters.insert(psa);
                plugin = psa;
            }

            if (!plugin->initialise(m_channels, m_blockSize, m_blockSize)) {
                SVCERR << "ERROR: Plugin initialise (channels = " << m_channels << ", stepSize = " << m_blockSize << ", blockSize = " << m_blockSize << ") failed." << endl;    
                return false;
            }

            SVDEBUG << "Initialised plugin" << endl;

            size_t actualStepSize = 0;
            size_t actualBlockSize = 0;
            pba->getActualStepAndBlockSizes(actualStepSize, actualBlockSize);
            transform.setStepSize(int(actualStepSize));
            transform.setBlockSize(int(actualBlockSize));

            Plugin::OutputList outputs = plugin->getOutputDescriptors();
            for (int i = 0; i < (int)outputs.size(); ++i) {

                SVDEBUG << "Newly initialised plugin output " << i << " has bin count " << outputs[i].binCount << endl;

                m_pluginOutputs[plugin][outputs[i].identifier] = outputs[i];
                m_pluginOutputIndices[outputs[i].identifier] = i;
            }

            SVCERR << "NOTE: Loaded and initialised plugin for transform \""
                 << transform.getIdentifier().toStdString()
                 << "\" with plugin step size " << actualStepSize
                 << " and block size " << actualBlockSize
                 << " (adapter step and block size " << m_blockSize << ")"
                 << endl;

            SVDEBUG << "NOTE: That transform is: " << transform.toXmlString() << endl;
            
            if (pida) {
                SVCERR << "NOTE: PluginInputDomainAdapter timestamp adjustment is "
                     << pida->getTimestampAdjustment() << endl;
            }

        } else {

            if (transform.getStepSize() == 0 || transform.getBlockSize() == 0) {

                auto pw = dynamic_pointer_cast<PluginWrapper>(plugin);
                if (pw) {
                    PluginBufferingAdapter *pba =
                        pw->getWrapper<PluginBufferingAdapter>();
                    if (pba) {
                        size_t actualStepSize = 0;
                        size_t actualBlockSize = 0;
                        pba->getActualStepAndBlockSizes(actualStepSize,
                                                        actualBlockSize);
                        if (transform.getStepSize() == 0) {
                            transform.setStepSize(int(actualStepSize));
                        }
                        if (transform.getBlockSize() == 0) {
                            transform.setBlockSize(int(actualBlockSize));
                        }
                    }
                }
            }
        }

        if (transform.getPluginVersion() != "") {
            if (QString("%1").arg(plugin->getPluginVersion())
                != transform.getPluginVersion()) {
                SVCERR << "ERROR: Transform specifies version "
                     << transform.getPluginVersion()
                     << " of plugin \"" << plugin->getIdentifier()
                     << "\", but installed plugin is version "
                     << plugin->getPluginVersion()
                     << endl;
                return false;
            }
        }

        if (transform.getOutput() == "") {
            transform.setOutput
                (plugin->getOutputDescriptors()[0].identifier.c_str());
        } else {
            if (m_pluginOutputs[plugin].find
                (transform.getOutput().toLocal8Bit().data()) ==
                m_pluginOutputs[plugin].end()) {
                SVCERR << "ERROR: Transform requests nonexistent plugin output \""
                     << transform.getOutput()
                     << "\"" << endl;
                return false;
            }
        }

        m_transformPluginMap[transform] = plugin;

        SVDEBUG << "NOTE: Assigned plugin " << plugin << " for transform: " << transform.toXmlString() << endl;

        if (!(originalTransform == transform)) {
            m_transformPluginMap[originalTransform] = plugin;
            SVDEBUG << "NOTE: Also assigned plugin " << plugin << " for original transform: " << originalTransform.toXmlString() << endl;
        }

    } else {
        
        plugin = m_transformPluginMap[transform];
    }

    if (m_plugins.find(plugin) == m_plugins.end()) {
        m_orderedPlugins.push_back(plugin);
    }

    m_plugins[plugin][transform] = writers;

    return true;
}

bool FeatureExtractionManager::addDefaultFeatureExtractor
(TransformId transformId, const vector<FeatureWriter*> &writers)
{
    TransformFactory *tf = TransformFactory::getInstance();

    if (m_sampleRate == 0) {
        if (m_defaultSampleRate == 0) {
            SVCERR << "ERROR: Default transform requested, but no default sample rate available" << endl;
            return false;
        } else {
            SVCERR << "NOTE: Using default sample rate of " << m_defaultSampleRate << " for default transform" << endl;
            m_sampleRate = m_defaultSampleRate;
        }
    }

    Transform transform = tf->getDefaultTransformFor(transformId, m_sampleRate);

    bool result = addFeatureExtractor(transform, writers);
    if (!result) {
        if (transform.getType() == Transform::UnknownType) {
            SVCERR << "(Maybe mixed up filename with transform, or --transform with --default?)" << endl;
        }
    }
    return result;
}

bool FeatureExtractionManager::addFeatureExtractorFromFile
(QString transformFile, const vector<FeatureWriter*> &writers)
{
    // We support two formats for transform description files, XML (in
    // a format specific to Sonic Annotator) and RDF/Turtle. The RDF
    // format can describe multiple transforms in a single file, the
    // XML only one.
    
    // Possible errors we should report:
    //
    // 1. File does not exist or cannot be opened
    // 2. File is ostensibly XML, but is not parseable
    // 3. File is ostensibly Turtle, but is not parseable
    // 4. File is XML, but contains no valid transform (e.g. is unrelated XML)
    // 5. File is Turtle, but contains no valid transform(s)
    // 6. File is Turtle and contains both valid and invalid transform(s)

    {
        // We don't actually need to open this here yet, we just hoist
        // it to the top for error reporting purposes
        QFile file(transformFile);
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            // Error case 1. File does not exist or cannot be opened
            SVCERR << "ERROR: Failed to open transform file \"" << transformFile
                 << "\" for reading" << endl;
            return false;
        }
    }
    
    bool tryRdf = true;
    if (transformFile.endsWith(".xml") || transformFile.endsWith(".XML")) {
        // We don't support RDF-XML (and nor does the underlying
        // parser library) so skip the RDF parse if the filename
        // suggests XML, to avoid puking out a load of errors from
        // feeding XML to a Turtle parser
        tryRdf = false;
    }

    bool tryXml = true;
    if (transformFile.endsWith(".ttl") || transformFile.endsWith(".TTL") ||
        transformFile.endsWith(".ntriples") || transformFile.endsWith(".NTRIPLES") ||
        transformFile.endsWith(".n3") || transformFile.endsWith(".N3")) {
        tryXml = false;
    }

    QString rdfError, xmlError;
    
    if (tryRdf) {

        RDFTransformFactory factory
            (QUrl::fromLocalFile(QFileInfo(transformFile).absoluteFilePath())
             .toString());
        ProgressPrinter printer("Parsing transforms RDF file");
        std::vector<Transform> transforms = factory.getTransforms
            (m_verbose ? &printer : 0);

        if (factory.isOK()) {
            if (transforms.empty()) {
                SVCERR << "ERROR: Transform file \"" << transformFile
                     << "\" is valid RDF but defines no transforms" << endl;
                return false;
            } else {
                bool success = true;
                for (int i = 0; i < (int)transforms.size(); ++i) {
                    if (!addFeatureExtractor(transforms[i], writers)) {
                        success = false;
                    }
                }
                return success;
            }
        } else { // !factory.isOK()
            if (factory.isRDF()) {
                SVCERR << "ERROR: Invalid transform RDF file \"" << transformFile
                     << "\": " << factory.getErrorString() << endl;
                return false;
            }

            // the not-RDF case: fall through without reporting an
            // error, so we try the file as XML, and if that fails, we
            // print a general unparseable-file error
            rdfError = factory.getErrorString();
        }
    }

    if (tryXml) {
        
        QFile file(transformFile);
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            SVCERR << "ERROR: Failed to open transform file \""
                 << transformFile.toStdString() << "\" for reading" << endl;
            return false;
        }
        
        QTextStream *qts = new QTextStream(&file);
        QString qs = qts->readAll();
        delete qts;
        file.close();
    
        Transform transform(qs);
        xmlError = transform.getErrorString();

        if (xmlError == "") {

            if (transform.getIdentifier() == "") {
                SVCERR << "ERROR: Transform file \"" << transformFile
                     << "\" is valid XML but defines no transform" << endl;
                return false;
            }

            return addFeatureExtractor(transform, writers);
        }
    }

    SVCERR << "ERROR: Transform file \"" << transformFile
         << "\" could not be parsed" << endl;
    if (rdfError != "") {
        SVCERR << "ERROR: RDF parser reported: " << rdfError << endl;
    }
    if (xmlError != "") {
        SVCERR << "ERROR: XML parser reported: " << xmlError << endl;
    }

    return false;
}

void FeatureExtractionManager::addSource(QString audioSource, bool willMultiplex)
{
    SVCERR << "Have audio source: \"" << audioSource.toStdString() << "\"" << endl;

    // We don't actually do anything with it here, unless it's the
    // first audio source and we need it to establish default channel
    // count and sample rate

    if (m_channels == 0 || m_defaultSampleRate == 0) {

        ProgressPrinter retrievalProgress("Retrieving first input file to determine default rate and channel count...");

        FileSource source(audioSource, m_verbose ? &retrievalProgress : 0);
        if (!source.isAvailable()) {
            SVCERR << "ERROR: File or URL \"" << audioSource.toStdString()
                 << "\" could not be located";
            if (source.getErrorString() != "") {
                SVCERR << ": " << source.getErrorString();
            }
            SVCERR << endl;
            throw FileNotFound(audioSource);
        }
    
        source.waitForData();

        // Open to determine validity, channel count, sample rate only
        // (then close, and open again later with actual desired rate &c)

        AudioFileReaderFactory::Parameters params;
        params.normalisation = (m_normalise ?
                                AudioFileReaderFactory::Normalisation::Peak :
                                AudioFileReaderFactory::Normalisation::None);
        
        AudioFileReader *reader =
            AudioFileReaderFactory::createReader
            (source, params, m_verbose ? &retrievalProgress : 0);
    
        if (!reader) {
            throw FailedToOpenFile(audioSource);
        }

        if (m_verbose) retrievalProgress.done();

        SVCERR << "File or URL \"" << audioSource.toStdString() << "\" opened successfully" << endl;

        if (!willMultiplex) {
            if (m_channels == 0) {
                m_channels = reader->getChannelCount();
                SVCERR << "Taking default channel count of "
                     << reader->getChannelCount() << " from audio file" << endl;
            }
        }

        if (m_defaultSampleRate == 0) {
            m_defaultSampleRate = reader->getNativeRate();
            SVCERR << "Taking default sample rate of "
                 << reader->getNativeRate() << "Hz from audio file" << endl;
            SVCERR << "(Note: Default may be overridden by transforms)" << endl;
        }

        m_readyReaders[audioSource] = reader;
    }

    if (willMultiplex) {
        ++m_channels; // channel count is simply number of sources
        SVCERR << "Multiplexing, incremented target channel count to " 
             << m_channels << endl;
    }
}

void FeatureExtractionManager::extractFeatures(QString audioSource)
{
    if (m_plugins.empty()) return;

    testOutputFiles(audioSource);

    if (m_sampleRate == 0) {
        throw FileOperationFailed
            (audioSource, "internal error: have sources and plugins, but no sample rate");
    }
    if (m_channels == 0) {
        throw FileOperationFailed
            (audioSource, "internal error: have sources and plugins, but no channel count");
    }

    AudioFileReader *reader = prepareReader(audioSource);
    extractFeaturesFor(reader, audioSource); // Note this also deletes reader
}

void FeatureExtractionManager::extractFeaturesMultiplexed(QStringList sources)
{
    if (m_plugins.empty() || sources.empty()) return;

    QString nominalSource = sources[0];

    testOutputFiles(nominalSource);

    if (m_sampleRate == 0) {
        throw FileOperationFailed
            (nominalSource, "internal error: have sources and plugins, but no sample rate");
    }
    if (m_channels == 0) {
        throw FileOperationFailed
            (nominalSource, "internal error: have sources and plugins, but no channel count");
    }

    QList<AudioFileReader *> readers;
    foreach (QString source, sources) {
        AudioFileReader *reader = prepareReader(source);
        readers.push_back(reader);
    }

    AudioFileReader *reader = new MultiplexedReader(readers);
    extractFeaturesFor(reader, nominalSource); // Note this also deletes reader
}

AudioFileReader *
FeatureExtractionManager::prepareReader(QString source)
{
    AudioFileReader *reader = 0;
    if (m_readyReaders.contains(source)) {
        reader = m_readyReaders[source];
        m_readyReaders.remove(source);
        if (reader->getSampleRate() != m_sampleRate) {
            // can't use this; open it again
            delete reader;
            reader = 0;
        }
    }

    if (!reader) {
        ProgressPrinter retrievalProgress("Retrieving audio data...");
        FileSource fs(source, m_verbose ? &retrievalProgress : 0);
        fs.waitForData();

        AudioFileReaderFactory::Parameters params;
        params.targetRate = m_sampleRate;
        params.normalisation = (m_normalise ?
                                AudioFileReaderFactory::Normalisation::Peak :
                                AudioFileReaderFactory::Normalisation::None);
        
        reader = AudioFileReaderFactory::createReader
            (fs, params, m_verbose ? &retrievalProgress : 0);
        if (m_verbose) retrievalProgress.done();
    }
    
    if (!reader) {
        throw FailedToOpenFile(source);
    }
    if (reader->getChannelCount() != m_channels ||
        reader->getNativeRate() != m_sampleRate) {
        SVCERR << "NOTE: File will be mixed or resampled for processing, to: "
             << m_channels << "ch at " 
             << m_sampleRate << "Hz" << endl;
    }
    return reader;
}

void
FeatureExtractionManager::extractFeaturesFor(AudioFileReader *reader,
                                             QString audioSource)
{
    // Note: This also deletes reader

    SVCERR << "Audio file \"" << audioSource.toStdString() << "\": "
         << reader->getChannelCount() << "ch at " 
         << reader->getNativeRate() << "Hz" << endl;

    // allocate audio buffers
    float **data = new float *[m_channels];
    for (int c = 0; c < m_channels; ++c) {
        data[c] = new float[m_blockSize];
    }
    
    struct LifespanMgr { // unintrusive hack introduced to ensure
                         // destruction on exceptions
        AudioFileReader *m_r;
        int m_c;
        float **m_d;
        LifespanMgr(AudioFileReader *r, int c, float **d) :
            m_r(r), m_c(c), m_d(d) { }
        ~LifespanMgr() { destroy(); }
        void destroy() {
            if (!m_r) return;
            delete m_r;
            for (int i = 0; i < m_c; ++i) delete[] m_d[i];
            delete[] m_d;
            m_r = 0;
        }
    };
    LifespanMgr lifemgr(reader, m_channels, data);

    sv_frame_t frameCount = reader->getFrameCount();
    
    SVDEBUG << "FeatureExtractionManager: file has " << frameCount << " frames" << endl;

    sv_frame_t earliestStartFrame = 0;
    sv_frame_t latestEndFrame = frameCount;
    bool haveExtents = false;

    for (auto plugin: m_orderedPlugins) {

        PluginMap::iterator pi = m_plugins.find(plugin);

        SVDEBUG << "FeatureExtractionManager: Calling reset on " << plugin << endl;
        plugin->reset();

        for (TransformWriterMap::iterator ti = pi->second.begin();
             ti != pi->second.end(); ++ti) {

            const Transform &transform = ti->first;

            sv_frame_t startFrame = RealTime::realTime2Frame
                (transform.getStartTime(), m_sampleRate);
            sv_frame_t duration = RealTime::realTime2Frame
                (transform.getDuration(), m_sampleRate);
            if (duration == 0) {
                duration = frameCount - startFrame;
            }

            if (!haveExtents || startFrame < earliestStartFrame) {
                earliestStartFrame = startFrame;
            }
            if (!haveExtents || startFrame + duration > latestEndFrame) {
                latestEndFrame = startFrame + duration;
            }

/*
            SVDEBUG << "startFrame for transform " << startFrame << endl;
            SVDEBUG << "duration for transform " << duration << endl;
            SVDEBUG << "earliestStartFrame becomes " << earliestStartFrame << endl;
            SVDEBUG << "latestEndFrame becomes " << latestEndFrame << endl;
*/
            haveExtents = true;

            string outputId = transform.getOutput().toStdString();
            if (m_pluginOutputs[plugin].find(outputId) ==
                m_pluginOutputs[plugin].end()) {
                // We shouldn't actually reach this point:
                // addFeatureExtractor tests whether the output exists
                SVCERR << "ERROR: Nonexistent plugin output \"" << outputId << "\" requested for transform \""
                     << transform.getIdentifier().toStdString() << "\", ignoring this transform"
                     << endl;

                SVDEBUG << "Known outputs for all plugins are as follows:" << endl;
                for (PluginOutputMap::const_iterator k = m_pluginOutputs.begin();
                     k != m_pluginOutputs.end(); ++k) {
                    SVDEBUG << "Plugin " << k->first << ": ";
                    if (k->second.empty()) {
                        SVDEBUG << "(none)";
                    }
                    for (OutputMap::const_iterator i = k->second.begin();
                         i != k->second.end(); ++i) {
                        SVDEBUG << "\"" << i->first << "\" ";
                    }
                    SVDEBUG << endl;
                }
            }
        }
    }
    
    sv_frame_t startFrame = earliestStartFrame;
    sv_frame_t endFrame = latestEndFrame;
    
    for (auto plugin: m_orderedPlugins) {

        PluginMap::iterator pi = m_plugins.find(plugin);

        for (TransformWriterMap::const_iterator ti = pi->second.begin();
             ti != pi->second.end(); ++ti) {
        
            const vector<FeatureWriter *> &writers = ti->second;
            
            for (int j = 0; j < (int)writers.size(); ++j) {
                FeatureWriter::TrackMetadata m;
                m.title = reader->getTitle();
                m.maker = reader->getMaker();
                m.duration = RealTime::frame2RealTime(reader->getFrameCount(),
                                                      reader->getSampleRate());
                writers[j]->setTrackMetadata(audioSource, m);
            }
        }
    }

    ProgressPrinter extractionProgress("Extracting and writing features...");
    int progress = 0;

    for (sv_frame_t i = startFrame; i < endFrame; i += m_blockSize) {
        
        //!!! inefficient, although much of the inefficiency may be
        // susceptible to compiler optimisation
        
        auto frames = reader->getInterleavedFrames(i, m_blockSize);
        
        // We have to do our own channel handling here; we can't just
        // leave it to the plugin adapter because the same plugin
        // adapter may have to serve for input files with various
        // numbers of channels (so the adapter is simply configured
        // with a fixed channel count).

        int rc = reader->getChannelCount();

        // m_channels is the number of channels we need for the plugin

        int index;
        int fc = (int)frames.size();

        if (m_channels == 1) { // only case in which we can sensibly mix down
            for (int j = 0; j < m_blockSize; ++j) {
                data[0][j] = 0.f;
            }
            for (int c = 0; c < rc; ++c) {
                for (int j = 0; j < m_blockSize; ++j) {
                    index = j * rc + c;
                    if (index < fc) data[0][j] += frames[index];
                }
            }
            for (int j = 0; j < m_blockSize; ++j) {
                data[0][j] /= float(rc);
            }
        } else {                
            for (int c = 0; c < m_channels; ++c) {
                for (int j = 0; j < m_blockSize; ++j) {
                    data[c][j] = 0.f;
                }
                if (c < rc) {
                    for (int j = 0; j < m_blockSize; ++j) {
                        index = j * rc + c;
                        if (index < fc) data[c][j] += frames[index];
                    }
                }
            }
        }                

        RealTime timestamp = RealTime::frame2RealTime(i, m_sampleRate);
        
        for (auto plugin: m_orderedPlugins) {

            PluginMap::iterator pi = m_plugins.find(plugin);

            // Skip any plugin none of whose transforms have come
            // around yet. (Though actually, all transforms for a
            // given plugin must have the same start time -- they can
            // only differ in output and summary type.)
            bool inRange = false;
            for (TransformWriterMap::const_iterator ti = pi->second.begin();
                 ti != pi->second.end(); ++ti) {
                sv_frame_t startFrame = RealTime::realTime2Frame
                    (ti->first.getStartTime(), m_sampleRate);
                if (i >= startFrame || i + m_blockSize > startFrame) {
                    inRange = true;
                    break;
                }
            }
            if (!inRange) {
                continue;
            }

            Plugin::FeatureSet featureSet =
                plugin->process(data, timestamp.toVampRealTime());

            if (!m_summariesOnly) {
                writeFeatures(audioSource, plugin, featureSet);
            }
        }

        int pp = progress;
        progress = int((double(i - startFrame) * 100.0) /
                       double(endFrame - startFrame) + 0.1);
        if (progress > pp && m_verbose) extractionProgress.setProgress(progress);
    }

    SVDEBUG << "FeatureExtractionManager: deleting audio file reader" << endl;

    lifemgr.destroy(); // deletes reader, data
        
    for (auto plugin: m_orderedPlugins) {

        Plugin::FeatureSet featureSet = plugin->getRemainingFeatures();

        if (!m_summariesOnly) {
            writeFeatures(audioSource, plugin, featureSet);
        }

        if (!m_summaries.empty()) {
            // Summaries requested on the command line, for all transforms
            auto adapter =
                dynamic_pointer_cast<PluginSummarisingAdapter>(plugin);
            if (!adapter) {
                SVCERR << "WARNING: Summaries requested, but plugin is not a summarising adapter" << endl;
            } else {
                for (SummaryNameSet::const_iterator sni = m_summaries.begin();
                     sni != m_summaries.end(); ++sni) {
                    featureSet.clear();
                    //!!! problem here -- we are requesting summaries
                    //!!! for all outputs, but they in principle have
                    //!!! different averaging requirements depending
                    //!!! on whether their features have duration or
                    //!!! not
                    featureSet = adapter->getSummaryForAllOutputs
                        (getSummaryType(*sni),
                         PluginSummarisingAdapter::ContinuousTimeAverage);
                    writeFeatures(audioSource, plugin, featureSet,
                                  Transform::stringToSummaryType(sni->c_str()));
                }
            }
        }

        // Summaries specified in transform definitions themselves
        writeSummaries(audioSource, plugin);
    }

    if (m_verbose) extractionProgress.done();

    finish();
    
    TempDirectory::getInstance()->cleanup();
}

void
FeatureExtractionManager::writeSummaries(QString audioSource,
                                         shared_ptr<Plugin> plugin)
{
    // caller should have ensured plugin is in m_plugins
    PluginMap::iterator pi = m_plugins.find(plugin);

    for (TransformWriterMap::const_iterator ti = pi->second.begin();
         ti != pi->second.end(); ++ti) {
        
        const Transform &transform = ti->first;

        SVDEBUG << "FeatureExtractionManager::writeSummaries: plugin is " << plugin
                << ", found transform: " << transform.toXmlString() << endl;
        
        Transform::SummaryType summaryType = transform.getSummaryType();
        PluginSummarisingAdapter::SummaryType pType =
            (PluginSummarisingAdapter::SummaryType)summaryType;

        if (transform.getSummaryType() == Transform::NoSummary) {
            SVDEBUG << "FeatureExtractionManager::writeSummaries: no summary for this transform" << endl;
            continue;
        }

        auto adapter = dynamic_pointer_cast<PluginSummarisingAdapter>(plugin);
        if (!adapter) {
            SVCERR << "FeatureExtractionManager::writeSummaries: INTERNAL ERROR: Summary requested for transform, but plugin is not a summarising adapter" << endl;
            continue;
        }

        Plugin::FeatureSet featureSet = adapter->getSummaryForAllOutputs
            (pType, PluginSummarisingAdapter::ContinuousTimeAverage);

        SVDEBUG << "summary type " << int(pType) << " for transform:" << endl << transform.toXmlString().toStdString()<< endl << "... feature set with " << featureSet.size() << " elts" << endl;

        writeFeatures(audioSource, plugin, featureSet, summaryType);
    }
}

void FeatureExtractionManager::writeFeatures(QString audioSource,
                                             shared_ptr<Plugin> plugin,
                                             const Plugin::FeatureSet &features,
                                             Transform::SummaryType summaryType)
{
    // caller should have ensured plugin is in m_plugins
    PluginMap::iterator pi = m_plugins.find(plugin);

    // Write features from the feature set passed in, according to the
    // transforms listed for the given plugin with the given summary type
    
    for (TransformWriterMap::const_iterator ti = pi->second.begin();
         ti != pi->second.end(); ++ti) {
        
        const Transform &transform = ti->first;
        const vector<FeatureWriter *> &writers = ti->second;

//        SVDEBUG << "writeFeatures: plugin " << plugin << " has transform: " << transform.toXmlString() << endl;

        if (transform.getSummaryType() == Transform::NoSummary &&
            !m_summaries.empty()) {
            SVDEBUG << "writeFeatures: transform has no summary, but summaries requested on command line, so going for it anyway" << endl;
        } else if (transform.getSummaryType() != summaryType) {
            // Either we're not writing a summary and the transform
            // has one, or we're writing a summary but the transform
            // has none or a different one; either way, skip it
            SVDEBUG << "writeFeatures: transform summary type " << transform.getSummaryType() << " differs from passed-in one " << summaryType << ", skipping" << endl;
            continue;
        }

        string outputId = transform.getOutput().toStdString();

        if (m_pluginOutputs[plugin].find(outputId) ==
            m_pluginOutputs[plugin].end()) {
            continue;
        }
        
        const Plugin::OutputDescriptor &desc =
            m_pluginOutputs[plugin][outputId];
        
        int outputIndex = m_pluginOutputIndices[outputId];
        Plugin::FeatureSet::const_iterator fsi = features.find(outputIndex);
        if (fsi == features.end()) continue;

//        SVDEBUG << "this transform has " << writers.size() << " writer(s)" << endl;
        
        for (int j = 0; j < (int)writers.size(); ++j) {
            writers[j]->write
                (audioSource, transform, desc, fsi->second,
                 Transform::summaryTypeToString(summaryType).toStdString());
        }
    }
}

void FeatureExtractionManager::testOutputFiles(QString audioSource)
{
    for (PluginMap::iterator pi = m_plugins.begin();
         pi != m_plugins.end(); ++pi) {

        for (TransformWriterMap::iterator ti = pi->second.begin();
             ti != pi->second.end(); ++ti) {
        
            vector<FeatureWriter *> &writers = ti->second;

            for (int i = 0; i < (int)writers.size(); ++i) {
                writers[i]->testOutputFile(audioSource, ti->first.getIdentifier());
            }
        }
    }
}

void FeatureExtractionManager::finish()
{
    for (auto plugin: m_orderedPlugins) {

        PluginMap::iterator pi = m_plugins.find(plugin);

        for (TransformWriterMap::iterator ti = pi->second.begin();
             ti != pi->second.end(); ++ti) {
        
            vector<FeatureWriter *> &writers = ti->second;

            for (int i = 0; i < (int)writers.size(); ++i) {
                writers[i]->flush();
                writers[i]->finish();
            }
        }
    }
}