Jump to content

Coloring successive intervals

From LilyPond wiki

This scheme engraver searches for specified successive intervals in a voice, e.g. a minor third. If the interval occurs somewhere, the note heads of both notes defining the interval are colored. The engraver can be added to every Voice context.

Usage: \color_interval_engraver #intervaldefs #debug? #intervals-given

intervaldefs: alist containing information about semitonical distances for certain intervals, diatonical distance is calculated in the engraver using `string-diatonic-semi-tonic-list`, relying on the key.

debug?: (optional) boolean, if true, output information about the processed pitches

intervals-given: list of the form

#`((interval1 ,dir1 enh1 ,color1)
   (interval2 ,dir2 enh2 ,color2)
    ...
   (intervalN ,dirN enhN ,colorN))

with
intervalN: string - specifying the interval to search after
dirN: integer - UP (=1) DOWN (=-1) or 0 (up and down)
enhN: boolean - search for enharmonically equivalent intervals, too?
colorN: lilypond color value, see NR A.7.

Example:

\layout {
   \context {
     \Voice
     \consists \color_interval_engraver #intervaldefs
       #`(("2--" ,UP #f ,green)
          ("3+" ,DOWN #t ,blue))
   }
}

Debug mode:

With debug? set to #t, the engraver does output

  • The preprocessed list of intervals to search after
  • Detailed information about all note head grobs the engraver has acknowledged

Warnings:

The engraver tries to provide helpful warning messages when fed with invalid input or if other issues prevent the engraver from working correctly.

Some examples:

  • Warning: Color_interval_engraver: In interval (2++ 2 #f (0.5 0.5 0.0)), wrong type argument: 2, needs to be a direction.
  • Warning: Color_interval_engraver: Recoloring note head in Voice N/A, bar number 1 #<Pitch ees'> belongs to intervals ((2 . 3) 1 #f (0.0 1.0 0.0)) and ((3 . 5) -1 #f (1.0 0.0 0.0))
  • Warning: Adding color_interval_engraver to a Staff context may lead to unexpected results if the Staff contains more than one voice.

\version "2.24.0"

%%% Create an engraver that colors note heads depending on the 
%%% intervals between successive pitches

%% Interval definitions alist
%%
%% Key:
%% number determines the interval type, 1=prime, 2=second, 3=third ...
%% plus and minus signs determine variant, no sign=perfect interval, +=major,
%% ++=augmented, -=minor, --=diminished
%% This naming scheme is arbitrary, it is possible to label the interval-types
%% differently, like
%%
%% '(("A1" . (0 . 1))
%%   ("P1" . (0 . 0))
%%   ("m2" . (1 . 1)) etc.
%%
%% if an argument list using the same labels is passed to the engraver.
%%
%% Value:
%% the car represents the diatonic, the cdr the semitonic steps.
%% Only positive values are specified, negative values for
%% intervals downwards are generated in the engraver.
%% This list may be extended or completely overwritten
%% Usage: #(display (assoc-get "4--" intervaldefs))

#(define intervaldefs
   '(("1++" . (0 . 1))
     ("1" . (0 . 0))
     ("2-" . (1 . 1))
     ("2--" . (1 . 0))
     ("2+" . (1 . 2))
     ("2++" . (1 . 3))
     ("3-" . (2 . 3))
     ("3--" . (2 . 2))
     ("3+" . (2 . 4))
     ("3++" . (2 . 5))
     ("4--" . (3 . 4))
     ("4++" . (3 . 6))
     ("4" . (3 . 5))
     ("5--" . (4 . 6))
     ("5++" . (4 . 8))
     ("5" . (4 . 7))
     ("6-" . (5 . 8))
     ("6--" . (5 . 7))
     ("6+" . (5 . 9))
     ("6++" . (5 . 10))
     ("7-" . (6 . 10))
     ("7--" . (6 . 9))
     ("7+" . (6 . 11))
     ("7++" . (6 . 12))
     ("8--" . (7 . 11))
     ("8++" . (7 . 13))
     ("8" . (7 . 12))
     ("9-" . (8 . 13))
     ("9--" . (8 . 12))
     ("9+" . (8 . 14))
     ("9++" . (8 . 15))
     ("10-" . (9 . 15))
     ("10--" . (9 . 14))
     ("10+" . (9 . 16))
     ("10++" . (9 . 17))
     ("11--" . (10 . 16))
     ("11++" . (10 . 18))
     ("11" . (10 . 17))
     ("12--" . (11 . 18))
     ("12" . (11 . 19))))

%% Create an engraver that compares the intervals between sequential pitches
%% of a voice with a given list of intervals.
%% If a specified interval is found, the heads of both notes encompassing
%% the interval are colored.
%%
%% Mode of operation:
%% Intervals are defined by two integers representing the diatonic
%% resp. semitonic distance between two pitches.
%% It is necessary to take both distances into account to distinguish
%% between enharmonically identical intervals, e.g. a major third
%% and a diminished fourth.
%% Example:
%% d -> f# : diatonic distance = 2 steps (f# is derived from f natural),
%% semitonic distance = 4 steps
%% d -> gb: diatonic distance = 3 steps (gb is derived from g natural),
%% semitonic distance = 4 steps
%%
%% The engraver consists of two parts:
%%
%% color_interval_engraver: checks, whether the given parameters are valid,
%% looks up the interval in the interval definitions alist and hands
%% the determined interval distances together with the other unchanged
%% parameters over to the actual engraver color-interval-engraver-core.
%%
%% color-interval-engraver-core: creates a scheme-engraver which
%% acknowledges note head grobs and stores the last and
%% current grob locally. Then the pitches are extracted and the interval between
%% the last and current pitch is compared to the specified interval.
%%
%% Usage:
%% \color_interval_engraver #intervaldefs #debug? intervals-given
%%
%% intervaldefs: alist containing information about semitonical distances for
%% certain intervals, diatonical distance is calculated in the engraver using
%% `string-diatonic-semi-tonic-list`, relying on the key.
%%
%% debug?: (optional) boolean, if true, output information about the processed
%% pitches
%%
%% intervals-given: list of the form
%%   #`((interval1 ,dir1 enh1 ,color1)
%%      (interval2 ,dir2 enh2 ,color2)
%%      ...
%%      (intervalN ,dirN enhN ,colorN))
%% with
%% intervalN: string - specifying the interval to search after
%% dirN: integer - UP (=1) DOWN (=-1) or 0 (up and down)
%% enhN: boolean - search for enharmonically equivalent intervals, too?
%% colorN: lilypond color value, see NR A.7.
%%
%% Constructing the argument list with `(= quasiquote) provides
%% an elegant shorthand for (list (list interval1 dir1 enh1 color1)
%%                                (list interval2 dir2 enh2 color2))
%% This would not work with '(= quote), because this special form does
%% not allow to unquote certain list elements with the comma ,
%% The directions UP and DOWN and the color values, however, need
%% to be evaluated to the corresponding integer values resp.
%% RGB values.
%%
%% \layout {
%%   \context {
%%     \Voice
%%     \consists \color_interval_engraver #intervaldefs
%%       `(("2--" ,UP #f ,green)
%%         ("3+" ,DOWN #t ,blue))
%%   }
%% }

color_interval_engraver =
#(define-scheme-function (interval-defs debug? intervals-given)
   (list? (boolean?) list?) ;; debug? is optional, defaults to #f

   (let* ((msg-header "Color_interval_engraver:")
          ;; 2.18.2 does not accept an empty list as engraver, unlike 2.19.x
          (empty-engraver
           (make-engraver ((initialize translator) '())))
          (type-check-interval
           (lambda (interval)
             ;; basic check for amount of args
             (if (not (= 4 (length interval)))
                 (begin
                  (ly:warning
                   "~a Interval ~a must have 4 entries" msg-header interval)
                  #f)
                 ;; check every entry for type, additionally the first entry
                 ;; whether it's a key in intervaldefs
                 (let ((name (car interval))
                       (dir (second interval))
                       (enh? (third interval))
                       (color (fourth interval)))
                   (and
                    ;; check first entry for string? and
                    ;; whether it's in intervaldefs
                    (if (and (string? name) (assoc-get name intervaldefs))
                        #t
                        (begin
                         (ly:warning
"~a In interval ~a, ~a not found in interval definitions"
                          msg-header
                          interval
                          (car interval))
                         #f))
                    ;; check second entry for ly:dir?
                    ;; As opposed to the normal meaning of 0 (=CENTER),
                    ;; 0 means up >and< down here
                    (if (ly:dir? dir)
                        #t
                        (begin
                         (ly:warning
"~a In interval ~a, wrong type argument: ~a, needs to be a direction."
                          msg-header
                          interval
                          dir)
                         #f))
                    ;; check third entry for boolean?
                    (if (boolean? enh?)
                        #t
                        (begin
                         (ly:warning
"~a In interval ~a, wrong type argument: ~a, needs to be a boolean."
                          msg-header
                          interval
                          enh?)
                         #f))
                    ;; check fourth entry for color?
                    (if (color? color)
                        #t
                        (begin
                         (ly:warning
"~a In interval ~a, wrong type argument: ~a, needs to be a color."
                          msg-header
                          interval
                          color)
                         #f)))))))
          (cleaned-intervals-given
           (filter type-check-interval intervals-given))
          (search-intervals
           (map
            (lambda (interval)
              (let ((diatonic-semitonic-pair
                     (assoc-get (car interval) interval-defs)))
                (cons diatonic-semitonic-pair (cdr interval))))
            cleaned-intervals-given)))

     (if debug?
         (begin
          (ly:message "~a Preprocessed intervals:\n" msg-header)
          (for-each
           (lambda (search-interval)
             (format (current-error-port)
               "Distances (DT/ST):~a, direction:~a, enharmonic:~a, color:~a\n"
               (car search-interval)
               (second search-interval)
               (third search-interval)
               (fourth search-interval)))
           search-intervals)))

     (if (null? search-intervals)
         (begin
          (ly:warning
           "~a No valid interval found. Returning empty engraver" msg-header)
          empty-engraver)
         ;; Instantiate actual engraver
         (color-interval-engraver-core search-intervals debug?))))


#(define (color-interval-engraver-core search-intervals debug?)
   (lambda (context)
     ;; Context type: Staff, Voice, etc.
     ;; Context id: arbitrary string
     ;; \new <context-type> = <context-id> \music
     ;; \new Voice = "soprano" \music
     (let ((engraver-name "Color_interval_engraver")
           (context-type (ly:context-name context))
           (context-id (let ((id (ly:context-id context)))
                         (if (string-null? id)
                             "N/A"
                             id)))
           ;; Later we want to extract the current bar number from there
           (score-context (ly:context-find context 'Score))
           (noteheads-to-process '())
           (ready-to-process? #f)
           (last-noteheads-color #f)
           (last-interval #f))
       (make-engraver
        ((initialize translator)
         ;; Output a warning if the engraver has been added to a Staff context
         ;; If the Staff consists of more than one Voice, the engraver cannot
         ;; distinguish the different voices and will mix them up
         (if (eq? context-type 'Staff)
             (ly:warning
              (string-append
               "Adding color_interval_engraver to a Staff context may lead "
               "to unexpected results if the Staff contains more than one "
               "voice."))))
        ;; This engraver does not listen to events, thus it does not
        ;; define listeners. It does only acknowledge grobs,
        ;; specifically note heads created by other engravers.
        (acknowledgers
         ((note-head-interface engraver grob source-engraver)
          (if ready-to-process?
              ;; if we have two note heads already, push the old one out
              (set! noteheads-to-process (list grob (car noteheads-to-process)))
              ;; We need two note heads to compare the underlying pitches
              ;; -> store note heads until we have two
              (begin
               (set! noteheads-to-process (cons grob noteheads-to-process))
               (if (= (length noteheads-to-process) 2)
                   (set! ready-to-process? #t))))

          ;; Check for grobs in the queue, before continuing
          (if ready-to-process?
              ;; Note head grobs store a reference to the
              ;; event that caused their generation
              ;; Thus we can extract the pitch
              (let* ((current-bar-number
                      (ly:context-property score-context 'currentBarNumber))
                     (current-moment (ly:context-current-moment context))
                     (grob-causes (map (lambda (grob)
                                         (ly:grob-property grob 'cause))
                                    noteheads-to-process))
                     (pitches (map (lambda (cause)
                                     (ly:event-property cause 'pitch))
                                grob-causes))
                     ;; Calculate interval distances, diatonic and semitonic
                     (current-interval-dist-diatonic
                      (apply - (map ly:pitch-steps pitches)))
                     (current-interval-dist-semitonic
                      (apply - (map ly:pitch-semitones pitches)))
                     ;; Check if a given interval matches the current interval
                     (interval-match?
                      (lambda (search-interval)
                        (let* ((search-interval-dist (car search-interval))
                               (search-interval-dir (second search-interval))
                               (search-interval-enh? (third search-interval))
                               (search-interval-dist-diatonic
                                (car search-interval-dist))
                               (search-interval-dist-semitonic
                                (cdr search-interval-dist)))
                          ;; if search-interval-enh? was set to true for
                          ;; the current interval, compare only the semitonic
                          ;; distances, e.g. c#-f would also match a major 3rd,
                          ;; not only a diminished 4th
                          ;;
                          ;; search-interval-dir can only be -1, 0, 1
                          ;; other values are excluded by typechecking,
                          ;; thus 0 needs special casing,
                          ;; for other cases multiplying relevant value with
                          ;; search-interval-dir is enough
                          ;;   -- harm
                          (if (zero? search-interval-dir)
                              (and
                               ;; if direction does not matter, compare
                               ;; with absolute values
                               (= search-interval-dist-semitonic
                                  (abs current-interval-dist-semitonic))
                               (if (not search-interval-enh?)
                                   (= search-interval-dist-diatonic
                                      (abs current-interval-dist-diatonic))
                                   #t))
                              (and
                               (= search-interval-dist-semitonic
                                  (* search-interval-dir
                                    current-interval-dist-semitonic))
                               (if (not search-interval-enh?)
                                   (= search-interval-dist-diatonic
                                      (* search-interval-dir
                                        current-interval-dist-diatonic))
                                   #t))))))
                     ;; Get first occurrence of a matching interval
                     (matching-interval (find interval-match? search-intervals))
                     ;; Extract color from matching interval
                     (search-interval-color (if matching-interval
                                                (fourth matching-interval)
                                                #f)))

                (if debug?
                    (let* ((cep (current-error-port)))
                      (format cep
                        "\n*** This is ~a from ~a ~a ***\n"
                        engraver-name context-type context-id)
                      (format cep "\nBar number ~a, moment ~a\n"
                        current-bar-number current-moment)
                      (format cep "\nPitches (last/current): ~a/~a\n"
                        (second pitches)
                        (first pitches))
                      (format cep "\nDistance (diatonic/semitonic): ~a/~a\n"
                        current-interval-dist-diatonic
                        current-interval-dist-semitonic)
                      (if matching-interval
                          (begin
                           (format cep "\nMatch! Found interval ~a, coloring ~a\n"
                             matching-interval search-interval-color)
                           (if last-noteheads-color
                               (format cep
                                 "\nRecoloring - Last note heads color: ~a\n"
                                 last-noteheads-color))))
                      (display "\n---------------------\n" cep)))

                (if search-interval-color
                    (begin
                     ;; Check if the note heads directly preceding were
                     ;; colored, too. If true, the last note head belongs
                     ;; to two distinct intervals
                     ;;
                     ;; <noteheads-to-process>
                     ;; (grobB grobA)
                     ;; interval grobB<->grobA matches -> color!
                     ;; (grobB_colored grobA_colored)
                     ;; <next iteration>
                     ;; (grobC grobB_colored)
                     ;; interval grobC<->grobB matches -> color!
                     ;; (grobC_colored grobB_colored_colored (!))
                     ;; -> information about interval grobA<->grobB gets lost
                     ;; In this case, print a warning
                     (if last-noteheads-color
                         (ly:warning
                          (string-append
                           "~a: Recoloring note head in ~a ~a, bar number ~a\n"
                           "~a belongs to intervals ~a and ~a")
                          engraver-name
                          context-type
                          context-id
                          current-bar-number
                          (second pitches)
                          last-interval
                          matching-interval))
                     ;; Color current and last note head grob
                     (for-each
                      (lambda (grob)
                        (ly:grob-set-property!
                         grob
                         'color
                         search-interval-color))
                      noteheads-to-process)))
                ;; Preserve the current color (if any) for recoloring check
                ;; (see above)
                (set! last-noteheads-color search-interval-color)
                (set! last-interval matching-interval)))))))))


\markup \column {
  \line {
    "Diminished second," \with-color #green "up" "and" \with-color #blue "down"
  }
  \line {
    "Minor second," \with-color #yellow "up" "and" \with-color #cyan "down"
  }
  \line {
    "Major second," \with-color #red "up" "and" \with-color #darkgreen "down"
  }
  \line {
    "Augmented second," \with-color #darkcyan "up"
    "and" \with-color #darkyellow "down"
  }
}

\score {
  \new Voice
  \relative c'' {
    fis4 g e d as gis cis bes f g cis des des, e g fis
  }
  \layout {
    \context {
      \Voice
      \consists
      \color_interval_engraver #intervaldefs
      #`(("2--" ,UP #f ,green)
         ("2--" ,DOWN #f ,blue)
         ("2-" ,UP #f ,yellow)
         ("2-" ,DOWN #f ,cyan)
         ("2+" ,UP #f ,red)
         ("2+" ,DOWN #f ,darkgreen)
         ("2++" ,UP #f ,darkcyan)
         ("2++" ,DOWN #f ,darkyellow)
         ;; Not specified interval
         ;("2+++" ,DOWN #f ,darkyellow)
         ;; Direction not suitable
         ;("2++" 2 #f ,darkyellow)
         ;; Wrong type argument for 'searching enharmonically equivalent, too?'
         ;("2++" ,DOWN foo ,darkyellow)
         ;; Wrong type for color
         ;("2++" ,DOWN #f (1 2 3 4 5))
         ;; Wrong amount of entries
         ;("2++" ,DOWN #f)
         )
    }
  }
}

\markup \column {
  "Color intervals regardless of direction"
  \with-color #green "Diminished third"
  \with-color #yellow "Minor third"
  \with-color #red "Major third"
  \with-color #darkcyan "Augmented third"
}

\score {
  \new Staff \relative c' { d4 f e cis gis' e f a d bis cis as e ges des fis }
  \layout {
    \context {
      \Voice
      \consists \color_interval_engraver #intervaldefs
      #`(("3--" 0 #f ,green)
         ("3-" 0 #f ,yellow)
         ("3+" 0 #f ,red)
         ("3++" 0 #f ,darkcyan))
    }
  }
}

\markup \column {
  "Color enharmonically equivalent intervals, too"
  \with-color #green "Augmented second, minor third"
}

\score {
  \new Staff \relative c' { d4 f e a ges }
  \layout {
    \context {
      \Voice
      \consists \color_interval_engraver #intervaldefs #`(("3-" 0 #t ,green))
    }
  }
}

\markup \column {
  "Output warning, if note belongs to two intervals"
  \line { \with-color #green "Minor third" and
          \with-color #red "perfect fourth" }
}

\score {
  \new Staff \relative c' { c4 es bes des }
  \layout {
    \context {
      \Voice
      \consists \color_interval_engraver #intervaldefs
      #`(("3-" ,UP #f ,green)
         ("4" ,DOWN #f ,red))
    }
  }
}

\markup \column {
  "Output debug information"
  \line { \with-color #green "Minor second" and
          \with-color #red "perfect fourth" }
}

\score {
  <<
    \new Voice = "Soprano" \relative c' {
      \key b \minor
      \partial 2 fis2
      e2. fis4
      b2 a4 r
      cis fis, b a
      gis2 fis4 r
    }
    \new Voice = "Alto" \relative c' {
      \key b \minor
      \partial 2 d2
      cis2. d4
      d (e) fis r
      fis (fis) eis fis
      fis eis fis r
    }
  >>
  \layout {
    \context {
      \Voice
      \consists \color_interval_engraver #intervaldefs ##t
      #`(("2-" ,DOWN #f ,green)
         ("4" ,UP #f ,red))
    }
  }
}

\markup \column {
  "Output warning, if engraver has been added to staff instead of voice context"
  \line { \with-color #green "Minor second" and
          \with-color #red "perfect fourth" }
}

\score {
  \new Staff <<
    \new Voice = "Soprano" \relative c' {
      \voiceOne
      \key b \minor
      \partial 2 fis2
      e2. fis4
      b2 a4 r
      cis fis, b a
      gis2 fis4 r
    }
    \new Voice = "Alto" \relative c' {
      \voiceTwo
      \key b \minor
      \partial 2 d2
      cis2. d4
      d (e) fis r
      fis (fis) eis fis
      fis eis fis r
    }
  >>
  \layout {
    \context {
      \Staff
      \consists \color_interval_engraver #intervaldefs
      #`(("4" ,UP #f ,red)
         ("2-" ,DOWN #f ,green))
    }
  }
}

\paper { tagline = ##f }