On Sat, Jul 15, 2023 at 1:26 AM Richard Shann <[email protected]>
wrote:

> On Fri, 2023-07-14 at 21:08 +0000, Eef Weenink wrote:
> > Maybe somebody already made a script for this:
> >
> > In figured bass there are two fundamental rules: Avoid parallell
> > octaves and or fifhts.
> > So it would be nice to have some scipts what checks for this.
>

I converted the file that Jean suggested to 2.24 and ran it several times.
It seems to work fine.  I've attached the file here.
\version "2.24.0"

%%% File: partwriter7.ly

%%% Pitch Utility

#(define (pitch>=? a b)
   (not (ly:pitch<? a b)))

#(define (pitch=? a b)
   (and (not (ly:pitch<? a b))
        (not (ly:pitch<? b a))))

#(define (same-pc? a b)
   (eqv?
    (modulo (ly:pitch-semitones (ly:pitch-diff a b)) 12)
    0))

#(define (enharmonic? a b)
   (and (same-pc? a b)
        (eqv? (ly:pitch-octave a) (ly:pitch-octave b))))

% Convert a pitch object into a music expression with duration
#(define (make-note pitch dur)
   (make-music
    'NoteEvent
    'duration
    dur ;(ly:make-duration dur)
    'pitch
    pitch))

%%% Interval Utility

#(define (ordered-pitch-interval p1 p2)
   ; pitches are exchanged so result is positive when p2 is higher than p1
   (ly:pitch-semitones (ly:pitch-diff p2 p1)))

#(define (unordered-pitch-interval p1 p2)
   (abs (ordered-pitch-interval p1 p2)))

#(define interval unordered-pitch-interval)

#(define (base-interval n1 n2)
   (modulo (interval n1 n2) 12))

#(define (interval->interval-class interval)
   (let ((iv (calc-base-interval interval)))
     (if (< iv 6)
         iv
         (- 12 iv))))

#(define (interval=? iv1 iv2)
   (eqv? (interval (car iv1) (cadr iv1))
         (interval (car iv2) (cadr iv2))))

#(define (semitone? p1 p2)
   (eqv? 1 (interval p1 p2)))

#(define half-step? semitone?)

#(define (whole-tone? p1 p2)
   (eqv? 2 (interval p1 p2)))

#(define whole-step? whole-tone?)

#(define (step? p1 p2)
   (or (half-step? p1 p2)
       (whole-step? p1 p2)))

