JMU
Described Auditory Content
An Introduction with Examples in Java


Prof. David Bernstein
James Madison University

Computer Science Department
bernstdh@jmu.edu


Introduction
Introduction (cont.)
A "Quick Start"
A "Quick Start" (cont.)
A "Quick Start" (cont.)
A "Quick Start" (cont.)
Playing MIDI Files
javaexamples/auditory/described/MidiPlayer.java
        import java.io.*;
import javax.sound.midi.*;


/**
 * An application that plays a MIDI file
 *
 * @author   Prof. David Bernstein, James Madison University
 * @version  1.0
 */
public class MidiPlayer
{
    /**
     * The entry point
     *
     * @param args   Command-line arguments (args[0] should contain the file)
     */
    public static void main(String[] args) throws Exception
    {
        Sequencer            sequencer;
        Sequence             seq;

        seq = MidiSystem.getSequence(new File(args[0]));

        sequencer = MidiSystem.getSequencer(); 
        sequencer.open();

        sequencer.setSequence(seq);
        sequencer.start();
    }
}
        
Presenting Described Audio
images/midi-sequence.gif
Presenting Described Audio (cont.)
  1. Obtain a Synthesizer object by calling the MidiSystem.getSynthesizer() method
  2. Get a Soundbank object (e.g., by calling the Synthesizer object's getDefaultSoundBank() method)
  3. Call the Synthesizer object's loadAllInstruments() method (passing it the Soundbank object
  4. Get the MidiChannel objects by calling the Synthesizer object's getChannels() method
Presenting Described Audio (cont.)
Encapsulating Described Auditory Content

Design Alternatives

images/DescribedAuditoryContent_duplicative.gif

images/DescribedAuditoryContent_inflexible.gif

images/DescribedAuditoryContent_flexible.gif

images/DescribedAuditoryContent_composite.gif

Encapsulating Described Auditory Content (cont.)
The Content Interface
javaexamples/auditory/described/Content.java
        package auditory.described;

import javax.sound.midi.*;


/**
 * The requirements of a piece of described auditory content 
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public interface Content
{
    /**
     * Get the type of this Note [i.e., 1 for whole notes, 
     * 2 for half notes, etc...]
     *
     * @return   The  type
     */
    public abstract int  getType();
    
    /**
     * Is this MidiSound dotted?
     *
     * @return true if dotted; false otherwise
     */
    public abstract boolean isDotted();    
    
    /**
     * Render this Note on the given MidiChannel
     *
     * @param MidiChannel   The MIDI channel to use
     */
    public abstract void render(MidiChannel channel);

    /**
     * Set whether this MidiSound is audible or note
     *
     * @param audible  true to make it audible; false otherwise
     */
    public abstract void setAudible(boolean audible);    
}
        
The AbstractContent Class
javaexamples/auditory/described/AbstractContent.java
        package auditory.described;

import javax.sound.midi.*;



/**
 * A partial encapsulation of a piece of described auditory
 * content
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public abstract class      AbstractContent
                implements Content
{
    protected boolean                 audible, dotted, playing;
    protected int                     type;

    /**
     * Default Constructor
     */
    public AbstractContent()
    {
        this(1, false);
    }


    /**
     * Explicit Value Constructor
     *
     * @param type     1 for whole notes, 2 for half notes, etc...
     * @param dotted   Whether the note is dotted
     */
    public AbstractContent(int type, boolean dotted)
    {
        this.type     = type;
        this.dotted   = dotted;
        this.audible  = false;
        this.playing  = false;        
    }

    /**
     * Get the type of this AbstractContent [i.e., 1 for whole notes, 
     * 2 for half notes, etc...]
     * (required by MidiSound)
     *
     * @return   The  type
     */
    public int getType()
    {
       return type;
    }

    /**
     * Is this AbstractContent dotted?
     * (required by MidiSound)
     *
     * @return true if dotted; false otherwise
     */
    public boolean isDotted()
    {
       return dotted;       
    }


    /**
     * Render this AbstractContent on the given MidiChannel
     * (required by MidiSound)
     *
     * @param channel   The MIDI channel to use
     */
    public void render(MidiChannel channel)
    {
       if      ( audible && !playing) 
       {
          playing = true;             
          startPlaying(channel);
       }
       else if (!audible &&  playing) 
       {
          playing = false;             
          stopPlaying(channel);
       }
    }
    
    /**
     * Set whether this AbstractContent is audible or note
     *
     * @param audible  true to make it audible; false otherwise
     */
    public void setAudible(boolean audible)
    {
       this.audible = audible; 
    }

    /** 
     * Set whether this AbstractContent is dotted
     *
     * @param dotted   Whether the note is dotted
     */
    protected void setDotted(boolean dotted)
    {
       this.dotted = dotted;       
    }
    

    /**
     * Set the type of this AbstractContent
     *
     * @param type     1 for whole notes, 2 for half notes, etc...
     */
    protected void setType(int type)
    {
       this.type = type;       
    }
    

    /**
     * Start playing this AbstractContent on the given MidiChannel
     *
     * @param channel   The MIDI channel to use
     */
    protected abstract void startPlaying(MidiChannel channel);


    /**
     * Start playing this AbstractContent on the given MidiChannel
     *
     * @param channel   The MIDI channel to use
     */
    protected abstract void stopPlaying(MidiChannel channel);

}
        
