com.rockhoppertech.music.midi.js.MIDITrack.java Source code

Java tutorial

Introduction

Here is the source code for com.rockhoppertech.music.midi.js.MIDITrack.java

Source

package com.rockhoppertech.music.midi.js;

/*
 * #%L
 * Rocky Music Core
 * %%
 * Copyright (C) 1996 - 2013 Rockhopper Technologies
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.sound.midi.MidiEvent;

import org.apache.commons.lang3.Range;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.rockhoppertech.music.Pitch;
import com.rockhoppertech.music.PitchFactory;
import com.rockhoppertech.music.PitchFormat;
import com.rockhoppertech.music.Timed;
import com.rockhoppertech.music.midi.parse.MIDIStringParser;
import com.rockhoppertech.music.modifiers.ChannelModifier;
import com.rockhoppertech.music.modifiers.DurationModifier;
import com.rockhoppertech.music.modifiers.InstrumentModifier;
import com.rockhoppertech.music.modifiers.MIDINoteModifier;
import com.rockhoppertech.music.modifiers.Modifier;
import com.rockhoppertech.music.modifiers.Modifier.Operation;
import com.rockhoppertech.music.modifiers.NoteModifier;
import com.rockhoppertech.music.modifiers.StartBeatModifier;
import com.rockhoppertech.music.modifiers.VelocityModifier;
import com.rockhoppertech.music.series.time.TimeSeries;

/**
 * 
 * A collection of {@code MIDINote}s and {@code MIDIEvent}s along with a pile of
 * things you can do to them
 * 
 * <p>
 * A rewrite of my ancient {@code MIDITrack} and {@code MIDINoteList} from the
 * 1990s.
 * <p>
 * I removed the JavaBean event code, thinking that JavaFX could handle events
 * easily. I've put it back in since I don't want JavaFX classes e.g.
 * {@code javafx.beans.property.DoubleProperty} here. So, the onus is on JavaFX
 * authors to handle property change events. Take a look at
 * {@code javafx.beans.property.adapter.JavaBeanStringPropertyBuilder} for
 * example.
 * 
 * 
 * @author <a href="http://genedelisa.com/">Gene De Lisa</a>
 * @see javafx.beans.property.adapter.JavaBeanStringPropertyBuilder
 */

public class MIDITrack implements Serializable, Iterable<MIDINote>, Comparable<MIDITrack> {

    private static final class AscendingPitchComparator implements Comparator<MIDINote>, Serializable {
        /**
         * Serialization.
         */
        private static final long serialVersionUID = 171644120355550199L;

        @Override
        public int compare(MIDINote o1, MIDINote o2) {
            if (o1.getMidiNumber() > o2.getMidiNumber()) {
                return 1;
            } else if (o1.getMidiNumber() < o2.getMidiNumber()) {
                return -1;
            }
            return 0;
        }
    }

    /**
     * For serialization.
     */
    private static final long serialVersionUID = 1L;

    private static final Logger logger = LoggerFactory.getLogger(MIDITrack.class);

    /**
     * JavaBean property.
     */
    public static final String MODIFIED = "MIDITrack.MODIFIED";

    /**
     * JavaBean property.
     */
    public static final String STARTBEAT = "startBeat";

    /**
     * JavaBean property.
     */
    public static final String ADD = "MIDITrack.ADD";

    /**
     * JavaBean property.
     */
    public static final String REMOVE = "MIDITrack.REMOVED";

    /**
     * Meta text track name.
     */
    private String name;

    /**
     * Meta text inserted at tick 0.
     */
    private String description;

    /**
     * The events.
     */
    private List<MIDIEvent> events;

    /**
     * The notes will be turned into note on/note off messages.
     */
    private List<MIDINote> notes;

    /**
     * The GM patch to use.
     */
    private Instrument instrument = Instrument.PIANO;

    /**
     * The Score to which this track is attached. Might be null.
     */
    private Score score;

    /**
     * The time signatures.
     */
    private NavigableMap<Double, TimeSignature> timeSignatures = new TreeMap<Double, TimeSignature>();
    /**
     * The key signatures.
     */
    private NavigableMap<Double, KeySignature> keySignatureMap = new TreeMap<Double, KeySignature>();

    /**
     * The tempi.
     */
    private NavigableMap<Double, Integer> tempoMap = new TreeMap<Double, Integer>();

    /**
     * To parse midi strings into notes etc.
     */
    private transient MIDIStringParser midiStringParser = new MIDIStringParser();

    /**
     * JavaBean property event helper.
     */
    private transient PropertyChangeSupport changes = new PropertyChangeSupport(this);

    /**
     * Whatever data you want to attach to this track.
     */
    private Serializable userData;

    /**
     * Initializes a new {@code MIDITrack} with no {@code MIDINote}s nor
     * {@code MIDIEvent}s.
     */
    public MIDITrack() {
        this.events = new ArrayList<>();
        this.notes = new ArrayList<>();
    }

    /**
     * Initializes a new {@code MIDITrack} instance as a deep copy of specified
     * MIDITrack. Sort of like a C++ copy constructor (- but without C++ crap
     * like virtual destructors and overloaded operators...)
     * 
     * 
     * <pre>
     * {@code
     * MIDITrack track = new MIDITrack();
     * MIDINote note = new MIDINote(Pitch.C5);
     * track.add(note);
     * MIDITrack trackCopy = new MIDITrack(track);
     * }
     * </pre>
     * 
     * 
     * @param orig
     *            The {@code MIDITrack} that will be "cloned".
     */
    public MIDITrack(final MIDITrack orig) {
        this();
        if (orig.getName() != null) {
            name = orig.getName();
        }
        if (orig.getDescription() != null) {
            description = orig.getDescription();
        }

        for (MIDINote n : orig.notes) {
            notes.add(new MIDINote(n));
        }
        for (MIDIEvent n : orig.events) {
            events.add(new MIDIEvent(n.toMidiEvent(), this));
        }

        // NavigableMap<Double, TimeSignature> ts = orig.getTimeSignatures();
        // for (Iterator<Double> i = ts.keySet().iterator(); i.hasNext();) {
        // double time = i.next();
        // TimeSignature sig = ts.get(time);
        // this.timeSignatures.put(time, sig);
        // }
        //
        // NavigableMap<Double, KeySignature> ks = orig.keySignatureMap;
        // for (Double time : ks.keySet()) {
        // KeySignature sig = ks.get(time);
        // this.keySignatureMap.put(time, sig);
        // }
        // for (Iterator<Double> i = ts.keySet().iterator(); i.hasNext();) {
        // double time = i.next();
        // KeySignature sig = ks.get(time);
        // this.keySignatureMap.put(time, sig);
        // }

    }

    /**
     * Initializes a new <code>MIDITrack</code> instance. The MIDINotes from the
     * provided collection are copied - not cloned.
     * 
     * @param c
     *            a {@code Collection<MIDINote>} value
     */
    public MIDITrack(Collection<MIDINote> c) {
        this();
        for (MIDINote n : c) {
            notes.add(n);
        }
    }

    /**
     * Parses a note string. It is not sequential by default since the
     * noteString may contain start beats and durations.
     * 
     * @param noteString
     *            a note string
     * @see MIDIStringParser
     */
    public MIDITrack(final String noteString) {
        this();
        midiStringParser.parseString(this, noteString);
    }

    /**
     * Initialize a track from a Guava Iterable of MIDINote.
     * 
     * @param iterable
     *            a Guava Iterable
     */
    public MIDITrack(final Iterable<MIDINote> iterable) {
        this(Lists.newArrayList(iterable));
    }

    /**
     * @return the description
     */
    public final String getDescription() {
        return description;
    }

    /**
     * @param description
     *            the description
     */
    public void setDescription(final String description) {
        this.description = description;
    }

    /**
     * Adds the event. {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * @param event
     *            the event to add
     * @return this to cascade calls
     */
    public final MIDITrack add(final MIDIEvent event) {
        events.add(event);
        event.setTrack(this);
        this.changes.firePropertyChange(ADD, null, this);
        return this;
    }

    /**
     * @return the name
     */
    public final String getName() {
        return name;
    }

    /**
     * @param name
     *            the name
     */
    public final void setName(final String name) {
        this.name = name;
    }

    /**
     * @return the score
     */
    public final Score getScore() {
        return score;
    }

    /**
     * @param score
     *            the score to set
     */
    public final void setScore(final Score score) {
        this.score = score;
    }

