001package net.kreatious.pianoleopard.midi;
002
003import java.util.ArrayList;
004import java.util.HashMap;
005import java.util.List;
006import java.util.Map;
007import java.util.Optional;
008import java.util.concurrent.CopyOnWriteArrayList;
009import java.util.function.Consumer;
010
011import javax.sound.midi.MidiDevice;
012import javax.sound.midi.MidiDevice.Info;
013import javax.sound.midi.MidiMessage;
014import javax.sound.midi.MidiUnavailableException;
015import javax.sound.midi.Receiver;
016
017import net.kreatious.pianoleopard.intervalset.IntervalSet;
018import net.kreatious.pianoleopard.midi.event.Event;
019import net.kreatious.pianoleopard.midi.event.EventFactory;
020import net.kreatious.pianoleopard.midi.event.EventPair;
021import net.kreatious.pianoleopard.midi.event.NoteEvent;
022import net.kreatious.pianoleopard.midi.event.PedalEvent;
023import net.kreatious.pianoleopard.midi.track.ParsedSequence;
024import net.kreatious.pianoleopard.midi.track.ParsedTrack;
025
026/**
027 * Model for the MIDI input keyboard, allows controllers to listen for events.
028 *
029 * @author Jay-R Studer
030 */
031public class InputModel implements AutoCloseable, ParsedTrack {
032    private Optional<MidiDevice> input = Optional.empty();
033    private final UserNoteRecorder userRecorder = new UserNoteRecorder();
034
035    private final IntervalSet<EventPair<NoteEvent>> notes = new IntervalSet<>();
036    private final IntervalSet<EventPair<PedalEvent>> pedals = new IntervalSet<>();
037
038    private final List<Consumer<? super Info>> inputDeviceListeners = new CopyOnWriteArrayList<>();
039    private final List<Consumer<? super Event>> inputListeners = new CopyOnWriteArrayList<>();
040
041    private InputModel(MidiDevice input) throws MidiUnavailableException {
042        setInputDevice(input);
043    }
044
045    /**
046     * Constructs a new {@link InputModel} with the specified initial state.
047     * <p>
048     * After construction, the input model is not connected to any actual
049     * devices. It is expected that the consumer will change the input device.
050     *
051     * @param outputModel
052     *            the output model to coordinate with
053     * @return a new instance of {@link InputModel}. The caller is responsible
054     *         for releasing the resource.
055     * @throws MidiUnavailableException
056     *             if the MIDI system is unavailable.
057     */
058    public static InputModel create(OutputModel outputModel) throws MidiUnavailableException {
059        final InputModel input = new InputModel(new InitialMidiDevice());
060        outputModel.addOpenListener(input::setCurrentSequence);
061        outputModel.addPlayListener(input.userRecorder::clear);
062        outputModel.addCurrentTimeListener(input.userRecorder::setCurrentTime);
063        return input;
064    }
065
066    private void setCurrentSequence(@SuppressWarnings("unused") ParsedSequence sequence) {
067        userRecorder.clear();
068    }
069
070    private final class UserNoteRecorder implements Receiver, ParsedTrack {
071        private final Map<Object, NoteEvent> onNotes = new HashMap<>();
072        private final Map<Object, PedalEvent> onPedals = new HashMap<>();
073
074        private long currentTime;
075
076        private synchronized void setCurrentTime(long time) {
077            currentTime = time;
078        }
079
080        @Override
081        public synchronized void send(MidiMessage message, long timeStamp) {
082            EventFactory.create(message, currentTime).ifPresent(this::userPressedEvent);
083        }
084
085        private void userPressedEvent(Event event) {
086            if (event instanceof NoteEvent) {
087                userPressedEvent((NoteEvent) event, onNotes, notes);
088            } else if (event instanceof PedalEvent) {
089                userPressedEvent((PedalEvent) event, onPedals, pedals);
090            }
091            inputListeners.forEach(listener -> listener.accept(event));
092        }
093
094        private <K extends Event> void userPressedEvent(K event, Map<Object, K> onEvents,
095                IntervalSet<EventPair<K>> fullEvents) {
096            synchronized (fullEvents) {
097                final long eventTime = event.getTime();
098                if (event.isOn()) {
099                    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}