com.rockhoppertech.music.series.time.TimeSeries.java Source code

Java tutorial

Introduction

Here is the source code for com.rockhoppertech.music.series.time.TimeSeries.java

Source

package com.rockhoppertech.music.series.time;

/*
 * #%L
 * Rocky Music Core
 * %%
 * Copyright (C) 1996 - 2014 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.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;
import com.rockhoppertech.collections.CircularArrayList;
import com.rockhoppertech.collections.CircularList;
import com.rockhoppertech.collections.ListUtils;
import com.rockhoppertech.music.DurationParser;
import com.rockhoppertech.music.Note;
import com.rockhoppertech.music.Pitch;
import com.rockhoppertech.music.Timed;
import com.rockhoppertech.music.midi.gm.MIDIGMPatch;
import com.rockhoppertech.music.midi.js.MIDINote;
import com.rockhoppertech.music.midi.js.MIDIPerformer;
import com.rockhoppertech.music.midi.js.MIDITrack;
import com.rockhoppertech.music.midi.js.MIDITrackFactory;
import com.rockhoppertech.music.modifiers.DurationModifier;
import com.rockhoppertech.music.modifiers.Modifier.Operation;
import com.rockhoppertech.music.modifiers.StartBeatModifier;
import com.rockhoppertech.music.modifiers.TimedModifier;

//import com.rockhoppertech.series.Permutations;
//import com.rockhoppertech.series.time.modifiers.criteria.TimeModifierCriteria;

/**
 * Class <code>TimeSeries</code> is a series of {@code TimeEvents}.
 * 
 * 
 * @author <a href="mailto:gene@rockhoppertech.com">Gene De Lisa</a>
 * @since 1.0
 */
public class TimeSeries implements Iterable<TimeEvent>, Serializable {
    /**
     * Serialization.
     */
    private static final long serialVersionUID = 3559349459462266457L;
    static Logger logger = LoggerFactory.getLogger(TimeSeries.class);

    public static final String END_BEAT_CHANGE = "TimeSeries.END_BEAT_CHANGE";
    public static final String DURATION_CHANGE = "TimeSeries.DURATION_CHANGE";
    public static final String START_BEAT_CHANGE = "TimeSeries.START_BEAT_CHANGE";
    public static final String MODIFIED = "TimeSeries.MODIFIED";

    private transient RangeSet<Double> rangeSet = TreeRangeSet.create();
    private CircularArrayList<TimeEvent> list = new CircularArrayList<TimeEvent>();
    private List<Range<Double>> ranges;
    private List<Range<Double>> exactRanges;
    private String name = "Untitled";
    private String description;
    // private int index;
    private transient PropertyChangeSupport changes;

    /**
     * Series are sorted when a time event is added.
     */
    private transient Comparator<? super TimeEvent> timeComparator = new TimeComparator();

    /**
     * Initialize a new empty series.
     */
    public TimeSeries() {
        // this.list = Collections.synchronizedList( new LinkedList() );
        // this.list = new ArrayList<TimeEvent>(); // we do lots of gets/sets
        this.ranges = new ArrayList<Range<Double>>();
        this.exactRanges = new ArrayList<Range<Double>>();
        this.changes = new PropertyChangeSupport(this);
    }

    /**
     * Extract time events from a track to initialize this series.
     * 
     * @param track
     *            a {@code MIDITrack}
     */
    public TimeSeries(final MIDITrack track) {
        this();
        // this.list = new ArrayList<TimeEvent>();
        for (final Note n : track) {
            this.add(new TimeEvent(n.getStartBeat(), n.getDuration()));
        }
    }

    /**
     * Copy Constructor. Clone is evil.
     * 
     * @param timeSeries
     *            a {@code TimeSeries} to copy
     */
    public TimeSeries(final TimeSeries timeSeries) {
        this();
        for (final TimeEvent te : timeSeries.list) {
            this.list.add(new TimeEvent(te.getStartBeat(), te.getDuration()));
        }
        this.sort();
    }

    /**
     * Initialize a series from a duration string.
     * 
     * @param durationString
     *            a duration string
     * @see DurationParser
     */
    public TimeSeries(String durationString) {
        this();
        TimeSeriesFactory.createFromDurationString(this, durationString);
    }

    /**
     * Creates a new {@code TimeEvent} with the specified duration, and adds it
     * to the series.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param duration
     * @return this to cascade calls
     */
    public TimeSeries add(final double duration) {
        final double start = this.getEndBeat();
        return this.add(new TimeEvent(start, duration));
    }

    /**
     * Creates a new {@code TimeEvent} with the specified start andduration, and
     * adds it to the series.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param start a start time
     * @param duration a duration
     * @return this to cascade calls
     */
    public TimeSeries add(final double start, final double duration) {
        logger.debug("adding a new time event, start {} durtion {}", start, duration);
        return this.add(new TimeEvent(start, duration));
    }

    /**
     * Adds the TimeEvent at its given time. (i.e. the series is resorted)
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param t
     */
    public TimeSeries add(final TimeEvent t) {
        logger.debug("adding a new time event {}", t);
        this.list.add(t);
        Collections.sort(this.list, this.timeComparator);
        this.addRange(t.getOpenRange());
        // this.addExactRange(t.getClosedRange());
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
        return this;
    }

    /**
     * Append a {@code TimeSeries} to another. Neither series are modified. A
     * new instance is returned.
     * 
     * @param ts1
     *            the first {@code TimeSeries}
     * @param ts2
     *            the second {@code TimeSeries}
     * @return a new {@code TimeSeries}
     */
    public static TimeSeries append(final TimeSeries ts1, final TimeSeries ts2) {
        final TimeSeries ts = new TimeSeries(ts1);
        return ts.append(ts2);
    }

    /**
     * Create a series from a track.
     * 
     * @param track
     *            the source {@code MIDITrack}
     * @return a new {@code TimeSeries}
     */
    public static TimeSeries extract(final MIDITrack track) {
        final TimeSeries ts = new TimeSeries();
        for (final MIDINote note : track) {
            final TimeEvent te = new TimeEvent(note.getStartBeat(), note.getDuration());
            ts.add(te);
        }
        return ts;
    }

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

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

    /**
     * Adds a range. No TimeEvents are created here. This is called internally
     * when an event is added.
     * 
     * @param r
     *            a {@code Range}
     * @return this to cascade calls
     */
    private TimeSeries addRange(final Range<Double> r) {
        this.ranges.add(r);
        rangeSet.add(r);
        return this;
    }

    // private TimeSeries addRange(final Range<Double> r) {
    //
    // // first one
    // if (this.ranges.isEmpty()) {
    // this.ranges.add(r);
    // } else {
    // // if the new even overlaps an existing event,
    // // merge the 2 ranges into one
    // boolean added = false;
    // for (final Range<Double> r1 : this.ranges) {
    // // logger.debug("r1 " + r1 + " new r " + r);
    //
    // if (r1.overlaps(r)) {
    // logger.debug("overlaps");
    // r1.setMax(Math.max(r1.upperEndpoint(),
    // r.upperEndpoint()));
    // added = true;
    // }
    // }
    // // otherwise just add it
    // if (added == false) {
    // this.ranges.add(r);
    // // logger.debug("just adding r2 " + r);
    // }
    // }
    // return this;
    // }

    // private TimeSeries addExactRange(final Range<Double> r) {
    // // first one
    // if (this.exactRanges.isEmpty()) {
    // this.exactRanges.add(r);
    // } else {
    //
    // r.containsAll(r.);
    //
    // r.containsAll(Ints.asList(1, 2, 3));
    // //Range.closed(3, 5).span(Range.open(5, 10)); // returns [3, 10)
    //
    // Range
    // // if the new even overlaps an existing event,
    // // merge the 2 ranges into one
    // boolean added = false;
    // for (final Range<Double> r1 : this.exactRanges) {
    // // logger.debug("r1 " + r1 + " new r " + r);
    // if (r1.overlaps(r)) {
    // logger.debug("overlaps");
    // r1.setMax(Math.max(r1.getMax(),
    // r.getMax()));
    // added = true;
    // }
    // }
    // // otherwise just add it
    // if (added == false) {
    // this.exactRanges.add(r);
    // // logger.debug("just adding r2 " + r);
    // }
    // }
    // return this;
    // }

