ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


O'Reilly Book Excerpts: Java Examples in a Nutshell, 3rd Edition

Java and Sound, Part 2

by David Flanagan

Related Reading

Java Examples in a Nutshell
By David Flanagan

Editor's note: This second installment in a two-part series of excerpts from Java Examples in a Nutshell, 3rd Edition follows on last week's (which showed how to play streaming sounds in both sampled audio and MIDI formats) with examples that show how to read a simple musical score and convert it into a MIDI sequence. Author David Flanagan also shows you how to make music by directly controlling a MidiChannel of a Synthesizer, thereby bypassing the need to play a Sequence of MIDI events through a Sequencer object.

Note: The Java Sound API is a low-level interface to sound, and the default implementation does not support compressed formats such as MP3. In these sections we'll be working with simple audio files, with types such as .wav, .aiff, and .au. Although Java Sound supports the recording of sounds, there is no recording example in this chapter: setup and configuration problems on different platforms make sound recording a topic beyond the scope of this chapter.

Synthesizing a MIDI Sequence

MIDI audio is quite different from sampled audio. Instead of recording samples of an actual audio waveform, MIDI files record a sequence of keystrokes on a real or virtual synthesizer keyboard. MIDI can't be used to represent voice data, but it is a versatile and compact format for polyphonic electronic music. Many hobbyists transcribe well-known works to MIDI or publish their own compositions as MIDI files. An Internet search will reveal many MIDI samples that you can play with any of the audio player programs we've developed previously in this chapter.

The javax.sound.midi package is useful not only for playback of predefined MIDI files but also for synthesis or playback of MIDI Sequence objects. The program in Example 17-5, PlayerPiano, takes as input a musical score (defined using a simple grammar) and creates a Sequence of MidiEvent objects. It then either plays that Sequence through a Sequencer object or saves the Sequence as a MIDI file for playback with some other sound program.

Invoke PlayerPiano with the score as a single quoted argument. You can use the -o argument to specify a filename to save the MIDI file to. Without this argument, the score will be played instead. Use -i to specify a MIDI instrument number between 0 and 127. Use -t to specify the tempo in beats (quarter notes) per minute. The notes to play are indicated using the letters A through G, with b and # for flat and sharp, respectively, and "." for rests. Notes separated by spaces are played sequentially. Notes that are not separated by spaces are played together, i.e., as a chord. The notation includes modifier characters, which make persistent changes to the way notes are played. Use + and - to increment and decrement the octave. Use > and < to increase and decrease the volume. Use "s" to toggle the damper pedal (to sustain notes) on and off. /1 indicates that the notes following it are whole notes, /2 denotes half notes, and /4, /8, /16, /32, and /64 denote quarter notes, eighth notes, and so on. For example, here the program is invoked to play a scale, followed by a trill and a chord:

java je3.sound.PlayerPiano "A B C D E F G +A s/32 D E C D E C /1-->>CEG"

One problem with the javax.sound.midi package is that although it represents the fundamental MIDI infrastructure, it does not define symbolic constants for many numbers used in the MIDI protocol. The PlayerPiano program defines some of its own constants as needed.

Example 17-5. PlayerPiano.java

package je3.sound;
import java.io.*;
import javax.sound.midi.*;

public class PlayerPiano {
    // These are some MIDI constants from the spec.  They aren't defined
    // for us in javax.sound.midi.
    public static final int DAMPER_PEDAL = 64;
    public static final int DAMPER_ON = 127;
    public static final int DAMPER_OFF = 0;
    public static final int END_OF_TRACK = 47;

    public static void main(String[  ] args)
        throws MidiUnavailableException, InvalidMidiDataException, IOException
    {
        int instrument = 0;
        int tempo = 120;
        String filename = null;

        // Parse the options
        // -i <instrument number> default 0, a piano.  Allowed values: 0-127
        // -t <beats per minute>  default tempo is 120 quarter notes per minute
        // -o <filename>          save to a midi file instead of playing
        int a = 0; 
        while(a < args.length) {
            if (args[a].equals("-i")) {
                instrument = Integer.parseInt(args[a+1]);
                a+=2;
            }
            else if (args[a].equals("-t")) {
                tempo = Integer.parseInt(args[a+1]);
                a+=2;
            }
            else if (args[a].equals("-o")) {
                filename = args[a+1];
                a += 2;
            }
            else break;
        }

        char[  ] notes = args[a].toCharArray( );

        // 16 ticks per quarter note. 
        Sequence sequence = new Sequence(Sequence.PPQ, 16);

        // Add the specified notes to the track
        addTrack(sequence, instrument, tempo, notes);

        if (filename == null) {  // no filename, so play the notes
            // Set up the Sequencer and Synthesizer objects
            Sequencer sequencer = MidiSystem.getSequencer( );
            sequencer.open( );  
            Synthesizer synthesizer = MidiSystem.getSynthesizer( );
            synthesizer.open( );
            sequencer.getTransmitter( ).setReceiver(synthesizer.getReceiver( ));

            // Specify the sequence to play, and the tempo to play it at
            sequencer.setSequence(sequence);
            sequencer.setTempoInBPM(tempo);

            // Let us know when it is done playing
            sequencer.addMetaEventListener(new MetaEventListener( ) {
                    public void meta(MetaMessage m) {
                        // A message of this type is automatically sent
                        // when we reach the end of the track
                        if (m.getType( ) == END_OF_TRACK) System.exit(0);
                    }
                });
            // And start playing now.
            sequencer.start( );
        }
        else {  // A file name was specified, so save the notes
            int[  ] allowedTypes = MidiSystem.getMidiFileTypes(sequence);
            if (allowedTypes.length == 0) {
                System.err.println("No supported MIDI file types.");
            }
            else {
                MidiSystem.write(sequence, allowedTypes[0],
                                 new File(filename));
                System.exit(0);
            }
        }
    }

