View Javadoc
1   package net.kreatious.pianoleopard.midi;
2   
3   import java.util.ArrayList;
4   import java.util.HashMap;
5   import java.util.List;
6   import java.util.Map;
7   import java.util.Optional;
8   import java.util.concurrent.CopyOnWriteArrayList;
9   import java.util.function.Consumer;
10  
11  import javax.sound.midi.MidiDevice;
12  import javax.sound.midi.MidiDevice.Info;
13  import javax.sound.midi.MidiMessage;
14  import javax.sound.midi.MidiUnavailableException;
15  import javax.sound.midi.Receiver;
16  
17  import net.kreatious.pianoleopard.intervalset.IntervalSet;
18  import net.kreatious.pianoleopard.midi.event.Event;
19  import net.kreatious.pianoleopard.midi.event.EventFactory;
20  import net.kreatious.pianoleopard.midi.event.EventPair;
21  import net.kreatious.pianoleopard.midi.event.NoteEvent;
22  import net.kreatious.pianoleopard.midi.event.PedalEvent;
23  import net.kreatious.pianoleopard.midi.track.ParsedSequence;
24  import net.kreatious.pianoleopard.midi.track.ParsedTrack;
25  
26  /**
27   * Model for the MIDI input keyboard, allows controllers to listen for events.
28   *
29   * @author Jay-R Studer
30   */
31  public class InputModel implements AutoCloseable, ParsedTrack {
32      private Optional<MidiDevice> input = Optional.empty();
33      private final UserNoteRecorder userRecorder = new UserNoteRecorder();
34  
35      private final IntervalSet<EventPair<NoteEvent>> notes = new IntervalSet<>();
36      private final IntervalSet<EventPair<PedalEvent>> pedals = new IntervalSet<>();
37  
38      private final List<Consumer<? super Info>> inputDeviceListeners = new CopyOnWriteArrayList<>();
39      private final List<Consumer<? super Event>> inputListeners = new CopyOnWriteArrayList<>();
40  
41      private InputModel(MidiDevice input) throws MidiUnavailableException {
42          setInputDevice(input);
43      }
44  
45      /**
46       * Constructs a new {@link InputModel} with the specified initial state.
47       * <p>
48       * After construction, the input model is not connected to any actual
49       * devices. It is expected that the consumer will change the input device.
50       *
51       * @param outputModel
52       *            the output model to coordinate with
53       * @return a new instance of {@link InputModel}. The caller is responsible
54       *         for releasing the resource.
55       * @throws MidiUnavailableException
56       *             if the MIDI system is unavailable.
57       */
58      public static InputModel create(OutputModel outputModel) throws MidiUnavailableException {
59          final InputModel input = new InputModel(new InitialMidiDevice());
60          outputModel.addOpenListener(input::setCurrentSequence);
61          outputModel.addPlayListener(input.userRecorder::clear);
62          outputModel.addCurrentTimeListener(input.userRecorder::setCurrentTime);
63          return input;
64      }
65  
66      private void setCurrentSequence(@SuppressWarnings("unused") ParsedSequence sequence) {
67          userRecorder.clear();
68      }
69  
70      private final class UserNoteRecorder implements Receiver, ParsedTrack {
71          private final Map<Object, NoteEvent> onNotes = new HashMap<>();
72          private final Map<Object, PedalEvent> onPedals = new HashMap<>();
73  
74          private long currentTime;
75  
76          private synchronized void setCurrentTime(long time) {
77              currentTime = time;
78          }
79  
80          @Override
81          public synchronized void send(MidiMessage message, long timeStamp) {
82              EventFactory.create(message, currentTime).ifPresent(this::userPressedEvent);
83          }
84  
85          private void userPressedEvent(Event event) {
86              if (event instanceof NoteEvent) {
87                  userPressedEvent((NoteEvent) event, onNotes, notes);
88              } else if (event instanceof PedalEvent) {
89                  userPressedEvent((PedalEvent) event, onPedals, pedals);
90              }
91              inputListeners.forEach(listener -> listener.accept(event));
92          }
93  
94          private <K extends Event> void userPressedEvent(K event, Map<Object, K> onEvents,
95                  IntervalSet<EventPair<K>> fullEvents) {
96              synchronized (fullEvents) {
97                  final long eventTime = event.getTime();
98                  if (event.isOn()) {
99                      onEvents.put(event.getSlot(), event);
100                 } else {
101                     Optional.ofNullable(onEvents.remove(event.getSlot())).ifPresent(onEvent -> {
102                         fullEvents.put(onEvent.getTime(), eventTime, new EventPair<>(onEvent, event));
103                     });
104                 }
105             }
106         }
107 
108         @Override
109         public Iterable<EventPair<NoteEvent>> getNotePairs(long low, long high) {
110             return getPairs(low, high, onNotes, notes);
111         }
112 
113         @Override
114         public Iterable<EventPair<PedalEvent>> getPedalPairs(long low, long high) {
115             return getPairs(low, high, onPedals, pedals);
116         }
117 
118         private synchronized <K extends Event> Iterable<EventPair<K>> getPairs(long low, long high,
119                 Map<Object, K> onEvents, IntervalSet<EventPair<K>> fullEvents) {
120             synchronized (fullEvents) {
121                 final List<EventPair<K>> result = new ArrayList<>();
122                 fullEvents.subSet(low, high).forEach(result::add);
123                 onEvents.values().forEach(event -> result.add(new EventPair<>(event, event.createOff(currentTime))));
124                 return result;
125             }
126         }
127 
128         void clear() {
129             synchronized (notes) {
130                 notes.clear();
131                 onNotes.clear();
132             }
133             synchronized (pedals) {
134                 pedals.clear();
135                 onPedals.clear();
136             }
137         }
138 
139         @Override
140         public void close() {
141             // Intentionally empty; this receiver holds no system resources
142         }
143     }
144 
145     /**
146      * Reconnects the input to a different MIDI input device.
147      *
148      * @param input
149      *            the new input MIDI device to reconnect to
150      * @throws MidiUnavailableException
151      *             if the MIDI system is unavailable.
152      */
153     public void setInputDevice(MidiDevice input) throws MidiUnavailableException {
154         this.input.ifPresent(MidiDevice::close);
155         this.input = Optional.of(input);
156 
157         input.open();
158         input.getTransmitter().setReceiver(userRecorder);
159         userRecorder.clear();
160         inputDeviceListeners.forEach(listener -> listener.accept(input.getDeviceInfo()));
161     }
162 
163     /**
164      * Adds a listener to notify when the input device has changed.
165      *
166      * @param listener
167      *            the listener to add
168      */
169     public void addInputDeviceListener(Consumer<? super Info> listener) {
170         inputDeviceListeners.add(listener);
171     }
172 
173     /**
174      * Adds a listener to notify when the user has pressed a key
175      *
176      * @param listener
177      *            the listener to add
178      */
179     public void addInputListener(Consumer<? super Event> listener) {
180         inputListeners.add(listener);
181     }
182 
183     @Override
184     public void close() {
185         input.ifPresent(MidiDevice::close);
186     }
187 
188     @Override
189     public Iterable<EventPair<NoteEvent>> getNotePairs(long low, long high) {
190         return userRecorder.getNotePairs(low, high);
191     }
192 
193     @Override
194     public Iterable<EventPair<PedalEvent>> getPedalPairs(long low, long high) {
195         return userRecorder.getPedalPairs(low, high);
196     }
197 }