001package bradleyross.music.MidiRouter;
002// import java.awt.FlowLayout;
003import java.awt.GridLayout;
004import java.awt.GridBagLayout;
005// import java.awt.GridBagLayoutInfo;
006import java.awt.GridBagConstraints;
007import java.awt.HeadlessException;
008import java.awt.Dimension;
009import java.awt.Color;
010// import java.awt.Component;
011import java.awt.event.MouseListener;
012import java.awt.event.MouseEvent;
013// import java.awt.event.ComponentListener;
014// import java.awt.event.ComponentEvent;
015import java.awt.event.ActionListener;
016import java.awt.event.ActionEvent;
017import java.io.File;
018import java.io.FileInputStream;
019import java.io.PrintWriter;
020import java.io.IOException;
021import java.io.FileNotFoundException;
022import java.io.UnsupportedEncodingException;
023// import java.nio.ByteBuffer;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.Date;
027import java.util.GregorianCalendar;
028import javax.sound.midi.MidiSystem;
029import javax.sound.midi.MidiDevice;
030import javax.sound.midi.MidiUnavailableException;
031import javax.sound.midi.InvalidMidiDataException;
032import javax.sound.midi.Synthesizer;
033import javax.sound.midi.Transmitter;
034import javax.sound.midi.Sequence;
035import javax.sound.midi.Sequencer;
036import javax.sound.midi.Receiver;
037// import javax.sound.midi.MidiDeviceReceiver;
038// import javax.sound.midi.MidiDeviceTransmitter;
039import javax.sound.midi.MidiMessage;
040import javax.sound.midi.MidiChannel;
041import javax.swing.SwingUtilities;
042import javax.swing.JFrame;
043import javax.swing.JPanel;
044import javax.swing.JTextArea;
045import javax.swing.JMenuBar;
046import javax.swing.JMenu;
047import javax.swing.JMenuItem;
048import javax.swing.JFileChooser;
049import javax.swing.JOptionPane;
050import javax.swing.JScrollPane;
051// import javax.swing.JButton;
052import javax.swing.BoxLayout;
053// import javax.swing.border.Border;
054import javax.swing.BorderFactory;
055// import javax.swing.JTextField;
056import javax.swing.JLabel;
057/**
058 * Sample Java MIDI router.
059 * 
060 * @author Bradley Ross
061 * 
062 * <p>Does MidiSystem.getMidiDevice always return the same object given the
063 *    same MidiDevice.Info - The answer is no when the MIDI device is
064 *    a software synthesizer or sequencer.</p>
065 * 
066 * @see FlowLayout
067 * @see GridLayout
068 * @see GridBagLayout
069 * @see BoxLayout
070 *
071 */
072public class MidiRouter implements Runnable{
073        /**
074         * A value of zero means no diagnostic output, while 
075         * higher values create more output.
076         */
077        protected int debugFlag = 1;
078        /**
079         * Outermost panel for the main window.
080         */
081        protected JPanel outer;
082        /**
083         * Pane at the top of the main window that
084         * contains general information.
085         */
086        protected JPanel top;
087        /**
088         * Pane containing a list of potential MIDI
089         * sources.
090         */
091        protected JPanel left;
092        /**
093         * Pane containing a list of potential MIDI
094         * destinations.
095         */
096        protected JPanel right;
097        /**
098         * Swing {@link JFrame} attached to the main window.
099         */
100        protected JFrame frame;
101        /**
102         * Menu bar for the main window.
103         */
104        protected JMenuBar menuBar;
105
106
107        protected String title;
108        /**
109         * MIDI file to be used as source of MIDI information
110         * (e.g.: {@link Sequencer}).
111         */
112        protected File sourceFile;
113        /**
114         * MIDI file to be used as destination of MIDI
115         * information (e.g.: {@link Sequencer} and
116         * {@link Synthesizer})
117         */
118        protected File destFile;
119        protected MidiDevice.Info[] allDevices;
120        protected ArrayList<MidiDevice> sourceMidiDevices;
121        protected ArrayList<MidiDevice> destMidiDevices;
122        protected ArrayList<MidiDevice.Info> sourceDevices;
123        protected ArrayList<MidiDevice.Info> destDevices; 
124        protected ArrayList<Listing> sourceItems;
125        protected ArrayList<Listing>  destItems;
126
127        protected MidiMenuBar midiMenuBar = null;
128        protected File inputFile = null;
129        protected File outputFile = null;
130        protected File logFile = null;
131        protected Listing selectedSource = null;
132        protected Listing selectedDest = null;
133        protected Connection connection = new Connection();
134        /**
135         * This acts as a {@Link Receiver} of MIDI
136         * information and displays the data in a window.
137         */
138        protected LogReceiver logReceiver;
139        protected GregorianCalendar calendar;
140        /**
141         * Time MIDI connection was opened.
142         */
143        protected long musicStart;
144        /**
145         * This Connection object will send information from the
146         * source object to the log window.
147         */
148        protected Connection logConnection = new Connection();
149        /**
150         * If true, no changes are allowed to the selections in the
151         * source and destination columns.
152         */
153        boolean selectionLocked = false;
154        /**
155         * If true, MIDI messages from the source will be echoed to a
156         * Java Swing window.
157         */
158        boolean useLogWindow = true;
159        /**
160         * Specifies the way in which an endpoint is specified.
161         * 
162         * @author Bradley Ross
163         *
164         */
165        protected enum specType { 
166                /**
167                 * Specifying a {@link Receiver} object.
168                 */
169                RECEIVER, 
170                /**
171                 * Specifying a {@link Transmitter} object.
172                 */
173                TRANSMITTER, 
174                /**
175                 * Specifying a {@link MidiDevice} object.
176                 */
177                DEVICE, 
178                /**
179                 * Specifying a {@link MidiDevice.Info} object.
180                 */
181                INFO }
182        public MidiRouter() {
183                logReceiver = new LogReceiver();
184                calendar = new GregorianCalendar();
185        }
186        /**
187         * Constructs the menu bar.
188         * 
189         * @author Bradley Ross
190         *
191         */
192        protected class MidiMenuBar implements ActionListener {
193
194                JMenuItem toggleLog = null;
195                /**
196                 * This is the menu bar for the application.
197                 */
198                JMenuBar menuBar = null;
199
200                /**
201                 * Constructor populates Java application menu bar.
202                 * 
203                 * @param value JMenuBar structure belonging to {@link frame}.
204                 */
205                public MidiMenuBar(JMenuBar value) {
206                        menuBar = value;
207                        /*
208                         * Main menu
209                         */
210                        JMenu mainMenu = new JMenu("Midi Router");
211                        menuBar.add(mainMenu);
212                        JMenuItem aboutRouter = new JMenuItem("About MIDI Router");
213                        mainMenu.add(aboutRouter);
214                        aboutRouter.addActionListener(this);
215                        aboutRouter.setActionCommand("about");
216                        mainMenu.addSeparator();
217                        JMenuItem preferences = new JMenuItem("Preferences");
218                        preferences.setActionCommand("preferences");
219                        mainMenu.add(preferences);
220                        preferences.addActionListener(this);
221                        mainMenu.addSeparator();
222                        JMenuItem quit = new JMenuItem("Quit");
223                        quit.setActionCommand("quit");
224                        mainMenu.add(quit);
225                        quit.addActionListener(this);
226                        /*
227                         * File menu
228                         */
229                        JMenu fileMenu = new JMenu("File");
230                        JMenuItem setInput = new JMenuItem("Choose Input File");
231                        setInput.setActionCommand("setInput");
232                        setInput.addActionListener(this);
233                        JMenuItem setOutput = new JMenuItem("Choose Output File");
234                        setOutput.setActionCommand("setOutput");
235                        setOutput.addActionListener(this);
236                        JMenuItem setLog = new JMenuItem("Choose Log File");
237                        setLog.setActionCommand("setLog");
238                        setLog.addActionListener(this);
239                        toggleLog = new JMenuItem();
240                        if (useLogWindow) {
241                                toggleLog.setText("Turn Log Window off");
242                        } else {
243                                toggleLog.setText("Turn Log Window on");
244                        }
245                        toggleLog.setActionCommand("toggleLog");
246                        toggleLog.addActionListener(this);
247                        menuBar.add(fileMenu);
248                        fileMenu.add(setInput);
249                        fileMenu.add(setOutput);
250                        fileMenu.add(setLog);
251                        fileMenu.add(toggleLog);
252                        /*  Connections menu */
253                        JMenu connections = new JMenu("Connections");
254                        JMenuItem openConnection = new JMenuItem("Open");
255                        openConnection.setActionCommand("openConnection");
256                        openConnection.addActionListener(this);
257                        connections.add(openConnection);
258                        JMenuItem closeConnection = new JMenuItem("Close/Save");
259                        closeConnection.setActionCommand("closeConnection");
260                        closeConnection.addActionListener(this);
261                        connections.add(closeConnection);
262                        menuBar.add(connections);
263                }
264                /**
265                 * Listener for menu bar.
266                 * 
267                 * @param e action transmitted from clicking menu item
268                 */
269                public void actionPerformed(ActionEvent e) {
270                        try {
271                                String command = e.getActionCommand();
272                                if (debugFlag > 5) {
273                                        System.out.println(command);
274                                }
275                                if (command.equalsIgnoreCase("about")) {
276                                        System.out.println("About menu item clicked");
277
278                                } else if (command.equalsIgnoreCase("preferences")) {
279                                        System.out.println("Preferences menu item clicked");
280
281                                } else if (command.equalsIgnoreCase("quit")) {
282                                        frame.setVisible(false);
283                                        frame.dispose();
284                                } else if (command.equalsIgnoreCase("setInput")) {
285                                        if (debugFlag > 0) {
286                                                System.out.println("Choose Input File menu item clicked");
287                                        }
288                                        JFileChooser chooser = new JFileChooser();
289                                        int returnVal = chooser.showOpenDialog(frame);
290                                        if (returnVal == JFileChooser.APPROVE_OPTION) {
291                                                inputFile = chooser.getSelectedFile();
292                                        }
293                                        if (inputFile != null) {
294                                                String text = "Input file is " + inputFile.getName();
295                                                System.out.println(text);
296                                        } else {
297                                                showPopup("Problem", "No input file selected");
298                                        }
299                                } else if (command.equalsIgnoreCase("setOutput")) {
300                                        if (debugFlag > 0) {
301                                                System.out.println("Choose Output File menu item clicked");
302                                        }
303                                        JFileChooser chooser = new JFileChooser();
304                                        int returnVal = chooser.showSaveDialog(frame);
305                                        if (returnVal == JFileChooser.APPROVE_OPTION) {
306                                                outputFile = chooser.getSelectedFile();
307                                        }
308                                        if (outputFile != null) {
309                                                String text = "Output File is " + outputFile.getName();
310                                                System.out.println(text);
311                                        } else {
312                                                showPopup("Problem", "No output file selected");
313                                        }
314
315                                } else if (command.equalsIgnoreCase("setLog")) {
316                                        if (debugFlag > 0) {
317                                                System.out.println("Choose Log File menu item clicked");
318                                        }
319                                        JFileChooser chooser = new JFileChooser();
320                                        int returnVal = chooser.showSaveDialog(frame);
321                                        if (returnVal == JFileChooser.APPROVE_OPTION) {
322                                                logFile = chooser.getSelectedFile();
323                                        }
324                                        if (logFile != null) {
325                                                String text = "Log File is " + logFile.getName();
326                                                System.out.println(text);
327                                        } else {
328                                                showPopup("Problem", "No log file selected");
329                                        }
330                                } else if (command.equalsIgnoreCase("openConnection")) {
331                                        selectionLocked = true;
332                                        if (debugFlag > 0) {
333                                                System.out.println("Open Connection menu item clicked");
334                                                if (selectedSource != null && selectedSource.isSequencer) {
335                                                        System.out.println("Source is sequencer");
336                                                }
337                                                if (selectedDest != null && selectedDest.isSequencer) {
338                                                        System.out.println("Destination is sequencer");
339                                                }
340                                                if (selectedDest != null && selectedDest.isSynthesizer) {
341                                                        System.out.println("Destination is synthesizer");
342                                                }
343                                        }
344                                        if (selectedSource == null) {
345                                                String text = "Source MIDI not selected - Select a source";
346                                                showPopup("Problem", text);
347                                                return;
348                                        }
349
350                                        if (selectedDest == null) {
351                                                String text = "Destination MIDI not selected - Select a destination";
352                                                showPopup("Problem", text);
353                                                return;
354                                        }
355                                        if (selectedSource.isSequencer && 
356                                                        (inputFile == null || !inputFile.exists())) {
357                                                String text = "Must select input file for sequencer as input" +
358                                                                System.lineSeparator() +
359                                                                "Use File -> Choose Input File menu item to select file";
360                                                showPopup("Problem", text);
361                                                return;
362                                        }
363
364                                        if (selectedDest.isSequencer &&         outputFile == null) {
365                                                String text = "Must select output file for sequencer as output" +
366                                                                System.lineSeparator() +
367                                                                "Use File -> Choose Output File menu item to select file";
368                                                showPopup("Problem", text);
369                                                return;
370                                        }
371                                        if (useLogWindow) {
372                                                StringBuffer text = 
373                                                                new StringBuffer("Starting setup of connection to log window");
374                                                System.out.println(text.toString());
375                                                logConnection.setSourceDevice(selectedSource.getMidiDevice());
376                                                logConnection.setDestReceiver(logReceiver);
377                                                logConnection.openConnection();
378                                                if (debugFlag > 0) {
379                                                        System.out.println("Log connection opened");
380                                                }
381                                        }
382                                        MidiDevice sourceMidiDevice = selectedSource.getMidiDevice();
383                                        connection.setSourceDevice(sourceMidiDevice);
384                                        if (!sourceMidiDevice.isOpen()) {
385                                                sourceMidiDevice.open();
386                                        }
387                                        MidiDevice destMidiDevice = selectedDest.getMidiDevice();
388                                        connection.setDestDevice(destMidiDevice);
389                                        if (!destMidiDevice.isOpen()) {
390                                                destMidiDevice.open();
391                                        }
392                                        // connection.setSourceInfo(selectedSource.getMidiDeviceInfo());
393                                        // connection.setDestInfo(selectedDest.getMidiDeviceInfo());
394                                        if (sourceFile != null) {
395                                                connection.setSourceFile(sourceFile);
396                                        }
397                                        if (destFile != null) {
398                                                connection.setDestFile(destFile);
399                                        }
400                                        connection.openConnection();    
401                                        if (debugFlag > 0) {
402                                                System.out.println("Data connection opened");
403                                                MidiDevice source = selectedSource.getMidiDevice();
404                                                StringBuffer text2 = new StringBuffer("There are " +
405                                                                Integer.toString(source.getTransmitters().size()) +
406                                                                " transmitters for source");
407                                                System.out.println(text2.toString());
408                                        }
409                                        if (selectedSource.isSequencer) {
410                                                if (debugFlag > 0) {
411                                                        System.out.println("About to start playing MIDI file");
412                                                }
413                                                Sequence sequence2 = MidiSystem.getSequence(new FileInputStream(inputFile));
414                                                Sequencer sequencer = (Sequencer) selectedSource.getMidiDevice();
415                                                sequencer.setSequence(sequence2);
416                                                sequencer.start();
417                                        }
418                                } else if (command.equalsIgnoreCase("closeConnection")) {
419                                        selectionLocked = false;
420                                        connection.closeConnection();
421                                        System.out.println("Data connection closed");
422                                        if (useLogWindow) {
423                                                logConnection.closeConnection();
424                                                System.out.println("Log connection closed");
425                                        }
426                                        selectedSource.getMidiDevice().close();
427                                        selectedDest.getMidiDevice().close();
428                                } else if (command.equalsIgnoreCase("toggleLog")) {
429                                        if (selectionLocked) { return; }
430                                        if (useLogWindow) {
431                                                useLogWindow = false;
432                                                toggleLog.setText("Turn Log Window on");
433                                        } else {
434                                                useLogWindow = true;
435                                                toggleLog.setText("Turn Log Window off");
436                                        }
437                                }
438                        } catch (HeadlessException e2) {
439                                e2.printStackTrace();
440                                String text = "Display not found";
441                                showPopup("Error", text);
442                        } catch (MidiUnavailableException ex) {
443                                ex.printStackTrace();
444                                String text = "MidiUnavailableException Error";
445                                showPopup("Error", text);
446                        } catch (InvalidMidiDataException ex) {
447                                ex.printStackTrace();
448                                showPopup("Error", "InvalidMidiDataException");
449                        } catch (IOException ex) {
450                                ex.printStackTrace();
451                                showPopup("Error", "IOException error");
452                        }
453                }
454        }
455        /** 
456         * Represents connection between MIDI devices.
457         * 
458         * @author Bradley Ross
459         *
460         */
461        protected class Connection {
462                /**
463                 * Type of specification for the source of the connection.
464                 */
465                protected specType sourceType;
466                protected MidiDevice.Info sourceInfo = null;
467                protected MidiDevice sourceDevice = null;
468                protected Transmitter sourceTransmitter = null;
469                protected boolean sourceIsSequencer = false;
470                protected boolean sourceIsSynthesizer = false;
471                protected File sourceFile = null;
472                protected Receiver receiver = null;
473                protected Transmitter transmitter = null;
474                /**
475                 * Provide information on the source device.
476                 * @param value MidiDevice used for input to connection
477                 */
478                public void setSourceDevice(MidiDevice value) throws MidiUnavailableException {
479                        if (debugFlag > 0) {
480                                System.out.println("Starting Connection.setSourceDevice");
481                        }
482                        sourceType = specType.DEVICE;
483                        sourceDevice = value;
484                        sourceInfo = value.getDeviceInfo();
485                        if (Sequencer.class.isAssignableFrom(sourceDevice.getClass())) {
486                                sourceIsSequencer = true;
487                        }
488                        if (Synthesizer.class.isAssignableFrom(sourceDevice.getClass())) {
489                                sourceIsSynthesizer = true;
490                        }
491                }
492                public MidiDevice getSourceDevice() {
493                        return sourceDevice;
494                }
495                /**
496                 * Specify device providing input to connection.
497                 * <p>This method is deprecated because there is a problem
498                 *    with the Java code when dealing with software synthesizers
499                 *    and sequencers.  {@link MidiDevice.getDeviceInfo()} and
500                 *    {@link MidiSystem.getMidiDevice(MidiDevice.Info)} are not
501                 *    inverse functions in this situation.</p>
502                 *    
503                 * @param input device
504                 */
505                @Deprecated
506                public void setSourceInfo(MidiDevice.Info value) {
507                        sourceInfo = value;
508                        sourceType = specType.INFO;
509                        try {
510                                sourceDevice = MidiSystem.getMidiDevice(sourceInfo);
511                                if (Sequencer.class.isAssignableFrom(sourceDevice.getClass())) {
512                                        sourceIsSequencer = true;
513                                }
514                                if (Synthesizer.class.isAssignableFrom(sourceDevice.getClass())) {
515                                        sourceIsSynthesizer = true;
516                                }
517                        } catch (MidiUnavailableException ex) {
518                                ex.printStackTrace();
519                        }
520                }
521                /**
522                 * Specifies the {@link Transmitter} object that is the
523                 * source of the MIDI data.
524                 * @param value
525                 */
526                public void setSourceTransmitter(Transmitter value) {
527                        if (debugFlag > 0) {
528                                System.out.println("Starting Connection.setSourceTransmitter");
529                        }
530                        if (value == null) {
531                                throw new NullPointerException("Input value is null");
532                        }
533                        sourceType = specType.TRANSMITTER;
534                        transmitter = value;
535                        sourceTransmitter = value;
536                }
537                public MidiDevice.Info getSourceInfo() {
538                        return sourceInfo;
539                }
540                public void setSourceFile(File value) {
541                        sourceFile = value;
542                }
543                public File getSourceFile() {
544                        return sourceFile;
545                }
546                /**
547                 * Type of specification for the destination of the connection.
548                 */
549                protected specType destType;
550                protected MidiDevice.Info destInfo = null;
551                protected MidiDevice destDevice = null;
552                protected Receiver destReceiver = null;
553                protected boolean destIsSequencer = false;
554                protected boolean destIsSynthesizer  = false;
555                protected File destFile = null;
556                protected boolean active = false;
557                public void setDestDevice(MidiDevice value) {
558                        if (value == null) {
559                                throw new NullPointerException("Input value is null");
560                        }
561                        if (debugFlag > 0) {
562                                System.out.println("Starting Connection.setDestDevice");
563                        }
564                        destDevice = value;
565                        destType = specType.DEVICE;
566                        destInfo = value.getDeviceInfo();
567                        if (Sequencer.class.isAssignableFrom(destDevice.getClass())) {
568                                destIsSequencer = true;
569                        }
570                        if (Synthesizer.class.isAssignableFrom(destDevice.getClass())) {
571                                destIsSynthesizer = true;
572                                ((Synthesizer) destDevice).getDefaultSoundbank();
573                        }
574                }
575                /**
576                 * Specify MIDI device to receive information from connection.
577                 * 
578                 * <p>This method is deprecated because there is a problem
579                 *    with the Java code when dealing with software synthesizers
580                 *    and sequencers.  {@link MidiDevice.getDeviceInfo()} and
581                 *    {@link MidiSystem.getMidiDevice(MidiDevice.Info)} are not
582                 *    inverse functions in this situation.</p>
583                 *    
584                 * @param value device receiving information from connection
585                 */
586                @Deprecated
587                public void setDestInfo(MidiDevice.Info value) {
588                        destType = specType.INFO;
589                        destInfo = value;
590                        try {
591                                MidiDevice destDevice = MidiSystem.getMidiDevice(destInfo);
592                                if (Sequencer.class.isAssignableFrom(destDevice.getClass())) {
593                                        destIsSequencer = true;
594                                }
595                                if (Synthesizer.class.isAssignableFrom(destDevice.getClass())) {
596                                        destIsSynthesizer = true;
597                                }
598                        } catch (MidiUnavailableException ex) {
599                                ex.printStackTrace();
600                        }
601                }
602                public void setDestReceiver(Receiver value) {
603                        if (debugFlag > 0) {
604                                StringBuffer text3 = new StringBuffer();
605                                text3.append("Starting Connection.setDestReceiver");
606                                if (value == null) {
607                                        text3.append(System.lineSeparator() + "Input parameter is null");
608                                }
609                                System.out.println(text3.toString());
610                        }
611                        if (value == null) {
612                                throw new NullPointerException("Input value is null");
613                        }
614                        destType = specType.RECEIVER;
615                        receiver = value;
616                        destReceiver = value;
617                }
618                public Receiver getDestReceiver() {
619                        return destReceiver;
620                }
621                public void setDestFile(File value) {
622                        destFile = value;
623
624                }
625                public File getDestFile() {
626                        return destFile;
627                }
628                public void openConnection() 
629                                throws MidiUnavailableException, IOException, InvalidMidiDataException {
630                        System.out.println("Running Connection.openConnection");
631
632                        musicStart = (new Date()).getTime();
633                        if (destType == specType.DEVICE || destType == specType.INFO) {
634                                if (!destDevice.isOpen()) {
635                                        System.out.println("     Open destination device");
636                                        destDevice.open();
637                                } else {
638                                        System.out.println("     Destination device was open");
639                                }
640                                receiver =      destDevice.getReceiver();
641                                destReceiver = receiver;
642                                if (Synthesizer.class.isAssignableFrom(destDevice.getClass())) {
643                                        Synthesizer synth = (Synthesizer) destDevice;
644                                        MidiChannel[] channels = synth.getChannels();
645                                        /* 
646                                         * Set a starting value for the instrument
647                                         * 
648                                         */
649                                        System.out.println("     Set default instrument for dest.");
650                                        for (MidiChannel item : channels) {
651                                                item.programChange(0, 45);
652                                        }
653                                }
654                        } else if (destType == specType.RECEIVER) {
655
656
657                        } else {
658                                System.out.println("Receiver not set");
659                        }
660                        if (sourceType == specType.DEVICE || sourceType == specType.INFO) {
661                                transmitter = sourceTransmitter;
662                                if (!sourceDevice.isOpen()) {
663                                        System.out.println("     Opening source device");
664                                        sourceDevice.open();
665                                } else {
666                                        System.out.println("     Source device was open");
667                                }
668
669                                transmitter = sourceDevice.getTransmitter();
670                                sourceTransmitter = transmitter;
671                        } else if (sourceType == specType.TRANSMITTER) {
672
673                        } else {
674                                System.out.println("Transmitter not set");
675                        }
676                        transmitter.setReceiver(receiver);
677                        active = true;
678                }
679                public void closeConnection() throws MidiUnavailableException {
680                        System.out.println("Calling Connection.closeConnection");
681                        if (sourceType != specType.TRANSMITTER) {
682                                transmitter.close();
683                                int xmtrCount = sourceDevice.getTransmitters().size();
684                                System.out.println(Integer.toString(xmtrCount) + " transmitters for source");
685                        }
686                        if (destType != specType.RECEIVER) {
687                                receiver.close();
688                                int rcvrCount = destDevice.getReceivers().size();
689                                System.out.println(Integer.toString(rcvrCount) + " receivers for dest.");
690                        }
691                        active = false;
692
693                }
694        }
695        /**
696         * Opens the main window for the application.
697         * 
698         * @author Bradley Ross
699         *
700         */
701        protected class Outer implements Runnable {
702                public void run() {
703                        frame = new JFrame();
704                        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
705                        // frame.addComponentListener(sizeListener);
706                        menuBar = new JMenuBar();
707                        frame.setJMenuBar(menuBar);
708                        midiMenuBar = new MidiMenuBar(menuBar);
709                        title = "Test for MIDI Application";
710                        frame.setTitle(title);
711                        // menuBar.add(fileMenu);
712                        GridBagLayout layout = new GridBagLayout();
713                        frame.setLayout(layout);
714                        GridBagConstraints upper = new GridBagConstraints();
715                        GridBagConstraints lower = new GridBagConstraints();
716                        upper.gridwidth = GridBagConstraints.REMAINDER;
717                        upper.gridheight = 1;
718                        upper.fill = GridBagConstraints.HORIZONTAL;
719                        upper.weightx = 1.0d;
720                        upper.weighty = 0.0d;
721                        lower.gridwidth = 1;
722                        lower.gridheight = 1;
723                        lower.fill = GridBagConstraints.BOTH;
724                        lower.weightx = 1.0d;
725                        lower.weighty = 1.0d;
726                        top = new Top();
727                        layout.setConstraints(top, upper);
728                        top.doLayout();
729                        frame.add(top);
730                        left = new Left();
731                        layout.setConstraints(left, lower);
732                        left.doLayout();
733                        frame.add(left);
734                        right = new Right();
735                        layout.setConstraints(right, lower);
736                        right.doLayout();
737                        frame.add(right);
738                        frame.setMinimumSize(new Dimension(800,750));
739                        //frame.pack();
740                        frame.setVisible(true);
741                }
742        }
743        /**
744         * This represents a cell in the source or destinations column.
745         * 
746         * <p>The MIDI devices represented by these cells must be
747         *    defined using a {@link MidiDevice} object.
748         * 
749         * @author Bradley Ross
750         *
751         */
752        @SuppressWarnings("serial")
753        protected abstract class Listing extends JPanel implements MouseListener {      
754                protected boolean selected = false;
755                protected JTextArea contents;
756                protected MidiDevice device = null;
757                protected MidiDevice.Info info = null;
758                // protected Listing selectedValue;
759                protected List<Listing> parentList;
760                protected boolean isSynthesizer = false;
761                protected boolean isSequencer = false;
762                /**
763                 * Specify the MIDI device using a
764                 * {@link MidiDevice object}.
765                 * 
766                 * @param device identifies MIDI device
767                 */
768                public Listing(MidiDevice device) {
769                        setMidiDevice(device);
770                        setMidiDeviceInfo(device.getDeviceInfo());
771                        createComponent();
772                }
773                /**
774                 * Specify the MIDI device using a {@link MidiDevice.Info} object.
775                 * 
776                 * <p>This is deprecated since 
777                 *    {@link MidiSystem.getMidiDevice(MidiDevice.Info)} can't 
778                 *    be relied upon to identify a single device, especially when
779                 *    dealing with synthesizers and sequencers.  The problem
780                 *    may be related to software MIDI devices.</p>
781                 *    
782                 * @param info identifies MIDI device
783                 */
784                @Deprecated
785                public Listing(MidiDevice.Info info) {
786                        setMidiDeviceInfo(info);
787                        try {
788                                device = MidiSystem.getMidiDevice(info);
789                        } catch (MidiUnavailableException ex) {
790                                ex.printStackTrace();
791                        }
792                        createComponent();
793                }
794                /**
795                 * Creates the {@link JTextArea} object that contains
796                 * the content of the cell.
797                 */
798                protected void createComponent() {
799                        StringBuffer text = new StringBuffer();
800                        text.append(info.getName() + " : " + info.getVendor() +
801                                        " : " + info.getVersion() + System.lineSeparator());
802                        text.append(info.getDescription() + System.lineSeparator());
803                        if (Synthesizer.class.isAssignableFrom(device.getClass())) {
804                                text.append("Synthesizer" + System.lineSeparator());
805                                isSynthesizer = true;
806                        }
807                        if (Sequencer.class.isAssignableFrom(device.getClass())) {
808                                text.append("Sequencer" + System.lineSeparator());
809                                isSequencer = true;
810                        }
811                        text.append("Class: " + device.getClass() + System.lineSeparator());
812                        BoxLayout layout = new BoxLayout(this, BoxLayout.X_AXIS);
813                        this.setLayout(layout);
814                        contents = new JTextArea(text.toString());
815                        contents.setEditable(false);
816                        contents.addMouseListener(this);
817                        contents.setBackground(Color.WHITE);
818                        this.add(contents);     
819                        setBorder(BorderFactory.createLineBorder(Color.CYAN, 3));
820                }
821                public MidiDevice getMidiDevice() {
822                        return device;
823                }
824                protected void setMidiDevice(MidiDevice value) {
825                        device = value;
826                }
827                public MidiDevice.Info getMidiDeviceInfo() {
828                        return info;
829                }
830                protected void setMidiDeviceInfo(MidiDevice.Info value) {
831                        info = value;
832                }
833                /**
834                 * Handles mouse clicks within the cell.
835                 */
836                public void mouseClicked(MouseEvent event) {
837                        if (selectionLocked) { return; }
838                        if (selected) {
839                                selected = false;
840                                setSelectedValue(null);
841                                contents.setBackground(Color.WHITE);
842                        } else {
843                                for (Listing item : parentList) {
844                                        item.clear();
845                                }
846                                selected = true;
847                                setSelectedValue(this);
848                                contents.setBackground(Color.LIGHT_GRAY);
849                        }
850                        if (debugFlag > 0) {
851                                System.out.println("Cell in list of devices clicked");
852                                StringBuffer diag1 = new StringBuffer();
853                                diag1.append("Selected source is ");
854                                if (selectedSource == null) {
855                                        diag1.append("null");
856                                } else {
857                                        diag1.append("not null");
858                                }
859                                diag1.append(System.lineSeparator());
860                                diag1.append("Selected destination is ");
861                                if (selectedDest == null) {
862                                        diag1.append("null");
863                                } else {
864                                        diag1.append("not null");
865                                }
866                                System.out.println(diag1.toString());
867                        }
868                }
869                public void mousePressed(MouseEvent e) { ; }
870                public void mouseReleased(MouseEvent e) { ; }
871                public void mouseEntered(MouseEvent e) { ; }
872                public void mouseExited(MouseEvent e) { ; }
873                protected void clear() {
874                        selected = false;
875                        contents.setBackground(Color.WHITE);
876                }
877                /**
878                 * Depending on the subclass, this method will set either
879                 * {@link selectedSource} or {@link selectedDest}.
880                 * @param value logical value to be used
881                 */
882                protected abstract void setSelectedValue(Listing value); 
883        }
884        /**
885         * Creates a panel that contains information on a potential destination.
886         * 
887         * @author Bradley Ross
888         *
889         */
890        @SuppressWarnings("serial")
891        protected class DestListing extends Listing {
892                public DestListing(MidiDevice.Info info) {
893                        super( info);
894                        parentList = destItems;
895                }
896                public DestListing(MidiDevice device) {
897                        super(device);
898                        parentList = destItems;
899                }
900                protected void setSelectedValue(Listing value) {
901                        selectedDest = value;
902                }
903        }       
904        /**
905         * Creates a panel that contains information on a potential
906         * source.
907         * @author Bradley Ross
908         *
909         */
910        @SuppressWarnings("serial")
911        protected class SourceListing extends Listing  {
912
913                public SourceListing(MidiDevice.Info info) {
914                        super( info);
915                        parentList = sourceItems;
916                }
917                public SourceListing(MidiDevice device) {
918                        super(device);
919                        parentList = sourceItems;
920                }
921                protected void setSelectedValue(Listing value) {
922                        selectedSource = value;
923                }
924        }
925        /**
926         * Create a widow displaying text and optionally save the text to a file.
927         * 
928         * @author Bradley Ross
929         *
930         */
931        protected class DisplayFrame {
932                JFrame displayFrame = null;
933                boolean isOpen = false;
934                JTextArea textArea = null;
935                JMenuBar menuBar = null;
936                MenuHandler menuHandler = null;
937                protected class MenuHandler implements ActionListener {
938                        /**
939                         * The constructor creates the items for the menu bar.
940                         * 
941                         * @param menuBar JMenuBar object used for the frame
942                         */
943                        public MenuHandler(JMenuBar menuBar) {
944                                JMenu fileMenu = new JMenu("File");
945                                JMenuItem saveContents = new JMenuItem("Save");
946                                saveContents.setActionCommand("save");
947                                saveContents.addActionListener(this);
948                                menuBar.add(fileMenu);
949                                fileMenu.add(saveContents);
950                        }
951
952                        /**
953                         * Actions to be taken when menu item is clicked.
954                         * 
955                         * @param ev Event created by clicking on menu item
956                         */
957                        public void actionPerformed(ActionEvent ev) {
958                                String command = ev.getActionCommand();
959                                if (command.equalsIgnoreCase("save")) {
960                                        try {
961                                                File saveFile = null;
962                                                JFileChooser chooser = new JFileChooser();
963                                                int returnVal = chooser.showSaveDialog(frame);
964                                                if (returnVal == JFileChooser.APPROVE_OPTION) {
965                                                        saveFile = chooser.getSelectedFile();
966                                                        saveFile.createNewFile();
967                                                }
968                                                if (saveFile != null) {
969                                                        String text = "Saving display to " + saveFile.getName();
970                                                        System.out.println(text);
971                                                        PrintWriter writer = new PrintWriter(saveFile, "UTF-8"); 
972                                                        textArea.write(writer);
973                                                        writer.close();
974                                                } else {
975                                                        showPopup("Problem", "No save file selected");
976                                                }
977                                        } catch (FileNotFoundException ex) {
978                                                String text = "FileNotFoundException encountered " +
979                                                                "while saving contents of window to file";
980                                                showPopup("Error", text);
981                                                ex.printStackTrace();
982                                        } catch (UnsupportedEncodingException ex) {
983                                                String text = "UnsupportedEncodingException encountered " +
984                                                                "while saving contents of window to file";
985                                                showPopup("Error", text);
986                                                ex.printStackTrace();
987                                        } catch (IOException ex) {
988                                                String text = "IOException encountered " +
989                                                                "while saving contents of window to file";
990                                                showPopup("Error", text);
991                                                ex.printStackTrace();
992                                        }
993                                }
994                        }
995                }
996                /**
997                 * Open the window that displays information.
998                 */
999                protected void open() {
1000                        if (isOpen) { return; }
1001                        isOpen = true;
1002                        displayFrame = new JFrame();
1003                        displayFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
1004                        menuBar = new JMenuBar();
1005                        menuHandler = new MenuHandler(menuBar);
1006                        displayFrame.setJMenuBar(menuBar);
1007                        displayFrame.setSize(new Dimension(600, 600));
1008                        GridBagLayout layout1 = new GridBagLayout();
1009                        GridBagConstraints c = new GridBagConstraints();
1010                        displayFrame.setLayout(layout1);
1011                        c.fill = GridBagConstraints.BOTH;
1012                        c.weightx = 1.0d;
1013                        c.weighty = 1.0d;
1014                        textArea = new JTextArea();
1015                        textArea.setColumns(80);
1016                        textArea.setRows(100);
1017                        textArea.setLineWrap(true);
1018                        textArea.setWrapStyleWord(true);
1019                        textArea.setBackground(new Color(200, 255, 255));
1020                        JScrollPane scrollPane = new JScrollPane(textArea);
1021                        layout1.setConstraints(scrollPane, c);          
1022                        displayFrame.add(scrollPane);
1023                        displayFrame.setVisible(true);
1024                }
1025                /**
1026                 * Close the window for the display.
1027                 */
1028                protected void close() {
1029                        if (!isOpen) { return; }
1030                        isOpen = false;
1031                        displayFrame.setVisible(false);
1032                        displayFrame.dispose();
1033                }
1034                public JFrame getJFrame() {
1035                        return frame;
1036                }
1037                /**
1038                 * Send a message to the display.
1039                 * @param text message to be displayed
1040                 */
1041                protected void write(String text) {
1042                        if (!isOpen) { return; }
1043                        textArea.append(text + System.lineSeparator());
1044                }
1045        }
1046        /**
1047         * Displays MIDI information in a Java Swing window.
1048         * @author Bradley Ross
1049         *
1050         */
1051        protected class LogReceiver implements Receiver {
1052                DisplayFrame display = null;
1053                boolean isOpen = false;
1054                public LogReceiver() {
1055                        display = new DisplayFrame();
1056                        display.open();
1057                        isOpen = true;
1058                }
1059
1060                /**
1061                 * Used by MIDI transmitter to send message to this receiver.
1062                 * 
1063                 * <p><a href="https://www.midi.org/specifications/item/table-1-summary-of-midi-message"
1064                 *    target="_blank">
1065                 *    https://www.midi.org/specifications/item/table-1-summary-of-midi-message</a>
1066                 *    List of MIDI codes.
1067                 *    </p>
1068                 * @param message string of bytes containing message
1069                 * @param timeStamp time generated by transmitting device
1070                 */             
1071                public void send(MidiMessage message, long timeStamp) {
1072                        StringBuffer build = new StringBuffer();
1073                        int length = message.getLength();
1074                        int status = message.getStatus();
1075                        byte[] data = message.getMessage();     
1076                        if (timeStamp > 0) {
1077                                String seconds = Long.toString(timeStamp/1000000l);
1078                                String micros = String.format("%06d", timeStamp % 1000000l);
1079                                build.append(" " + seconds + "." + micros + " ");
1080                        } else {
1081                                long time = (new Date()).getTime() - musicStart;
1082                                String seconds = String.format("%6d", time / 1000l);
1083                                String millis = String.format("%03d", time % 1000l);
1084                                build.append(" " + seconds + "." + millis + " ");
1085                        }
1086                        build.append("Status: " + String.format("%02x", status));
1087                        build.append(" Length: " + Integer.toString(length) + " -- ");
1088                        for (int i = 0; i < data.length; i++) {
1089                                build.append(String.format("%02x", data[i]));
1090                                if (i > 10) {
1091                                        build.append(" ...");
1092                                        break;
1093                                }
1094                                build.append(" ");
1095                        }
1096                        display.write(build.toString());
1097                }
1098                public void close() {
1099                        System.out.println("Calling LogReceiver.close()");
1100                }
1101
1102        }
1103        /**
1104         * Creates top panel of display.
1105         * @author Bradley Ross
1106         *
1107         */
1108        @SuppressWarnings("serial")
1109        protected class Top extends JPanel {
1110                public Top() {
1111                        BoxLayout layout = new BoxLayout(this, BoxLayout.Y_AXIS);
1112                        this.setLayout(layout);
1113                        JTextArea textBlock = new JTextArea();
1114                        textBlock.setEditable(false);
1115                        textBlock.append("The purpose of this application is to " +
1116                                        "route MIDI information between devices" +
1117                                        System.lineSeparator());
1118                        this.add(textBlock);
1119                        setBorder(BorderFactory.createLineBorder(Color.red, 3));
1120                }
1121        }
1122        /**
1123         * Creates the panel at the lower left corner of the window which
1124         * contains the list of possible sources.
1125         * @author Bradley Ross
1126         *
1127         */
1128        @SuppressWarnings("serial")
1129        protected class Left extends JPanel {
1130                protected BoxLayout layout;     
1131                protected JLabel titleField = new JLabel("Sources");
1132                public Left() {
1133                        rebuild();
1134                }
1135                protected void rebuild() {
1136                        sourceItems = new ArrayList<Listing>();
1137                        layout = new BoxLayout(this, BoxLayout.Y_AXIS);
1138                        this.setLayout(layout);
1139                        setBorder(BorderFactory.createLineBorder(Color.green, 3));              
1140                        this.add(titleField);
1141                        for (MidiDevice.Info item : sourceDevices) {
1142                                SourceListing block = new SourceListing(item);
1143                                this.add((Listing) block);
1144                                sourceItems.add((Listing) block);
1145                        }
1146                }
1147        }
1148        /**
1149         * Creates the panel at the lower right hand corner of the
1150         * window which contains information for destination devices.
1151         * 
1152         * @author Bradley Ross
1153         *
1154         */
1155        @SuppressWarnings("serial")
1156        protected class Right extends JPanel {
1157                protected BoxLayout layout;
1158                protected JLabel titleField = new JLabel("Destinations");
1159                public Right() {
1160                        rebuild();
1161                }
1162                protected void rebuild() {
1163                        destItems = new ArrayList<Listing>();
1164                        layout = new BoxLayout(this, BoxLayout.Y_AXIS);
1165                        this.setLayout(layout);
1166                        setBorder(BorderFactory.createLineBorder(Color.BLACK, 3));
1167                        this.add(titleField);
1168                        for (MidiDevice.Info item : destDevices) {
1169                                DestListing block = new DestListing(item);
1170                                this.add((Listing) block);
1171                                destItems.add((Listing) block);
1172                        }
1173                }
1174        }
1175        /**
1176         * Display a message in a popup window.
1177         * 
1178         * @param message message to appear in popup window
1179         * @throws HeadlessException
1180         */
1181        protected void showPopup(String message) throws HeadlessException {
1182                showPopup("Problem Detected", message);
1183        }
1184        /**
1185         * Display a message in a popup window.
1186         * 
1187         * @param title text to be displayed on title of popup window
1188         * @param message message to appear in popup window
1189         * @throws HeadlessException
1190         */
1191        protected void showPopup(String title, String message) throws HeadlessException{
1192                JOptionPane.showMessageDialog(frame, message, title, JOptionPane.INFORMATION_MESSAGE);
1193        }
1194        /**
1195         * Constructs the lists of potential source and destination devices.
1196         */
1197        protected void buildLists() {
1198                allDevices = MidiSystem.getMidiDeviceInfo();
1199                destDevices = new ArrayList<MidiDevice.Info>();
1200                sourceDevices = new ArrayList<MidiDevice.Info>();
1201                for (MidiDevice.Info item : allDevices) {
1202                        MidiDevice device;
1203                        try {
1204                                device = MidiSystem.getMidiDevice(item);
1205
1206                                if (device.getMaxReceivers() != 0) {
1207                                        destDevices.add(item);
1208                                }
1209                                if (device.getMaxTransmitters() != 0) {
1210                                        sourceDevices.add(item);
1211                                }
1212                                if (device.getMaxReceivers() == 0 &&
1213                                                device.getMaxTransmitters() == 0) {
1214                                        System.out.println(item.getName());
1215                                }
1216                        } catch (MidiUnavailableException e) {
1217                                e.printStackTrace();
1218                        }
1219                }
1220        }
1221        /**
1222         * Starts the Swing graphics operations.
1223         */
1224        public void run() {
1225                buildLists();
1226                /* Start Swing graphics */
1227                try {
1228                        Outer instance = new Outer();
1229                        SwingUtilities.invokeLater(instance);
1230                } catch (Exception e) {
1231                        e.printStackTrace();
1232                }
1233        }
1234        /**
1235         * This is a test of the {@link DisplayFrame} class.
1236         */
1237        protected void testDisplayFrame() {
1238                DisplayFrame display = new DisplayFrame();
1239                display.open();
1240                for (int i = 0; i <= 100; i++) {
1241                        display.write(Integer.toString(i) + "  This is a test of the system");
1242                }
1243        }
1244        /**
1245         * Convenience method for tests of log software.
1246         * @param values items used to build Midi message
1247         * @return message
1248         */
1249        protected byte[] buildMessage2(int... values) {
1250                return buildMessage(values);
1251        }
1252        /**
1253         * Convenience method for tests of log software.
1254         * @param data items used to build Midi message
1255         * @return message
1256         */
1257        protected byte[] buildMessage(int[] data) {
1258                byte[] buffer = new byte[data.length];
1259                for (int i = 0; i < data.length; i++) {
1260                        if (data[i] > 255) {
1261                                buffer[i] = (byte)( data[i] - 256);
1262                        } else {
1263                                buffer[i] = (byte) data[i];
1264                        }
1265                }
1266                return buffer;
1267        }
1268        /**
1269         * Subclass of MidiMessage used for testing.
1270         * @author Bradley Ross
1271         * 
1272         * <p>The constructor for MidiMessage is protected, which
1273         *    makes it difficult to use in test cases.</p>
1274         *
1275         */
1276        protected class TestMidiMessage extends MidiMessage {
1277                protected byte[] inter;
1278                public TestMidiMessage(byte[] data) {
1279                        super(data);
1280                        inter = data;
1281                }
1282                public TestMidiMessage clone() {
1283                        return new TestMidiMessage(inter);
1284                }
1285        }
1286        /**
1287         * This is a test of the {@link LogReceiver} class.
1288         * 
1289         * <p>Must use subclass of MidiMessage.</p>
1290         */
1291        protected void testLogReceiver() {
1292                LogReceiver receiver = new LogReceiver();
1293                byte[] message = buildMessage2(0, 255, 128, 17);
1294                TestMidiMessage testObject = new TestMidiMessage(message);
1295                receiver.send(testObject, 1000000l);
1296                for (int i = 0; i < message.length; i++) {
1297                        System.out.print(String.format("%O2x ", message[i]));
1298                }
1299                System.out.println();
1300                receiver.close();
1301        }
1302        /**
1303         * Test driver.
1304         * 
1305         * @param args first argument can optionally run test cases
1306         */
1307        public static void main(String[] args) {
1308                MidiRouter instance = new MidiRouter();
1309                if (args.length == 0) {
1310                        instance.run();
1311                        return;
1312                }
1313                if (args[0].equalsIgnoreCase("display")) {
1314                        instance.testDisplayFrame();
1315                }
1316                if (args[0].equalsIgnoreCase("log")) {
1317                        instance.testLogReceiver();
1318                }
1319        }
1320}