diff --git a/Java/MoppyControlGUI/src/main/java/com/moppy/control/MoppyControlGUI.java b/Java/MoppyControlGUI/src/main/java/com/moppy/control/MoppyControlGUI.java index 2fe198f..d7ae005 100644 --- a/Java/MoppyControlGUI/src/main/java/com/moppy/control/MoppyControlGUI.java +++ b/Java/MoppyControlGUI/src/main/java/com/moppy/control/MoppyControlGUI.java @@ -76,7 +76,7 @@ public void run() { java.awt.EventQueue.invokeLater(new Runnable() { @Override public void run() { - new MainWindow(statusBus, midiSequencer, netManager, mappers, postProcessor).setVisible(true); + new MainWindow(statusBus, receiverSender, midiSequencer, netManager, mappers, postProcessor).setVisible(true); } }); } diff --git a/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/MainWindow.form b/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/MainWindow.form index 6b9b502..aceae89 100644 --- a/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/MainWindow.form +++ b/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/MainWindow.form @@ -72,7 +72,7 @@ - + diff --git a/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/MainWindow.java b/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/MainWindow.java index 34e14b0..f8dc6c8 100644 --- a/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/MainWindow.java +++ b/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/MainWindow.java @@ -3,6 +3,7 @@ import com.moppy.control.GUIControlledPostProcessor; import com.moppy.control.NetworkManager; import com.moppy.core.events.mapper.MapperCollection; +import com.moppy.core.midi.MoppyMIDIReceiverSender; import com.moppy.core.midi.MoppyMIDISequencer; import com.moppy.core.status.StatusBus; import javax.sound.midi.MidiMessage; @@ -13,6 +14,7 @@ public class MainWindow extends javax.swing.JFrame { private final StatusBus statusBus; + private final MoppyMIDIReceiverSender receiverSender; private final MoppyMIDISequencer midiSequencer; private final NetworkManager netManager; private final MapperCollection mappers; @@ -22,8 +24,9 @@ public class MainWindow extends javax.swing.JFrame { /** * Creates new form MainWindow */ - public MainWindow(StatusBus statusBus, MoppyMIDISequencer midiSequencer, NetworkManager netManager, MapperCollection mappers, GUIControlledPostProcessor postProc) { + public MainWindow(StatusBus statusBus, MoppyMIDIReceiverSender receiverSender, MoppyMIDISequencer midiSequencer, NetworkManager netManager, MapperCollection mappers, GUIControlledPostProcessor postProc) { this.statusBus = statusBus; + this.receiverSender = receiverSender; this.midiSequencer = midiSequencer; this.netManager = netManager; this.mappers = mappers; @@ -40,6 +43,7 @@ public MainWindow(StatusBus statusBus, MoppyMIDISequencer midiSequencer, Network private void initComponents() { sequencerPanel = new com.moppy.control.gui.SequencerPanel(); + sequencerPanel.setReceiverSender(receiverSender); sequencerPanel.setMidiSequencer(midiSequencer); sequencerPanel.setPostProcessor(postProc); statusBus.registerConsumer(sequencerPanel); diff --git a/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/SequencerPanel.form b/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/SequencerPanel.form index c36d4e2..5f43115 100644 --- a/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/SequencerPanel.form +++ b/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/SequencerPanel.form @@ -80,6 +80,11 @@ + + + + + @@ -92,31 +97,46 @@ - - - - + + - + + + + + + + + + + + + + + - - - + + + + + + + + - + + + + + - - - - - @@ -144,7 +164,17 @@ - + + + + + + + + + + + @@ -225,6 +255,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/SequencerPanel.java b/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/SequencerPanel.java index 89affcd..e42fed7 100644 --- a/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/SequencerPanel.java +++ b/Java/MoppyControlGUI/src/main/java/com/moppy/control/gui/SequencerPanel.java @@ -2,6 +2,7 @@ import com.moppy.control.GUIControlledPostProcessor; import com.moppy.control.MoppyPreferences; +import com.moppy.core.midi.MoppyMIDIReceiverSender; import com.moppy.core.midi.MoppyMIDISequencer; import com.moppy.core.status.StatusConsumer; import com.moppy.core.status.StatusUpdate; @@ -11,9 +12,14 @@ import java.io.IOException; import java.time.Duration; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.sound.midi.InvalidMidiDataException; +import javax.sound.midi.MidiDevice; +import javax.sound.midi.MidiSystem; +import javax.sound.midi.MidiUnavailableException; import javax.swing.JFileChooser; import javax.swing.JPanel; import javax.swing.Timer; @@ -29,10 +35,16 @@ public class SequencerPanel extends JPanel implements StatusConsumer, ActionList private static final String BTN_PLAY = "⏵"; private static final String BTN_PAUSE = "⏸"; + private MoppyMIDIReceiverSender receiverSender; private MoppyMIDISequencer midiSequencer; private GUIControlledPostProcessor postProc; private final Timer sequenceProgressUpdateTimer; + private Map midiInDevices = new HashMap<>(); + private Map midiOutDevices = new HashMap<>(); + private MidiDevice currentMidiInDevice = null; + private MidiDevice currentMidiOutDevice = null; + /** * Creates new form SequencerPanel */ @@ -43,6 +55,11 @@ public SequencerPanel() { initComponents(); setControlsEnabled(false); // Leave these disabled until we've loaded a sequence + refreshMidiDevices(); + } + + public void setReceiverSender(MoppyMIDIReceiverSender receiverSender) { + this.receiverSender = receiverSender; } public void setMidiSequencer(MoppyMIDISequencer midiSequencer) { @@ -53,6 +70,33 @@ public void setPostProcessor(GUIControlledPostProcessor postProc) { this.postProc = postProc; } + private void refreshMidiDevices() { + try{ + // Get all MIDI devices and add them to the devices maps based on capabilities + for (MidiDevice.Info mdi : MidiSystem.getMidiDeviceInfo()) { + if (MidiSystem.getMidiDevice(mdi).getMaxTransmitters() != 0) { + midiInDevices.put(mdi.getName(), mdi); + } + + if (MidiSystem.getMidiDevice(mdi).getMaxReceivers() != 0){ + midiOutDevices.put(mdi.getName(), mdi); + } + } + } + catch (Exception ex) { + Logger.getLogger(SequencerPanel.class.getName()).log(Level.WARNING, "Exception getting list of MIDI devices-- MIDI In/Out will be unavailable", ex); + } + + midiInCB.removeAllItems(); + midiOutCB.removeAllItems(); + + midiInCB.addItem("None"); + midiOutCB.addItem("None"); + + midiInDevices.keySet().forEach((key) -> midiInCB.addItem(key)); + midiOutDevices.keySet().forEach((key) -> midiOutCB.addItem(key)); + } + /** * This method is called from within the constructor to initialize the form. WARNING: Do NOT modify this code. The content of this method is always regenerated by the Form Editor. */ @@ -72,6 +116,10 @@ private void initComponents() { volumeSlider = new javax.swing.JSlider(); volumeSliderLabel = new javax.swing.JLabel(); volumeOverrideCB = new javax.swing.JCheckBox(); + midiInLabel = new javax.swing.JLabel(); + midiInCB = new javax.swing.JComboBox<>(); + midiOutLabel = new javax.swing.JLabel(); + midiOutCB = new javax.swing.JComboBox<>(); sequenceFileChooser.setCurrentDirectory(new File(MoppyPreferences.getConfiguration().getFileLoadDirectory())); sequenceFileChooser.setDialogTitle("Select MIDI File"); @@ -90,6 +138,8 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { } }); + controlsPane.setMinimumSize(new java.awt.Dimension(400, 149)); + sequenceCurrentTimeLabel.setText("00:00"); sequenceSlider.setMajorTickSpacing(60); @@ -147,6 +197,26 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { } }); + midiInLabel.setText("MIDI In:"); + midiInLabel.setToolTipText("MIDI device to receive events from"); + + midiInCB.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" })); + midiInCB.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + midiInCBActionPerformed(evt); + } + }); + + midiOutLabel.setText("MIDI Out:"); + midiOutLabel.setToolTipText("MIDI device to send all raw MIDI events to."); + + midiOutCB.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" })); + midiOutCB.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + midiOutCBActionPerformed(evt); + } + }); + javax.swing.GroupLayout controlsPaneLayout = new javax.swing.GroupLayout(controlsPane); controlsPane.setLayout(controlsPaneLayout); controlsPaneLayout.setHorizontalGroup( @@ -158,24 +228,34 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { .addComponent(sequenceCurrentTimeLabel) .addGap(175, 362, Short.MAX_VALUE)) .addGroup(controlsPaneLayout.createSequentialGroup() - .addComponent(stopButton, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(controlsPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlsPaneLayout.createSequentialGroup() - .addComponent(sequenceSlider, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(controlsPaneLayout.createSequentialGroup() + .addComponent(stopButton, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(sequenceTotalTimeLabel)) + .addGroup(controlsPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlsPaneLayout.createSequentialGroup() + .addComponent(sequenceSlider, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(sequenceTotalTimeLabel)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlsPaneLayout.createSequentialGroup() + .addComponent(playButton, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(volumeSliderLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(volumeSlider, javax.swing.GroupLayout.PREFERRED_SIZE, 104, javax.swing.GroupLayout.PREFERRED_SIZE)))) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlsPaneLayout.createSequentialGroup() - .addComponent(playButton, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(volumeSliderLabel) + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(volumeOverrideCB)) + .addGroup(controlsPaneLayout.createSequentialGroup() + .addGroup(controlsPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(midiInLabel) + .addComponent(midiOutLabel)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(volumeSlider, javax.swing.GroupLayout.PREFERRED_SIZE, 104, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGroup(controlsPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(midiOutCB, javax.swing.GroupLayout.PREFERRED_SIZE, 190, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(midiInCB, javax.swing.GroupLayout.PREFERRED_SIZE, 190, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(0, 0, Short.MAX_VALUE))) .addContainerGap()))) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlsPaneLayout.createSequentialGroup() - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(volumeOverrideCB) - .addContainerGap()) ); controlsPaneLayout.setVerticalGroup( controlsPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -197,7 +277,15 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { .addComponent(volumeSliderLabel))) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(volumeOverrideCB) - .addContainerGap(64, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(controlsPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(midiInLabel) + .addComponent(midiInCB, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(controlsPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(midiOutLabel) + .addComponent(midiOutCB, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap(16, Short.MAX_VALUE)) ); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); @@ -275,11 +363,62 @@ private void volumeOverrideCBActionPerformed(java.awt.event.ActionEvent evt) {// postProc.setOverrideVelocity(volumeOverrideCB.isSelected()); }//GEN-LAST:event_volumeOverrideCBActionPerformed + private void midiOutCBActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_midiOutCBActionPerformed + if (receiverSender == null) { + return; // We can't do anything if the receiverSender hasn't been initialized / set yet. + } + + String selectedName = midiOutCB.getSelectedItem().toString(); + + if (midiOutDevices.containsKey(selectedName)) { + try { + currentMidiOutDevice = MidiSystem.getMidiDevice(midiOutDevices.get(selectedName)); + currentMidiOutDevice.open(); + receiverSender.setMidiThru(MidiSystem.getMidiDevice(midiOutDevices.get(selectedName)).getReceiver()); + } catch (MidiUnavailableException ex) { + Logger.getLogger(SequencerPanel.class.getName()).log(Level.SEVERE, null, ex); + midiOutCB.setSelectedIndex(0); // On exception, set menu back to "None" + } + } else { + receiverSender.setMidiThru(null); // Disable thru sending + } + }//GEN-LAST:event_midiOutCBActionPerformed + + private void midiInCBActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_midiInCBActionPerformed + // If we'd previously selected a MIDI In device, remove us as a receiver so we're not getting + // events from it. + if (currentMidiInDevice != null) { + currentMidiInDevice.close(); + } + + if (receiverSender == null) { + return; // We can't do anything if the receiverSender hasn't been initialized / set yet. + } + + String selectedName = midiInCB.getSelectedItem().toString(); + + if (midiInDevices.containsKey(selectedName)) { + try { + // Set currentMidiInDevice so we can remove ourselves as its receiver later + currentMidiInDevice = MidiSystem.getMidiDevice(midiInDevices.get(selectedName)); + currentMidiInDevice.open(); + currentMidiInDevice.getTransmitter().setReceiver(receiverSender); + } catch (MidiUnavailableException ex) { + Logger.getLogger(SequencerPanel.class.getName()).log(Level.SEVERE, null, ex); + midiInCB.setSelectedIndex(0); // On exception, set menu back to "None" + } + } + }//GEN-LAST:event_midiInCBActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JPanel controlsPane; private javax.swing.JLabel fileNameLabel; private javax.swing.JButton loadFileButton; + private javax.swing.JComboBox midiInCB; + private javax.swing.JLabel midiInLabel; + private javax.swing.JComboBox midiOutCB; + private javax.swing.JLabel midiOutLabel; private javax.swing.JButton playButton; private javax.swing.JLabel sequenceCurrentTimeLabel; private javax.swing.JFileChooser sequenceFileChooser; diff --git a/Java/MoppyLib/src/main/java/com/moppy/core/midi/MoppyMIDIReceiverSender.java b/Java/MoppyLib/src/main/java/com/moppy/core/midi/MoppyMIDIReceiverSender.java index 923497d..6572e8d 100644 --- a/Java/MoppyLib/src/main/java/com/moppy/core/midi/MoppyMIDIReceiverSender.java +++ b/Java/MoppyLib/src/main/java/com/moppy/core/midi/MoppyMIDIReceiverSender.java @@ -6,6 +6,7 @@ import com.moppy.core.events.mapper.MapperCollection; import com.moppy.core.events.postprocessor.MessagePostProcessor; import java.io.IOException; +import java.util.Optional; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -20,6 +21,7 @@ public class MoppyMIDIReceiverSender extends StatusSender implements Receiver { private final MapperCollection mappers; private final MessagePostProcessor postProcessor; + private Optional midiThru = Optional.empty(); public MoppyMIDIReceiverSender(MapperCollection mapperCollection, MessagePostProcessor postProcessor, NetworkBridge netBridge) throws IOException { super(netBridge); @@ -39,6 +41,11 @@ public void send(MidiMessage message, long timeStamp) { Logger.getLogger(MoppyMIDIReceiverSender.class.getName()).log(Level.WARNING, null, ex); } }); + + // If a midiThru receiver has been specified, forward the message. + if (midiThru.isPresent()) { + midiThru.get().send(message, timeStamp); + } } @Override @@ -47,4 +54,12 @@ public void close() { // or just control those directly on bridge instance } + /** + * Sets a midi receiver to receive all MIDI messages. + * @param midiThru Receiver for MIDI messages, or null to disable MIDI Throughput + */ + public void setMidiThru(Receiver midiThru) { + this.midiThru = midiThru != null ? Optional.of(midiThru) : Optional.empty(); + } + }