changeset 6:02d388f98c23

Introduce a number of new classes derived from the Java
author Chris Cannam
date Tue, 27 Sep 2011 18:37:01 +0100
parents 2150607d4726
children 3c11becfc81a
files Agent.cpp Agent.h BeatRootProcessor.h BeatTracker.h Event.h Makefile
diffstat 6 files changed, 544 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Agent.cpp	Tue Sep 27 18:37:01 2011 +0100
@@ -0,0 +1,79 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Vamp feature extraction plugin for the BeatRoot beat tracker.
+
+  Centre for Digital Music, Queen Mary, University of London.
+  This file copyright 2011 Simon Dixon, Chris Cannam and QMUL.
+    
+  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 "Agent.h"
+#include "BeatTracker.h"
+
+double Agent::POST_MARGIN_FACTOR = 0.3;
+double Agent::PRE_MARGIN_FACTOR = 0.15;
+const double Agent::INNER_MARGIN = 0.040;
+double Agent::MAX_CHANGE = 0.2;
+double Agent::CONF_FACTOR = 0.5;
+const double Agent::DEFAULT_CORRECTION_FACTOR = 50.0;
+const double Agent::DEFAULT_EXPIRY_TIME = 10.0;
+
+int Agent::idCounter = 0;
+
+double Agent::innerMargin = 0.0;
+double Agent::outerMargin = 0.0;
+double Agent::correctionFactor = 0.0;
+double Agent::expiryTime = 0.0;
+double Agent::decayFactor = 0.0;
+
+bool Agent::considerAsBeat(Event e, const AgentList &a) {
+    double err;
+    if (beatTime < 0) {	// first event
+	accept(e, 0, 1);
+	return true;
+    } else {			// subsequent events
+	if (e.keyDown - events.l.getLast().keyDown > expiryTime) {
+	    phaseScore = -1.0;	// flag agent to be deleted
+	    return false;
+	}
+	double beats = Math.round((e.keyDown - beatTime) / beatInterval);
+	err = e.keyDown - beatTime - beats * beatInterval;
+	if ((beats > 0) && (-preMargin <= err) && (err <= postMargin)) {
+	    if (Math.abs(err) > innerMargin)	// Create new agent that skips this
+		a.add(new Agent(this));	//  event (avoids large phase jump)
+	    accept(e, err, (int)beats);
+	    return true;
+	}
+    }
+    return false;
+} // considerAsBeat()
+
+
+void fillBeats(double start) {
+    double prevBeat = 0, nextBeat, currentInterval, beats;
+    EventList::iterator list = events.begin();
+    if (list != events.end()) {
+	++list;
+	prevBeat = list->time;
+	--list;
+    }
+    for ( ; list != events.end(); ++list) {
+	++list;
+	nextBeat = list->time;
+	--list;
+	beats = nearbyint((nextBeat - prevBeat) / beatInterval - 0.01); //prefer slow
+	currentInterval = (nextBeat - prevBeat) / beats;
+	for ( ; (nextBeat > start) && (beats > 1.5); beats--) {
+	    prevBeat += currentInterval;
+	    //!!! need to insert this event after current itr
+	    list.add(BeatTracker::newBeat(prevBeat, 0));	// more than once OK??
+	}
+	prevBeat = nextBeat;
+    }
+} // fillBeats()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Agent.h	Tue Sep 27 18:37:01 2011 +0100
@@ -0,0 +1,217 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Vamp feature extraction plugin for the BeatRoot beat tracker.
+
+  Centre for Digital Music, Queen Mary, University of London.
+  This file copyright 2011 Simon Dixon, Chris Cannam and QMUL.
+    
+  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_H_
+#define _AGENT_H_
+
+#include "Event.h"
+
+/** Agent is the central class for beat tracking.
+ *  Each Agent object has a tempo hypothesis, a history of tracked beats, and
+ *  a score evaluating the continuity, regularity and salience of its beat track.
+ */
+class Agent
+{
+public:
+
+    typedef std::vector<Agent> AgentList;
+
+    /** The maximum amount by which a beat can be later than the predicted beat time,
+     *  expressed as a fraction of the beat period. */
+    static double POST_MARGIN_FACTOR;
+
+    /** The maximum amount by which a beat can be earlier than the predicted beat time,
+     *  expressed as a fraction of the beat period. */
+    static double PRE_MARGIN_FACTOR;
+	
+    /** The default value of innerMargin, which is the maximum time (in seconds) that a
+     * 	beat can deviate from the predicted beat time without a fork occurring. */
+    static const double INNER_MARGIN;
+	
+    /** The maximum allowed deviation from the initial tempo, expressed as a fraction of the initial beat period. */
+    static double MAX_CHANGE;
+		
+    /** The slope of the penalty function for onsets which do not coincide precisely with predicted beat times. */
+    static double CONF_FACTOR;
+	
+    /** The reactiveness/inertia balance, i.e. degree of change in the tempo, is controlled by the correctionFactor
+     *  variable.  This constant defines its default value, which currently is not subsequently changed. The
+     *  beat period is updated by the reciprocal of the correctionFactor multiplied by the difference between the
+     *  predicted beat time and matching onset. */
+    static const double DEFAULT_CORRECTION_FACTOR;
+	
+    /** The default value of expiryTime, which is the time (in seconds) after which an Agent that
+     *  has no Event matching its beat predictions will be destroyed. */
+    static const double DEFAULT_EXPIRY_TIME;
+
+protected:
+    /** The identity number of the next created Agent */
+    static int idCounter;
+	
+    /** The maximum time (in seconds) that a beat can deviate from the predicted beat time
+     *  without a fork occurring (i.e. a 2nd Agent being created). */
+    static double innerMargin;
+
+    /** Controls the reactiveness/inertia balance, i.e. degree of change in the tempo.  The
+     *  beat period is updated by the reciprocal of the correctionFactor multiplied by the difference between the
+     *  predicted beat time and matching onset. */
+    static double correctionFactor;
+
+    /** The time (in seconds) after which an Agent that
+     *  has no Event matching its beat predictions will be destroyed. */
+    static double expiryTime;
+	
+    /** For scoring Agents in a (non-existent) real-time version (otherwise not used). */
+    static double decayFactor;
+
+public:
+    /** The size of the outer half-window before the predicted beat time. */
+    double preMargin;
+
+    /** The size of the outer half-window after the predicted beat time. */
+    double postMargin;
+	
+    /** The Agent's unique identity number. */
+    int idNumber;
+	
+    /** To be used in real-time version?? */
+    double tempoScore;
+	
+    /** Sum of salience values of the Events which have been interpreted
+     *  as beats by this Agent, weighted by their nearness to the predicted beat times. */
+    double phaseScore;
+	
+    /** How long has this agent been the best?  For real-time version; otherwise not used. */
+    double topScoreTime;
+	
+    /** The number of beats found by this Agent, including interpolated beats. */
+    int beatCount;
+	
+    /** The current tempo hypothesis of the Agent, expressed as the beat period in seconds. */
+    double beatInterval;
+
+    /** The initial tempo hypothesis of the Agent, expressed as the beat period in seconds. */
+    double initialBeatInterval;
+	
+    /** The time of the most recent beat accepted by this Agent. */
+    double beatTime;
+	
+    /** The list of Events (onsets) accepted by this Agent as beats, plus interpolated beats. */
+    EventList events;
+
+    /** Constructor: the work is performed by init()
+     *  @param ibi The beat period (inter-beat interval) of the Agent's tempo hypothesis.
+     */
+    Agent(double ibi) {
+	init(ibi);
+    } // constructor
+
+    /** Copy constructor.
+     *  @param clone The Agent to duplicate. */
+    Agent(const Agent &clone) {
+	idNumber = idCounter++;
+	phaseScore = clone.phaseScore;
+	tempoScore = clone.tempoScore;
+	topScoreTime = clone.topScoreTime;
+	beatCount = clone.beatCount;
+	beatInterval = clone.beatInterval;
+	initialBeatInterval = clone.initialBeatInterval;
+	beatTime = clone.beatTime;
+	events = EventList(clone.events);
+	postMargin = clone.postMargin;
+	preMargin = clone.preMargin;
+    } // copy constructor
+
+protected:
+    /** Initialise all the fields of this Agent.
+     *  @param ibi The initial tempo hypothesis of the Agent.
+     */
+    void init(double ibi) {
+	innerMargin = INNER_MARGIN;
+	correctionFactor = DEFAULT_CORRECTION_FACTOR;
+	expiryTime = DEFAULT_EXPIRY_TIME;
+	decayFactor = 0;
+	beatInterval = ibi;
+	initialBeatInterval = ibi;
+	postMargin = ibi * POST_MARGIN_FACTOR;
+	preMargin = ibi * PRE_MARGIN_FACTOR;
+	idNumber = idCounter++;
+	phaseScore = 0.0;
+	tempoScore = 0.0;
+	topScoreTime = 0.0;
+	beatCount = 0;
+	beatTime = -1.0;
+	events.clear();
+    } // init()
+
+
+    double threshold(double value, double min, double max) {
+	if (value < min)
+	    return min;
+	if (value > max)
+	    return max;
+	return value;
+    }
+
+    /** Accept a new Event as a beat time, and update the state of the Agent accordingly.
+     *  @param e The Event which is accepted as being on the beat.
+     *  @param err The difference between the predicted and actual beat times.
+     *  @param beats The number of beats since the last beat that matched an Event.
+     */
+    void accept(Event e, double err, int beats) {
+	beatTime = e.time;
+	events.push_back(e);
+	if (fabs(initialBeatInterval - beatInterval -
+		 err / correctionFactor) < MAX_CHANGE * initialBeatInterval)
+	    beatInterval += err / correctionFactor;// Adjust tempo
+	beatCount += beats;
+	double conFactor = 1.0 - CONF_FACTOR * err /
+	    (err>0? postMargin: -preMargin);
+	if (decayFactor > 0) {
+	    double memFactor = 1. - 1. / threshold((double)beatCount,1,decayFactor);
+	    phaseScore = memFactor * phaseScore +
+		(1.0 - memFactor) * conFactor * e.salience;
+	} else
+	    phaseScore += conFactor * e.salience;
+    } // accept()
+
+    /** The given Event is tested for a possible beat time. The following situations can occur:
+     *  1) The Agent has no beats yet; the Event is accepted as the first beat.
+     *  2) The Event is beyond expiryTime seconds after the Agent's last 'confirming' beat; the Agent is terminated.
+     *  3) The Event is within the innerMargin of the beat prediction; it is accepted as a beat.
+     *  4) The Event is within the outerMargin's of the beat prediction; it is accepted as a beat by this Agent,
+     *     and a new Agent is created which doesn't accept it as a beat.
+     *  5) The Event is ignored because it is outside the windows around the Agent's predicted beat time.
+     * @param e The Event to be tested
+     * @param a The list of all agents, which is updated if a new agent is created.
+     * @return Indicate whether the given Event was accepted as a beat by this Agent.
+     */
+    bool considerAsBeat(Event e, const AgentList &a);
+
+    /** Interpolates missing beats in the Agent's beat track, starting from the beginning of the piece. */
+    void fillBeats() {
+	fillBeats(-1.0);
+    } // fillBeats()/0
+
+    /** Interpolates missing beats in the Agent's beat track.
+     *  @param start Ignore beats earlier than this start time 
+     */
+    void fillBeats(double start);
+
+}; // class Agent
+
+typedef Agent::AgentList AgentList;
+
+#endif
--- a/BeatRootProcessor.h	Mon Sep 19 15:58:12 2011 +0100
+++ b/BeatRootProcessor.h	Tue Sep 27 18:37:01 2011 +0100
@@ -17,6 +17,8 @@
 #define _BEATROOT_PROCESSOR_H_
 
 #include "Peaks.h"