Encapsulating Described Auditory Content (cont.)
The Note Class
javaexamples/auditory/described/Note.java
        package auditory.described;

import javax.sound.midi.*;

/**
 * An encapsulation of a single note in a song
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class   Note 
       extends AbstractContent
{
    private boolean                 sharp;
    private char                    pitch;
    private int                     midiNumber;


    /**
     * Default Constructor
     *
     * Constructs a whole note at middle C
     */
    public Note()
    {
        this('C', false, 0, 1, false);
    }

    /**
     * Explicit Value Constructor
     *
     * @param pitch    The pitch ('A','B','C','D','E','F', 'G' or 'R')
     * @param sharp    true for a sharp and false for a natural
     * @param octave   The octave (relative to middle C)
     * @param type     1 for whole notes, 2 for half notes, etc...
     * @param dotted   Whether the note is dotted
     */
    public Note(char pitch,   boolean sharp, int octave, 
                int type, boolean dotted)
    {
       super(type, dotted);
       
       this.pitch = Character.toUpperCase(pitch);
       this.sharp = sharp;
       midiNumber = MidiCalculator.numberFor(pitch, sharp, octave);
    }

    /**
     * Get the MIDI number associated with this Note
     *
     * @return  The MIDI number
     */
    public int getMIDI()
    {
       return midiNumber;       
    }
    
    /**
     * Start playing this Note on the given MidiChannel
     *
     * @param channel   The MIDI channel to use
     */
    protected void startPlaying(MidiChannel channel)
    {
       channel.noteOn(midiNumber, 127);
    }


    /**
     * Start playing this Note on the given MidiChannel
     *
     * @param channel   The MIDI channel to use
     */
    protected void stopPlaying(MidiChannel channel)
    {
       channel.noteOff(midiNumber, 127);
    }

}
        
The NoteFactory Class
javaexamples/auditory/described/NoteFactory.java
        package auditory.described;

import java.util.*;



/**
 * A factory that can be used to create Note objects
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */ 
public class NoteFactory
{

    /**
     * Parse a String representation of a note and
     * construct a Note from the constituent parts.
     * In the event of a format error, this method returns null.
     *
     * For example:
     * 
     *  "C ,0,1"  is a middle C whole note
     *  "A#,1,4." is an A-sharp (one octave above middle C) dotted quarter note
     *  "D ,-1,8" is a D (one octave below middle c) eigth note
     *
     * @param   s   The String to parse
     */
    public static Note parseNote(String s)
    {
        boolean           dotted, sharp;
        char              pitch, sharpChar;
        int               duration, octave, octaveEnd;
        Note              theNote;
        String            durationString, octaveString, token;
        StringTokenizer   st;
        
        st = new StringTokenizer(s, ", ");

        try
        {
           token = st.nextToken();
        
           // Determine the pitch
           pitch = token.charAt(0);
           
           // Determine if this is a sharp or a natural
           sharp = false;        
           if (token.length() == 2)
           {
              sharpChar = token.charAt(1);
              if (sharpChar == '#') sharp = true;
           }
           
           // Detemine the octave (relative to middle C)
           // 
           octaveString = st.nextToken();
           octave       = Integer.parseInt(octaveString);
           
           // Determine the duration (which has an arbitrary length)
           dotted         = false;
           durationString = st.nextToken();
           if (durationString.endsWith(".")) dotted = true;
           duration = (int)Double.parseDouble(durationString);
           
           // Construct a new Note
           theNote = new Note(pitch, sharp, octave, duration, dotted);
        }
        catch (NoSuchElementException nsee)
        {
           theNote = null;           
        }
        
        return theNote;
    }
}
        