    /**
     * Returns an unmodifiable {@code List} of the {@code MIDIEvent}s.
     * 
     * @return a {@code List} of {@code MIDIEvent}s
     */
    public final List<MIDIEvent> getEvents() {
        return Collections.unmodifiableList(events);
    }

    /**
     * Set the events in this track. {@code PropertyChange MIDITrack.MODIFIED}
     * is fired.
     * 
     * @param events
     *            a List of events
     */
    public void setEvents(final List<MIDIEvent> events) {
        this.events = Lists.newArrayList(events);
        this.changes.firePropertyChange(MODIFIED, null, this);
    }

    /**
     * Returns an unmodifiable {@code List} of the {@code MIDINote}s.
     * 
     * @return a {@code List} of {@code MIDINote}s
     */
    public List<MIDINote> getNotes() {
        return Collections.unmodifiableList(notes);
    }

    public void setNotes(List<MIDINote> notes) {
        this.notes = Lists.newArrayList(notes);
        this.changes.firePropertyChange(MODIFIED, null, this);

    }

    @Override
    public Iterator<MIDINote> iterator() {
        return notes.iterator();
    }

    /**
     * Append a {@code MIDINote} to this track.
     * {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * @param note
     *            the note to append
     * @return this to cascade calls
     */
    public MIDITrack append(final MIDINote note) {
        double end = getEndBeat();
        note.setStartBeat(end);
        notes.add(note);
        this.changes.firePropertyChange(ADD, null, this);
        this.changes.firePropertyChange("notes", null, this);
        return this;
    }

    /**
     * Creates a {@code MIDINote} and appends it to this track.
     * {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * @param midiNumber
     *            a MIDI pitch number
     * @return this to cascade calls
     */
    public MIDITrack append(final int midiNumber) {
        MIDINote note = new MIDINote(midiNumber);
        return this.append(note);
    }

    /**
     * Retrieve the number of {@code MIDINote}s in this track.
     * 
     * @return the number of notes in the track
     */
    public int size() {
        return notes.size();
    }

