Mercurial > hg > beatroot-vamp
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: