001package net.kreatious.pianoleopard.midi.track;
002
003import static java.util.stream.Collectors.toList;
004
005import java.io.File;
006import java.util.Collection;
007import java.util.Collections;
008import java.util.List;
009import java.util.Optional;
010import java.util.concurrent.CopyOnWriteArrayList;
011import java.util.stream.Stream;
012
013import javax.sound.midi.InvalidMidiDataException;
014import javax.sound.midi.Sequence;
015import javax.sound.midi.Track;
016
017import net.kreatious.pianoleopard.midi.event.TempoCache;
018
019/**
020 * Represents a parsed MIDI sequence containing multiple parsed tracks
021 *
022 * @author Jay-R Studer
023 */
024public class ParsedSequence {
025    private final List<ParsedTrack> inactiveTracks = new CopyOnWriteArrayList<>();
026    private final List<ParsedTrack> activeTracks = new CopyOnWriteArrayList<>();
027    private final List<ParsedTrack> tracks;
028    private final Sequence sequence;
029
030    /**
031     * Originally set to null to signify that the value has not been set -- this
032     * is contrary to the normal expectations for an optional field
033     */
034    private Optional<File> file = null;
035
036    private ParsedSequence(Sequence sequence, Track[] tracks, TempoCache cache) {
037        this.sequence = sequence;
038        this.tracks = Stream.of(tracks).map(track -> new ImmutableParsedTrack(track, cache)).collect(toList());
039        activeTracks.addAll(this.tracks);
040    }
041
042    /**
043     * Gets the file that this sequence was originally created from.
044     * <p>
045     * If the returned file is not empty, subsequent calls to this function are
046     * guaranteed to return the same value.
047     *
048     * @return An optional containing the original file this sequence was
049     *         created with.
050     */
051    public Optional<File> getFile() {
052        return Optional.ofNullable(file).orElse(Optional.empty());
053    }
054
055    /**
056     * Sets the file that this sequence was originally created from.
057     * <p>
058     * This setter may only be called once per sequence. Subsequent calls will
059     * throw an exception.
060     *
061     * @param file
062     *            the file for this sequence
063     * @throws IllegalStateException
064     *             if the file has already been set.
065     */
066    public void setFile(Optional<File> file) {
067        if (this.file != null) {
068            throw new IllegalStateException("Cannot set the file to " + file + "; already set to " + this.file);
069        }
070        this.file = file;
071    }
072
073    /**
074     * Gets all tracks stored by this parsed MIDI sequence.
075     *
076     * @return a read only view of all parsed tracks contained in this sequence
077     */
078    public List<ParsedTrack> getTracks() {
079        return Collections.unmodifiableList(tracks);
080    }
081
082    /**
083     * Gets the active tracks stored by this parsed MIDI sequence.
084     * <p>
085     * Active tracks are those selected by the user for practice.
086     *
087     * @return a read only unordered view of the active parsed tracks contained
088     *         in this sequence
089     */
090    public Collection<ParsedTrack> getActiveTracks() {
091        return Collections.unmodifiableCollection(activeTracks);
092    }
093
094    /**
095     * Gets the inactive tracks stored by this parsed MIDI sequence.
096     * <p>
097     * Inactive tracks are those not selected by the user for practice.
098     *
099     * @return a read only unordered view of the inactive parsed tracks
100     *         contained in this sequence
101     */
102    public Collection<ParsedTrack> getInactiveTracks() {
103        return Collections.unmodifiableCollection(inactiveTracks);
104    }
105
106    /**
107     * Sets the specified track as active.
108     * <p>
109     * If the track is already active, no changes occur.
110     *
111     * @param track
112     *            the parsed track in this sequence to modify
113     * @param active
114     *            true if the track should be active, otherwise false
115     * @throws IllegalArgumentException
116     *             if the track is not contained in this sequence
117     */
118    public void setTrackActive(ParsedTrack track, boolean active) {
119        if (!tracks.contains(track)) {
120            throw new IllegalArgumentException("Specified track is not contained by this container.");
121        }
122
123        if (active && inactiveTracks.contains(track)) {
124            inactiveTracks.remove(track);
125            activeTracks.add(track);
126        } else if (!active && activeTracks.contains(track)) {
127            activeTracks.remove(track);
128            inactiveTracks.add(track);
129        }
130    }
131
132    /**
133     * Gets the original MIDI sequence used to create this parsed MIDI sequence.
134     *
135     * @return the original MIDI {@link Sequence}
136     */
137    public Sequence getSequence() {
138        return sequence;
139    }
140
141    /**
142     * Returns an empty parsed sequence containing nothing.
143     *
144     * @return a new empty {@link ParsedSequence}
145     */
146    public static ParsedSequence createEmpty() {
147        try {
148            final Sequence sequence = new Sequence(Sequence.SMPTE_25, 1);
149            return new ParsedSequence(sequence, new Track[0], new TempoCache(sequence));
150        } catch (final InvalidMidiDataException e) {
151            throw new IllegalStateException(e);
152        }
153    }
154
155    /**
156     * Parses a MIDI sequence, arranging it by tracks
157     *
158     * @param sequence
159     *            the sequence to parse
160     * @return a new {@link ParsedSequence}
161     */
162    public static ParsedSequence parseByTracks(Sequence sequence) {
163        return new ParsedSequence(sequence, sequence.getTracks(), new TempoCache(sequence));
164    }
165}