001package net.kreatious.pianoleopard.midi;
002
003import java.io.Closeable;
004import java.io.File;
005import java.io.FileInputStream;
006import java.io.IOException;
007import java.io.InputStream;
008import java.lang.Thread.State;
009import java.util.List;
010import java.util.Optional;
011import java.util.concurrent.CopyOnWriteArrayList;
012import java.util.concurrent.TimeUnit;
013import java.util.function.BiFunction;
014import java.util.function.Consumer;
015import java.util.function.LongConsumer;
016
017import javax.sound.midi.InvalidMidiDataException;
018import javax.sound.midi.MidiDevice;
019import javax.sound.midi.MidiDevice.Info;
020import javax.sound.midi.MidiMessage;
021import javax.sound.midi.MidiSystem;
022import javax.sound.midi.MidiUnavailableException;
023import javax.sound.midi.Receiver;
024import javax.sound.midi.Sequencer;
025import javax.sound.midi.ShortMessage;
026
027import net.kreatious.pianoleopard.midi.event.Event;
028import net.kreatious.pianoleopard.midi.event.EventFactory;
029import net.kreatious.pianoleopard.midi.track.ParsedSequence;
030
031import com.google.common.annotations.VisibleForTesting;
032
033/**
034 * Model for the MIDI output sequencer, allows controllers to listen for events.
035 *
036 * @author Jay-R Studer
037 */
038public class OutputModel implements AutoCloseable {
039    /**
040     * Indicates which action to take in response to an event handler.
041     * <p>
042     * The action with the highest priority takes precedence.
043     *
044     * @author Jay-R Studer
045     */
046    public enum EventAction {
047        /**
048         * Return if the event should be played. Has the highest priority.
049         */
050        PLAY,
051
052        /**
053         * Returned if the event should be muted. Has medium priority.
054         */
055        MUTE,
056
057        /**
058         * Returned if it doesn't matter if the event is muted. Has the lowest
059         * priority.
060         * <p>
061         * An event that is not handled by all handlers will be played.
062         * <p>
063         * Event handlers are allowed to change the MidiMessage before it is
064         * sent.
065         */
066        UNHANDLED;
067    }
068
069    private static final long ALWAYS_SEND = -10;
070    private final Sequencer sequencer;
071    private ParsedSequence sequence = ParsedSequence.createEmpty();
072    private Optional<MidiDevice> output = Optional.empty();
073    private Optional<Receiver> receiver = Optional.empty();
074
075    private final List<Consumer<? super Info>> outputDeviceListeners = new CopyOnWriteArrayList<>();
076    private final List<Consumer<? super ParsedSequence>> openListeners = new CopyOnWriteArrayList<>();
077    private final List<Runnable> playListeners = new CopyOnWriteArrayList<>();
078    private final List<LongConsumer> currentTimeListeners = new CopyOnWriteArrayList<>();
079    private final List<BiFunction<MidiMessage, Optional<Event>, EventAction>> eventHandlers = new CopyOnWriteArrayList<>();
080    private final List<Closeable> closeables = new CopyOnWriteArrayList<>();
081
082    private final Thread tickThread = new Thread("output model current tick thread") {
083        @Override
084        public void run() {
085            try {
086                while (true) {
087                    currentTimeListeners.forEach(listener -> listener.accept(sequencer.getMicrosecondPosition()));
088                    Thread.sleep(TimeUnit.SECONDS.toMillis(1) / 120);
089                }
090            } catch (final InterruptedException e) {
091                Thread.currentThread().interrupt();
092            }
093        };
094    };
095
096    /**
097     * Constructs a new {@link OutputModel} with the specified initial state.
098     * <p>
099     * 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}