Encapsulating Described Auditory Content (cont.)
The Chord Class
javaexamples/auditory/described/Chord.java
        package auditory.described;

import java.util.*;
import javax.sound.midi.*;


/**
 * A Chord is a collection of Note objects that are
 * rendered simultaneously
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class      Chord
       extends    AbstractContent
{
    private ArrayList<Note>       notes;
    

    /**
     * Default Constructor
     */
    public Chord()
    {
       this(1, false);
    }
    

    /**
     * Explicit Value Constructor
     *
     * @param type     1 for whole notes, 2 for half notes, etc...
     * @param dotted   Whether the note is dotted
     */
    public Chord(int type, boolean dotted)
    {
       super(type, dotted);
       
       notes = new ArrayList<Note>();       
    }

    /**
     * Add a Note to this Chord
     *
     * @param note  The Note to add
     */
    public void addNote(Note note)
    {
       notes.add(note);       
    }

    /**
     * Start playing this Chord on the given MidiChannel
     *
     * @param channel   The MIDI channel to use
     */
    protected void startPlaying(MidiChannel channel)
    {
       Iterator<Note>     i;
       Note               note;
       

       i = notes.iterator();
       while (i.hasNext())
       {
          note = i.next();
          if (note != null)
          {
             note.setType(type);
             note.setDotted(dotted);
             note.startPlaying(channel);          
          }
       }
    }

    /**
     * Start playing this Chord on the given MidiChannel
     *
     * @param channel   The MIDI channel to use
     */
    protected void stopPlaying(MidiChannel channel)
    {
       Iterator<Note>     i;
       Note               note;
       

       i = notes.iterator();
       while (i.hasNext())
       {          
          note = i.next();          
          if (note != null)
          {
             note.stopPlaying(channel);          
          }
       }
    }
}

        
Encapsulating Described Audio (cont.)
A Part Class
javaexamples/auditory/described/Part.java
        package auditory.described;

import java.util.*;
import javax.sound.midi.*;

import event.*;

/**
 * A Part (i.e., a sequence of Content objects) in a Score
 *
 * Note: This class is not thread safe.  Hence, Part objects should
 * be modified as required before they are rendered.
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class Part implements MetronomeListener
{
    private ArrayList<Content>     sounds;
    private double                 timeSignatureDenominator, 
                                   timeSignatureNumerator;
    private int                    millisPerMeasure, stopTime;

    private Content                currentContent, 
                                   previousContent;
    private Iterator<Content>      iterator;    
    private Metronome              metronome; // Not Owned    
    
    /**
     * Default Constructor
     */
    public Part()
    {
       sounds       = new ArrayList<Content>();
    }
    /**
     * Add a Content to this Part
     *
     * @param ms   The Content to add
     */
    public void add(Content c)
    {
       if (c != null) sounds.add(c);
    }

    /**
     * Handle a Metronome tick
     * (required by MetronomeListener)
     *
     * @param millis  The number of milliseconds since the start
     */
    public void handleTick(int millis)
    {
       double   beats, millisPerBeat, type;    

       if (iterator == null) 
          throw(new IllegalStateException("No upbeat()"));


       if (millis >= stopTime)
       {
          if (currentContent != null) 
             currentContent.setAudible(false);

          if (iterator.hasNext())
          {
             previousContent = currentContent;             
             currentContent  = iterator.next();

             // This calculation needn't really be done each 
             // iteration.  It could be done once.
             millisPerBeat=1.0/(double)timeSignatureNumerator*
                           millisPerMeasure;

             
             beats        =(1.0/(double)currentContent.getType())* 
                           (double)timeSignatureDenominator;
 
             if (currentContent.isDotted()) beats = beats * 1.5;
            
             stopTime = millis + (int)(beats * millisPerBeat);

             currentContent.setAudible(true);             
          }
          else
          {
             metronome.removeListener(this);             
          }          
       }
    }

    /**
     * Render the current note in this Part
     *
     * @param channel  The MidiChannel to use
     */
    public void render(MidiChannel channel)
    {
       if (previousContent != null) 
          previousContent.render(channel);       

       if (currentContent  != null) 
          currentContent.render(channel);       
    }

    /**
     * Set the tempo for this Part
     *
     * @param millisPerMeasure  The tempo (in milliseconds per measure)
     */
    public void setTempo(int millisPerMeasure)
    {
       this.millisPerMeasure = millisPerMeasure;       
    }
    

    /**
     * Set the time signature for this Part
     *
     * @param numerator    The numerator of the time signature
     * @param denominator  The denominator of the time signature
     */
    public void setTimeSignature(int numerator, int denominator)
    {
       this.timeSignatureNumerator   = numerator;
       this.timeSignatureDenominator = denominator;
    }

    /**
     * Alert this Part to the fact that is should
     * make itself ready to be played
     *
     * @param metronome   The Metronome it will listen to
     */
    public void upbeat(Metronome metronome)
    {
       this.metronome   = metronome;

       iterator         = sounds.iterator();
       currentContent   = null;
       previousContent  = null;
       stopTime         = -1;       

       metronome.addListener(this);       
    }

}
        
