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