Am Sa., 21. Dez. 2019 um 00:35 Uhr schrieb Michael Käppler <[email protected]>:
>
> Hi Harm et al.,
> attached is my updated version.
> I decided to split up the validity checks from the actual engraver,
> because otherwise in case of invalid parameters the engraver would
> only be instantiated to do nothing and nevertheless have its
> acknowledger called every time.
> The code is much more verbose now, which makes the flow of information
> and control
> clearer I hope.
>
> Please let me know what you think about it.
>
> Cheers,
> Michael
Hi Michael,
thanks again for your updated snippet.
I've taken a closer look.
You try to give the user always meaningful warning-messages.
That's great, putting out really helpful messages is hard work...
Alas, speaking only for me, I don't like those multiple nested `if`.
Thus I defined `type-check-intervals-given` and use it to filter the
user-given interval-list.
`filter` will return false for the first occurrence of failed
`type-check-intervals-given`. Thus it can be used to deal with
user-errors step-by-step.
Along with it, I added a basic check for the user provided list (about
equal length of each sublist)
There was an undefined variable `gen-warntext`, which is now gone as well.
Furthermore, I changed the basic `intervaldefs` to take only pairs of
the interval-string and the semi-tonoc steps. The diatonic steps are
calculated relying on the interval-string.
I found no need to do work in `process-acknowledged`.
Thus all work is done in 'note-head-interface of `acknowledgers`
Probably more efficient, but I have not really checked.
A plethora of minor changes in code and comments... ;)
WDYT?
Btw, there is one case, where I don't know how to deal with:
2.18.2 can't cope with an empty engraver, see:
\score {
\new Staff \relative c' { c4 d }
\layout {
\context {
\Voice
\consists \color_interval_engraver #intervaldefs #`(("30-" 0 #t ,green))
}
}
}
No problem for 2.19.83, though.
Cheers,
Harm
\version "2.18.2"
%% 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
%% Value:
%% the cdr represents the semitonic steps.
%% diatonic steps are calculated relying on the key in the engraver
%% 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++" . 1)
("1" . 0)
("2-" . 1)
("2--" . 0)
("2+" . 2)
("2++" . 3)
("3-" . 3)
("3--" . 2)
("3+" . 4)
("3++" . 5)
("4--" . 4)
("4++" . 6)
("4" . 5)
("5--" . 6)
("5++" . 8)
("5" . 7)
("6-" . 8)
("6--" . 7)
("6+" . 9)
("6++" . 10)
("7-" . 10)
("7--" . 9)
("7+" . 11)
("7++" . 12)
("8--" . 11)
("8++" . 13)
("8" . 12)
("9-" . 13)
("9--" . 12)
("9+" . 14)
("9++" . 15)
("10-" . 15)
("10--" . 14)
("10+" . 16)
("10++" . 17)
("11--" . 16)
("11++" . 18)
("11" . 17)
("12--" . 18)
("12" . 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))
%% }
%% }
#(use-modules (ice-9 pretty-print))
color_interval_engraver =
#(define-scheme-function (parser location intervaldefs debug? intervals-given)
(list? (boolean?) list?) ;; debug? is optional, defaults to #f
(define (string-diatonic-semi-tonic-list string-semi-tonic-list)
(map
(lambda (e)
(let* ((interval-string
(string-trim-both
(car e)
(lambda (c) (or (eqv? c #\+) (eqv? c #\-)))))
(interval-diatonic
(string->number interval-string)))
(cons (car e) (cons (1- interval-diatonic) (cdr e)))))
string-semi-tonic-list))
(define (type-check-intervals-given msg-header)
(lambda (interval)
;; basic check for amount of args
(if (= 4 (length interval))
#t
(begin
(ly:error
"~a Interval ~a must have 4 entries" msg-header interval)
#f))
;; check every entry for type, additonally 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?
(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))))))
(let* ((msg-header "Color_interval_engraver:")
(interval-defs-list (string-diatonic-semi-tonic-list intervaldefs))
(cleaned-intervals-given
(filter (type-check-intervals-given msg-header) intervals-given))
(search-intervals
;; mmh, not sure if `reverse` is really needed
(reverse
(map
(lambda (interval)
(let ((diatonic-semitonic-pair
(assoc-get (car interval) interval-defs-list)))
(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)
'())
;; Instantiate actual engraver
(color-interval-engraver-core search-intervals debug?))))
#(define (color-interval-engraver-core search-intervals debug?)
(lambda (context)
(let ((last-grob #f)
(current-grob #f))
(make-engraver
;; 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)
;; Store current and last note head grob
(set! last-grob current-grob)
(set! current-grob grob)
;; Check for grobs in the queue, before continuing
(if (and last-grob current-grob)
;; Note head grobs store a reference to the
;; event that caused their generation
;; Thus we can extract the pitch
(let* ((current-grob-cause
(ly:grob-property current-grob 'cause))
(current-pitch
(ly:event-property current-grob-cause 'pitch))
(last-grob-cause (ly:grob-property last-grob 'cause))
(last-pitch (ly:event-property last-grob-cause 'pitch))
;; Calculate interval distances, diatonic and semitonic
(current-interval-dist-diatonic
(- (ly:pitch-steps current-pitch)
(ly:pitch-steps last-pitch)))
(current-interval-dist-semitonic
(- (ly:pitch-semitones current-pitch)
(ly:pitch-semitones last-pitch)))
;; 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)))
(newline)
(format cep "Previous pitch: ~a\n" last-pitch)
(format cep "Current pitch: ~a\n" current-pitch)
(format cep "Diatonic diff: ~a\n"
current-interval-dist-diatonic)
(format cep "Semitonic diff: ~a\n"
current-interval-dist-semitonic)
(format cep "Matching interval: ~a\n" matching-interval)
(format cep "Grob color: ~a\n" search-interval-color)
(display "**********\n" cep)))
(if search-interval-color
(begin
;; Color current and last note head grob
(set! (ly:grob-property current-grob 'color)
search-interval-color)
(set! (ly:grob-property last-grob 'color)
search-interval-color)))))))))))
\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 Staff
\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))
}
}
}
\score {
\new Staff \relative c' { c4 d }
\layout {
\context {
\Voice
\consists \color_interval_engraver #intervaldefs #`(("30-" 0 #t ,green))
}
}
}