View Javadoc
1   package net.kreatious.pianoleopard.midi;
2   
3   import java.io.Closeable;
4   import java.io.File;
5   import java.io.FileInputStream;
6   import java.io.IOException;
7   import java.io.InputStream;
8   import java.lang.Thread.State;
9   import java.util.List;
10  import java.util.Optional;
11  import java.util.concurrent.CopyOnWriteArrayList;
12  import java.util.concurrent.TimeUnit;
13  import java.util.function.BiFunction;
14  import java.util.function.Consumer;
15  import java.util.function.LongConsumer;
16  
17  import javax.sound.midi.InvalidMidiDataException;
18  import javax.sound.midi.MidiDevice;
19  import javax.sound.midi.MidiDevice.Info;
20  import javax.sound.midi.MidiMessage;
21  import javax.sound.midi.MidiSystem;
22  import javax.sound.midi.MidiUnavailableException;
23  import javax.sound.midi.Receiver;
24  import javax.sound.midi.Sequencer;
25  import javax.sound.midi.ShortMessage;
26  
27  import net.kreatious.pianoleopard.midi.event.Event;
28  import net.kreatious.pianoleopard.midi.event.EventFactory;
29  import net.kreatious.pianoleopard.midi.track.ParsedSequence;
30  
31  import com.google.common.annotations.VisibleForTesting;
32  
33  /**
34   * Model for the MIDI output sequencer, allows controllers to listen for events.
35   *
36   * @author Jay-R Studer
37   */
38  public class OutputModel implements AutoCloseable {
39      /**
40       * Indicates which action to take in response to an event handler.
41       * <p>
42       * The action with the highest priority takes precedence.
43       *
44       * @author Jay-R Studer
45       */
46      public enum EventAction {
47          /**
48           * Return if the event should be played. Has the highest priority.
49           */
50          PLAY,
51  
52          /**
53           * Returned if the event should be muted. Has medium priority.
54           */
55          MUTE,
56  
57          /**
58           * Returned if it doesn't matter if the event is muted. Has the lowest
59           * priority.
60           * <p>
61           * An event that is not handled by all handlers will be played.
62           * <p>
63           * Event handlers are allowed to change the MidiMessage before it is
64           * sent.
65           */
66          UNHANDLED;
67      }
68  
69      private static final long ALWAYS_SEND = -10;
70      private final Sequencer sequencer;
71      private ParsedSequence sequence = ParsedSequence.createEmpty();
72      private Optional<MidiDevice> output = Optional.empty();
73      private Optional<Receiver> receiver = Optional.empty();
74  
75      private final List<Consumer<? super Info>> outputDeviceListeners = new CopyOnWriteArrayList<>();
76      private final List<Consumer<? super ParsedSequence>> openListeners = new CopyOnWriteArrayList<>();
77      private final List<Runnable> playListeners = new CopyOnWriteArrayList<>();
78      private final List<LongConsumer> currentTimeListeners = new CopyOnWriteArrayList<>();
79      private final List<BiFunction<MidiMessage, Optional<Event>, EventAction>> eventHandlers = new CopyOnWriteArrayList<>();
80      private final List<Closeable> closeables = new CopyOnWriteArrayList<>();
81  
82      private final Thread tickThread = new Thread("output model current tick thread") {
83          @Override
84          public void run() {
85              try {
86                  while (true) {
87                      currentTimeListeners.forEach(listener -> listener.accept(sequencer.getMicrosecondPosition()));
88                      Thread.sleep(TimeUnit.SECONDS.toMillis(1) / 120);
89                  }
90              } catch (final InterruptedException e) {
91                  Thread.currentThread().interrupt();
92              }
93          };
94      };
95  
96      /**
97       * Constructs a new {@link OutputModel} with the specified initial state.
98       * <p>
99       * The output model is initially unconnected to any MIDI devices. After
100      * construction, it is expected that an output device will be set by the
101      * consumer.
102      *
103      * @param sequencerFactory
104      *            A factory for producing the {@link Sequencer}, such as
105      *            {@link SystemSequencerFactory}.
106      * @throws MidiUnavailableException
107      *             if the MIDI system is unavailable.
108      */
109     public OutputModel(SequencerFactory sequencerFactory) throws MidiUnavailableException {
110         sequencer = sequencerFactory.getSequencer();
111         setOutputDevice(new InitialMidiDevice());
112     }
113 
114     /**
115      * Reconnects the sequencer to a different MIDI output device.
116      *
117      * @param output
118      *            the new output MIDI device to reconnect to
119      * @throws MidiUnavailableException
120      *             if the MIDI system is unavailable.
121      */
122     public synchronized void setOutputDevice(MidiDevice output) throws MidiUnavailableException {
123         try {
124             sequencer.close();
125             this.output.ifPresent(MidiDevice::close);
126             this.output = Optional.of(output);
127 
128             output.open();
129             receiver = Optional.of(new MutingReceiverProxy(output.getReceiver()));
130             sequencer.getTransmitter().setReceiver(receiver.get());
131             sequencer.open();
132             sequencer.setSequence(sequence.getSequence());
133             outputDeviceListeners.forEach(listener -> listener.accept(output.getDeviceInfo()));
134         } catch (final InvalidMidiDataException e) {
135             // Sequence should still be valid since openMidiFile didn't throw
136             throw new IllegalStateException(e);
137         }
138     }
139 
140     /**
141      * Starts playback of the currently loaded MIDI file.
142      * <p>
143      * Any registered start listeners will be called with the last opened MIDI
144      * sequence.
145      */
146     public void start() {
147         sequencer.stop();
148         sequencer.setMicrosecondPosition(0);
149         resetReceiver();
150         playListeners.forEach(Runnable::run);
151         sequencer.start();
152     }
153 
154     private class MutingReceiverProxy implements Receiver {
155         private final Receiver wrapped;
156 
157         private MutingReceiverProxy(Receiver wrapped) {
158             this.wrapped = wrapped;
159         }
160 
161         @Override
162         public void send(MidiMessage message, long timeStamp) {
163             if (timeStamp == ALWAYS_SEND) {
164                 wrapped.send(message, -1);
165                 return;
166             }
167 
168             final Optional<Event> event = EventFactory.create(message, sequencer.getMicrosecondPosition());
169             if (eventHandlers.stream().map(eventHandler -> eventHandler.apply(message, event)).min(Enum::compareTo)
170                     .orElse(EventAction.UNHANDLED) != EventAction.MUTE) {
171                 wrapped.send(message, timeStamp);
172             }
173         }
174 
175         @Override
176         public void close() {
177             wrapped.close();
178         }
179     }
180 
181     /**
182      * Adjusts the tempo of played back sequences.
183      * <p>
184      * Values higher than 1.0 are faster than normal, less than 1.0 are slower
185      * than normal. A value of 1.0 indicates that the regular tempo should be
186      * applied. Tempo factors do not affect the microsecond values of MIDI
187      * events.
188      *
189      * @param factor
190      *            the tempo factor to set
191      */
192     public void setTempoFactor(float factor) {
193         sequencer.setTempoFactor(factor);
194     }
195 
196     /**
197      * Seeks the sequence to the specified time
198      *
199      * @param time
200      *            the time in microseconds to seek to
201      */
202     public void setCurrentTime(long time) {
203         if (sequencer.isRunning()) {
204             sequencer.stop();
205             sequencer.setMicrosecondPosition(time);
206             sequencer.start();
207         } else {
208             sequencer.setMicrosecondPosition(time);
209         }
210     }
211 
212     /**
213      * Parses a MIDI file and prepares it for playback.
214      * <p>
215      * Any registered start listeners will be called with the parsed sequence.
216      * The file of the parsed sequence will be the specified MIDI file.
217      *
218      * @param midi
219      *            the MIDI file to open
220      * @throws IOException
221      *             if an I/O error occurs
222      */
223     public void openMidiFile(File midi) throws IOException {
224         try (InputStream in = new FileInputStream(midi)) {
225             openMidiFile(in, Optional.of(midi));
226         }
227     }
228 
229     @VisibleForTesting
230     void openMidiFile(InputStream midiStream, Optional<File> midi) throws IOException {
231         try {
232             if (tickThread.getState() == State.NEW) {
233                 tickThread.start();
234             }
235 
236             sequence = ParsedSequence.parseByTracks(MidiSystem.getSequence(midiStream));
237             sequence.setFile(midi);
238             sequencer.stop();
239             sequencer.setSequence(sequence.getSequence());
240             sequencer.setMicrosecondPosition(0);
241             resetReceiver();
242             openListeners.forEach(listener -> listener.accept(sequence));
243         } catch (final InvalidMidiDataException e) {
244             throw new IOException(e);
245         }
246     }
247 
248     /**
249      * Adds a listener to notify when the output device has changed.
250      *
251      * @param listener
252      *            the listener to add
253      */
254     public void addOutputDeviceListener(Consumer<? super Info> listener) {
255         outputDeviceListeners.add(listener);
256     }
257 
258     /**
259      * Adds a listener to notify when a parsed MIDI file is opened.
260      *
261      * @param listener
262      *            the listener to add
263      */
264     public void addOpenListener(Consumer<? super ParsedSequence> listener) {
265         openListeners.add(listener);
266     }
267 
268     /**
269      * Adds a listener to notify when a parsed MIDI file is played from the
270      * beginning.
271      *
272      * @param listener
273      *            the listener to add
274      */
275     public void addPlayListener(Runnable listener) {
276         playListeners.add(listener);
277     }
278 
279     /**
280      * Adds an event handler to handle MIDI events.
281      * <p>
282      * The return value of the handler determines the action to take. A list of
283      * actions is provided on {@link EventAction}. The default action is to play
284      * the event.
285      * <p>
286      * Event handlers are allowed to mutate the channel of the MidiMessage
287      * object before returning. The {@link Event} object contains the original
288      * message before any mutations are applied. Handlers are encouraged to use
289      * the Event object whenever possible.
290      *
291      * @param handler
292      *            the event handler to add.
293      */
294     public void addEventHandler(BiFunction<MidiMessage, Optional<Event>, EventAction> handler) {
295         eventHandlers.add(handler);
296     }
297 
298     /**
299      * Adds a listener to notify when the current playback time in microseconds
300      * has changed.
301      * <p>
302      * This listener is called asynchronously several times per second for the
303      * lifetime of this object from a different thread than the one which
304      * invokes this method.
305      *
306      * @param listener
307      *            the listener to add
308      */
309     public void addCurrentTimeListener(LongConsumer listener) {
310         currentTimeListeners.add(listener);
311     }
312 
313     /**
314      * Adds a closeable to close when this output model is closed.
315      * <p>
316      * Used for releasing resources closely tied with the lifetime of this
317      * output model. Resources will be released in the same order they are
318      * registered.
319      *
320      * @param closeable
321      *            the closeable to add
322      */
323     public void addCloseable(Closeable closeable) {
324         closeables.add(closeable);
325     }
326 
327     /**
328      * Sends a MIDI message to the output.
329      *
330      * @param message
331      *            the MIDI message to send to the connected output device
332      */
333     public synchronized void sendMessage(MidiMessage message) {
334         receiver.ifPresent(receive -> receive.send(message, ALWAYS_SEND));
335     }
336 
337     @Override
338     public void close() throws InterruptedException, IOException {
339         tickThread.interrupt();
340         tickThread.join();
341 
342         sequencer.close();
343         resetReceiver();
344         output.ifPresent(MidiDevice::close);
345 
346         Optional<IOException> exception = Optional.empty();
347         for (final Closeable closeable : closeables) {
348             try {
349                 closeable.close();
350             } catch (final IOException e) {
351                 if (exception.isPresent()) {
352                     exception.get().addSuppressed(e);
353                 } else {
354                     exception = Optional.of(e);
355                 }
356             }
357         }
358         if (exception.isPresent()) {
359             throw exception.get();
360         }
361     }
362 
363     private synchronized void resetReceiver() {
364         try {
365             for (int channel = 0; channel != 16; channel++) {
366                 // All notes off, reset all controllers, reset programs
367                 sendMessage(new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, 123, 0));
368                 sendMessage(new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, 121, 0));
369                 sendMessage(new ShortMessage(ShortMessage.PROGRAM_CHANGE, channel, 0, 0));
370             }
371         } catch (final InvalidMidiDataException e) {
372             // Unreachable
373             throw new IllegalStateException(e);
374         }
375     }
376 }