%% Intervals specified as a pitch (relative to middle C)
#(define disallowed-intervals
   `(
      ,#{ dis' #}
      ,#{ fis' #}
      ; ,#{ ges' #}
      ))

%%% List utility

% permute, for example, (1 3 5)
% Procedure:
% Put one to end: (3 5 1), put 5 to end (3 1 5)
% Put three to end: (5 1 3), put 1 to end: (5 3 1)
% Put five to end: (1 3 5), put 3 to end: (1 5 3)

% '(1 2 3) => '(2 3 1)
#(define (rotate-list ls)
   (append (cdr ls) (list (car ls))))

% '(1 2 3) => '((1 2 3) (2 3 1) (3 1 2))
#(define (all-rotations ls)
   (define (helper cp ls)
     (if (null? cp)
         '()
         (cons ls
           (helper (cdr cp) (rotate-list ls)))))
   (helper ls ls))

% All possible orderings of a list
% '(1 2 3) => '((1 2 3) (2 3 1) (3 1 2) (1 3 2) (3 2 1) (2 1 3))
#(define (permute-list ls)
   (define (helper head tail)
     (map (lambda (t) (append head t))
       (all-rotations tail)))

   (let loop ((idx (1- (length ls))) (res (list ls)))
     (if (< idx 0)
         res
         (loop
          (1- idx)
          (append-map
           (lambda (r) (helper (list-head r idx) (list-tail r idx)))
           res)))))

% return list of lists of every ordered combination of single elements
% from a list of lists
% '((1 2) (a b) (100) (x)) ==> '((1 a 100 x) (1 b 100 x) (2 a 100 x) (2 b 100 x))
#(define (every-one-of-each ls)
   (define (helper ls1 ls2)
     (append-map
      (lambda (x)
        (map (lambda (y) (cons y x))
          ls2))
      ls1))
   (let loop ((ls ls) (seed '(())))
     (if (null? ls)
         (map reverse seed)
         (loop (cdr ls) (helper seed (car ls))))))

%%% Ranges

#(define full-ranges
   `(
      ("bass" . (,#{ e, #} . ,#{ d' #}))
      ("tenor" . (,#{ c #} . ,#{ g' #}))
      ("alto" . (,#{ g #} . ,#{ d'' #}))
      ("soprano" . (,#{ c' #} . ,#{ g'' #}))
      ))

#(define moderate-ranges
   `(
      ("bass" . (,#{ g, #} . ,#{ b #}))
      ("tenor" . (,#{ e #} . ,#{ e' #}))
      ("alto" . (,#{ b #}. ,#{ b' #}))
      ("soprano" . (,#{ e' #} . ,#{ e'' #}))
      ))

% Locate every pitch expression of chord member within a voice range
#(define (voice-member-possibilities mem range)
   (filter-map
    (lambda (p)
      (and (pitch>=? p (car range))
           (pitch>=? (cdr range) p)
           p))
    (map
     (lambda (o)
       (ly:make-pitch o
         (ly:pitch-notename mem)
         (ly:pitch-alteration mem)))
     (iota (1+ (- (ly:pitch-octave (cdr range))
                 (ly:pitch-octave (car range))))
       (ly:pitch-octave (car range))))))

% Find every pitch expression of a chord's members for a voice type.
#(define (voice-all-member-possibilities voice-str chord)
   (let ((range (ly:assoc-get voice-str moderate-ranges)))
     (map (lambda (mem)
            (cons (car mem)
              (list
               (voice-member-possibilities
                (cdr mem)
                range))))
       chord)))

%%% Voicing utility

% Check that each voice is in unison with or higher than next lower voice
#(define (ascending? ls)
   (let loop ((l ls) (good #f))
     (cond
      ((null? (cdr l)) good)
      ((ly:pitch<? (cadr l) (car l))
       #f)
      (else (loop (cdr l) #t)))))

% Check that upper parts are no more than an octave apart
#(define (well-spaced? ls)
   (let ((TAS (cdr ls)))
     (let loop ((l TAS) (good #f))
       (cond
        ((null? (cdr l)) good)
        ((< 12 (ly:pitch-semitones (ly:pitch-diff (cadr l) (car l))))
         #f)
        (else (loop (cdr l) #t))))))

#(define (filter-pitch-lists p-lists)
   (filter well-spaced?
           (filter ascending? p-lists)))

% correlation of inversion and bass member
% TODO: calculate this from \chordmode expression
#(define inversion-lookup
   '(
      ("root" . "root")
      ("first" . "third")
      ("second" . "fifth")
      ("third" . "seventh")
      ))

% Returns a list of lists of possible chord voicings.
% Pitches are arranged from lowest to highest, corresponding
% to an uncrossed SATB arrangement.
#(define (make-pitch-lists chord inversion upper-members)
   (let* ((bass (voice-all-member-possibilities "bass" chord))
          (tenor (voice-all-member-possibilities "tenor" chord))
          (alto (voice-all-member-possibilities "alto" chord))
          (soprano (voice-all-member-possibilities "soprano" chord))
          ; all dispositions of chord members in upper voices
          (arrangements (permute-list upper-members))
          ; add bass note
          (bass-member
           (ly:assoc-get
            (symbol->string inversion) inversion-lookup))
          (arrangements
           (map
            (lambda (a) (cons bass-member a))
            arrangements))
          (pitched-arrangements
           (map (lambda (arr)
                  (list
                   (ly:assoc-get (first arr) bass)
                   (ly:assoc-get (second arr) tenor)
                   (ly:assoc-get (third arr) alto)
                   (ly:assoc-get (fourth arr) soprano)))
             arrangements))
          ;; YUCK.  Shouldn't need to unnest like this.
          (pitched-arrangements
           (map (lambda (arr)
                  (map (lambda (a) (car a)) arr))
             pitched-arrangements))
          (pitched-arrangements (map every-one-of-each pitched-arrangements))
          (pitched-arrangements
           (append-map identity pitched-arrangements)))
     ; Get rid of bad arrangements. (Wide spacing in upper voices,
     ; crossed parts)
     (filter-pitch-lists pitched-arrangements)))

%%% \chordmode input processing

% Convert a member of a \chordmode expression into
% an alist pairing chord member names with pitches
% This will only handle triads and seventh chords.
#(define (parse-chord mus)
   (let* ((note-events (extract-named-music mus 'NoteEvent))
          (pitches (map (lambda (n) (ly:music-property n 'pitch))
                     note-events))
          (chord (list
                  (cons "root" (first pitches))
                  (cons "third" (second pitches))
                  (cons "fifth" (third pitches))))
          (chord (if (= 4 (length pitches))
                     (append
                      chord
                      (list (cons "seventh" (last pitches))))
                     chord)))
     chord))

% Borrowed from definition of \table
#(define (split-lst initial-lst lngth result-lst)
   ;; split a list into a list of sublists of length lngth
   ;; eg. (split-lst '(1 2 3 4 5 6) 2 '())
   ;; -> ((1 2) (3 4) (5 6))
   (cond ((not (integer? (/ (length initial-lst) lngth)))
          (ly:warning
           "Can't split list of length ~a into ~a parts, returning empty list"
           (length initial-lst) lngth)
          '())
     ((null? initial-lst)
      (reverse result-lst))
     (else
      (split-lst
       (drop initial-lst lngth)
       lngth
       (cons (take initial-lst lngth) result-lst)))))

#(define (get-rhythm mus)
   (let* ((ev-chs (extract-named-music mus 'EventChord))
          (n-evs (map (lambda (ne) (extract-named-music ne 'NoteEvent)) ev-chs))
          (durations (map (lambda (ne) (ly:music-property (car ne) 'duration)) n-evs)))
     durations))

% Build a list of voice/pitch alists for all the elements in a
% \chordmode expression
#(define (parse-progression mus)
   (let ((ev-ch (extract-named-music mus 'EventChord)))
     (map parse-chord ev-ch)))

%%% Building spacing output

#(define (get-voice-contents chord inversion members duration)
   (let* ((arr (make-pitch-lists chord inversion members))
          (zipped (apply zip arr)))
     (map (lambda (z)
            (map (lambda (n)
                   (make-note n duration))
              z))
       zipped)))

allSpacings =
#(define-music-function (keey chord inversion members)
   (ly:music? ly:music? symbol? list?)
   (let* ((duration (car (get-rhythm chord)))
          (chord-contents (parse-chord chord))
          (voice-contents
           (get-voice-contents chord-contents inversion members duration)))
     #{
       <<
         \context Staff = "top" <<
           \context Voice = "1" {
             #keey
             \voiceOne
             #@(last voice-contents)
           }
           \context Voice = "2" {
             \voiceTwo
             #@(third voice-contents)
           }
         >>
         \context Staff = "bottom" <<
           \context Voice = "3" {
             #keey
             \voiceOne
             \clef bass
             #@(second voice-contents)
           }
           \context Voice = "4" {
             \voiceTwo
             #@(first voice-contents)
           }
         >>
       >>
     #}))

%%% Voice-leading utility

#(define (octave-equivalent? p1 p2)
   (and (eqv? (ly:pitch-notename p1) (ly:pitch-notename p2))
        (eqv? (ly:pitch-alteration p1) (ly:pitch-alteration p2))))

#(define (tonic? p keey)
   (octave-equivalent? (ly:music-property keey 'tonic) p))

% There is currently no property in 'KeychangeEvent to identify
% major vs. minor.  'pitch-alist gives degree 7 of natural minor.
% Thus, we derive leading tone as note m2 below tonic.
#(define (leading-tone? p keey)
   (let* ((tonic (ly:music-property keey 'tonic))
          (leading-tone (ly:pitch-transpose tonic #{ b, #})))
     (octave-equivalent? p leading-tone)))

#(define (dominant? p keey)
   (let ((dominant (list-ref (ly:music-property keey 'pitch-alist) 4)))
     (and (eqv? (ly:pitch-notename p) (car dominant))
          (eqv? (ly:pitch-alteration p) (cdr dominant)))))

% TODO: Take into consideration enharmonic spellings.  D#-A# -> Eb-Bb is not motion.
#(define (both-parts-same-direction? iv1 iv2)
   (or
    (and (ly:pitch<? (car iv1) (car iv2))
         (ly:pitch<? (cadr iv1) (cadr iv2)))
    (and (ly:pitch<? (car iv2) (car iv1))
         (ly:pitch<? (cadr iv2) (cadr iv1)))))

#(define (parallel-motion? iv1 iv2)
   (and (interval=? iv1 iv2)
        (both-parts-same-direction? iv1 iv2)))

#(define (similar-motion? iv1 iv2)
   (and (not (interval=? iv1 iv2))
        (both-parts-same-direction? iv1 iv2)))

#(define (oblique-motion? iv1 iv2)
   (or
    (and (pitch=? (car iv1) (car iv2))
         (not (pitch=? (cadr iv1) (cadr iv2))))
    (and (pitch=? (cadr iv1) (cadr iv2))
         (not (pitch=? (car iv1) (car iv2))))))

#(define (contrary-motion? iv1 iv2)
   (or
    (and (ly:pitch<? (car iv1) (car iv2))
         (ly:pitch<? (cadr iv2) (cadr iv1)))
    (and (ly:pitch<? (car iv2) (car iv1))
         (ly:pitch<? (cadr iv1) (cadr iv2)))))

% Given two chords, expressed as
% '(bass-pitch    tenor-pitch    alto-pitch    soprano-pitch),
% check for various voice-leading problems

#(define (parallel-perfects-or-perfects-by-contrary-motion? iv1 iv2)
   (and (or (not (pitch=? (car iv1) (car iv2))) ; octave leap? enharmonic?
            (not (pitch=? (cadr iv1) (cadr iv2))))
        (let ((s1 (base-interval (cadr iv1) (car iv1)))
              (s2 (base-interval (cadr iv2) (car iv2))))
          (or
           (and (= 7 s1)(= 7 s2))
           (and (= 0 s1)(= 0 s2))))))

% Return #f if all clear, #t if any errors found
#(define (any-parallel-perfects-or-perfects-by-contrary-motion? c1 c2)
   (let loop ((c1 c1) (c2 c2))
     (cond
      ((null? (cdr c1)) #f)
      (else
       (let inner ((top1 (cdr c1)) (top2 (cdr c2)))
         (cond
          ((null? top1)
           (loop (cdr c1) (cdr c2)))
          ((parallel-perfects-or-perfects-by-contrary-motion?
            (list (car c1) (car top1))
            (list (car c2) (car top2)))
           #t)
          (else
           (inner (cdr top1) (cdr top2)))))))))

#(define (outer-voice-direct-fifths-or-octaves? c1 c2)
   (let* ((outer1 (list (first c1) (last c1)))
          (outer2 (list (first c2) (last c2)))
          (outer-iv2 (apply base-interval outer2))
          (similar? (similar-motion? outer1 outer2)))
     (and similar?
          (or (eqv? outer-iv2 0)
              (eqv? outer-iv2 7))
          (not (step? (last c1) (last c2))))))

#(define (voice-overlap? iv1 iv2)
   (or (ly:pitch<? (second iv1) (first iv2))
       (ly:pitch<? (second iv2) (first iv1))))

#(define (any-overlapping-voices? c1 c2)
   (let loop ((v1 c1) (v2 c2))
     (cond
      ((null? (cdr v1)) #f)
      ((voice-overlap?
        (list (first v1) (second v1))
        (list (first v2) (second v2)))
       #t)
      (else (loop (cdr v1) (cdr v2))))))

% TODO: allow tripled root
% TODO: leading tone moving down in iii->IV
% TODO: leading-tone to chord seventh
% TODO: possibility of delayed resolution
% TODO: LT in sequences
#(define (unresolved-leading-tone? p1 p2 keey inner-voice?)
   (cond
    ((not (leading-tone? p1 keey)) #f)
    ((and (tonic? p2 keey)
          (semitone? p1 p2))
     #f)
    ((and inner-voice?
          (eqv? -4 (ordered-pitch-interval p1 p2)))
     #f)
    (else #t)))

#(define (any-poorly-handled-leading-tone? c1 c2 keey)
   (let* ((inner-vv
           (map (lambda (p1 p2)
                  (unresolved-leading-tone? p1 p2 keey #t))
             (list-head (cdr c1) 2)
             (list-head (cdr c2) 2)))
          (outer-vv
           (map (lambda (p1 p2)
                  (unresolved-leading-tone? p1 p2 keey #f))
             (list (first c1) (last c1))
             (list (first c2) (last c2)))))
     (or (any identity inner-vv)
         (any identity outer-vv))))

#(define (disallowed-melodic-interval? p1 p2)
   (let* ((asc (sort (list p1 p2) ly:pitch<?))
          (diff (ly:pitch-diff (cadr asc) (car asc))))
     (any (lambda (p) (octave-equivalent? p diff)) disallowed-intervals)))

#(define (any-disallowed-melodic-interval? c1 c2)
   (any identity
     (map (lambda (p1 p2) (disallowed-melodic-interval? p1 p2))
       c1 c2)))

#(define (bass-leap-over-octave? c1 c2)
   (< 12 (interval (first c1) (first c2))))

%%% Building progressions

% Progressions are built from linked two-chord progressions
% (1) All acceptable possibilities for the pairing of the first and second chords are found
% (2) These are joined with all acceptable possibilites for the second and third chords
% (3) Process continues until the progression is complete

% Discover all ways of writing a two-chord progression.  Throw out any
% that break certain voice-leading rules.
#(define (two-chord-progression-possibilities p-list1 p-list2 keey)
   (let loop ((result '()) (p-ls1 p-list1) (p-ls2 p-list2))
     (cond
      ((null? p-ls1) (reverse result))
      (else
       (let inner ((result result) (p-l2 p-ls2))
         (cond
          ((null? p-l2)
           (loop result (cdr p-ls1) p-list2))
          ((or
            (any-parallel-perfects-or-perfects-by-contrary-motion? (car p-ls1) (car p-l2))
            (outer-voice-direct-fifths-or-octaves? (car p-ls1) (car p-l2))
            (any-overlapping-voices? (car p-ls1) (car p-l2))
            (bass-leap-over-octave? (car p-ls1) (car p-l2))
            (any-disallowed-melodic-interval? (car p-ls1) (car p-l2))
            (any-poorly-handled-leading-tone? (car p-ls1) (car p-l2) keey))
           (inner result (cdr p-l2)))
          (else
           (inner
            (cons (list (car p-ls1) (car p-l2))
              result)
            (cdr p-l2)))))))))

%% TODO: allow variable doublings, inversions
#(define (get-two-chord-realizations c1 c2 keey)
   (let ((prog-lists
          (two-chord-progression-possibilities
           (make-pitch-lists c1 'root '("root" "third" "fifth"))
           (make-pitch-lists c2 'root '("root" "third" "fifth"))
           keey)))
     prog-lists))

% For linking chord pairs

#(define (same-chord? c1 c2)
   (every identity (map (lambda (x y) (pitch=? x y)) c1 c2)))

% Return #f if not a match
#(define (get-chord-pair-beginning-with chord chord-list)
   (and (same-chord? chord (car chord-list))
        chord-list))


% Create a random path through all possibilities
% This can hang when no viable continuation is found.

#(let ((time (gettimeofday)))
   (set! *random-state*
         (seed->random-state (+ (car time)
                               (cdr time)))))

#(define (randomly-realize-progression pr keey)
   (let* ((prog (parse-progression pr))
          (all-starting-pairs (get-two-chord-realizations (car prog) (cadr prog) keey)))

     (define (get-starting-pair)
       (list-ref all-starting-pairs (random (length all-starting-pairs))))

     (let crawler ((result (get-starting-pair)) (p (cdr prog)))
       (cond
        ((null? (cdr p)) result)
        (else
         (let* ((all-next-pairs (get-two-chord-realizations (car p) (cadr p) keey))
                (linked-pairs
                 (filter-map
                  (lambda (x) (get-chord-pair-beginning-with (last result) x))
                  all-next-pairs)))
           (if (null? linked-pairs)
               ;result ; opt out at failure
               (crawler (get-starting-pair) (cdr prog))
               (let ((next-pair (list-ref linked-pairs (random (length linked-pairs)))))
                 (crawler (append result (list (cadr next-pair))) (cdr p))))))))))

% Generate pitch lists representing every allowed variant of progression
#(define (realize-progression pr keey)
   (let* ((prog (parse-progression pr))
          (all-starting-pairs
           (get-two-chord-realizations (car prog) (cadr prog) keey)))

     (let crawler ((result all-starting-pairs) (p (cdr prog)))
       (cond
        ((null? (cdr p))
         (apply append result))
        (else
         (let ((all-next-pairs
                (get-two-chord-realizations (car p) (cadr p) keey)))
           (crawler
            ; branch every existing chord path to add any new chord pair which connects
            (append-map
             (lambda (r)
               (let (; all the possibilities for continuation
                      (linked-pairs
                       (filter-map
                        (lambda (x) (get-chord-pair-beginning-with (last r) x))
                        all-next-pairs)))
                 (filter-map
                  (lambda (lp)
                    ; if linked-pairs is the empty list (no continuation
                    ; available), that path is discarded
                    (and (pair? lp)
                         (append r (list (cadr lp)))))
                  linked-pairs)))
             result)
            (cdr p))))))))

% Build lists of music expressions for each voice part from chord list
#(define (build-voice-contents chord-ls keey rhythm)
   (let ((zipped (apply zip chord-ls)))
     (map (lambda (zl)
            (map (lambda (n d)
                   (make-note n d))
              zl rhythm))
       zipped)))

allRealizations =
#(define-music-function (keey prog)
   (ly:music? ly:music?)
   (let* ((rhythm (apply circular-list (get-rhythm prog)))
          (chord-ls (realize-progression prog keey))
          (voice-contents (build-voice-contents chord-ls keey rhythm)))
     #{
       <<
         \context Staff = "top" <<
           \context Voice = "1" {
             \voiceOne
             #keey
             #@(last voice-contents)
           }
           \context Voice = "2" {
             \voiceTwo
             #@(third voice-contents)
           }
         >>
         \context Staff = "bottom" <<
           \context Voice = "3" {
             \voiceOne
             \clef bass
             #keey
             #@(second voice-contents)
           }
           \context Voice = "4" {
             \voiceTwo
             #@(first voice-contents)
           }
         >>
       >>
     #}))

blindRealization =
#(define-music-function (keey prog)
   (ly:music? ly:music?)
   (let* ((rhythm (get-rhythm prog))
          (chord-ls (randomly-realize-progression prog keey))
          (voice-contents (build-voice-contents chord-ls keey rhythm)))
     #{
       <<
         \context Staff = "top" <<
           \context Voice = "1" {
             \voiceOne
             #keey
             #@(last voice-contents)
           }
           \context Voice = "2" {
             \voiceTwo
             #@(third voice-contents)
           }
         >>
         \context Staff = "bottom" <<
           \context Voice = "3" {
             \voiceOne
             \clef bass
             #keey
             #@(second voice-contents)
           }
           \context Voice = "4" {
             \voiceTwo
             #@(first voice-contents)
           }
         >>
       >>
     #}))

%%%%%%%%%%%%%%%%%%%%%%%%%% EXAMPLE %%%%%%%%%%%%%%%%%%%%%%%%%%

\markup \bold \huge {
  vi-ii-V-I, moderate range:
}

setup = \new PianoStaff <<
  \new Staff = "top" {
    \time 4/2
  }
  \new Staff = "bottom" {
    \time 4/2
  }
>>

{
  \setup
  \allRealizations \key b \major \chordmode { gis2:m cis:m fis b }
}

\markup \underline { Random realization: }

{
  \setup
  \blindRealization \key b \major \chordmode { gis2:m cis:m fis b }
}

\markup \bold \huge {
  i-iv-V-i, moderate range:
}

\new PianoStaff {
  \allRealizations \key c \minor \chordmode { c4:m f:m g c:m }
}

\markup \underline "F half-dim7, first inversion:"

\new PianoStaff {
  \allSpacings \key es \minor \chordmode { f1:m7.5- } first #'("root" "fifth" "seventh")
}

\layout {
  \context {
    \Score
    measureBarType = #"||"
  }
}

#(set-default-paper-size "letter")

\header {
  title = "SATB Progressions"
}

\paper {
  top-margin = 1\in
  bottom-margin = 1\in
  left-margin = 0.75\in
  right-margin = 0.75\in
  markup-markup-spacing.padding = 3
  markup-system-spacing.padding = 3
  %page-breaking = #ly:optimal-breaking
  tagline = ##f
}

Reply via email to