UNPKG

@sebastbake/music-tempo

Version:

Finding out tempo of the music

230 lines (218 loc) 8.98 kB
type AgentOptions = { expiryTime?: number; toleranceWndInner?: number; toleranceWndPre?: number; toleranceWndPost?: number; correctionFactor?: number; maxChange?: number; penaltyFactor?: number; } const defaultAgentOptions = { expiryTime: 10, toleranceWndInner: 0.04, toleranceWndPre: 0.15, toleranceWndPost: 0.3, correctionFactor: 50, maxChange: 0.2, penaltyFactor: 0.5, } /** * Agent is the central class for beat tracking * @class */ export default class Agent { expiryTime: number; toleranceWndInner: number; toleranceWndPre: number; toleranceWndPost: number; correctionFactor: number; maxChange: number; penaltyFactor: number; beatInterval?: number; initialBeatInterval?: number; beatTime: number; totalBeatCount: number; events: number[]; score?: number; agentListRef?: 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?: number, firstBeatTime?: number, firsteventScore?: number, agentList?: Agent[], params: AgentOptions = defaultAgentOptions ) { /** * 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 || 0; this.toleranceWndPost *= tempo || 0; /** * 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 || 0; /** * 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 || 0]; /** * 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(): Agent { 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; } }