changeset 181:10e7c3ff575e noteagent

Experimental branch toward note-agent stuff (not actually plumbed in yet)
author Chris Cannam
date Fri, 23 May 2014 12:40:18 +0100
parents 825193ef09d2
children e1718e64a921
files Makefile.inc src/AgentFeeder.h src/AgentFeederMono.h src/AgentFeederPoly.h src/AgentHypothesis.h src/MedianFilter.h src/NoteHypothesis.cpp src/NoteHypothesis.h
diffstat 8 files changed, 1148 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile.inc	Thu May 22 15:06:37 2014 +0100
+++ b/Makefile.inc	Fri May 23 12:40:18 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/AgentFeeder.h	Fri May 23 12:40:18 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	Fri May 23 12:40:18 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	Fri May 23 12:40:18 2014 +0100
@@ -0,0 +1,285 @@
+/* -*- 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>
+
+//#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
+{
+public:
+    typedef std::set<Hypothesis> Hypotheses;
+    
+private:
+    struct State {
+        Hypotheses provisional;
+        Hypotheses satisfied;
+        Hypotheses completed;
+    };
+    State m_state;
+
+public:
+    AgentFeederPoly() { }
+
+    virtual void feed(AgentHypothesis::Observation o) {
+#ifdef DEBUG_FEEDER        
+        std::cerr << "feed: have observation [value = " << o.value << ", time = " << o.time << "]" << std::endl;
+#endif
+
+        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.insert(*i);
+        }
+    }
+
+    Hypotheses getAcceptedHypotheses() const {
+        return m_state.completed;
+    }
+
+private:
+    void 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.
+        */
+
+        // 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).
+
+        Hypotheses toCompleted;
+        Hypotheses toSatisfied;
+        Hypotheses toProvisional;
+        
+        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
+
+            } 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;
+                } else if (h.getState() == Hypothesis::Expired) {
+                    toCompleted.insert(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
+
+        } 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) {
+                    toSatisfied.insert(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) {
+                    // leave as provisional
+                }
+            }
+
+            if (promoted == Hypothesis()) {
+
+                // No provisional hypothesis has become satisfied
+
+                Hypothesis h;
+                h.accept(o);
+
+                if (h.getState() == Hypothesis::Provisional) {
+                    toProvisional.insert(h);
+                } else if (h.getState() == Hypothesis::Satisfied) {
+                    toSatisfied.insert(h);
+                }
+
+#ifdef DEBUG_FEEDER        
+                std::cerr << "update: new hypothesis of state " << h.getState() << ", provisional count -> " << newState.provisional.size() << std::endl;
+#endif
+
+            }
+        }
+
+        for (typename Hypotheses::const_iterator i = toCompleted.begin();
+             i != toCompleted.end(); ++i) {
+            s.satisfied.erase(*i);
+            s.completed.insert(*i);
+        }
+        for (typename Hypotheses::const_iterator i = toSatisfied.begin();
+             i != toSatisfied.end(); ++i) {
+            s.provisional.erase(*i);
+            s.satisfied.insert(*i);
+        }
+        for (typename Hypotheses::const_iterator i = toProvisional.begin();
+             i != toProvisional.end(); ++i) {
+            s.provisional.insert(*i);
+        }
+
+        reap(s);
+    }
+
+    void 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;
+
+        int reaped = 0;
+
+        Hypotheses toRemove = Hypotheses();;
+
+        for (typename Hypotheses::const_iterator hi = s.provisional.begin();
+             hi != s.provisional.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) {
+                toRemove.insert(*hi);
+                ++reaped;
+            }
+        }
+        
+        for (typename Hypotheses::const_iterator i = toRemove.begin();
+             i != toRemove.end(); ++i) {
+            s.provisional.erase(i);
+        }
+
+#ifdef DEBUG_FEEDER
+        std::cerr << "reap: have "
+                  << s.satisfied.size() << " satisfied, "
+                  << s.provisional.size() << " provisional, "
+                  << s.completed.size() << " completed, reaped "
+                  << reaped << std::endl;
+#endif
+    }
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/AgentHypothesis.h	Fri May 23 12:40:18 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	Thu May 22 15:06:37 2014 +0100
+++ b/src/MedianFilter.h	Fri May 23 12:40:18 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	Fri May 23 12:40:18 2014 +0100
@@ -0,0 +1,356 @@
+/* -*- 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>
+
+using Vamp::RealTime;
+
+//#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
+    std::cerr << "isWithinTolerance: this " << s.value << " is " << cents
+              << " cents from prior " << last.value << std::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
+    std::cerr << "isWithinTolerance: this " << s.value << " is " << cents
+              << " cents from mean " << meanFreq << std::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
+    std::cerr << "isOutOfDateFor: this " << s.time << " is "
+              << (s.time - last.time) << " from last " << last.time
+              << " (threshold " << RealTime::fromMilliseconds(40) << ")"
+              << std::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();
+
+    int lengthRequired = 100;
+    if (meanConfidence > 0.0) {
+        lengthRequired = int(2.0 / meanConfidence + 0.5);
+    }
+//    if (lengthRequired < 1) lengthRequired = 1;
+
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    std::cerr << "meanConfidence " << meanConfidence << ", lengthRequired " << lengthRequired << std::endl;
+#endif
+
+    return ((int)m_pending.size() > lengthRequired);
+}
+
+bool
+NoteHypothesis::accept(Observation s)
+{
+    bool accept = false;
+
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    std::cerr << "NoteHypothesis[" << this << "]::accept: state " << m_state << "..." << std::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) {
+        m_pending.insert(s);
+        if (m_state == Provisional && isSatisfied()) {
+            m_state = Satisfied;
+        }
+    }
+
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    std::cerr << "... -> " << m_state << " (pending: " << m_pending.size() << ")" << std::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::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;
+}
+
+NoteHypothesis::Note
+NoteHypothesis::getAveragedNote() const
+{
+    Note n;
+
+    n.time = getStartTime();
+    n.duration = getDuration();
+
+    // just mean frequency for now, but this isn't at all right perceptually
+    n.freq = getMeanFrequency();
+    
+    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;
+    }
+}
+
+std::vector<double>
+NoteHypothesis::sample(const std::set<NoteHypothesis> &notes,
+                       RealTime startTime,
+                       RealTime endTime,
+                       RealTime interval)
+{
+    //!!! where should this live? in AgentHypothesis? a Feeder class?
+
+    assert(interval > RealTime::zeroTime);
+
+    Observations obs = flatten(notes);
+    Observations::const_iterator oi = obs.begin();
+
+//    std::cerr << "sample: start " << startTime << " end " << endTime << " interval " << interval << std::endl;
+
+//    std::cerr << "sample: flatten gives " << obs.size() << " observations" << std::endl;
+    
+    std::vector<double> samples;
+
+    RealTime obsInterval;
+
+    RealTime t = startTime;
+
+    while (oi != obs.end()) {
+
+        Observation o = *oi;
+
+        if (obsInterval == RealTime()) {
+            //!!! should pull out a function to establish this from the list
+            Observations::const_iterator oj = oi;
+            ++oj;
+            if (oj != obs.end()) {
+                obsInterval = oj->time - o.time;
+            }
+        }
+
+//        std::cerr << "t = " << t << ", o.time = " << o.time << ", interval = " << interval << ", obsInterval = " << obsInterval << std::endl;
+
+        if (t > endTime) {
+            break;
+        } else if (o.time > t) {
+            samples.push_back(0.0);
+            t = t + interval;
+        } else if (o.time + obsInterval <= t) {
+            ++oi;
+        } else {
+            samples.push_back(o.value);
+            t = t + interval;
+        }
+    }
+
+    while (1) {
+//        std::cerr << "t = " << t << std::endl;
+        if (t > endTime) {
+            break;
+        } else {
+            samples.push_back(0.0);
+            t = t + interval;
+        }
+    }
+
+    return samples;
+}
+
+std::vector<double>
+NoteHypothesis::winnow(const Observations &obs,
+                       RealTime startTime,
+                       RealTime endTime,
+                       RealTime interval)
+{
+    AgentFeederPoly<NoteHypothesis> feeder;
+    
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    std::cerr << "winnow: " << obs.size() << " input observations"
+              << std::endl;
+    int nonzero = 0;
+#endif
+
+    for (Observations::const_iterator i = obs.begin();
+         i != obs.end(); ++i) {
+        if (i->value != 0.0) { // 0.0 is a special unvoiced value
+            feeder.feed(*i);
+#ifdef DEBUG_NOTE_HYPOTHESIS
+            std::cerr << i->value << "  ";
+            ++nonzero;
+            if (nonzero % 6 == 0) {
+                std::cerr << std::endl;
+            }
+#endif
+        }
+    }
+    
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    std::cerr << "winnow: " << nonzero << " non-zero"
+              << std::endl;
+#endif
+
+    feeder.finish();
+
+    AgentFeederPoly<NoteHypothesis>::Hypotheses accepted =
+        feeder.getAcceptedHypotheses();
+    
+#ifdef DEBUG_NOTE_HYPOTHESIS
+    std::cerr << "winnow: " << accepted.size() << " accepted hypotheses"
+              << std::endl;
+#endif
+
+    return NoteHypothesis::sample(accepted, startTime, endTime, interval);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/NoteHypothesis.h	Fri May 23 12:40:18 2014 +0100
@@ -0,0 +1,137 @@
+/* -*- 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() { }
+        Note(double _f, Vamp::RealTime _t, Vamp::RealTime _d) :
+            freq(_f), time(_t), duration(_d) { }
+        bool operator==(const Note &e) const {
+            return e.freq == freq && e.time == time && e.duration == duration;
+        }
+	double freq;
+	Vamp::RealTime time;
+	Vamp::RealTime duration;
+    };
+    
+    /**
+     * Return the mean frequency of the accepted observations
+     */
+    double getMeanFrequency() 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;
+
+    /**
+     * Convert the given sequence of accepted hypotheses into a
+     * sampled series of pitches (in Hz), returned at regular
+     * intervals determined by the given start time, end time, and
+     * interval. The range is [start,end], i.e. the end time is
+     * included. The interval must be greater than zero.
+     *
+     * Unvoiced samples are returned as 0Hz.
+     */
+    static std::vector<double> sample(const std::set<NoteHypothesis> &,
+                                      Vamp::RealTime startTime,
+                                      Vamp::RealTime endTime,
+                                      Vamp::RealTime interval);
+
+    /**
+     *!!! No! Not equally spaced, should be able to be anything [ordered]
+     
+     * Given a series of equally spaced observations, return a series
+     * of the same number of pitches (in Hz) calculated by running an
+     * AgentFeeder<NoteHypothesis> on the observations and flattening
+     * and sampling the resulting accepted hypotheses.
+     *
+     * The result should contain only pitches that contributed to
+     * recognised notes in the input observations, with the remaining
+     * (unvoiced) samples returned as 0Hz.
+     *
+     * If the input observations are not equally spaced, the result is
+     * undefined.
+     *!!! (what about rounding errors from RealTime to frame and vice versa?)
+     *!!! (should provide a Timebase?)
+     *!!! update docs for updated api
+     */
+    static std::vector<double> winnow(const Observations &,
+                                      Vamp::RealTime startTime,
+                                      Vamp::RealTime endTime,
+                                      Vamp::RealTime interval);
+
+    //!!!
+    bool operator==(const NoteHypothesis &other) const {
+        return m_state == other.m_state && m_pending == other.m_pending;
+    }
+
+    bool operator<(const NoteHypothesis &other) const {
+        return getStartTime() < other.getStartTime();
+    }
+
+private:
+    bool isWithinTolerance(Observation) const;
+    bool isOutOfDateFor(Observation) const;
+    bool isSatisfied() const;
+    
+    State m_state;
+    Observations m_pending;
+};
+
+#endif