Chris@31: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@31: /* Chris@31: Vamp Test Plugin Chris@31: Copyright (c) 2013-2016 Queen Mary, University of London Chris@0: Chris@31: Permission is hereby granted, free of charge, to any person Chris@31: obtaining a copy of this software and associated documentation Chris@31: files (the "Software"), to deal in the Software without Chris@31: restriction, including without limitation the rights to use, copy, Chris@31: modify, merge, publish, distribute, sublicense, and/or sell copies Chris@31: of the Software, and to permit persons to whom the Software is Chris@31: furnished to do so, subject to the following conditions: Chris@31: Chris@31: The above copyright notice and this permission notice shall be Chris@31: included in all copies or substantial portions of the Software. Chris@31: Chris@31: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, Chris@31: EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF Chris@31: MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND Chris@31: NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY Chris@31: CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF Chris@31: CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION Chris@31: WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Chris@31: Chris@31: Except as contained in this notice, the names of the Centre for Chris@31: Digital Music and Queen Mary, University of London shall not be Chris@31: used in advertising or otherwise to promote the sale, use or other Chris@31: dealings in this Software without prior written authorization. Chris@31: */ Chris@0: Chris@0: #include "VampTestPlugin.h" Chris@0: Chris@22: #include Chris@22: Chris@19: #include Chris@3: #include Chris@18: #include Chris@3: Chris@19: using namespace std; Chris@3: Chris@3: using Vamp::RealTime; Chris@0: Chris@20: VampTestPlugin::VampTestPlugin(float inputSampleRate, bool freq) : Chris@3: Plugin(inputSampleRate), Chris@20: m_frequencyDomain(freq), Chris@17: m_produceOutput(true), Chris@3: m_n(0), Chris@18: m_channels(1), Chris@3: m_stepSize(0), Chris@3: m_blockSize(0) Chris@0: { Chris@3: for (int i = 0; i < 10; ++i) { Chris@31: m_instants.push_back(RealTime::fromSeconds(1.5 * i)); Chris@3: } Chris@0: } Chris@0: Chris@0: VampTestPlugin::~VampTestPlugin() Chris@0: { Chris@0: } Chris@0: Chris@0: string Chris@0: VampTestPlugin::getIdentifier() const Chris@0: { Chris@20: if (m_frequencyDomain) { Chris@31: return "vamp-test-plugin-freq"; Chris@20: } else { Chris@31: return "vamp-test-plugin"; Chris@20: } Chris@0: } Chris@0: Chris@0: string Chris@0: VampTestPlugin::getName() const Chris@0: { Chris@20: if (m_frequencyDomain) { Chris@31: return "Vamp Test Plugin (Frequency-Domain Input)"; Chris@20: } else { Chris@31: return "Vamp Test Plugin"; Chris@20: } Chris@0: } Chris@0: Chris@0: string Chris@0: VampTestPlugin::getDescription() const Chris@0: { Chris@0: return "Test plugin for hosts handling various output types"; Chris@0: } Chris@0: Chris@0: string Chris@0: VampTestPlugin::getMaker() const Chris@0: { Chris@0: return "Chris Cannam"; Chris@0: } Chris@0: Chris@0: int Chris@0: VampTestPlugin::getPluginVersion() const Chris@0: { Chris@20: return 3; Chris@0: } Chris@0: Chris@0: string Chris@0: VampTestPlugin::getCopyright() const Chris@0: { Chris@0: return "BSD"; Chris@0: } Chris@0: Chris@0: VampTestPlugin::InputDomain Chris@0: VampTestPlugin::getInputDomain() const Chris@0: { Chris@20: return m_frequencyDomain ? FrequencyDomain : TimeDomain; Chris@0: } Chris@0: Chris@0: size_t Chris@0: VampTestPlugin::getPreferredBlockSize() const Chris@0: { Chris@0: return 0; Chris@0: } Chris@0: Chris@0: size_t Chris@0: VampTestPlugin::getPreferredStepSize() const Chris@0: { Chris@0: return 0; Chris@0: } Chris@0: Chris@0: size_t Chris@0: VampTestPlugin::getMinChannelCount() const Chris@0: { Chris@0: return 1; Chris@0: } Chris@0: Chris@0: size_t Chris@0: VampTestPlugin::getMaxChannelCount() const Chris@0: { Chris@18: return 10; Chris@0: } Chris@0: Chris@0: VampTestPlugin::ParameterList Chris@0: VampTestPlugin::getParameterDescriptors() const Chris@0: { Chris@0: ParameterList list; Chris@17: Chris@17: // Provide one parameter, and make it so that we can easily tell Chris@17: // whether it has been changed Chris@17: ParameterDescriptor d; Chris@17: d.identifier = "produce_output"; Chris@17: d.name = "Produce some output"; Chris@17: d.description = "Whether to produce any output. If this parameter is switched off, the plugin will produce no output. This is intended for basic testing of whether a host's parameter setting logic is functioning."; Chris@17: d.unit = ""; Chris@17: d.minValue = 0; Chris@17: d.maxValue = 1; Chris@17: d.defaultValue = 1; Chris@17: d.isQuantized = true; Chris@17: d.quantizeStep = 1; Chris@17: list.push_back(d); Chris@17: Chris@0: return list; Chris@0: } Chris@0: Chris@0: float Chris@0: VampTestPlugin::getParameter(string identifier) const Chris@0: { Chris@17: if (identifier == "produce_output") { Chris@31: return m_produceOutput ? 1.f : 0.f; Chris@17: } Chris@0: return 0; Chris@0: } Chris@0: Chris@0: void Chris@0: VampTestPlugin::setParameter(string identifier, float value) Chris@0: { Chris@17: if (identifier == "produce_output") { Chris@31: m_produceOutput = (value > 0.5); Chris@17: } Chris@0: } Chris@0: Chris@0: VampTestPlugin::ProgramList Chris@0: VampTestPlugin::getPrograms() const Chris@0: { Chris@0: ProgramList list; Chris@0: return list; Chris@0: } Chris@0: Chris@0: string Chris@0: VampTestPlugin::getCurrentProgram() const Chris@0: { Chris@0: return ""; // no programs Chris@0: } Chris@0: Chris@0: void Chris@23: VampTestPlugin::selectProgram(string) Chris@0: { Chris@0: } Chris@0: Chris@0: VampTestPlugin::OutputList Chris@0: VampTestPlugin::getOutputDescriptors() const Chris@0: { Chris@0: OutputList list; Chris@0: Chris@7: int n = 0; Chris@7: Chris@0: OutputDescriptor d; Chris@1: Chris@1: d.identifier = "instants"; Chris@1: d.name = "Instants"; Chris@2: d.description = "Single time points without values"; Chris@1: d.unit = ""; Chris@1: d.hasFixedBinCount = true; Chris@1: d.binCount = 0; Chris@1: d.hasKnownExtents = false; Chris@1: d.isQuantized = false; Chris@1: d.sampleType = OutputDescriptor::VariableSampleRate; Chris@1: d.hasDuration = false; Chris@7: m_outputNumbers[d.identifier] = n++; Chris@1: list.push_back(d); Chris@1: Chris@1: d.identifier = "curve-oss"; Chris@1: d.name = "Curve: OneSamplePerStep"; Chris@2: d.description = "A time series with one value per process block"; Chris@0: d.unit = ""; Chris@0: d.hasFixedBinCount = true; Chris@0: d.binCount = 1; Chris@0: d.hasKnownExtents = false; Chris@0: d.isQuantized = false; Chris@0: d.sampleType = OutputDescriptor::OneSamplePerStep; Chris@0: d.hasDuration = false; Chris@7: m_outputNumbers[d.identifier] = n++; Chris@0: list.push_back(d); Chris@0: Chris@1: d.identifier = "curve-fsr"; Chris@1: d.name = "Curve: FixedSampleRate"; Chris@2: d.description = "A time series with equally-spaced values (independent of process step size)"; Chris@1: d.unit = ""; Chris@1: d.hasFixedBinCount = true; Chris@1: d.binCount = 1; Chris@1: d.hasKnownExtents = false; Chris@1: d.isQuantized = false; Chris@1: d.sampleType = OutputDescriptor::FixedSampleRate; Chris@8: d.sampleRate = 2.5; Chris@1: d.hasDuration = false; Chris@7: m_outputNumbers[d.identifier] = n++; Chris@1: list.push_back(d); Chris@1: Chris@7: d.identifier = "curve-fsr-timed"; Chris@7: d.name = "Curve: FixedSampleRate/Timed"; Chris@7: d.description = "A time series with a fixed sample rate (independent of process step size) but with timestamps on features"; Chris@7: d.unit = ""; Chris@7: d.hasFixedBinCount = true; Chris@7: d.binCount = 1; Chris@7: d.hasKnownExtents = false; Chris@7: d.isQuantized = false; Chris@7: d.sampleType = OutputDescriptor::FixedSampleRate; Chris@8: d.sampleRate = 2.5; Chris@7: d.hasDuration = false; Chris@7: m_outputNumbers[d.identifier] = n++; Chris@7: list.push_back(d); Chris@7: Chris@1: d.identifier = "curve-vsr"; Chris@1: d.name = "Curve: VariableSampleRate"; Chris@2: d.description = "A variably-spaced series of values"; Chris@1: d.unit = ""; Chris@1: d.hasFixedBinCount = true; Chris@1: d.binCount = 1; Chris@1: d.hasKnownExtents = false; Chris@1: d.isQuantized = false; Chris@1: d.sampleType = OutputDescriptor::VariableSampleRate; Chris@2: d.sampleRate = 0; Chris@1: d.hasDuration = false; Chris@7: m_outputNumbers[d.identifier] = n++; Chris@1: list.push_back(d); Chris@1: Chris@2: d.identifier = "grid-oss"; Chris@2: d.name = "Grid: OneSamplePerStep"; Chris@2: d.description = "A fixed-height grid of values with one column per process block"; Chris@2: d.unit = ""; Chris@2: d.hasFixedBinCount = true; Chris@4: d.binCount = 10; Chris@2: d.hasKnownExtents = false; Chris@2: d.isQuantized = false; Chris@4: d.sampleType = OutputDescriptor::OneSamplePerStep; Chris@2: d.sampleRate = 0; Chris@2: d.hasDuration = false; Chris@7: m_outputNumbers[d.identifier] = n++; Chris@2: list.push_back(d); Chris@2: Chris@2: d.identifier = "grid-fsr"; Chris@2: d.name = "Grid: FixedSampleRate"; Chris@2: d.description = "A fixed-height grid of values with equally-spaced columns (independent of process step size)"; Chris@2: d.unit = ""; Chris@2: d.hasFixedBinCount = true; Chris@4: d.binCount = 10; Chris@2: d.hasKnownExtents = false; Chris@2: d.isQuantized = false; Chris@4: d.sampleType = OutputDescriptor::FixedSampleRate; Chris@8: d.sampleRate = 2.5; Chris@2: d.hasDuration = false; Chris@7: m_outputNumbers[d.identifier] = n++; Chris@2: list.push_back(d); Chris@2: Chris@5: d.identifier = "notes-regions"; Chris@5: d.name = "Notes or Regions"; Chris@5: d.description = "Variably-spaced features with one value and duration"; Chris@5: d.unit = ""; Chris@5: d.hasFixedBinCount = true; Chris@5: d.binCount = 1; Chris@5: d.hasKnownExtents = false; Chris@5: d.isQuantized = false; Chris@5: d.sampleType = OutputDescriptor::VariableSampleRate; Chris@5: d.sampleRate = 0; Chris@5: d.hasDuration = true; Chris@7: m_outputNumbers[d.identifier] = n++; Chris@5: list.push_back(d); Chris@2: Chris@19: d.identifier = "input-summary"; Chris@19: d.name = "Data derived from inputs"; Chris@22: d.description = "One-sample-per-step features with n values, where n is the number of input channels. Each feature contains, for each input channel, the first sample value on that channel plus the total number of non-zero samples on that channel. (\"Non-zero\" is determined by comparison against a magnitude threshold which is actually 1e-6 rather than exactly zero.)"; Chris@18: d.unit = ""; Chris@18: d.hasFixedBinCount = true; Chris@18: d.binCount = m_channels; Chris@18: d.hasKnownExtents = false; Chris@18: d.isQuantized = false; Chris@18: d.sampleType = OutputDescriptor::OneSamplePerStep; Chris@18: d.hasDuration = false; Chris@18: m_outputNumbers[d.identifier] = n++; Chris@18: list.push_back(d); Chris@18: Chris@20: d.identifier = "input-timestamp"; Chris@20: d.name = "Input timestamp"; Chris@21: d.description = "One-sample-per-step features with one value, containing the time in sample frames converted from the timestamp of the corresponding process input block."; Chris@21: d.unit = "samples"; Chris@20: d.hasFixedBinCount = true; Chris@20: d.binCount = 1; Chris@20: d.hasKnownExtents = false; Chris@20: d.isQuantized = false; Chris@20: d.sampleType = OutputDescriptor::OneSamplePerStep; Chris@20: d.hasDuration = false; Chris@20: m_outputNumbers[d.identifier] = n++; Chris@20: list.push_back(d); Chris@20: Chris@0: return list; Chris@0: } Chris@0: Chris@0: bool Chris@0: VampTestPlugin::initialise(size_t channels, size_t stepSize, size_t blockSize) Chris@0: { Chris@0: if (channels < getMinChannelCount() || Chris@31: channels > getMaxChannelCount()) return false; Chris@0: Chris@18: m_channels = channels; Chris@3: m_stepSize = stepSize; Chris@3: m_blockSize = blockSize; Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: void Chris@0: VampTestPlugin::reset() Chris@0: { Chris@3: m_n = 0; Chris@3: } Chris@3: Chris@3: static Vamp::Plugin::Feature Chris@3: instant(RealTime r, int i, int n) Chris@3: { Chris@19: stringstream s; Chris@3: Vamp::Plugin::Feature f; Chris@3: f.hasTimestamp = true; Chris@3: f.timestamp = r; Chris@3: f.hasDuration = false; Chris@3: s << i+1 << " of " << n << " at " << r.toText(); Chris@3: f.label = s.str(); Chris@3: return f; Chris@3: } Chris@3: Chris@3: static Vamp::Plugin::Feature Chris@3: untimedCurveValue(RealTime r, int i, int n) Chris@3: { Chris@19: stringstream s; Chris@3: Vamp::Plugin::Feature f; Chris@3: f.hasTimestamp = false; Chris@3: f.hasDuration = false; Chris@3: float v = float(i) / float(n); Chris@3: f.values.push_back(v); Chris@3: s << i+1 << " of " << n << ": " << v << " at " << r.toText(); Chris@3: f.label = s.str(); Chris@3: return f; Chris@0: } Chris@0: Chris@4: static Vamp::Plugin::Feature Chris@4: timedCurveValue(RealTime r, int i, int n) Chris@4: { Chris@19: stringstream s; Chris@4: Vamp::Plugin::Feature f; Chris@4: f.hasTimestamp = true; Chris@4: f.timestamp = r; Chris@4: f.hasDuration = false; Chris@4: float v = float(i) / float(n); Chris@4: f.values.push_back(v); Chris@4: s << i+1 << " of " << n << ": " << v << " at " << r.toText(); Chris@4: f.label = s.str(); Chris@4: return f; Chris@4: } Chris@4: Chris@4: static Vamp::Plugin::Feature Chris@7: snappedCurveValue(RealTime r, RealTime sn, int i, int n) Chris@7: { Chris@19: stringstream s; Chris@7: Vamp::Plugin::Feature f; Chris@7: f.hasTimestamp = true; Chris@7: f.timestamp = r; Chris@7: f.hasDuration = false; Chris@7: float v = float(i) / float(n); Chris@7: f.values.push_back(v); Chris@7: s << i+1 << " of " << n << ": " << v << " at " << r.toText() << " snap to " << sn.toText(); Chris@7: f.label = s.str(); Chris@7: return f; Chris@7: } Chris@7: Chris@7: static Vamp::Plugin::Feature Chris@4: gridColumn(RealTime r, int i, int n) Chris@4: { Chris@19: stringstream s; Chris@4: Vamp::Plugin::Feature f; Chris@4: f.hasTimestamp = false; Chris@4: f.hasDuration = false; Chris@4: for (int j = 0; j < 10; ++j) { Chris@31: float v = float(j + i + 2) / float(n + 10); Chris@31: f.values.push_back(v); Chris@4: } Chris@4: s << i+1 << " of " << n << " at " << r.toText(); Chris@4: f.label = s.str(); Chris@4: return f; Chris@4: } Chris@4: Chris@5: static Vamp::Plugin::Feature Chris@5: noteOrRegion(RealTime r, RealTime d, int i, int n) Chris@5: { Chris@19: stringstream s; Chris@5: Vamp::Plugin::Feature f; Chris@5: f.hasTimestamp = true; Chris@5: f.timestamp = r; Chris@5: f.hasDuration = true; Chris@5: f.duration = d; Chris@5: float v = float(i) / float(n); Chris@5: f.values.push_back(v); Chris@5: s << i+1 << " of " << n << ": " << v << " at " << r.toText() << " dur. " << d.toText(); Chris@5: f.label = s.str(); Chris@5: return f; Chris@5: } Chris@5: Chris@7: static Chris@7: float snap(float x, float r) Chris@7: { Chris@7: int n = int(x / r + 0.5); Chris@7: return n * r; Chris@7: } Chris@7: Chris@5: Vamp::Plugin::FeatureSet Chris@5: VampTestPlugin::featuresFrom(RealTime timestamp, bool final) Chris@0: { Chris@3: FeatureSet fs; Chris@3: Chris@3: RealTime endTime = timestamp + RealTime::frame2RealTime Chris@31: (m_stepSize, m_inputSampleRate); Chris@3: Chris@3: for (int i = 0; i < (int)m_instants.size(); ++i) { Chris@5: Chris@31: if (m_instants[i] >= timestamp && (final || m_instants[i] < endTime)) { Chris@31: fs[m_outputNumbers["instants"]] Chris@31: .push_back(instant(m_instants[i], i, m_instants.size())); Chris@31: } Chris@4: Chris@31: RealTime variCurveTime = m_instants[i] / 2; Chris@31: if (variCurveTime >= timestamp && (final || variCurveTime < endTime)) { Chris@31: fs[m_outputNumbers["curve-vsr"]] Chris@31: .push_back(timedCurveValue(variCurveTime, i, m_instants.size())); Chris@31: } Chris@5: Chris@31: RealTime noteTime = (m_instants[i] + m_instants[i]) / 3; Chris@31: RealTime noteDuration = RealTime::fromSeconds((i % 2 == 0) ? 1.75 : 0.5); Chris@5: Chris@31: if (noteTime >= timestamp && (final || noteTime < endTime)) { Chris@31: fs[m_outputNumbers["notes-regions"]] Chris@31: .push_back(noteOrRegion(noteTime, noteDuration, i, m_instants.size())); Chris@31: } Chris@3: } Chris@3: Chris@5: if (!final) { Chris@3: Chris@31: if (m_n < 20) { Chris@31: fs[m_outputNumbers["curve-oss"]] Chris@31: .push_back(untimedCurveValue(timestamp, m_n, 20)); Chris@31: } Chris@3: Chris@31: if (m_n < 5) { Chris@31: fs[m_outputNumbers["curve-fsr"]] Chris@31: .push_back(untimedCurveValue(RealTime::fromSeconds(m_n / 2.5), m_n, 10)); Chris@6: Chris@31: float s = (m_n / 4) * 2; Chris@31: if ((m_n % 4) > 0) { Chris@31: s += float((m_n % 4) - 1) / 6.0; Chris@31: } Chris@31: fs[m_outputNumbers["curve-fsr-timed"]] Chris@31: .push_back(snappedCurveValue(RealTime::fromSeconds(s), Chris@31: RealTime::fromSeconds(snap(s, 0.4)), Chris@31: m_n, 10)); Chris@31: } Chris@5: Chris@31: if (m_n < 20) { Chris@31: fs[m_outputNumbers["grid-oss"]] Chris@31: .push_back(gridColumn(timestamp, m_n, 20)); Chris@31: } Chris@5: Chris@5: } else { Chris@5: Chris@31: for (int i = (m_n > 5 ? 5 : m_n); i < 10; ++i) { Chris@31: fs[m_outputNumbers["curve-fsr"]] Chris@31: .push_back(untimedCurveValue(RealTime::fromSeconds(i / 2.5), i, 10)); Chris@7: Chris@31: float s = (i / 4) * 2; Chris@31: if ((i % 4) > 0) { Chris@31: s += float((i % 4) - 1) / 6.0; Chris@31: } Chris@31: fs[m_outputNumbers["curve-fsr-timed"]] Chris@31: .push_back(snappedCurveValue(RealTime::fromSeconds(s), Chris@31: RealTime::fromSeconds(snap(s, 0.4)), Chris@31: i, 10)); Chris@31: } Chris@31: Chris@31: for (int i = 0; i < 10; ++i) { Chris@31: fs[m_outputNumbers["grid-fsr"]] Chris@31: .push_back(gridColumn(RealTime::fromSeconds(i / 2.5), i, 10)); Chris@31: } Chris@4: } Chris@4: Chris@3: m_lastTime = endTime; Chris@3: m_n = m_n + 1; Chris@3: return fs; Chris@5: } Chris@5: Chris@5: VampTestPlugin::FeatureSet Chris@5: VampTestPlugin::process(const float *const *inputBuffers, RealTime timestamp) Chris@5: { Chris@17: if (!m_produceOutput) return FeatureSet(); Chris@5: FeatureSet fs = featuresFrom(timestamp, false); Chris@22: Chris@22: Feature f; Chris@22: float eps = 1e-6f; Chris@18: Chris@18: for (int c = 0; c < m_channels; ++c) { Chris@31: if (!m_frequencyDomain) { Chris@31: // first value plus number of non-zero values Chris@31: float sum = inputBuffers[c][0]; Chris@31: for (int i = 0; i < m_blockSize; ++i) { Chris@31: if (fabsf(inputBuffers[c][i]) >= eps) sum += 1; Chris@31: } Chris@31: f.values.push_back(sum); Chris@31: } else { Chris@31: // If we're in frequency-domain mode, we convert back to Chris@31: // time-domain to calculate the input-summary feature Chris@31: // output. That should help the caller check that Chris@31: // time-frequency conversion has gone more or less OK, Chris@31: // though they'll still have to bear in mind windowing and Chris@31: // FFT shift (i.e. phase shift which puts the first Chris@31: // element in the middle of the frame) Chris@31: vector ri(m_blockSize, 0.0); Chris@31: vector ii(m_blockSize, 0.0); Chris@31: vector ro(m_blockSize, 0.0); Chris@31: vector io(m_blockSize, 0.0); Chris@31: for (int i = 0; i <= m_blockSize/2; ++i) { Chris@31: ri[i] = inputBuffers[c][i*2]; Chris@31: ii[i] = inputBuffers[c][i*2 + 1]; Chris@31: if (i > 0) ri[m_blockSize-i] = ri[i]; Chris@31: if (i > 0) ii[m_blockSize-i] = -ii[i]; Chris@31: } Chris@31: Vamp::FFT::inverse(m_blockSize, &ri[0], &ii[0], &ro[0], &io[0]); Chris@31: float sum = 0; Chris@31: for (int i = 0; i < m_blockSize; ++i) { Chris@31: if (fabs(ro[i]) >= eps) sum += 1; Chris@31: } Chris@31: sum += ro[0]; Chris@31: f.values.push_back(sum); Chris@31: } Chris@18: } Chris@22: Chris@19: fs[m_outputNumbers["input-summary"]].push_back(f); Chris@20: Chris@20: f.values.clear(); Chris@22: float frame = RealTime::realTime2Frame(timestamp, m_inputSampleRate); Chris@22: f.values.push_back(frame); Chris@20: fs[m_outputNumbers["input-timestamp"]].push_back(f); Chris@18: Chris@5: return fs; Chris@0: } Chris@0: Chris@0: VampTestPlugin::FeatureSet Chris@0: VampTestPlugin::getRemainingFeatures() Chris@0: { Chris@17: if (!m_produceOutput) return FeatureSet(); Chris@5: FeatureSet fs = featuresFrom(m_lastTime, true); Chris@3: return fs; Chris@0: } Chris@0: