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}