MIDI Support in Common Music

Heinrich Taube

Zentrum fuer Kunst und Medientechnologie
Ritterstr. 42
7500 Karlsruhe 1
Germany
		
				Mail:  hkt@zkm.de
				Fax: +49 721 9340 39 
				Vox: +49 721 9340 300

___________________________________________________________

Introduction

Common Music supports MIDI real time and MIDI file generation through the midi syntax.  MIDI real time is implemented at the lowest level by a MIDI message facility, and provides the ability to directly read and write MIDI messages to a MIDI driver.  An object oriented representation of MIDI objects is built on top of the basic messaging facility and provides the ability to use midi-notes, algorithms, generators, and item streams when working with midi data.

1.1 MIDI Real Time Messages

To support MIDI real time, a low-level representation of a MIDI event, called a "message", is implemented.  MIDI messages are really just formatted 28-bit fixnums containing up to three bytes of data.  All of the MIDI messages defined in the MIDI specification, (including system exclusive and meta event messages) are supported through a notion of MIDI message type and subtype.

1.2 MIDI Message Types

Each MIDI message type is implemented by a type name (either symbol or keyword), a constructor function, a predicate, and one or more message data field accessors, if appropriate.  For example, MIDI note on messages are supported through the note-on message type:

Type name			note-on, :note-on
Message constructor		make-note-on
Message predicate		note-on-p
Data field accessors	 	note-on-channel, note-on-key, note-on-velocity
			
The data field accessors are implemented as functions, not macros, so they are suitable for use in mapping.  For example:

