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}