Home Reference Source Repository

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;
    }
}