Mercurial > hg > silvet
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> ¬es, + 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