Described Auditory Content
An Introduction with Examples in Java |
Prof. David Bernstein |
Computer Science Department |
bernstdh@jmu.edu |
String
representationSequence
from a
File
or an InputStream
Sequencer
Sequencer
.Sequence
with the
Sequencer
.Sequencer
.Synthesizer
directlyimport 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(); } }
Synthesizer
object by calling the
MidiSystem.getSynthesizer()
method
Soundbank
object (e.g., by calling
the Synthesizer
object's getDefaultSoundBank()
method)
Synthesizer
object's
loadAllInstruments()
method (passing it the
Soundbank
object
MidiChannel
objects by calling the
Synthesizer
object's getChannels()
method
Receiver
object by calling the
Synthesizer
object's
getReceiver()
method
ShortMessage
object
that turns the note on
Receiver
object's
send()
method (passing it the
ShortMessage
object)
ShortMessage
object
that turns the note off
Receiver
object's
send()
method (passing it the
ShortMessage
object)
MidiChannel
object's
noteOn
method
MidiChannel
object's
noteOff
method
Design Alternatives
Content
Interface
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); }
AbstractContent
Class
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); }
Note
Class
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); } }
NoteFactory
Class
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; } }
Chord
Class
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); } } } }
Part
Class
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); } }
PartFactory
Class
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); } }
Score
Class
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(); } }
ScoreFactory
Class
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); } }