/* 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 at.ofai.music.beatroot; import java.util.ListIterator; import at.ofai.music.util.Event; import at.ofai.music.util.EventList; /** Class for maintaining the set of all Agents involved in beat tracking a piece of music. * Implements a simple linked list terminated by an AgentList with a null Agent (ag). */ public class AgentList { /** Flag for choice between sum and average beat salience values for Agent scores. * The use of summed saliences favours faster tempi or lower metrical levels. */ public static boolean useAverageSalience = false; /** Flag for printing debugging output. */ public static boolean debug = false; /** For the purpose of removing duplicate agents, the default JND of IBI */ public static final double DEFAULT_BI = 0.02; /** For the purpose of removing duplicate agents, the default JND of phase */ public static final double DEFAULT_BT = 0.04; /** A beat tracking Agent */ public Agent ag; /** The remainder of the linked list */ public AgentList next; /** The length of the list (number of beat tracking Agents) */ public static int count = 0; /** For the purpose of removing duplicate agents, the JND of IBI. * Not changed in the current version. */ public static double thresholdBI = DEFAULT_BI; /** For the purpose of removing duplicate agents, the JND of phase. * Not changed in the current version. */ public static double thresholdBT = DEFAULT_BT; /** Default constructor */ public AgentList() { this(null, null); } /** Constructor for an AgentList: the Agent a is prepended to the list al. * @param a The Agent at the head of the list * @param al The tail of the list */ public AgentList(Agent a, AgentList al) { ag = a; next = al; if (next == null) { if (ag != null) next = new AgentList(); // insert null-terminator if it was forgotten else { count = 0; thresholdBI = DEFAULT_BI; thresholdBT = DEFAULT_BT; } } } // constructor /** Deep print of AgentList for debugging */ public void print() { System.out.println("agentList.print: (size=" + count + ")"); for (AgentList ptr = this; ptr.ag != null; ptr = ptr.next) ptr.ag.print(2); System.out.println("End of agentList.print()"); } // print() /** Inserts newAgent into the list in ascending order of beatInterval */ public void add(Agent a) { add(a, true); } // add()/1 /** Appends newAgent to list (sort==false), or inserts newAgent into the list * in ascending order of beatInterval * @param newAgent The agent to be added to the list * @param sort Flag indicating whether the list is sorted or not */ public void add(Agent newAgent, boolean sort){ if (newAgent == null) return; AgentList ptr; count++; for (ptr = this; ptr.ag != null; ptr = ptr.next) if (sort && (newAgent.beatInterval <= ptr.ag.beatInterval)) { ptr.next = new AgentList(ptr.ag, ptr.next); ptr.ag = newAgent; return; } ptr.next = new AgentList(); ptr.ag = newAgent; } // add()/2 /** Sorts the AgentList by increasing beatInterval, using a bubble sort * since it is assumed that the list is almost sorted. */ public void sort() { boolean sorted = false; while (!sorted) { sorted = true; for (AgentList ptr = this; ptr.ag != null; ptr = ptr.next) { if ((ptr.next.ag != null) && (ptr.ag.beatInterval > ptr.next.ag.beatInterval)) { Agent temp = ptr.ag; ptr.ag = ptr.next.ag; ptr.next.ag = temp; sorted = false; } } // for } // while } // sort() /** Removes the current item from the list. * The current item does not need to be the head of the whole list. * @param ptr Points to the Agent which is removed from the list */ public void remove(AgentList ptr) { count--; ptr.ag = ptr.next.ag; // null-terminated list always has next ptr.next = ptr.next.next; } // remove() /** Removes Agents from the list which are duplicates of other Agents. * A duplicate is defined by the tempo and phase thresholds * thresholdBI and thresholdBT respectively. */ protected void removeDuplicates() { sort(); for (AgentList ptr = this; ptr.ag != null; ptr = ptr.next) { if (ptr.ag.phaseScore < 0.0) // already flagged for deletion continue; for (AgentList ptr2 = ptr.next; ptr2.ag != null; ptr2 = ptr2.next) { if (ptr2.ag.beatInterval - ptr.ag.beatInterval > thresholdBI) break; if (Math.abs(ptr.ag.beatTime - ptr2.ag.beatTime) > thresholdBT) continue; if (ptr.ag.phaseScore < ptr2.ag.phaseScore) { ptr.ag.phaseScore = -1.0; // flag for deletion if (ptr2.ag.topScoreTime < ptr.ag.topScoreTime) ptr2.ag.topScoreTime = ptr.ag.topScoreTime; break; } else { ptr2.ag.phaseScore = -1.0; // flag for deletion if (ptr.ag.topScoreTime < ptr2.ag.topScoreTime) ptr.ag.topScoreTime = ptr2.ag.topScoreTime; } } } for (AgentList ptr = this; ptr.ag != null; ) { if (ptr.ag.phaseScore < 0.0) { remove(ptr); } else ptr = ptr.next; } } // removeDuplicates() /** Perform beat tracking on a list of events (onsets). * @param el The list of onsets (or events or peaks) to beat track */ public void beatTrack(EventList el) { beatTrack(el, -1.0); } // beatTrack()/1 /** Perform beat tracking on a list of events (onsets). * @param el The list of onsets (or events or peaks) to beat track. * @param stop Do not find beats after <code>stop</code> seconds. */ public void beatTrack(EventList el, double stop) { ListIterator<Event> ptr = el.listIterator(); boolean phaseGiven = (ag != null) && (ag.beatTime >= 0); // if given for one, assume given for others while (ptr.hasNext()) { Event ev = ptr.next(); if ((stop > 0) && (ev.keyDown > stop)) break; boolean created = phaseGiven; double prevBeatInterval = -1.0; for (AgentList ap = this; ap.ag != null; ap = ap.next) { Agent currentAgent = ap.ag; if (currentAgent.beatInterval != prevBeatInterval) { if ((prevBeatInterval>=0) && !created && (ev.keyDown<5.0)) { // Create new agent with different phase Agent newAgent = new Agent(prevBeatInterval); newAgent.considerAsBeat(ev, this); add(newAgent); } prevBeatInterval = currentAgent.beatInterval; created = phaseGiven; } if (currentAgent.considerAsBeat(ev, this)) created = true; if (currentAgent != ap.ag) // new one been inserted, skip it ap = ap.next; } // loop for each agent removeDuplicates(); } // loop for each event } // beatTrack() /** Finds the Agent with the highest score in the list. * @return The Agent with the highest score */ public Agent bestAgent() { double best = -1.0; Agent bestAg = null; for (AgentList ap = this; ap.ag != null; ap = ap.next) { // double startTime = ap.ag.events.l.getFirst().keyDown; double conf = (ap.ag.phaseScore + ap.ag.tempoScore) / (useAverageSalience? (double)ap.ag.beatCount: 1.0); if (conf > best) { bestAg = ap.ag; best = conf; } // if (debug) { // ap.ag.print(0); // System.out.printf(" +%5.3f Av-salience = %3.1f\n", // startTime, conf); // } } // if (debug) { // if (bestAg != null) { // System.out.print("Best "); // bestAg.print(0); // System.out.printf(" Av-salience = %5.1f\n", best); // // bestAg.events.print(); // } else // System.out.println("No surviving agent - beat tracking failed"); // } return bestAg; } // bestAgent() // public ArrayList<Agent> goodAgents() { // ArrayList<Agent> result = new ArrayList<Agent>(); // // // Put all agents into arraylist // ArrayList<Agent> agents = new ArrayList<Agent>(); // for (AgentList ap = this; ap.ag != null; ap = ap.next) { // agents.add(ap.ag); // } // // // First sort agents by (phaseScore + tempoScore) // Collections.sort(agents, new Comparator<Agent>() { // // @Override // public int compare(Agent o1, Agent o2) { // return (int) (10 * ((o2.phaseScore + o2.tempoScore) - (o1.phaseScore + o1.tempoScore))); // } // }); // // if(0 < agents.size()) { // // Then determine the first agents score (which has the best score after sorting) // result.add(agents.get(0)); // double bestScore = agents.get(0).phaseScore + agents.get(0).tempoScore; // double scoreThreshold = bestScore * 0.9; // // // Now add all other agents within 5% of this score. // for(int i = 1; i < agents.size(); i++) { // if(scoreThreshold <= (agents.get(i).phaseScore + agents.get(i).tempoScore)) { // result.add(agents.get(i)); // } // } // } // // // Return them // return result; // } // goodAgents() } // class AgentList