Nyquist can read and write midi files. Midi files are read into and written from a
special XLisp data type called a SEQ
, which is short for
"sequence". (This is not part of standard XLisp, but rather part of Nyquist.)
Nyquist can examine the contents of a SEQ
type, modify the SEQ
by adding or deleting Midi notes and other messages. Finally, and perhaps most
importantly, Nyquist can use the data in a SEQ
type along with a sound
behavior to generate sound. In other words, Nyquist can become a Midi synthesizer.
SEQ
TypeTo create a SEQ
data object:
> (setf my-seq (seq-create)) #<SEQ:0x7a6f60> > (type-of my-seq) SEQ >
Once you have a sequence, you can read Midi data into it from a file. You do this in
three steps. First, you open the file in binary mode (using open-binary
, a
Nyquist extension to XLisp). Then you read from the file. Finally, you (normally) close
the file.
(setf midi-file (open-binary "demo.mid")) (seq-read-smf my-seq midi-file) (close midi-file)
A sequence can be written to a file. First you open the file as a binary output file; then you write it; then you close it.
(setf midi-file (open-binary "copy.mid" :direction :output)) (seq-write-smf my-seq midi-file t) (close midi-file)
The result will not be a bit-for-bit copy of the original Midi file because the SEQ
representation is not a complete representation of the Midi data. For example, the Midi
file can contain headers and meta-data that is not captured by Nyquist. Nevertheless, the
resulting Midi file should sound the same if you play it with a sequencer or Midi file
player.
One very handy feature of the SEQ
datatype is that it was originally
developed for a text-based representation of files called the Adagio Score Language, or
just "Adagio." You can write an Adagio file from a sequence by opening a text
file and calling seq-write
.
(setf gio-file (open "copy.gio" :direction :output)) (seq-write my-seq gio-file) (close gio-file)
The basic format of the Adagio file is pretty intuitive, but you can find the full description in the CMU Midi Toolkit manual or in a chapter of the Nyquist manual, including the online version in HTML.
Because Adagio is text, you can easily edit them or compose your own Adagio file. You
should be aware that Adagio supports numerical parameters, where pitch and duration are
just numbers, and symbolic parameter, where a pitch might be Bf4
(for B-flat above middle-C) and a duration might be QT
(for a quarter note
triplet). Symbolic parameters are especially convenient for manual entry of data. Once you
have an Adagio file, you can create a sequence from it in Nyquist:
(setf seq-2 (seq-create)) (setf gio-file (open "demo.gio")) (seq-read seq-2 gio-file) (close gio-file)
SEQ
TypeAlthough not originally intended for this purpose, XLisp and Nyquist form a powerful language for generating Midi files. These can then be played using a Midi synthesizer or using Nyquist, as will be illustrated later.
To add notes to a sequence, you call seq-insert-note
as illustrated in
this routine, called midinote
. Since seq-insert-note
requires
integer parameters, with time in milliseconds, midinote
performes some
conversions and limiting to keep data in range:
(defun midinote (seq time dur voice pitch vel)
(setf time (round (* time 1000)))
(setf dur (round (* dur 1000)))
(setf pitch (round pitch))
(setf vel (round vel))
(seq-insert-note seq time 0 (1+ voice) pitch dur vel))
Now, let's add some notes to a sequence:
(defun test () (setf *seq* (seq-create)) (midinote *seq* 0.0 1.0 1 c4 100) (midinote *seq* 1.0 0.5 1 d4 100) (midinote *seq* 2.0 0.8 1 a4 100) (setf seqfile (open-binary "test.mid" :direction :output)) (seq-write-smf *seq* seqfile) (close seqfile))
This example illustrates the creation of random note onset times using the Poisson
distribution. One way to generate this distribution, as seen here, is to create uniformly
distributed random times, and then sort these. The function that creates times and then
quantizes them to 24ths of a beat is shown here. The len
parameter is the
number of times, and the average-ioi
parameter is the average
inter-onset-interval, the average time interval between two adjacent times:
;; create list of random times and sort it ;; dur in ms. (defun poisson-gen (len average-ioi) (let ((dur (* len average-ioi)) poisson-list) (dotimes (i len) (push (* dur (random 10000) 0.0001) poisson-list)) (setf poisson-list (sort poisson-list #'<)) (display "initial list" poisson-list) ;; map list to 24ths: (setf poisson-list (quantize-times-to-24ths poisson-list)) ))
We add a few functions to help express time in terms of beats:
(defun set-tempo (tempo) (setf qtr (/ 60.0 tempo)) (setf 8th (* qtr 0.5)) (setf half (* qtr 2)) (setf whole (* qtr 4)) (setf 16th (* qtr 0.25))) (if (not (boundp 'qtr)) (set-tempo 100)) (defun quantize-times-to-24ths (list) (mapcar #'quantize-time-to-24ths list)) (defun quantize-time-to-24ths (time) (* (/ qtr 24.0) (round (* 24 (/ time qtr)))))
Now, let's create Midi notes using Poisson-based onset times:
(defun melody (seq onsets) (dolist (onset onsets) (midinote seq onset 16th 1 (+ 48 (random 24)) 100))) (defun poisson-melody () (setf *seq* (seq-create)) (melody *seq* (poisson-gen 50 8th)) ;; adds notes to *seq* (setf seqfile (open-binary "pois.mid" :direction :output)) (seq-write-smf *seq* seqfile) (close seqfile))
After evaluating (poisson-melody)
, you can play the file
"pois.mid" to hear the result. The times are quantized to 24th notes at a tempo
of 100, so you can even use a notation editor to display the result in common music
notation.
To synthesize sound from a Midi file, use the seq-midi
control construct.
This behavior reads the data in the seq
object and for each note, creates an
instance of the behavior you provide. You will need an instrument, so let's define a
simple FM instrument to play the notes of the Midi data:
(defun fm-note (p) (mult (pwl 0.01 1 .5 1 1) (fmosc p (mult (step-to-hz p) (pwl 0.01 6 0.5 4 1) (osc p)))))
Now let's use fm-note
to play the previously defined poisson-melody
,
which was saved in the variable *seq*
:
(play (seq-midi *seq* (note (chan pitch vel) (a-note pitch))))
The seq-midi
construct automatically uses time transformations to place
notes at the proper time and to stretch them to the indicated duration. In addition, it
sets the chan
, pitch
, and vel
parameters according
to the Midi data before invoking your behavior. In this simple example, we ignored chan
and vel
, but we used pitch
to get the right pitch. You might
write a more complicated behavior that uses chan
to select different
synthesis algorithms according to the Midi channel.
The syntax for the seq-midi
construct may be a little confusing. The
symbol note
appears to be a function call, but it is not. It is really there
to say that the following parameter list and behavior expression apply to Midi notes.
There can be other terms for other Midi messages, e.g.
(seq-midi my-seq (note (chan pitch velocity) (my-note pitch velocity)) (ctrl (chan control value) (...)) (bend (chan value) (...)) (touch (chan value) (...)) (prgm (chan value) (setf (aref my-prgm chan) value))
SEQ
DataIn the lib folder of the standard Nyquist installation, there is a file called midishow.lsp
.
If you load this, you can call some functions that help you examine SEQ
data.
Try the following (after running poisson-melody
above).
(load "midishow") (midi-show *seq*)
You will see a printout of the data inside the SEQ
data object. Unlike
Midi, which stores note-on and note-off messages separately, the SEQ
structure saves notes as a single message that includes a duration. This is translated to
and from Midi format when you write and read Midi files.
You can also examine a Midi file by calling:
(midi-show-file "demo.mid")
This function can take an optional second argument specifying an opened text file if you want to write the data to a file rather than standard (console) output:
(midi-show-file "demo.mid" (open "dump.txt" :direction :output)) (gc)
What is going on here? I did not save the opened file, but rather passed it directly to
midi-show-file
. Therefore, I did not have a value to pass to the close
function. However, I know that files are closed by the garbage collector when there are no
more references to them, so I simply called the garbage collector (gc)
to run
and close the file.