<cl> (mapcar #'note-on-key message-list)

would return a list of all the key numbers in a list of note-on messages.

All data field accessors are setf-able, that is, they may be used to both set and retrieve the contents of message data fields:

<cl> (setf x (make-note-on 0 60 64))
59784256

<cl> (note-on-key x)
60

<cl> (incf (note-on-key x) 10)
70

<cl> (midi-print-message x)
#<NoteOn: 0, 70, 64>

Some message types, such as MIDI file "meta events" are implemented as composite messages.  Constructors for composite message types return multiple values: the message created and a (possibly empty) list of message data.  Each element of the message data list is itself a message of type midi-data.  A midi-data message contains from one to three bytes of raw MIDI data.

The following is a list of supported MIDI message types along with their constructors, predicates and data accessors:
	
channel-message							[basic message type]
	make-channel-message status channel data1 &optional data2
	channel-message-p message
	channel-message-status message
	channel-message-channel message
	channel-message-data1 message
	channel-message-data2 message

note-on									[channel message]
	make-note-on channel key velocity
	note-on-p message
	note-on-channel message
	note-on-key message
	note-on-velocity message

note-off									[channel message]
	make-note-off channel key velocity
	note-off-p message		
	note-off-channel message
	note-off-key message
	note-off-velocity message

key-pressure								[channel message]
	make-key-pressure channel key velocity
	key-pressure-p message
	key-pressure-channel message
	key-pressure-key message
	key-pressure-pressure message

control-change								[channel message]
	make-control-change channel controller value
	control-change-p message
	control-change-channel message
	control-change-control message
	control-change-change message

program-change							[channel message]
	make-program-change channel program
	program-change-p message
	program-change-channel message
	program-change-program message

channel-pressure							[channel message]
	make-channel-pressure channel pressure
	channel-pressure-p message
	channel-pressure-channel message
	channel-pressure-pressure message

pitch-bend								[channel message]
	make-pitch-bend channel lsb msb
	pitch-bend-p message
	pitch-bend-channel message
	pitch-bend-lsb message
	pitch-bend-msb message

channel-mode								[channel message]
	make-channel-mode channel control change
	channel-mode-p message
	channel-mode-channel message
	channel-mode-control message
	channel-mode-change message

system-message							[basic message type]
	make-system-message status &optional data1 data2
	system-message-p message
	system-message-status message
	system-message-data1 message
	system-message-data2 message

song-position								[system message]
	make-song-position lsb msb
	song-position-p message
	song-position-lsb message
	song-position-msb message

song-select								[system message]
	make-song-select song
	song-select-p message
	song-select-song message

tune-request								[system message]
	make-tune-request ()
	tune-request-p message

sysex-message								[system message]

The :sysex-message type is implemented across multiple messages.  The constructor function make-sysex-message returns multiple values:  a message (of type :sysex-message) and a (possibly empty) list of message data. Each element in the data list is itself a message of type midi-data.  Each data element in a sysex messages holds one byte of MIDI data.

	make-sysex-message &rest data
	sysex-message-p message

meta-message								[basic message type]

The :meta-message type and all its subtypes are implemented across multiple messages.   Every meta-message constructor function returns multiple values:  a meta message and a (possibly empty) list of  meta message data. Each element in the data list is itself a message of type 	midi-data.  Each data element of a meta message holds one byte of MIDI data.
	make-meta-message type
	meta-message-p message
	meta-message-type message

time-signature									[meta message]
	make-time-signature n d &optional (clock 24) (32nds 8)
	time-signature-p message

tempo-change									[meta message]
	make-tempo-change usecs
	tempo-change-p message

eot										[meta message]
	make-eot ()
	eot-p message

midi-data									[basic message type]
	make-midi-data data1 &optional data2 data3
	midi-data-p message
	midi-data-size message
	midi-data-data1 message
	midi-data-data2 message
	midi-data-data3 message

1.3 MIDI Message Utilities

A number of general purpose functions are provided for working with MIDI messages and MIDI files:

make-midi-message  type &rest args 					[function]

This function provides the most general means for creating MIDI messages.  type must be one of the defined MIDI message types,  specified as either a keyword or a symbol. args is zero or more data arguments appropriate for the type of message.

Example:

;; The following two calls are equivalent:

<cl> (make-note-on 0 60 64)
126893120 

<cl> (make-midi-message 'note-on 0 60 64)
126893120 

midi-print-message message &optional time 				[function]
	                            &key stream time-format  message-data

Prints message on stream, which defaults to the standard output.  If time is specified, it is printed first according to the format string time-format.  midi-print-message returns message as its value.

Examples:

<cl> (midi-print-message (make-note-on 0 60 127))
#<NoteOn: 0, 60, 127>
126893183	

<cl> (multiple-value-bind (msg data) (make-time-signature 4 4)
	     (midi-print-message msg nil :message-data data))
#<TimeSig: 4/4 clocks=24 32nds=8>
251615232 

1.4 Using MIDI Real Time 

To use MIDI messages in real time, a MIDI driver must be opened on some port.  MIDI messages are then directly read from and written to the MIDI driver.   Here is
a very brief MIDI session:

<cl> (midi-open :port :A)
:A

<cl> (midi-write-message (make-note-on 0 60 127))
T

<cl> (midi-write-message (make-note-off 0 60 127))
T

<cl> (midi-close)
T

1.4.1 Opening and Closing the MIDI port

Before any MIDI messages are sent or received, MIDI must first be opened on some port.  This port should then be closed after it is no longer needed.  Opening and closing MIDI ports are implemented by the following functions:

midi-open &key port								[function]

Open MIDI driver on the specified port. The port may designated by number: 1 or 2, or by symbol: a or b.  (The keyword forms of the symbols may also be used.) midi-open returns the port name  if it was able to open the port, otherwise nil.

Example:

<cl> (midi-open :port :b)
:B

midi-close ()									[function]

Close the current MIDI port, if one is open.  midi-close returns t if it was able to close the port, otherwise nil.

midi-open-p ()									[function]

Returns the port name if MIDI port is open, otherwise nil.

with-midi-open ((&key port) &body body)					[macro]

Forms in the body of with-midi-open are evaluated with MIDI open on the port specified by port.  If port is not specified its value defaults to the global variable *midi-port*.  After the forms inside the body have been evaluated,  the MIDI port is closed if it was not previously open, otherwise it is  left open.

Example:

(defun listen ()
  (with-midi-open (:port :b) 
	 (loop doing (midi-read-messages #'midi-write-message))))

*midi-port*									[variable]

The value of *midi-port* provides a default port value for MIDI input and output.   Names for the first serial port are:  A, :A, 1 and for the second:  B,:B, 2.

1.4.2  Writing and reading MIDI messages

Once a port is opened, a MIDI message may be sent to the driver:

midi-write-message message &optional (time 0) message-data		[function]

Sends MIDI message at optionally specified quanta time time.  If time is not specifed the messsage is sent immediately (time 0).

Examples:

<cl> (midi-write-message (make-note-on 0 60 127))
<cl> (midi-write-message (make-note-off 0 60 127) 1000)
	  
;;; here is a slightly more adventurous example (for my
;;; spiffy Yamaha TG33...) that plays 100 notes and chooses
;;; some sort of drum sound every so often.

(let ((on (make-note-on 0 0 127))
	    (off (make-note-off 0 0 127))
	    (cng (make-program-change 0 62))      
	    (time (midi-get-time)))
	(loop for i below 100 
	      do
	  (when (= 1 (random 10)) 
	    (setf (program-change-program cng) (+ 59 (random 6)))
	    (midi-write-message cng))
	  (setf (note-on-key on) (+ 60 (random 12)))
	  (setf (note-off-key off) (note-on-key on))
	  (incf time (+ 50 (random 450)))
	  (midi-write-message on time)
	  (midi-write-message off (+ time 600)))) 

All current MIDI messages (messages that have been received by the driver from some MIDI device, such as an external keyboard) may be read by the function midi-read-messages:

midi-read-messages &optional function					[function]
	
Reads all MIDI messages currently available (received).  If function is supplied it is invoked on each message read. The function must accept two arguments, the current message and its quanta time.

Examples:

;; read and print all currently pending messages.
(midi-read-messages #'midi-print-message)

;; return a sequence of all pending messages
(let (l '())
	(midi-read-messages #'(lambda (m q) (push m l)))
	(nreverse l))

;; define a harmonizing function and use it in a loop 
(defun harmonize (msg time)
	(midi-write-message msg time)
	(incf (note-on-key msg) 3)
	(midi-write-message msg time)
	(incf (note-on-key msg) 4)			
	(midi-write-message msg time))

(loop doing (midi-read-messages #'harmonize))

Alternately, the global variable *midi-read-hook* may be set to a function that should be invoked on a message whenever it is read by midi-read-messages.

1.4.3  MIDI Message Time

MIDI message time is expressed in terms of integer "quanta".  The default quantum size is 1000 microseconds.  The MIDI driver's current time may be accessed and set with the following functions:

midi-get-time ()									 [function]

Returns multiple values, the current quanta time and the current quantum size.

midi-set-time quanta								[function]

Sets MIDI time to new quanta time.

midi-set-quanta-size size							 [function]

Sets the number of micro seconds per quantum.

Two functions are supplied for converting between quanta (integer) and real (floating point) time:

quanta-time rtime								 [function]

Convert real time (floating point) to quanta time.

real-time qtime									 [function]

Convert quanta time (integer) to real time.

In addition to getting and setting the time, the timer itself may be stopped and started with the following two of functions: 

midi-start-timer ()								 [function]

Starts the MIDI driver's timer, if currently stopped.

midi-stop-timer ()								 [function]

Stops the midi driver's timer.

1.4.5 Real Time Scheduling
	
It is easy to write a real time scheduling loops in lisp,  so that MIDI messages
(or whatever)  are processed in real time.  As an example, here is a function
that invokes a user supplied function on each element of an event list at its proper time.   Each item in the event list is itself a list whose first element is a MIDI message and whose second element is a delta millisecond time:

(defun real-time (event-list event-fn)
  (declare (optimize (speed 3)(safety 0)))
  (excl:without-interrupts
    (let (base-time next-time next-event)
      (declare (fixnum base-time next-time))
      (setf base-time (get-internal-real-time))
      (loop while event-list
        do 
	(setf next-event (pop event-list))
	(setf next-time (+ (the fixnum (cadr next-event)) 				   base-time))
  	(loop while (< (get-internal-real-time) next-time)
	      do nil)
	(funcall event-fn next-event)
	(setf base-time next-time)))))

After compiling this function, it could be invoked with an event function that just outputs the event's message:

(real-time my-list #'(lambda (e)(midi-write-message (car e))))

The file cm/midi/examples/real-time.lisp implements a simple scheduler for scheduling MIDI messages in real time, and the file cm/midi/examples/real-time-example.lisp contains an example of using this code.  Both files should be compiled before loading.

1.5 MIDI file utilities

*default-midi-pathname* 	              				 [variable]

The value of *default-midi-pathname* is the default pathname for midi file operations.  All midi file utilities merge the user supplied pathname with the value of *default-midi-pathname* to form the fully specified pathname.  *default-midi-pathname* is initially set to "test.midi" in the user's home directory (whatever (user-homedir-pathname) returns as a value.)

midifile-print pathname &key (tracks t)  (stream *standard-output*)	[function]
	 		
Prints the contents of the MIDI file specified by pathname on stream. stream defaults to the standard output.  tracks should either be t, (in which case all MIDI tracks in the file are printed) or a list of track numbers.

midifile-map pathname &key (tracks t)					[function] 
				header-fn channel-message-fn
				track-fn system-message-fn meta-message-fn

Maps optionally supplied functions over the contents of the MIDI file specified by pathname.   tracks is the same as in midifile-print.  If header-fn is supplied, it is passed the header information in the MIDI file, and must accept 3 arguments:  file-format number-of-tracks divisions.  If track-fn is supplied, it is passed information about each track in the MIDI file, and must accept 2 arguments: track-number length.  If channel-message-fn is supplied it is invoked on all channel messages in each track mapped. The function must accept 2 arguments: message time.  If meta-message-fn is supplied it is invoked on all meta event messages in each track mapped. The function must accept 4 arguments: message time data-length message-data.  If system-message-fn is supplied it is invoked on all system messages in each track mapped. The function must accept 4 arguments: message time data-length message-data

Example:

;; return all the channel messages and times in a list.

(let ((list '()))
	(midifile-map "~/test.midi"
	               :channel-message-fn 
	               #'(lambda (msg time)
	                   (push (list msg time) list)))
	(nreverse list))

midifile-parse pathname fn &key (channels t) merge			[function] 

Parses midi messages into parameter note information and maps a user specified function across the data.  The user supplied function must accept 6 arguments:
(channel begin rhythm duration frequency amplitude)
where channel is the midi channel of the note, begin is the starting time (seconds) of the note, rhythm is the real time until the next note (if any), duration is the total length of the note (seconds), frequency is the midi key number of the note and amplitude is the midi velocity value of the original note on.  Note that the parsing process ignores note off velocity and sets the rhythic value of the last note to nil.  The keyword channels controls how the midifile is to be "time lined".  If channels is t (the default), each channel is parsed in its own time line, ie start times, rhythms and durations are calculated using only notes from the same channel.  If channels is nil, then all the channels are "collapsed" into one parsing time line, which is indentical to the time line represented by the midifile itself.  Otherwise, channels should be lists of lists; each sublist represents channels that are to be grouped in a single time line.  Merge controls whether or not data in seperate time lines is sorted prior to function mapping.  The default value nil means that the seperate time lines are not merged, and the function is mapped over the notes in one time line before the next time line is considered.  If merge is t then the function is mapped over notes sorted by start time.

midifile-play &optional pathname &key (start 0) end (timescale 1.0)	[function]
                                                                   (headstart 1000) port

Plays the contents of the MIDI file specified by pathname.  If pathname is not specified, it defaults to the value of *last-scorefile-written*.   Start is the start time in the file to begin listening.  End is the last time in the file to listen to, and defaults to end-of-file. Timescale is a tempo factor to appy to messages and defaults to 1.0.  Headstart is the number of milliseconds to delay the onset of sound by, and defaults to 1000 (1.0 seconds).  Port is the midi port to open if midi is not currently open.

2.1 MIDI Output Streams

The MIDI syntax implements both MIDI listening and midifiles.  MIDI output files have the following slots:

format

The MIDI format of the file.  Currently only level 0 is supported.

tracks

The number of tracks to write. Currently only 1 track is supported.

time-signature

If supplied, a time signature is written at the beginning of track 0.  The time signature is supplied as a list of the form:
(n d &optional (clocks-per-click 24) (32nd-per-quarter 8))
where n and d are the numerator and denominator of the time signature, and clocks-per-click and 32nd-per-quarter are optionally specified values. The default time signature is '(4 4).

key-signature

If supplied, a key signature is written at the start of track 0.  Currently not implemented.

divisions-per-quarter

The divisions per MIDI quarter note. The default value is 96.

tempo

The tempo written to the MIDI file.  Defaults to metronome 120.

2.2 MIDI Objects

Common Music provides two classes of MIDI parts. Both of these part classes contain the basic set of slots available to all classes of parts, such as time, rhythm, status, and count.  And, like all types of score parts, time and rhythm for MIDI parts is expressed in seconds.  The system automatically translates floating point time into the variable length delta times written to the MIDI file. 

2.2.1 The midi-message Class

Midi-message is capable of producing any type of MIDI message.  It declares two additional slots:

message

The current message.  message must be one of the defined message types.

data

The current message data of the part, if any.
	
Example:

(algorithm examp1 midi-message (message (make-note-on 0 0 64) 
                                length 10)
	(setf (note-on-key message)
	  (item (notes c4 e ef f g af bf c5 in heap))))

2.2.2 The midi-note Class

Midi-note implements a convenient interface for generating paired note-on and note-off messages.  Output is expressed in terms of note, amplitude and duration; the system automatically coerces these values to MIDI message data for the separate note on and note off pairs actually written to the score file.  midi-note declares six additional slots:

channel

The MIDI channel of the part.   The default channel  is 0.
	
note
The current frequency of the part. This value may be expressed as either note name, frequency or degree and the value will automatically be converted to a MIDI key number.
	
duration
The duration of the current note.  Like rhythm, duration is expressed in real seconds. The system automatically writes a note off event at time+duration time.
		
amplitude

The amplitude of the current note expressed as a real value between 0.0 and 1.0 and the system will automatically coerce this value to an integer MIDI note on velocity value. 	
	
release

The release speed of the paired note of expressed as a real value between 0.0 and 1.0 and the system will automatically coerce this value to an integer MIDI note off velocity value.  The default value for release is the maximum velocity.

Examples:

(in-package :stella)
(in-syntax :midi)
	
(algorithm examp2 midi-note (rhythm .1 duration .25)
  (setf note (item (notes c4 d ef f g af bf c5 in heap)
                   :kill 8))
  (setf amplitude 
    (item (amplitudes .1 .2 .3 .4 .5 .6 .7 .8))))

;;;
;;; this example writes 64 program changes with 2 test notes	;;; per change. the midi part's message program is initialized ;;; to -1 because we want to start with program 0 yet
;;; increment the value before each output event.
;;;

(in-package :stella)
(in-syntax :midi)

(merge foobar ()

  (algorithm foo midi-message (length 64 rhythm 1.0 
                               message (make-program-change
                                           0 -1))
  (incf (program-change-program message) 1))      
  
  ;; use midi-notes to write some test tones.
  (algorithm bar midi-note (start .25 rhythm .5 duration .24
                            amplitude .5)
    (setf note (item (notes c4 c5) :kill 64))))