+#include "Event.h"
+#include "BeatTracker.h"
 
 #include <vector>
 #include <cmath>
@@ -88,8 +90,7 @@
     vector<double> onsets;
 	
     /** The estimated onset times and their saliences. */	
-    //!!!EventList onsetList;
-    vector<double> onsetList; //!!! corresponding to keyDown member of events in list
+    EventList onsetList;
 
     /** Total number of audio frames if known, or -1 for live or compressed input. */
     int totalFrames;
@@ -287,20 +288,20 @@
         onsets.clear();
         onsets.resize(peaks.size(), 0);
         vector<int>::iterator it = peaks.begin();
-        onsetList = new EventList();
-        double minSalience = Peaks.min(spectralFlux);
-        for (int i = 0; i < onsets.length; i++) {
+        onsetList.clear();
+        double minSalience = Peaks::min(spectralFlux);
+        for (int i = 0; i < onsets.size(); i++) {
             int index = *it;
             ++it;
             onsets[i] = index * hop;
-            Event e = BeatTrackDisplay.newBeat(onsets[i], 0);
+            Event e = BeatTracker::newBeat(onsets[i], 0);
 //			if (debug)
 //				System.err.printf("Onset: %8.3f  %8.3f  %8.3f\n",
 //						onsets[i], energy[index], slope[index]);
 //			e.salience = slope[index];	// or combination of energy + slope??
             // Note that salience must be non-negative or the beat tracking system fails!
             e.salience = spectralFlux[index] - minSalience;
-            onsetList.add(e);
+            onsetList.push_back(e);
         }
 
         //!!! This onsetList is then fed in to BeatTrackDisplay::beatTrack
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/BeatTracker.h	Tue Sep 27 18:37:01 2011 +0100
@@ -0,0 +1,207 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Vamp feature extraction plugin for the BeatRoot beat tracker.
+
+  Centre for Digital Music, Queen Mary, University of London.
+  This file copyright 2011 Simon Dixon, Chris Cannam and QMUL.
+    
+  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 _BEAT_TRACKER_H_
+#define _BEAT_TRACKER_H_
+
+#include "Event.h"
+#include "Agent.h"
+
+using std::vector;
+
+class BeatTracker
+{
+protected:
+    /** beat data encoded as a list of Events */
+    EventList beats;
+	
+    /** a list of onset events for passing to the tempo induction and beat tracking methods */
+    EventList onsetList;
+	
+    /** the times of onsets (in seconds) */
+    vector<double> onsets;
+	
+    /** the times corresponding to each point in the <code>magnitudes</code> array */
+    vector<double> env;
+	
+    /** smoothed amplitude envelope of audio signal */ 
+    vector<int> magnitudes;
+
+public:
+    /** Constructor:
+     *  @param b The list of beats
+     */
+    BeatTracker(EventList b) {
+	beats = b;
+    } // BeatTracker constructor
+
+    /** Creates a new Event object representing a beat.
+     *  @param time The time of the beat in seconds
+     *  @param beatNum The index of the beat
+     *  @return The Event object representing the beat
+     */
+    static Event newBeat(double time, int beatNum) {
+	return Event(time, beatNum, 0);
+    } // newBeat()
+
+    /** Perform beat tracking.
+     *  @param events The onsets or peaks in a feature list
+     *  @return The list of beats, or an empty list if beat tracking fails
+     */
+    static EventList beatTrack(EventList events) {
+	return beatTrack(events, EventList());
+    }
+	
+    /** Perform beat tracking.
+     *  @param events The onsets or peaks in a feature list
+     *  @param beats The initial beats which are given, if any
+     *  @return The list of beats, or an empty list if beat tracking fails
+     */
+    static EventList beatTrack(EventList events, EventList beats) {
+	AgentList agents = null;
+	int count = 0;
+	double beatTime = -1;
+	if (!beats.empty()) {
+	    count = beats.size() - 1;
+	    beatTime = beats.l.getLast().keyDown;
+	}
+	if (count > 0) { // tempo given by mean of initial beats
+	    double ioi = (beatTime - beats.l.getFirst().keyDown) / count;
+	    agents = new AgentList(new Agent(ioi), null);
+	} else									// tempo not given; use tempo induction
+	    agents = Induction.beatInduction(events);
+	if (beats != null)
+	    for (AgentList ptr = agents; ptr.ag != null; ptr = ptr.next) {
+		ptr.ag.beatTime = beatTime;
+		ptr.ag.beatCount = count;
+		ptr.ag.events = new EventList(beats);
+	    }
+	agents.beatTrack(events, -1);
+	Agent best = agents.bestAgent();
+	if (best != null) {
+	    best.fillBeats(beatTime);
+	    return best.events;
+	}
+	return new EventList();
+    } // beatTrack()/1
+	
+    /** Finds the mean tempo (as inter-beat interval) from an array of beat times
+     *  @param d An array of beat times
+     *  @return The average inter-beat interval
+     */
+    static double getAverageIBI(vector<double> d) {
+	if ((d == null) || (d.length < 2))
+	    return -1.0;
+	return (d[d.length - 1] - d[0]) / (d.length - 1);
+    } // getAverageIBI()
+	
+    /** Finds the median tempo (as inter-beat interval) from an array of beat times
+     *  @param d An array of beat times
+     *  @return The median inter-beat interval
+     */
+    static double getMedianIBI(vector<double> d) {
+	if ((d == null) || (d.length < 2))
+	    return -1.0;
+	vector<double> ibi = new double[d.length-1];
+	for (int i = 1; i < d.length; i++)
+	    ibi[i-1] = d[i] - d[i-1];
+	Arrays.sort(ibi);
+	if (ibi.length % 2 == 0)
+	    return (ibi[ibi.length / 2] + ibi[ibi.length / 2 - 1]) / 2;
+	else
+	    return ibi[ibi.length / 2];
+    } // getAverageIBI()
+	
+	
+    // Various get and set methods
+	
+    /** @return the list of beats */
+    EventList getBeats() {
+	return beats;
+    } // getBeats()
+
+    /** @return the array of onset times */
+    vector<double> getOnsets() {
+	return onsets;
+    } // getOnsets()
+
+    /** @return the array of offset times */
+    vector<double> getOffsets() {
+	return offsets;
+    } // getOffsets()
+
+    /** @return the array of MIDI pitches */
+    vector<int> getPitches() {
+	return pitches;
+    } // getPitches()
+
+    /** Sets the onset times as a list of Events, for use by the beat tracking methods. 
+     *  @param on The times of onsets in seconds
+     */
+    void setOnsetList(EventList on) {
+	onsetList = on;
+    } // setOnsetList()
+
+    /** Sets the array of onset times, for displaying MIDI or audio input data.
+     *  @param on The times of onsets in seconds
+     */
+    void setOnsets(vector<double> on) {
+	onsets = on;
+    } // setOnsets()
+
+    /** Sets the array of offset times, for displaying MIDI input data.
+     *  @param off The array of MIDI offset times
+     */
+    void setOffsets(vector<double> off) {
+	offsets = off;
+	// setMode(SHOW_MIDI, SHOW_AUDIO | SHOW_SPECTRO);
+    } // setOffsets()
+
+    /** Sets the array of times of amplitude envelope points, for displaying.
+     *  @param envTimes The array of times in seconds corresponding to the values in <code>magnitudes</code>
+     */
+    void setEnvTimes(vector<double> envTimes) {
+	env = envTimes;
+	setMode(SHOW_AUDIO, SHOW_MIDI);
+    } // setEnvTimes()
+
+    /** Sets the array of magnitude values, for displaying.
+     *  @param mag The array of amplitude envelope values
+     */
+    void setMagnitudes(vector<int> mag) {
+	magnitudes = mag;
+    } // setMagnitudes()
+
+    /** Sets the array of pitch values, for displaying MIDI input data.
+     *  @param p The array of MIDI pitch values
+     */
+    void setPitches(vector<int> p) {
+	pitches = p;
+    } // setPitches()
+
+    /** Sets the list of beats.
+     * @param b The list of beats
+     */
+    void setBeats(EventList b) {
+	beats = b;
+	selectedBeat = null;
+	beatPtr = beats.listIterator();
+    } // setBeats()
+
+}; // class BeatTrackDisplay
+
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Event.h	Tue Sep 27 18:37:01 2011 +0100
@@ -0,0 +1,32 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Vamp feature extraction plugin for the BeatRoot beat tracker.
+
+  Centre for Digital Music, Queen Mary, University of London.
+  This file copyright 2011 Simon Dixon, Chris Cannam and QMUL.
+    
+  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 _EVENT_H_
+#define _EVENT_H_
+
+#include <vector>
+
+struct Event {
+    double time;
+    double beat;
+    double salience;
+
+    Event(double t, double b, double s) : time(t), beat(b), salience(s) { }
+};
+
+typedef std::vector<Event> EventList;
+
+#endif
+
--- a/Makefile	Mon Sep 19 15:58:12 2011 +0100
+++ b/Makefile	Tue Sep 27 18:37:01 2011 +0100
@@ -1,7 +1,7 @@
 
 CXXFLAGS	:= -g
 
-beatroot-vamp.so:	BeatRootProcessor.o BeatRootVampPlugin.o Peaks.o
+beatroot-vamp.so:	BeatRootProcessor.o BeatRootVampPlugin.o BeatTracker.o Peaks.o Agent.o
 	g++ -shared $^ -o $@ -Wl,-Bstatic -lvamp-sdk -Wl,-Bdynamic -lpthread -Wl,--version-script=vamp-plugin.map
 
 clean: