All Downloads are FREE. Search and download functionalities are using the official Maven repository.

core.be.tarsos.dsp.beatroot.Agent Maven / Gradle / Ivy

The newest version!
/*
*      _______                       _____   _____ _____  
*     |__   __|                     |  __ \ / ____|  __ \ 
*        | | __ _ _ __ ___  ___  ___| |  | | (___ | |__) |
*        | |/ _` | '__/ __|/ _ \/ __| |  | |\___ \|  ___/ 
*        | | (_| | |  \__ \ (_) \__ \ |__| |____) | |     
*        |_|\__,_|_|  |___/\___/|___/_____/|_____/|_|     
*                                                         
* -------------------------------------------------------------
*
* TarsosDSP is developed by Joren Six at IPEM, University Ghent
*  
* -------------------------------------------------------------
*
*  Info: http://0110.be/tag/TarsosDSP
*  Github: https://github.com/JorenSix/TarsosDSP
*  Releases: http://0110.be/releases/TarsosDSP/
*  
*  TarsosDSP includes modified source code by various authors,
*  for credits and info, see README.
* 
*/

/*  BeatRoot: An interactive beat tracking system
    Copyright (C) 2001, 2006 by Simon Dixon

    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.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program (the file gpl.txt); if not, download it from
	http://www.gnu.org/licenses/gpl.txt or write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

package be.tarsos.dsp.beatroot;

import java.util.ListIterator;


/** 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.
 */
public class Agent {

	/** Print debugging information */
	public static boolean debug = false;

	/** The maximum amount by which a beat can be later than the predicted beat time,
	 *  expressed as a fraction of the beat period. */
	public static double POST_MARGIN_FACTOR = 0.3;

	/** The maximum amount by which a beat can be earlier than the predicted beat time,
	 *  expressed as a fraction of the beat period. */
	public static double PRE_MARGIN_FACTOR = 0.15;
	
	/** 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. */
	public static final double INNER_MARGIN = 0.040;
	
	/** The maximum allowed deviation from the initial tempo, expressed as a fraction of the initial beat period. */
	public static double MAX_CHANGE = 0.2;
		
	/** The slope of the penalty function for onsets which do not coincide precisely with predicted beat times. */
	public static double CONF_FACTOR = 0.5;
	
	/** 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. */
	public static final double DEFAULT_CORRECTION_FACTOR = 50.0;
	
	/** 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. */
	public static final double DEFAULT_EXPIRY_TIME = 10.0;

	/** The identity number of the next created Agent */
	protected static int idCounter = 0;
	
	/** 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). */
	protected 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. */
	protected static double correctionFactor;

	/** The time (in seconds) after which an Agent that
	 *  has no Event matching its beat predictions will be destroyed. */
	protected static double expiryTime;
	
	/** For scoring Agents in a (non-existent) real-time version (otherwise not used). */
	protected static double decayFactor;

	/** The size of the outer half-window before the predicted beat time. */
	public double preMargin;

	/** The size of the outer half-window after the predicted beat time. */
	public double postMargin;
	
	/** The Agent's unique identity number. */
	protected int idNumber;
	
	/** To be used in real-time version?? */
	public 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. */
	public double phaseScore;
	
	/** How long has this agent been the best?  For real-time version; otherwise not used. */
	public double topScoreTime;
	
	/** The number of beats found by this Agent, including interpolated beats. */
	public int beatCount;
	
	/** The current tempo hypothesis of the Agent, expressed as the beat period in seconds. */
	public double beatInterval;

	/** The initial tempo hypothesis of the Agent, expressed as the beat period in seconds. */
	public double initialBeatInterval;
	
	/** The time of the most recent beat accepted by this Agent. */
	public double beatTime;
	
	/** The list of Events (onsets) accepted by this Agent as beats, plus interpolated beats. */
	public EventList events;

	/** Constructor: the work is performed by init()
	 *  @param ibi The beat period (inter-beat interval) of the Agent's tempo hypothesis.
	 */
	public Agent(double ibi) {
		init(ibi);
	} // constructor

	/** Copy constructor.
	 *  @param clone The Agent to duplicate. */
	public Agent(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 = new EventList(clone.events);
		postMargin = clone.postMargin;
		preMargin = clone.preMargin;
	} // copy constructor

