I wrote some macros to help write analysis of musical pieces.
Here is an example file (on Bach's Invention I).

Structural analysis is (of course) performed by hand, and displayed on
a Lyrics structure on top of the music. (This part is only a set of
very simple macros).

Harmonic analysis is performed mostly by hand (I also have some code
that does harmonic analysis, but it works mostly on chorales; the code
here only detects octave-drop cadences) and displayed on a Lyrics
structure below the music.

The code also does a bit of motif analysis, which is done
automatically (motives are declared once by hand, then later
occurrences and inversions are identified automatically).

The enclosed files:
  motif.scm  contains most of the parentheses
  bwv772.ly  is the example for Invention I

TThe code compiles with both v2.18 (Debian stable; this is the only
version I have access to on some of my systems) and v2.19.

I am interested in any feedback you would have on this code!

-- 
        Jérôme
; (music-fold-time order f data init music):  f(data, leaf, X, pos)

; General utilities {{{1
(define (print . l) (map display l) (newline) #f)
(define (assert b . l) (or b (apply error l)))
; 
https://stackoverflow.com/questions/108169/how-do-i-take-a-slice-of-a-list-a-sublist-in-scheme
(define (slice l start length) (take (drop l start) length))
(define (insert l position x)
  (append (take l position) (cons x (drop l position))))
; returns interval [start, stop[ with given step
(define (interval-open start step stop) (if (>= start stop) '()
  (cons start (interval-open (+ start step) step stop))))
; returns the last element of a closed list
(define (last l) (car (last-pair l)))
(define (anything->color c) (cond
   ((symbol? c) (x11-color c))
   ((and (number? c) (> c 1)) (map (lambda (x) (/ x 255.)) `(,c ,c ,c)))
   ((number? c) `(,c ,c ,c))
   ((and (list? c) (> (car c) 1) (map (lambda(x) (/ x 255.)) c)))
   (else c)))
; Cosmetic functions {{{1
; Color variant {{{2
(define (color-comp-variant1 t x) (/ (* x t) (+ 1 (* (- t 1) x))))
(define (color-variant c n) (let* (;{{{
  (n (modulo (* 4 n) 7))
  (t `((0 0 1) (1 1 0) (0 1 1) (1 0 1) (0 1 0) (1 0 1) (0 0 0)))
  (d (map (lambda (x) (list-ref `(1.3 .8) x)) (list-ref t n)))
  )
  ; c = RGB color
  ; n = integer 0..6
  (map color-comp-variant1 d c))
);}}}
(define (theme-color-variant c m);{{{
  (color-variant c (ly:pitch-notename (first-note m))));}}}
; hsv->rgb {{{2
(define (hsv->rgb z) (let* (
  (h (modulo (car z) 360)) (s (cadr z)) (v (caddr z))
  (i (floor (/ h 60.)))
  (c (* v s)) (t (/ h 60.)) (hmod2 (- t (* 2 (floor (/ t 2)))))
  (absh (abs (- hmod2 1))) (x (* c (- 1 absh)))
  ) (map (lambda (y) (+ y (- v c))) (cond
    ((<= t 1) `(,c ,x 0))
    ((<= t 2) `(,x ,c 0))
    ((<= t 3) `(0 ,c ,x))
    ((<= t 4) `(0 ,x ,c))
    ((<= t 5) `(,x 0 ,c))
    ((<= t 6) `(,c 0 ,x))))));}}}
; with-background {{{2
; after http://lsr.di.unimi.it/LSR/Snippet?id=969
(define-markup-command (with-background layout props color arg) (color? markup?)
   (let* ((stencil (interpret-markup layout props arg))
          (X-ext (ly:stencil-extent stencil X))
          (Y-ext (ly:stencil-extent stencil Y)))
     (ly:stencil-add (ly:make-stencil
                      (list 'color color
                        (ly:stencil-expr (ly:round-filled-box X-ext Y-ext 0))
                        X-ext Y-ext)) stencil)))
(define mark-below (define-music-function (parser location label) (markup?)
  (make-sequential-music (list
    (prop-override '(Score RehearsalMark extra-offset) '(0 . -8.5) #t)
    (prop-override '(Score RehearsalMark baseline-skip) 9 #t)
    (make-music 'MarkEvent 'label label)))))
(define framed-mark (define-music-function (parser location text1) (markup?)
  (make-sequential-music (list
    (prop-override `(Bottom LyricText self-alignment-X) LEFT)
    (make-music 'MarkEvent
                'label (markup #:line (#:box #:fontsize -3 text1)))))))
(define corner-mark (define-music-function (parser location text2) (markup?)
  (make-music 'MarkEvent 'label (markup #:fontsize -3
    (#:combine (#:path .15 '((lineto 0 2) (lineto 3 2)))
       #:line (" " text2))))))
; General utilities for music {{{1
; Naming convention:
; make-foobar: direct constructor for a foobar
; create-foobar: defines a function which returns a foobar
(define (pitch->int p)
  (assert (ly:pitch? p) "pitch->int: must be a pitch: " p)
  (if (ly:pitch? p)
   (+ (* 7 (ly:pitch-octave p)) (ly:pitch-notename p))))
(define (pitch->semitone p)
  (assert (ly:pitch? p) "pitch->semitone: must be a pitch: " p)
  (if (ly:pitch? p)
  (+ (* 12 (ly:pitch-octave p))
     (list-ref `(0 2 4 5 7 9 11) (ly:pitch-notename p))
     (* 2 (ly:pitch-alteration p)))))
; music-length: duration (as a rational) {{{2
(define moment->rational ly:moment-main)
(define (duration->rational dur) (ly:moment-main (ly:duration-length dur)))
(define (music-length m)
  "Duration of m, as a rational"
  (moment->rational (ly:music-length m)))
(define (rational->duration r)
  (ly:make-duration 0 0 (numerator r) (denominator r)))
;}}}2
; prop-override / prop-revert {{{2
; (inspired by scm/ly-syntax-constructors.scm)
; (why is this not exported?)
; anyway, our version is easier to use
; 2.18 does not have ly:set-origin!
(if (< (cadr (ly:version)) 19)
  (define ly:set-origin! identity))
(define* (prop-override path value #:optional once)
  (ly:set-origin! (context-spec-music (ly:set-origin!
    (make-music 'OverrideProperty
                'symbol (cadr path)
                'grob-property-path (cddr path)
                'once once
                'grob-value value
                'pop-first #t))
    (car path))))

(define (prop-revert path)
  (ly:set-origin! (context-spec-music (ly:set-origin!
    (make-music 'RevertProperty
                'symbol (cadr path)
                'grob-property-path (cddr path)))
    (car path))))
; music-fold-time: fold music expression with user-supplied function {{{2
(define (music-fold-time order f data init music)
"Descend recursively in music.
On leaf nodes, call X ← f(data, leaf, X, pos),
  where · data is the user data
        · X    is initialized as init
        · pos  is the current time position (rational)
Returns the value of X.
On non-leaf nodes, f(data, node, X, pos) is called
 - before the descendants if order is 'pre,
 - after the descendants if order is 'post,
 - otherwise not at all.
"
  (music-fold-time-rec order f data init music 0))

(define (music-fold-time-rec order f data init music start);{{{
"This is the function that does the work,
  passing around $start = current time position (rational)"
  (let* (
    (prop (lambda* (n #:optional o) (ly:music-property music n o)))
    (n (prop 'name))
    (this-node (lambda (value return)
                 (if (eq? order value) (f data music return start) return)))
   )(cond
    ((eq? n 'SequentialMusic)
     (let* (
       (return init)
       (return (this-node 'pre return))
       (return (car
     ; we fold using (X . time) ← g(music, (X . time))
     ; this means that:
     ; g(m, p) = (f(data, m, car p, cdr p), (cdr p)+length(m)
         (fold (lambda (m p)
                  (cons (music-fold-time-rec order f data (car p) m (cdr p))
                        (+ (cdr p) (music-length m))))
                (cons return start)
                (prop 'elements '()))))
       (return (this-node 'post return))
      ) return))
    ((member n '(EventChord SimultaneousMusic))
     ; we fold using X ← g(music, X)
     ; now g(m,X) = f(data, start, music, X)
     (let* (
       (return init)
       (return (this-node 'pre return))
       (return
        (fold (lambda (m x) (music-fold-time-rec order f data x m start))
              return
              (prop 'elements '())))
       (return (this-node 'post return))
     ) return))
    ((prop 'element #f)
     (let* (
       (return init)
       (return (this-node 'pre return))
       (return (music-fold-time-rec order f data return (prop 'element) start))
       (return (this-node 'post return))
      ) return))
    (else (f data music init start))
)));}}}
; music-map-time f data music {{{2
(define (music-map-time! f data music)
  (music-map-time-rec! f data music 0))
(define (music-map-time-rec! f data music start)
  (let* (
    (prop (lambda* (n #:optional o) (ly:music-property music n o)))
    (n (prop 'name))
   )(cond
    ((eq? n 'SequentialMusic)
     ; we fold using (return' . time') ← g(music, return . time)
     ; so that g(m, p) = ((f(m) . (car p)), (cdr p) + (length m))
     (ly:music-set-property! music 'elements
      (car (fold (lambda (m p)
                   (cons
                    (append (car p)
                            (list (music-map-time-rec! f data m (cdr p))))
                    (+ (cdr p) (music-length m))))
                 (cons '() start) (prop 'elements '()))))
     music)
    ((eq? n 'SimultaneousMusic)
     (ly:music-set-property! music 'elements
      (map (lambda (m) (music-map-time-rec! f data m start))
           (prop 'elements '())))
     music)
    ((prop 'element)
     (ly:music-set-property! music 'element
        (music-map-time-rec! f data (prop 'element) start))
     music)
    (else (f data music start))
)));}}}
; flatten-music: returns a list (time . (list of all pitches)) {{{2
; this list is sorted by time,
; and the value for each time is sorted by pitch (high to low)
; FIXME: find some way to incorporate rests in there
(define (flatten-music music)
  (sort
    ; we first sort the pitches at each time,
    (map (lambda (p)
           (cons (car p)
                 (sort (cdr p) (lambda (p1 p2) (ly:pitch<? p2 p1)))))
         (flatten-music-raw music))
    ; and then sort the whole list
    (lambda (p1 p2) (< (car p1) (car p2)))
))
(define (flatten-music-raw music)
  (music-fold-time #f
    (lambda (data leaf return time) (let* (
      (prop (lambda (n) (ly:music-property leaf n)))
      (name (prop 'name))
      (mark (lambda (x)
              (assoc-set! return time
                          (cons x (or (assoc-ref return time) '())))))
      ) (cond
      ((eq? name 'NoteEvent) (mark (prop 'pitch)))
      ((eq? name 'RestEvent) (mark 'rest))
      (else return))))
    '() ; data
    '() ; init
    music))
; keeps only the top-sounding pitch
(define (flatten-music-top music)
  (map (lambda (p) (cons (car p) (cadr p))) (flatten-music music)))
(define (flatten-music-bottom music)
  (map (lambda (p) (cons (car p) (last (cdr p)))) (flatten-music music)))

; call-on-first-note: apply a user function on the first note {{{2
(define (call-on-first-note f data music)
"Call function (f data leaf) on first note of the music and return the value."
  (let* (
    (prop (lambda (n) (ly:music-property music n #f)))
    (n (prop 'name))
   )(cond
    ((prop 'elements)
     (call-on-first-note f data (car (prop 'elements))))
    ((prop 'element)
     (call-on-first-note f data (car (prop 'element))))
    (else (f data music))
)))
; find-articulation-prop: search for a particular articulation {{{2
; (define (find-articulation-prop leaf name)
; "Finds the first articulation having a property with given name,
; and returns the value corresponding to this property."
;   (find identity
;         (map (lambda(x) (ly:music-property x name #f))
;                         (ly:music-property leaf 'articulations '()))))
; time-signature-changes: returns a (sorted) list of time sigs {{{2
(define (time-signature-changes . l)
"Returns a sorted list of all time signatures found in the music expressions,
together with their (rational) time position, as a list of
  (time-position . (numerator . denominator))"
  (sort (apply append (map time-signature-changes1 l))
        (lambda (p q) (< (car p) (car q)))))
(define (time-signature-changes1 music)
"Same as time-signature-changes, but for only one music expression"
  (music-fold-time #f
    (lambda (data leaf X time)
      (let* ((p (lambda(x) (ly:music-property leaf x))))
        (if (eq? (p 'name) 'TimeSignatureMusic)
          (append X `((,time . (,(p 'numerator) . ,(p 'denominator)))))
;             (list (cons time (cons (p 'numerator) (p 'denominator)))))
          X)))
    '() '() music))
; music-splice: extract a part of a sequential music list {{{2
(define (music-splice mlist start stop)
"Assuming (music-splice) is a list of (sequential) music,
returns the sub-list of all elements at positions [start, stop["
  (if (or (null? mlist) (<= stop 0)) '()
    (let* (
      (head (car mlist))
      (tail (cdr mlist))
      (dur (music-length head))
;       (_ (begin (print "splice [" start ", " stop "[; head = " dur) 
(display-scheme-music head)))
    ) (append
        (if (<= start 0) (list head) '())
        (music-splice (cdr mlist) (- start dur) (- stop dur))))))
; music-insert-before, music-insert-after {{{2
(define (music-list-insert-before pos mlist items)
  (cond
    ((= pos 0) (append items mlist))
    ((> pos 0) (cons (car mlist) (music-list-insert-before
                 (- pos (music-length (car mlist)))
                 (cdr mlist) items)))
    ((and
       (eq? (ly:music-property (car mlist) 'name) 'SequentialMusic)
       (> (music-length (car mlist)) pos))
      (music-insert-before! (car mlist) items)
     mlist)
    (else #f)))
(define (music-insert-before! pos music items)
"Insert items (music list) before position pos in music expression
music. Returns music if insertion was successful, otherwise #f.
Mutually recursive with music-list-insert-before (above)."
  (let* (
    (prop (lambda (x) (ly:music-property music x #f)))
    (n (prop 'name))
  ) (cond
  ((eq? n 'SequentialMusic)
   (print "in sequential: inserting @ " pos " in " (flatten-music music))
   (let* (
     (l (music-list-insert-before pos (prop 'elements) items))
     ) (and l (ly:music-set-property! music 'elements l))))
  ((eq? n 'SimultaneousMusic)
   ; we try to insert into each element
   ; the boolean b holds #f as long as insertion failed
   (fold (lambda (m b) (b or (music-insert-before! pos m items)))
         #f (prop 'elements)))
  ((prop 'element) (music-insert-before! pos (prop 'element) items))
  (else #f)
)))
; Motivic analysis {{{1
; Utility functions {{{2
; this is needed for 2.18
; (define (make-articulation . l)
;   (apply make-music (append `(ArticulationEvent articulation-type) l)))
; music->shape {{{2
; A shape is an alist of (position . movement),
; where a movement is either:
;  - the symbol 'initial = the first note of the shape,
;  - an integer (*diatonic* difference between pitches),
;  - the symbol 'rest.

(define (pitch-movement ref new)
"Takes a reference pitch and a new pitch, and returns the pair
(new reference, pitch movement).
This is different from a subtraction when the new pitch is a rest."
;   (assert (ly:pitch? ref) "pitch-movement: must be a Pitch: " ref)
  (if (ly:pitch? ref)
    (if (ly:pitch? new) (cons new (pitch->int (ly:pitch-diff new ref)))
                      (cons ref 'rest))
    (if (ly:pitch? new) (cons new 'initial) (cons ref new))))
(define (flat->shape flat offset)
"Converts flat music into a shape.
offset is the offset since last strong beat.
The first entry returned will be (offset . 'initial).
The flat input always starts at 0, so we need to shift everything by +offset."
  ; we fold the flatten-music onto a pair L containing:
  ;   (reference pitch . current shape)
  ; and p is the pair (new time, new pitch)
  (if (null? flat) '()
  (cdr (fold (lambda (p L) (let* (
          (ref (car L)) ; reference pitch
          (shape (cdr L)) ; current shape
          (pos (car p)) ; time from start
          (new-pitch (cdr p))
          (move (pitch-movement ref new-pitch))
          (new (cons (+ pos offset) (cdr move)))
        ) (cons (car move)
                (append shape (list new)))))
        `(,(cdar flat) . ((,(+ (caar flat) offset) . initial)))
        (cdr flat)))))
(define (music->shape music offset)
"Converts music into a list of (time . movement), where a movement is
either: a pitch interval, 'initial, or 'rest; and moment is a rational.
offset is the offset since last strong beat;
the first entry returned should be (offset . 'initial)"
  (flat->shape (flatten-music-top music) offset))
; strong-beats: {{{2
(define (strong-beats m)
  (strong-beats-sigs (music-length m) (time-signature-changes m)))
(define (strong-beats-sigs l sigs)
  (strong-beats-rec l sigs `(4 . 4) 0))
(define (strong-beats-rec l sigs r start)
  (let* ((step (/ (if (even? (car r)) 2 (car r)) (cdr r))))
  (if (null? sigs) (interval-open start step l)
    (append (interval-open start step (caar sigs))
            (strong-beats-rec l (cdr sigs) (cdar sigs) (caar sigs))
))))
(define (last-strong-beat-before s t)
"Returns the last strong beat in list l before time t"
  (car (last-pair (take-while (lambda (x) (<= x t)) s))))
; shape-inversion
(define (invert-movement m) (if (number? m) (- m) m))
(define (invert-shape s)
  (map (lambda (x) (cons (car x) (invert-movement (cdr x)))) s))
(define (invert-motif-name s) (string-append s "inv"))
;   (markup s #:super "inv")
; flat-music-slice: extract an interval from flattened music {{{2
; flat music is a list (time . pitch)
; we return those events in [start, start + dur]
; with the time part offset by (-start)
(define (flat-music-drop start flat)
"Returns a list containing all events after start"
  (cond
  ((null? flat) '())
  ((< start 0) flat)
  (else (append (if (> start (caar flat)) '() (list (car flat)))
                (flat-music-drop start (cdr flat))))))
(define (flat-music-take end flat)
"Returns a list containing all events before end"
  (if (or (null? flat) (> (caar flat) end)) '()
    (cons (car flat) (flat-music-take end (cdr flat)))))
(define (flat-music-slice start dur flat)
  (flat-music-take (+ start dur) (flat-music-drop start flat)))
; motif colors {{{2
(define motif-colors '())
(define (set-motif-color! name color)
"Defines the color associated to the motif given by name."
  (let* ((color (anything->color color)))
  (set! motif-colors (assoc-set! motif-colors name color))
  (set! motif-colors (assoc-set! motif-colors (invert-motif-name name) color))))
; XXX add a new color (as different from the previous ones as possible)
; if none exists (and of course add it to the alist)
(define (get-motif-color name)
  (or (assoc-ref motif-colors name) `(.5 0. .5)))
; The motif definitions {{{2
; define-motif: returns a music-function which marks motives {{{3
; defA = #(define-motif 'A)
; then
; \relative { c' d e \defA { f g a } b c }
; and a recursive descend in this music can extract the motif
(define* (define-motif name #:optional color)
"Creates a music function used to mark the definition of a shape
in a music expression. These definitions are then extracted by
extract-motives (below)."
  (cond
    ((string? color) (set-motif-color! name (get-motif-color color)))
    (color (set-motif-color! name color))
  )
  (define-music-function (parser location music) (ly:music?)
    (make-music 'SequentialMusic 'elements (list music)
      'motif-define name)))
; extract-motives {{{3
(define (extract-motives music)
"Returns a list of shapes, of the form (name . shape).
This extracts the shapes marked with define-motif (above)."
  ; plist holds the strong beats of the music, so that we can start the
  ; motif relatively to the last one
  (let* (
     (s (strong-beats music))
     (offset (lambda (x) (- x (last-strong-beat-before s x))))
  ) (music-fold-time 'pre
     (lambda (data node shapes time)
       (let* (
              (a (ly:music-property node 'motif-define #f))
              (e (ly:music-property node 'elements '()))
              (s (if a (music->shape (car e) (offset time)) '()))
              (ainv (if a (invert-motif-name a)))
              (sinv (invert-shape s))
;               (_ (if a (print "### a = " a  "; offset(" time ") = " (offset 
time))))
             )
        (if a `((,a . ,s) (,ainv . ,sinv) . ,shapes)
          shapes)))
     #f '() music)))
; Comparing shapes with the database {{{2
; compare-moves {{{3
(define (compare-moves m1 m2)
"Attributes a score for the comparison of movements m1 m2.
The lower the score, the better.
This is *not* symmetrical: m1 is the reference move.

"
(let* (
;   (_ (print "compare moves " m1 " and " m2))
  (d (and (number? m1) (number? m2) (- m2 m1)))
  (p (cons m1 m2))
  ) (cond
  ; if everything matches, no penalty
  ((equal? m1 m2) 0)
  ; delayed entry has a slight penalty
  ((member p `((initial . #f) (#f . initial))) 1)
  ; ... even if the match is up to alterations
   ; same octave - no penalty; different octave - slight penalty
  ((and d (eq? (modulo d 7) 0)) 1)
  ; a non-zero interval may be offset by 1 (mutation) for a small cost
  ((and d
        (not (zero? m1))
        (eq? (abs d) 1))
   2)
  ; if an extra note is added, small penalty
  ((and (not m1) (number? m2)) 2)
  ; if a note is missing, medium penalty
  ((and (number? m1) (not m2)) 3)
  ; if the direction is the same, medium penalty
  ((and (number? m1) (number? m2)
        (> (* m1 m2) 0)) 3)
  ; else, large penalty
  (else 8))))
; compare-shapes {{{3
(define (compare-shapes s1 s2)
"Returns a score for the comparison of shapes s1 and s2.
The lower the score, the better the match (0 = perfect match).
This *not* symmetrical: s1 is the reference shape."
  (let* (
;   (_ (print "comparing shapes...: " s1 "  and  " s2))
  (t (delete-duplicates (sort (append (map car s1) (map car s2)) <)))
  (moves-from (lambda (s) (map (lambda (x) (assoc-ref s x)) t)))
;   (score (fold + 0 (map compare-moves (moves-from s1) (moves-from s2))))
  )
  (fold + 0 (map compare-moves (moves-from s1) (moves-from s2)))
))
; find-motives {{{3
; maximum allowed score, as a function of the number of notes in the
; motif
(define (max-score n) (+ 0 (* 1 n)))
(define (find-motives db music)
"Finds all occurrences of shapes from database db in music.
Returns a list of (time . shape-name)
XXX Now returns a list of (time . pitch . (motif index start)),
where index is the position in the given motif.
"
; FIXME: we should instead return a list of
; (name . ((time1 . pitch1) … (timen . pitchn)))
;   (note: this is an alist-like, but with non-unique keys)
; this would ease a bit for marking the pitches later
;  (replace nodes by sequential-music using music-map)
; this requires that flat-music-slice does *not* translate the pattern
  (let* (
  (plist (strong-beats music))
  (flat (flatten-music-top music))
  ; we iterate over the database, with pairs (name . shape)
  ; this creates a list-of-lists-of-lists, which we flatten later
  (tmp (map (lambda (x)
;     (print "x = " x)
    (let* (
      ; cdr x: shape of the motif
      ; cadr x: (offset . 'initial)
      ; caadr x: offset
       (offset (caadr x))
       (len (- (car (last (cdr x))) offset))
;        (_ (print "trying motif " (car x) "  offset = " offset "  shape = " 
(cdr x)))
    ) (map (lambda (t) (let* (
      ; t iterates over the strong beats of the music
      ; we first extract the notes from [t + offset, t + len + offset]
       (start (+ t offset))
       (ex (flat-music-slice start len flat))
;        (_ (print "  extract = " ex))
       (candidate (flat->shape ex (- t)))
;        (_ (print "  Candidate at " t ", " start " = " candidate))
       (score (compare-shapes (cdr x) candidate))
;        (_ (print "     score = " score))
;        (_ (print " ex len= " (length ex) "; value " ex))
;        (_ (if (< score (* 1 (length (cdr x)))) (print " +++ adding...")))
       )
         (if (< score (max-score (length (cdr x))))
           ; we return a list of ((time . pitch) . (motif . index))
           ; ex contains a list of (time . pitch)
           ; p is the pair (time . pitch), i is the index
           (map (lambda (p i)
;                   (print "item " p ", " i)
                  `(,p . (,(car x) ,i ,(cdar ex)))
                ) ex (iota (length ex)) )
           '()
         )
      )) plist))
   ) db))
  )
  ; now we flatten the lolol
  ; using map(map) is easier on the eyes than fold(fold)...
;   (print "folding...")
  (fold append '() (fold append '() tmp))
))
; marking shapes {{{2
; motif-markup: the markup attached to the motif head {{{3
(define (motif-markup name)
"Returns the markup associated to a given motif name.
name is the name of the motif (string, but markup should also work)."
  (motif-mark (get-motif-color name) name))
(define (motif-mark color name)
  (markup #:with-color color #:center-column
    (#:left-align #:fontsize -3 name #:vspace -.25
     #:left-align #:fontsize 3 #:arrow-head 1 -1 #t))
)
; mark-motif-leaf: mark just one leaf with the motif colors {{{3
(define (mark-motif-leaf leaf name first trans) (let* (
  (p (ly:music-property leaf 'pitch))
  (color (color-variant (get-motif-color name) trans))
  (grobs '(NoteHead Stem Dots Flag Script Accidental))
  )
  (if first
    (ly:music-set-property! leaf 'articulations
      (cons (make-music 'TextScriptEvent
                        'direction 1
                        'text (motif-markup name))
            (ly:music-property leaf 'articulations))))
  (make-sequential-music (append
    (map (lambda (g) (prop-override `(Staff ,g color) color)) grobs)
    (list leaf)
    (map (lambda (g) (prop-revert `(Staff ,g color))) grobs)))
))
; mark-found-motives! {{{3
(define (mark-found-motives! found music)
;   (print "marking " (length found) " motives in music...")
  (music-map-time!
    (lambda (data leaf time) (let* (
      (prop (lambda (x) (ly:music-property leaf x)))
      (n (prop 'name))
;       (_ (begin (print "at time " time ": ") (display-scheme-music leaf)))
      ) (cond
      ((and (eq? n 'NoteEvent) (assoc-ref found (cons time (prop 'pitch))))
       => (lambda (p) (mark-motif-leaf leaf
                        (car p) (= 0 (cadr p)) (ly:pitch-notename (caddr p))
                      )))
      (else leaf)
      )
    )) '() music))
; User API {{{2
(define (motif-analysis! . l) (let* (
  (db (fold append '() (map extract-motives l)))
  )
  (map (lambda (x) (mark-found-motives! (find-motives db x) x)) l)
  *unspecified*
))

; Harmonic analysis {{{1
;  music-fold-time order f data init music -> f data leaf X time
; Convert pitch to color {{{2
(define* (pitch-if-minor p #:optional minor major)
  "Returns 0 for major mode, 1 for minor mode"
  (if (< (ly:pitch-octave p) -1) (or minor 1) (or major 0)))
(define (pitch->fifth p)
  (modulo (* 7 (+ (modulo (pitch->semitone p) 12) (pitch-if-minor p 3))) 12))
(define (pitch->hue p)
   (list-ref `(244 158 33 289 191 48 344) (modulo p 7)))
;   (list-ref `(244 191 158 48 33 344 289) (modulo p 7))
;   (list-ref `(244 48 289 158 344 191 33) (modulo p 7))
(define pitch-fg-table
  (map (lambda (i) (hsv->rgb (list (pitch->hue i) .5 .4))) (iota 7)))
(define pitch-bg-table
  (map (lambda (u)
       (define minor (< u 7))
       (define pitch (modulo u 7))
       (hsv->rgb (list
          ; +2 for minor because of relative tonality
          (pitch->hue (+ pitch (if minor 2 0)))
          (if minor .25 .33)
          (if minor 1.  .75))))
   (iota 14)))
; (define (pitch->bg p) (assoc-ref pitch-bg-table p))
(define (pitch->bg p)
  (list-ref pitch-bg-table (modulo (pitch->int p) 14)))
(define (pitch->fg p) (list-ref pitch-fg-table (modulo p 7)))

; pitch to string {{{2
(define (pitch->string p)
  (string-append
    (symbol->string
     (list-ref `(Do Ré Mi Fa Sol La Si do ré mi fa sol la si)
               (+ (ly:pitch-notename p) (pitch-if-minor p 7))))
    (list-ref `("♭" "" "♯") (+ 1 (* 2 (ly:pitch-alteration p))))))

; collect-metadata {{{2
(define (collect-metadata key music)
"Collects all harmony marks in music, and returns an alist of the form
(time . markup)"
  (music-fold-time 'pre
    (lambda (d node return time)
      (add-metadata-mark return time
                         (ly:music-property node key #f)
                         (ly:music-property node 'origin)))
    #f '() music))
(define (add-metadata-mark return time markup origin)
"Helper function for collect-metadata: ignores #f or adds markup."
;   (if markup (begin
;     (print "found markup at t = " time ":" markup)
;   ))
  (if markup (append return `((,time ,markup ,origin)) ) return))
; read-roman-degree {{{2
(define (prefix s n) (substring s 0 (min n (string-length s))))
(define (prefix-ci? s1 s2)
  (and (>= (string-length s1) (string-length s2))
       (string-ci=? s2 (substring s1 0 (string-length s2)))))
(define (degree-roman->int s)
"Returns a pair (value-of-roman-prefix . length-of-roman-prefix)"
  (cond
    ((prefix-ci? s "V/")
     ((lambda (p) (cons (modulo (+ (car p) 4) 7) (+ (cdr p) 2)))
          (degree-roman->int (substring s 2))))
    ((prefix-ci? s "VII") `(6 . 3))
    ((prefix-ci? s "VI") `(5 . 2))
    ((prefix-ci? s "V") `(4 . 1))
    ((prefix-ci? s "IV") `(3 . 2))
    ((prefix-ci? s "III") `(2 . 3))
    ((prefix-ci? s "II") `(1 . 2))
    ((prefix-ci? s "N") `(1 . 1))
    ((prefix-ci? s "I") `(0 . 1))
    (else `(0 . 0))
))
(define (chord-markup y l)
 (markup #:override `(baseline-skip . 1.8) (#:fontsize -3
   #:raise y (make-column-markup l))))
(define (degree-tail-markup s)
"Returns the markup for the tail of the degree"
; FIXME: replace [digit]/ by slashed-digit
; then split into individual [sign?][alteration?][digit] patterns
  (cond
    ((string-ci=? s "65") (chord-markup .8 `("6" "5")))
    ((string-ci=? s "+63") (chord-markup .8 `("+6" "3")))
    ((string-ci=? s "5/") (markup #:raise .6 #:fontsize -3 #:slashed-digit 5))
    ((string-ci=? s "7/") (markup #:raise .6 #:fontsize -3 #:slashed-digit 7))
    (else (markup #:raise .6 #:fontsize -2 s))))
(define* (degree-markup s #:optional base)
"Returns markup for this degree"
  (let* (
    (p (degree-roman->int s))
    (deg (car p))
    (head (substring s 0 (cdr p)))
    (tail (substring s (cdr p)))
  )
  (markup #:bold #:with-color (pitch->fg (+ deg (or base 0)))
    #:concat (head (degree-tail-markup tail)))
))

; collect-octave-drops {{{2
(define (shape-octave-drops shape)
"Looks for octave-drop cadences in a shape
Returns a list of times."
  (if (< (length shape) 3) '()
    (append
      (if (member (cons (cdadr shape) (cdaddr shape))
                  `((-7 . 3) (0 . 3) (-7 . -4) (0 . -4)))
          `(,(caar shape)) '())
      (shape-octave-drops (cdr shape)))))
(define (music-octave-drops music)
  (shape-octave-drops
    (filter (lambda (x) (number? (cdr x)))
          (flat->shape (flatten-music-bottom music) 0))))
; harmonic-analysis {{{2
; FIXME: include \new Lyrics { } around this
(define harmonic-mark-duration 1/32)
(define (metadata-analysis key music)
"Takes music as in input (e.g. the bass voice), and returns a music
suitable for inclusion in a Lyrics context, containing the harmonic marks
attached to the music.
The key is a symbol identifying which metadata we collect."
  ; l is a pair (current duration . list of lyrics)
  ; w is a list (new time, new markup, location)
  (make-music 'ContextSpeccedMusic 'create-new #t
              'context-type 'Lyrics 'element
  (make-sequential-music (cdr (fold
    (lambda (w p)
      ; car w = position of the markup
      ; cadr w = markup
      ; caddr w = origin
;       (print " w = " w ", p = " p)
      (cons
      (+ (car w) harmonic-mark-duration)
      (append (cdr p) (list
        (make-music 'SkipEvent
                    'duration (rational->duration (- (car w) (car p))))
        (prop-override `(Bottom LyricText self-alignment-X) LEFT)
        (make-music 'LyricEvent
                    'duration (rational->duration harmonic-mark-duration)
                    'origin (caddr w)
                    'text (cadr w))))))
    `(0 . ())
    (merge
      (collect-metadata key music)
      (map (lambda (x) (list x cadence-markup 'nothing)) (music-octave-drops 
music))
      (lambda (e1 e2) (< (car e1) (car e2))))
    )))))
(define (harmonic-analysis music) (metadata-analysis 'harmony music))

; music-with-metadata: creates an empty music expression with metadata {{{2
(define (music-with-metadata origin . l)
  (apply make-music `(Music void #t origin ,origin . ,l)))
(define (make-harmony-mark origin l)
  (music-with-metadata origin 'harmony l))
(define (harmony-mark-bg bg text)
  (define-music-function (parser location)()
    (make-harmony-mark location
      (markup #:with-background (anything->color bg) #:pad-markup .5 text))))
(define global-tonic 0)
(define modulation (define-music-function (parser location tonic) (ly:pitch?)
  (set! global-tonic (ly:pitch-notename tonic))
  (make-harmony-mark location
    (markup #:with-background (pitch->bg tonic)
            #:pad-markup .5 (pitch->string tonic)))))
(define degree (define-music-function (parser location deg) (string?)
  (make-harmony-mark location (degree-markup deg global-tonic))))
; Structural analysis {{{1
(define structural-analysis-is-lyrics #f)
(define (structural-analysis music)
  (if structural-analysis-is-lyrics
    (metadata-analysis 'structure music)
    (print "[ERROR] structural-analysis-is-lyrics positioned to #f.")))
(define (structural-framed origin text)
  (if structural-analysis-is-lyrics
    (music-with-metadata origin 'structure
                (markup #:line (#:box #:fontsize -3 text)))
    #{ \override RehearsalMark.padding = #4 \framed-mark $text #}))
(define (structural-corner origin text)
  (if structural-analysis-is-lyrics
    (music-with-metadata origin 'structure (markup #:fontsize -3
      (#:combine (#:path .15 '((lineto 0 2) (lineto 3 2)))
                #:line (" " text))))
    #{ \corner-mark $text #}))
; this is only a bunch of macros that define marks above the score... {{{2
; (define exposition (define-music-function (parser location) ()
;   #{ \override RehearsalMark.padding = #5 \framed-mark "Exposition" #}))
; (define step (define-music-function (parser location n) (number?)
;   #{ \corner-mark #(string-append "" (number->string n) "." ) #}))
; }}}1
; defaults and user functions

(define exposition (define-music-function (parser location) ()
  (structural-framed location "Exposition")))
(define counterexposition (define-music-function (parser location) ()
  (structural-framed location "Counter-Exposition")))
(define step (define-music-function (parser location n) (number?)
  (structural-corner location (string-append "" (number->string n) "."))))

; this is used by octave-drop detection:
(define cadence-markup
  (markup #:with-background (anything->color .8) #:pad-markup .5 "Cadence"))
(define cadence (harmony-mark-bg .8 "Cadence"))
(define halfcadence (harmony-mark-bg .9 "½ Cad."))
(define deceptive (harmony-mark-bg .9 "Deceptive"))

(map (lambda (x) (set-motif-color! (car x) (cdr x))) `(
   ("A" . DodgerBlue4) ("B" . OrangeRed4) ("C" . (.1 .4 0))
   ("D" . DarkGoldenrod4)
))
(define defA (define-motif "A"))
(define defB (define-motif "B"))
(define defC (define-motif "C"))
(define defD (define-motif "D"))
(define defE (define-motif "E"))
\version "2.18"
#(define (print . l) (map display l) (newline) #f)

#(load "motif.scm")

defA = #(define-motif "A")
defB = #(define-motif "B")
defBi = #(define-motif "B'" 'DarkGoldenrod4)
defC = #(define-motif "C")

#(set! structural-analysis-is-lyrics #t)
\header { opus = "BWV 772" } %<<<1
voiceone = \relative c' {%<<<
   \exposition
   r16 \defA { c[ d e]   f[ d e c] g'8[ } \defB { c b^\prall c]
   d16[ } g, a b]  c[ a b g] d'8[  \defBi { g f^\prall g]
   e16[ } \step #1
   a g f]  e[ g f a]  g[ \step #2  f e d] c[ e d f]
   e[ \step #3  d c b]  a[ c b d] c[  b a g]  fis[ a g b]
   a8[ d,]  c'8.[^\mordent d16]  b[  a g fis]  e[ g fis a]
   g[ b a c]  b[ d c e]  d[ b32 c d16 g]  b,8[^\prall a16 g]
   \exposition
   g8 r r4 r16   g[ a b]  c[ a b g]
   fis8^\prall r r4 r16   a[ b c]  d[ b c a]
   b8 \step #1         r r4 r16\step #2   d[ c b]  a[ c b d]
   c8 \step #3          r r4 r16\step #4 e[ d c]  b[ d cis e]
   d8[ \step 1  cis d e]  f[ \step 2  a, b! cis]
   d[ \step 3  fis, gis a]  b[ c] d4 ~
   d16[  e, fis gis]  a[ fis gis e]  e'[ d c e]  d[ c b d]
   c[ a' gis b]  a[ e f d]  gis,[ f' e d]  c8[ b16 a]
   \exposition
   a16[  a' g f]  e[ g f a] g2 ~
   g16[  e f g]   a[ f g e] f2 ~
   f16[ g f e]   d[ f e g] f2 ~
   f16[  d e f]   g[ e f d] e2 ~
   e16[  c d e]  f[ d e c]  d[ e f g]  a[ f g e]
   f[  g a b]  c[ a b g]  c8[ g]  e[ d16 c]
   c[  bes a g]  f[ a g bes]  a[ b c e,]  d[ c' f, b]
   <c g e>1^\fermata\arpeggio
   \bar "|."
}%>>>
voicetwo = \relative c {%<<<
   \clef "bass"
   \textLengthOff
   \modulation c \degree "I"
   r2         r16    c[ d e]  f[ d e c]
   \degree "V"
   g'8[g,] r4 r16   g'[ a b]  c[ a \cadence b g]
   c8[ \defC { b c d]  e[ } g, a b]
   \modulation g c[ e, fis g]  a[b] \degree "V+4" c4 ~
   c16[  d, e fis]  g[ e \cadence fis d]  \degree "I" g8[ b, c d]
   e[fis g e]  \degree "I6" b8.[ c16]  d8[ d,]
   r16   g[ a b]  c[ a b g]  d'8[ g fis \halfcadence g]
   a16[  d, e fis]  g[ e fis d]  a'8[ d c \cadence d]
   \modulation d,
   g,16[ \clef "treble"  g' f e]  d[ f e g]  f8[ e f d]
   e16[ a g f]  e[ g f a]  g8[ f g \cadence e]
   f16[ bes a g]  f[ a g bes] 
   \modulation a,
   a[ g f e]  d[ f e g]
   f[ e d c]  b[ d c e]  d[ c b a]  gis[ b a c]
   \clef "bass"
   b8[e,]  d'8.[^\mordent e16]  c[  b a g!]  fis[ a gis b]
   a[c b d]  c[ e d f]  e8[ a, e' e,]
   a8[ a,] r4 r16   e''16[ d c]  b[ d cis e]
   d2 ~ d16[  a b c]    d[ b c a]
   b2 ~ b16[  d c b]    a[ c b d]
   \modulation f
   c2~ c16[  g a bes]  c[ a bes g]
   a8[ bes a g]  f[ d' c bes]
   a[f' e d]    e16[ d, e f]  g[ e f d]
   e8[ c d e]
   \modulation c
   f16[ d e f] g8[ g,]
   <c c,>1\arpeggio_\fermata
   \bar "|."
}%>>>
#(motif-analysis! voiceone voicetwo)
\score {%<<<
  \context PianoStaff <<
    #(structural-analysis voiceone)
    \set PianoStaff.connectArpeggios = ##t
    \context Staff = "one" << \voiceone >>
    \context Staff = "two" << \voicetwo >>
    #(harmonic-analysis voicetwo)
   >>
   \layout { }
  \midi { \context { \Score tempoWholesPerMinute = #(ly:make-moment 80 4) } }
}%>>>
% we can also do without the music and just keep the analysis:
\score {%<<<
  \context PianoStaff <<
    #(structural-analysis voiceone)
    #(harmonic-analysis voicetwo)
   >>
   \layout { }
}%>>>
_______________________________________________
lilypond-user mailing list
lilypond-user@gnu.org
https://lists.gnu.org/mailman/listinfo/lilypond-user

Reply via email to