    /**
     * Predicate to test if the specified {@code Pitch} is in this track.
     * 
     * @param p
     *            a {@code Pitch} instance
     * @return true if the pitch is in this track
     */
    public boolean contains(final Pitch p) {
        for (MIDINote n : this) {
            if (n.getPitch().equals(p)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get the {@code MIDINote} at the specified index.
     * 
     * @param i
     *            an index
     * @return a {@code MIDINote}
     */
    public MIDINote get(final int i) {
        return notes.get(i);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Track Name:").append(name).append('\n');
        // if (this.description != null || this.description.equals(""))
        // sb.append("Description:").append(this.description).append('\n');
        // sb.append("Instrument:").append(this.gmpatch).append('\n');
        sb.append("Instrument:").append(this.instrument).append('\n');

        if (this.notes.size() > 0) {
            for (MIDINote n : notes) {
                sb.append(n).append('\n');
            }
        } else {
            sb.append("no notes").append('\n');
        }

        if (this.events.size() > 0) {
            sb.append("events").append('\n');
            for (MIDIEvent n : events) {
                sb.append(n.toReadableString()).append('\n');
                sb.append(n.toString()).append('\n');
            }
        } else {
            sb.append("no events").append('\n');
        }

        NavigableMap<Double, KeySignature> keys = this.getKeySignatures();
        for (Double time : keys.keySet()) {
            KeySignature key = keys.get(time);
            sb.append("Key: ").append(key.getDisplayName()).append(" at beat ").append(time).append('\n');
        }

        NavigableMap<Double, TimeSignature> timeSigs = this.getTimeSignatures();
        for (Double time : timeSigs.keySet()) {
            TimeSignature ts = timeSigs.get(time);
            sb.append("Time signature: ").append(ts.getDisplayName()).append(" at beat ").append(time).append('\n');
        }

        NavigableMap<Double, Integer> tempoMap = this.getTempoMap();
        for (Double time : tempoMap.keySet()) {
            Integer tempo = tempoMap.get(time);
            sb.append("Tempo: ").append(tempo).append(" at beat ").append(time).append('\n');
        }

        return sb.toString();
    }

    /**
     * Creates a {@code MIDIStringParser} compatible string with the pitch,
     * start beat, and duration of each {@code MIDINote} in the track.
     * 
     * @return a string
     * @see MIDIStringParser
     */
    public String toBriefMIDIString() {
        StringBuilder sb = new StringBuilder();

        for (MIDINote note : notes) {

            String spelling = note.getSpelling();
            if (spelling == null) {

                spelling = PitchFormat.getInstance().format(note.getPitch());
            }
            sb.append(spelling.trim()).append(",");

            // sb.append(PitchFormat.getInstance().format(note.getPitch()).trim())
            // .append(",");
            sb.append(note.getStartBeat()).append(",");
            sb.append(note.getDuration()).append(' ');
        }

        // for (MIDIEvent n : this.events) {
        // sb.append(n.toReadableString()).append('\n');
        // }

        return sb.toString();
    }

    public String toBriefMIDIString(String delimiter) {
        StringBuilder sb = new StringBuilder();

        for (MIDINote note : notes) {

            String spelling = note.getSpelling();
            if (spelling == null) {
                spelling = PitchFormat.getInstance().format(note.getPitch());
            }
            sb.append(spelling.trim()).append(",");
            sb.append(note.getStartBeat()).append(",");
            sb.append(note.getDuration()).append(delimiter);
        }
        return sb.toString();
    }

    /**
     * Creates a {@code MIDIStringParser} compatible string with all the
     * properties of each {@code MIDINote} in the track.
     * 
     * Pitch, start beat, duration, velocity, pan, channel, bank, program, pitch
     * bend.
     * 
     * @return a string
     * @see MIDIStringParser
     */
    public String toMIDIString() {
        StringBuilder sb = new StringBuilder();
        for (MIDINote note : notes) {
            String s = String.format("%s,%.3f,%.3f,%d,%d,%d,%d,%d,%d",
                    PitchFormat.getInstance().format(note.getPitch()).trim(), note.getStartBeat(),
                    note.getDuration(), note.getVelocity(), note.getPan(), note.getChannel(), note.getBank(),
                    note.getProgram(), note.getPitchBend());
            sb.append(s).append("\n");
        }

        return sb.toString();
    }

    /**
     * returns the last {@code MIDINote} in the list
     * 
     * @return a {@code MIDINote} that is last in the list
     */
    public MIDINote getLastNote() {
        return get(notes.size() - 1);
    }

    /**
     * Create and append the specified {@code MIDINote} to the list. The Note's
     * timing is unchanged. Use <code>append</code> if you want the timing
     * changed.
     * <p>
     * {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * 
     * <pre>
     * {@code
     * MIDITrack track = new MIDITrack();
     * MIDINote note = new MIDINote(Pitch.C5);
     * track.add(note);
     * }
     * </pre>
     * 
     * 
     * @param note
     *            The MIDINote instance to append to the list.
     * 
     * @see com.rockhoppertech.music.midi.js.MIDINote
     * @see MIDITrack#append(MIDINote)
     * @return this to cascade
     */
    public MIDITrack add(MIDINote note) {
        notes.add(note);
        note.setMidiTrack(this);
        this.changes.firePropertyChange(ADD, null, this);

        return this;
    }

    /**
     * Add notes to the track. {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * @param notes
     *            notes to add
     * @return this to cascade calls
     */
    public MIDITrack add(MIDINote... notes) {
        for (MIDINote note : notes) {
            this.notes.add(note);
            note.setMidiTrack(this);
        }
        this.changes.firePropertyChange(ADD, null, this);
        return this;
    }

    /**
     * Create and append a new {@code MIDINote} to the list. All the default
     * values besides pitch are set (e.g. startbeat = 1, duration = 1.)
     * <p>
     * {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * 
     * <pre>
     * {@code
     * MIDITrack track = new MIDITrack();
     * track.add("C5").add("D5");
     * }
     * </pre>
     * 
     * 
     * @param pitch
     *            The pitch name as a String e.g. C5 to be parsed by
     *            PitchFactory
     * 
     * @see com.rockhoppertech.music.midi.js.MIDINote
     * @see com.rockhoppertech.music.PitchFactory
     * @return this to cascade
     */
    public MIDITrack add(String pitch) {
        MIDINote note = new MIDINote(PitchFactory.getPitch(pitch).getMidiNumber());
        notes.add(note);
        note.setMidiTrack(this);
        this.changes.firePropertyChange(ADD, null, this);
        return this;
    }

    /**
     * Creates a {@code MIDINote} with the specified pitch and adds it to the
     * track. {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * @param midiNumber
     *            pitch number
     * @return this to cascade
     */
    public MIDITrack add(int midiNumber) {
        MIDINote note = new MIDINote(midiNumber);
        notes.add(note);
        note.setMidiTrack(this);
        this.changes.firePropertyChange(ADD, null, this);
        return this;
    }

    /**
     * Creates {@code MIDINote}s with the specified pitch and adds it to the
     * track. {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * @param midiNumbers
     *            pitches
     * @return this to cascade calls
     */
    public MIDITrack add(int... midiNumbers) {
        // MIDINote[] a = new MIDINote[midiNumbers.length];
        // int count = 0;
        for (int mn : midiNumbers) {
            MIDINote note = new MIDINote(mn);
            // a[count++] = n;
            notes.add(note);
            note.setMidiTrack(this);
        }
        this.changes.firePropertyChange(ADD, null, this);
        return this;
    }

    /**
     * Create and append a new {@code MIDINote} to the list.
     * <p>
     * {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * 
     * <pre>
     * {@code
     * MIDITrack track = new MIDITrack();
     * track.add(Pitch.C5, 2d).add(Pitch.D5, Duration.Q);
     * }
     * </pre>
     * 
     * 
     * @param pitch
     *            The pitch name e.g. C5 to be parsed by PitchFactory
     * @param duration
     *            The new MIDINote's duration
     * 
     * @see com.rockhoppertech.music.midi.js.MIDINote
     * @see com.rockhoppertech.music.PitchFactory
     * @return this to cascade
     */
    public MIDITrack add(String pitch, double duration) {
        MIDINote note = new MIDINote(PitchFactory.getPitch(pitch).getMidiNumber());
        note.setDuration(duration);
        note.setMidiTrack(this);
        notes.add(note);
        this.changes.firePropertyChange(ADD, null, this);
        return this;
    }

    /**
     * Create and append a new {@code MIDINote} to the list.
     * <p>
     * {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * 
     * <pre>
     * {@code
     * MIDITrack track = new MIDITrack();
     * track.add("C5", 1.5, 2d).add("D5", 2.5, 2d);
     * }
     * </pre>
     * 
     * 
     * @param pitch
     *            The pitch name e.g. C5 to be parsed by PitchFactory
     * @param startBeat
     *            The new MIDINote's start beat
     * @param duration
     *            The new MIDINote's duration
     * 
     * @see com.rockhoppertech.music.midi.js.MIDINote
     * @see com.rockhoppertech.music.PitchFactory
     * @return this to cascade
     */
    public MIDITrack add(String pitch, double startBeat, double duration) {
        MIDINote note = new MIDINote(PitchFactory.getPitch(pitch).getMidiNumber());
        note.setStartBeat(startBeat);
        note.setDuration(duration);
        note.setMidiTrack(this);
        notes.add(note);
        this.changes.firePropertyChange(ADD, null, this);
        return this;
    }

    /**
     * Removes the specified {@code MIDINote} from the track.
     * <p>
     * {@code PropertyChange MIDITrack.REMOVE} is fired.
     * 
     * <pre>
     * {@code
     * MIDITrack track = new MIDITrack();
     * MIDINote note = new MIDINote(Pitch.C5);
     * track.add(note);
     * track.remove(note);
     * }
     * </pre>
     * 
     * 
     * @param note
     *            The MIDINote to remove
     * @see com.rockhoppertech.music.midi.js.MIDINote
     */
    public void remove(MIDINote note) {
        boolean b = false;
        b = notes.remove(note);
        note.setMidiTrack(null);
        if (!b) {
            logger.debug(String.format("%s did not exist in this track%n", note));
            return;
        }
        this.changes.firePropertyChange(REMOVE, null, this);
    }

    public void removePitch(Pitch p) {
        for (Iterator<MIDINote> i = iterator(); i.hasNext();) {
            MIDINote note = i.next();
            if (note.getMidiNumber() == p.getMidiNumber()) {
                i.remove();
                note.setMidiTrack(null);
            }
        }
        this.changes.firePropertyChange(REMOVE, null, this);

        // you'll get a concurrent modification exception if you do this:
        // for(MIDINote n : this) {
        // if(n.getMidiNumber() == p.getMidiNumber()) {
        // this.remove(n);
        // }
        // }
    }

    /**
     * <code>remove</code> a MIDINote at the specified index.
     * <p>
     * {@code PropertyChange MIDITrack.REMOVE} is fired.
     * 
     * <pre>
     * {@code
     * MIDITrack track = new MIDITrack();
     * MIDINote note = new MIDINote(Pitch.C5);
     * track.add(note);
     * track.remove(0);
     * }
     * </pre>
     * 
     * 
     * @param index
     *            an <code>int</code> value
     * @see com.rockhoppertech.music.midi.js.MIDINote
     */
    public void remove(int index) {
        MIDINote note = get(index);
        notes.remove(note);
        note.setMidiTrack(null);
        this.changes.firePropertyChange(REMOVE, null, this);
    }

    /**
     * <code>remove</code>
     * 
     * @param event
     *            a <code>event</code> value
     */
    public void remove(MIDIEvent event) {
        events.remove(event);
        event.setTrack(null);

    }

    /**
     * <code>merge</code> merges and sorts the additions based on start time.
     * MIDINotes and MIDIEvents are comparable.
     * 
     * Duplicates the notes and events.
     * 
     * {@code PropertyChange MIDITrack.ADD} is fired.
     * 
     * @param l
     *            a <code>MIDITrack</code> value
     * @return this to cascade
     */
    public MIDITrack merge(MIDITrack l) {
        // notes.addAll(l.notes);
        for (MIDINote n : l.notes) {
            MIDINote dupe = n.duplicate();
            this.add(dupe);
        }
        for (MIDIEvent e : l.events) {
            MIDIEvent dupe = e.duplicate();
            this.add(dupe);
        }
        sortByStartBeat();
        this.changes.firePropertyChange(ADD, null, this);
        return this;
    }

    /**
     * Removes the notes and events from this track, and set's the track
     * reference for each back to the parameter track.
     * 
     * @param l
     *            another MIDITrack
     * @return this to cascade
     */
    public MIDITrack unmerge(MIDITrack l) {
        for (MIDINote n : l.notes) {
            this.remove(n);
            n.setMidiTrack(l);
        }
        for (MIDIEvent e : l.events) {
            this.remove(e);
            e.setTrack(l);
        }
        return this;
    }

    // TODO need this?
    // public MIDITrack merge(MIDITrack l, int index, boolean sort) {
    // notes.addAll(index, l.notes);
    //
    //
    //
    // if (sort) {
    // Collections.sort(notes);
    // }
    // return this;
    // }

    public int getResolution() {
        int resolution = 480;
        if (this.score != null) {
            resolution = this.score.getResolution();
        }
        return resolution;
    }

    public void sort(Comparator<MIDINote> c) {
        Collections.sort(notes, c);
    }

    /**
     * Note and MIDIEvent implement comparable which compares start beats.
     */
    public void sortByStartBeat() {
        Collections.sort(notes);
        Collections.sort(events);
    }

    /**
     * This does not change the start beats. It orders the list by pitch. This
     * is useful for chord production.
     */
    public void sortByAscendingPitches() {
        Comparator<MIDINote> comp = new AscendingPitchComparator();
        Collections.sort(notes, comp);
    }

    public MIDITrack insertListAtIndex(MIDITrack newlist, int index) {
        // this.notes.addAll(index, newlist.notes);
        newlist.sortByStartBeat();

        List<MIDINote> beginning = notes.subList(0, index);
        List<MIDINote> end = notes.subList(index, notes.size());
        MIDITrack n = new MIDITrack(beginning);
        MIDITrack endlist = new MIDITrack(end);

        double endBeat = n.getEndBeat();
        newlist.map(new StartBeatModifier(NoteModifier.Operation.ADD, endBeat));
        for (MIDINote note : newlist.notes) {
            MIDINote dupe = note.duplicate();
            n.add(dupe);
        }
        endBeat = n.getEndBeat();
        endlist.map(new StartBeatModifier(NoteModifier.Operation.ADD, endBeat));
        for (MIDINote note : endlist.notes) {
            MIDINote dupe = note.duplicate();
            n.add(dupe);
        }
        return n;

        // this.sequential();
    }

    /**
     * this is cumulative.
     * 
     * @param track
     *            the track
     * @param n
     *            number of times to append
     * @return a MIDITrack
     */
    public MIDITrack appendNTimes(MIDITrack track, int n) {
        MIDITrack r = null;
        for (int i = 0; i < n; i++) {
            r = this.append(track, 0);
        }
        return r;
    }

    /**
     * Appends the track to the current one. The start beats of the parameter
     * track are modified to start after the current track.
     * 
     * @param track
     *            the track to append
     * @return this to cascade
     */
    public MIDITrack append(MIDITrack track) {
        return this.append(track, 0);
    }

    /**
     * Append the entire track with the specified gap.
     * 
     * @param track
     *            the track to append
     * @param gap
     *            the duration between the end of the track and the appended
     *            track
     * @return this to cascade
     */
    public MIDITrack append(MIDITrack track, double gap) {
        int fromIndex = 0;
        int toIndex = track.size();
        return this.append(track, gap, fromIndex, toIndex);
    }

    /**
     * Appends the parameter track to the current one with an optional gap
     * between them. A negative value for the gap will overlap the tracks. The
     * Notes are cloned.
     * 
     * {@code nl.append(nl2, 0).append(nl3, 0);}
     * 
     * @param track
     *            the track
     * @param gap
     *            the durational space between the tracks in beats
     * @param fromIndex
     *            low endpoint (inclusive) of the subList.
     * @param toIndex
     *            high endpoint (exclusive) of the subList.
     * @return this to cascade calls
     */
    public MIDITrack append(MIDITrack track, double gap, int fromIndex, int toIndex) {
        if (this == track) {
            track = new MIDITrack(this);
        }
        double end = getEndBeat() + gap;

        logger.debug("track {}", track);
        logger.debug("end which is endbeat + gap {}", end);
        logger.debug("gap {}", gap);
        logger.debug("this.getEndBeat() {}", getEndBeat());
        logger.debug("this.size() {}", size());

        // case if the track appended to is empty.
        if (size() == 0) {
            end = 1d;
            track.map(new StartBeatModifier(NoteModifier.Operation.SET, end));
            logger.debug("setting to end {}", end);
        } else {
            // track
            // .map(new StartBeatModifier(NoteModifier.Operation.ADD, end));
            // logger.debug("adding to end {} ", end);
        }

        logger.debug("track after start beat mod {}", track);

        List<MIDINote> sub = track.notes.subList(fromIndex, toIndex);
        // sub.get(0).setStartBeat(end);
        // logger.debug("sublist {} from {} to {}", sub, fromIndex, toIndex);
        // sub = sequential(sub);
        // logger.debug("sublist after mod {} END {}", sub, end);

        double substart = sub.get(0).getStartBeat();
        for (MIDINote note : sub) {
            MIDINote noteClone = note.duplicate();
            logger.debug("clone {}", noteClone);
            noteClone.setStartBeat(noteClone.getStartBeat() + end - substart);
            this.add(noteClone);
        }
        // this.notes.addAll(sub);
        return this;
    }

    /**
     * <code>remove</code>
     * 
     * @param l
     *            a <code>MIDITrack</code> value
     */
    public void remove(MIDITrack l) {
        notes.removeAll(l.notes);
    }

    /**
     * <code>retain</code> discards all MIDINotes that are not in the specified
     * list.
     * 
     * @param l
     *            a <code>MIDITrack</code> value
     * @return true on success
     */
    public boolean retain(MIDITrack l) {
        boolean ret = notes.retainAll(l.notes);
        logger.debug("retainAll returned ", ret);
        return ret;
    }

    /**
     * <code>clear</code> emptys the track.
     * 
     */
    public void clear() {
        notes.clear();
        events.clear();
    }

    /**
     * <code>getStartBeat</code> returns the start beat of the first Note. track
     * is sorted on the start beat because Note implements Comparable.
     * 
     * @return a <code>double</code> value
     */
    public double getStartBeat() {
        if (notes.isEmpty()) {
            return 0d;
        }
        MIDINote first = notes.get(0);
        return first.getStartBeat();
    }

    /**
     * <code>getEndBeat</code>
     * 
     * @return a <code>double</code> value
     */
    public double getEndBeat() {
        double endBeat = 1d;
        for (MIDINote n : notes) {
            double d = n.getEndBeat();
            if (d > endBeat) {
                endBeat = d;
            }
        }
        return endBeat;
    }

    public double getShortestDuration() {
        double shortest = Double.MAX_VALUE;
        for (MIDINote n : notes) {
            double d = n.getDuration();
            if (d < shortest) {
                shortest = d;
            }
        }
        return shortest;
    }

    public double getLongestDuration() {
        double longest = Double.MIN_VALUE;
        for (MIDINote n : notes) {
            double d = n.getDuration();
            if (d > longest) {
                longest = d;
            }
        }
        return longest;
    }

    /**
     * <code>getPitchIntervals</code> returns intervals in relation to each
     * other.
     * 
     * @return an <code>int[]</code> value
     */
    public int[] getPitchIntervals() {
        int len = notes.size();
        int[] intervals = new int[len - 1];
        for (int i = 0; i < intervals.length; i++) {
            MIDINote i1 = notes.get(i);
            MIDINote i2 = notes.get(i + 1);
            intervals[i] = i2.getMidiNumber() - i1.getMidiNumber();
        }
        return intervals;
    }

    /**
     * There intervals are in relation to the first MIDINote's MIDI number. Like
     * a Chord.
     * 
     * @return an array of intervals
     */
    public int[] getPitchIntervalsAbsolute() {
        int len = notes.size();
        int[] intervals = new int[len - 1];
        int root = notes.get(0).getMidiNumber();
        for (int i = 1; i <= intervals.length; i++) {
            MIDINote i1 = notes.get(i);
            intervals[i - 1] = i1.getMidiNumber() - root;
        }
        return intervals;
    }

    /**
     * Reset each note's startBeat. The pad is 0.
     * 
     * @return this
     */
    public MIDITrack sequential() {

        return this.sequential(0);
    }

    /**
     * Reset each note's startbeat in the track to be sequential based on the
     * duration. The first note is unaffected.
     * 
     * @param pad
     *            amount added between the notes
     * @return this to cascade calls
     */
    public MIDITrack sequential(int pad) {
        int size = size();
        if (size == 0) {
            return this;
        }
        MIDINote n = notes.get(0);
        double s = n.getStartBeat();
        double d = n.getDuration();
        logger.debug("startbeat0: {}", s);
        logger.debug("dur0: {}", d);

        for (int i = 1; i < size; i++) {
            MIDINote nn = notes.get(i);
            logger.debug("old startbeat:" + nn.getStartBeat());
            nn.setStartBeat(s + d + pad);
            logger.debug("new startbeat:" + nn.getStartBeat());
            s = nn.getStartBeat();
            d = nn.getDuration();

        }

        this.changes.firePropertyChange(MODIFIED, null, this);
        return this;
    }

    /**
     * Modify the start beats of the given list to be sequential.
     * 
     * Fires MODIFIED.
     * 
     * @param list
     *            a {@code List} of {@code MIDINote}s
     * @return a sequential {@code List} of {@code MIDINote}s
     */
    public List<MIDINote> sequential(List<MIDINote> list) {
        int size = list.size();
        MIDINote n = list.get(0);
        double s = n.getStartBeat();
        double d = n.getDuration();
        logger.debug("startbeat0: {}", s);
        logger.debug("dur0: {}", d);

        for (int i = 1; i < size; i++) {
            MIDINote nn = list.get(i);
            logger.debug("old startbeat: {}", nn.getStartBeat());
            nn.setStartBeat(s + d);
            logger.debug("new startbeat: {}", nn.getStartBeat());
            s = nn.getStartBeat();
            d = nn.getDuration();

        }
        this.changes.firePropertyChange(MODIFIED, null, this);
        return list;
    }

    /**
     * <code>map</code> is the GoF visitor(331) design pattern. It loops through
     * all the MIDINotes and applies the modifier to each note. Sort of like
     * mapcar in LISP (but Java does not have lambdas - yet - so you need the
     * NoteModifier interface).
     * 
     * <pre>
     * {@code
     * MIDITrack track = new MIDITrack();
     * MIDINote note = new MIDINote(Pitch.C5);
     * track.add(note);
     * track.map(new DurationModifier(1d, Modifier.Operation.ADD));
     * }
     * </pre>
     * 
     * @param mod
     *            a <code>NoteModifier</code> implementation.
     * 
     * 
     * @see com.rockhoppertech.music.modifiers.Modifier
     * @see com.rockhoppertech.music.modifiers.NoteModifier
     * @see com.rockhoppertech.music.modifiers.ChannelModifier
     * @see com.rockhoppertech.music.modifiers.DurationModifier
     * @see com.rockhoppertech.music.modifiers.InstrumentModifier
     * @see com.rockhoppertech.music.modifiers.PitchModifier
     * @see com.rockhoppertech.music.modifiers.StartBeatModifier
     * @return this to cascade calls
     */
    public MIDITrack map(NoteModifier mod) {
        for (MIDINote n : this) {
            mod.modify(n);
        }
        this.changes.firePropertyChange(MODIFIED, null, this);
        return this;
    }

    /**
     * {@code map} is the GoF visitor(331) design pattern. It loops through all
     * the MIDINotes and applies the modifier to each note. Sort of like mapcar
     * in LISP (but Java does not have lambdas - yet - so you need the
     * {@code MIDINoteModifier} interface).
     * 
     * <pre>
     * {@code
     * MIDITrack track = new MIDITrack();
     * MIDINote note = new MIDINote(Pitch.C5);
     * track.add(note);
     * track.map(new VelocityModifier(1d, Modifier.Operation.ADD));
     * }
     * </pre>
     * 
     * @param mod
     *            a {@code MIDINoteModifier} implementation.
     * 
     * 
     * @see com.rockhoppertech.music.modifiers.Modifier
     * @see com.rockhoppertech.music.modifiers.MIDINoteModifier
     * @see com.rockhoppertech.music.modifiers.ChannelModifier
     * @see com.rockhoppertech.music.modifiers.InstrumentModifier
     * @return this to cascade calls
     */
    public MIDITrack map(MIDINoteModifier mod) {
        for (MIDINote n : this) {
            mod.modify(n);
        }
        this.changes.firePropertyChange(MODIFIED, null, this);

        return this;
    }

    /**
     * {@code map} calls the {@code NoteModifier}'s map only if the note's start
     * beat is after the specified value.
     * 
     * @param mod
     *            a <code>NoteModifier</code> implementation.
     * @param after
     *            a <code>double</code> value. Modify notes only after this
     *            start beat.
     * 
     * @see com.rockhoppertech.music.midi.js.MIDITrack#map(NoteModifier)
     * @return this to cascade calls
     */
    public MIDITrack map(NoteModifier mod, double after) {
        for (MIDINote n : this) {
            if (n.getStartBeat() >= after) {
                mod.modify(n);
            }
        }
        this.changes.firePropertyChange(MODIFIED, null, this);

        return this;
    }

    /**
     * {@code map} calls the {@code MIDINoteModifier}'s map only if the note's
     * start beat is after the specified value.
     * 
     * @param mod
     *            a MIDINoteModifier
     * @param after
     *            modify MIDINotes only after this start beat
     * @return this to cascade calls
     */
    public MIDITrack map(MIDINoteModifier mod, double after) {
        for (MIDINote n : this) {
            if (n.getStartBeat() >= after) {
                mod.modify(n);
            }
        }
        this.changes.firePropertyChange(MODIFIED, null, this);

        return this;
    }

    /**
     * {@code map} calls the {@code NoteModifier}'s map only if the note's start
     * beat is after the specified value and before the before the param.
     * 
     * @param mod
     *            a <code>NoteModifier</code> value
     * @param after
     *            a <code>double</code> value
     * @param before
     *            a <code>double</code> value
     * @see com.rockhoppertech.music.midi.js.MIDITrack#map(NoteModifier)
     * @return this to cascade calls
     */
    public MIDITrack map(NoteModifier mod, double after, double before) {
        for (MIDINote n : this) {
            double s = n.getStartBeat();
            if (s >= after && s <= before) {
                mod.modify(n);
            }
        }
        this.changes.firePropertyChange(MODIFIED, null, this);

        return this;
    }

    /**
     * {@code map} calls the {@code MIDINoteModifier}'s map only if the note's
     * start beat is after the specified value and before the before the param.
     * 
     * @param mod
     *            a <code>MIDINoteModifier</code> instance
     * @param after
     *            a <code>double</code> value
     * @param before
     *            a <code>double</code> value
     * @see com.rockhoppertech.music.midi.js.MIDITrack#map(MIDINoteModifier)
     * @return this to cascade calls
     */
    public MIDITrack map(MIDINoteModifier mod, double after, double before) {
        for (MIDINote n : this) {
            double s = n.getStartBeat();
            if (s >= after && s <= before) {
                mod.modify(n);
            }
        }
        this.changes.firePropertyChange(MODIFIED, null, this);

        return this;
    }

    /*
     * Modify MIDINotes only if the specified criteria tests true.
     * 
     * This is the preferred way. MIDITrack has two special criteria for start
     * beats. With this you can build arbitrary criteria. If the specified
     * critera are true then the note is modified.
     * 
     * <blockquote>
     * 
     * <pre> MIDITrack track = new MIDITrack(); MIDINote note = new
     * MIDINote(Pitch.C5); track.add(note); etc. // before or equal to start
     * beat 3 and the pitch is lower than or equal to E5 ModifierCriteria
     * criteria = new StartBeatCriteria(3d, ModifierCriteria.Operator.LT_EQ, new
     * PitchCriteria(Pitch.E5, ModifierCriteria.Operator.LT_EQ, null));
     * track.map(new StartBeatModifier(1d, NoteModifier.Operation.SET),
     * criteria); </pre>
     * 
     * </blockquote>
     * 
     * 
     * <p> I'm also playing around with commons
     * <code>org.apache.commons.collections.Predicate</code> in package
     * com.rockhoppertech.music.midi.js.modifiers.commons. Unfortunately commons
     * has not been updated for Java 5 yet. And their Closure class; what will
     * happen when closures are official?
     * 
     * @param mod The NoteModifier
     * 
     * @param criteria The ModifierCritera
     * 
     * @see com.rockhoppertech.music.midi.js.MIDITrack#map(NoteModifier)
     * 
     * @see com.rockhoppertech.music.modifiers.criteria.ModifierCriteria
     */
    // public MIDITrack map(NoteModifier mod, ModifierCriteria criteria) {
    // for (MIDINote n : this) {
    // if (criteria.test(n)) {
    // if (logger.isDebugEnabled()) {
    // logger.debug("Passed " + n);
    // }
    // mod.modify(n);
    // } else {
    // if (logger.isDebugEnabled()) {
    // logger.debug("Failed " + n);
    // }
    // }
    // }
    // return this;
    // }

    // public MIDITrack map(MIDINoteModifier mod, ModifierCriteria criteria) {
    // for (MIDINote n : this) {
    // if (criteria.test(n)) {
    // if (logger.isDebugEnabled()) {
    // logger.debug("Passed " + n);
    // }
    // mod.modify(n);
    // } else {
    // if (logger.isDebugEnabled()) {
    // logger.debug("Failed " + n);
    // }
    // }
    // }
    // return this;
    // }

    /**
     * Creates a {@code MIDIPerformer} and plays this track.
     */
    public void play() {
        MIDIPerformer perf = new MIDIPerformer();
        perf.play(this);
    }

    /**
     * <code>retrograde</code> reverses the order of the MIDINotes. The start
     * beats are not in order! It's up to you to change those if you so desire.
     * You might use sequential() followed by setStartBeat().
     * 
     * The original track is not modified either.
     * 
     * @return a new <code>MIDITrack</code>
     */
    public final MIDITrack retrograde() {
        List<MIDINote> retro = new ArrayList<>();

        for (MIDINote n : notes) {
            retro.add((MIDINote) n.duplicate());
        }
        Collections.reverse(retro);
        MIDITrack t = new MIDITrack(retro);
        // double startBeat = t.get(0).getStartBeat() + 1d; // beats are 1 based
        // logger.debug("retro start is {}", startBeat);
        // StartBeatModifier m = new StartBeatModifier(Operation.SUBTRACT,
        // startBeat);
        // t.map(m);
        // t.sequential();
        return t;
    }

    /**
     * Iterate through the track to find the {@code MIDINote} with the lowest
     * pitch midi number.
     * 
     * @return the lowest note
     */
    public MIDINote getLowestPitchedNote() {
        MIDINote n = new MIDINote(Pitch.GS9);
        for (MIDINote e : notes) {
            if (e.getMidiNumber() < n.getMidiNumber()) {
                n = e;
            }
        }
        return n;
    }

    /**
     * Iterate through the track to find the {@code MIDINote} with the highest
     * pitch midi number.
     * 
     * @return the lowest note
     */
    public MIDINote getHighestPitchedNote() {
        MIDINote n = new MIDINote(Pitch.A0);
        for (MIDINote e : notes) {
            if (e.getMidiNumber() > n.getMidiNumber()) {
                n = e;
            }
        }
        return n;
    }

    /**
     * Retrieve the duration of the entire MIDItrack. Literally the end beat
     * minus the startbeat.
     * 
     * @return the duration of the track in beats
     */
    public double getDuration() {
        return getEndBeat() - getStartBeat();
    }

    /**
     * Get a space delimited string representation of the durations of all the
     * {@code MIDINote}s in the track.
     * 
     * @param n
     *            a {@code MIDITrack}
     * @return a string
     */
    public static String getDurationsAsString(MIDITrack n) {
        StringBuilder sb = new StringBuilder();
        for (MIDINote note : n) {
            sb.append(note.getDuration()).append(' ');

        }
        return sb.toString();
    }

    /**
     * Get a space delimited string representation of the pitches of all the
     * {@code MIDINote}s in the track.
     * 
     * @param n
     *            a {@code MIDITrack}
     * @return a string
     */
    public static String getPitchesAsString(MIDITrack n) {
        StringBuilder sb = new StringBuilder();
        for (MIDINote note : n) {
            sb.append(note.getPitch().getPreferredSpelling()).append(' ');
            // sb.append(PitchFormat.getInstance().format(note.getPitch()))
            // .append(' ');
        }
        return sb.toString();
    }

    public static String getPitchesMIDINumbersAsString(MIDITrack n) {
        StringBuilder sb = new StringBuilder();
        for (MIDINote note : n) {
            sb.append(note.getPitch().getMidiNumber()).append(' ');
        }
        return sb.toString();
    }

    /**
     * Get a space delimited string representation of the start beats of all the
     * {@code MIDINote}s in the track.
     * 
     * @param n
     *            a {@code MIDITrack}
     * @return a string
     */
    public static String getStartBeatsAsString(MIDITrack n) {
        StringBuilder sb = new StringBuilder();
        for (MIDINote note : n) {
            sb.append(note.getStartBeat()).append(' ');
        }
        return sb.toString();
    }

    /**
     * <p>
     * Get the index of the specified note.
     * </p>
     * 
     * @param note
     *            a note in the track
     * @return the note's index or -1 if not found
     */
    public int indexOfNote(MIDINote note) {
        return notes.indexOf(note);
    }

    /**
     * {@code sublist} will get MIDINotes with start times between the given
     * times (inclusive). The MIDINotes contained are "live" - not cloned unless
     * you specify the clone parameter
     * 
     * @param after
     *            a <code>double</code> value
     * @param before
     *            a <code>double</code> value
     * @param clone
     *            clone the Notes
     * @return a <code>MIDITrack</code> value
     */
    public MIDITrack sublist(double after, double before, boolean clone) {
        MIDITrack list = new MIDITrack();
        for (MIDINote n : this) {
            double s = n.getStartBeat();
            if (s >= after && s <= before) {
                if (clone) {
                    list.add((MIDINote) n.duplicate());
                } else {
                    list.add(n);
                }
            }
        }
        return list;
    }

    // using apache collections predicates would make this nonsense easier
    /**
     * {@code sublist} will get MIDINotes with start times between the given
     * time.
     * 
     * @param after
     *            after this start beat
     * @param before
     *            before this start beat
     * @param clone
     *            make a clone
     * @param endInclusive
     *            include the end
     * @return
     */
    public MIDITrack sublist(double after, double before, boolean clone, boolean endInclusive) {
        MIDITrack list = new MIDITrack();
        for (MIDINote n : this) {
            double s = n.getStartBeat();
            if (endInclusive == false) {
                if (s >= after && s < before) {
                    if (clone) {
                        list.add((MIDINote) n.duplicate());
                    } else {
                        list.add(n);
                    }
                }
            } else {
                if (s >= after && s <= before) {
                    if (clone) {
                        list.add((MIDINote) n.duplicate());
                    } else {
                        list.add(n);
                    }
                }
            }
        }
        return list;
    }

    /**
     * MIDINotes are not cloned.
     * 
     * @param after
     *            a stat beat
     * @param before
     *            an end beat
     * @return a MIDITrack built from the sublist
     */
    public MIDITrack sublist(double after, double before) {
        return sublist(after, before, false);
    }

    /**
     * Create a sublist of the original MIDITrack beginning at the specified
     * index and continuing to the end of the list.
     * 
     * @param index
     *            The index to begin copying from.
     * @return a MIDITrack built from the sublist
     */
    public MIDITrack sublist(int index) {
        List<MIDINote> sl = notes.subList(index, notes.size());
        MIDITrack list = new MIDITrack(sl);
        return list;
    }

    /**
     * <code>sublist</code> is sort of a band pass filter. Returns a MIDITrack
     * with MIDINotes in the given range. No modifications to start times or any
     * other parameters.
     * 
     * @param low
     *            a <code>Pitch</code> value
     * @param high
     *            a <code>Pitch</code> value
     * @return a <code>MIDITrack</code> instance
     */
    public MIDITrack sublist(Pitch low, Pitch high) {
        MIDITrack list = new MIDITrack();
        for (MIDINote n : this) {
            int num = n.getMidiNumber();
            if (num >= low.getMidiNumber() && num <= high.getMidiNumber()) {
                list.add(n);
            }
        }
        // for (Iterator it = this.iterator(); it.hasNext();) {
        // MIDINote n = (MIDINote) it.next();
        // int num = n.getMidiNumber();
        // if (num >= low.getMidiNumber() && num <= high.getMidiNumber()) {
        // list.add(n);
        // }
        // }
        return list;
    }

    /**
     * <code>getInversion</code> creates a MIDITrack from this track's inverted
     * intervals.
     * 
     * @return a <code>MIDITrack</code> instance
     */
    public MIDITrack getInversion() {
        int[] intervals = getPitchIntervals();
        for (int i = 0; i < intervals.length; i++) {
            intervals[i] *= -1;
        }
        int base = notes.get(0).getMidiNumber();

        logger.debug("base ", base);
        // ArrayUtils.printArray(intervals, logger);

        return MIDITrackFactory.createFromIntervals(intervals, base, 1, false);
    }

    /**
     * Creates a new List of {@code List<MIDINote>}s organized by the
     * MIDItrack's note's channels. The original MIDItrack is unchanged.
     * 
     * @return A List of a List of MIDINotes organized by channel
     */
    public List<List<MIDINote>> channelize() {
        List<List<MIDINote>> channelList = new ArrayList<List<MIDINote>>(16);
        for (int i = 0; i < 16; i++) {
            channelList.add(i, new ArrayList<MIDINote>());
        }
        for (MIDINote n : notes) {
            int channel = n.getChannel();
            List<MIDINote> list = channelList.get(channel);
            list.add(n);
        }
        // remove all the empty ones
        List<List<MIDINote>> channels = new ArrayList<List<MIDINote>>();
        for (List<MIDINote> list : channelList) {
            if (list.isEmpty() == false) {
                channels.add(list);
            }
        }
        return channels;
    }

    /**
     * <code>setStartBeat</code> sets the start beat of the entire track. This
     * is useful in a GUI where you are moving track representations around. The
     * track needs to be sequential. If the start beats are not in ascending
     * order, it won't work. e.g. When you call retrograde(), the start beats
     * are not in ascending order.
     * 
     * @param b
     *            a <code>double</code> value
     */
    public void setStartBeat(double b) {
        double now = getStartBeat();
        double diff = now - b;

        if (b < 0) {
            logger.error("setStartBeat < 0");
            throw new IllegalArgumentException("Start beat < 0");
        }

        for (MIDINote n : notes) {
            double s = n.getStartBeat();
            double newStart = s - diff;
            logger.debug("old start {} new start {} diff {}", s, newStart, diff);
            n.setStartBeat(newStart);
            // n.setStartBeat(s - newStart);
        }
        this.changes.firePropertyChange(STARTBEAT, null, this);

        // StartBeatModifier m = new
        // StartBeatModifier(Modifier.Operation.SUBTRACT);
        // m.setValues(new double[]{diff});
        // this.map(m);
    }

    /**
     * <p>
     * Change the start beat of note at the specified index.
     * </p>
     * 
     * @param selectedIndex
     *            the index
     * @param i
     *            the new start beat
     */
    public void changeStartBeatOfNoteAtIndex(int selectedIndex, double i) {
        MIDINote n = get(selectedIndex);
        n.setStartBeat(i);
        this.changes.firePropertyChange(MODIFIED, null, this);

    }

    /**
     * <p>
     * Change the duration of note at the specified index.
     * </p>
     * 
     * @param selectedIndex
     *            the index
     * @param i
     *            the new duration
     */
    public void changeDurationOfNoteAtIndex(int selectedIndex, double i) {
        MIDINote n = get(selectedIndex);
        n.setDuration(i);
        this.changes.firePropertyChange(MODIFIED, null, this);

    }

    /**
     * <code>getNoteDurations</code> creates and returns an array of the
     * durations of all the {@code MIDINote}s in this track.
     * 
     * @return a <code>double[]</code> value
     */
    public double[] getNoteDurations() {
        int len = notes.size();
        double[] durations = new double[len];
        for (int i = 0; i < len; i++) {
            MIDINote note = notes.get(i);
            durations[i] = note.getDuration();
        }
        return durations;
    }

    /**
     * <p>
     * change the end beat of the note at this index.
     * </p>
     * 
     * @param selectedIndex
     *            the index.
     * @param d
     *            the end beat
     */
    public void changeEndBeatOfNoteAtIndex(int selectedIndex, double d) {
        MIDINote n = get(selectedIndex);
        n.setEndBeat(d);
        this.changes.firePropertyChange(MODIFIED, null, this);

    }

    /**
     * Get the MIDINote at the specified index and set its Pitch to the
     * specified pitch.
     * <p>
     * Fires a MIDItrack.PITCH_CHANGE event.
     * 
     * @param selectedIndex
     *            The index of the MIDINote to be modified.
     * @param num
     *            The MIDI number of the pitch to set
     */
    public void changePitchOfNoteAtIndex(final int selectedIndex, final int num) {
        MIDINote n = get(selectedIndex);
        n.setMidiNumber(num);
        this.changes.firePropertyChange(MODIFIED, null, this);

    }

    /**
     * Returns the {@code Map} of tempi in this {@code MIDITrack}.
     * 
     * Does not create a defensive copy.
     * 
     * @return the tempoMap
     */
    public NavigableMap<Double, Integer> getTempoMap() {
        return tempoMap;
    }

    /**
     * Does not create a defensive copy.
     * 
     * @return the keySignatureMap
     */
    public NavigableMap<Double, KeySignature> getKeySignatures() {
        return keySignatureMap;
    }

    /**
     * Returns the {@code Map} of time signatures in this {@code MIDITrack}.
     * Does not create a defensive copy. Wait until Java 8.
     * 
     * @return the timeSignatures
     */
    public NavigableMap<Double, TimeSignature> getTimeSignatures() {
        // return Collections.unmodifiableMap(this.timeSignatures);
        return timeSignatures;
    }

    /**
     * @param tempoMap
     *            the tempi to set
     */
    public final void setTempoMap(NavigableMap<Double, Integer> tempoMap) {
        this.tempoMap = tempoMap;
        this.changes.firePropertyChange(MODIFIED, null, this);

    }

    /**
     * @param timeSignatures
     *            the timeSignatures to set
     */
    public void setTimeSignatures(NavigableMap<Double, TimeSignature> timeSignatures) {
        this.timeSignatures = timeSignatures;
        this.changes.firePropertyChange(MODIFIED, null, this);

    }

    /**
     * This does no checking on whether the designated beat actually makes
     * sense.
     * 
     * @param beat
     *            the beat where this should occur.
     * @param tempo
     *            the tempo in BPM
     */
    public void addTempoAtBeat(double beat, Integer tempo) {
        tempoMap.put(beat, tempo);
        this.changes.firePropertyChange(ADD, null, this);

    }

    /**
     * This does no checking on whether the designated beat actually makes
     * sense.
     * 
     * @param beat
     *            the beat where this should occur.
     * @param ts
     *            the time signature
     */
    public final void addTimeSignatureAtBeat(double beat, TimeSignature ts) {
        timeSignatures.put(beat, ts);
        this.changes.firePropertyChange(ADD, null, this);

    }

    /**
     * Add a time signature to the track.
     * 
     * @param beat
     *            the beat to insert the time signature
     * @param numerator
     *            the time signature numerator
     * @param denominator
     *            the time signature denominator
     */
    public void addTimeSignatureAtBeat(double beat, int numerator, int denominator) {
        timeSignatures.put(beat, new TimeSignature(numerator, denominator));
        this.changes.firePropertyChange(ADD, null, this);

    }

    /**
     * Get the {@code TimeSignature} at the specified beat. Returns null if
     * there is no time signature at that beat.
     * 
     * @param beat
     *            the beat
     * @return the {@code TimeSignature} or null
     */
    public TimeSignature getTimeSignatureAtBeat(double beat) {
        TimeSignature ts = null;
        Double time = timeSignatures.floorKey(beat);
        if (time != null) {
            ts = timeSignatures.get(time);
        }
        return ts;
    }

    /**
     * Insert a key signature into this track.
     * 
     * @param beat
     *            the beat to insert the key signature
     * @param keysig
     *            the keysignature
     */
    public void addKeySignatureAtBeat(double beat, KeySignature keysig) {
        keySignatureMap.put(beat, keysig);
        this.changes.firePropertyChange(ADD, null, this);

    }

    /**
     * Get the tempo message at the specified beat. Returns null if there is no
     * message at that beat.
     * 
     * @param beat
     *            the beat "key" in the tempo map.
     * @return the tempo or null
     */
    public Integer getTempoAtBeat(double beat) {
        Integer tempo = null;
        Double key = tempoMap.floorKey(beat);
        if (key != null) {
            tempo = tempoMap.get(key);
        }
        return tempo;
    }

    /**
     * Get the key signature at the specified beat. Returns null if there is no
     * key signature at that beat.
     * 
     * @param beat
     *            the beat
     * @return the key signature or null
     */
    public KeySignature getKeySignatureAtBeat(double beat) {
        KeySignature ks = null;
        Double key = keySignatureMap.floorKey(beat);
        if (key != null) {
            ks = keySignatureMap.get(key);
        }
        return ks;
    }

    /**
     * Create a {@code List} of all the {@code MIDINote}'s pitch classes.
     * 
     * @return a List of the pitch classes
     */
    public List<Integer> getPitchClasses() {
        List<Integer> pitchClasses = new ArrayList<Integer>();
        for (MIDINote n : this) {
            int pc = n.getPitch().getMidiNumber() % 12;
            pitchClasses.add(pc);
        }
        return pitchClasses;
    }

    /**
     * Create a {@code List} of all the {@code MIDINote}'s MIDI pitch numbers.
     * 
     * @return a List of the pitch's MIDI numbers
     */
    public List<Integer> getPitchesAsIntegers() {
        List<Integer> pitches = new ArrayList<Integer>();
        for (MIDINote n : this) {
            int pc = n.getPitch().getMidiNumber();
            pitches.add(pc);
        }
        return pitches;
    }

    /**
     * Create a {@code List} of all the {@code MIDINote}'s {@code Pitch}es.
     * 
     * @return a list as Pitches
     */
    public List<Pitch> getPitches() {
        List<Pitch> pitches = new ArrayList<Pitch>();
        for (MIDINote n : this) {
            Pitch pc = n.getPitch();
            pitches.add(pc);
        }
        return pitches;
    }

    /**
     * Create and return a {@code Set} of all the {@code MIDINote}'s pitch
     * classes.
     * 
     * @return a Set of the pitch classes.
     */
    public Set<Integer> getPitchClassSet() {
        Set<Integer> pitchClasses = new TreeSet<Integer>();
        for (MIDINote n : this) {
            int pc = n.getPitch().getMidiNumber() % 12;
            pitchClasses.add(pc);
        }
        return pitchClasses;
    }

    /**
     * Modifies all MIDINotes in the Track to use this patch. Use Instrument
     * instead.
     * 
     * @deprecated
     * @param program
     *            The instrument number.
     */
    public void useInstrument(int program) {
        InstrumentModifier mod = new InstrumentModifier(program);
        this.map(mod);
    }

    /**
     * Retrieve the {@code Instrument} used to play this track.
     * 
     * @return the {@code Instrument}
     */
    public Instrument getInstrument() {
        return instrument;
    }

    /**
     * This will change the patch on existing and future MIDINotes in this
     * track.
     * 
     * @param instrument
     *            the {@code Instrument} to set
     */
    public void setInstrument(Instrument instrument) {
        this.instrument = instrument;
        if (this.notes.size() > 0) {
            this.useInstrument(instrument);
        }
        this.changes.firePropertyChange(MODIFIED, null, this);

    }

    /**
     * Changes the patch on all {@code MIDINote}s in this track.
     * 
     * @param instrument
     *            the {@code Instrument}
     */
    public void useInstrument(Instrument instrument) {
        this.instrument = instrument;
        InstrumentModifier mod = new InstrumentModifier(instrument.getPatch().getProgram());
        this.map(mod);
    }

    /**
     * Modifies all {@code MIDINote}s in the Track to use this patch. Use
     * Instrument instead
     * 
     * @deprecated
     * @param bank
     *            The instrument bank.
     * @param program
     *            The instrument number.
     */
    public void useInstrument(int bank, int program) {
        InstrumentModifier mod = new InstrumentModifier(bank, program);
        this.map(mod);
    }

    /**
     * Create a {@code MIDITrack} from the noteString and append it to this
     * track.
     * 
     * @param noteString
     *            a MIDIString
     * @return this to cascade calls
     */
    public MIDITrack append(String noteString) {
        MIDITrack tmp = new MIDITrack(noteString);
        tmp.useInstrument(this.instrument);
        this.append(tmp);
        return this;
        // returning tmp might be more useful
        // return tmp;
    }

    /**
     * This doesn't change the startBeats.
     * 
     * @param noteString
     *            a MIDIString
     */
    public void insertMIDIString(String noteString) {
        // calls track add or append
        midiStringParser.parseString(this, noteString);
    }

    /**
     * Change the channel of every {@code MIDINote} in this track.
     * 
     * @param channel
     *            the new MIDI channel
     * @see ChannelModifier
     */
    public void setChannel(int channel) {
        ChannelModifier mod = new ChannelModifier(Modifier.Operation.SET, channel);
        this.map(mod);
    }

    /**
     * Change the velocity of every {@code MIDINote} in this track.
     * 
     * @param velocity
     *            the new MIDI velocity
     */
    public void setVelocity(int velocity) {
        VelocityModifier mod = new VelocityModifier(Modifier.Operation.SET, velocity);
        this.map(mod);
    }

    /**
     * Adds a meta text message at the specified beat.
     * 
     * @param beat
     *            the beat where the text is inserted
     * @param text
     *            the text to insert
     */
    public void addMetaText(double beat, String text) {
        // ignored, since we're setting the beat afterward
        logger.debug("adding meta text '{}'", text);
        long tick = 0;
        // TODO add these factories to MIDIEvent
        MidiEvent event = MIDIUtils.createMetaTextMessage(tick, MIDIUtils.META_TEXT, text);
        MIDIEvent e = new MIDIEvent(event, this);
        // we're used to thinking in beats starting at 1 not zero.
        e.setStartBeat(beat - 1d);
        this.events.add(e);
        this.changes.firePropertyChange(MODIFIED, null, this);

    }

    /**
     * The opposite of sequential. Sets all the start beats to 1.
     */
    public void chordify() {
        StartBeatModifier mod = new StartBeatModifier(Modifier.Operation.SET, 1d);
        this.map(mod);
    }

    /**
     * Sets all the start beats to the specified beat.
     * 
     * @param beat
     *            the new start beat
     */
    public void chordify(double beat) {
        StartBeatModifier mod = new StartBeatModifier(Modifier.Operation.SET, beat);
        this.map(mod);
    }

    /**
     * Get the {@code MIDINote}s at the specified beat.
     * 
     * @param beat
     *            the beat
     * @return a {@code List} of {@code MIDINote}s at this beat
     */
    // public List<MIDINote> getNotesAtBeat(final double beat) {
    // List<MIDINote> notes = new ArrayList<MIDINote>();
    // logger.debug("looking for  sb {}", beat);
    // for (final MIDINote n : this) {
    // final double s = n.getStartBeat();
    // final double e = n.getEndBeat();
    // logger.debug("checking sb {} of note {}", s, n);
    // Range<Double> range = Range.between(s, e - .0001);
    // logger.debug("range {}", range);
    // if (range.contains(beat)) {
    // logger.debug("adding note {}", n);
    // notes.add(n);
    // }
    // }
    // return notes;
    // }

    public List<MIDINote> getNotesAtBeat(final double beat) {
        return getNotesBetweenStartBeatAndEndBeat(beat, beat + 1d);
    }

    /**
     * Return the notes that have a start beat between the specified beats.
     * 
     * @param startBeat
     *            begin of beat range
     * @param endBeat
     *            not inclusive
     * @return a {@code List} of matching {@code MIDINote}s
     */
    public List<MIDINote> getNotesBetweenStartBeatAndEndBeat(final double startBeat, final double endBeat) {
        List<MIDINote> notes = new ArrayList<MIDINote>();
        Range<Double> range = Range.between(startBeat, endBeat - .00001);
        logger.debug("range {}", range);
        logger.debug("looking for  sb {}", startBeat);
        for (final MIDINote n : this) {
            final double s = n.getStartBeat();
            logger.debug("checking sb {} of note {}", s, n);
            if (range.contains(s)) {
                logger.debug("adding note {}", n);
                notes.add(n);
            }
        }
        return notes;
    }

    /**
     * Change the duration of the entire track. Changes each {@code MIDINote}'s
     * duration and start beat.
     * 
     * @param duration
     *            the new duration
     */
    public void setDuration(double duration) {
        double now = this.getStartBeat();
        logger.debug("new duration {}", duration);
        double d = this.getDuration();
        logger.debug("current duration {}", d);
        double factor = duration / d;
        logger.debug("factor {}", factor);
        this.map(new StartBeatModifier(Operation.MULTIPLY, factor));
        this.map(new DurationModifier(Operation.MULTIPLY, factor));
        logger.debug("final duration {}", this.getDuration());

        this.setStartBeat(now);

        this.changes.firePropertyChange(MODIFIED, null, duration);
    }

    /**
     * Set the {@code MIDINote}s in this track to use the start beats and
     * durations in the provided {@code TimeSeries}.
     * 
     * @param timeSeries
     *            a {@code TimeSeries}
     */
    public void apply(final TimeSeries timeSeries) {
        final double nsize = this.size();
        final double tsize = timeSeries.getSize();
        final int n = (int) Math.round(nsize / tsize + .5);
        final TimeSeries ts = timeSeries.nCopies(n);
        ts.sequential();
        for (final MIDINote note : this) {
            final Timed te = ts.nextTimeEvent();
            note.setStartBeat(te.getStartBeat());
            note.setDuration(te.getDuration());
        }
    }

    /**
     * Serialization method.
     * 
     * @param out
     *            and output stream
     * @throws IOException
     *             if this cannot be serialized
     */
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
    }

    /**
     * Serializaiton method.
     * 
     * @param in
     *            the input stream
     * @throws IOException
     *             if something went wrong
     * @throws ClassNotFoundException
     *             if the class is not found
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // our "pseudo-constructor"
        in.defaultReadObject();
        // now we are a "live" object again
        this.changes = new PropertyChangeSupport(this);
        this.midiStringParser = new MIDIStringParser();
    }

    // JavaBeans event methods.

    public void addPropertyChangeListener(final PropertyChangeListener listener) {
        this.changes.addPropertyChangeListener(listener);
    }

    public void addPropertyChangeListener(final String propertyName, final PropertyChangeListener listener) {
        this.changes.addPropertyChangeListener(propertyName, listener);
    }

    public void removePropertyChangeListener(final PropertyChangeListener listener) {
        this.changes.removePropertyChangeListener(listener);
    }

    /**
     * Removes the property change listener.
     * 
     * @param propertyName
     *            the property name
     * @param listener
     *            the listener
     */
    public void removePropertyChangeListener(final String propertyName, final PropertyChangeListener listener) {
        this.changes.removePropertyChangeListener(propertyName, listener);
    }

    @Override
    public int compareTo(MIDITrack o) {
        if (o.getStartBeat() > o.getStartBeat()) {
            return 1;
        } else if (o.getStartBeat() < o.getStartBeat()) {
            return -1;
        }
        return 0;
    }

    /**
     * @return the userData
     */
    public Serializable getUserData() {
        return userData;
    }

    /**
     * @param userData the userData to set
     */
    public void setUserData(Serializable userData) {
        this.userData = userData;
    }
}