	/** Initialise all the fields of this Agent.
	 *  @param ibi The initial tempo hypothesis of the Agent.
	 */
	protected 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 = new EventList();
	} // init()

	/** Output debugging information about this Agent, at the default (highest) level of detail.	 */
	public void print() {
		print(100);
	} // print()/0
	
	/** Output debugging information about this Agent.
	 *  @param level The level of detail in debugging
	 */
	public void print(int level) {
		System.out.printf("\tAg#%4d: %5.3f", idNumber, beatInterval);
		if (level >= 1) {
			System.out.printf(
					"  Beat#%3d  Time=%7.3f  Score=%4.2f:P%4.2f:%3.1f",
					beatCount, beatTime, tempoScore, phaseScore,
					topScoreTime);
		}
		if (level >= 2)
			System.out.println();
		if (level >= 3)
			events.print();
	} // print()

	/** 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.
	 */
	protected void accept(Event e, double err, int beats) {
		beatTime = e.keyDown;
		events.add(e);
		if (Math.abs(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;
		if (debug) {
			print(1);
			System.out.printf("  Err=" + (err<0?"":"+") + "%5.3f" +
						(Math.abs(err) > innerMargin ? '*':' ') + "%5.3f\n",
						err, conFactor);
		}
	} // accept()

	private double threshold(double value, double min, double max) {
		if (value < min)
			return min;
		if (value > max)
			return max;
		return value;
	}

	
	/** 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.
	 */
	public boolean considerAsBeat(Event e, 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()

	/** Interpolates missing beats in the Agent's beat track, starting from the beginning of the piece. */
	protected 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 
	 */
	public void fillBeats(double start) {
		double prevBeat = 0, nextBeat, currentInterval, beats;
		ListIterator list = events.listIterator();
		if (list.hasNext()) {
			prevBeat = list.next().keyDown;
			// alt. to fill from 0:
			// prevBeat = Math.mod(list.next().keyDown, beatInterval);
			list.previous();
		}
		for ( ; list.hasNext(); list.next()) {
			nextBeat = list.next().keyDown;
			list.previous();
			beats = Math.round((nextBeat - prevBeat) / beatInterval - 0.01); //prefer slow
			currentInterval = (nextBeat - prevBeat) / beats;
			for ( ; (nextBeat > start) && (beats > 1.5); beats--) {
				prevBeat += currentInterval;
				if (debug)
					System.out.printf("Insert beat at: %8.3f (n=%1.0f)\n",
										prevBeat, beats - 1.0);
				list.add(newBeat(prevBeat, 0));	// more than once OK??
			}
			prevBeat = nextBeat;
		}
	} // fillBeats()
	
	/** 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
	 */
	private Event newBeat(double time, int beatNum) {
		return new Event(time,time, time, 56, 64, beatNum, 0, 1);
	} // newBeat()

	/** Show detailed debugging output describing the beat tracking behaviour of this agent.
	 *  Calls showTracking()/1 with a default metrical level of 1.
	 *  @param allEvents An EventList of all onsets
	 */	
	public void showTracking(EventList allEvents) {
		showTracking(allEvents, 1.0);
	} // showTracking()/1

	/** Show detailed debugging output describing the beat tracking behaviour of this agent.
	 *  @param allEvents An EventList of all onsets
	 *  @param level The metrical level of beat tracking relative to the notated beat (used to count beats)
	 */
	public void showTracking(EventList allEvents, double level) {
		int count = 1, gapCount;
		double prevBeat, nextBeat, gap;
		ListIterator beats = events.listIterator();	// point to 1st beat
		ListIterator all = allEvents.listIterator();	// point to 1st event 
		if (!beats.hasNext()) {
			System.err.println("No beats found");
			return;
		}
		prevBeat = events.l.getFirst().keyDown;
		// prevBeat = fmod(beats.next().keyDown, beatInterval);
		System.out.print("Beat  (IBI)   BeatTime   Other Events");
		boolean first = true;
		while (all.hasNext()) {	// print each real event
			Event currentEvent = all.next();
			Event currentBeat = null;
			while (beats.hasNext()) {	// if event was chosen as beat
				currentBeat = beats.next();
				if (currentBeat.keyDown > currentEvent.keyDown + Induction.clusterWidth)
					break;
				gap = currentBeat.keyDown - prevBeat;
				gapCount = (int) Math.round(gap / beatInterval);
				for (int j = 1; j < gapCount; j++) {	//empty beat(s) before event
					nextBeat = prevBeat + gap / gapCount;
					System.out.printf("\n%4d (%5.3f) [%7.3f ]",
						count++, nextBeat - prevBeat, nextBeat);
					prevBeat = nextBeat;
				}
				System.out.printf("\n%4d (%5.3f) ",
						count++, currentEvent.keyDown - prevBeat);
				prevBeat = currentBeat.keyDown;
				currentBeat = null;
				first = false;
			}
			if ((currentBeat != null) && (currentBeat.keyDown > currentEvent.keyDown)) {
				gap = currentBeat.keyDown - prevBeat;
				gapCount = (int) Math.round(gap / beatInterval);
				for (int j = 1; j < gapCount; j++) {	//empty beat(s) before event
					nextBeat = prevBeat + gap / gapCount;
					if (nextBeat >= currentEvent.keyDown)
						break;
					System.out.printf("\n%4d (%5.3f) [%7.3f ]",
						count++, nextBeat - prevBeat, nextBeat);
					prevBeat = nextBeat;
				}
				first = false;
			}
			if (first)	// for correct formatting of any initial (pre-beat) events
				System.out.print("\n                       ");
			System.out.printf("%8.3f%c ", currentEvent.keyDown,
					Math.abs(currentEvent.scoreBeat / level -
						Math.round(currentEvent.scoreBeat / level)) < 0.001?
					'*': ' ');
			first = false;
		}
		System.out.println();
	} // showTracking()

} // class Agent




© 2015 - 2024 Weber Informatics LLC | Privacy Policy