    static final int[  ] offsets = {  // add these amounts to the base value
        // A   B  C  D  E  F  G
          -4, -2, 0, 1, 3, 5, 7  
    };

    /*
     * This method parses the specified char[  ] of notes into a Track.
     * The musical notation is the following:
     * A-G:   A named note; Add b for flat and # for sharp.
     * +:     Move up one octave. Persists.
     * -:     Move down one octave.  Persists.
     * /1:    Notes are whole notes.  Persists 'till changed
     * /2:    Half notes
     * /4:    Quarter notes
     * /n:    N can also be 8, 16, 32, 64.
     * s:     Toggle sustain pedal on or off (initially off)
     * 
     * >:     Louder.  Persists
     * <:     Softer.  Persists
     * .:     Rest. Length depends on current length setting
     * Space: Play the previous note or notes; notes not separated by spaces
     *        are played at the same time
     */
    public static void addTrack(Sequence s, int instrument, int tempo,
                                char[  ] notes)
        throws InvalidMidiDataException
    {
        Track track = s.createTrack( );  // Begin with a new track

        // Set the instrument on channel 0
        ShortMessage sm = new ShortMessage( );
        sm.setMessage(ShortMessage.PROGRAM_CHANGE, 0, instrument, 0);
        track.add(new MidiEvent(sm, 0));

        int n = 0; // current character in notes[  ] array
        int t = 0; // time in ticks for the composition

        // These values persist and apply to all notes 'till changed
        int notelength = 16; // default to quarter notes
        int velocity = 64;   // default to middle volume
        int basekey = 60;    // 60 is middle C. Adjusted up and down by octave
        boolean sustain = false;   // is the sustain pedal depressed?
        int numnotes = 0;    // How many notes in current chord?

        while(n < notes.length) {
            char c = notes[n++];

            if (c == '+') basekey += 12;        // increase octave
            else if (c == '-') basekey -= 12;   // decrease octave
            else if (c == '>') velocity += 16;  // increase volume;
            else if (c == '<') velocity -= 16;  // decrease volume;
            else if (c == '/') {
                char d = notes[n++];
                if (d == '2') notelength = 32;  // half note
                else if (d == '4') notelength = 16;  // quarter note
                else if (d == '8') notelength = 8;   // eighth note
                else if (d == '3' && notes[n++] == '2') notelength = 2;
                else if (d == '6' && notes[n++] == '4') notelength = 1;
                else if (d == '1') {
                    if (n < notes.length && notes[n] == '6')
                        notelength = 4;    // 1/16th note
                    else notelength = 64;  // whole note
                }
            }
            else if (c == 's') {
                sustain = !sustain;
                // Change the sustain setting for channel 0
                ShortMessage m = new ShortMessage( );
                m.setMessage(ShortMessage.CONTROL_CHANGE, 0,
                             DAMPER_PEDAL, sustain?DAMPER_ON:DAMPER_OFF);
                track.add(new MidiEvent(m, t));
            }
            else if (c >= 'A' && c <= 'G') {
                int key = basekey + offsets[c - 'A'];
                if (n < notes.length) {
                    if (notes[n] == 'b') { // flat
                        key--; 
                        n++;
                    }
                    else if (notes[n] == '#') { // sharp
                        key++;
                        n++;
                    }
                }

                addNote(track, t, notelength, key, velocity);
                numnotes++;
            }
            else if (c == ' ') {
                // Spaces separate groups of notes played at the same time.
                // But we ignore them unless they follow a note or notes.
                if (numnotes > 0) {
                    t += notelength;
                    numnotes = 0;
                }
            }
            else if (c == '.') { 
                // Rests are like spaces in that they force any previous
                // note to be output (since they are never part of chords)
                if (numnotes > 0) {
                    t += notelength;
                    numnotes = 0;
                }
                // Now add additional rest time
                t += notelength;
            }
        }
    }
        
