MIDI Support in Common Music

[cmlogo.gif]

Heinrich Taube
School of Music, University of Illinois
taube@uiuc.edu




Table of Contents


Introduction

This document is an overview of the lower level MIDI functions and data structures in Common Music. High level functionality is documented in the Common Music Dictionary and the Stella Tutorial. For implementation details, see README and initial comments in md.lisp for more information.

Common Music's MIDI syntax supports reading and writing 32 channels of MIDI data to a driver and 16 channels of MIDI data to a midi file. The MIDI implementation is divided into three levels. The highest level consists of object-oriented class definitions that support composition using high level musical abstractions like patterns, notes, scales, rhythms, durations, algorithms, generators, etc. These high level objects automatically transform MIDI compositional data into a lower level representation used by the mid-level MIDI routines. Functions and data structures in the mid-level support creation, reading and writing of unsigned integer MIDI messages to a driver or a file. This document is primarily concerned with documenting the functionality of this layer. The lowest level of MIDI support is represented by hardware specific "foreign functions" that implement the actual connection to a MIDI driver resource. This layer is undocumented, see the various implementation files in "midi/*.lisp" and "midi/*.c" for more information.


MIDI Messages

A MIDI message is a formatted 28-bit fixnum containing up to three bytes of data. All of the messages types defined in the MIDI 1.0 specification are supported by MIDI message types and subtypes in Common Music.


MIDI Message Types

Each MIDI message type is implemented by a constructor function, a predicate, and one or more field accessors, if appropriate. The field accessors are functions, not macros, so they are suitable for use in mapping. For example:

