/****************************************************************************** * Compilation: javac MidiSource.java * Execution: java MidiSource [-p] [filename.mid] * * A MidiSource object produces MIDI (Musical Instrument Digital Interface) * messages, where the source can be a hardware MIDI controller * keyboard or a MIDI file. Generating MIDI messages from a MIDI * file uses the default Java MIDI Sequencer, so that MIDI messages are * scheduled appropriately. * * Version: .3 * ******************************************************************************/ import javax.sound.midi.*; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.concurrent.LinkedBlockingDeque; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.HashMap; /** * The {@code MidiSource} class is used to create objects that produce MIDI * (Musical Instrument Digital Interface) messages. The source can be a * hardware MIDI controller keyboard or a MIDI file. Generating MIDI messages * from a MIDIfile uses the default Java MIDI Sequencer, so that MIDI messages * are scheduled appropriately. * * @author Nico Toy * @author Alan Kaplan */ public final class MidiSource { // keep track if source if "live" controller or static file private static final int MIDI_CONTROLLER = 0; private static final int MIDI_FILE = 1; private int sourceType; // queue for midi messages- produced by MIDI transmitter (keyboard controller or sequencer) private LinkedBlockingDeque midiMessageQueue; private MidiDevice device; // hardware keyboard controller private Sequencer sequencer; // Java MIDI sequencer private boolean verbose = false; // indicates if MidiSource should print information // about MidiMessages to stdout as messages are // produced private boolean playSynth = false; // indicates if MidiSource should play notes using // default Java Synthesizer as messages are // produced // MetaMessage event code for end of track private static final int MIDI_END_OF_TRACK = 47; // short message field names private static final HashMap SM_FIELDS = MidiSource.setShortMessageFields(); private static HashMap setShortMessageFields() { HashMap map = new HashMap(); Field[] declaredFields = ShortMessage.class.getDeclaredFields(); for (Field field : declaredFields) { if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) { try { map.put(field.getInt(null), field.getName()); } catch (IllegalAccessException e) { throw new RuntimeException(e.getMessage()); } } } return map; } /** * Helper method - pretty prints a MidiMessage */ private static void print(MidiMessage message) { if (message instanceof ShortMessage) { ShortMessage shortMessage = (ShortMessage) message; if (shortMessage.getCommand() != 240) { // some MIDI controllers continousouly output a 240 System.out.print("ShortMessage: "); System.out.print(" Command: " + SM_FIELDS.get(shortMessage.getCommand()) + " (" + shortMessage.getCommand() + ") "); System.out.print(" Channel: " + shortMessage.getChannel()); if (shortMessage.getCommand() == ShortMessage.NOTE_ON) { System.out.print(" Number: " + shortMessage.getData1()); System.out.print(" Velocity: " + shortMessage.getData2()); } else if (shortMessage.getCommand() == ShortMessage.NOTE_OFF) { System.out.print(" Number: " + shortMessage.getData1()); System.out.print(" Velocity: " + shortMessage.getData2()); } else if (shortMessage.getCommand() == ShortMessage.CONTROL_CHANGE) { System.out.print(" Number: " + shortMessage.getData1()); System.out.print(" Data2: " + shortMessage.getData2()); } else { System.out.print(" Data1: " + shortMessage.getData1()); System.out.print(" Data2: " + shortMessage.getData2()); } System.out.println(); } else if (message instanceof SysexMessage) { System.out.println("SysexMessage"); } else if (message instanceof MetaMessage) { System.out.print("MetaMessage: "); MetaMessage metaMessage = (MetaMessage) message; System.out.println(metaMessage.getType()); } else { } } } /** * Private helper class that receives MidiMessages, and * adds each MIDI message received to a MidiSource queue. Optionally * (1) prints messages to stdout and (2) plays messages using Java * Synthesizer */ private class MidiKeyboardControllerReceiver implements Receiver { private boolean verbose = false; // default - do not print message to stdout private boolean playSynth = false; // default - do not play synthesizer private Synthesizer synth = null; // default Java Synthesizer private MidiChannel[] channels = null; // defaul - Java Sythesizer channels public MidiKeyboardControllerReceiver(boolean verbose, boolean playSynth) { midiMessageQueue = new LinkedBlockingDeque(); this.verbose = verbose; this.playSynth = playSynth; // if this Receiver needs to play notes, set up channels if (playSynth) { try { synth = MidiSystem.getSynthesizer(); } catch (MidiUnavailableException e) { e.printStackTrace(); System.exit(1); } try { synth.open(); } catch (MidiUnavailableException e) { e.printStackTrace(); System.exit(1); } channels = synth.getChannels(); } } @Override // Invoked each time Receiver gets a MidiMessage public void send(MidiMessage message, long timeStamp) { // add the message to the queue midiMessageQueue.add(message); // print message? if (verbose) print(message); // play this note for a keyboard controller if (playSynth) if (message instanceof ShortMessage) { ShortMessage shortMessage = (ShortMessage) message; if (shortMessage.getCommand() == ShortMessage.NOTE_ON) channels[shortMessage.getChannel()].noteOn(shortMessage.getData1(), shortMessage.getData2()); else if (shortMessage.getCommand() == ShortMessage.NOTE_OFF) { channels[shortMessage.getChannel()].noteOff(shortMessage.getData1(), shortMessage.getData2()); } } } // close the Receiver stream public void close() { synth.close(); midiMessageQueue = null; } } /** * Private helper class that receives MidiMessages, and * adds each MIDI message received to a MidiSource queue. Optionally * prints messages to stdout */ private class MidiFileReceiver implements Receiver { private boolean verbose = false; // default - do not print message to stdout public MidiFileReceiver(boolean verbose) { midiMessageQueue = new LinkedBlockingDeque(); this.verbose = verbose; } @Override // Invoked each time Receiver gets a MidiMessage public void send(MidiMessage message, long timeStamp) { // add the message to the queue midiMessageQueue.add(message); // print message? if (verbose) print(message); } // close the Receiver stream public void close() { midiMessageQueue = null; } } /** * Search for connected Midi Keyboard controller. If found, returns a * a openned MidiDevice. * * @param verbose log information about the device to stdout */ private static MidiDevice openMidiController(boolean verbose) { // get installed Midi devices MidiDevice.Info deviceInfo[] = MidiSystem.getMidiDeviceInfo(); MidiDevice device = null; for (int i = 0; i < deviceInfo.length; i++) { if (verbose) { System.out.print("DEVICE " + i + ": "); System.out.print(deviceInfo[i].getName() + ", "); System.out.print(deviceInfo[i].getVendor() + ", "); System.out.print(deviceInfo[i].getDescription() + ", "); } try { device = MidiSystem.getMidiDevice(deviceInfo[i]); if (verbose) System.out.print("Midi device available, "); } catch (MidiUnavailableException e) { if (verbose) System.out.println("Midi unavailable, trying next..."); continue; } // To detect if a MidiDevice represents a hardware MIDI port: // https://docs.oracle.com/javase/7/docs/api/javax/sound/midi/MidiDevice.html if ( ! (device instanceof Sequencer) && ! (device instanceof Synthesizer)) { if (!(device.isOpen())) { try { device.open(); } catch (MidiUnavailableException e) { if (verbose) System.out.println("Unable to open Midi device, trying next..."); continue; } } // check for a valid Transmitter try { Transmitter transmitter = device.getTransmitter(); } catch (MidiUnavailableException e) { if (verbose) System.out.println("Failed to get transmitter, trying next..."); device.close(); continue; } if (verbose) System.out.println("Valid MIDI controller connected."); break; } else { if (verbose) System.out.println("Not a MIDI keyboard controller, trying next..."); device = null; } } return device; } /** * Creates a MIDISource object listens to the first found connected MIDI input device. * * @param verbose true turns on logging * @param connectToSynth use default Java sound synthesizer * @throws RuntimeException if no device was found or if writing to the log * file failed */ public MidiSource(boolean verbose, boolean connectToSynth) { MidiDevice keyboard = openMidiController(verbose); if (keyboard == null) throw new RuntimeException("Unable to connect to a MIDI keyboard controller."); try { Transmitter transmitter = keyboard.getTransmitter(); transmitter.setReceiver(new MidiKeyboardControllerReceiver(verbose, connectToSynth)); sourceType = MIDI_CONTROLLER; } catch (MidiUnavailableException e) { e.printStackTrace(); System.exit(1); } } /** * * @param connectToSynth use default Java sound synthesizer * @throws RuntimeException if no device was found or if writing to the log * file failed */ /** * Creates a MIDISource object the produces MIDI messages from a * time-stamped MIDI file, where each * each message is buffered and becomes available for consumption by the * client once it is "played" from the file * @param filename the name of the file to play from * @param verbose true turns on logging * @param connectToSynth true if Sequencer should connect to Sequencer * @throws RuntimeException if the file is not found or not a valid MIDI * file, or if reading from the file failed */ public MidiSource(String filename, boolean verbose, boolean connectToSynth) { playSynth = connectToSynth; sourceType = MIDI_FILE; try { sequencer = MidiSystem.getSequencer(connectToSynth); } catch (MidiUnavailableException e) { e.printStackTrace(); } FileInputStream fileInputStream; try { fileInputStream = new FileInputStream(filename); } catch (FileNotFoundException e) { throw new RuntimeException("File not found"); } // connect file to sequencer try { sequencer.setSequence(fileInputStream); sequencer.getTransmitter().setReceiver(new MidiFileReceiver(verbose)); } catch (IOException e) { throw new RuntimeException("Error reading file: " + filename); } catch (InvalidMidiDataException e) { throw new RuntimeException("Invalid MIDI file: " + filename); } catch (MidiUnavailableException e) { throw new RuntimeException("MIDI unavailable: " + filename); } try { // Add a listener for meta message events sequencer.addMetaEventListener(new MetaEventListener() { public void meta(MetaMessage event) { // close the Sequencer when done if (event.getType() == MIDI_END_OF_TRACK) { // Sequencer is done playing close(); } } }); sequencer.open(); } catch (MidiUnavailableException e) { e.printStackTrace(); } } /** * Starts the MIDISource so it can produce messages. * */ public void start () { if (sourceType == MIDI_CONTROLLER) { } else if (sourceType == MIDI_FILE) sequencer.start(); else throw new RuntimeException("MidiSource: Illegal source type: " + sourceType); } /** * Return whether there are new MIDI messages available. * * @return true if and only if there are new messages available to consume */ public boolean isEmpty() { return midiMessageQueue.size() == 0; } /** * Return the next available short MIDI message (in FIFO order). All messages, * including the short MIDI message are "consumed", i.e., it will no longer be * available after this call. Returns null if queue is empty. * * @return The next available {@link MidiMessage} */ private ShortMessage getMidiMessage() { while (!this.isEmpty()) { MidiMessage message = midiMessageQueue.remove(); if (message instanceof ShortMessage) return (ShortMessage) message; // ignore other messages } return null; // if empty } /** * Return the code of the MIDIController key pressed. * * @return code of key pressed */ public int nextKeyPressed() { ShortMessage message = getMidiMessage(); if (message == null) return -1; else if (message.getCommand() == ShortMessage.NOTE_ON) return message.getData1(); else return -1; } /** * Return the next short MIDI message in the queue. * * @return Short MIDI message, null otherwise */ public ShortMessage nextMessage() { ShortMessage message = getMidiMessage(); if (message == null) return null; else return message; } /** * Static helper method. Extract the key code from a short * MIDI message, where commmand == NOTE_ON * * @param message ShortMessage object * @return key code number */ public static int getKey(ShortMessage message) { return message.getData1(); } /** * Static helper method. Extract the velocity from a short * MIDI message, where commmand == NOTE_ON * * @param message ShortMessage object * @return key code number */ public static int getVelocity(ShortMessage message) { return message.getData2(); } /** * Static helper method. Extract the channel from a short * MIDI message. * * @param message ShortMessage object * @return channel number */ public static int getChannel(ShortMessage message) { return message.getChannel(); } /** * Either stop listening for input from the device or stop playback from * the MIDI file. */ public void close() { if (sourceType == MIDI_CONTROLLER && device.isOpen()) { device.close(); } else if (sourceType == MIDI_FILE) { sequencer.stop(); sequencer.close(); } } /** * Return whether this MidiSource is still active * * @return if listening from device, true if and only if this instance is * still listening; if using from file, true if and only if the * playback is still active */ public boolean isActive() { if (sourceType == MIDI_CONTROLLER) { return device.isOpen(); } else if (sourceType == MIDI_FILE) { return sequencer.isRunning(); } else { return false; } } /** * Tests this {@code MIDISource} data type. * To test a MIDI keyboard controller connected to a computer: * java MidiSource [-p] * where the optional argument: * -p - indicates that the default JavaMIDI Synthesizer will * be used to play notes * * To test a MIDI file: * java MidiSource [-p] filename.mid * where the optional argument: * -p - indicates that the default JavaMIDI Synthesizer will * be used to play notes * and the argument: * filename - name of MIDI file * * * @param args the command-line arguments */ public static void main(String args[]) { String USAGE = "java MidiSource [-p] []"; String PLAY = "-p"; String VERSION = "MidiSource verison .3"; boolean VERBOSE = true; MidiSource source = null; System.out.println(VERSION); // make this receiver listen for input from first MIDI input device found if (args.length == 0) { // java MidiSource source = new MidiSource(VERBOSE, false); } else if (args.length == 1) { if (args[0].equals(PLAY)) // java MidiSource -p source = new MidiSource(VERBOSE, true); else { // java MidiSource somefile.mid source = new MidiSource(args[0], VERBOSE, false); source.start(); } } else if (args.length == 2) { if (args[0].equals(PLAY)) { // java MidiSource -p somefile.mid source = new MidiSource(args[1], VERBOSE, true); source.start(); } else if (args[1].equals(PLAY)) { // java MidiSource somefile.mid -p source = new MidiSource(args[0], VERBOSE, true); source.start(); } else System.out.println(USAGE); } else System.out.println(USAGE); } }