    /**
     * Creates a DurationModifier to add a value to each duration.
      * <p>
     * Uses the {@link #modifyDurations(com.rockhoppertech.music.modifiers.Modifier.Operation, Double[]) modifyDurations}
     * method.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * Same as doing this:
     * 
     * <pre> 
     *  <code>
    DurationModifier dm = new DurationModifier(Operation.ADD, d);
    ts.map(dm);
    </code>
    </pre>
     * 
     * @param d
     *            an amount to add
     * @see DurationModifier
     */
    public void addToDuration(final double d) {
        logger.debug("addToDuration {}", d);
        modifyDurations(Operation.ADD, d);

        // final DurationModifier dm = new DurationModifier(Operation.ADD, d);
        // this.map(dm);
        // this.computeRanges();
        // this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Creates a {@code DurationModifier} to perform the specified operation
     * with the specified values.
         
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param operation
     *            a modifier operation
     * @param values
     *            the values to use
     * @see com.rockhoppertech.music.modifiers.Modifier.Operation
     * @see DurationModifier
     */
    public void modifyDurations(Operation operation, Double... values) {
        final DurationModifier dm = new DurationModifier(operation, values);
        this.map(dm);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Createa a {@code StartBeatModifier} to perform the specified operation
     * with the specified values.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param operation
     *            modifier operation
     * @param values
     *            values to use
     * @see StartBeatModifier
     */
    public void modifyStarts(Operation operation, Double... values) {
        final StartBeatModifier dm = new StartBeatModifier(operation, values);
        this.map(dm);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param d
     *            an amount to add
     * @return this to cascade calls
     */
    public TimeSeries addToSilences(final double d) {
        logger.debug("addToSilences Before" + d);
        logger.debug("list {}", this.list);
        double lastTime = 1d;
        double newTime = 0d;
        for (final Timed te : this.list) {
            final double t = te.getStartBeat();
            logger.debug("doing " + te);

            // currrent time minus the end time of the last note
            final double delta = t - lastTime;
            logger.debug("delta " + delta);
            if (delta > 0) {
                final double time = newTime + delta + d;
                te.setStartBeat(time);
                logger.debug("changed time to " + time);
            }
            logger.debug("done " + te);

            // the new time
            newTime = (te.getStartBeat() + te.getDuration());
            // the original time, not the new one
            lastTime = (t + te.getDuration());

            logger.debug("new lt " + lastTime);
        }
        logger.debug("addToSilences After");
        logger.debug("list {}", this.list);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
        return this;
    }

    /**
     * A deep copy of the series is appended to this.
     * 
     * {@code ts.append(ts2).append(ts3);}
     * 
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param ts1
     *            not modified at all
     * @return this to cascade calls
     */
    public TimeSeries append(final TimeSeries ts1) {
        // don't change the one passed in
        final TimeSeries ts = new TimeSeries(ts1);
        final StartBeatModifier m = new StartBeatModifier(this.getEndBeat() - ts.getStartBeat());
        m.setOperation(Operation.ADD);
        ts.map(m);

        for (final TimeEvent te : ts.list) {
            this.list.add(new TimeEvent(te.getStartBeat(), te.getDuration()));
        }
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
        return this;
    }

    /**
     * Apply this time series to the track. The track's events have their start
     * and durations changed.
     * 
     * @param track
     *            is modified according to the timeseries
     */
    public void apply(final MIDITrack track) {
        final double nsize = track.size();
        final double tsize = this.getSize();
        final int n = (int) Math.round(nsize / tsize + .5);
        logger.debug("nsize {} tsize {} n {}\n", nsize, tsize, n);
        final TimeSeries ts = this.nCopies(n);
        // ts.sequential();
        for (final MIDINote note : track) {
            final Timed te = ts.nextTimeEvent();
            note.setStartBeat(te.getStartBeat());
            note.setDuration(te.getDuration());
        }
    }

    /**
     * @param track
     *            is NOT modified.
     * @param mask
     *            number of repeats for each pitch in the notelist
     * @return a new {@code MIDITrack}
     */
    public MIDITrack apply(final MIDITrack track, final CircularList<Integer> mask) {

        final MIDITrack applied = new MIDITrack(track);
        TimeSeries masked = TimeSeriesFactory.createRepeated(this, mask);
        masked.apply(applied);
        return applied;

    }

    /**
     * Create from repeat Mask for both the track and the timeseries.
     * 
     * @param track
     *            a track to repeat
     * @param mask
     *            a repeat mask
     * @return a new {@code MIDITrack}
     */
    public MIDITrack applyToBoth(final MIDITrack track, final CircularList<Integer> mask) {

        final MIDITrack applied = MIDITrackFactory.createRepeated(track, mask);

        TimeSeries masked = TimeSeriesFactory.createRepeated(this, mask);
        masked.apply(applied);
        return applied;

    }

    /**
     * Removes all events and ranges.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     */
    public void clear() {
        this.list.clear();
        this.ranges.clear();
        this.exactRanges.clear();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Used internally
     */
    private void computeRanges() {
        if (this.ranges == null) {
            this.ranges = new LinkedList<Range<Double>>();
        }

        if (this.ranges.isEmpty() == false) {
            this.ranges.clear();
        }

        for (final TimeEvent te : this.list) {
            this.addRange(te.getOpenRange());
        }

    }

    /**
     * Creates a {@code DurationModifier} to divide each duration by the
     * divisor. The start time remains intact but the durations are altered.
     *   <p>
     * Uses the {@link #modifyDurations(com.rockhoppertech.music.modifiers.Modifier.Operation, Double[]) modifyDurations}
     * method.
     * 
     * @param d
     *            the divisor. e.g. 2 will halve the durations.
     */
    public void divideDuration(final double d) {
        logger.debug("divideDuration " + d);
        this.modifyDurations(Operation.DIVIDE, d);
    }

    /**
     * Get the a {@code TimeEvent} at the specified index.
     * 
     * @param index
     *            an index
     * @return a {@code TimeEvent}
     */
    public TimeEvent get(final int index) {
        if (this.list.isEmpty()) {
            return null;
        }
        return this.list.get(index);
    }

    /**
     * Get the description or null if not set.
     * 
     * @return the description
     */
    public String getDescription() {
        return this.description;
    }

    /**
     * Get the durations as a List.
     * 
     * @return a List of Doubles
     */
    public List<Double> getDurations() {
        final List<Double> starts = new ArrayList<Double>();
        for (final Timed e : this) {
            starts.add(e.getDuration());
        }
        return starts;
    }

    // public Iterator<TimeEvent> getSeries() {
    // return this.list.iterator();
    // }

    /**
     * Finds the greatest end time of any time event in the series.
     * 
     * @return the end time
     */
    public double getEndBeat() {
        double lastTime = 1d;
        for (final Timed te : this.list) {
            final double t = te.getStartBeat() + te.getDuration();
            if (t > lastTime) {
                lastTime = t;
            }
        }
        return lastTime;
    }

    /**
     * Get the series' name.
     * 
     * @return the name
     */
    public String getName() {
        return this.name;
    }

    public Range<Double> getExactRange(final int index) {
        return this.exactRanges.get(index);
    }

    public Range<Double> getRange(final int index) {
        return this.ranges.get(index);
    }

    public List<Range<Double>> getRanges() {
        return this.ranges;
    }

    public Iterator<Range<Double>> getRangesIterator() {
        return this.ranges.iterator();
    }

    public TimeSeries getSilences() {
        final TimeSeries ts = new TimeSeries();
        double lastTime = 1d;
        logger.debug("number of ranges {}", this.ranges.size());
        for (final Range<Double> r1 : this.ranges) {
            logger.debug("range {}", r1);
            final double before = r1.lowerEndpoint() - lastTime;
            if (before > 0d) {
                ts.add(new TimeEvent(lastTime, before));
                // ts.add(new TimeEvent(lastTime + TimeEvent.rangeFudge, before
                // + TimeEvent.rangeFudge));
            }
            lastTime = r1.upperEndpoint();
        }
        return ts;
    }

    /*
     * public TimeSeries findSilences() { logger.debug(this.list);
     * logger.debug(this.ranges);
     * 
     * TimeSeries ts = new TimeSeries(); double lastTime = 1d;
     * 
     * for (final Range r1 : this.ranges) { logger.debug("start " +
     * r1.getMin()); final double before = r1.getMin() - lastTime;
     * logger.debug("before " + before); logger.debug("lasttime " + lastTime);
     * if (before >= 0d) { logger.debug("silence " + before + " at time " +
     * lastTime); if(before > 0d) ts.add(new TimeEvent(lastTime, before)); }
     * lastTime = r1.getMax(); logger.debug("new lt " + lastTime); } return ts;
     * }
     */

    /**
     * Get the number of time events in this series.
     * 
     * @return the number of events
     */
    public int getSize() {
        return this.list.size();
    }

    /**
     * Get the earliest start time of any time event in this series.
     * 
     * @return the start beat
     */
    public double getStartBeat() {
        double start = 1d;
        for (final Timed te : this.list) {
            final double s = te.getStartBeat();
            if (s < start) {
                start = s;
            }
        }
        return start;
    }

    /**
     * Get all the start times.
     * 
     * @return a List<Double> of start times
     */
    public List<Double> getStartTimes() {
        final List<Double> starts = new ArrayList<Double>();
        for (final Timed e : this) {
            starts.add(e.getStartBeat());
        }
        return starts;
    }

    /**
     * Insert another {@code TimeSeries} at the specified index to this series.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param index
     *            the index in this series
     * @param timeSeries
     *            the {@code TimeSeries} to insert
     */
    public void insertAtIndex(final int index, final TimeSeries timeSeries) {
        final Timed e = this.list.get(index);
        final TimedModifier stm = new StartBeatModifier(e.getStartBeat());
        stm.setOperation(Operation.ADD);
        TimeSeries dupe = new TimeSeries(timeSeries);
        dupe.map(stm);
        // this.list.addAll(index,
        // timeSeries.list);
        this.list.addAll(index, ListUtils.newCircularArrayList(dupe.list));
        this.sort();
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Ignores current rests and inserts the value before each start time.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param silence
     *            an amount to insert
     */
    public void insertSilence(final double silence) {
        logger.debug("insertSilenceBefore " + silence);
        logger.debug("list {}", this.list);
        for (final Timed te : this.list) {
            te.setStartBeat(te.getStartBeat() + silence);
        }
        // double lastTime = silence;
        // if (silence < 1d) {
        // lastTime = 1d + silence;
        // }
        //
        // for (final TimeEvent te : this.list) {
        // final double t = te.getStart();
        //
        // te.setStart(lastTime);
        //
        // logger.debug("start time " + t);
        // logger.debug("changed time to " + (t + silence));
        // lastTime = t + silence;
        //
        // // lastTime += te.getDuration() + silence;
        // }
        // logger.debug("After");
        // logger.debug(this.list);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    public Iterator<TimeEvent> iterator() {
        return this.list.iterator();
    }

    public ListIterator<TimeEvent> listIterator(final int index) {
        return this.list.listIterator(index);
    }

    /**
     * <code>map</code> is the GoF visitor(331) design pattern. It loops through
     * all the TimeEvent and applies the modifier to each event. Sort of like
     * mapcar in LISP (but Java does not have lambdas so you need the
     * TimeEventModifier interface).
     * 
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * 
     * <pre>
     * {@code
     * TimeSeries series = new TimeSeries();
     * TimeEvent e = new TimeEvent(1,2);
     * series.add(e);
     * series.map(new DurationModifier(1d, TimeEventModifier.Operation.ADD));
     * }
     * </pre>
     * 
     * 
     * @param mod
     *            a <code>TimeEventModifier</code> implementation.
     * 
     * 
     * @see com.rockhoppertech.music.modifiers.StartBeatModifier
     */
    public void map(final TimedModifier mod) {
        for (final Timed n : this) {
            mod.modify(n);
        }
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * <code>map</code> calls map(TimeEventModifier) only if the note's start
     * beat is after the specified value.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param mod
     *            a <code>TimeEventModifier</code> implementation.
     * @param after
     *            a <code>double</code> value. Modify notes only after this
     *            start beat.
     * 
     * 
     */
    public void map(final TimedModifier mod, final double after) {
        for (final Timed n : this) {
            if (n.getStartBeat() >= after) {
                mod.modify(n);
            }
        }
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * <code>map</code> affects only events after <b>after</b> and before
     * <b>before</b>.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param mod
     *            a <code>TimeEventModifier</code> value
     * @param after
     *            a <code>double</code> value
     * @param before
     *            a <code>double</code> value
     */
    public void map(final TimedModifier mod, final double after, final double before) {
        for (final Timed n : this) {
            final double s = n.getStartBeat();
            if ((s >= after) && (s <= before)) {
                mod.modify(n);
            }
        }
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * 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>
     * TimeSeries notelist = new TimeSeries();
     * TimeEvent event = new TimeEvent(Pitch.C5);
     * notelist.add(event);
     * etc.
     * // before or equal to start beat 3 and the pitch is lower than or equal to E5
     * TimeModifierCriteria criteria = new StartBeatCriteria(3d,
     *         TimeModifierCriteria.Operator.LT_EQ, new PitchCriteria(Pitch.E5,
     *                 TimeModifierCriteria.Operator.LT_EQ, null));
     * notelist.map(new StartBeatModifier(1d, TimeModifierCriteria.Operation.SET), criteria);
     * </pre>
     * 
     * </blockquote>
     * 
     * @param mod
     *            The TimeEventModifier
     * @param criteria
     *            The TimeModifierCriteria
     * 
     * @see com.rockhoppertech.series.time.modifiers.criteria.TimeModifierCriteria
     */
    // TODO make sthe criteria a guava predicate
    // public void map(final TimedModifier mod, final TimeModifierCriteria
    // criteria) {
    // for (final Timed n : this) {
    // if (criteria.test(n)) {
    // logger.debug("Passed " + n);
    // mod.modify(n);
    // } else {
    // logger.debug("Failed " + n);
    // }
    // }
    // this.computeRanges();
    // this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    // }

    /**
     * Copy the durations from the List and apply them to the events in this
     * series.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param durations
     *            a List<Double> of durations
     */
    public void mapDurations(final List<TimeEvent> durations) {
        logger.debug("mapDurations ");
        if (durations.size() < this.list.size()) {
            logger.debug("not enough durations");
            return;
        }
        final Iterator<TimeEvent> d = durations.iterator();
        for (final Timed te : this.list) {
            if (d.hasNext() == false) {
                break;
            }
            final Timed newd = d.next();
            te.setDuration(newd.getDuration());
        }
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Copy the durations from the provided series and apply them to the events
     * in this series.
     * 
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param durations
     */
    public void mapDurations(final TimeSeries durations) {
        logger.debug("mapDurations ");
        if (durations.getSize() < this.list.size()) {
            logger.debug("not enough durations");
            return;
        }
        final Iterator<TimeEvent> d = durations.iterator();
        for (final Timed te : this.list) {
            if (d.hasNext() == false) {
                break;
            }
            final Timed newd = d.next();
            te.setDuration(newd.getDuration());
        }
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Each duration is multiplied by this value.
      * <p>
     * Uses the {@link #modifyDurations(com.rockhoppertech.music.modifiers.Modifier.Operation, Double[]) modifyDurations}
     * method.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param d
     *            multiplier
     * @see DurationModifier
     */
    public void multiplyDuration(final double d) {
        logger.debug("multiplyDuration " + d);
        modifyDurations(Operation.MULTIPLY, d);
    }

    /**
     * Create the specified number of copies of this series.
     * <p>
     * The receiver is not modified at all. You get a brand new TimeSeries. it
     * is sequential.
     * 
     * @param n
     *            number of copies
     * @return a new {@code TimeSeries}
     */
    public TimeSeries nCopies(final int n) {
        final TimeSeries ts = new TimeSeries(this);
        for (int i = 0; i < n - 1; i++) {
            ts.append(this);
        }
        return ts;
    }

    /**
     * Does t2 occur after t1's start plus duration?
     * 
     * @param t1
     *            a time event
     * @param t2
     *            a time event
     * @return whether t2 occurs after t1
     */
    public static boolean isAfter(Timed t1, Timed t2) {
        return t2.getStartBeat() >= t1.getStartBeat() + t1.getDuration();
    }

    /**
     * Does t2 occur before t1's start plus duration?
     * 
     * @param t1
     *            a {@code Timed} object
     * @param t2
     *            a {@code Timed} object
     * @return whether t2 occurs before t1
     */
    public static boolean isBefore(Timed t1, Timed t2) {
        return t2.getStartBeat() <= t1.getStartBeat() + t1.getDuration();
    }

    /**
     * Get the next event.
     * <p>
     * This will wrap when the end of the list of events is reached.
     * 
     * @return the next {@code TimeEvent}
     */
    public Timed nextTimeEvent() {
        return this.list.next();
        // if (this.index + 1 > this.list.size()) {
        // this.index = 0;
        // }
        // final TimeEvent v = this.list.get(this.index++);
        // return v;
    }

    // TODO port Permutations
    // public void permute() {
    // final long np = Permutations.getNumberOfPermutations(this.list.size(),
    // this.list.size());
    // logger.debug("Number of permutations: " + np);
    // final List<int[]> list = Permutations.getPermutations(this.list.size());
    // if (logger.isDebugEnabled()) {
    // logger.debug(String.format("permutation list %d",
    // list.size()));
    // for (final int[] a : list) {
    // ArrayUtils.printArray(a,
    // logger);
    // }
    // }
    // double time = this.getEndBeat();
    // for (final int[] array : list) {
    // for (final int element : array) {
    // final TimeEvent te = (TimeEvent) this.get(element).clone();
    // time += te.getStartBeat();
    // te.setStartBeat(time);
    // this.list.add(te);
    // }
    // }
    // logger.debug("permute new end: " + this.getEndBeat());
    // logger.debug("permute new size: " + this.getSize());
    // this.computeRanges();
    // this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    // }

    /**
     * Turn the series into a track.
     * 
     * @return a new {@code MIDITrack}
     */
    public MIDITrack toMIDITrack() {
        final MIDITrack notelist = new MIDITrack();
        int channel = 0;
        for (final Timed te : this) {
            final MIDINote nn = new MIDINote(Pitch.C5, te.getStartBeat(), te.getDuration(), channel);
            notelist.add(nn);
        }
        return notelist;
    }

    public void play() {
        final MIDITrack track = toMIDITrack();

        // click track
        int channel = 9;
        final double end = this.getEndBeat();
        final int hi = MIDIGMPatch.HI_WOOD_BLOCK_PERC.getProgram();
        final int low = MIDIGMPatch.LOW_WOOD_BLOCK_PERC.getProgram();
        for (double i = 1d; i < end; i++) {
            MIDINote nn = null;
            if (i % 4 == 1) {
                nn = new MIDINote(hi, i, 1d, channel);
            } else {
                nn = new MIDINote(low, i, 1d, channel);
            }
            track.add(nn);
        }
        track.play();
    }

    public void play(boolean withClickTrack) {
        final MIDITrack track = toMIDITrack();
        MIDIPerformer mp = new MIDIPerformer();
        mp.play(track, withClickTrack);
    }

    /**
     * Quantize durations.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param q
     *            quantization value
     * @see DurationModifier
     * @see com.rockhoppertech.music.modifiers.Modifier.Operation
     */
    public void quantizeDurations(final double q) {
        logger.debug("quantizeDurations " + q);

        DurationModifier mod = new DurationModifier(Operation.QUANTIZE, q);
        this.map(mod);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Quantize start times.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param q
     *            quantization value
     * @see StartBeatModifier
     * @see com.rockhoppertech.music.modifiers.Modifier.Operation
     */
    public void quantizeStartBeats(final double q) {
        logger.debug("quantizeStartBeats " + q);

        StartBeatModifier mod = new StartBeatModifier(Operation.QUANTIZE, q);
        this.map(mod);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Remove the time event at the specified index.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param index
     *            the index of the Timed event to remove
     */
    public void remove(final int index) {
        final Timed t = this.get(index);
        this.remove(t);
    }

    /**
     * Removes this exact Timed instance.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param t
     */
    public void remove(final Timed t) {
        this.list.remove(t);
        Collections.sort(this.list, new TimeComparator());
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Subtracts the values from each duration.
     * <p>
     * Uses the {@link #modifyDurations(com.rockhoppertech.music.modifiers.Modifier.Operation, Double[]) modifyDurations}
     * method.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param d
     *            amount to remove
     * 
     * @see DurationModifier
     * @see com.rockhoppertech.music.modifiers.Modifier.Operation
     */
    public void removeFromDuration(final double d) {
        logger.debug("removeFromDuration " + d);
        modifyDurations(Operation.SUBTRACT, d);
    }

    /**
     * Remove the specified amount from each silence.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param d
     *            amount to remove
     */
    public void removeFromSilences(final double d) {
        logger.debug("removeFromSilences Before");
        logger.debug("list {}", this.list);
        double lastTime = 1d;
        double newTime = 1d;
        for (final Timed te : this.list) {
            final double t = te.getStartBeat();
            logger.debug("doing " + te);
            final double delta = t - lastTime;
            if (delta > 0) {
                final double time = newTime + delta - d;
                if (time >= 0) {
                    te.setStartBeat(time);
                    logger.debug("changed time to " + time);
                }
            }
            logger.debug("new lt " + lastTime);
            // the new time
            newTime = (te.getStartBeat() + te.getDuration());
            // the original time, not the new one
            lastTime = (t + te.getDuration());
        }
        logger.debug("removeFromSilences after");
        logger.debug("list {}", this.list);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Modifies the start times of each time event to remove overlaps.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     */
    public void removeOverlaps() {
        logger.debug("removeOverlaps Before");
        logger.debug("list {}", this.list);
        double lastTime = 1d;
        double newTime = 1d;
        for (final Timed te : this.list) {
            final double t = te.getStartBeat();
            logger.debug("doing " + te);
            final double delta = t - newTime;
            logger.debug("delta " + delta);
            if (delta < 0) {
                // double time = t + delta;
                final double time = newTime;
                if (time >= 0) {
                    te.setStartBeat(time);
                    logger.debug("changed time to " + time);
                }
            }
            logger.debug("new lt " + lastTime);
            // the new time
            newTime = (te.getStartBeat() + te.getDuration());
            // the original time, not the new one
            lastTime = (t + te.getDuration());
        }
        logger.debug("removeOverlaps after");
        logger.debug("list {}", this.list);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Removes the property change listener.
     * 
     * @param listener
     *            the listner to remove
     */
    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);
    }

    /**
     * Changes start times to remove silences.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     */
    public void removeSilences() {
        // TimeSeries silences = this.getSilences();
        // int index = 0;
        // for (final Timed te : this.list) {
        // Timed ste = silences.get(index++);
        // te.setStartBeat(ste.getStartBeat());
        // }
        // no, the silences change after the first one
        // for(int i = 1; i < this.list.size(); i++) {
        // Timed te = this.list.get(i);
        // Timed ste = silences.get(index++);
        // // te.setStartBeat(te.getStartBeat() - ste.getDuration());
        // te.setStartBeat(ste.getStartBeat());
        // }

        logger.debug("Before");
        logger.debug("list {}", this.list);
        double lastTime = 1d;
        for (final Timed te : this.list) {
            final double t = te.getStartBeat();
            double delta = t - lastTime;
            logger.debug(String.format("start time %f delta %f", t, delta));
            if (delta > 0d) {
                te.setStartBeat(lastTime);
                logger.debug("changed start to " + lastTime);
            }

            lastTime += te.getDuration();
            logger.debug("new lastTime with added dur " + lastTime);
        }
        logger.debug("After");
        logger.debug("list {}", this.list);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Replace the time event at the specified index with the specified time
     * event.
     * 
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param e a {@code TimeEvent}
     * @param index the index
     */
    public void set(final TimeEvent e, final int index) {
        logger.debug("Timeseries set " + e);
        this.list.set(index, e);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Set an optional description of the series.
     * 
     * @param description
     *            the description to set
     */
    public void setDescription(final String description) {
        this.description = description;
    }

    /**
     * Sets each time event's duration to the specified value.
     *  <p>
     * Uses the {@link #modifyDurations(com.rockhoppertech.music.modifiers.Modifier.Operation, Double[]) modifyDurations}
     * method.
     * @param d
     *            new duration
     * 
     */
    public void setDuration(final double d) {
        logger.debug("setDuration " + d);
        modifyDurations(Operation.SET, d);
    }

    /**
     * Set the series' optional name.
     * 
     * @param name
     *            the name to set
     */
    public void setName(final String name) {
        this.name = name;
    }

    /**
     * Change all silences to this value. Modifies the start times of the time
     * events.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     * 
     * @param d
     */
    public void setSilences(final double d) {

        logger.debug("setSilences Before" + d);
        logger.debug("list {}", this.list);
        double lastTime = 1d;
        double newTime = 1d;
        for (final Timed te : this.list) {
            final double t = te.getStartBeat();
            logger.debug("doing " + te);

            // currrent time minus the end time of the last note
            final double delta = t - lastTime;
            logger.debug("delta " + delta);
            if (delta > 0) {
                // double time = newTime + delta + d;
                final double time = newTime + d;
                te.setStartBeat(time);
                logger.debug("changed time to " + time);
            }
            logger.debug("done " + te);

            // the new time

            newTime = (te.getStartBeat() + te.getDuration());
            // the original time, not the new one
            // lastTime = (t + te.getDuration());
            lastTime = t;

            logger.debug("new lt " + lastTime);
        }
        logger.debug("addToSilences After");
        logger.debug("list {}", this.list);
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    /**
     * Change the start beat of each time event by getting the difference
     * between the specified start and the first event's start.
      * <p>
     * Uses the {@link #modifyStarts(com.rockhoppertech.music.modifiers.Modifier.Operation, Double[]) modifyDurations}
     * method.
     * 
     * @param start
     *            new start time
     */
    public void setStart(final double start) {
        final Timed e = this.get(0);
        final double delta = start - e.getStartBeat();
        // final Operation op = Operation.ADD;
        // final TimedModifier stm = new StartBeatModifier(delta);
        // stm.setOperation(op);
        // this.map(stm);
        modifyStarts(Operation.ADD, delta);
    }

    /**
     * Sorts by start time.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event, with this as
     * the new value and no old value.
     */
    public void sort() {
        Collections.sort(this.list, new TimeComparator());
        this.computeRanges();
        this.changes.firePropertyChange(TimeSeries.MODIFIED, null, this);
    }

    @Override
    public String toString() {
        final StringBuilder sw = new StringBuilder();
        for (final Timed te : this.list) {
            sw.append(te).append('\n');
        }
        for (final Range<Double> r : this.ranges) {
            sw.append(r).append('\n');
        }

        return sw.toString();
    }

    /**
     * Change the start beat of the event at the specified index.
     * <p>
     * Fires a {@code TimeSeries.START_BEAT_CHANGE} property change event.
     * 
     * @param selectedIndex
     *            index of the event to change
     * @param startBeat
     *            the new start
     */
    public void changeStartBeat(int selectedIndex, double startBeat) {
        TimeEvent n = this.get(selectedIndex);
        n.setStartBeat(startBeat);
        this.changes.firePropertyChange(TimeSeries.START_BEAT_CHANGE, null, n);
    }

    /**
     * Change the start beat of the first event, then make the series
     * sequential.
     * <p>
     * Fires a {@code TimeSeries.START_BEAT_CHANGE} property change event.
     * 
     * @param startBeat
     *            the new start beat
     */
    public void changeStartBeat(double startBeat) {
        TimeEvent n = this.get(0);
        n.setStartBeat(startBeat);
        this.sequential();

        this.changes.firePropertyChange(TimeSeries.START_BEAT_CHANGE, null, n);
    }

    /**
     * Change the duration of the event at the specified index.
     * <p>
     * Fires a {@code TimeSeries.DURATION_CHANGE} property change event.
     * 
     * @param selectedIndex
     *            index of the event to change
     * @param duration
     *            the new duration
     */
    public void changeDuration(int selectedIndex, double duration) {
        TimeEvent n = this.get(selectedIndex);
        n.setDuration(duration);
        this.changes.firePropertyChange(TimeSeries.DURATION_CHANGE, null, n);
    }

    /**
     * Make the events sequenial with a 0 pad between events.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event.
     * 
     * @return this {@code TimeSeries}
     */
    public TimeSeries sequential() {
        return this.sequential(0);
    }

    /**
     * Reset each note's startbeat in the series to be sequential based on the
     * duration. The first event is unaffected.
     * <p>
     * Fires a {@code TimeSeries.MODIFIED} property change event.
     * 
     * @param pad
     *            amount added between the notes
     */
    public TimeSeries sequential(int pad) {
        int size = this.getSize();
        Timed n = this.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++) {
            Timed nn = this.list.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(TimeSeries.MODIFIED, null, this);
        return this;
    }

    /**
     * Change the endBeat of the event at the specified index.
     * <p>
     * Fires a {@code TimeSeries.END_BEAT_CHANGE} property change event.
     * 
     * @param selectedIndex
     *            index of the event to change
     * @param endBeat
     *            the new endBeat
     */
    public void changeEndBeat(int selectedIndex, double endBeat) {
        TimeEvent n = this.get(selectedIndex);
        n.setEndBeat(endBeat);
        this.changes.firePropertyChange(TimeSeries.END_BEAT_CHANGE, null, n);
    }

    /*
      * (non-Javadoc)
      * 
      * @see java.lang.Object#hashCode()
      */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((list == null) ? 0 : list.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        TimeSeries other = (TimeSeries) obj;
        if (list == null) {
            if (other.list != null) {
                return false;
            }

        } else if (!list.equals(other.list)) {
            logger.debug("lists dont match ");
            logger.debug("this {}", list);
            logger.debug("other {}", other.list);
            return false;
        }
        // if (name == null) {
        // if (other.name != null) {
        // return false;
        // }
        // } else if (!name.equals(other.name)) {
        // return false;
        // }
        return true;
    }

    /**
     * For serialization.
     * 
     * @param out
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
    }

    /**
     * For Serialization.
     * 
     * @param in
     * @throws IOException
     * @throws ClassNotFoundException
     */
    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.ranges = new ArrayList<Range<Double>>();
        this.exactRanges = new ArrayList<Range<Double>>();
        this.timeComparator = new TimeComparator();
        this.rangeSet = TreeRangeSet.create();
    }

}

/*
 * insert silence series map over list and change start times keep existing
 * silences?
 * 
 * expand/contract current silence series if non overlapping notes, it's easy
 */