(mapcar #'note-on-key message-list)
would return a list of all the key numbers in a list of note-on messages.

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

? (setf x (make-note-on 0 60 64))
=59784256

? (note-on-key x)
60

? (incf (note-on-key x) 10)
70

? (midi-print-message x)
#<NoteOn: 0, 0, 70, 64>
Some message types, such as MIDI file meta events, are implemented as composite messages. Constructors for composite messages return multiple values, the message created and a (possibly empty) list of message data. Each element in the message data list is a message of type midi-data. A midi-data message contains from one to three bytes of raw MIDI data.


Channel Messages

Channel messages define the basic device operations in MIDI.

channel-message [Message Type]

make-channel-message port status channel data1 &optional data2
channel-message-p message
channel-message-status message
channel-message-channel message
channel-message-port message
channel-message-data1 message
channel-message-data2 message

channel-mode [Channel Message]

make-channel-mode channel control change &optional(port 0)
channel-mode-p message
channel-mode-channel message
channel-mode-control message
channel-mode-change message
channel-mode-port message

channel-pressure [Channel Message]

make-channel-pressure channel pressure &optional(port 0)
channel-pressure-p message
channel-pressure-channel message
channel-pressure-pressure message
channel-pressure-port message

control-change [Channel Message]

make-control-change channel controller value &optional(port 0)
control-change-p message
control-change-channel message
control-change-control message
control-change-change message
control-change-port message

key-pressure [Channel Message]

make-key-pressure channel key velocity &optional(port 0)
key-pressure-p message
key-pressure-channel message
key-pressure-key message
key-pressure-pressure message
key-pressure-port message

note-off [Channel Message]

make-note-off channel key velocity &optional(port 0)
note-off-p message
note-off-channel message
note-off-key message
note-off-velocity message
note-off-port message

note-on [Channel Message]

make-note-on channel key velocity &optional(port 0)
note-on-p message
note-on-channel message
note-on-key message
note-on-velocity message
note-on-port message

pitch-bend [Channel Message]

make-pitch-bend channel lsb msb &optional(port 0)
pitch-bend-p message
pitch-bend-channel message
pitch-bend-lsb message
pitch-bend-msb message
pitch-bend-port message

program-change [Channel Message]

make-program-change channel program &optional(port 0)
program-change-p message
program-change-channel message
program-change-program message
program-change-port message


Meta Messages

Meta messages store various sorts of non-event information in MIDI tracks. All meta-message types are implemented as multiple messages. meta-message constructors return multiple values, a message of type meta-message and a (possibly empty) list of midi-data. Each element of midi-data holds one byte of MIDI data.

meta-message [Message Type]

make-meta-message type
meta-message-p message
meta-message-type message

eot [Meta Message]

make-eot
eot-p message

tempo-change [Meta Message]

make-tempo-change usecs
tempo-change-p message

time-signature [Meta Message]

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


Data Messages

Data returned as the second value of composite message constructors are of type midi-data. midi-data is not a message type implemented by the MIDI protocol.

midi-data [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


System Messages

MIDI sends system messages to all devices at once.

system-message [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

sysex-message [System Message]

System exclusive messages allow synthesizer specific information to be sent. The sysex-message type is implemented across multiple messages. make-sysex-message returns multiple values, a message of type sysex-message and a (possibly empty) list of midi-data. Each element of midi-data holds one byte of MIDI data.

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

tune-request [System Message]

make-tune-request ()
tune-request-p message


MIDI Message Utilities

Common Music provides several general purpose functions for working with MIDI messages:


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

Creates a MIDI message of type type, which must be one of the defined MIDI message types. args is zero or more additional arguments appropriate to type.

Example:

? (make-midi-message 'note-on 0 60 64)
126893120

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

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

Example:

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

? (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

MIDI Real Time

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

? (midi-open :port :A)
:A

? (midi-write-message (make-note-on 0 60 127))
T

? (midi-write-message (make-note-off 0 60 127))
T

? (midi-close)
T

Opening and Closing the MIDI port

Before any MIDI messages are sent or received MIDI must first be opened on some port. The port should be closed after it is no longer needed.

midi-open &key :port [Function]

Opens the MIDI driver on port. Valid ports are 1 a :a :modem, 2 b :b :printer. midi-open returns the port name if it was able to open the port, otherwise nil.

Example:

? (midi-open :port :b)
:B

midi-close [Function]

Closes the current MIDI port if one is open. Returns t if it was able to close the port, otherwise nil.


midi-open? [Function]

Returns a port reference if MIDI open, otherwise nil.


*default-midi-port* [Variable]

Provides the default port value for MIDI input and output. Ports are designated 0, a, modem; 1, b, printer; 2, a+b, modem+printer


Reading and Writing MIDI Messages

Once a port is opened MIDI messages may be read from, or sent to, the driver.

midi-read-messages &optional function [Function]

Reads all pending MIDI messages. If function is supplied it is passed each message and its millisecond time as they are read from the driver.

Example:

;; 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 variable *midi-read-hook* may be set to the function to invoke on each message read by midi-read-messages.


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

Sends message at the optionally specified millisecond time time. If time is not specified the message is sent immediately.

Example:

? (midi-open :port :a)
:A

? (midi-write-message (make-note-on 0 60 127))
T

? (midi-write-message (make-note-off 0 60 127) 1000)
T

;;; here is a slightly more adventurous example that plays 100
;;; notes and chooses some sort of drum sound on my synth 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 Time

MIDI time is expressed in terms of integer milliseconds.

midi-get-time [Function]

Returns the current millisecond MIDI time.


midi-set-time quanta [Function]

Sets MIDI time to new millisecond time.


midi-start-time [Function]

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


midi-stop-time [Function]

Stops the midi driver's timer.


quanta-time rtime [Function]

Convert real time (floating point) to quanta time.


real-time qtime [Function]

Convert quanta time (integer) to real time.


Real Time Scheduling

It is easy to implement a real time scheduling loop in lisp but it may not work very well! Here is one that invokes a user supplied function on each element in an event queue at its appropriate time. Each entry in the queue is a list whose first element is a MIDI message and whose second element is a millisecond time delta.

(defun real-time (event-list
                  &optional (event-fn
                              #'(lambda (e)
                                  (midi-write-message (car e)))))
  (declare (optimize (speed 3)(safety 0)))
  (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)))))
The file rt.lisp and rtexamp.lisp implement a simple scheduler for real time MIDI messaging. Both files should be compiled before loading.


MIDI File Utilities

*default-midi-pathname* [Variable]

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 file name. *default-midi-pathname* is initially set to "test.midi" in whatever directory user-homedir-pathname returns.


midifile-map pathname &key :tracks :header-fn :channel-message-fn :track-fn :system-message-fn :meta-message-fn [Function]

Maps optionally supplied functions over the contents of the MIDI file pathname. If :tracks is t all MIDI tracks in the file are mapped, otherwise :tracks should be a track number or list of track numbers to map. If :header-fn is supplied it is passed the MIDI file header information in 3 values: file-format number-of-tracks divisions. If :track-fn is supplied it is passed information about each track in 2 values: track-number length. If :channel-message-fn is supplied it is invoked on all channel messages in each track mapped. The function is passed 2 values: message time. If :meta-message-fn is supplied it is invoked on all meta event messages in each track mapped. The function is passed 4 values: 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 is passed 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 :merge [Function]

Parses MIDI messages into parameter note information and maps a user specified function across this data. The function is passed 6 values: channel begin rhythm duration frequency amplitude. 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. The parsing process ignores note off velocity and sets the rhythmic value of the last note to nil. :channels controls how the midifile is "time lined". If t (the default), each channel is parsed in its own time line, i.e. start times, rhythms and durations are calculated using only notes from the same channel. If the value is nil, then all the channels are merged into one time line, which is identical to the time line represented by the MIDI file 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 separate time lines are sorted prior to function mapping. If nil (the default), separate time lines are not merged, and the user supplied function is mapped over all the events in one time line before the next time line is considered. If :merge is t then the function is mapped over the time lines in parallel, sorted by start time.


midifile-play &optional pathname &key :start :end :timescale :headstart :port [Function]

Plays the contents of the MIDI file pathname. If pathname is not specified, it defaults to the value of *default-midi-pathname*. :start is the start time in the file to begin listening at. :end is the time in the file to stop listening at. :timescale is a tempo scaler to apply to the 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 one is not currently open.


midifile-print pathname &key :tracks :stream [Function]

Prints the contents of the MIDI file pathname on stream, which defaults to the standard output. If :tracks is t all MIDI tracks in the file are printed, otherwise :tracks should be a track number or list of track numbers to print.


©MCMXCV by hkt@ccrma.stanford.edu