MIDI I/O (Part 1)
A slight divergence from the original project plan has been made. The program will no longer play audio itself, but merely take MIDI as input, and provide MIDI as output.
I did have some success in playing audio from a MIDI file, using the overtone package, and I would like to share the associated code in this post. Sadly, overtone will not play a part in my final project.
In my core
namespace, I parse a MIDI file using
overtone.midi.file/midi-file
. This constructs a hash map containing several
things, one of which is :tracks
, which maps to several hash maps,
representing tracks in the file. Each of those tracks contains :events
,
which maps to a sequence of the events which occur on that track. I construct
a lazy sequence of these events, which are preprocessed by a function I wrote
called parse-midi-events
. Then I play each of these event sequences in
parallel, by mapping a function I call play-midi
over the events. The
following code snippet demonstrates this.
(let [midi (midi-file (:input options))
events (map (comp parse-midi-events :events) (:tracks midi))
start-time (+ (now) 1000)]
(doall
(pmap #(play-midi %
piano
start-time
(:division options))
events)))
The two functions I defined, parse-midi-events
and play-midi
, reside in
the namespace hidden-markov-music.midi
. time-elapsed
and
parse-midi-events
will continue to exist in some form, but play-midi
will
not. Please note that some important information from the MIDI file, pertaining
to key and timing, are lost in parse-midi-events
. An attempt is made to
alleviate this by introducing a division
parameter to play-midi
, which
scales the timing.
(ns hidden-markov-music.midi
(:require [overtone.live :refer [at]]
[overtone.music.time :refer [apply-by now]]))
(defn play-midi
"Plays a sequence of midi events using the given instrument."
([midi-seq inst]
(play-midi midi-seq inst (now)))
([midi-seq inst start-time]
(play-midi midi-seq inst start-time 1))
([midi-seq inst start-time division]
(when (seq midi-seq)
(let [{:keys [duration note timestamp velocity]} (first midi-seq)
midi-seq-rest (next midi-seq)
next-event (first midi-seq-rest)
next-timestamp (:timestamp next-event)]
(at (+ start-time (* timestamp division))
(inst :note note :velocity velocity
:sustain (* duration division)))
(apply-by (+ start-time (* next-timestamp division))
#'play-midi midi-seq-rest inst start-time division [])))))
(defn- time-elapsed
[next-event prev-event]
(- (:timestamp next-event)
(:timestamp prev-event)))
(defn parse-midi-events
"Extracts the useful information from a sequence of midi events. Combines
note-on/note-off events into a single event, which plays the note for the
duration of time between the two events."
[events]
(loop [events events
active-events {}
notes []]
(if (seq events)
(let [next-event (first events)]
(if-let [prev-event (active-events (:channel next-event))]
(case (:command next-event)
;; terminate active note on channel
:note-off
(let [duration (time-elapsed next-event
prev-event)
note (-> prev-event
(assoc :duration duration)
(dissoc :command :status :msg))]
;; append new note to notes,
;; remove the active event which has just been terminated,
;; and recur on the remaining events
(recur (next events)
(dissoc active-events (:channel note))
(conj notes note)))
;; terminate active event on channel and activate current event
:note-on
(let [duration (time-elapsed next-event
prev-event)
note (-> prev-event
(assoc :duration duration)
(dissoc :command :status :msg))]
;; append new note to notes,
;; replace the active event which has just been terminated, with
;; the new event,
;; and recur on the remaining events
(recur (next events)
(assoc active-events
(:channel note)
next-event)
(conj notes note)))
;; event is neither :note-on or :note-off, so we discard it
(recur (next events)
active-events
notes))
(if (= :note-on (:command next-event))
;; add current event as the new active event,
;; and recur on the remaining events
(recur (next events)
(assoc active-events
(:channel next-event)
next-event)
notes)
;; event is not :note-on, and channel is not currently in :note-on
;; state, so we discard the current event
(recur (next events)
active-events
notes))))
;; all events processed, return notes
notes)))