# HG changeset patch # User Chris Cannam # Date 1416244229 0 # Node ID 6370575a812c45bc3ac3586218ede1b73173d798 # Parent ee9f4477f65bbaa76f5bd684138d374b6c82ba27# Parent 36f79bc5c3d751409e3075df278168c5d7880f71 Merge diff -r ee9f4477f65b -r 6370575a812c data/fileio/AudioFileReader.h --- a/data/fileio/AudioFileReader.h Mon Nov 17 17:09:32 2014 +0000 +++ b/data/fileio/AudioFileReader.h Mon Nov 17 17:10:29 2014 +0000 @@ -62,6 +62,16 @@ */ virtual QString getMaker() const { return ""; } + /** + * Return the local file path of the audio data. This is the + * location most likely to contain readable audio data: it may be + * in a different place or format from the originally specified + * location, for example if the file has been retrieved and + * decoded. In some cases there may be no local file path, and + * this will return "" if there is none. + */ + virtual QString getLocalFilename() const { return ""; } + typedef std::map TagMap; virtual TagMap getTags() const { return TagMap(); } @@ -75,7 +85,9 @@ /** * Return interleaved samples for count frames from index start. * The resulting sample block will contain count * - * getChannelCount() samples (or fewer if end of file is reached). + * getChannelCount() samples (or fewer if end of file is + * reached). The caller does not need to allocate space and any + * existing content in the SampleBlock will be erased. * * The subclass implementations of this function must be * thread-safe -- that is, safe to call from multiple threads with diff -r ee9f4477f65b -r 6370575a812c data/fileio/CSVFileReader.cpp --- a/data/fileio/CSVFileReader.cpp Mon Nov 17 17:09:32 2014 +0000 +++ b/data/fileio/CSVFileReader.cpp Mon Nov 17 17:10:29 2014 +0000 @@ -37,42 +37,54 @@ CSVFileReader::CSVFileReader(QString path, CSVFormat format, int mainModelSampleRate) : m_format(format), - m_file(0), + m_device(0), + m_ownDevice(true), m_warnings(0), m_mainModelSampleRate(mainModelSampleRate) { - m_file = new QFile(path); + QFile *file = new QFile(path); bool good = false; - if (!m_file->exists()) { + if (!file->exists()) { m_error = QFile::tr("File \"%1\" does not exist").arg(path); - } else if (!m_file->open(QIODevice::ReadOnly | QIODevice::Text)) { + } else if (!file->open(QIODevice::ReadOnly | QIODevice::Text)) { m_error = QFile::tr("Failed to open file \"%1\"").arg(path); } else { good = true; } - if (!good) { - delete m_file; - m_file = 0; + if (good) { + m_device = file; + } else { + delete file; } } +CSVFileReader::CSVFileReader(QIODevice *device, CSVFormat format, + int mainModelSampleRate) : + m_format(format), + m_device(device), + m_ownDevice(false), + m_warnings(0), + m_mainModelSampleRate(mainModelSampleRate) +{ +} + CSVFileReader::~CSVFileReader() { - SVDEBUG << "CSVFileReader::~CSVFileReader: file is " << m_file << endl; + SVDEBUG << "CSVFileReader::~CSVFileReader: device is " << m_device << endl; - if (m_file) { - SVDEBUG << "CSVFileReader::CSVFileReader: Closing file" << endl; - m_file->close(); + if (m_device && m_ownDevice) { + SVDEBUG << "CSVFileReader::CSVFileReader: Closing device" << endl; + m_device->close(); + delete m_device; } - delete m_file; } bool CSVFileReader::isOK() const { - return (m_file != 0); + return (m_device != 0); } QString @@ -136,7 +148,7 @@ Model * CSVFileReader::load() const { - if (!m_file) return 0; + if (!m_device) return 0; CSVFormat::ModelType modelType = m_format.getModelType(); CSVFormat::TimingType timingType = m_format.getTimingType(); @@ -168,8 +180,7 @@ EditableDenseThreeDimensionalModel *model3 = 0; Model *model = 0; - QTextStream in(m_file); - in.seek(0); + QTextStream in(m_device); unsigned int warnings = 0, warnLimit = 10; unsigned int lineno = 0; @@ -215,7 +226,7 @@ for (int li = 0; li < lines.size(); ++li) { QString line = lines[li]; - + if (line.startsWith("#")) continue; QStringList list = StringBits::split(line, separator, allowQuoting); diff -r ee9f4477f65b -r 6370575a812c data/fileio/CSVFileReader.h --- a/data/fileio/CSVFileReader.h Mon Nov 17 17:09:32 2014 +0000 +++ b/data/fileio/CSVFileReader.h Mon Nov 17 17:10:29 2014 +0000 @@ -28,16 +28,31 @@ class CSVFileReader : public DataFileReader { public: + /** + * Construct a CSVFileReader to read the CSV file at the given + * path, with the given format. + */ CSVFileReader(QString path, CSVFormat format, int mainModelSampleRate); + + /** + * Construct a CSVFileReader to read from the given + * QIODevice. Caller retains ownership of the QIODevice: the + * CSVFileReader will not close or delete it and it must outlive + * the CSVFileReader. + */ + CSVFileReader(QIODevice *device, CSVFormat format, int mainModelSampleRate); + virtual ~CSVFileReader(); virtual bool isOK() const; virtual QString getError() const; + virtual Model *load() const; protected: CSVFormat m_format; - QFile *m_file; + QIODevice *m_device; + bool m_ownDevice; QString m_error; mutable int m_warnings; int m_mainModelSampleRate; diff -r ee9f4477f65b -r 6370575a812c data/fileio/CSVFormat.h --- a/data/fileio/CSVFormat.h Mon Nov 17 17:09:32 2014 +0000 +++ b/data/fileio/CSVFormat.h Mon Nov 17 17:10:29 2014 +0000 @@ -100,8 +100,8 @@ void setTimingType(TimingType t) { m_timingType = t; } void setTimeUnits(TimeUnits t) { m_timeUnits = t; } void setSeparator(QChar s) { m_separator = s; } - void setSampleRate(int r) { m_sampleRate = r; } - void setWindowSize(int s) { m_windowSize = s; } + void setSampleRate(int r) { m_sampleRate = r; } + void setWindowSize(int s) { m_windowSize = s; } void setColumnCount(int c) { m_columnCount = c; } void setAllowQuoting(bool q) { m_allowQuoting = q; } diff -r ee9f4477f65b -r 6370575a812c data/fileio/CodedAudioFileReader.h --- a/data/fileio/CodedAudioFileReader.h Mon Nov 17 17:09:32 2014 +0000 +++ b/data/fileio/CodedAudioFileReader.h Mon Nov 17 17:10:29 2014 +0000 @@ -43,6 +43,8 @@ virtual int getNativeRate() const { return m_fileRate; } + virtual QString getLocalFilename() const { return m_cacheFileName; } + /// Intermediate cache means all CodedAudioFileReaders are quickly seekable virtual bool isQuicklySeekable() const { return true; } diff -r ee9f4477f65b -r 6370575a812c data/fileio/FileSource.cpp --- a/data/fileio/FileSource.cpp Mon Nov 17 17:09:32 2014 +0000 +++ b/data/fileio/FileSource.cpp Mon Nov 17 17:10:29 2014 +0000 @@ -487,6 +487,7 @@ m_done = true; if (m_reply) { QNetworkReply *r = m_reply; + disconnect(r, 0, this, 0); m_reply = 0; // Can only call abort() when there are no errors. if (r->error() == QNetworkReply::NoError) { diff -r ee9f4477f65b -r 6370575a812c data/fileio/MIDIFileWriter.cpp --- a/data/fileio/MIDIFileWriter.cpp Mon Nov 17 17:09:32 2014 +0000 +++ b/data/fileio/MIDIFileWriter.cpp Mon Nov 17 17:10:29 2014 +0000 @@ -317,7 +317,6 @@ m_numberOfTracks = 1; int track = 0; - int midiChannel = 0; MIDIEvent *event; @@ -349,10 +348,14 @@ int duration = i->duration; int pitch = i->midiPitch; int velocity = i->velocity; + int channel = i->channel; if (pitch < 0) pitch = 0; if (pitch > 127) pitch = 127; + if (channel < 0) channel = 0; + if (channel > 15) channel = 0; + // Convert frame to MIDI time double seconds = double(frame) / double(m_sampleRate); @@ -370,13 +373,13 @@ // in place). event = new MIDIEvent(midiTime, - MIDI_NOTE_ON | midiChannel, + MIDI_NOTE_ON | channel, pitch, velocity); m_midiComposition[track].push_back(event); event = new MIDIEvent(endTime, - MIDI_NOTE_OFF | midiChannel, + MIDI_NOTE_OFF | channel, pitch, 127); // loudest silence you can muster diff -r ee9f4477f65b -r 6370575a812c data/fileio/WavFileReader.h --- a/data/fileio/WavFileReader.h Mon Nov 17 17:09:32 2014 +0000 +++ b/data/fileio/WavFileReader.h Mon Nov 17 17:10:29 2014 +0000 @@ -42,6 +42,8 @@ virtual QString getLocation() const { return m_source.getLocation(); } virtual QString getError() const { return m_error; } + virtual QString getLocalFilename() const { return m_path; } + virtual bool isQuicklySeekable() const { return m_seekable; } /** diff -r ee9f4477f65b -r 6370575a812c data/model/AggregateWaveModel.cpp --- a/data/model/AggregateWaveModel.cpp Mon Nov 17 17:09:32 2014 +0000 +++ b/data/model/AggregateWaveModel.cpp Mon Nov 17 17:10:29 2014 +0000 @@ -118,14 +118,21 @@ } } - int sz = count; - + int longest = 0; + for (int c = ch0; c <= ch1; ++c) { - int szHere = + int here = m_components[c].model->getData(m_components[c].channel, start, count, readbuf); - if (szHere < sz) sz = szHere; + if (here > longest) { + longest = here; + } + if (here < count) { + for (int i = here; i < count; ++i) { + readbuf[i] = 0.f; + } + } if (mixing) { for (int i = 0; i < count; ++i) { buffer[i] += readbuf[i]; @@ -134,7 +141,7 @@ } if (mixing) delete[] readbuf; - return sz; + return longest; } int @@ -157,14 +164,21 @@ } } - int sz = count; + int longest = 0; for (int c = ch0; c <= ch1; ++c) { - int szHere = + int here = m_components[c].model->getData(m_components[c].channel, start, count, readbuf); - if (szHere < sz) sz = szHere; + if (here > longest) { + longest = here; + } + if (here < count) { + for (int i = here; i < count; ++i) { + readbuf[i] = 0.; + } + } if (mixing) { for (int i = 0; i < count; ++i) { buffer[i] += readbuf[i]; @@ -173,7 +187,7 @@ } if (mixing) delete[] readbuf; - return sz; + return longest; } int diff -r ee9f4477f65b -r 6370575a812c data/model/AlignmentModel.h --- a/data/model/AlignmentModel.h Mon Nov 17 17:09:32 2014 +0000 +++ b/data/model/AlignmentModel.h Mon Nov 17 17:10:29 2014 +0000 @@ -32,7 +32,7 @@ public: AlignmentModel(Model *reference, Model *aligned, - Model *inputModel, // probably an AggregateWaveModel; I take ownership + Model *inputModel, // probably an AggregateWaveModel; may be null; I take ownership SparseTimeValueModel *path); // I take ownership ~AlignmentModel(); diff -r ee9f4477f65b -r 6370575a812c data/model/NoteData.h --- a/data/model/NoteData.h Mon Nov 17 17:09:32 2014 +0000 +++ b/data/model/NoteData.h Mon Nov 17 17:10:29 2014 +0000 @@ -23,14 +23,15 @@ { NoteData(int _start, int _dur, int _mp, int _vel) : start(_start), duration(_dur), midiPitch(_mp), frequency(0), - isMidiPitchQuantized(true), velocity(_vel) { }; + isMidiPitchQuantized(true), velocity(_vel), channel(0) { }; - int start; // audio sample frame - int duration; // in audio sample frames - int midiPitch; // 0-127 + int start; // audio sample frame + int duration; // in audio sample frames + int midiPitch; // 0-127 float frequency; // Hz, to be used if isMidiPitchQuantized false bool isMidiPitchQuantized; - int velocity; // MIDI-style 0-127 + int velocity; // MIDI-style 0-127 + int channel; // MIDI 0-15 float getFrequency() const { if (isMidiPitchQuantized) { diff -r ee9f4477f65b -r 6370575a812c data/model/WaveFileModel.cpp --- a/data/model/WaveFileModel.cpp Mon Nov 17 17:09:32 2014 +0000 +++ b/data/model/WaveFileModel.cpp Mon Nov 17 17:10:29 2014 +0000 @@ -183,6 +183,13 @@ if (m_reader) return m_reader->getLocation(); return ""; } + +QString +WaveFileModel::getLocalFilename() const +{ + if (m_reader) return m_reader->getLocalFilename(); + return ""; +} int WaveFileModel::getData(int channel, int start, int count, diff -r ee9f4477f65b -r 6370575a812c data/model/WaveFileModel.h --- a/data/model/WaveFileModel.h Mon Nov 17 17:09:32 2014 +0000 +++ b/data/model/WaveFileModel.h Mon Nov 17 17:10:29 2014 +0000 @@ -52,6 +52,8 @@ QString getMaker() const; QString getLocation() const; + QString getLocalFilename() const; + virtual Model *clone() const; float getValueMinimum() const { return -1.0f; } diff -r ee9f4477f65b -r 6370575a812c rdf/RDFFeatureWriter.cpp --- a/rdf/RDFFeatureWriter.cpp Mon Nov 17 17:09:32 2014 +0000 +++ b/rdf/RDFFeatureWriter.cpp Mon Nov 17 17:10:29 2014 +0000 @@ -15,9 +15,6 @@ #include -#include "vamp-hostsdk/PluginHostAdapter.h" -#include "vamp-hostsdk/PluginLoader.h" - #include "base/Exceptions.h" #include "RDFFeatureWriter.h" @@ -36,7 +33,8 @@ RDFFeatureWriter::RDFFeatureWriter() : FileFeatureWriter(SupportOneFilePerTrackTransform | SupportOneFilePerTrack | - SupportOneFileTotal, + SupportOneFileTotal | + SupportStdOut, "n3"), m_plain(false), m_network(false), @@ -49,6 +47,12 @@ { } +string +RDFFeatureWriter::getDescription() const +{ + return "Write output in Audio Features Ontology RDF/Turtle format."; +} + RDFFeatureWriter::ParameterList RDFFeatureWriter::getSupportedParameters() const { @@ -679,29 +683,45 @@ stream << "\n:feature_timeline_" << featureNumber << " a tl:DiscreteTimeLine .\n\n"; - int stepSize = transform.getStepSize(); - if (stepSize == 0) { - cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the step size properly!" << endl; - return; - } + float sampleRate; + int stepSize, blockSize; - int blockSize = transform.getBlockSize(); - if (blockSize == 0) { - cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the block size properly!" << endl; - return; - } + // If the output is FixedSampleRate, we need to draw the + // sample rate and step size from the output descriptor; + // otherwise they come from the transform - float sampleRate = transform.getSampleRate(); - if (sampleRate == 0.f) { - cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the sample rate properly!" << endl; - return; + if (od.sampleType == Plugin::OutputDescriptor::FixedSampleRate) { + + sampleRate = od.sampleRate; + stepSize = 1; + blockSize = 1; + + } else { + + sampleRate = transform.getSampleRate(); + if (sampleRate == 0.f) { + cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the sample rate properly!" << endl; + return; + } + + stepSize = transform.getStepSize(); + if (stepSize == 0) { + cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the step size properly!" << endl; + return; + } + + blockSize = transform.getBlockSize(); + if (blockSize == 0) { + cerr << "RDFFeatureWriter: INTERNAL ERROR: writing dense features without having set the block size properly!" << endl; + return; + } } stream << ":feature_timeline_map_" << featureNumber << " a tl:UniformSamplingWindowingMap ;\n" << " tl:rangeTimeLine :feature_timeline_" << featureNumber << " ;\n" << " tl:domainTimeLine " << timelineURI << " ;\n" - << " tl:sampleRate \"" << int(sampleRate) << "\"^^xsd:int ;\n" + << " tl:sampleRate \"" << sampleRate << "\"^^xsd:float ;\n" << " tl:windowLength \"" << blockSize << "\"^^xsd:int ;\n" << " tl:hopSize \"" << stepSize << "\"^^xsd:int .\n\n"; diff -r ee9f4477f65b -r 6370575a812c rdf/RDFFeatureWriter.h --- a/rdf/RDFFeatureWriter.h Mon Nov 17 17:09:32 2014 +0000 +++ b/rdf/RDFFeatureWriter.h Mon Nov 17 17:10:29 2014 +0000 @@ -44,6 +44,8 @@ RDFFeatureWriter(); virtual ~RDFFeatureWriter(); + virtual string getDescription() const; + virtual ParameterList getSupportedParameters() const; virtual void setParameters(map ¶ms); diff -r ee9f4477f65b -r 6370575a812c rdf/RDFTransformFactory.cpp --- a/rdf/RDFTransformFactory.cpp Mon Nov 17 17:09:32 2014 +0000 +++ b/rdf/RDFTransformFactory.cpp Mon Nov 17 17:10:29 2014 +0000 @@ -212,7 +212,8 @@ "window_type", "sample_rate", "start", - "duration" + "duration", + "plugin_version" }; for (int j = 0; j < int(sizeof(optionals)/sizeof(optionals[0])); ++j) { @@ -249,6 +250,8 @@ if (duration == RealTime::zeroTime) { cerr << "\nRDFTransformFactory: WARNING: Duration is specified as \"" << onode.value << "\" in RDF file,\n but this evaluates to zero when parsed as an xsd:duration datatype.\n The duration property will therefore be ignored.\n To specify start time and duration use the xsd:duration format,\n for example \"PT2.5S\"^^xsd:duration (for 2.5 seconds).\n\n"; } + } else if (optional == "plugin_version") { + transform.setPluginVersion(onode.value); } else { cerr << "RDFTransformFactory: ERROR: Inconsistent optionals lists (unexpected optional \"" << optional << "\"" << endl; } @@ -397,6 +400,11 @@ if (transform.getBlockSize() != 0) { s << " vamp:block_size \"" << transform.getBlockSize() << "\"^^xsd:int ; " << endl; } + if (transform.getWindowType() != HanningWindow) { + s << " vamp:window_type \"" << + Window::getNameForType(transform.getWindowType()).c_str() + << "\" ; " << endl; + } if (transform.getStartTime() != RealTime::zeroTime) { s << " vamp:start \"" << transform.getStartTime().toXsdDuration().c_str() << "\"^^xsd:duration ; " << endl; } @@ -406,6 +414,9 @@ if (transform.getSampleRate() != 0) { s << " vamp:sample_rate \"" << transform.getSampleRate() << "\"^^xsd:float ; " << endl; } + if (transform.getPluginVersion() != "") { + s << " vamp:plugin_version \"\"\"" << transform.getPluginVersion() << "\"\"\" ; " << endl; + } QString program = transform.getProgram(); if (program != "") { diff -r ee9f4477f65b -r 6370575a812c transform/CSVFeatureWriter.cpp --- a/transform/CSVFeatureWriter.cpp Mon Nov 17 17:09:32 2014 +0000 +++ b/transform/CSVFeatureWriter.cpp Mon Nov 17 17:10:29 2014 +0000 @@ -29,10 +29,14 @@ CSVFeatureWriter::CSVFeatureWriter() : FileFeatureWriter(SupportOneFilePerTrackTransform | - SupportOneFileTotal, + SupportOneFileTotal | + SupportStdOut, "csv"), m_separator(","), - m_sampleTiming(false) + m_sampleTiming(false), + m_endTimes(false), + m_forceEnd(false), + m_omitFilename(false) { } @@ -40,6 +44,12 @@ { } +string +CSVFeatureWriter::getDescription() const +{ + return "Write features in comma-separated (CSV) format. If transforms are being written to a single file or to stdout, the first column in the output will contain the input audio filename, or an empty string if the feature hails from the same audio file as its predecessor. If transforms are being written to multiple files, the audio filename column will be omitted. Subsequent columns will contain the feature timestamp, then any or all of duration, values, and label."; +} + CSVFeatureWriter::ParameterList CSVFeatureWriter::getSupportedParameters() const { @@ -51,10 +61,25 @@ p.hasArg = true; pl.push_back(p); + p.name = "omit-filename"; + p.description = "Omit the filename column. May result in confusion if sending more than one audio file's features to the same CSV output."; + p.hasArg = false; + pl.push_back(p); + p.name = "sample-timing"; p.description = "Show timings as sample frame counts instead of in seconds."; p.hasArg = false; pl.push_back(p); + + p.name = "end-times"; + p.description = "Show start and end time instead of start and duration, for features with duration."; + p.hasArg = false; + pl.push_back(p); + + p.name = "fill-ends"; + p.description = "Include durations (or end times) even for features without duration, by using the gap to the next feature instead."; + p.hasArg = false; + pl.push_back(p); return pl; } @@ -70,8 +95,18 @@ cerr << i->first << " -> " << i->second << endl; if (i->first == "separator") { m_separator = i->second.c_str(); + cerr << "m_separator = " << m_separator << endl; + if (m_separator == "\\t") { + m_separator = QChar::Tabulation; + } } else if (i->first == "sample-timing") { m_sampleTiming = true; + } else if (i->first == "end-times") { + m_endTimes = true; + } else if (i->first == "fill-ends") { + m_forceEnd = true; + } else if (i->first == "omit-filename") { + m_omitFilename = true; } } } @@ -83,18 +118,83 @@ const Plugin::FeatureList& features, std::string summaryType) { + TransformId transformId = transform.getIdentifier(); + // Select appropriate output file for our track/transform // combination - QTextStream *sptr = getOutputStream(trackId, transform.getIdentifier()); + QTextStream *sptr = getOutputStream(trackId, transformId); if (!sptr) { - throw FailedToOpenOutputStream(trackId, transform.getIdentifier()); + throw FailedToOpenOutputStream(trackId, transformId); } QTextStream &stream = *sptr; - for (unsigned int i = 0; i < features.size(); ++i) { + int n = features.size(); + if (n == 0) return; + + DataId tt(trackId, transform); + + if (m_pending.find(tt) != m_pending.end()) { + writeFeature(tt, + stream, + m_pending[tt], + &features[0], + m_pendingSummaryTypes[tt]); + m_pending.erase(tt); + m_pendingSummaryTypes.erase(tt); + } + + if (m_forceEnd) { + // can't write final feature until we know its end time + --n; + m_pending[tt] = features[n]; + m_pendingSummaryTypes[tt] = summaryType; + } + + for (int i = 0; i < n; ++i) { + writeFeature(tt, + stream, + features[i], + m_forceEnd ? &features[i+1] : 0, + summaryType); + } +} + +void +CSVFeatureWriter::finish() +{ + for (PendingFeatures::const_iterator i = m_pending.begin(); + i != m_pending.end(); ++i) { + DataId tt = i->first; + Plugin::Feature f = i->second; + QTextStream *sptr = getOutputStream(tt.first, tt.second.getIdentifier()); + if (!sptr) { + throw FailedToOpenOutputStream(tt.first, tt.second.getIdentifier()); + } + QTextStream &stream = *sptr; + // final feature has its own time as end time (we can't + // reliably determine the end of audio file, and because of + // the nature of block processing, the feature could even + // start beyond that anyway) + writeFeature(tt, stream, f, &f, m_pendingSummaryTypes[tt]); + } + + m_pending.clear(); +} + +void +CSVFeatureWriter::writeFeature(DataId tt, + QTextStream &stream, + const Plugin::Feature &f, + const Plugin::Feature *optionalNextFeature, + std::string summaryType) +{ + QString trackId = tt.first; + Transform transform = tt.second; + + if (!m_omitFilename) { if (m_stdout || m_singleFileName != "") { if (trackId != m_prevPrintedTrackId) { stream << "\"" << trackId << "\"" << m_separator; @@ -103,45 +203,68 @@ stream << m_separator; } } + } - if (m_sampleTiming) { + Vamp::RealTime duration; + bool haveDuration = true; + + if (f.hasDuration) { + duration = f.duration; + } else if (optionalNextFeature) { + duration = optionalNextFeature->timestamp - f.timestamp; + } else { + haveDuration = false; + } - stream << Vamp::RealTime::realTime2Frame - (features[i].timestamp, transform.getSampleRate()); + if (m_sampleTiming) { - if (features[i].hasDuration) { - stream << m_separator; + float rate = transform.getSampleRate(); + + stream << Vamp::RealTime::realTime2Frame(f.timestamp, rate); + + if (haveDuration) { + stream << m_separator; + if (m_endTimes) { stream << Vamp::RealTime::realTime2Frame - (features[i].duration, transform.getSampleRate()); + (f.timestamp + duration, rate); + } else { + stream << Vamp::RealTime::realTime2Frame(duration, rate); } - - } else { - - QString timestamp = features[i].timestamp.toString().c_str(); - timestamp.replace(QRegExp("^ +"), ""); - stream << timestamp; - - if (features[i].hasDuration) { - QString duration = features[i].duration.toString().c_str(); - duration.replace(QRegExp("^ +"), ""); - stream << m_separator << duration; - } } - if (summaryType != "") { - stream << m_separator << summaryType.c_str(); - } + } else { - for (unsigned int j = 0; j < features[i].values.size(); ++j) { - stream << m_separator << features[i].values[j]; - } + QString timestamp = f.timestamp.toString().c_str(); + timestamp.replace(QRegExp("^ +"), ""); + stream << timestamp; - if (features[i].label != "") { - stream << m_separator << "\"" << features[i].label.c_str() << "\""; - } + if (haveDuration) { + if (m_endTimes) { + QString endtime = + (f.timestamp + duration).toString().c_str(); + endtime.replace(QRegExp("^ +"), ""); + stream << m_separator << endtime; + } else { + QString d = duration.toString().c_str(); + d.replace(QRegExp("^ +"), ""); + stream << m_separator << d; + } + } + } - stream << "\n"; + if (summaryType != "") { + stream << m_separator << summaryType.c_str(); } + + for (unsigned int j = 0; j < f.values.size(); ++j) { + stream << m_separator << f.values[j]; + } + + if (f.label != "") { + stream << m_separator << "\"" << f.label.c_str() << "\""; + } + + stream << "\n"; } diff -r ee9f4477f65b -r 6370575a812c transform/CSVFeatureWriter.h --- a/transform/CSVFeatureWriter.h Mon Nov 17 17:09:32 2014 +0000 +++ b/transform/CSVFeatureWriter.h Mon Nov 17 17:10:29 2014 +0000 @@ -40,6 +40,8 @@ CSVFeatureWriter(); virtual ~CSVFeatureWriter(); + virtual string getDescription() const; + virtual ParameterList getSupportedParameters() const; virtual void setParameters(map ¶ms); @@ -49,12 +51,29 @@ const Vamp::Plugin::FeatureList &features, std::string summaryType = ""); + virtual void finish(); + virtual QString getWriterTag() const { return "csv"; } private: QString m_separator; bool m_sampleTiming; + bool m_endTimes; + bool m_forceEnd; + bool m_omitFilename; QString m_prevPrintedTrackId; + + typedef pair DataId; // track id, transform + typedef map PendingFeatures; + typedef map PendingSummaryTypes; + PendingFeatures m_pending; + PendingSummaryTypes m_pendingSummaryTypes; + + void writeFeature(DataId, + QTextStream &, + const Vamp::Plugin::Feature &f, + const Vamp::Plugin::Feature *optionalNextFeature, + std::string summaryType); }; #endif diff -r ee9f4477f65b -r 6370575a812c transform/FeatureExtractionModelTransformer.cpp --- a/transform/FeatureExtractionModelTransformer.cpp Mon Nov 17 17:09:32 2014 +0000 +++ b/transform/FeatureExtractionModelTransformer.cpp Mon Nov 17 17:10:29 2014 +0000 @@ -679,11 +679,17 @@ if (frequencyDomain) { for (int ch = 0; ch < channelCount; ++ch) { int column = (blockFrame - startFrame) / stepSize; - fftModels[ch]->getValuesAt(column, reals, imaginaries); - for (int i = 0; i <= blockSize/2; ++i) { - buffers[ch][i*2] = reals[i]; - buffers[ch][i*2+1] = imaginaries[i]; - } + if (fftModels[ch]->getValuesAt(column, reals, imaginaries)) { + for (int i = 0; i <= blockSize/2; ++i) { + buffers[ch][i*2] = reals[i]; + buffers[ch][i*2+1] = imaginaries[i]; + } + } else { + for (int i = 0; i <= blockSize/2; ++i) { + buffers[ch][i*2] = 0.f; + buffers[ch][i*2+1] = 0.f; + } + } error = fftModels[ch]->getError(); if (error != "") { cerr << "FeatureExtractionModelTransformer::run: Abandoning, error is " << error << endl; diff -r ee9f4477f65b -r 6370575a812c transform/FeatureWriter.h --- a/transform/FeatureWriter.h Mon Nov 17 17:09:32 2014 +0000 +++ b/transform/FeatureWriter.h Mon Nov 17 17:10:29 2014 +0000 @@ -39,6 +39,8 @@ public: virtual ~FeatureWriter() { } + virtual string getDescription() const = 0; + struct Parameter { // parameter of the writer, not the plugin string name; string description; @@ -77,8 +79,15 @@ QString m_transformId; }; + /** + * Notify the writer that we are about to start extraction for + * input file N of M (where N is 1..M). May be useful when writing + * multiple outputs into a single file where some syntactic + * element is needed to connect them. + */ + virtual void setNofM(int /* N */, int /* M */) { } + // may throw FailedToOpenFile or other exceptions - virtual void write(QString trackid, const Transform &transform, const Vamp::Plugin::OutputDescriptor &output, diff -r ee9f4477f65b -r 6370575a812c transform/FileFeatureWriter.cpp --- a/transform/FileFeatureWriter.cpp Mon Nov 17 17:09:32 2014 +0000 +++ b/transform/FileFeatureWriter.cpp Mon Nov 17 17:10:29 2014 +0000 @@ -75,14 +75,14 @@ Parameter p; p.name = "basedir"; - p.description = "Base output directory path. (The default is the same directory as the input file.)"; + p.description = "Base output directory path. (The default is the same directory as the input file.) The directory must exist already."; p.hasArg = true; pl.push_back(p); if (m_support & SupportOneFilePerTrackTransform && m_support & SupportOneFilePerTrack) { p.name = "many-files"; - p.description = "Create a separate output file for every combination of input file and transform. The output file names will be based on the input file names. (The default is to create one output file per input audio file, and write all transform results for that input into it.)"; + p.description = "Create a separate output file for every combination of input file and transform. The output file names will be based on the input file names. (The default is to create one output file per input audio file, and write all transform results for that input into it.)"; p.hasArg = false; pl.push_back(p); } @@ -91,13 +91,15 @@ if (m_support & ~SupportOneFileTotal) { // not only option p.name = "one-file"; if (m_support & SupportOneFilePerTrack) { - p.description = "Write all transform results for all input files into the single named output file. (The default is to create one output file per input audio file, and write all transform results for that input into it.)"; + p.description = "Write all transform results for all input files into the single named output file. (The default is to create one output file per input audio file, and write all transform results for that input into it.)"; } else { - p.description = "Write all transform results for all input files into the single named output file. (The default is to create a separate output file for each combination of input audio file and transform.)"; + p.description = "Write all transform results for all input files into the single named output file. (The default is to create a separate output file for each combination of input audio file and transform.)"; } p.hasArg = true; pl.push_back(p); } + } + if (m_support & SupportStdOut) { p.name = "stdout"; p.description = "Write all transform results directly to standard output."; p.hasArg = false; @@ -149,7 +151,7 @@ } } } else if (i->first == "stdout") { - if (m_support & SupportOneFileTotal) { + if (m_support & SupportStdOut) { if (m_singleFileName != "") { SVDEBUG << "FileFeatureWriter::setParameters: WARNING: Both stdout and one-file provided, ignoring stdout" << endl; } else { @@ -165,8 +167,8 @@ } QString -FileFeatureWriter::getOutputFilename(QString trackId, - TransformId transformId) +FileFeatureWriter::createOutputFilename(QString trackId, + TransformId transformId) { if (m_singleFileName != "") { if (QFileInfo(m_singleFileName).exists() && !(m_force || m_append)) { @@ -177,7 +179,9 @@ return m_singleFileName; } - if (m_stdout) return ""; + if (m_stdout) { + return ""; + } QUrl url(trackId, QUrl::StrictMode); QString scheme = url.scheme().toLower(); @@ -235,29 +239,49 @@ // leave it to it if (m_stdout || m_singleFileName != "") return; - QString filename = getOutputFilename(trackId, transformId); + QString filename = createOutputFilename(trackId, transformId); if (filename == "") { throw FailedToOpenOutputStream(trackId, transformId); } } +FileFeatureWriter::TrackTransformPair +FileFeatureWriter::getFilenameKey(QString trackId, + TransformId transformId) +{ + TrackTransformPair key; + + if (m_singleFileName != "") { + key = TrackTransformPair("", ""); + } else if (m_manyFiles) { + key = TrackTransformPair(trackId, transformId); + } else { + key = TrackTransformPair(trackId, ""); + } + + return key; +} + +QString +FileFeatureWriter::getOutputFilename(QString trackId, + TransformId transformId) +{ + TrackTransformPair key = getFilenameKey(trackId, transformId); + if (m_filenames.find(key) == m_filenames.end()) { + m_filenames[key] = createOutputFilename(trackId, transformId); + } + return m_filenames[key]; +} + QFile * FileFeatureWriter::getOutputFile(QString trackId, TransformId transformId) { - pair key; - - if (m_singleFileName != "") { - key = pair("", ""); - } else if (m_manyFiles) { - key = pair(trackId, transformId); - } else { - key = pair(trackId, ""); - } + TrackTransformPair key = getFilenameKey(trackId, transformId); if (m_files.find(key) == m_files.end()) { - QString filename = getOutputFilename(trackId, transformId); + QString filename = createOutputFilename(trackId, transformId); if (filename == "") { // stdout or failure return 0; diff -r ee9f4477f65b -r 6370575a812c transform/FileFeatureWriter.h --- a/transform/FileFeatureWriter.h Mon Nov 17 17:09:32 2014 +0000 +++ b/transform/FileFeatureWriter.h Mon Nov 17 17:10:29 2014 +0000 @@ -50,20 +50,34 @@ enum FileWriteSupport { SupportOneFilePerTrackTransform = 1, SupportOneFilePerTrack = 2, - SupportOneFileTotal = 4 + SupportOneFileTotal = 4, + SupportStdOut = 8 }; FileFeatureWriter(int support, QString extension); QTextStream *getOutputStream(QString, TransformId); typedef pair TrackTransformPair; + typedef map FileNameMap; typedef map FileMap; typedef map FileStreamMap; FileMap m_files; + FileNameMap m_filenames; FileStreamMap m_streams; QTextStream *m_prevstream; + TrackTransformPair getFilenameKey(QString, TransformId); + + // Come up with a suitable output filename for the given track ID - + // transform ID combo. Fail if it already exists, etc. + QString createOutputFilename(QString, TransformId); + + // Look up and return the output filename for the given track ID - + // transform ID combo. QString getOutputFilename(QString, TransformId); + + // Look up and return the output file handle for the given track + // ID - transform ID combo. Return 0 if it could not be opened. QFile *getOutputFile(QString, TransformId); // subclass can implement this to be called before file is opened for append