Mercurial > hg > amuse
view implementations/midi/midifile-import.lisp @ 204:10d47e78a53d
Added support for most significant midi messages (some remain missing,
but these do not occur in a 1000-file test sample)
author | David Lewis <d.lewis@gold.ac.uk> |
---|---|
date | Fri, 17 Sep 2010 10:28:27 +0100 |
parents | 4e0a5c7026ca |
children |
line wrap: on
line source
(cl:in-package #:amuse-midi) (defparameter *default-tempo* 120) (defclass midifile-identifier (composition-identifier midi-object) ((pathname :initarg :path :reader midifile-identifier-pathname :initform 'nil)) (:documentation "Identifier for MIDI files, containing pathname information")) (defun midifile-id (pathname) "Creates an identifier for MIDI files, based on a pathname" (make-instance 'midifile-identifier :path pathname)) (defmethod get-composition ((identifier midifile-identifier)) (%initialise-midifile-composition (midi:read-midi-file (midifile-identifier-pathname identifier)))) (defun %initialise-midifile-composition (midifile) ;; Takes a midifile object (from the "MIDI" package) ;; and returns an amuse midi object ;; FIXME: gets it wrong if patch changes in mid-note ;; FIXME: assumes controllers are global in scope and location (let ((tracks (midi:midifile-tracks midifile)) (division (midi:midifile-division midifile)) (notes) (time-sigs) (key-sigs) (tempi) (pitch-bends) (texts) (lyrics) (rpn-lsb) (rpn-msb) (nrpn-msb) (nrpn-lsb) (misc-controllers) (track-no -1) (last-time 0)) (when *default-tempo* (push (make-instance 'standard-tempo-period :time 0 :bpm *default-tempo*) tempi)) (dolist (track tracks) (incf track-no) (setf track (sort (copy-seq track) #'(lambda (x y) (or (< (midi:message-time x) (midi:message-time y)) (and (= (midi:message-time x) (midi:message-time y)) (typep x 'midi:note-off-message)))))) (let ((ons (make-array '(16 128) :initial-element nil)) (offs) (patches (make-array 16 :initial-element 0))) (dolist (event track) (let* ((midi-time (midi:message-time event)) (event-time (/ midi-time division))) (when (> midi-time last-time) (setf last-time midi-time)) (cond ((or (typep event 'midi:note-off-message) (and (typep event 'midi:note-on-message) (= (midi:message-velocity event) 0))) (let ((pitch (midi:message-key event)) (channel (midi:message-channel event))) (if (aref ons channel pitch) (progn (push (make-event-from-on-off-pair (aref ons channel pitch) midi-time division track-no (aref patches channel)) notes) (setf (aref ons channel pitch) nil)) ;; if there's no matching on, wait until the beat ;; is done. (push event offs)))) ((typep event 'midi:note-on-message) (let ((pitch (midi:message-key event)) (channel (midi:message-channel event))) (when (aref ons channel pitch) ;; there's a note already sounding. End it. (push (make-event-from-on-off-pair (aref ons channel pitch) midi-time division track-no (aref patches channel)) notes)) (setf (aref ons channel pitch) event))) ((typep event 'midi:time-signature-message) ;; FIXME: Should I make a midi version of this object, ;; with track/channel? (when time-sigs (setf (duration (car time-sigs)) (- event-time (timepoint (car time-sigs))))) (push (make-instance 'standard-time-signature-period :time event-time :numerator (midi:message-numerator event) :denominator (expt 2 (midi:message-denominator event))) time-sigs)) ((typep event 'midi:key-signature-message) ;; FIXME: Should I make a midi version of this object, ;; with track/channel? [probably, yes] (when key-sigs (setf (duration (car time-sigs)) (- event-time (timepoint (car time-sigs))))) (push (make-instance 'midi-key-signature-period :time event-time :sharp-count (midi:message-sf event) :mode (midi:message-mi event)) key-sigs)) ((typep event 'midi:tempo-message) (when tempi (if (= midi-time 0) ;; to allow for default tempo kludge. Nightingale ;; seems to happily export midi with no initial ;; tempo, but tempo changes later. Making this ;; uncertainty last beyond import could prove ;; confusing (though is probably the `right' ;; answer) (setf tempi nil) (setf (duration (car tempi)) (- event-time (timepoint (car tempi)))))) (push (make-instance 'standard-tempo-period :time event-time :bpm (microsecond-per-crotchet-to-bpm (midi:message-tempo event))) tempi)) ((typep event 'midi:program-change-message) (setf (aref patches (midi:message-channel event)) (midi:message-program event))) ((typep event 'midi:pitch-bend-message) (when pitch-bends (setf (duration (car pitch-bends)) (- event-time (timepoint (car pitch-bends))))) (push (make-controller-moment event track-no event-time) pitch-bends)) ((typep event 'midi::lyric-message) (push (make-controller-moment event track-no event-time) lyrics)) ((typep event 'midi::text-message) (push (make-controller-moment event track-no event-time) texts)) ((and (typep event 'midi::control-change-message) (< 97 (slot-value event 'midi::controller) 102) (= (slot-value event 'midi::controller) 127)) ;; this can only be part of an end-of-this-rpn/nrpn message, ;; and these don't really nest because of shared controllers. (setf rpn-lsb nil rpn-msb nil nrpn-lsb nil nrpn-msb nil)) ((and (typep event 'midi::control-change-message) (< 99 (slot-value event 'midi::controller) 102)) ;; setting rpn, not nrpn (setf nrpn-lsb nil nrpn-msb nil) (if (= (slot-value event 'midi::controller) 100) (setf rpn-lsb (slot-value event 'midi::value)) (setf rpn-msb (slot-value event 'midi::value)))) ((and (typep event 'midi::control-change-message) (< 97 (slot-value event 'midi::controller) 100)) ;; setting nrpn, not rpn (setf rpn-lsb nil rpn-msb nil) (if (= (slot-value event 'midi::controller) 98) (setf nrpn-lsb (slot-value event 'midi::value)) (setf nrpn-msb (slot-value event 'midi::value)))) ((and (typep event 'midi::control-change-message) (= (slot-value event 'midi::controller) 38)) (push (make-parameter-moment (slot-value event 'midi::value) nil event-time track-no rpn-lsb rpn-msb nrpn-lsb nrpn-msb) misc-controllers)) ((and (typep event 'midi::control-change-message) (= (slot-value event 'midi::controller) 6)) (push (make-parameter-moment nil (slot-value event 'midi::value) event-time track-no rpn-lsb rpn-msb nrpn-lsb nrpn-msb) misc-controllers)) ((typep event 'midi::channel-prefix-message) ;; I think this is basically a channel specifying hack for a ;; preceding instrument name. If I'm wrong, I'll need to ;; FIXME (when texts (setf (%midi-message-channel (car texts)) (slot-value event 'midi::channel)))) (t (push (make-controller-moment event event-time track-no) misc-controllers))))))) (when tempi (setf (duration (car tempi)) (- (/ last-time division) (timepoint (car tempi))))) (when pitch-bends (setf (duration (car pitch-bends)) (- (/ last-time division) (timepoint (car pitch-bends))))) (when time-sigs (setf (duration (car time-sigs)) (- (/ last-time division) (timepoint (car time-sigs))))) (when key-sigs (setf (duration (car key-sigs)) (- (/ last-time division) (timepoint (car key-sigs))))) ;; make a midi object from notes, etc. (let ((composition (make-instance 'midi-composition :time 0 :interval (/ last-time division) :time-signatures (if time-sigs (sort time-sigs #'time<) (list (make-standard-time-signature-period 4 4 0 (/ last-time division)))) :tempi (sort tempi #'time<) :key-signatures (sort key-sigs #'time<) :pitch-bends (sort pitch-bends #'time<) :texts (sort texts #'time<) :lyrics (sort lyrics #'time<) :controllers (sort misc-controllers #'time<)))) (sequence:adjust-sequence composition (length notes) :initial-contents (sort notes #'time<))))) (defun make-event-from-on-off-pair (note-on cut-off divisions track patch) (cond ((or (= (midi:message-channel note-on) 9) (> patch 111)) ;; percussive (make-instance 'midi-percussive-event :channel (1+ (midi:message-channel note-on)) :track track :time (/ (midi:message-time note-on) divisions) :interval (/ (- cut-off (midi:message-time note-on)) divisions) :velocity (midi:message-velocity note-on) :patch patch :sound (midi:message-key note-on))) (t ;; pitched (make-instance 'midi-pitched-event :channel (1+ (midi:message-channel note-on)) :track track :time (/ (midi:message-time note-on) divisions) :interval (/ (- cut-off (midi:message-time note-on)) divisions) :velocity (midi:message-velocity note-on) :patch patch :number (midi:message-key note-on))))) (macrolet ((rpn (m-type) `(make-instance ,m-type :lsb value-lsb :msb value-msb :time time :track track-no))) (defun make-parameter-moment (value-lsb value-msb time track-no rpn-lsb rpn-msb nrpn-lsb nrpn-msb) (cond ((and nrpn-lsb nrpn-msb) (make-instance 'non-registered-parameter-moment :lsb value-lsb :msb value-msb :nrpn-lsb nrpn-lsb :nrpn-msb nrpn-msb :time time :track track-no)) ((or (not rpn-lsb) (not rpn-msb) (= rpn-lsb 0)) ;; default behaviour (rpn 'pitch-bend-sensitivity-moment)) ((= rpn-lsb 1) (rpn 'channel-fine-tuning-moment)) ((= rpn-lsb 2) (rpn 'channel-course-tuning-moment)) ((= rpn-lsb 3) (rpn 'tuning-program-moment)) ((= rpn-lsb 4) (rpn 'tuning-bank-moment)) ((= rpn-lsb 5) (rpn 'modulation-depth-moment)) (t (make-instance 'unknown-registered-parameter-moment :lsb value-lsb :msb value-msb :reg-lsb rpn-lsb :reg-msb rpn-msb :time time :track track-no))))) (defgeneric control-number (message) (:method ((m midi::reset-all-controllers-message)) 121) (:method ((m midi::local-control-message)) 122) (:method ((m midi::all-notes-off-message)) 123) (:method ((m midi::omni-mode-off-message)) 123) (:method ((m midi::omni-mode-on-message)) 124) (:method ((m midi::mono-mode-on-message)) 125) (:method ((m midi::mono-mode-off-message)) 126) (:method ((m midi::poly-mode-on-message)) 127)) (defgeneric control-value (message) (:method ((m midi::reset-all-controllers-message)) 0) (:method ((m midi::local-control-message)) (slot-value m 'midi::mode)) (:method ((m midi::all-notes-off-message)) 0) (:method ((m midi::omni-mode-off-message)) 0) (:method ((m midi::omni-mode-on-message)) 0) (:method ((m midi::mono-mode-on-message)) (slot-value m 'midi::nb-channels)) (:method ((m midi::mono-mode-off-message)) 0) (:method ((m midi::poly-mode-on-message)) 0)) (defgeneric make-controller-moment (event track time)) (defgeneric channel-messagep (event) (:method ((e channel-message)) t) (:method (e) nil)) ;; There's a lot in common between controller making instances, so ;; let's automate a bit (macrolet ((controller-maker (midi-class amuse-class &rest slots-and-options) `(defmethod make-controller-moment ((event ,midi-class) track time) (if (channel-messagep event) (make-instance ,amuse-class :time time :track track :channel (midi::message-channel event) ,@slots-and-options) (make-instance ,amuse-class :time time :track track ,@slots-and-options))))) ;; Both of these are control change messages. The latter set are more ;; specifically defined ones in the MIDI library (omni off, for ;; example). We may want to separate these, but I see no reason to ;; yet. We may also want to consider having more types made explicit ;; in the library, but again, there's no obvious functionality gain. (controller-maker midi::control-change-message 'control-change-moment :value (slot-value event 'midi::value) :control (slot-value event 'midi::controller)) (controller-maker midi::mode-message 'control-change-moment :value (control-value event) :control (control-number event)) ;; I think that MIDI.LISP treats sysexs as two very distinct ;; things. I'm not sure that's true, so I'm treating them as the same ;; thing for now. (controller-maker midi::system-exclusive-message 'system-exclusive-moment :status 240 :value (slot-value event 'midi::data)) (controller-maker midi::authorization-system-exclusive-message 'system-exclusive-moment :status 247 :value (slot-value event 'midi::data)) (controller-maker midi::polyphonic-key-pressure-message 'aftertouch-moment :key (slot-value event 'midi::key) :pressure (slot-value event 'midi::pressure)) (controller-maker midi::channel-pressure-message 'channel-aftertouch-moment :pressure (slot-value event 'midi::pressure)) ;; This is a period, but we add duration later anyway (controller-maker midi:pitch-bend-message 'pitch-bend-period :value (midi::message-value event)) ;; Text messages (controller-maker midi::lyric-message 'lyric-moment :text (slot-value event 'midi::text)) (controller-maker midi::general-text-message 'text-moment :text (slot-value event 'midi::text)) (controller-maker midi::copyright-message 'copyright-moment :text (slot-value event 'midi::text)) (controller-maker midi::sequence/track-name-message 'sequence-or-track-moment :text (slot-value event 'midi::text)) (controller-maker midi::instrument-message 'instrument-name-moment :text (slot-value event 'midi::text)) (controller-maker midi::marker-message 'marker-moment :text (slot-value event 'midi::text)) (controller-maker midi::cue-point-message 'cue-moment :text (slot-value event 'midi::text)) (controller-maker midi::program-name-message 'program-name-moment :text (slot-value event 'midi::text)) (controller-maker midi::device-name-message 'device-name-moment :text (slot-value event 'midi::text)) ;; Not used: (controller-maker midi::program-change-message 'program-change-moment :program (midi::message-program event)))