Play musical instruments on a piano-like keyboard Music is an important part of our lives, including the music in the computer games we play. Perhaps you have wanted to create a Java-based music-editor program to help you compose pieces of music for your own computer games. This program would present a GUI with some means to choose an instrument (a piano, for example), a piano-like keyboard for playing notes (via that instrument), some way to record and playback compositions, and so on.This Java Fun and Games installment presents an applet that gets you started on that program. The applet, which I call Javano (for Java-based piano), reveals a piano-like keyboard and a means to choose an instrument —but nothing else. I’ll leave it up to your creativity to add extra capabilities to this applet.The figure below reveals Javano’s GUI. Press the keys on Javano’s keyboard to play the bagpipes or another musical instrument. Click on thumbnail to view full-sized image.The GUI is created from the contents of Javano.java. That source file is described in Listing 1.Listing 1. Javano.java // Javano.javaimport java.awt.*; import java.awt.event.*;import javax.swing.*;public class Javano extends JApplet { Keyboard keyboard; JComboBox instruments; public void init () { JPanel panel = new JPanel (); panel.add (new JLabel ("Instruments:")); instruments = new JComboBox (); panel.add (instruments); getContentPane ().add (panel, BorderLayout.NORTH); keyboard = new Keyboard (); getContentPane ().add (keyboard, BorderLayout.SOUTH); } public void start () { keyboard.turnOn (); DefaultComboBoxModel dcbm; dcbm = new DefaultComboBoxModel (keyboard.getInstruments ()); instruments.setModel (dcbm); ActionListener al; al = new ActionListener () { public void actionPerformed (ActionEvent e) { JComboBox cb = (JComboBox) e.getSource (); keyboard.chooseInstrument (cb.getSelectedIndex ()); } }; instruments.addActionListener (al); } public void stop () { keyboard.turnOff (); } } Although I haven’t commented Javano.java, the source code should not be too hard to follow. For starters, Javano is organized around a custom Swing-based keyboard component represented by the Keyboard class. The Keyboard presents a no-argument constructor for creating this component. That constructor initializes the keyboard’s keys, including their locations. Furthermore, it attaches a mouse listener to this component—to respond to mouse-press, mouse-release, and mouse-exit events.The Keyboard also presents four methods:public void turnOn() turns on the keyboard. Turning on the keyboard causes the default MIDI (Musical Instrument Digital Interface) synthesizer to be acquired, instruments to be loaded into that synthesizer, and a channel to be established for receiving note messages that result in music. Until you call this method, you will hear no music when you press keys, although you will get visual feedback of those key presses. The best place to call turnOn() is the applet’s start() method.public String [] getInstruments() returns an array of strings that describe various instruments; you will probably use that information to populate a combo box or a list. Until you invoke turnOn(), this method returns null.public boolean chooseInstrument (int instrumentID) switches from the current instrument to the instrument associated with the integer represented by instrumentID. The array index used to select a string from the array returned by getInstruments() can be passed to chooseInstrument () to select the instrument described by that String object. Until you invoke turnOn(), this method returns false.public void turnOff() turns off the keyboard. You must turn off the keyboard before leaving the applet’s Webpage. Failure to do so could result in various problems due to resources being tied up as a result of the turnOn() method call. The best place to call that method is the applet’s stop() method.The init() method constructs the GUI. Because we cannot invoke getInstruments() until we have called turnOn(), and because we call turnOn() in the start() method, we populate the combo box in start(). Listing 2 describes the implementation of the Keyboard class’s constructor and its four methods.Listing 2. Keyboard.java // Keyboard.java/* * Portions of Keyboard's source code were excerpted from Sun's MidiSynth.java * source file. I've included Sun's original copyright and license, to be fair * to Sun. * * Copyright (c) 1999 Sun Microsystems, Inc. All Rights Reserved. * * Sun grants you ("Licensee") a non-exclusive, royalty free, license to use, * modify and redistribute this software in source and binary code form, * provided that i) this copyright notice and license appear on all copies of * the software; and ii) Licensee does not utilize the software in a manner * which is disparaging to Sun. * * This software is provided "AS IS," without a warranty of any kind. ALL * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT * BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, * MODIFYING OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL * SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, * HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF * THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF * THE POSSIBILITY OF SUCH DAMAGES. * * This software is not designed or intended for use in on-line control of * aircraft, air traffic, aircraft navigation or aircraft communications; or * in the design, construction, operation or maintenance of any nuclear * facility. Licensee represents and warrants that it will not use or * redistribute the Software for such purposes. */import java.awt.*; import java.awt.event.*; import java.util.Vector;import javax.sound.midi.*; import javax.swing.*;/* This class creates a keyboard component that knows how to play a specific instrument. */public class Keyboard extends JPanel { public final static Color KEY_BLUE = new Color (204, 204, 255); public final static int KEY_HEIGHT = 80, KEY_WIDTH = 16; private Key theKey; private MidiChannel channel; private Synthesizer synthesizer; private Vector blackKeys = new Vector (); private Vector keys = new Vector (); private Vector whiteKeys = new Vector (); public Keyboard () { setLayout (new BorderLayout ()); setPreferredSize (new Dimension (42*KEY_WIDTH+1, KEY_HEIGHT+1)); int transpose = 24; int [] whiteIDs = { 0, 2, 4, 5, 7, 9, 11 }; for (int i = 0, x = 0; i < 6; i++) { for (int j = 0; j < 7; j++, x += KEY_WIDTH) { int keyNum = i * 12 + whiteIDs [j] + transpose; whiteKeys.add (new Key (x, 0, KEY_WIDTH, KEY_HEIGHT, keyNum)); } } for (int i = 0, x = 0; i < 6; i++, x += KEY_WIDTH) { int keyNum = i * 12 + transpose; blackKeys.add (new Key ((x += KEY_WIDTH)-4, 0, KEY_WIDTH/2, KEY_HEIGHT/2, keyNum+1)); blackKeys.add (new Key ((x += KEY_WIDTH)-4, 0, KEY_WIDTH/2, KEY_HEIGHT/2, keyNum+3)); x += KEY_WIDTH; blackKeys.add (new Key ((x += KEY_WIDTH)-4, 0, KEY_WIDTH/2, KEY_HEIGHT/2, keyNum+6)); blackKeys.add (new Key ((x += KEY_WIDTH)-4, 0, KEY_WIDTH/2, KEY_HEIGHT/2, keyNum+8)); blackKeys.add (new Key ((x += KEY_WIDTH)-4, 0, KEY_WIDTH/2, KEY_HEIGHT/2, keyNum+10)); } keys.addAll (blackKeys); keys.addAll (whiteKeys); addMouseListener (new MouseAdapter () { public void mousePressed (MouseEvent e) { // Identify the key that was pressed. A null // value indicates something other than a key // was pressed. theKey = getKey (e.getPoint ()); // If a key was pressed ... if (theKey != null) { // Tell key to start playing note. theKey.on (); // Update key's visual appearance. repaint (); } } public void mouseReleased (MouseEvent e) { if (theKey != null) { // Tell key to stop playing note. theKey.off (); // Update key's visual appearance. repaint (); } } public void mouseExited (MouseEvent e) { // This method is called if the mouse is moved // off the keyboard component. If a key was // pressed, we release that key. if (theKey != null) { // Tell key to stop playing note. theKey.off (); // Update key's visual appearance. repaint (); // The following assignment is needed so // that we don't execute the code within // mouseReleased()'s if statement should we // release a key after exiting the keyboard // component. There is no need to tell the // key to stop playing a note after we have // already told it to do so. Furthermore, // we prevent an unnecessary repaint. theKey = null; } } public Key getKey (Point point) { // Identify the key that was clicked. for (int i = 0; i < keys.size (); i++) { if (((Key) keys.get (i)).contains (point)) return (Key) keys.get (i); } return null; } }); } public boolean chooseInstrument (int instrumentID) { if (channel == null) return false; // Select new instrument based on ID. channel.programChange (instrumentID); return true; } public String [] getInstruments () { if (synthesizer == null) return null; Instrument [] instruments = synthesizer.getLoadedInstruments (); String [] ins = new String [instruments.length]; for (int i = 0; i < instruments.length; i++) ins [i] = instruments [i].toString (); return ins; } public void paint (Graphics g) { Graphics2D g2 = (Graphics2D) g; Dimension d = getSize (); g2.setBackground (getBackground ()); g2.clearRect (0, 0, d.width, d.height); g2.setColor (Color.white); g2.fillRect (0, 0, 42*KEY_WIDTH, KEY_HEIGHT); for (int i = 0; i < whiteKeys.size (); i++) { Key key = (Key) whiteKeys.get (i); if (key.isNoteOn ()) { g2.setColor (KEY_BLUE); g2.fill (key); } g2.setColor (Color.black); g2.draw (key); } for (int i = 0; i < blackKeys.size (); i++) { Key key = (Key) blackKeys.get (i); if (key.isNoteOn ()) { g2.setColor (KEY_BLUE); g2.fill (key); g2.setColor (Color.black); g2.draw (key); } else { g2.setColor (Color.black); g2.fill (key); } } } public void turnOff () { if (synthesizer == null) return; // Attempt to unload all instruments. synthesizer.unloadAllInstruments (synthesizer.getDefaultSoundbank ()); // Close the synthesizer so that it can release any system resources // previously acquired during the open() call. synthesizer.close (); synthesizer = null; } public boolean turnOn () { try { if (synthesizer == null) { // Obtain the default synthesizer. if ((synthesizer = MidiSystem.getSynthesizer ()) == null) return false; // Open the synthesizer so that it can acquire any system // resources and become operational. synthesizer.open (); } } catch (Exception e) { e.printStackTrace(); return false; } // Attempt to load all instruments. synthesizer.loadAllInstruments (synthesizer.getDefaultSoundbank ()); // Obtain the set of MIDI channels controlled by the synthesizer. MidiChannel [] midiChannels = synthesizer.getChannels (); // There must be at least one channel. Furthermore, we assume that the // first channel is used by the synthesizer. If you run into a problem, // use the index (other than 0) of the first non-null midiChannels // entry. if (midiChannels.length == 0 || midiChannels [0] == null) { synthesizer.close (); synthesizer = null; return false; } // Identify the channel to which note messages are sent. channel = midiChannels [0]; return true; } /* This inner class describes an instrument key that knows how to start sounding and stop sounding its note. Furthermore, each key knows its size and location. */ class Key extends Rectangle { final static int ON = 0, OFF = 1, VELOCITY = 64; int kNum, noteState = OFF; public Key (int x, int y, int width, int height, int num) { super (x, y, width, height); kNum = num; } public boolean isNoteOn () { return noteState == ON; } public void off () { setNoteState (OFF); //Keyboard.this.repaint (); // Send the key number to the channel so the note stops sounding. // Also send a keyup VELOCITY, which might affect how quickly the // note decays. if (channel != null) channel.noteOff (kNum, VELOCITY); } public void on () { setNoteState (ON); //Keyboard.this.repaint (); // Send the key number (0 - 127, where 60 indicates Middle C) to the // channel so the note starts to sound. Also send a keydown VELOCITY, // which indicates the speed at which a key was pressed (the loudness // or volume of the note). if (channel != null) channel.noteOn (kNum, VELOCITY); } public void setNoteState (int state) { noteState = state; } } } Listing 2 excerpts a source file called MidiSynth.java, part of a demo that Sun created several years ago. To save time developing the keyboard component’s internal organization and painting logic, I excerpted some code from MidiSynth.java and made modifications. Listing 2 depends on MIDI to play music. If you have never experienced working with MIDI from a Java perspective, I encourage you to read the MIDI chapters in the Java Sound documentation that accompanies the Java SDK.After compiling Listings 1 and 2, you will want to run this applet. Before you can do that, however, you must describe the applet to appletviewer via HTML. Listing 3 provides the needed HTML.Listing 3. Javano.html <applet code=Javano.class width=675 height=125> </applet> After you’ve played with Javano, you will probably become aware of its various limitations. There are many things you can do to improve this applet. Here are three suggestions:Add features to save and play back compositionsAllow multiple keys to be pressed simultaneouslyReplace the VELOCITY constant with a GUI feature for choosing the volumeReviewA Java-based music editor can help you compose music for your computer games. Javano gets you started by offering a piano-like keyboard and the means for choosing an instrument. After adding features to save and play back your compositions, press multiple keys simultaneously, and choose an appropriate volume (from the GUI), you will be well on your way to developing a music editor for your computer gaming needs.Jeff Friesen is a freelance software developer and educator specializing in C, C++, and Java technology. Software DevelopmentComputers and PeripheralsTechnology IndustryJava