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();
+ }
+
}