Chris@6: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@6: Chris@6: /* Chris@6: Vamp feature extraction plugin for the BeatRoot beat tracker. Chris@6: Chris@6: Centre for Digital Music, Queen Mary, University of London. Chris@6: This file copyright 2011 Simon Dixon, Chris Cannam and QMUL. Chris@6: Chris@6: This program is free software; you can redistribute it and/or Chris@6: modify it under the terms of the GNU General Public License as Chris@6: published by the Free Software Foundation; either version 2 of the Chris@6: License, or (at your option) any later version. See the file Chris@6: COPYING included with this distribution for more information. Chris@6: */ Chris@6: Chris@6: #ifndef _AGENT_H_ Chris@6: #define _AGENT_H_ Chris@6: Chris@6: #include "Event.h" Chris@6: Chris@6: /** Agent is the central class for beat tracking. Chris@6: * Each Agent object has a tempo hypothesis, a history of tracked beats, and Chris@6: * a score evaluating the continuity, regularity and salience of its beat track. Chris@6: */ Chris@6: class Agent Chris@6: { Chris@6: public: Chris@6: Chris@6: typedef std::vector AgentList; Chris@6: Chris@6: /** The maximum amount by which a beat can be later than the predicted beat time, Chris@6: * expressed as a fraction of the beat period. */ Chris@6: static double POST_MARGIN_FACTOR; Chris@6: Chris@6: /** The maximum amount by which a beat can be earlier than the predicted beat time, Chris@6: * expressed as a fraction of the beat period. */ Chris@6: static double PRE_MARGIN_FACTOR; Chris@6: Chris@6: /** The default value of innerMargin, which is the maximum time (in seconds) that a Chris@6: * beat can deviate from the predicted beat time without a fork occurring. */ Chris@6: static const double INNER_MARGIN; Chris@6: Chris@6: /** The maximum allowed deviation from the initial tempo, expressed as a fraction of the initial beat period. */ Chris@6: static double MAX_CHANGE; Chris@6: Chris@6: /** The slope of the penalty function for onsets which do not coincide precisely with predicted beat times. */ Chris@6: static double CONF_FACTOR; Chris@6: Chris@6: /** The reactiveness/inertia balance, i.e. degree of change in the tempo, is controlled by the correctionFactor Chris@6: * variable. This constant defines its default value, which currently is not subsequently changed. The Chris@6: * beat period is updated by the reciprocal of the correctionFactor multiplied by the difference between the Chris@6: * predicted beat time and matching onset. */ Chris@6: static const double DEFAULT_CORRECTION_FACTOR; Chris@6: Chris@6: /** The default value of expiryTime, which is the time (in seconds) after which an Agent that Chris@6: * has no Event matching its beat predictions will be destroyed. */ Chris@6: static const double DEFAULT_EXPIRY_TIME; Chris@6: Chris@6: protected: Chris@6: /** The identity number of the next created Agent */ Chris@6: static int idCounter; Chris@6: Chris@6: /** The maximum time (in seconds) that a beat can deviate from the predicted beat time Chris@6: * without a fork occurring (i.e. a 2nd Agent being created). */ Chris@6: static double innerMargin; Chris@6: Chris@6: /** Controls the reactiveness/inertia balance, i.e. degree of change in the tempo. The Chris@6: * beat period is updated by the reciprocal of the correctionFactor multiplied by the difference between the Chris@6: * predicted beat time and matching onset. */ Chris@6: static double correctionFactor; Chris@6: Chris@6: /** The time (in seconds) after which an Agent that Chris@6: * has no Event matching its beat predictions will be destroyed. */ Chris@6: static double expiryTime; Chris@6: Chris@6: /** For scoring Agents in a (non-existent) real-time version (otherwise not used). */ Chris@6: static double decayFactor; Chris@6: Chris@6: public: Chris@6: /** The size of the outer half-window before the predicted beat time. */ Chris@6: double preMargin; Chris@6: Chris@6: /** The size of the outer half-window after the predicted beat time. */ Chris@6: double postMargin; Chris@6: Chris@6: /** The Agent's unique identity number. */ Chris@6: int idNumber; Chris@6: Chris@6: /** To be used in real-time version?? */ Chris@6: double tempoScore; Chris@6: Chris@6: /** Sum of salience values of the Events which have been interpreted Chris@6: * as beats by this Agent, weighted by their nearness to the predicted beat times. */ Chris@6: double phaseScore; Chris@6: Chris@6: /** How long has this agent been the best? For real-time version; otherwise not used. */ Chris@6: double topScoreTime; Chris@6: Chris@6: /** The number of beats found by this Agent, including interpolated beats. */ Chris@6: int beatCount; Chris@6: Chris@6: /** The current tempo hypothesis of the Agent, expressed as the beat period in seconds. */ Chris@6: double beatInterval; Chris@6: Chris@6: /** The initial tempo hypothesis of the Agent, expressed as the beat period in seconds. */ Chris@6: double initialBeatInterval; Chris@6: Chris@6: /** The time of the most recent beat accepted by this Agent. */ Chris@6: double beatTime; Chris@6: Chris@6: /** The list of Events (onsets) accepted by this Agent as beats, plus interpolated beats. */ Chris@6: EventList events; Chris@6: Chris@6: /** Constructor: the work is performed by init() Chris@6: * @param ibi The beat period (inter-beat interval) of the Agent's tempo hypothesis. Chris@6: */ Chris@6: Agent(double ibi) { Chris@6: init(ibi); Chris@6: } // constructor Chris@6: Chris@6: /** Copy constructor. Chris@6: * @param clone The Agent to duplicate. */ Chris@6: Agent(const Agent &clone) { Chris@6: idNumber = idCounter++; Chris@6: phaseScore = clone.phaseScore; Chris@6: tempoScore = clone.tempoScore; Chris@6: topScoreTime = clone.topScoreTime; Chris@6: beatCount = clone.beatCount; Chris@6: beatInterval = clone.beatInterval; Chris@6: initialBeatInterval = clone.initialBeatInterval; Chris@6: beatTime = clone.beatTime; Chris@6: events = EventList(clone.events); Chris@6: postMargin = clone.postMargin; Chris@6: preMargin = clone.preMargin; Chris@6: } // copy constructor Chris@6: Chris@6: protected: Chris@6: /** Initialise all the fields of this Agent. Chris@6: * @param ibi The initial tempo hypothesis of the Agent. Chris@6: */ Chris@6: void init(double ibi) { Chris@6: innerMargin = INNER_MARGIN; Chris@6: correctionFactor = DEFAULT_CORRECTION_FACTOR; Chris@6: expiryTime = DEFAULT_EXPIRY_TIME; Chris@6: decayFactor = 0; Chris@6: beatInterval = ibi; Chris@6: initialBeatInterval = ibi; Chris@6: postMargin = ibi * POST_MARGIN_FACTOR; Chris@6: preMargin = ibi * PRE_MARGIN_FACTOR; Chris@6: idNumber = idCounter++; Chris@6: phaseScore = 0.0; Chris@6: tempoScore = 0.0; Chris@6: topScoreTime = 0.0; Chris@6: beatCount = 0; Chris@6: beatTime = -1.0; Chris@6: events.clear(); Chris@6: } // init() Chris@6: Chris@6: Chris@6: double threshold(double value, double min, double max) { Chris@6: if (value < min) Chris@6: return min; Chris@6: if (value > max) Chris@6: return max; Chris@6: return value; Chris@6: } Chris@6: Chris@6: /** Accept a new Event as a beat time, and update the state of the Agent accordingly. Chris@6: * @param e The Event which is accepted as being on the beat. Chris@6: * @param err The difference between the predicted and actual beat times. Chris@6: * @param beats The number of beats since the last beat that matched an Event. Chris@6: */ Chris@6: void accept(Event e, double err, int beats) { Chris@6: beatTime = e.time; Chris@6: events.push_back(e); Chris@6: if (fabs(initialBeatInterval - beatInterval - Chris@6: err / correctionFactor) < MAX_CHANGE * initialBeatInterval) Chris@6: beatInterval += err / correctionFactor;// Adjust tempo Chris@6: beatCount += beats; Chris@6: double conFactor = 1.0 - CONF_FACTOR * err / Chris@6: (err>0? postMargin: -preMargin); Chris@6: if (decayFactor > 0) { Chris@6: double memFactor = 1. - 1. / threshold((double)beatCount,1,decayFactor); Chris@6: phaseScore = memFactor * phaseScore + Chris@6: (1.0 - memFactor) * conFactor * e.salience; Chris@6: } else Chris@6: phaseScore += conFactor * e.salience; Chris@6: } // accept() Chris@6: Chris@6: /** The given Event is tested for a possible beat time. The following situations can occur: Chris@6: * 1) The Agent has no beats yet; the Event is accepted as the first beat. Chris@6: * 2) The Event is beyond expiryTime seconds after the Agent's last 'confirming' beat; the Agent is terminated. Chris@6: * 3) The Event is within the innerMargin of the beat prediction; it is accepted as a beat. Chris@6: * 4) The Event is within the outerMargin's of the beat prediction; it is accepted as a beat by this Agent, Chris@6: * and a new Agent is created which doesn't accept it as a beat. Chris@6: * 5) The Event is ignored because it is outside the windows around the Agent's predicted beat time. Chris@6: * @param e The Event to be tested Chris@6: * @param a The list of all agents, which is updated if a new agent is created. Chris@6: * @return Indicate whether the given Event was accepted as a beat by this Agent. Chris@6: */ Chris@6: bool considerAsBeat(Event e, const AgentList &a); Chris@6: Chris@6: /** Interpolates missing beats in the Agent's beat track, starting from the beginning of the piece. */ Chris@6: void fillBeats() { Chris@6: fillBeats(-1.0); Chris@6: } // fillBeats()/0 Chris@6: Chris@6: /** Interpolates missing beats in the Agent's beat track. Chris@6: * @param start Ignore beats earlier than this start time Chris@6: */ Chris@6: void fillBeats(double start); Chris@6: Chris@6: }; // class Agent Chris@6: Chris@6: typedef Agent::AgentList AgentList; Chris@6: Chris@6: #endif