view tools/midi-output.lisp @ 189:70c8d723fb8a

fix undefined *default-tempo* warning darcs-hash:20090103010419-16a00-54f1baa3eb4d382c6c533d17af89d2b7cd73bfca.gz
author j.forth <j.forth@gold.ac.uk>
date Sat, 03 Jan 2009 01:04:19 +0000
parents 18d3480e7ec8
children
line wrap: on
line source
;; Make midifiles from basic amuse objects methods here can be
;; overridden for more specific types
;; 

;; FIXME: Need to push some structures from geerdes to make this work.

(in-package #:amuse-tools)

(defgeneric play (music)
  (:method (m) (play-midifile (make-midi m)))
  (:documentation "Plays using timidity, unless mac os is
  present, in which case open (=QuickTime) is used. CoreAudio
  code exists, but is awkward to maintain."))
(defmethod play ((music composition))
  (play-midifile (make-midi music)))

(defun write-midi (music pathname)
  "Exports music as midi file."
  (midi:write-midi-file (make-midi music) pathname))

(defun play-midifile (midifile)
  ;; coremidi is easy as an alternative, but we'll probably want midi
  ;; file export anyway, so it makes some sense to focus our efforts
  ;; on this first. That said, is there a CoreAudio midi file player
  ;; routine?
  (midi:write-midi-file midifile "tmp.mid")
      #+darwin
      (sb-ext:run-program "open" '("tmp.mid") :search t)
      #+linux
      (sb-ext:run-program "xdg-open" '("tmp.mid") :search t))

(defgeneric make-midi (sequence))
(defmethod make-midi ((sequence sequence))
  ;; Make a midifile object. Collects global midi messages (that
  ;; require a sequence) and event-based messages (that don't).
  ;; FIXME: Something about this strikes me as very stupid. Must
  ;; revisit
  ;; FIXME: Only making type 0. Is this a problem?
  (let* ((events (event-sequence-messages sequence))
	 (globals (global-messages sequence))
	 (patches (patch-messages sequence)))
    (make-midifile-from-messages (nconc events globals patches)
				 :type 0)))

(defun make-midifile-from-messages (events &key (type 0))
  ;; FIXME: clearly broken if type 1
  ;; First have to get rid of all fractional times and choose a
  ;; timebase
  (let* ((timebase (apply #'lcm (mapcar #'(lambda (x)
					    (denominator
					     (midi:message-time x)))
					events))))
    (when (< timebase 4)
      (setf timebase (* 4 timebase)))
    (loop for e in events
       do (setf (midi:message-time e) (* timebase
					 (midi:message-time e))))
    (make-instance 'midi:midifile
		   :format type
		   :division timebase
		   :tracks (list (sort-midi-messages-for-output events)))))

(defun sort-midi-messages-for-output (messages)
  (sort messages #'(lambda (x y) (or (< (midi:message-time x)
					(midi:message-time y))
				     (and (= (midi:message-time x)
					     (midi:message-time y))
					  (< (midi::message-status x)
					     (midi::message-status y)))))))

(defun event-sequence-messages (sequence)
  (let ((midinotes))
    (sequence:dosequence (event sequence midinotes)
      (let ((messages (event-messages event)))
	(dolist (message messages)
	  (push message midinotes))))))

(defun patch-messages (sequence)
  (let ((patches (make-array 16 :initial-element nil))
	(patch-list)
	(channel 0)
	(patch 0))
    (sequence:dosequence (event sequence patch-list)
      (setf channel (get-channel-for-midi event)
	    patch (get-patch-for-midi event))
      (when (or (not (aref patches channel))
		(not (= (aref patches channel)
			patch)))
	(push (make-instance 'midi:program-change-message
			     :program patch
			     :time (timepoint event)
			     :status (+ channel 192))
	      patch-list)
	(setf (aref patches channel) patch)))))

(defparameter *default-tempo* 80)
(defgeneric default-tempo-for-midi (anchored-period)
  (:method ((o standard-anchored-period))
    (make-standard-tempo-period *default-tempo* (timepoint o) (duration o))))

(defgeneric global-messages (sequence)
  (:method (s) (declare (ignore s)) nil))
(defmethod global-messages ((sequence composition))
  ;; FIXME: missing plenty of other messages
  ;; FIXME: messy
  (let ((tempi (tempi sequence))
	(temp)
	(time-sigs (time-signatures sequence))
	(events))
    (if tempi
	(dolist (tempo tempi)
	  (setf temp (tempo-message tempo))
	  (when temp
	    (push temp events)))
	(push (tempo-message (default-tempo-for-midi sequence))
	      events))
    (dolist (time-sig time-sigs events)
      (setf temp (time-sig-message time-sig))
      (when temp
	(push temp events)))))

(defgeneric tempo-message (tempo)
  (:method (tp)
    (progn
      (let ((speed (make-instance 'midi:tempo-message
				  :time (timepoint tp)
				  :status 255)))
	(setf (slot-value speed 'midi::tempo) (microseconds-per-crotchet tp))
	speed))))

(defgeneric time-sig-message (time-sig)
  (:method (ts) (declare (ignore ts)) nil))

(defgeneric event-messages (event)
  (:method (e) (declare (ignore e)) nil))
(defmethod event-messages ((event standard-chromatic-pitched-event))
  (list (make-instance 'midi:note-on-message
		       :status (+ (get-channel-for-midi event) 144)
		       :key (midi-pitch-number event)
		       :velocity (get-velocity-for-midi event)
		       :time (timepoint event))
	(make-instance 'midi:note-off-message
		       :status (+ (get-channel-for-midi event) 128)
		       :key (midi-pitch-number event)
		       :velocity (get-velocity-for-midi event)
		       :time (timepoint (cut-off event)))))

(defmethod event-messages ((event standard-percussive-event))
  (list (make-instance 'midi:note-on-message
		       :status 153
		       :key (get-pitch-for-midi event)
		       :velocity (get-velocity-for-midi event)
		       :time (timepoint event))
	(make-instance 'midi:note-off-message
		       :status 137
		       :key (get-pitch-for-midi event)
		       :velocity (get-velocity-for-midi event)
		       :time (timepoint (cut-off event)))))

(defgeneric get-pitch-for-midi (event))
(defgeneric get-velocity-for-midi (event)
  (:method (e) (declare (ignore e)) 100))
(defgeneric get-patch-for-midi (event)
  (:method (e) (declare (ignore e)) 0))
(defgeneric get-channel-for-midi (event)
  (:method (e) (declare (ignore e)) 0))