A PartFactory Class
javaexamples/auditory/described/PartFactory.java
        package auditory.described;

import java.io.*;

/**
 * A factory that creates Part objects
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class PartFactory
{
    
    /**
     * Create a Part object from a BufferedReader that
     * is reading a stream containing a string representation
     *
     * @param in     The BufferedReader
     */
    public static Part createPart(BufferedReader in) 
                       throws IOException
    {
       Note                   note;
       Part                   part;       
       String                 line;
       

       part = new Part();
       
       while ((line = in.readLine()) != null && 
              (!line.equals("X"))              )
       {
          if (!line.equals(""))
          {
             note = NoteFactory.parseNote(line);
             if (note != null) part.add(note);          
          }
       }
       
       return part;       
    }
    

    /**
     * Create a Part object from a file containing a 
     * String representation
     *
     * @param filename   The name of the file
     */
    public static Part createPart(String filename) 
                       throws IOException
    {
       BufferedReader         in;
       

       in = new BufferedReader(new FileReader(filename));

       return createPart(in);
    }
    
}
        
Encapsulating Described Audio (cont.)
A Score Class
javaexamples/auditory/described/Score.java
        package auditory.described;

import java.awt.event.*;
import java.util.Enumeration;
import java.util.Hashtable;
import javax.sound.midi.*;

import event.*;


/**
 * An encapsulation of a music score
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class Score implements MetronomeListener
{
    private Hashtable<Part, String>            parts;
    private int                                timeSignatureDenominator, 
                                               timeSignatureNumerator,
                                               millisPerMeasure;
    private Hashtable<String, Instrument>      instruments;
    private Hashtable<Part, MidiChannel>       channelTable;
    private Metronome                          metronome;    
    private MidiChannel[]                      channels;
    

    /**
     * Default Constructor
     */
    public Score() throws Exception 
    {
       Instrument[]    loaded;
       Soundbank       defaultSB;
       Synthesizer     synthesizer;


       metronome    = new Metronome(10);
       metronome.addListener(this);       
              
       parts        = new Hashtable<Part, String>();       
       instruments  = new Hashtable<String, Instrument>();
       channelTable = new Hashtable<Part, MidiChannel>();
       
       synthesizer = MidiSystem.getSynthesizer();
       synthesizer.open();
       defaultSB = synthesizer.getDefaultSoundbank();

       synthesizer.loadAllInstruments(defaultSB);

       channels    = synthesizer.getChannels();

       loaded = synthesizer.getLoadedInstruments();
       for (int i=0; i<loaded.length; i++)
       {
          instruments.put(loaded[i].getName().trim(), loaded[i]);
          System.out.println("|"+loaded[i].getName().trim()+"|");
          
       }
       
    }

    /**
     * Handle a Metronome tick
     * (required by MetronomeListener)
     *
     * This method tells each Part to render itself.  In addition, if
     * there are no Part objects listening to the Metronome, this
     * method stops the Metronome
     *
     * @param millis   The number of milliseconds since the Metronome started
     */
    public void handleTick(int millis)
    {
       Enumeration<Part>           e;
       MidiChannel                 channel;
       Part                        part;
       

       e = parts.keys();
       while (e.hasMoreElements())
       {
          part       = e.nextElement();
          channel    = channelTable.get(part);
          part.render(channel);          
       }
       

       if (metronome.getNumberOfListeners() == 1)
       {
          metronome.stop();          
       }
    }
    
    /**
     * Add a Part to this Score
     *
     * @param part   The Part to add
     */
    public void addPart(Part part, String instrument)
    {
       parts.put(part, instrument);
    }

    /**
     * Remove a Part from this Score
     *
     * @param part   The Part to remove
     */
    public void removePart(Part part)
    {
       parts.remove(part);
    }    


    /**
     * Set the tempo for this Part
     *
     * @param millisPerMeasure  The tempo (in milliseconds per measure)
     */
    public void setTempo(int millisPerMeasure)
    {
       this.millisPerMeasure = millisPerMeasure;       
    }
    

    /**
     * Set the time signature for this Part
     *
     * @param numerator    The numerator of the time signature
     * @param denominator  The denominator of the time signature
     */
    public void setTimeSignature(int numerator, int denominator)
    {
       this.timeSignatureNumerator   = numerator;
       this.timeSignatureDenominator = denominator;
    }

    /**
     * Play this  Score
     */
    public void play()
    {
       Enumeration<Part>           e;
       Instrument                  instrument;       
       int                         i;       
       MidiChannel                 channel;       
       String                      name;       
       Patch                       patch;       
       Part                        part;
       

       e = parts.keys();
       i = 0;       

       while (e.hasMoreElements())
       {
          part       = e.nextElement();
          name       = parts.get(part);          
          instrument = instruments.get(name);
          System.out.println("|"+name+"|"+"\t"+instrument);
          
          
          
          // Have the channel use the appropriate instrument
          if (instrument == null)
          {
             channels[i].programChange(0, 0);
          }
          else
          {
             patch = instrument.getPatch();
             channels[i].programChange(patch.getBank(), 
                                       patch.getProgram());
          }
          channelTable.put(part, channels[i]);             
          
          
          // Get the Part ready
          part.upbeat(metronome);
          part.setTimeSignature(timeSignatureNumerator, 
                                timeSignatureDenominator);
          part.setTempo(millisPerMeasure);
          

          i++;          
       }
              
       
       // Start the metronome
       metronome.start();
    }

}

        
A ScoreFactory Class
javaexamples/auditory/described/ScoreFactory.java
        package auditory.described;

import java.io.*;
import java.util.StringTokenizer;
import javax.sound.midi.MidiUnavailableException;

/**
 * A factory that creates Score objects
 */
public class ScoreFactory
{
    
    /**
     * Create a Score object from a BufferedReader that
     * is reading a stream containing a string representation
     *
     * @param in     The BufferedReader
     */
    public static Score createScore(BufferedReader in) 
                        throws Exception
    {
       int                    denominator, numerator, tempo;       
       Note                   note;
       Part                   part;       
       Score                  score;       
       String                 line, voice;
       StringTokenizer        st;
       
       
       // Read the time signature and tempo
       line        = in.readLine();
       st          = new StringTokenizer(line, ",/");
       numerator   = Integer.parseInt(st.nextToken());
       denominator = Integer.parseInt(st.nextToken());
       tempo       = Integer.parseInt(st.nextToken());

       score = new Score();
       
       score.setTimeSignature(numerator, denominator);
       score.setTempo(tempo);       

       while ((voice = in.readLine()) != null)
       {
          part = PartFactory.createPart(in);
          score.addPart(part, voice);
       }
       
       return score;       
    }
    


    
    /**
     * Create a Score object from a file containing a string representation
     *
     * @param filename   The name of the file
     */
    public static Score createScore(String filename) 
                        throws Exception
    {
       BufferedReader         in;
       

       in = new BufferedReader(new FileReader(filename));

       return createScore(in);
    }
    
}