plectrum

Plectrum: instrument tuner for Android
Log | Files | Refs | README | LICENSE

Agent.java (14868B)


      1 /*
      2 *      _______                       _____   _____ _____  
      3 *     |__   __|                     |  __ \ / ____|  __ \ 
      4 *        | | __ _ _ __ ___  ___  ___| |  | | (___ | |__) |
      5 *        | |/ _` | '__/ __|/ _ \/ __| |  | |\___ \|  ___/ 
      6 *        | | (_| | |  \__ \ (_) \__ \ |__| |____) | |     
      7 *        |_|\__,_|_|  |___/\___/|___/_____/|_____/|_|     
      8 *                                                         
      9 * -------------------------------------------------------------
     10 *
     11 * TarsosDSP is developed by Joren Six at IPEM, University Ghent
     12 *  
     13 * -------------------------------------------------------------
     14 *
     15 *  Info: http://0110.be/tag/TarsosDSP
     16 *  Github: https://github.com/JorenSix/TarsosDSP
     17 *  Releases: http://0110.be/releases/TarsosDSP/
     18 *  
     19 *  TarsosDSP includes modified source code by various authors,
     20 *  for credits and info, see README.
     21 * 
     22 */
     23 
     24 /*  BeatRoot: An interactive beat tracking system
     25     Copyright (C) 2001, 2006 by Simon Dixon
     26 
     27     This program is free software; you can redistribute it and/or modify
     28     it under the terms of the GNU General Public License as published by
     29     the Free Software Foundation; either version 2 of the License, or
     30     (at your option) any later version.
     31 
     32     This program is distributed in the hope that it will be useful,
     33     but WITHOUT ANY WARRANTY; without even the implied warranty of
     34     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     35     GNU General Public License for more details.
     36 
     37     You should have received a copy of the GNU General Public License along
     38     with this program (the file gpl.txt); if not, download it from
     39 	http://www.gnu.org/licenses/gpl.txt or write to the Free Software Foundation, Inc.,
     40     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
     41 */
     42 
     43 package be.tarsos.dsp.beatroot;
     44 
     45 import java.util.ListIterator;
     46 
     47 
     48 /** Agent is the central class for beat tracking.
     49  *  Each Agent object has a tempo hypothesis, a history of tracked beats, and
     50  *  a score evaluating the continuity, regularity and salience of its beat track.
     51  */
     52 public class Agent {
     53 
     54 	/** Print debugging information */
     55 	public static boolean debug = false;
     56 
     57 	/** The maximum amount by which a beat can be later than the predicted beat time,
     58 	 *  expressed as a fraction of the beat period. */
     59 	public static double POST_MARGIN_FACTOR = 0.3;
     60 
     61 	/** The maximum amount by which a beat can be earlier than the predicted beat time,
     62 	 *  expressed as a fraction of the beat period. */
     63 	public static double PRE_MARGIN_FACTOR = 0.15;
     64 	
     65 	/** The default value of innerMargin, which is the maximum time (in seconds) that a
     66 	 * 	beat can deviate from the predicted beat time without a fork occurring. */
     67 	public static final double INNER_MARGIN = 0.040;
     68 	
     69 	/** The maximum allowed deviation from the initial tempo, expressed as a fraction of the initial beat period. */
     70 	public static double MAX_CHANGE = 0.2;
     71 		
     72 	/** The slope of the penalty function for onsets which do not coincide precisely with predicted beat times. */
     73 	public static double CONF_FACTOR = 0.5;
     74 	
     75 	/** The reactiveness/inertia balance, i.e. degree of change in the tempo, is controlled by the correctionFactor
     76 	 *  variable.  This constant defines its default value, which currently is not subsequently changed. The
     77 	 *  beat period is updated by the reciprocal of the correctionFactor multiplied by the difference between the
     78 	 *  predicted beat time and matching onset. */
     79 	public static final double DEFAULT_CORRECTION_FACTOR = 50.0;
     80 	
     81 	/** The default value of expiryTime, which is the time (in seconds) after which an Agent that
     82 	 *  has no Event matching its beat predictions will be destroyed. */
     83 	public static final double DEFAULT_EXPIRY_TIME = 10.0;
     84 
     85 	/** The identity number of the next created Agent */
     86 	protected static int idCounter = 0;
     87 	
     88 	/** The maximum time (in seconds) that a beat can deviate from the predicted beat time
     89 	 *  without a fork occurring (i.e. a 2nd Agent being created). */
     90 	protected static double innerMargin;
     91 
     92 	/** Controls the reactiveness/inertia balance, i.e. degree of change in the tempo.  The
     93 	 *  beat period is updated by the reciprocal of the correctionFactor multiplied by the difference between the
     94 	 *  predicted beat time and matching onset. */
     95 	protected static double correctionFactor;
     96 
     97 	/** The time (in seconds) after which an Agent that
     98 	 *  has no Event matching its beat predictions will be destroyed. */
     99 	protected static double expiryTime;
    100 	
    101 	/** For scoring Agents in a (non-existent) real-time version (otherwise not used). */
    102 	protected static double decayFactor;
    103 
    104 	/** The size of the outer half-window before the predicted beat time. */
    105 	public double preMargin;
    106 
    107 	/** The size of the outer half-window after the predicted beat time. */
    108 	public double postMargin;
    109 	
    110 	/** The Agent's unique identity number. */
    111 	protected int idNumber;
    112 	
    113 	/** To be used in real-time version?? */
    114 	public double tempoScore;
    115 	
    116 	/** Sum of salience values of the Events which have been interpreted
    117 	 *  as beats by this Agent, weighted by their nearness to the predicted beat times. */
    118 	public double phaseScore;
    119 	
    120 	/** How long has this agent been the best?  For real-time version; otherwise not used. */
    121 	public double topScoreTime;
    122 	
    123 	/** The number of beats found by this Agent, including interpolated beats. */
    124 	public int beatCount;
    125 	
    126 	/** The current tempo hypothesis of the Agent, expressed as the beat period in seconds. */
    127 	public double beatInterval;
    128 
    129 	/** The initial tempo hypothesis of the Agent, expressed as the beat period in seconds. */
    130 	public double initialBeatInterval;
    131 	
    132 	/** The time of the most recent beat accepted by this Agent. */
    133 	public double beatTime;
    134 	
    135 	/** The list of Events (onsets) accepted by this Agent as beats, plus interpolated beats. */
    136 	public EventList events;
    137 
    138 	/** Constructor: the work is performed by init()
    139 	 *  @param ibi The beat period (inter-beat interval) of the Agent's tempo hypothesis.
    140 	 */
    141 	public Agent(double ibi) {
    142 		init(ibi);
    143 	} // constructor
    144 
    145 	/** Copy constructor.
    146 	 *  @param clone The Agent to duplicate. */
    147 	public Agent(Agent clone) {
    148 		idNumber = idCounter++;
    149 		phaseScore = clone.phaseScore;
    150 		tempoScore = clone.tempoScore;
    151 		topScoreTime = clone.topScoreTime;
    152 		beatCount = clone.beatCount;
    153 		beatInterval = clone.beatInterval;
    154 		initialBeatInterval = clone.initialBeatInterval;
    155 		beatTime = clone.beatTime;
    156 		events = new EventList(clone.events);
    157 		postMargin = clone.postMargin;
    158 		preMargin = clone.preMargin;
    159 	} // copy constructor
    160 
    161 	/** Initialise all the fields of this Agent.
    162 	 *  @param ibi The initial tempo hypothesis of the Agent.
    163 	 */
    164 	protected void init(double ibi) {
    165 		innerMargin = INNER_MARGIN;
    166 		correctionFactor = DEFAULT_CORRECTION_FACTOR;
    167 		expiryTime = DEFAULT_EXPIRY_TIME;
    168 		decayFactor = 0;
    169 		beatInterval = ibi;
    170 		initialBeatInterval = ibi;
    171 		postMargin = ibi * POST_MARGIN_FACTOR;
    172 		preMargin = ibi * PRE_MARGIN_FACTOR;
    173 		idNumber = idCounter++;
    174 		phaseScore = 0.0;
    175 		tempoScore = 0.0;
    176 		topScoreTime = 0.0;
    177 		beatCount = 0;
    178 		beatTime = -1.0;
    179 		events = new EventList();
    180 	} // init()
    181 
    182 	/** Output debugging information about this Agent, at the default (highest) level of detail.	 */
    183 	public void print() {
    184 		print(100);
    185 	} // print()/0
    186 	
    187 	/** Output debugging information about this Agent.
    188 	 *  @param level The level of detail in debugging
    189 	 */
    190 	public void print(int level) {
    191 		System.out.printf("\tAg#%4d: %5.3f", idNumber, beatInterval);
    192 		if (level >= 1) {
    193 			System.out.printf(
    194 					"  Beat#%3d  Time=%7.3f  Score=%4.2f:P%4.2f:%3.1f",
    195 					beatCount, beatTime, tempoScore, phaseScore,
    196 					topScoreTime);
    197 		}
    198 		if (level >= 2)
    199 			System.out.println();
    200 		if (level >= 3)
    201 			events.print();
    202 	} // print()
    203 
    204 	/** Accept a new Event as a beat time, and update the state of the Agent accordingly.
    205 	 *  @param e The Event which is accepted as being on the beat.
    206 	 *  @param err The difference between the predicted and actual beat times.
    207 	 *  @param beats The number of beats since the last beat that matched an Event.
    208 	 */
    209 	protected void accept(Event e, double err, int beats) {
    210 		beatTime = e.keyDown;
    211 		events.add(e);
    212 		if (Math.abs(initialBeatInterval - beatInterval -
    213 				err / correctionFactor) < MAX_CHANGE * initialBeatInterval)
    214 			beatInterval += err / correctionFactor;// Adjust tempo
    215 		beatCount += beats;
    216 		double conFactor = 1.0 - CONF_FACTOR * err /
    217 								(err>0? postMargin: -preMargin);
    218 		if (decayFactor > 0) {
    219 			double memFactor = 1. - 1. / threshold((double)beatCount,1,decayFactor);
    220 			phaseScore = memFactor * phaseScore +
    221 						 (1.0 - memFactor) * conFactor * e.salience;
    222 		} else
    223 			phaseScore += conFactor * e.salience;
    224 		if (debug) {
    225 			print(1);
    226 			System.out.printf("  Err=" + (err<0?"":"+") + "%5.3f" +
    227 						(Math.abs(err) > innerMargin ? '*':' ') + "%5.3f\n",
    228 						err, conFactor);
    229 		}
    230 	} // accept()
    231 
    232 	private double threshold(double value, double min, double max) {
    233 		if (value < min)
    234 			return min;
    235 		if (value > max)
    236 			return max;
    237 		return value;
    238 	}
    239 
    240 	
    241 	/** The given Event is tested for a possible beat time. The following situations can occur:
    242 	 *  1) The Agent has no beats yet; the Event is accepted as the first beat.
    243 	 *  2) The Event is beyond expiryTime seconds after the Agent's last 'confirming' beat; the Agent is terminated.
    244 	 *  3) The Event is within the innerMargin of the beat prediction; it is accepted as a beat.
    245 	 *  4) The Event is within the outerMargin's of the beat prediction; it is accepted as a beat by this Agent,
    246 	 *     and a new Agent is created which doesn't accept it as a beat.
    247 	 *  5) The Event is ignored because it is outside the windows around the Agent's predicted beat time.
    248 	 * @param e The Event to be tested
    249 	 * @param a The list of all agents, which is updated if a new agent is created.
    250 	 * @return Indicate whether the given Event was accepted as a beat by this Agent.
    251 	 */
    252 	public boolean considerAsBeat(Event e, AgentList a) {
    253 		double err;
    254 		if (beatTime < 0) {	// first event
    255 			accept(e, 0, 1);
    256 			return true;
    257 		} else {			// subsequent events
    258 			if (e.keyDown - events.l.getLast().keyDown > expiryTime) {
    259 				phaseScore = -1.0;	// flag agent to be deleted
    260 				return false;
    261 			}
    262 			double beats = Math.round((e.keyDown - beatTime) / beatInterval);
    263 			err = e.keyDown - beatTime - beats * beatInterval;
    264 			if ((beats > 0) && (-preMargin <= err) && (err <= postMargin)) {
    265 				if (Math.abs(err) > innerMargin)	// Create new agent that skips this
    266 					a.add(new Agent(this));	//  event (avoids large phase jump)
    267 				accept(e, err, (int)beats);
    268 				return true;
    269 			}
    270 		}
    271 		return false;
    272 	} // considerAsBeat()
    273 
    274 	/** Interpolates missing beats in the Agent's beat track, starting from the beginning of the piece. */
    275 	protected void fillBeats() {
    276 		fillBeats(-1.0);
    277 	} // fillBeats()/0
    278 
    279 	/** Interpolates missing beats in the Agent's beat track.
    280 	 *  @param start Ignore beats earlier than this start time 
    281 	 */
    282 	public void fillBeats(double start) {
    283 		double prevBeat = 0, nextBeat, currentInterval, beats;
    284 		ListIterator<Event> list = events.listIterator();
    285 		if (list.hasNext()) {
    286 			prevBeat = list.next().keyDown;
    287 			// alt. to fill from 0:
    288 			// prevBeat = Math.mod(list.next().keyDown, beatInterval);
    289 			list.previous();
    290 		}
    291 		for ( ; list.hasNext(); list.next()) {
    292 			nextBeat = list.next().keyDown;
    293 			list.previous();
    294 			beats = Math.round((nextBeat - prevBeat) / beatInterval - 0.01); //prefer slow
    295 			currentInterval = (nextBeat - prevBeat) / beats;
    296 			for ( ; (nextBeat > start) && (beats > 1.5); beats--) {
    297 				prevBeat += currentInterval;
    298 				if (debug)
    299 					System.out.printf("Insert beat at: %8.3f (n=%1.0f)\n",
    300 										prevBeat, beats - 1.0);
    301 				list.add(newBeat(prevBeat, 0));	// more than once OK??
    302 			}
    303 			prevBeat = nextBeat;
    304 		}
    305 	} // fillBeats()
    306 	
    307 	/** Creates a new Event object representing a beat.
    308 	 *  @param time The time of the beat in seconds
    309 	 *  @param beatNum The index of the beat
    310 	 *  @return The Event object representing the beat
    311 	 */
    312 	private Event newBeat(double time, int beatNum) {
    313 		return new Event(time,time, time, 56, 64, beatNum, 0, 1);
    314 	} // newBeat()
    315 
    316 	/** Show detailed debugging output describing the beat tracking behaviour of this agent.
    317 	 *  Calls showTracking()/1 with a default metrical level of 1.
    318 	 *  @param allEvents An EventList of all onsets
    319 	 */	
    320 	public void showTracking(EventList allEvents) {
    321 		showTracking(allEvents, 1.0);
    322 	} // showTracking()/1
    323 
    324 	/** Show detailed debugging output describing the beat tracking behaviour of this agent.
    325 	 *  @param allEvents An EventList of all onsets
    326 	 *  @param level The metrical level of beat tracking relative to the notated beat (used to count beats)
    327 	 */
    328 	public void showTracking(EventList allEvents, double level) {
    329 		int count = 1, gapCount;
    330 		double prevBeat, nextBeat, gap;
    331 		ListIterator<Event> beats = events.listIterator();	// point to 1st beat
    332 		ListIterator<Event> all = allEvents.listIterator();	// point to 1st event 
    333 		if (!beats.hasNext()) {
    334 			System.err.println("No beats found");
    335 			return;
    336 		}
    337 		prevBeat = events.l.getFirst().keyDown;
    338 		// prevBeat = fmod(beats.next().keyDown, beatInterval);
    339 		System.out.print("Beat  (IBI)   BeatTime   Other Events");
    340 		boolean first = true;
    341 		while (all.hasNext()) {	// print each real event
    342 			Event currentEvent = all.next();
    343 			Event currentBeat = null;
    344 			while (beats.hasNext()) {	// if event was chosen as beat
    345 				currentBeat = beats.next();
    346 				if (currentBeat.keyDown > currentEvent.keyDown + Induction.clusterWidth)
    347 					break;
    348 				gap = currentBeat.keyDown - prevBeat;
    349 				gapCount = (int) Math.round(gap / beatInterval);
    350 				for (int j = 1; j < gapCount; j++) {	//empty beat(s) before event
    351 					nextBeat = prevBeat + gap / gapCount;
    352 					System.out.printf("\n%4d (%5.3f) [%7.3f ]",
    353 						count++, nextBeat - prevBeat, nextBeat);
    354 					prevBeat = nextBeat;
    355 				}
    356 				System.out.printf("\n%4d (%5.3f) ",
    357 						count++, currentEvent.keyDown - prevBeat);
    358 				prevBeat = currentBeat.keyDown;
    359 				currentBeat = null;
    360 				first = false;
    361 			}
    362 			if ((currentBeat != null) && (currentBeat.keyDown > currentEvent.keyDown)) {
    363 				gap = currentBeat.keyDown - prevBeat;
    364 				gapCount = (int) Math.round(gap / beatInterval);
    365 				for (int j = 1; j < gapCount; j++) {	//empty beat(s) before event
    366 					nextBeat = prevBeat + gap / gapCount;
    367 					if (nextBeat >= currentEvent.keyDown)
    368 						break;
    369 					System.out.printf("\n%4d (%5.3f) [%7.3f ]",
    370 						count++, nextBeat - prevBeat, nextBeat);
    371 					prevBeat = nextBeat;
    372 				}
    373 				first = false;
    374 			}
    375 			if (first)	// for correct formatting of any initial (pre-beat) events
    376 				System.out.print("\n                       ");
    377 			System.out.printf("%8.3f%c ", currentEvent.keyDown,
    378 					Math.abs(currentEvent.scoreBeat / level -
    379 						Math.round(currentEvent.scoreBeat / level)) < 0.001?
    380 					'*': ' ');
    381 			first = false;
    382 		}
    383 		System.out.println();
    384 	} // showTracking()
    385 
    386 } // class Agent