view utils/utils.lisp @ 61:c911d65ae94d

pointy-clicky misspelling correction darcs-hash:20070627102839-dc3a5-16cb5156dafee4b4a1aa3442274fab6c7260ed9c.gz
author c.rhodes <c.rhodes@gold.ac.uk>
date Wed, 27 Jun 2007 11:28:39 +0100
parents 13033824fa7d
children 32314fefc706
line wrap: on
line source
;;; General purpose utilities 

(cl:in-package #:amuse-utils) 

;; Booleans (for filters)
(defgeneric pitchedp (event)
  (:method (e) (declare (ignore e)) nil))
(defmethod pitchedp ((event amuse:pitched-event))
  T)
(defgeneric unpitchedp (event)
  (:method (e) (not (pitchedp e))))

;; Rhythm methods
(defgeneric crotchets-in-a-bar (time-signature))
(defmethod crotchets-in-a-bar ((time-signature basic-time-signature))
  (let ((num (time-signature-numerator time-signature))
	(den (time-signature-denominator time-signature)))
    (* num (/ 4 den))))

(defgeneric beats-to-seconds (object1 object2))
(defmethod beats-to-seconds ((object1 anchored-period)
			     (object2 constituent))
  (let ((tempi (get-applicable-tempi object1 object2))
	(s 0))
    (dolist (tempo tempi (/ s 1000000))
      (incf s (* (duration (period-intersection tempo object1))
		 (amuse:microseconds-per-crotchet tempo))))))
(defmethod beats-to-seconds ((object1 moment)
			     (object2 constituent))
  (beats-to-seconds (make-anchored-period 0
					  (timepoint object1))
		    object2))
					  

;; Pitch methods

(defgeneric sounding-events (anchored-period sequence))
(defmethod sounding-events ((anchored-period anchored-period)
			    (sequence composition))
  (let ((sounding))
    (sequence:dosequence (event sequence (reverse sounding))
      (cond
	((time>= event (cut-off anchored-period))
	 (return-from sounding-events (reverse sounding)))
	((period-intersection anchored-period event)
	 (push event sounding))))))

(defgeneric midi-pitch-distribution (anchored-period composition))
(defmethod midi-pitch-distribution ((anchored-period anchored-period)
				    composition)
  (let ((pitches (make-array 128 :initial-element 0)))
    (dolist (event (remove-if #'unpitchedp (sounding-events anchored-period composition)) pitches)
      (let ((overlap (period-intersection anchored-period event)))
	(if overlap
	    (incf (aref pitches (midi-pitch-number event))
		  (duration overlap))
	    (if (= (duration event) 0)
		(format t "~%Note ~D beats in has no duration" (timepoint event))
		(error "This function has gone wrong - looking for overlaps that don't exist")))))))

(defgeneric pitch-class-distribution (anchored-period composition))
(defmethod pitch-class-distribution ((anchored-period anchored-period)
				     composition)
  (let ((pitches (make-array 12 :initial-element 0)))
    (dolist (event (remove-if #'unpitchedp (sounding-events anchored-period composition)) pitches)
      (let ((overlap (period-intersection anchored-period event)))
	(if overlap
	    (incf (aref pitches (pitch-class event))
		  (duration overlap))
	    (if (= (duration event) 0)
		(format t "~%Note ~D beats in has no duration" (timepoint event))
		(error "This function has gone wrong - looking for overlaps that don't exist")))))))

(defun normalised-midi-pitch-distribution (object1 object2)
  (normalise-vector (midi-pitch-distribution object1 object2)))
(defun normalised-pitch-class-distribution (object1 object2)
  (normalise-vector (pitch-class-distribution object1 object2)))
(defun normalise-vector (vector &optional (target-sum 1))
  (let ((total (loop for i from 0 to (1- (length vector))
		  sum (aref vector i))))
    (cond
      ((= total target-sum)
       vector)
      ((= total 0)
       (make-array (length vector)
		   :initial-element (/ target-sum (length vector))))
      (t
       (map 'vector #'(lambda (x) (* x (/ target-sum total))) vector)))))

;; Not as simple as it seems - have to take into account numbering
;; practices and leading silences in representations where bar number
;; isn't part of the explicit structure.
(defgeneric bar-number (moment composition))
(defgeneric bar-onset (bar-number composition))

(defgeneric bass-note (anchored-period composition))

(defun vector-correlation (vector1 vector2)
  ;; useful for Krumhansl-Schmukler-like calculations
  (assert (= (length vector1) (length vector2)))
  (let* ((n (length vector1))
	 (sum-x (loop for i from 0 to (1- n)
		      sum (aref vector1 i)))
	 (sum-y (loop for i from 0 to (1- n)
		      sum (aref vector2 i)))
	 (equation-bl (sqrt (- (* n
				  (loop for i from 0 to (1- n)
					sum (expt (aref vector1 i) 2)))				  
			       (expt sum-x 2))))
	 (equation-br (sqrt (- (* n
				  (loop for i from 0 to (1- n)
					sum (expt (aref vector2 i) 2)))
			       (expt sum-y 2))))
	 (equation-b (* equation-br equation-bl))
	 (equation-tr (* sum-x sum-y))
	 (equation-t 0)
	 (results-array (make-array n)))
    (if (= equation-b 0)
	(make-array 12 :initial-element 0)
	(do ((i 0 (1+ i)))
	    ((= i n) results-array)
	  (setf equation-t (- (* n (loop for j from 0 to (1- n)
					 sum (* (aref vector1 (mod (+ i j) n))
						(aref vector2 j))))
			      equation-tr)
		(aref results-array i) (/ equation-t equation-b))))))


(defparameter *krumhansl-schmuckler-minor-key*
  (make-array 12 :initial-contents '(6.33 2.68 3.52 5.38 2.6 3.53 2.54 4.75 3.98 2.69 3.34 3.17)))

(defparameter *krumhansl-schmuckler-major-key*
  (make-array 12 :initial-contents '(6.35 2.23 3.48 2.33 4.38 4.09 2.52 5.19 2.39 3.66 2.29 2.88)))

(defun krumhansl-key-finder (anchored-period composition
			     &key (major *krumhansl-schmuckler-major-key*)
			     (minor *krumhansl-schmuckler-minor-key*))
  (let* ((key) (best-score -1)
	 (pitches (pitch-class-distribution anchored-period composition))
	 (majors (vector-correlation pitches major))
	 (minors (vector-correlation pitches minor)))
    (loop for i from 0 to 11
	  do (when (> (aref majors i) best-score)
	       (setf key (list i :major)
		     best-score (aref majors i))))
    (loop for i from 0 to 11
	  do (when (> (aref minors i) best-score)
	       (setf key (list i :minor)
		     best-score (aref minors i))))
    (values key (key->midi-key-signature key anchored-period))))

(defvar *line-of-fifths* (list 1 8 3 10 5 0 7 2 9 4 11 6))

(defun key->midi-key-signature (key anchored-period)
  (let* ((tonic (car key))
         (mode (cadr key))
         (sharps (- (ecase mode
                      (:major (position tonic *line-of-fifths*))
                      (:minor (position (mod (- tonic 9) 12) *line-of-fifths*)))
                    5))
         (mode (ecase mode (:major 0) (:minor 9))))
    (amuse:make-midi-key-signature sharps mode 
                                   (timepoint anchored-period) 
                                   (duration anchored-period))))

(defun levenshtein-distance (s1 s2 &key (insertion-cost 1)
			  (insertion-function) (deletion-cost 1)
			  (deletion-function) (substitution-cost 1)
			  (substitution-test #'equal) (substitution-function))
  ;; This is an implementation of the Levenshtein distance measure
  ;; based on the cliki asdf package, itself based on the wikipedia
  ;; scheme example of the same algorithm. This version is generalised
  ;; such that operations costs may take constant or calculated
  ;; values. If insertion-function, deletion-function or
  ;; substitution-test are specified, the applicable cost values are
  ;; ignored and the function output is used instead.
  (let* ((width (1+ (length s1)))
	 (height (1+ (length s2)))
	 (d (make-array (list height width))))
    (dotimes (x width)
      (setf (aref d 0 x) (* x deletion-cost)))
    (dotimes (y height)
      (setf (aref d y 0) (* y insertion-cost)))
    (dotimes (x (length s1))
      (dotimes (y (length s2))
	(setf (aref d (1+ y) (1+ x))
	      (min (+ (if insertion-function
			  (apply insertion-function (elt s1 x))
			  insertion-cost)
		      (aref d y (1+ x)))
		   (+ (if deletion-function
			  (apply deletion-function (elt s2 y))
			  deletion-cost)
		      (aref d (1+ y) x))
		   (+ (aref d y x)
		      (if substitution-function
			  (apply substitution-function (list (elt s1 x) (elt s2 y)))
			  (if (apply substitution-test (list (elt s1 x) (elt s2 y)))
			      0
			      substitution-cost)))))))
    (aref d (1- height) (1- width))))

;;;;;;;;;;;;;;;;;;;;;;
;;
;; More experimental (from amuse-geerdes)
;;
;; Monody functions

(defun monodificate (composition)
  (let ((events-bags) (latest-cut-off))
  ;; - Filter out very short notes (<50ms) 
  ;; - If there are notes with the same onset time or a large
  ;;   proportion (e.g. >25%) of the notes in the segment have
  ;;   overlapping durations (of >75%), do for every simultaneous or
  ;;   overlapping pair of notes
  ;; -- if one note is louder than the other note (e.g. quieter note
  ;;    <75% of louder one) select it as melody note
  ;; -- else select note with higher pitch
  ;; [FIXME: I'm ignoring overlaps for the time being]
  ;; - For non-simultaneous notes with little overlap, set note ends
  ;;   to beginning of of onset of next (overlapping) note.

  ;; STEP 1: 
  ;; `Filter out very short notes (<50ms)' and find `segments' for
  ;; further filtering.
    (sequence::dosequence (event composition)
      (when (> (beats-to-seconds event composition)
	       1/20)
	(if (or (not latest-cut-off)
		(time> (onset event) latest-cut-off))
	    (push (list event) events-bags)
	    (push event (car events-bags)))
	(when (or (not latest-cut-off)
		  (time> (cut-off event) latest-cut-off))
	  (setf latest-cut-off (cut-off event)))))
    ;; Now check each segment for overlaps and
    ;; simultanaieties. N.B. this is a reverse list of reversed
    ;; lists.
    (let ((adjusted-bags))
      (dolist (events-bag events-bags)
	(setf events-bag (reverse events-bag))
	(let ((polyphonic-p (check-events-bag-for-polyphony events-bag)))
	  (cond
	    (polyphonic-p
	     (push (resolve-polyphony events-bag composition) adjusted-bags))
	    (t
	     (if (cdr events-bag)		   
		 (push (adjust-durations events-bag) adjusted-bags)
		 (push events-bag adjusted-bags))))))
      (apply #'nconc adjusted-bags))))

(defun resolve-polyphony (event-list composition)
  (do ((i 0 (1+ i)))
      ((>= i (length event-list)) event-list)
    (let ((event (nth i event-list)))
      (do ((j (1+ i) (1+ j)))
	  ((or (>= j (length event-list))
	       (time>= (onset (nth j event-list))
		       (cut-off event))))
	(let* ((event-2 (nth j event-list))
	       (inter-onset (time- (onset event-2) (onset event))))
	  (cond
	    ((and inter-onset
		  (< (* 2 (duration inter-onset))
		      (duration event))
		  (< (* 2 (duration inter-onset))
		      (duration event-2))
		  (< (beats-to-seconds inter-onset composition)
		      1/8))
	     ;; This is clearly polyphony
	     (cond
	       ((significantly-louderp event-2 event)
		;; Take event-2
		(setf event-list (remove event event-list))
		(decf i)
		(return))
	       ((significantly-louderp event event-2)
		;; Take event
		(setf event-list (remove event-2 event-list))
		(decf j))
	       ((pitch> event event-2)
		;; Take event
		(setf event-list (remove event-2 event-list))
		(decf j))
	       (t
		;; Take event-2
		(setf event-list (remove event event-list))
		(decf i)
		(return))))
	    (t
	     (cond
	       ((substantially-louderp event-2 event)
		;; Take event-2
		(setf event-list (remove event event-list))
		(decf i)
		(return))
	       ((substantially-louderp event event-2)
		;; Take event
		(setf event-list (remove event-2 event-list))
		(decf j))
	       (t
		;; Take both
		(let ((event-overlap (period-intersection event event-2)))
		  (when event-overlap
		    (setf (duration event)
			  (duration (time- event-overlap event))))))))))))))

(defgeneric significantly-louderp (event1 event2)
  ;; noticably louder
  (:method (e1 e2) (declare (ignore e1 e2)) nil))

(defgeneric substantially-louderp (event1 event2)
  ;; much louder
  (:method (e1 e2) (declare (ignore e1 e2)) nil))

(defun adjust-durations (events-list)
  (do* ((old-list events-list (cdr old-list))
	(event (first old-list) (first old-list))
	(event-2 (second old-list) (second old-list)))
       ((not event-2) events-list)
    (let ((event-overlap (period-intersection event event-2)))
      (when event-overlap
	(setf (duration event)
	      (duration (time- event-overlap event)))))))

(defun check-events-bag-for-polyphony (events-bag)
  (let ((overlaps (make-array (length events-bag) :initial-element nil)))
    (when (= (length events-bag) 1)
      ;; obviously no overlaps
      (return-from check-events-bag-for-polyphony nil))
    (unless (= (length (remove-duplicates events-bag :test #'time=))
	       (length events-bag))
      ;; Duplicated onsets
      (return-from check-events-bag-for-polyphony 'T))
    ;; Now for the main bit
    (do* ((events events-bag (cdr events))
	  (i 0 (1+ i))
	  (event (car events) (car events)))
	 ((null (cdr events)))
      (unless (and (aref overlaps i)
		   (= (aref overlaps i) 1))
	;; Would mean we already have a maximal value
	;; and don't need any more checks
	(do* ((events-2 (cdr events) (cdr events-2))
	      (j (1+ i) (1+ j))
	      (event-2 (car events-2) (car events-2)))
	     ((null events-2))
	  (when (time>= (onset event-2) (cut-off event))
	    ;; So no more overlaps
	    (return))
	  (let ((shorter (if (duration< event event-2)
			     i
			     j))
		(overlap (/ (duration (period-intersection event event-2))
			    (min (duration event) (duration event-2)))))
	    ;; only look at pairings for the shorter note. This can
	    ;; have odd side effects, but means we never
	    ;; under-represent an overlap (I think)
	    (when (or (not (aref overlaps shorter))
		      (>= overlap (aref overlaps shorter)))
	      (setf (aref overlaps shorter) overlap)
	      (when (and (= shorter i)
			 (= overlap 1))
		;; Maximum value - we can stop
		(return)))))))
    (let ((total 0) (overs 0))
      (loop for i from 0 to (1- (length events-bag))
	 do (when (aref overlaps i)
	      (incf total)
	      (when (>= (aref overlaps i) 3/4)
		(incf overs))))
      (if (and (> total 0)
	       (>= (/ overs total)
		   1/4))
	  'T
	  'nil))))

(defgeneric inter-onset-intervals (composition &key rounding-divisor))
(defmethod inter-onset-intervals ((composition composition) &key (rounding-divisor 1/4))
  ;; returns values - list inter-onset intervals in beats, modal i-o-i
  ;; and i-o-is in seconds.
  ;; ** Only makes sense for monodic music
  ;; FIXME: Should this keep in objects or am I right to make numbers
  ;; here?
  ;; FIXME: Should I (do I) filter out 0s?
  (let ((i-o-i-list) (i-o-i-secs-list) (prev)
	(hits (make-array (/ 32 rounding-divisor))))
    (loop for event being the elements of composition
       do (progn
	    (when prev
	      (let* ((i-o-i-period (inter-onset-interval prev event))
		     (i-o-i (duration i-o-i-period))
		     (i-o-i-secs (amuse-utils:beats-to-seconds i-o-i-period composition)))
		(when (= i-o-i-secs 0)
		  (format t "~D, ~D -- " (timepoint prev) (timepoint event)))
		(push i-o-i i-o-i-list)
		(push i-o-i-secs i-o-i-secs-list)
		(when (< i-o-i 32)
		  ;; Not really interested in very long results for the
		  ;; modal value anyway.
		  (incf (aref hits (round i-o-i rounding-divisor))))))
	    (setf prev event)))
    (let ((mode '(0 0)))
      ;; we want the position of the highest mode
      (loop for i downfrom (1- (length hits)) to 0
	 when (> (aref hits i) (car mode))
	 do (setf mode (list (aref hits i) i)))
      (values (reverse i-o-i-list)
	      (* (cadr mode) rounding-divisor)
	      (reverse i-o-i-secs-list)))))

(defun pitch-interval-list (composition)
  (let ((intervals)
	(previous-event))
    (sequence:dosequence (event composition (reverse intervals))
      (when previous-event
	(push (span (pitch- event previous-event))
	      intervals))
      (setf previous-event event))))