changeset 185:78212f764251 noteagent

Merge from default branch
author Chris Cannam
date Wed, 28 May 2014 14:56:01 +0100
parents 9b9cdfccbd14 (diff) 59e3cca75b8d (current diff)
children 9d70d687e4eb
files src/Silvet.cpp
diffstat 10 files changed, 1193 insertions(+), 183 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile.inc	Fri May 23 18:17:59 2014 +0100
+++ b/Makefile.inc	Wed May 28 14:56:01 2014 +0100
@@ -19,11 +19,30 @@
 
 PLUGIN	:= silvet$(PLUGIN_EXT)
 
-PLUGIN_HEADERS := $(SRC_DIR)/Silvet.h $(SRC_DIR)/EM.h $(SRC_DIR)/Instruments.h
-PLUGIN_SOURCES := $(SRC_DIR)/Silvet.cpp $(SRC_DIR)/EM.cpp $(SRC_DIR)/Instruments.cpp $(SRC_DIR)/libmain.cpp
+PLUGIN_HEADERS := \
+	$(SRC_DIR)/Silvet.h \
+	$(SRC_DIR)/EM.h \
+	$(SRC_DIR)/Instruments.h \
+	$(SRC_DIR)/AgentFeeder.h \
+	$(SRC_DIR)/AgentFeederMono.h \
+	$(SRC_DIR)/AgentFeederPoly.h \
+	$(SRC_DIR)/AgentHypothesis.h \
+	$(SRC_DIR)/NoteHypothesis.h
 
-BQVEC_HEADERS	:= $(BQVEC_DIR)/Allocators.h $(BQVEC_DIR)/Restrict.h $(BQVEC_DIR)/VectorOps.h
-BQVEC_SOURCES	:= $(BQVEC_DIR)/Allocators.cpp
+PLUGIN_SOURCES := \
+	$(SRC_DIR)/Silvet.cpp \
+	$(SRC_DIR)/EM.cpp \
+	$(SRC_DIR)/Instruments.cpp \
+	$(SRC_DIR)/NoteHypothesis.cpp \
+	$(SRC_DIR)/libmain.cpp
+
+BQVEC_HEADERS	:= \
+	$(BQVEC_DIR)/Allocators.h \
+	$(BQVEC_DIR)/Restrict.h \
+	$(BQVEC_DIR)/VectorOps.h
+
+BQVEC_SOURCES	:= \
+	$(BQVEC_DIR)/Allocators.cpp
 
 HEADERS	     := $(PLUGIN_HEADERS) $(BQVEC_HEADERS)
 SOURCES	     := $(PLUGIN_SOURCES) $(BQVEC_SOURCES)
@@ -48,7 +67,9 @@
 
 # DO NOT DELETE
 
-src/Silvet.o: src/Silvet.h src/MedianFilter.h src/Instruments.h src/EM.h
+src/Silvet.o: src/Silvet.h src/MedianFilter.h src/Instruments.h
+src/Silvet.o: src/NoteHypothesis.h src/AgentHypothesis.h src/EM.h
+src/Silvet.o: src/AgentFeederPoly.h src/AgentFeeder.h
 src/Silvet.o: constant-q-cpp/src/dsp/Resampler.h
 src/EM.o: src/EM.h src/Instruments.h
 src/Instruments.o: src/Instruments.h data/include/templates.h
@@ -58,9 +79,17 @@
 src/Instruments.o: data/include/oboe.h data/include/tenorsax.h
 src/Instruments.o: data/include/violin.h data/include/piano1.h
 src/Instruments.o: data/include/piano2.h data/include/piano3.h
+src/NoteHypothesis.o: src/NoteHypothesis.h src/AgentHypothesis.h
+src/NoteHypothesis.o: src/AgentFeederPoly.h src/AgentFeeder.h
 src/libmain.o: src/Silvet.h src/MedianFilter.h src/Instruments.h
+src/libmain.o: src/NoteHypothesis.h src/AgentHypothesis.h
 bqvec/src/Allocators.o: bqvec/src/Allocators.h bqvec/src/VectorOps.h
 bqvec/src/Allocators.o: bqvec/src/Restrict.h
-src/Silvet.o: src/MedianFilter.h src/Instruments.h
+src/Silvet.o: src/MedianFilter.h src/Instruments.h src/NoteHypothesis.h
+src/Silvet.o: src/AgentHypothesis.h
+src/AgentFeeder.o: src/AgentHypothesis.h
+src/AgentFeederMono.o: src/AgentFeeder.h src/AgentHypothesis.h
+src/AgentFeederPoly.o: src/AgentFeeder.h src/AgentHypothesis.h
+src/NoteHypothesis.o: src/AgentHypothesis.h
 bqvec/src/Allocators.o: bqvec/src/VectorOps.h bqvec/src/Restrict.h
 bqvec/src/VectorOps.o: bqvec/src/Restrict.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/AgentFeeder.h	Wed May 28 14:56:01 2014 +0100
