This is midi.rtf in view mode; [Download] [Up]
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))))
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.