MIDI simulation
As you already might have noticed I love simulations. To test our MIDI connection and message handling we need a simple
Application which queries the javax.sound subsystem for receivers and let us send midi messages. And because I am to
lazy to type, my sidekick steps in:
Can you sketch a JavaFX app which has one slider and sends midi volume messages on channel 1?
Midi Transmitter
public class MainVolumeSlider
extends Application
{
private static final int MIDI_CHANNEL = 0; // Channel 1 β index 0
private static final int CC_VOLUME = 7; // Channel Volume (MSB)
private Receiver receiver; // currently selected receiver
private MidiDevice currentDevice; // keep a handle so we can close it
@Override
public void start(final Stage stage)
{
Let’s create a dropdown with all MIDI receivers
// --- UI: device chooser + volume slider
final Label deviceLabel = new Label("MIDI Output:");
final ComboBox<MidiDevice.Info> deviceCombo
= new ComboBox<>(FXCollections.observableArrayList(listOutputDevices()));
deviceCombo.setCellFactory(cb -> new MidiInfoListCell());
deviceCombo.setButtonCell(new MidiInfoListCell());
And a slider which send Volume Change message values between 0..127
final Label valueLabel = new Label("Volume: 100");
final Slider slider = new Slider(0, 127, 100);
slider.setMajorTickUnit(16);
slider.setMinorTickCount(3);
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
slider.setBlockIncrement(1);
slider.setSnapToTicks(true);
// Send CC on change
final ChangeListener<Number> sliderListener = (obs, oldV, newV) -> {
int v = newV.intValue();
valueLabel.setText("Volume: " + v);
sendCC(CC_VOLUME, v);
};
slider.valueProperty().addListener(sliderListener);
// When a device is selected, open it and send the current slider value
deviceCombo.getSelectionModel().selectedItemProperty().addListener((obs, oldInfo, newInfo) -> {
openReceiverFor(newInfo);
sendCC(CC_VOLUME, (int) slider.getValue());
});
// Pick the first available device if any; else fall back to default system receiver
if (!deviceCombo.getItems().isEmpty()) {
deviceCombo.getSelectionModel().selectFirst();
} else {
// As a fallback, try default system receiver (often routes to the default synth)
try {
receiver = MidiSystem.getReceiver();
} catch (MidiUnavailableException e) {
receiver = null;
}
}
final Label title = new Label("MIDI Volume (Ch 1, CC 7)");
final VBox root = new VBox(12, title, deviceLabel, deviceCombo, slider, valueLabel);
root.setPadding(new Insets(16));
stage.setTitle("MIDI Volume β Specific Output");
stage.setScene(new Scene(root, 460, 220));
stage.show();
}
/** Enumerate MIDI devices that can provide a Receiver (maxReceivers != 0). */
private List<MidiDevice.Info> listOutputDevices()
{
...
}
/** Open the device for the given Info and keep its Receiver as the current target. */
private void openReceiverFor(final MidiDevice.Info info)
{
...
}
whenever slider is dragged a value is send as MIDI message to listener(s)
private void sendCC(int controller, int value)
{
if (receiver == null) return;
try {
ShortMessage msg = new ShortMessage();
msg.setMessage(ShortMessage.CONTROL_CHANGE, MIDI_CHANNEL, controller, value);
receiver.send(msg, -1);
} catch (InvalidMidiDataException e) {
e.printStackTrace();
}
}
private void closeCurrentReceiver()
{
...
}
@Override
public void stop()
{
closeCurrentReceiver();
}
// Small helper to show nicer names in the ComboBox
private static class MidiInfoListCell extends javafx.scene.control.ListCell<MidiDevice.Info>
{
@Override protected void updateItem(final MidiDevice.Info item, final boolean empty)
{
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.getName() + " β " + item.getVendor() + " (" + item.getDescription() + ")");
}
}
}
public static void main(final String[] args)
{
launch(args);
}
}
As usual find source on github
Alright, letβs fire it up and see transmitter and receiver in action. On the left side we have our JavaFX application, on the right the midi viewer. You can see: as soon as the slider is moved, the viewer receives MIDI messages (in HEX): B0 07 XX
MIDI
Let’s break down what we’re seeing: MIDI messages are divided into Channel Messages and System Messages indicated by the upper 4 bits (the nibble) of the first byte. For now, we’ll focus on Channel Messages.
MIDI Channel Messages
Channel messages consist of either two or three bytes. Hereβs an overview:
| Status Byte (Hex) | Status Byte (Binary) | Message Type | Data Byte 1 | Data Byte 2 | Description |
|---|---|---|---|---|---|
| 0x8n | 1000cccc | Note Off | Note Number | Release Velocity | Turns a note off on channel. |
| 0x9n | 1001cccc | Note On | Note Number | Velocity | Turns a note on; velocity 0 is often treated as Note Off. |
| 0xAn | 1010cccc | Polyphonic Aftertouch | Note Number | Pressure Amount | Per-note pressure after the key is pressed. |
| 0xBn | 1011cccc | Control Change (CC) | Controller Number | Controller Value | Adjusts parameters like modulation, volume, pan, etc. |
| 0xCn | 1100cccc | Program Change | Program Number | None | Changes instrument/patch. |
| 0xDn | 1101cccc | Channel Pressure Aftertouch | Pressure Amount | None | Pressure applied to all notes on the channel. |
| 0xEn | 1110cccc | Pitch Bend Change | LSB | MSB | 14-bit value, center = 8192. |
The upper 4 bits of the status byte define the message type (0x8..0xE = 0b1000..0b1110 = 8..14), which all describes Channel Messages. The lower 4 bits (cccc) select one of 16 possible channels (values 0..15 correspond to channel 1..16). Each data byte always starts with a leading 0 bit (0b0vvvvvvv), therefore MIDI data values range from 0β127. Most of the Channel Messages are 3 byte messages (one status byte + two data bytes), except for Program Change and Channel Pressure Aftertouch, which only have one data byte.
Thus 0xB0 0x07 0x1C means: Control Change (CC), on Channel 1, Value 28
Time to send these messages to real hardware!