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
35
36
37
38 public class OutputModel implements AutoCloseable {
39
40
41
42
43
44
45
46 public enum EventAction {
47
48
49
50 PLAY,
51
52
53
54
55 MUTE,
56
57
58
59
60
61
62
63
64
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
98
99
100
101
102
103
104
105
106
107
108
109 public OutputModel(SequencerFactory sequencerFactory) throws MidiUnavailableException {
110 sequencer = sequencerFactory.getSequencer();
111 setOutputDevice(new InitialMidiDevice());
112 }
113
114
115
116
117
118
119
120
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
136 throw new IllegalStateException(e);
137 }
138 }
139
140
141
142
143
144
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
183
184
185
186
187
188
189
190
191
192 public void setTempoFactor(float factor) {
193 sequencer.setTempoFactor(factor);
194 }
195
196
197
198
199
200
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
214
215
216
217
218
219
220
221
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
250
251
252
253
254 public void addOutputDeviceListener(Consumer<? super Info> listener) {
255 outputDeviceListeners.add(listener);
256 }
257
258
259
260
261
262
263
264 public void addOpenListener(Consumer<? super ParsedSequence> listener) {
265 openListeners.add(listener);
266 }
267
268
269
270
271
272
273
274
275 public void addPlayListener(Runnable listener) {
276 playListeners.add(listener);
277 }
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294 public void addEventHandler(BiFunction<MidiMessage, Optional<Event>, EventAction> handler) {
295 eventHandlers.add(handler);
296 }
297
298
299
300
301
302
303
304
305
306
307
308
309 public void addCurrentTimeListener(LongConsumer listener) {
310 currentTimeListeners.add(listener);
311 }
312
313
314
315
316
317
318
319
320
321
322
323 public void addCloseable(Closeable closeable) {
324 closeables.add(closeable);
325 }
326
327
328
329
330
331
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
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
373 throw new IllegalStateException(e);
374 }
375 }
376 }