src/Agent.js
/**
* Agent is the central class for beat tracking
* @class
*/
export default class Agent {
/**
* Constructor
* @param {Number} tempo - tempo hypothesis of the Agent
* @param {Number} firstBeatTime - the time of the first beat accepted by this Agent
* @param {Number} firsteventScore - salience value of the first beat accepted by this Agent
* @param {Array} agentList - reference to the agent list
* @param {Object} [params={}] - parameters
* @param {Number} [params.expiryTime=10] - the time after which an Agent that has not accepted any beat will be destroyed
* @param {Number} [params.toleranceWndInner=0.04] - the maximum time that a beat can deviate from the predicted beat time without a fork occurring
* @param {Number} [params.toleranceWndPre=0.15] - the maximum amount by which a beat can be earlier than the predicted beat time, expressed as a fraction of the beat period
* @param {Number} [params.toleranceWndPost=0.3] - the maximum amount by which a beat can be later than the predicted beat time, expressed as a fraction of the beat period
* @param {Number} [params.correctionFactor=50] - correction factor for updating beat period
* @param {Number} [params.maxChange=0.2] - the maximum allowed deviation from the initial tempo, expressed as a fraction of the initial beat period
* @param {Number} [params.penaltyFactor=0.5] - factor for correcting score, if onset do not coincide precisely with predicted beat time
*/
constructor(tempo, firstBeatTime, firsteventScore, agentList, params = {}) {
/**
* the time after which an Agent that has not accepted any beat will be destroyed
* @type {Number}
*/
this.expiryTime = params.expiryTime || 10;
/**
* the maximum time that a beat can deviate from the predicted beat time without a fork occurring
* @type {Number}
*/
this.toleranceWndInner = params.toleranceWndInner || 0.04;
/**
* the maximum amount by which a beat can be earlier than the predicted beat time, expressed as a fraction of the beat period
* @type {Number}
*/
this.toleranceWndPre = params.toleranceWndPre || 0.15;
/**
* the maximum amount by which a beat can be later than the predicted beat time, expressed as a fraction of the beat period
* @type {Number}
*/
this.toleranceWndPost = params.toleranceWndPost || 0.3;
this.toleranceWndPre *= tempo;
this.toleranceWndPost *= tempo;
/**
* correction factor for updating beat period
* @type {Number}
*/
this.correctionFactor = params.correctionFactor || 50;
/**
* the maximum allowed deviation from the initial tempo, expressed as a fraction of the initial beat period
* @type {Number}
*/
this.maxChange = params.maxChange || 0.2;
/**
* factor for correcting score, if onset do not coincide precisely with predicted beat time
* @type {Number}
*/
this.penaltyFactor = params.penaltyFactor || 0.5;
/**
* the current tempo hypothesis of the Agent, expressed as the beat period
* @type {Number}
*/
this.beatInterval = tempo;
/**
* the initial tempo hypothesis of the Agent, expressed as the beat period
* @type {Number}
*/
this.initialBeatInterval = tempo;
/**
* the time of the most recent beat accepted by this Agent
* @type {Number}
*/
this.beatTime = firstBeatTime;
/**
* the number of beats found by this Agent, including interpolated beats
* @type {Number}
*/
this.totalBeatCount = 1;
/**
* the array of onsets accepted by this Agent as beats, plus interpolated beats
* @type {Array}
*/
this.events = [firstBeatTime];
/**
* sum of salience values of the onsets which have been interpreted as beats by this Agent
* @type {Number}
*/
this.score = firsteventScore;
/**
* reference to the agent list
* @type {Array}
*/
this.agentListRef = agentList;
}
/**
* The event time is tested if it is a beat time
* @param {Number} eventTime - the event time to be tested
* @param {Number} eventScore - salience values of the event time
* @return {Boolean} indicate whether the given event time was accepted as a beat time
*/
considerEvent(eventTime, eventScore) {
if (eventTime - this.events[this.events.length - 1] > this.expiryTime) {
this.score = -1;
return false;
}
let beatCount = Math.round( (eventTime - this.beatTime) / this.beatInterval );
let err = eventTime - this.beatTime - beatCount * this.beatInterval;
if (beatCount > 0 && err >= -this.toleranceWndPre && err <= this.toleranceWndPost) {
if (Math.abs(err) > this.toleranceWndInner) {
this.agentListRef.push(this.clone());
}
this.acceptEvent(eventTime, eventScore, err, beatCount);
return true;
}
return false;
}
/**
* Accept the event time as a beat time, and update the state of the Agent accordingly
* @param {Number} eventTime - the event time to be accepted
* @param {Number} eventScore - salience values of the event time
* @param {Number} err - the difference between the predicted and actual beat times
* @param {Number} beatCount - the number of beats since the last beat
*/
acceptEvent(eventTime, eventScore, err, beatCount) {
this.beatTime = eventTime;
this.events.push(eventTime);
let corrErr = err / this.correctionFactor;
if (Math.abs(this.initialBeatInterval - this.beatInterval - corrErr) < this.maxChange * this.initialBeatInterval) {
this.beatInterval += corrErr;
}
this.totalBeatCount += beatCount;
let errFactor = err > 0 ? err / this.toleranceWndPost : err / -this.toleranceWndPre;
let scoreFactor = 1 - this.penaltyFactor * errFactor;
this.score += eventScore * scoreFactor;
}
/**
* Interpolates missing beats in the Agent's beat track
*/
fillBeats() {
let prevBeat, nextBeat, currentInterval, beats;
prevBeat = 0;
if (this.events.length > 2) {
prevBeat = this.events[0];
}
for (let i = 0; i < this.events.length; i++) {
nextBeat = this.events[i];
beats = Math.round((nextBeat - prevBeat) / this.beatInterval - 0.01);
currentInterval = (nextBeat - prevBeat) / beats;
let k = 0;
for ( ; beats > 1; beats--) {
prevBeat += currentInterval;
this.events.splice(i + k, 0, prevBeat);
k++;
}
prevBeat = nextBeat;
}
}
/**
* Makes a clone of the Agent
* @return {Agent} agent's clone
*/
clone() {
let newAgent = new Agent();
newAgent.beatInterval = this.beatInterval;
newAgent.initialBeatInterval = this.initialBeatInterval;
newAgent.beatTime = this.beatTime;
newAgent.totalBeatCount = this.totalBeatCount;
newAgent.events = this.events.slice();
newAgent.expiryTime = this.expiryTime;
newAgent.toleranceWndInner = this.toleranceWndInner;
newAgent.toleranceWndPre = this.toleranceWndPre;
newAgent.toleranceWndPost = this.toleranceWndPost;
newAgent.correctionFactor = this.correctionFactor;
newAgent.maxChange = this.maxChange;
newAgent.penaltyFactor = this.penaltyFactor;
newAgent.score = this.score;
newAgent.agentListRef = this.agentListRef;
return newAgent;
}
}