    // A convenience method to add a note to the track on channel 0
    public static void addNote(Track track, int startTick,
                               int tickLength, int key, int velocity)
        throws InvalidMidiDataException
    {
        ShortMessage on = new ShortMessage( );
        on.setMessage(ShortMessage.NOTE_ON,  0, key, velocity);
        ShortMessage off = new ShortMessage( );
        off.setMessage(ShortMessage.NOTE_OFF, 0, key, velocity);
        track.add(new MidiEvent(on, startTick));
        track.add(new MidiEvent(off, startTick + tickLength));
    }
}

Real-Time MIDI Sounds

Example 17-5 played a Sequence of MIDI events through a Sequencer object. It is also possible to synthesize music by directly controlling one or more MidiChannel objects of a Synthesizer object. This technique requires you to turn notes on and off in real time; it is demonstrated in Example 17-6, which turns your computer keyboard into a drum machine. This program uses MIDI channel 10, which is reserved (by the General Midi specification) for percussion. When sending notes to this channel, the different key numbers don't produce a different pitch, but instead make different percussive sounds. The Drums program displays an AWT window that is used to capture java.awt.event.KeyEvent objects. Key down and key up events are translated into noteOn and noteOff calls to the MidiChannel, using the keycode of the key as the number of the percussion instrument to play. The MIDI standard defines percussion instruments for notes 35 through 81, inclusive. You can examine the VK_ constants of KeyEvent to determine which keys produce which codes, or you can just experiment by striking keys at random!

Note that the program also uses the position of the mouse to control the volume of the percussion sounds. Move the mouse to the right side of the window for louder sounds, and move it to the left for softer sounds. If you don't hear anything, be sure that it is correctly positioned in the right side of the displayed window.

You may notice a slight delay between the time you strike a key and the time you hear a sound. This latency is inevitable with software synthesizers. The getLatency( ) method of the Synthesizer object is supposed to return the worst-case latency in microseconds.

Example 17-6. Drums.java

package je3.sound;
import javax.sound.midi.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * This program the MIDI percussion channel with a Swing window.  It monitors
 * keystrokes and mouse motion in the window and uses them to create music.
 * Keycodes between 35 and 81, inclusive, generate different percussive sounds.
 * See the VK_ constants in java.awt.event.KeyEvent, or just experiment.
 * Mouse position controls volume: move the mouse to the right of the window
 * to increase the volume.
 */
public class Drums extends JFrame {
    MidiChannel channel;  // The channel we play on: 10 is for percussion
    int velocity = 64;    // Default volume is 50%

    public static void main(String[  ] args) throws MidiUnavailableException
    {
        // We don't need a Sequencer in this example, since we send MIDI
        // events directly to the Synthesizer instead.
        Synthesizer synthesizer = MidiSystem.getSynthesizer( );
        synthesizer.open( );
        JFrame frame = new Drums(synthesizer);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(50, 128);  // We use window width as volume control
        frame.setVisible(true);
    }    

    public Drums(Synthesizer synth) {
        super("Drums");

        // Channel 10 is the GeneralMidi percussion channel.  In Java code, we
        // number channels from 0 and use channel 9 instead.
        channel = synth.getChannels( )[9];

        addKeyListener(new KeyAdapter( ) {
                public void keyPressed(KeyEvent e) {
                    int key = e.getKeyCode( );
                    if (key >= 35 && key <= 81) {
                        channel.noteOn(key, velocity);
                    }
                }
                public void keyReleased(KeyEvent e) {
                    int key = e.getKeyCode( );
                    if (key >= 35 && key <= 81) channel.noteOff(key);
                }
            });

        addMouseMotionListener(new MouseMotionAdapter( ) {
                public void mouseMoved(MouseEvent e) {
                    velocity = e.getX( );
                }
            });
    }
}

Exercises

If you want to delve deeper into the Java Sound API, read the JavaSound Programmer's Guide included with the Java SDK documentation bundle. Also visit the jsresources.org web site for a useful FAQ and many more example programs. When working with MIDI programs, you may need information from the MIDI specification. Unfortunately, this spec is not available online. It can be ordered in printed form from the official midi.org web site, but you can often find the information you need by searching the Web.

These exercises will start you on your way to JavaSound fun:

David Flanagan is the author of a number of O'Reilly books, including Java in a Nutshell, Java Examples in a Nutshell, Java Foundation Classes in a Nutshell, JavaScript: The Definitive Guide, and JavaScript Pocket Reference.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.