@@ -0,0 +1,31 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Silvet
+
+  A Vamp plugin for note transcription.
+  Centre for Digital Music, Queen Mary University of London.
+  This file Copyright 2012 Chris Cannam.
+    
+  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.
+*/
+
+#ifndef AGENT_FEEDER_H
+#define AGENT_FEEDER_H
+
+#include "AgentHypothesis.h"
+
+class AgentFeeder
+{
+public:
+    virtual void feed(AgentHypothesis::Observation) = 0;
+    virtual void finish() = 0;
+
+    virtual ~AgentFeeder() { }
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/AgentFeederMono.h	Wed May 28 14:56:01 2014 +0100
@@ -0,0 +1,161 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Silvet
+
+  A Vamp plugin for note transcription.
+  Centre for Digital Music, Queen Mary University of London.
+  This file Copyright 2012 Chris Cannam.
+    
+  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.
+*/
+
+#ifndef AGENT_FEEDER_MONO_H
+#define AGENT_FEEDER_MONO_H
+
+#include "AgentFeeder.h"
+
+//#define DEBUG_FEEDER 1
+
+/**
+ * Take a series of observations or estimates (one at a time) and feed
+ * them to a set of agent hypotheses, creating a new candidate agent
+ * for each observation and also testing the observation against the
+ * existing set of hypotheses.
+ *
+ * One satisfied hypothesis is considered to be "accepted" at any
+ * moment (that is, the earliest contemporary hypothesis to have
+ * become satisfied). The series of accepted and completed hypotheses
+ * from construction to the present time can be queried through
+ * getAcceptedHypotheses().
+ *
+ * Call feed() to provide a new observation. Call finish() when all
+ * observations have been provided. The set of hypotheses returned by
+ * getAcceptedHypotheses() will not be complete unless finish() has
+ * been called.
+ */
+template <typename Hypothesis>
+class AgentFeederMono : public AgentFeeder
+{
+public:
+    AgentFeederMono() { }
+
+    typedef std::set<Hypothesis> Hypotheses;
+
+    virtual void feed(AgentHypothesis::Observation o) {
+
+#ifdef DEBUG_FEEDER        
+        std::cerr << "feed: have observation [value = " << o.value << ", time = " << o.time << "]" << std::endl;
+#endif
+        
+        if (!m_current.accept(o)) {
+
+            if (m_current.getState() == Hypothesis::Expired) {
+                m_accepted.insert(m_current);
+#ifdef DEBUG_FEEDER        
+                std::cerr << "current has expired, pushing to accepted" << std::endl;
+#endif
+            }
+
+            bool swallowed = false;
+
+#ifdef DEBUG_FEEDER        
+            std::cerr << "not swallowed by current" << std::endl;
+#endif
+
+            Hypotheses newCandidates;
+
+            for (typename Hypotheses::iterator i = m_candidates.begin();
+                 i != m_candidates.end(); ++i) {
+
+                Hypothesis h = *i;
+                
+                if (swallowed) {
+
+                    // don't offer: each observation can only belong to one
+                    // satisfied hypothesis
+                    newCandidates.insert(h);
+
+                } else {
+
+                    if (h.accept(o)) {
+#ifdef DEBUG_FEEDER        
+                        std::cerr << "accepted, state is " << h.getState() << std::endl;
+#endif
+                        if (h.getState() == Hypothesis::Satisfied) {
+
+                            swallowed = true;
+        
+                            if (m_current.getState() == Hypothesis::Expired ||
+                                m_current.getState() == Hypothesis::Rejected) {
+#ifdef DEBUG_FEEDER        
+                                std::cerr << "current has ended, updating from candidate" << std::endl;
+#endif
+                                m_current = h;
+                            } else {
+                                newCandidates.insert(h);
+                            }
+                            
+                        } else {
+                            newCandidates.insert(h);
+                        }
+                    }
+                }
+            }
+            
+            if (!swallowed) {
+                Hypothesis h;
+                h.accept(o); // must succeed, as h is new
+                newCandidates.insert(h);
+#ifdef DEBUG_FEEDER        
+                std::cerr << "not swallowed, creating new hypothesis" << std::endl;
+#endif
+            }
+
+            // reap rejected/expired hypotheses from candidates list,
+            // and assign back to m_candidates
+
+            m_candidates.clear();
+
+            for (typename Hypotheses::const_iterator i = newCandidates.begin();
+                 i != newCandidates.end(); ++i) {
+                Hypothesis h = *i;
+                if (h.getState() != Hypothesis::Rejected && 
+                    h.getState() != Hypothesis::Expired) {
+                    m_candidates.insert(h);
+                } else {
+#ifdef DEBUG_FEEDER        
+                    std::cerr << "reaping a candidate" << std::endl;
+#endif
+                }
+            }
+        }  
+#ifdef DEBUG_FEEDER        
+        std::cerr << "have " << m_candidates.size() << " candidates" << std::endl;
+#endif
+    }
+
+    virtual void finish() {
+        if (m_current.getState() == Hypothesis::Satisfied) {
+#ifdef DEBUG_FEEDER        
+            std::cerr << "finish: current is satisfied, pushing to accepted" << std::endl;
+#endif
+            m_accepted.insert(m_current);
+        }
+    }
+
+    Hypotheses getAcceptedHypotheses() const {
+        return m_accepted;
+    }
+
+private:
+    Hypotheses m_candidates;
+    Hypothesis m_current;
+    Hypotheses m_accepted;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/AgentFeederPoly.h	Wed May 28 14:56:01 2014 +0100
@@ -0,0 +1,284 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Silvet
+
+  A Vamp plugin for note transcription.
+  Centre for Digital Music, Queen Mary University of London.
+  This file Copyright 2012 Chris Cannam.
+    
+  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.
+*/
+
+#ifndef AGENT_FEEDER_POLY_H
+#define AGENT_FEEDER_POLY_H
+
+#include "AgentFeeder.h"
+
+#include <cassert>
+#include <stdexcept>
+
+#define DEBUG_FEEDER 1
+
+/**
+ * Take a series of observations or estimates (one at a time) and feed
+ * them to a set of agent hypotheses, creating a new candidate agent
+ * for each observation and also testing the observation against the
+ * existing set of hypotheses.
+ *
+ *!!! -- todo: document poly-ness of it
+ *
+ * Call feed() to provide a new observation. Call finish() when all
+ * observations have been provided. The set of hypotheses returned by
+ * getAcceptedHypotheses() will not be complete unless finish() has
+ * been called.
+ */
+template <typename Hypothesis>
+class AgentFeederPoly : public AgentFeeder
+{
+private:
+    typedef std::vector<Hypothesis> Hypotheses;
+
+    struct State {
+        Hypotheses provisional;
+        Hypotheses satisfied;
+        Hypotheses completed;
+    };
+    State m_state;
+
+public:
+    AgentFeederPoly() { }
+
+    virtual void feed(AgentHypothesis::Observation o) {
+#ifdef DEBUG_FEEDER        
+        std::cerr << "\nfeed: have observation [value = " << o.value << ", time = " << o.time << "]" << std::endl;
+#endif
+
+        m_state = update(m_state, o);
+    }
+
+    virtual void finish() {
+#ifdef DEBUG_FEEDER        
+        std::cerr << "finish: satisfied count == " << m_state.satisfied.size() << std::endl;
+#endif
+        for (typename Hypotheses::const_iterator i = m_state.satisfied.begin();
+             i != m_state.satisfied.end(); ++i) {
+            m_state.completed.push_back(*i);
+        }
+    }
+
+    std::set<Hypothesis> getAcceptedHypotheses() const {
+        std::set<Hypothesis> hs;
+        for (typename Hypotheses::const_iterator i = m_state.completed.begin();
+             i != m_state.completed.end(); ++i) {
+            hs.insert(*i);
+        }
+        return hs;
+    }
+
+private:
+    State update(State s, AgentHypothesis::Observation o) {
+
+        /*
+          An observation can "belong" to any number of provisional
+          hypotheses, but only to one satisfied hypothesis.
+
+          A new observation is first offered to the hypotheses that
+          have already been satisfied. If one of these accepts it, it
+          gets to keep it and no other hypothesis can have it.
+         
+          Any observation not accepted by a hypothesis in satisfied
+          state is then offered to the provisional hypotheses; any
+          number of these may accept it. Also, every observation that
+          belongs to no satisfied hypothesis is used as the first
+          observation in its own new hypothesis (regardless of how
+          many other provisional hypotheses have accepted it).
+
+          When a hypothesis subsequently becomes satisfied, all other
+          provisional hypotheses containing any of its observations
+          must be discarded.
+        */
+
+        State newState;
+
+        // We only ever add to the completed hypotheses, never remove
+        // anything from them. But we may remove from provisional (if
+        // rejected or transferred to satisfied) and satisfied (when
+        // completed).
+        
+        newState.completed = s.completed;
+        
+        bool swallowed = false;
+
+        for (typename Hypotheses::iterator i = s.satisfied.begin();
+             i != s.satisfied.end(); ++i) {
+
+            Hypothesis h = *i;
+
+            assert(h.getState() == Hypothesis::Satisfied);
+
+            if (swallowed) {
+
+                // An observation that has already been accepted by a
+                // hypothesis cannot be offered to any other, because
+                // it can only belong to one satisfied hypothesis. Any
+                // subsequent satisfied hypotheses are retained
+                // unchanged in our updated state. We can't test them
+                // for expiry, because the state is only updated when
+                // accept() is called.
+                //!!! That looks like a limitation in the Hypothesis API
+                newState.satisfied.push_back(h);
+
+            } else { // !swallowed
+
+                if (h.accept(o)) {
+#ifdef DEBUG_FEEDER        
+                    std::cerr << "accepted by satisfied hypothesis " << &(*i) << ", state is " << h.getState() << std::endl;
+#endif
+                    swallowed = true;
+                    newState.satisfied.push_back(h);
+                } else if (h.getState() == Hypothesis::Expired) {
+                    newState.completed.push_back(h);
+                } else {
+                    newState.satisfied.push_back(h);
+                }
+            }
+        }
+
+        if (swallowed) {
+
+#ifdef DEBUG_FEEDER        
+            std::cerr << "was swallowed by satisfied hypothesis" << std::endl;
+#endif
+            // no provisional hypotheses have become satisfied, no new
+            // ones have been introduced
+            newState.provisional = s.provisional;
+
+        } else {
+
+#ifdef DEBUG_FEEDER        
+            std::cerr << "remained unswallowed by " << newState.satisfied.size() << " satisfied hypotheses" << std::endl;
+#endif
+
+            // not swallowed by any satisfied hypothesis
+
+            Hypothesis promoted;
+        
+            for (typename Hypotheses::iterator i = s.provisional.begin();
+                 i != s.provisional.end(); ++i) {
+
+                Hypothesis h = *i;
+
+                assert(h.getState() == Hypothesis::Provisional);
+
+                // can only have one satisfied hypothesis for each
+                // observation, so try this only if promoted has not been
+                // set to something else yet
+                if (promoted == Hypothesis() &&
+                    h.accept(o) &&
+                    h.getState() == Hypothesis::Satisfied) {
+                    newState.satisfied.push_back(h);
+#ifdef DEBUG_FEEDER        
+                    std::cerr << "promoting a hypothesis to satisfied, have " << newState.satisfied.size() << " satisfied now" << std::endl;
+#endif
+                    promoted = h;
+                } else if (h.getState() != Hypothesis::Rejected) {
+                    newState.provisional.push_back(h);
+                }
+            }
+
+            if (promoted == Hypothesis()) {
+
+                // No provisional hypothesis has become satisfied
+
+                Hypothesis h;
+                h.accept(o);
+
+                if (h.getState() == Hypothesis::Provisional) {
+                    newState.provisional.push_back(h);
+                } else if (h.getState() == Hypothesis::Satisfied) {
+                    newState.satisfied.push_back(h);
+                }
+
+#ifdef DEBUG_FEEDER        
+                std::cerr << "update: new hypothesis of state " << h.getState() << ", provisional count -> " << newState.provisional.size() << std::endl;
+#endif
+            } else {
+
+#ifdef DEBUG_FEEDER        
+                std::cerr << "a hypothesis became satisfied, reaping its observations" << std::endl;
+#endif
+                newState = reap(newState);
+            }
+        }
+
+        return newState;
+    }
+
+    State reap(State s) {
+
+        // "When a hypothesis subsequently becomes satisfied, all
+        // other provisional hypotheses containing any of its
+        // observations must be discarded."
+
+        if (s.provisional.empty()) return s;
+
+        int reaped = 0;
+
+        Hypotheses prior = s.provisional;
+        s.provisional = Hypotheses();
+
+        for (typename Hypotheses::const_iterator hi = prior.begin();
+             hi != prior.end(); ++hi) {
+                    
+            const AgentHypothesis::Observations obs =
+                hi->getAcceptedObservations();
+
+            bool keep = true;
+
+            for (AgentHypothesis::Observations::const_iterator oi = obs.begin();
+                 oi != obs.end(); ++oi) {
+
+                for (typename Hypotheses::const_iterator si = s.satisfied.end();
+                     si != s.satisfied.begin(); ) {
+                
+                    --si;
+
+                    const AgentHypothesis::Observations sobs = 
+                        si->getAcceptedObservations();
+
+                    if (sobs.find(*oi) != sobs.end()) {
+                        keep = false;
+                        break;
+                    }
+                }
+
+                if (!keep) {
+                    break;
+                }
+            }
+
+            if (keep) {
+                s.provisional.push_back(*hi);
+            } else {
+                ++reaped;
+            }
+        }
+
+#ifdef DEBUG_FEEDER
+        std::cerr << "reap: have "
+                  << s.satisfied.size() << " satisfied, "
+                  << s.provisional.size() << " provisional, "
+                  << s.completed.size() << " completed, reaped "
+                  << reaped << std::endl;
+#endif
+
+        return s;
+    }
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/AgentHypothesis.h	Wed May 28 14:56:01 2014 +0100
@@ -0,0 +1,146 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Silvet
+
+  A Vamp plugin for note transcription.
+  Centre for Digital Music, Queen Mary University of London.
+  This file Copyright 2012 Chris Cannam.
+    
+  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.
+*/
+
+#ifndef AGENT_HYPOTHESIS_H
+#define AGENT_HYPOTHESIS_H
+
+#include "vamp-sdk/RealTime.h"
+
+#include <set>
+#include <map>
+
+/**
+ * An agent used to test an incoming series of timed observations or
+ * estimates to see whether they fit a consistent single-object
+ * relationship.
+ *
+ * A freshly constructed hypothesis object should be in New state and
+ * should accept any observation.
+ */
+
+class AgentHypothesis
+{
+public:
+    virtual ~AgentHypothesis() { }
+
+    enum State {
+
+	/// Just constructed, will provisionally accept any observation
+	New,
+
+	/// Accepted at least one observation, but not enough evidence to satisfy
+	Provisional,
+
+	/// Could not find enough consistency in offered observations
+	Rejected,
+
+	/// Have accepted enough consistent observations to satisfy hypothesis
+	Satisfied,
+
+	/// Have been satisfied, but evidence has now changed: we're done
+	Expired
+    };
+
+    struct Observation {
+
+        Observation() : value(0), time(), confidence(1) { }
+
+        Observation(double _f, Vamp::RealTime _t, double _c) :
+            value(_f), time(_t), confidence(_c) { }
+
+        bool operator==(const Observation &o) const {
+            return o.value == value && o.time == time && o.confidence == confidence;
+        }
+        bool operator<(const Observation &o) const {
+            return
+                (time < o.time || 
+                 (time == o.time && value < o.value) ||
+                 (time == o.time && value == o.value && confidence < o.confidence));
+        }
+
+	double value;
+        Vamp::RealTime time;
+	double confidence;
+    };
+    typedef std::set<Observation> Observations;
+
+    /**
+     * Test the given observation to see whether it is consistent with
+     * this hypothesis, and adjust the hypothesis' internal state
+     * accordingly. If the observation is not inconsistent with the
+     * hypothesis, return true.
+     *!!! should be called e.g. test?
+     */
+    virtual bool accept(Observation) = 0;
+
+    /**
+     * Return the current state of this hypothesis.
+     */
+    virtual State getState() const = 0;
+
+    /**
+     * If the hypothesis has been satisfied (i.e. is in Satisfied or
+     * Expired state), return the set of observations that it
+     * accepted. Otherwise return an empty set
+     */
+    virtual Observations getAcceptedObservations() const = 0;
+
+    /**
+     * Convert the given set of accepted hypotheses (of type
+     * subclassed from AgentHypothesis) into a flattened set of their
+     * accepted observations.
+     * 
+     * That is, only one is included for at any given moment, so in
+     * the case of overlapping hypotheses, the observations for the
+     * earlier are taken until the next hypothesis begins and then the
+     * latter's observations begin instead.
+     *
+     * (If there are gaps between hypotheses, the gaps remain in the
+     * output.)
+     */
+    template <typename HypothesisType>
+    static Observations flatten(const std::set<HypothesisType> &agents) {
+
+        typedef typename std::set<HypothesisType>::const_iterator Itr;
+        Observations flattened;
+
+        if (agents.empty()) return flattened;
+        Observations obs = agents.begin()->getAcceptedObservations();
+
+        for (Itr i = agents.begin(); i != agents.end(); ++i) {
+
+            Itr j = i;
+            ++j;
+
+            Observations nextObs;
+            if (j != agents.end()) nextObs = j->getAcceptedObservations();
+
+            for (Observations::const_iterator i = obs.begin();
+                 i != obs.end(); ++i) {
+                if (!nextObs.empty() && i->time >= nextObs.begin()->time) {
+                    break;
+                }
+                flattened.insert(*i);
+            }
+
+            obs = nextObs;
+        }
+
+        return flattened;
+    }
+};
+
+#endif
--- a/src/MedianFilter.h	Fri May 23 18:17:59 2014 +0100
+++ b/src/MedianFilter.h	Wed May 28 14:56:01 2014 +0100
@@ -1,16 +1,17 @@
 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
 
 /*
-    QM DSP Library
+  Silvet
 
-    Centre for Digital Music, Queen Mary, University of London.
-    This file Copyright 2010 Chris Cannam.
+  A Vamp plugin for note transcription.
+  Centre for Digital Music, Queen Mary University of London.
+  This file Copyright 2010 Chris Cannam.
 
-    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.
+  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.
 */
 
 #ifndef MEDIAN_FILTER_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/NoteHypothesis.cpp	Wed May 28 14:56:01 2014 +0100
@@ -0,0 +1,299 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Silvet
+
+  A Vamp plugin for note transcription.
+  Centre for Digital Music, Queen Mary University of London.
+  This file Copyright 2012 Chris Cannam.
+    
+  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 "NoteHypothesis.h"
+#include "AgentFeederPoly.h"
+
+#include <cmath>
+#include <cassert>
+
+#include <map>
+#include <algorithm>
+
+using Vamp::RealTime;
+
+using std::cerr;
+using std::endl;
+
+#define DEBUG_NOTE_HYPOTHESIS 1
+
+NoteHypothesis::NoteHypothesis()
+{
+    m_state = New;
+}
+
+NoteHypothesis::~NoteHypothesis()
+{
+}
+
+bool
+NoteHypothesis::isWithinTolerance(Observation s) const
+{
+    if (m_pending.empty()) {
+        return true;
+    }
+
+    // check we are within a relatively close tolerance of the last
+    // candidate
+    Observations::const_iterator i = m_pending.end();
+    --i;
+    Observation last = *i;
+    double r = s.value / last.value;
+    int cents = lrint(1200.0 * (log(r) / log(2.0)));
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    cerr << "isWithinTolerance: this " << s.value << " is " << cents
+              << " cents from prior " << last.value << endl;
+#endif
+    if (cents < -60 || cents > 60) return false;
+
+    // and within a slightly bigger tolerance of the current mean
+    double meanFreq = getMeanFrequency();
+    r = s.value / meanFreq;
+    cents = lrint(1200.0 * (log(r) / log(2.0)));
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    cerr << "isWithinTolerance: this " << s.value << " is " << cents
+              << " cents from mean " << meanFreq << endl;
+#endif
+    if (cents < -80 || cents > 80) return false;
+    
+    return true;
+}
+
+bool
+NoteHypothesis::isOutOfDateFor(Observation s) const
+{
+    if (m_pending.empty()) return false;
+
+    Observations::const_iterator i = m_pending.end();
+    --i;
+    Observation last = *i;
+
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    cerr << "isOutOfDateFor: this " << s.time << " is "
+              << (s.time - last.time) << " from last " << last.time
+              << " (threshold " << RealTime::fromMilliseconds(40) << ")"
+              << endl;
+#endif
+
+    return ((s.time - last.time) > RealTime::fromMilliseconds(40));
+}
+
+bool 
+NoteHypothesis::isSatisfied() const
+{
+    if (m_pending.empty()) return false;
+    
+    double meanConfidence = 0.0;
+    for (Observations::const_iterator i = m_pending.begin();
+         i != m_pending.end(); ++i) {
+        meanConfidence += i->confidence;
+    }
+    meanConfidence /= m_pending.size();
+
+    //!!! surely this depends on the hop size?
+    int lengthRequired = 100;
+    if (meanConfidence > 0.0) {
+        lengthRequired = int(2.0 / meanConfidence + 0.5);
+    }
+    //!!! 
+    lengthRequired = lengthRequired / 2;
+    if (lengthRequired < 1) lengthRequired = 1;
+
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    cerr << "meanConfidence " << meanConfidence << ", lengthRequired " << lengthRequired << endl;
+#endif
+
+    return ((int)m_pending.size() > lengthRequired);
+}
+
+static void printState(NoteHypothesis::State s)
+{
+    switch (s) {
+    case NoteHypothesis::New: cerr << "New"; break;
+    case NoteHypothesis::Provisional: cerr << "Provisional"; break;
+    case NoteHypothesis::Rejected: cerr << "Rejected"; break;
+    case NoteHypothesis::Satisfied: cerr << "Satisfied"; break;
+    case NoteHypothesis::Expired: cerr << "Expired"; break;
+    }
+}
+
+bool
+NoteHypothesis::accept(Observation s)
+{
+    bool accept = false;
+
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    cerr << "NoteHypothesis[" << this << "]::accept (value " << s.value << ", time " << s.time << ", confidence " << s.confidence << "): state ";
+    printState(m_state);
+    cerr << "..." << endl;
+#endif
+
+    static double negligibleConfidence = 0.0001;
+
+    if (s.confidence < negligibleConfidence) {
+        // avoid piling up a lengthy sequence of estimates that are
+        // all acceptable but are in total not enough to cause us to
+        // be satisfied
+        if (m_state == New) {
+            m_state = Rejected;
+        }
+        return false;
+    }
+
+    switch (m_state) {
+
+    case New:
+        m_state = Provisional;
+        accept = true;
+        break;
+
+    case Provisional:
+        if (isOutOfDateFor(s)) {
+            m_state = Rejected;
+        } else if (isWithinTolerance(s)) {
+            accept = true;
+        }
+        break;
+        
+    case Satisfied:
+        if (isOutOfDateFor(s)) {
+            m_state = Expired;
+        } else if (isWithinTolerance(s)) {
+            accept = true;
+        }
+        break;
+
+    case Rejected:
+        break;
+
+    case Expired:
+        break;
+    }
+
+    if (accept) {
+#ifdef DEBUG_NOTE_HYPOTHESIS
+        cerr << "... accepting" << endl;
+#endif
+        m_pending.insert(s);
+        if (m_state == Provisional && isSatisfied()) {
+            m_state = Satisfied;
+        }
+    } else {
+#ifdef DEBUG_NOTE_HYPOTHESIS
+        cerr << "... not accepting" << endl;
+#endif
+    }
+
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    cerr << "... -> ";
+    printState(m_state);
+    cerr << " (pending: " << m_pending.size() << ")" << endl;
+#endif
+
+    return accept;
+}        
+
+NoteHypothesis::State
+NoteHypothesis::getState() const
+{
+    return m_state;
+}
+
+NoteHypothesis::Observations
+NoteHypothesis::getAcceptedObservations() const
+{
+    if (m_state == Satisfied || m_state == Expired) {
+        return m_pending;
+    } else {
+        return Observations();
+    }
+}
+
+double
+NoteHypothesis::getMedianFrequency() const
+{
+    if (m_pending.empty()) return 0.0;
+    std::vector<double> freqs;
+    for (Observations::const_iterator i = m_pending.begin();
+         i != m_pending.end(); ++i) {
+        freqs.push_back(i->value);
+    }
+    std::sort(freqs.begin(), freqs.end());
+    return freqs[freqs.size()/2];
+}
+
+double
+NoteHypothesis::getMeanFrequency() const
+{
+    double acc = 0.0;
+    if (m_pending.empty()) return acc;
+    for (Observations::const_iterator i = m_pending.begin();
+         i != m_pending.end(); ++i) {
+        acc += i->value;
+    }
+    acc /= m_pending.size();
+    return acc;
+}
+
+double
+NoteHypothesis::getMedianConfidence() const
+{
+    if (m_pending.empty()) return 0.0;
+    std::vector<double> confs;
+    for (Observations::const_iterator i = m_pending.begin();
+         i != m_pending.end(); ++i) {
+        confs.push_back(i->confidence);
+    }
+    std::sort(confs.begin(), confs.end());
+    return confs[confs.size()/2];
+}
+
+NoteHypothesis::Note
+NoteHypothesis::getAveragedNote() const
+{
+    Note n;
+
+    n.time = getStartTime();
+    n.duration = getDuration();
+    n.freq = getMedianFrequency();
+    n.confidence = getMedianConfidence();
+    
+    return n;
+}
+
+RealTime
+NoteHypothesis::getStartTime() const
+{
+    if (!(m_state == Satisfied || m_state == Expired)) {
+        return RealTime::zeroTime;
+    } else {
+        return m_pending.begin()->time;
+    }
+}
+
+RealTime
+NoteHypothesis::getDuration() const
+{
+//!!! test this! it is wrong
+    if (!(m_state == Satisfied || m_state == Expired)) {
+        return RealTime::zeroTime;
+    } else {
+        RealTime start = m_pending.begin()->time;
+        Observations::const_iterator i = m_pending.end();
+        --i;
+        return i->time - start;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/NoteHypothesis.h	Wed May 28 14:56:01 2014 +0100
@@ -0,0 +1,130 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Silvet
+
+  A Vamp plugin for note transcription.
+  Centre for Digital Music, Queen Mary University of London.
+  This file Copyright 2012 Chris Cannam.
+    
+  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.
+*/
+
+#ifndef NOTE_HYPOTHESIS_H
+#define NOTE_HYPOTHESIS_H
+
+#include "AgentHypothesis.h"
+
+#include <set>
+#include <vector>
+
+/**
+ * An AgentHypothesis which tests a series of instantaneous pitch
+ * estimates to see whether they fit a single-note relationship.
+ * Contains rules specific to testing note pitch and timing.
+ */
+
+class NoteHypothesis : public AgentHypothesis
+{
+public:
+    /**
+     * Construct an empty hypothesis. This will be in New state and
+     * will provisionally accept any estimate.
+     */
+    NoteHypothesis();
+
+    /**
+     * Destroy the hypothesis
+     */
+    ~NoteHypothesis();
+
+    virtual bool accept(Observation);
+    virtual State getState() const;
+    virtual Observations getAcceptedObservations() const;
+
+    struct Note {
+        Note() : freq(0), time(), duration(), confidence(1.0) { }
+        Note(double _f, Vamp::RealTime _t, Vamp::RealTime _d, double _i) :
+            freq(_f), time(_t), duration(_d), confidence(_i) { }
+        bool operator==(const Note &e) const {
+            return e.freq == freq && e.time == time && 
+                e.duration == duration && e.confidence == confidence;
+        }
+	double freq;
+	Vamp::RealTime time;
+	Vamp::RealTime duration;
+        double confidence;
+    };
+    
+    /**
+     * Return the mean frequency of the accepted observations
+     */
+    double getMeanFrequency() const;
+    
+    /**
+     * Return the median frequency of the accepted observations
+     */
+    double getMedianFrequency() const;
+    
+    /**
+     * Return the median confidence of the accepted observations
+     */
+    double getMedianConfidence() const;
+
+    /**
+     * Return a single note roughly matching this hypothesis
+     */
+    Note getAveragedNote() const;
+    
+    /**
+     * Return the time of the first accepted observation
+     */
+    Vamp::RealTime getStartTime() const;
+
+    /**
+     * Return the difference between the start time and the end of the
+     * final accepted observation
+     */
+    Vamp::RealTime getDuration() const;
+
+    //!!!
+    bool operator==(const NoteHypothesis &other) const {
+        return m_state == other.m_state && m_pending == other.m_pending;
+    }
+
+    bool operator<(const NoteHypothesis &other) const {
+        if (getStartTime() != other.getStartTime()) {
+            return getStartTime() < other.getStartTime();
+        } else if (m_state != other.m_state) {
+            return m_state < other.m_state;
+        } else if (m_pending.size() != other.m_pending.size()) {
+            return m_pending.size() < other.m_pending.size();
+        } else {
+            Observations::const_iterator i = m_pending.begin();
+            Observations::const_iterator j = other.m_pending.begin();
+            while (i != m_pending.end()) {
+                if (*i == *j) {
+                    ++i;
+                    ++j;
+                } else {
+                    return *i < *j;
+                }
+            }
+            return false;
+        }
+    }
+
+private:
+    bool isWithinTolerance(Observation) const;
+    bool isOutOfDateFor(Observation) const;
+    bool isSatisfied() const;
+    
+    State m_state;
+    Observations m_pending;
+};
+
+#endif
--- a/src/Silvet.cpp	Fri May 23 18:17:59 2014 +0100
+++ b/src/Silvet.cpp	Wed May 28 14:56:01 2014 +0100
@@ -19,6 +19,9 @@
 #include <cq/CQSpectrogram.h>
 
 #include "MedianFilter.h"
+#include "AgentFeederPoly.h"
+#include "NoteHypothesis.h"
+
 #include "constant-q-cpp/src/dsp/Resampler.h"
 
 #include <vector>
@@ -42,7 +45,8 @@
     m_hqMode(true),
     m_fineTuning(false),
     m_instrument(0),
-    m_colsPerSec(50)
+    m_colsPerSec(50),
+    m_agentFeeder(0)
 {
 }
 
@@ -53,6 +57,7 @@
     for (int i = 0; i < (int)m_postFilter.size(); ++i) {
         delete m_postFilter[i];
     }
+    delete m_agentFeeder;
 }
 
 string
@@ -353,6 +358,7 @@
 {
     delete m_resampler;
     delete m_cq;
+    delete m_agentFeeder;
 
     if (m_inputSampleRate != processingSampleRate) {
 	m_resampler = new Resampler(m_inputSampleRate, processingSampleRate);
@@ -393,15 +399,18 @@
     for (int i = 0; i < m_instruments[0].templateNoteCount; ++i) {
         m_postFilter.push_back(new MedianFilter<double>(3));
     }
-    m_pianoRoll.clear();
-    m_columnCount = 0;
+
+    m_columnCountIn = 0;
+    m_columnCountOut = 0;
     m_startTime = RealTime::zeroTime;
+
+    m_agentFeeder = new AgentFeederPoly<NoteHypothesis>();
 }
 
 Silvet::FeatureSet
 Silvet::process(const float *const *inputBuffers, Vamp::RealTime timestamp)
 {
-    if (m_columnCount == 0) {
+    if (m_columnCountIn == 0) {
         m_startTime = timestamp;
     }
     
@@ -423,7 +432,17 @@
 Silvet::getRemainingFeatures()
 {
     Grid cqout = m_cq->getRemainingOutput();
+
     FeatureSet fs = transcribe(cqout);
+
+    m_agentFeeder->finish();
+
+    FeatureList noteFeatures = obtainNotes();
+    for (FeatureList::const_iterator fi = noteFeatures.begin();
+         fi != noteFeatures.end(); ++fi) {
+        fs[m_notesOutputNo].push_back(*fi);
+    }
+
     return fs;
 }
 
@@ -453,7 +472,7 @@
     //!!! pitches or notes? [terminology]
     Grid localPitches(width, vector<double>(pack.templateNoteCount, 0.0));
 
-    bool wantShifts = m_hqMode && m_fineTuning;
+    bool wantShifts = m_hqMode;
     int shiftCount = 1;
     if (wantShifts) {
         shiftCount = pack.templateMaxShift * 2 + 1;
@@ -515,16 +534,13 @@
             for (int j = 0; j < pack.templateNoteCount; ++j) {
                 m_postFilter[j]->push(0.0);
             }
-            m_pianoRoll.push_back(map<int, double>());
-            if (wantShifts) {
-                m_pianoRollShifts.push_back(map<int, int>());
-            }
             continue;
         }
 
-        postProcess(localPitches[i], localBestShifts[i], wantShifts);
+        postProcess(localPitches[i], localBestShifts[i], 
+                    wantShifts, shiftCount);
         
-        FeatureList noteFeatures = noteTrack(shiftCount);
+        FeatureList noteFeatures = obtainNotes();
 
         for (FeatureList::const_iterator fi = noteFeatures.begin();
              fi != noteFeatures.end(); ++fi) {
@@ -558,13 +574,13 @@
 
     for (int i = 0; i < width; ++i) {
 
-        if (m_columnCount < latentColumns) {
-            ++m_columnCount;
+        if (m_columnCountIn < latentColumns) {
+            ++m_columnCountIn;
             continue;
         }
 
-        int prevSampleNo = (m_columnCount - 1) * m_cq->getColumnHop();
-        int sampleNo = m_columnCount * m_cq->getColumnHop();
+        int prevSampleNo = (m_columnCountIn - 1) * m_cq->getColumnHop();
+        int sampleNo = m_columnCountIn * m_cq->getColumnHop();
 
         bool select = (sampleNo / spacing != prevSampleNo / spacing);
 
@@ -613,7 +629,7 @@
             out.push_back(outCol);
         }
 
-        ++m_columnCount;
+        ++m_columnCountIn;
     }
 
     return out;
@@ -622,7 +638,8 @@
 void
 Silvet::postProcess(const vector<double> &pitches,
                     const vector<int> &bestShifts,
-                    bool wantShifts)
+                    bool wantShifts,
+                    int shiftCount)
 {
     const InstrumentPack &pack = m_instruments[m_instrument];
 
@@ -633,172 +650,82 @@
         filtered.push_back(m_postFilter[j]->get());
     }
 
-    // Threshold for level and reduce number of candidate pitches
+    double threshold = 1; //!!! pack.levelThreshold
 
-    typedef std::multimap<double, int> ValueIndexMap;
-
-    ValueIndexMap strengths;
+    double columnDuration = 1.0 / m_colsPerSec;
+    int postFilterLatency = int(m_postFilter[0]->getSize() / 2);
+    RealTime t = RealTime::fromSeconds
+        (columnDuration * (m_columnCountOut - postFilterLatency) + 0.02);
 
     for (int j = 0; j < pack.templateNoteCount; ++j) {
+
         double strength = filtered[j];
-        if (strength < pack.levelThreshold) continue;
-        strengths.insert(ValueIndexMap::value_type(strength, j));
+        if (strength < threshold) {
+            continue;
+        }
+
+        double freq;
+        if (wantShifts) {
+            freq = noteFrequency(j, bestShifts[j], shiftCount);
+        } else {
+            freq = noteFrequency(j, 0, shiftCount);
+        }
+
+        double confidence = strength / 50.0; //!!!???
+        if (confidence > 1.0) confidence = 1.0;
+
+        AgentHypothesis::Observation obs(freq, t, confidence);
+        m_agentFeeder->feed(obs);
     }
 
-    ValueIndexMap::const_iterator si = strengths.end();
-
-    map<int, double> active;
-    map<int, int> activeShifts;
-
-    while (int(active.size()) < pack.maxPolyphony && si != strengths.begin()) {
-
-        --si;
-
-        double strength = si->first;
-        int j = si->second;
-
-        active[j] = strength;
-
-        if (wantShifts) {
-            activeShifts[j] = bestShifts[j];
-        }
-    }
-
-    m_pianoRoll.push_back(active);
-
-    if (wantShifts) {
-        m_pianoRollShifts.push_back(activeShifts);
-    }
+    m_columnCountOut ++;
 }
 
 Vamp::Plugin::FeatureList
-Silvet::noteTrack(int shiftCount)
+Silvet::obtainNotes()
 {        
-    // Minimum duration pruning, and conversion to notes. We can only
-    // report notes that have just ended (i.e. that are absent in the
-    // latest active set but present in the prior set in the piano
-    // roll) -- any notes that ended earlier will have been reported
-    // already, and if they haven't ended, we don't know their
-    // duration.
-
-    int width = m_pianoRoll.size() - 1;
-
-    const map<int, double> &active = m_pianoRoll[width];
-
-    double columnDuration = 1.0 / m_colsPerSec;
-
-    // only keep notes >= 100ms or thereabouts
-    int durationThreshold = floor(0.1 / columnDuration); // columns
-    if (durationThreshold < 1) durationThreshold = 1;
-
     FeatureList noteFeatures;
 
-    if (width < durationThreshold + 1) {
+    typedef AgentFeederPoly<NoteHypothesis> NoteFeeder;
+
+    NoteFeeder *feeder = dynamic_cast<NoteFeeder *>(m_agentFeeder);
+
+    if (!feeder) {
+        cerr << "INTERNAL ERROR: Feeder is not a poly-note-hypothesis-feeder!"
+             << endl;
         return noteFeatures;
     }
-    
-    //!!! try: repeated note detection? (look for change in first derivative of the pitch matrix)
 
-    for (map<int, double>::const_iterator ni = m_pianoRoll[width-1].begin();
-         ni != m_pianoRoll[width-1].end(); ++ni) {
+    std::set<NoteHypothesis> hh = feeder->getAcceptedHypotheses();
 
-        int note = ni->first;
-        
-        if (active.find(note) != active.end()) {
-            // the note is still playing
-            continue;
+    //!!! inefficient
+    for (std::set<NoteHypothesis>::const_iterator hi = hh.begin();
+         hi != hh.end(); ++hi) { 
+
+        NoteHypothesis h(*hi);
+
+        if (m_emitted.find(h) != m_emitted.end()) {
+            continue; // already returned this one
         }
 
-        // the note was playing but just ended
-        int end = width;
-        int start = end-1;
+        m_emitted.insert(h);
 
-        while (m_pianoRoll[start].find(note) != m_pianoRoll[start].end()) {
-            --start;
-        }
-        ++start;
+        NoteHypothesis::Note n = h.getAveragedNote();
 
-        if ((end - start) < durationThreshold) {
-            continue;
-        }
+        int velocity = n.confidence * 127;
+        if (velocity > 127) velocity = 127;
 
-        emitNote(start, end, note, shiftCount, noteFeatures);
+        Feature f;
+        f.hasTimestamp = true;
+        f.hasDuration = true;
+        f.timestamp = n.time;
+        f.duration = n.duration;
+        f.values.clear();
+        f.values.push_back(n.freq);
+        f.values.push_back(velocity);
+//        f.label = noteName(note, partShift, shiftCount);
+        noteFeatures.push_back(f);
     }
 
-//    cerr << "returning " << noteFeatures.size() << " complete note(s) " << endl;
-
     return noteFeatures;
 }
-
-void
-Silvet::emitNote(int start, int end, int note, int shiftCount,
-                 FeatureList &noteFeatures)
-{
-    int partStart = start;
-    int partShift = 0;
-    int partVelocity = 0;
-
-    Feature f;
-    f.hasTimestamp = true;
-    f.hasDuration = true;
-
-    double columnDuration = 1.0 / m_colsPerSec;
-    int postFilterLatency = int(m_postFilter[0]->getSize() / 2);
-    int partThreshold = floor(0.05 / columnDuration);
-
-    for (int i = start; i != end; ++i) {
-        
-        double strength = m_pianoRoll[i][note];
-
-        int shift = 0;
-
-        if (shiftCount > 1) {
-
-            shift = m_pianoRollShifts[i][note];
-
-            if (i == partStart) {
-                partShift = shift;
-            }
-
-            if (i > partStart + partThreshold && shift != partShift) {
-                
-//                cerr << "i = " << i << ", partStart = " << partStart << ", shift = " << shift << ", partShift = " << partShift << endl;
-
-                // pitch has changed, emit an intermediate note
-                f.timestamp = RealTime::fromSeconds
-                    (columnDuration * (partStart - postFilterLatency) + 0.02);
-                f.duration = RealTime::fromSeconds
-                    (columnDuration * (i - partStart));
-                f.values.clear();
-                f.values.push_back
-                    (noteFrequency(note, partShift, shiftCount));
-                f.values.push_back(partVelocity);
-                f.label = noteName(note, partShift, shiftCount);
-                noteFeatures.push_back(f);
-                partStart = i;
-                partShift = shift;
-                partVelocity = 0;
-            }
-        }
-
-        int v = strength * 2;
-        if (v > 127) v = 127;
-
-        if (v > partVelocity) {
-            partVelocity = v;
-        }
-    }
-
-    if (end >= partStart + partThreshold) {
-        f.timestamp = RealTime::fromSeconds
-            (columnDuration * (partStart - postFilterLatency) + 0.02);
-        f.duration = RealTime::fromSeconds
-            (columnDuration * (end - partStart));
-        f.values.clear();
-        f.values.push_back
-            (noteFrequency(note, partShift, shiftCount));
-        f.values.push_back(partVelocity);
-        f.label = noteName(note, partShift, shiftCount);
-        noteFeatures.push_back(f);
-    }
-}
--- a/src/Silvet.h	Fri May 23 18:17:59 2014 +0100
+++ b/src/Silvet.h	Wed May 28 14:56:01 2014 +0100
@@ -24,6 +24,7 @@
 
 #include "MedianFilter.h"
 #include "Instruments.h"
+#include "NoteHypothesis.h"
 
 using std::string;
 using std::vector;
@@ -32,6 +33,7 @@
 
 class Resampler;
 class CQSpectrogram;
+class AgentFeeder;
 
 class Silvet : public Vamp::Plugin
 {
@@ -84,19 +86,18 @@
     typedef vector<vector<double> > Grid;
 
     vector<MedianFilter<double> *> m_postFilter;
-    vector<map<int, double> > m_pianoRoll;
-    vector<map<int, int> > m_pianoRollShifts;
+
+    AgentFeeder *m_agentFeeder;
+    std::set<NoteHypothesis> m_emitted;
 
     Grid preProcess(const Grid &);
 
     void postProcess(const vector<double> &pitches,
                      const vector<int> &bestShifts,
-                     bool wantShifts); // -> piano roll column
+                     bool wantShifts,
+                     int shiftCount); // -> feeder
 
-    FeatureList noteTrack(int shiftCount);
-
-    void emitNote(int start, int end, int note, int shiftCount,
-                  FeatureList &noteFeatures);
+    FeatureList obtainNotes();
 
     FeatureSet transcribe(const Grid &);
 
@@ -104,7 +105,8 @@
     float noteFrequency(int n, int shift, int shiftCount) const;
 
     int m_blockSize;
-    int m_columnCount;
+    int m_columnCountIn;
+    int m_columnCountOut;
     Vamp::RealTime m_startTime;
 
     mutable int m_notesOutputNo;