view utils/harmony/chord-labelling.lisp @ 34:81b4228e26f5

Primarily corrections to large push earlier, including one missed file darcs-hash:20070418135009-f76cc-011412bf4b5a6bb20bd43b41a8a145f69e941926.gz
author David Lewis <d.lewis@gold.ac.uk>
date Wed, 18 Apr 2007 14:50:09 +0100
parents d1010755f507
children 90abdf9adb60
line wrap: on
line source
(in-package #:amuse-harmony)

;; This file contains functions for performing harmonic analysis and
;; chord labelling. At the moment it's quite crude.
;; 
;; Probability can be estimated based on a function that must take a
;; window on the music (i.e. an anchored period and a composition (?
;; or perhaps a 'constituent' in future?). The functionality below is
;; a cut-down version of its predecessors and only models one pitch
;; model, derived by combining dirichlet distributions on the local
;; distribution of pitch-class durations in terms of
;; chord-note:non-chord-note ratios and relative weighting of chord
;; notes.
;;
;; * Chord objects contain details of chord types including the
;; intervals of their constituents and any putative distributional
;; information or note profiles or templates.
;;
;; * Chordset objects gather chord-types together for a given
;; experiment. They have a slot for priors for historical reasons, but
;; at the moment this is unused - I'm using other structures for this.
;;
;; * likelihoods are currently alists with a host of methods. (FIXME:
;;   this doesn't seem very clever)
;;

;; FIXME: this is in the wrong place
(defparameter *keys* (make-array 12 :initial-contents '(:c :c# :d :eb :e :f :f# :g :ab :a :bb :b)))

(defparameter *path-options*
  ;; Each of these is a set of division-of-the-bar options for each
  ;; metrical type.
  ;;
  ;; FIXME: behaviour if the time-signature numerator is absent from
  ;; the alist is undefined.
  '((4 (1 1 1 1) (1 1 2) (1 2 1) (1 3)
     (2 1 1) (2 2) (3 1) (4))
    ;; (4 (1 1 1 1) (1 1 2) (2 1 1) (2 2) (4))
    (2 (1 1) (2))
    (3 (1 1 1) (1 2) (2 1) (3))
    (6 (3 3) (6))
    (5 (1 1 1 1 1) (1 1 1 2) (1 1 2 1) (1 1 3)
     (1 2 1 1) (1 2 2) (1 3 1) (1 4)
     (2 1 1 1) (2 1 2) (2 2 1) (2 3)
     (3 1 1) (3 2) (4 1) (5))
    (9 (3 3 3) (3 6) (6 3) (9))
    (12 (3 3 3 3) (3 3 6) (3 6 3) (3 9)
     (6 3 3) (6 6) (9 3) (12))))

#+nil
(defparameter *default-models* '(:constant-prior :gamma))
#+nil
(defparameter *default-models* '(:scaled-prior :gamma :naive-bass))
;; #+nil
(defparameter *default-models* '(:scaled-prior :gamma))
#+nil
(defparameter *default-models* '(:scaled-prior :gamma :metrical-prior))

;;; ACCESSORS
;; Nearly empty now. And not much point in what's left
(defgeneric normalised-distribution (chord &optional total))
(defmethod normalised-distribution ((chord chord) &optional (total 1))
  ;; normalised distributions will be reused, so it makes sense to
  ;; store them.
  ;; FIXME: Are these ever going to be useful again?
  (unless (assoc total (slot-value chord 'normalised-distribution))
    (setf (slot-value chord 'normalised-distribution)
	  (acons total (normalise-vector (slot-value chord 'distribution) total)
		 (slot-value chord 'normalised-distribution))))
  (cdr (assoc total (slot-value chord 'normalised-distribution))))

;;; Object definitions

#+nil
(defparameter *major-ratios* (mapcar (lambda (x) (/ x 217))
				     '(180 1 1 1 20 1 1 8 1 1 1 1)))
#+nil
(defparameter *major-ratios* (mapcar (lambda (x) (/ x 20))
				     '(5 1 2 1 3 1 1 2 1 1 1 1)))
(defparameter *major-ratios* #(0.72 0.02 0.02 0.02 0.08 0.02 0.02 0.02 0.02 0.02 0.02 0.02))
#+nil
(defparameter *minor-ratios* (mapcar (lambda (x) (/ x 302))
				     '(280 1 1 4 1 1 1 9 1 1 1 1)))
(defparameter *minor-ratios* #(0.78 0.02 0.02 0.02 0.02 0.02 0.02 0.02 0.02 0.02 0.02 0.02))
#+nil
(defparameter *minor-ratios* (mapcar (lambda (x) (/ x 20))
				     '(7 1 2 2 1 1 1 1 1 1 1 1)))
#+nil
(defparameter *sus-ratios* (mapcar (lambda (x) (/ x 21))
				     '(10 1 1 1 1 1 1 1 1 1 1 1)))
(defparameter *sus-ratios* #(0.78 0.02 0.02 0.02 0.02 0.02 0.02 0.02 0.02 0.02 0.02 0.02))

(defparameter *dim-ratios* (copy-seq *sus-ratios*))
(defparameter *aug-ratios* (copy-seq *sus-ratios*))

;; Chords
(defparameter *major-chord*
  (make-instance 'chord
		 :label :major
		 :notes '(0 4 7)
		 :bass-likelihoods (make-array 12
					   :initial-contents *major-ratios*)
		 :min-distribution (make-array 4 :initial-contents '(35 25 20 20))
		 :distribution (make-array 12
					   :initial-contents '(6 1 2 1 5 2 1 5 1 2 2 2))))

(defparameter *minor-chord*
  (make-instance 'chord :label :minor :notes '(0 3 7)
		 :min-distribution (make-array 4 :initial-contents '(35 25 20 20))
		 :bass-likelihoods (make-array 12
					   :initial-contents *minor-ratios*)
		 :distribution (make-array 12
					   :initial-contents '(6 1 2 5 1 2 1 5 2 1 2 1))))

(defparameter *diminished-chord*
  (make-instance 'chord :label :dim :notes '(0 3 6)
		 :min-distribution (make-array 4 :initial-contents '(35 25 20 20))
		 :bass-likelihoods (make-array 12
					   :initial-contents *dim-ratios*)
		 :distribution (make-array 12
					   :initial-contents '(6 1 1 5 1 1 5 1 1 4 1 1))))

(defparameter *diminished-chord-short*
  (make-instance 'chord :label :dim :notes '(0 3 6 9)
		 :min-distribution (make-array 4 :initial-contents '(35 25 20 20))
		 :distribution (make-array 3
					   :initial-contents '(6 1 1))))

(defparameter *augmented-chord*
  (make-instance 'chord :label :aug :notes '(0 4 8) :min-distribution #(35 25 20 20)
		 :bass-likelihoods (make-array 12
					   :initial-contents *aug-ratios*)
		 :distribution (make-array 12
					   :initial-contents '(6 1 1 1 5 1 1 1 5 1 1 1))))

(defparameter *augmented-chord-short*
  (make-instance 'chord :label :aug :notes '(0 4 8)
		 :distribution (make-array 4
					   :initial-contents '(6 1 1 1))))

(defparameter *suspended4th-chord*
  (make-instance 'chord :label :sus4 :notes '(0 5 7)
		 :min-distribution (make-array 4 :initial-contents '(35 25 20 20))
		 :bass-likelihoods (make-array 12
					   :initial-contents *sus-ratios*)
		 :distribution (make-array 12
					   :initial-contents '(6 1 2 3 3 5 1 5 1 1 2 2))))

(defparameter *suspended9th-chord*
  (make-instance 'chord :label :sus9 :notes '(0 2 7)
		 :min-distribution (make-array 4 :initial-contents '(35 25 20 20))
		 :bass-likelihoods (make-array 12
					   :initial-contents *sus-ratios*)
		 :distribution (make-array 12
					   :initial-contents '(6 1 5 2 2 2 1 5 1 2 2 1))))

;; CHORDSETS
(defparameter *full-set*
  (make-instance 'chordset :chords (list *major-chord* *minor-chord*
				     *diminished-chord* *augmented-chord*
				     *suspended4th-chord* *suspended9th-chord*)))

(defparameter *full-set-variable-length*
  (make-instance 'chordset :chords (list *major-chord* *minor-chord*
					 *diminished-chord-short* *augmented-chord-short*
					 *suspended4th-chord* *suspended9th-chord*)))

(defparameter *partial-set*
  (make-instance 'chordset :chords (list *major-chord* *minor-chord*
				     *diminished-chord* *augmented-chord*)))

(defparameter *partial-set-variable-length*
  (make-instance 'chordset
		 :chords (list *major-chord* *minor-chord*
			       *diminished-chord-short*
			       *augmented-chord-short*)))

(defparameter *minimal-set*
  (make-instance 'chordset :chords (list *major-chord* *minor-chord*)))

#+nil
(defparameter *chord-proportions* ;; guess
  (list (cons *major-chord* 17/30) (cons *minor-chord* 10/30)
	(cons *diminished-chord* 1/60) (cons *augmented-chord* 1/60)
	(cons *suspended4th-chord* 1/30) (cons *suspended9th-chord* 1/30)))

#+nil
(defparameter *chord-proportions* ;; another guess
  (list (cons *major-chord* 1/3) (cons *minor-chord* 1/3)
	(cons *diminished-chord* 1/30) (cons *augmented-chord* 1/60)
	(cons *suspended4th-chord* 1/5) (cons *suspended9th-chord* 1/12)))
#+nil
(defparameter *chord-proportions* ;; flat
  (list (cons *major-chord* 1/6) (cons *minor-chord* 1/6)
	(cons *diminished-chord* 1/6) (cons *augmented-chord* 1/6)
	(cons *suspended4th-chord* 1/6) (cons *suspended9th-chord* 1/6)))

(defparameter *chord-proportions*
  ;; observed
  ;; FIXME: This seriously impairs dim and aug. Do they ever get
  ;; diagnosed now?
  (list (cons *major-chord* 546/917) (cons *minor-chord* 312/917)
	(cons *diminished-chord* 2/917) (cons *augmented-chord* 1/917)
	(cons *suspended4th-chord* 44/917) (cons *suspended9th-chord* 12/917)))


;; First steps to chord labelling
(defun get-chord-likelihoods-for-model (anchored-period music
					&key (model :gamma)
					(chordset *full-set*))
  ;; Currently expects and returns an alist of (identifier
  ;; . likelihood) (unnormalised, but can use normalise-likelihoods)
  (ecase model
    (:constant-prior 
     ;; results are divided by number of chords in chordset (times 12)
     (constant-prior-likelihoods anchored-period
				 music
				 chordset))
    (:scaled-prior
     ;; results are divided by preset chord weightings (times 12)
     (scaled-prior-likelihoods anchored-period
			       music
			       chordset))
    (:naive-bass
     (naive-bass-prior-likelihoods anchored-period
				   music
				   chordset))
    (:metrical-prior
     (metrical-prior-likelihoods anchored-period
				 music
				 chordset))
    (:gamma
     ;; dirichlet-based likelihood calculation
     (3ple-gamma-likelihoods anchored-period
			     music
			     chordset))))

;; LIKELIHOOD-CALCULATION FUNCTIONS
(defgeneric metrical-prior-likelihoods  (anchored-period music chordset))
(defmethod metrical-prior-likelihoods ((anchored-period anchored-period)
				       music chordset)
  (let* ((metrical-level (metrical-level-for-likelihood anchored-period music))
	 (p (if (= metrical-level 1)
		0.51
		0.07)))
    (loop for chord in (chords chordset)
       nconc (loop for i from 0 to 11
		  collect (cons (list i chord)
				(/ p (* 12 (length (chords chordset)))))))))

(defgeneric constant-prior-likelihoods  (anchored-period music chordset))
(defmethod constant-prior-likelihoods ((anchored-period anchored-period)
				       music chordset)
  ;; returns a flat distribution totalling 1
  (loop for chord in (chords chordset)
     nconc (loop for i from 0 to 11
	      collect (cons (list i chord)
			    (/ 1 (* 12 (length
					(chords chordset))))))))

(defgeneric scaled-prior-likelihoods (anchored-period music chordset &key prior-alist))
(defmethod scaled-prior-likelihoods ((anchored-period anchored-period) music chordset
				     &key (prior-alist *chord-proportions*))
  ;; returns a distribution based on the relative likelihood of chord types
  (loop for chord in (chords chordset)
     nconc (loop for i from 0 to 11
	      collect (cons (list i chord)
			    (/ (cdr (assoc chord prior-alist))
			       12)))))

(defgeneric naive-bass-prior-likelihoods (anchored-period music chordset))
(defmethod naive-bass-prior-likelihoods ((anchored-period anchored-period) music chordset)
  (let ((pc (bass-note anchored-period music)))
    (loop for chord in (chords chordset)
       nconc (loop for i from 0 to 11
		collect (cons (list i chord)
			      (/ (aref (bass-likelihoods chord)
				       (mod (+ i pc) 12))
				 (length (chords chordset))))))))

(defgeneric 3ple-gamma-likelihoods (anchored-period music chordset))
(defmethod 3ple-gamma-likelihoods ((anchored-period anchored-period) music chordset)
  ;; Ask Christophe about what this one does - this function just
  ;; provides data to his dirichlet likelihood functions. Currently
  ;; limited to triads, this has two distributions for relative
  ;; strengths of chord notes and for the relation between chord and
  ;; non-chord notes.
  (let ((pitch-classes (normalised-pitch-class-distribution anchored-period music))
	(metrical-level (metrical-level-for-likelihood anchored-period music))
	(likelihoods))
    (dolist (chord (chords chordset) likelihoods)
      (let ((chord-likelihoods (subseq (min-distribution chord) 0 3))
	    (non-chord (aref (min-distribution chord) 3)))
	(loop for offset from 0 to 11
	   do (setf likelihoods
		    (set-likelihood
		     likelihoods chord offset
		     (3ple-likelihood (chromatic-rotate pitch-classes (- offset))
				      chord-likelihoods
				      non-chord
				      (main-notes chord)
				      metrical-level 1
				      (get-alphas chord metrical-level :version :learned)
				      (get-betas chord metrical-level :version :learned)))))))))

#+nil
(defun get-alphas (chord metrical-level &key (version :map))
  ;; MP values, map commented
  (cond 
    ((or (eq *major-chord* chord)
	 (eq *minor-chord* chord))
     (cond 
       ((< metrical-level 1)
	(case version
	  (:map #(3.7812 2.4955 2.1525))
	  (:ml #(4.0398 2.6624 2.2942))
	  (:learned #(2.0475 1.365 1.1374999))))
       (t
	(case version
	  (:map #(3.6626 1.5234 2.3395))
	  (:ml #(3.9119 1.6193 2.4955))
	  (:learned #(2.0475 1.365 1.1374999))))))
    (t 
     (case version
       (:map #(3.5110 2.0252 1.2963))
       (:ml #(4.0822 2.3459 1.4874))
       (:learned #(2.0475 1.365 1.1374999))))))

;; New, corrected ground truth
(defun get-alphas (chord metrical-level &key (version :map))
  ;; MP values, map commented
  (cond 
    ((or (eq *major-chord* chord)
	 (eq *minor-chord* chord))
     (cond 
       ((< metrical-level 1)
	(case version
	  (:map #(3.7397 2.4923 2.0187))
	  (:ml #(3.9434 2.6253 2.1239))
	  (:learned #(2.0475 1.365 1.1374999))))
       (t
	(case version
	  (:map #(3.2620 1.3882 2.2542))
	  (:ml #(3.5200 1.4889 2.4293))
	  (:learned #(2.0475 1.365 1.1374999))))))
    (t 
     (case version
       (:map #(3.1963 1.8187 1.3340))
       (:ml #(3.6371 2.0621 1.2799))
       (:learned #(2.0475 1.365 1.1374999))))))

#+nil
(defun get-betas (chord metrical-level &key (version :map))
  (cond 
    ((eq version :learned)
     (cond
       ((> metrical-level 1/2)
	#(0.97 12))
       ((= metrical-level 1/2)
	#(0.97 6))
       (t #(0.97 4))))
    ((or (eq *major-chord* chord)
	 (eq *minor-chord* chord))
     (if (< metrical-level 1)
	 (if (eq version :map)
	     #(0.6987 3.1724)
	     #(0.7164 3.2640))
	 (if (eq version :map)
	     #(1.3677 5.9215)
	     #(1.4454 6.2843))))
    (t 
     (if (eq version :map)
	 #(0.9358 5.2212)
	 #(1.0431 5.8530)))))

;; With new, corrected ground truth
(defun get-betas (chord metrical-level &key (version :map))
  (cond 
    ((eq version :learned)
     (cond
       ((> metrical-level 1/2)
	#(0.97 12))
       ((= metrical-level 1/2)
	#(0.97 6))
       (t #(0.97 4))))
    ((or (eq *major-chord* chord)
	 (eq *minor-chord* chord))
     (if (< metrical-level 1)
	 (if (eq version :map)
	     #(0.7041 3.3448)
	     #(0.7190 3.4260))
	 (if (eq version :map)
	     #(1.3838 6.4581)
	     #(1.4872 6.9785))))
    (t 
     (if (eq version :map)
	 #(0.9558 5.0847)
	 #(1.0551 5.6740)))))


(defun chromatic-rotate (vector offset)
  ;; transpose an n-member (chromatic) vector by an integral number of
  ;; steps (semitones)
  (let* ((size (length vector))
	 (result (make-array size)))
    (dotimes (i size result)
      (setf (aref result i) (aref vector (mod (- i offset) size))))))

(defgeneric metrical-level-for-likelihood (anchored-period music))
(defmethod metrical-level-for-likelihood (anchored-period (music composition))
  ;; metrical level is a function of time signature and window size
  ;; and is used to modify the gamma function.
  (let ((time-sigs (get-applicable-time-signatures anchored-period music)))
    (cond
      ((= (length time-sigs) 1)
       (/ (duration anchored-period)
	  (crotchets-in-a-bar (first time-sigs))))
      ((null time-sigs)
       ;; If, for some reason, we have no time-signature, midi specs
       ;; say assume 4/4.
       (/ (duration anchored-period) 4))
      (t
       (loop for sig in time-sigs
	  sum (/ (duration (period-intersection sig
						anchored-period))
		 (crotchets-in-a-bar sig)))))))

;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Hypothesis comparison / level navigation
;;

(defun chord-labels (anchored-period music
		     &key (chordset *full-set*)
		     (models *default-models*))
  (let ((harmonic-analysis (best-level anchored-period music :chordset chordset :models models))
	(best-likelihood) (chord-labels))
    (do ((path (first harmonic-analysis) (cdr path))
	 (likelihoods (second harmonic-analysis) (cdr likelihoods)))
	((null path) (reverse chord-labels))
      (dolist (likelihood (car likelihoods))
	(when (or (null best-likelihood)
		  (> (likelihood-likelihood likelihood)
		     (likelihood-likelihood best-likelihood)))
	  (setf best-likelihood likelihood)))
      (push (cons (first path) (likelihood-chord best-likelihood)) chord-labels)
      (setf best-likelihood nil))))

(defun best-level (anchored-period music
		   &key (chordset *full-set*)
		   (models *default-models*))
  ;; Takes a period for the largest time-unit being considered and
  ;; returns the highest probability subdivision, its likelihood
  ;; values (and the probability of that subdivision, but that's a bit
  ;; of a coincidence and may want not to happen)
  (best-level-hypothesis (make-metrical-divisions anchored-period music)
			 music :chordset chordset :models models))

(defgeneric make-metrical-divisions (anchored-period music))
(defmethod make-metrical-divisions ((anchored-period anchored-period)
				    (music composition))
  ;; Prepares a set of divisions of the period based on time-sig and a
  ;; pre-set list of options for each possible time-sig numerator.
  (let ((time-sigs (get-applicable-time-signatures anchored-period music)))
    (if
     (< (length time-sigs) 2)
     (let ((candidates))
       ;; get an appropriate set of divisions. Not sure this is right
       ;; - it relies on bar position being irrelevant. Is this true?
       ;; This isn't really clear from this code, but if there are no
       ;; time-signatures, make-divisions-with-timesigs has a test for
       ;; it and will pretend it's 4/4.
       (dolist (divisions (make-divisions-with-time-signature anchored-period (car time-sigs))
		candidates)
	 (do ((time (onset anchored-period) (cut-off (car candidate-set)))
	      (divisions divisions (cdr divisions))
	      (candidate-set))
	     ((null divisions) (push (reverse candidate-set) candidates))
	   (push (make-anchored-period (timepoint time) (first divisions))
		 candidate-set))))
     ;; otherwise, there are lots. Run this function once for each
     ;; time-signature.
     (loop for time-sig in time-sigs
	  nconc (make-metrical-divisions (period-intersection anchored-period time-sig)
					   music)))))

(defgeneric make-divisions-with-time-signature (period time-signature))
(defmethod make-divisions-with-time-signature ((period period-designator)
					       (time-signature basic-time-signature))
  (let* ((numerator (time-signature-numerator time-signature))
	 (denominator (time-signature-denominator time-signature))
	 (path-options (cdr (assoc numerator *path-options*))))
    (loop for divisions in path-options
       collect (period-fill period divisions denominator))))

(defmethod make-divisions-with-time-signature ((period period-designator)
					       time-signature)
  ;; not a known time-signature type. Assume 4/4
  (let ((path-options (cdr (assoc 4 *path-options*))))
    (loop for divisions in path-options
       collect (period-fill period divisions 4))))
    
(defun period-fill (period path-options denominator)
  ;; take a division of the ?bar and then repeat it until the period
  ;; is filled.
  ;;
  ;; Perhaps this and surrounding function need to make more use of
  ;; time interface?
  (let ((duration-list)
	;; Multiply path-options by unit of meter.
	(path-options (map 'list
			   #'(lambda (x) (* x (/ 4 denominator)))
			   path-options)))
    (do* ((circular-path path-options (or (cdr circular-path)
					  path-options))
	  (current-duration (car circular-path) (car circular-path))
	  (prev-remaining (duration period) remaining)
	  (remaining (- (duration period) current-duration) (- remaining current-duration)))
	 ((<= remaining 0) (reverse (cons prev-remaining duration-list)))
      (push current-duration duration-list))))

(defun best-level-hypothesis (division-hypotheses music
			      &key (chordset *full-set*)
			      (models *default-models*))
  ;; Rather messy wrapper for level-hypothesis-likelihoods. Should
  ;; probably make this a structure or something, but use looks like
  ;; being quite limited. might revisit.
  (first (sort (level-hypothesis-likelihoods division-hypotheses
					     music
					     :chordset chordset
					     :models models)
	       #'> :key #'third)))

(defun level-hypothesis-likelihoods (division-hypotheses music
				     &key (chordset *full-set*)
				     (models *default-models*))
  ;; This function takes the candidate windows being considered (as
  ;; lists of anchored periods) and, for each, works out likelihoods
  ;; and the most probable hypothesis. This should come from taking
  ;; the likelihoods and dividing by the product of the internal sums
  ;; (don't ask!)
  (let ((hypothesis-likelihoods))
    (dolist (hypothesis division-hypotheses hypothesis-likelihoods)
      (let ((likelihoods (map 'list
			      #'(lambda (x)
				  (get-chord-likelihoods x music models chordset))
			      hypothesis)))
	(push (list hypothesis likelihoods (combined-likelihoods-sum likelihoods))
	      hypothesis-likelihoods)))))

(defun get-chord-likelihoods (anchored-period music models chordset)
  (let ((model-likelihoods
	 (loop for model in models
	    collect (get-chord-likelihoods-for-model anchored-period
						     music
						     :model model
						     :chordset chordset))))
    (combine-multimodel-likelihoods model-likelihoods)))

(defun combine-multimodel-likelihoods (likelihoods-list)
  (cond
    ((= (length likelihoods-list) 1)
     (car likelihoods-list))
    (t
     (let ((combined-likelihoods))
       (dolist (reference-likelihood (car likelihoods-list) combined-likelihoods)
	 (setf combined-likelihoods
	       (set-likelihood combined-likelihoods
			       (likelihood-chordtype reference-likelihood)
			       (likelihood-pitch-class reference-likelihood)
			       (apply #'* (loop for model-likelihoods in likelihoods-list
					     collect (likelihood-likelihood
						      (assoc (car reference-likelihood)
							     model-likelihoods
							     :test #'equal)))))))))))

;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Likelihood (structure) manipulation and access methods
;;

(defgeneric set-likelihood (likelihoods offset chord likelihood))
(defmethod set-likelihood ((likelihoods list) chord offset likelihood)
  (acons (list offset chord) likelihood likelihoods))

(defgeneric get-likelihood (likelihoods offset chord))
(defmethod get-likelihood ((likelihoods list) offset chord)
  (assoc (list offset chord) likelihoods :test #'equal))

(defgeneric best-n-likelihoods (n likelihoods))
(defmethod best-n-likelihoods (n (likelihoods list))
  (let ((ranked (ordered-likelihoods likelihoods)))
    (subseq ranked 0 n)))

(defgeneric ordered-likelihoods (likelihoods))
(defmethod ordered-likelihoods ((likelihoods list))
  (sort (copy-seq likelihoods) #'> :key #'cdr))

(defgeneric pretty-display-likelihoods (likelihoods))
(defmethod pretty-display-likelihoods ((likelihoods list))
  (dolist (p likelihoods)
    (format *standard-output* "~%~A~C~A~C~A"
	    (likelihood-key p) #\Tab
	    (chord-label (likelihood-chordtype p)) #\Tab
	    (likelihood-likelihood p))))

(defgeneric likelihood-key (likelihood))
(defmethod likelihood-key ((likelihood list))
  (aref *keys* (first (first likelihood))))

(defgeneric likelihood-pitch-class (likelihood))
(defmethod likelihood-pitch-class ((likelihood list))
  (first (first likelihood)))

(defgeneric likelihood-chordtype (likelihood))
(defmethod likelihood-chordtype ((likelihood list))
  (second (first likelihood)))

(defgeneric likelihood-chord (likelihood))
(defmethod likelihood-chord ((likelihood list))
  (first likelihood))

(defgeneric likelihood-likelihood (likelihood))
(defmethod likelihood-likelihood ((likelihood list))
  (cdr likelihood))

(defgeneric likelihoods-sum (likelihoods))
(defmethod likelihoods-sum ((likelihoods list))
  (loop for likelihood in likelihoods
     sum (likelihood-likelihood likelihood)))

(defgeneric combined-likelihoods-sum (combined-likelihoods))
(defmethod combined-likelihoods-sum ((combined-likelihoods list))
  ;; Will be needed for hypothesis comparison - sums the likelihoods
  ;; for all chords within a window for multiple likelihood
  ;; calculations
  (apply #'* (map 'list #'(lambda (window)
			    (loop for likelihood in window
			       sum (likelihood-likelihood likelihood)))
		  combined-likelihoods)))

(defgeneric normalise-likelihoods (likelihoods))
(defmethod normalise-likelihoods ((likelihoods list))
  (let ((p-sum (sum-likelihoods likelihoods)))
    (if (= p-sum 1) 
	likelihoods
	(scale-likelihoods likelihoods (/ 1 p-sum)))))

(defgeneric scale-likelihoods (likelihoods scale-factor))
(defmethod scale-likelihoods ((likelihoods list) (scale-factor number))
  (map 'list #'(lambda (x)
		 (cons (first x)
		       (* (cdr x) scale-factor)))
       likelihoods))

(defgeneric sum-likelihoods (likelihoods))
(defmethod sum-likelihoods ((likelihoods list))
  (apply #'+ (map 'list #'cdr likelihoods)))

;; Probably useless vestigial stuff from here

(defun vector-list-apply (predicate vector-list &optional other-args)
  (let ((result-list))
    (dolist (vector vector-list (reverse result-list))
      (push (make-array (array-dimensions vector)) result-list)
      (loop for i from 0 to (1- (length vector))
	 do (setf (aref (first result-list) i)
		  (apply predicate (cons (aref vector i) other-args)))))))

(defun vector-sum (vector)
  (loop for i from 0 to (1- (length vector))
     sum (aref vector i)))

(defun make-flat-result (chordset)
  (map 'list #'(lambda (x)
		 (make-array (length (distribution x))
			     :initial-element 0))
       (chords chordset)))

(defun key-name (pitch-class)
  (if pitch-class
      (aref #("C" "C#" "D" "Eb" "E" "F" "F#" "G" "G#" "A" "Bb" "B") (mod pitch-class 12))
      nil))