17.6 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));
}
}