Hi Ben, hi Greg,
thanks for bringing this up - in fact I started this morning to dig up
my old work, prompted by Greg's question.
It seems I only developed the functions a but further back then
(unfortunately I don't have time at the moment to look into it in
detail) - the difference seems to be that now, bends also avoid Dots.
See the attached version.
Lukas
Am 13.04.24 um 10:58 schrieb Benjamin Tordoff:
Hi Greg, the conversation on this list back in Dec'22 and Jan'23 ended
up with Lukas-Fabian Moser sending me the attached script and example.
I was able to use this successfully to typeset a range of scoops,
bends, and falls. It'd be better to get the latest version from
Lukas-Fabian in case he has done more work on it, but this might do
what you need. You can see the results I got using this here:
https://pluralsax.com/music-entry/147/alabamy-bound.
Whilst the script is complicated (and well beyond my understanding),
using it was not.
There was some discussion of whether this script (or something like
it) could make it into the main lilypond install but I don't think it
has yet.
Cheers
Ben
On 13 Apr 2024, at 08:23, Hans Aikema <[email protected]> wrote:
On 13 Apr 2024, at 01:07, Greg Lindstrom <[email protected]> wrote:
Hello -
I would like to write a glissando leading into a note without a
starting note. Perhaps the term is not glissando, but we encounter
it often in jazz where the trumpets and trombones will "slide" up to
a note to accentuate. It may be called a "scoop" but that's
speculation on my part. Here's an example: Can this be done (by a
mortal)?
Should be possible by tricking lilypond like in
https://lists.gnu.org/archive/html/lilypond-user/2009-06/msg00132.html
In that and other threads on scoops there are a few other tricks to
achieve the same goal, but I think the approach in that message is
the easiest to comprehend.
My impression (no time to validate now) is that the trick from that
post would pollute the midi file. So in case you need a more or less
clean midi (which will nit contain any effect for the scoop) rather
than just display in the sheet music the more complex workaround given in
https://lists.nongnu.org/archive/html/lilypond-user/2022-12/msg00314.html
might be worth a try
HTH
Hans
<image.png>
\version "2.24.0"
#
(define-public (number-or-number-list? x)
(or (number? x) (number-list? x)))
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#(set-object-property! 'contour 'backend-type? number-or-number-list?)
#(set-object-property! 'maximum-length 'backend-type? ly:dimension?)
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#(define-public (ly:event-length ev now)
;;; quick'n'dirty variant of proper C++ function
(ly:event-property ev 'length))
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#
(define (hash-move target source key)
(let ((value (hash-ref source key)))
(when value
(hash-set! target key value)
(hash-remove! source key))))
#
(define (Bend_after_engraver context)
(let
((bend-after-ev #f)
(current-note-column #f)
(note-heads '())
(bend-after-grobs (make-hash-table))
(finished-bend-after-grobs (make-hash-table)))
(make-engraver
((start-translation-timestep engraver)
(hash-clear! finished-bend-after-grobs))
(listeners
((bend-after-event engraver event)
(set! bend-after-ev event)))
(acknowledgers
((note-column-interface engraver grob source-engraver)
(set! current-note-column grob))
((note-head-interface engraver grob source-engraver)
(set! note-heads (cons grob note-heads))))
((stop-translation-timestep engraver)
(let ((now (ly:context-current-moment context)))
(hash-for-each
(lambda (grob alist)
(when
;; For falls starting in grace time, compare actual
;; moments to make sure the band spans the full (grace)
;; length of the main note.
;; For falls starting in non-grace time, compare only
;; moment-main's to make sure the fall gets terminated
;; before any grace notes coming before the next note.
(if (assq-ref alist 'start-in-grace)
(not (ly:moment<? now (assq-ref alist 'stop-moment)))
(>= (ly:moment-main now)
(ly:moment-main (assq-ref alist 'stop-moment))))
(ly:spanner-set-bound!
grob RIGHT
(if (ly:grob? (ly:context-property context 'currentBarLine))
;; don't cross a barline!
(ly:context-property context 'currentCommandColumn)
(if current-note-column
current-note-column
(ly:context-property context 'currentMusicalColumn))))
(hash-move finished-bend-after-grobs bend-after-grobs grob)))
bend-after-grobs)
(for-each
(lambda (head)
(let
((head-bend-after-ev
(find
(lambda (art) (memq 'bend-after-event
(ly:event-property art 'class)))
(ly:event-property (event-cause head) 'articulations))))
(when (or bend-after-ev head-bend-after-ev)
(let*
((event (if bend-after-ev bend-after-ev head-bend-after-ev))
(after-bend (ly:engraver-make-grob engraver 'BendAfter event))
(start-in-grace (negative? (ly:moment-grace now)))
(stop-moment (ly:moment-add now
(ly:event-length (event-cause head)
now)))
(contour (ly:event-property event 'bend-contour)))
(ly:grob-set-property! after-bend 'contour contour)
(ly:spanner-set-bound! after-bend LEFT head)
(ly:grob-set-parent! after-bend Y head)
(hash-set!
bend-after-grobs after-bend
`((start-in-grace . ,start-in-grace)
(stop-moment . ,stop-moment)))))))
note-heads))
(set! note-heads '())
(set! bend-after-ev #f))
((finalize engraver)
(hash-for-each
(lambda (grob alist)
(ly:spanner-set-bound!
grob RIGHT (ly:context-property context 'currentCommandColumn)))
finished-bend-after-grobs)))))
#
(define (Bend_before_engraver context)
(let
((previous-notehead-column #f)
(current-note-column #f)
(bend-before-ev #f)
(bends-created '())
(noteheads '()))
(make-engraver
(listeners
((bend-before-event engraver event #:once)
(set! bend-before-ev event)))
(acknowledgers
((note-column-interface engraver grob source-engraver)
(set! current-note-column grob))
((rhythmic-grob-interface engraver grob source-engraver)
(set! noteheads (cons grob noteheads))))
((process-acknowledged engraver)
(when (ly:context-property context 'currentBarLine #f)
(set! previous-notehead-column #f))
(for-each
(lambda (notehead)
(let
((head-bend-before-ev
(find
(lambda (art) (memq 'bend-before-event
(ly:event-property art 'class)))
(ly:event-property (event-cause notehead) 'articulations))))
(when (or bend-before-ev head-bend-before-ev)
(let*
((event (if bend-before-ev bend-before-ev head-bend-before-ev))
(bend-before
(ly:engraver-make-grob engraver 'BendBefore event)))
(set! bends-created (cons bend-before bends-created))
(ly:grob-set-parent! bend-before Y notehead)
(ly:grob-set-property! bend-before 'contour
(ly:event-property event 'bend-contour))
(ly:spanner-set-bound!
bend-before LEFT
(if previous-notehead-column
previous-notehead-column
(ly:context-property context 'currentCommandColumn)))
(ly:spanner-set-bound! bend-before RIGHT
notehead)))))
noteheads)
(set! noteheads '()))
((stop-translation-timestep engraver)
(for-each
(lambda (grob) ;;; Proof of concept: It's possible to compensate for
accidentals.
;;; but it's hard to calculate the right amount of correction ...
(let
((acc (ly:grob-object (ly:grob-parent grob Y) 'accidental-grob)))
(when (ly:grob? acc)
(ly:grob-set-property! grob 'minimum-length
(+ (ly:grob-property grob 'minimum-length)
(interval-length (ly:grob-extent acc acc X))
)))))
bends-created)
(set! bends-created '())
(set! bend-before-ev #f)
(set! previous-notehead-column current-note-column)))))
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#
(define-public (map-neighbors fun lst)
"Apply the bivariate function @code{fun} to all successive pairs of
elements of @code{lst}: If @code{lst} is @code{(a b c ... z)},
then @code{(list (fun a b) (fun b c) ... (fun y z))} is returned."
;;;; equivalent to (map (lambda (p) (apply fun p)) (zip lst (cdr lst))))
(if (and (pair? lst) (pair? (cdr lst)))
(cons (fun (first lst) (second lst))
(map-neighbors fun (cdr lst)))
'()))
#
(define*-public (make-bend-stencil thickness dx def dir left-flat right-flat)
"Make bend stencil of given thickness. It will start near-horizontally at
@code{(0,0)} and end at x-value @code{dx}, passing through the y-values given in
@code{def}. If @code{left-flat} resp. @code{right-flat} is @code{#t}, then
the respective end will be near-horizontal. @code{dx} may also be negative.
If @code{direction} is negative, then the curve will be mirrored,
starting in @code{(dx,0)}."
(if (negative? dir)
(ly:stencil-translate-axis
(make-bend-stencil thickness (- dx) (reverse def) RIGHT right-flat
left-flat) dx X)
(let*
((y-stations (cons 0 def))
(steps (length def))
(step-dx (/ dx steps))
(make-path-section
(lambda (from to)
(let ((delta-y (- to from)))
`(rcurveto
,(* 1/3 step-dx) 0
,(* 2/3 step-dx) ,delta-y
,step-dx ,delta-y))))
(path-desc
(apply append (map-neighbors make-path-section y-stations))))
(when (not left-flat)
;; replace near-horizontal start of the form
;; rcurveto 1/3*dx 0 2/3*dx dy dx dy
;; by
;; rcurveto 0 1/3*dy 2/3*dx dy dx dy
(list-set! path-desc 1 0)
;; take 6th element to be safe in the case that
;; left and right are attached to the same part-curve
(list-set! path-desc 2 (* 1/3 (list-ref path-desc 6))))
(when (not right-flat)
;; replace near-horizontal tail end of the form
;; delta-y 1/3*dx 0 2/3*dx dy dx dy
;; by
;; delta-y 1/3*dx 0 dx 2/3*dy dx dy
;; for steep ending (as in pre-2.25.1 \bendAfter)
(let*
((tail-quadruple-ref (- (* steps 7) 4))
(tail-quadruple (list-tail path-desc tail-quadruple-ref)))
(list-cdr-set!
path-desc (1- tail-quadruple-ref)
(list
(third tail-quadruple) (* 2/3 (fourth tail-quadruple))
(third tail-quadruple) (fourth tail-quadruple)))))
(ly:make-stencil
`(path ,thickness ,path-desc)
(ordered-cons 0 dx)
(cons (apply min y-stations) (apply max y-stations))))))
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#
(define (get-accidentals grob)
;;; return AccidentalPlacement for grob.
;;; grob might be a "small" grob (NoteHead), a MusicalPaperColumn (which
actually
;;; has the Accidentals I think), or a "large" grob (PaperColumn), so
;;; we have to look both ways
(cond
((not (ly:grob? grob))
'())
((grob::has-interface grob 'rhythmic-grob-interface)
(get-accidentals (ly:grob-object grob 'axis-group-parent-Y)))
((grob::has-interface grob 'note-column-interface)
(ly:note-column-accidentals grob))
(else
'())))
#
(define (get-dots grob)
;;; return DotColumn for grob.
;;; grob might be a "small" grob (NoteHead), a MusicalPaperColumn (which
actually
;;; has the Accidentals I think), or a "large" grob (PaperColumn), so
;;; we have to look both ways
(cond
((not (ly:grob? grob))
'())
((grob::has-interface grob 'rhythmic-grob-interface)
(get-dots (ly:grob-object grob 'axis-group-parent-Y)))
((grob::has-interface grob 'dot-column-interface)
(let ((dot-array (ly:grob-object grob 'dots #f)))
(if dot-array (ly:grob-array->list dot-array) '())))
((grob::has-interface grob 'note-column-interface)
(get-dots (ly:note-column-dot-column grob)))
(else
'())))
#
(define-public (new::bend::print spanner)
;;; For best results, make sure both bounds of spanner are either noteheads
;;; or NoteColumns.
(define (close? a b)
(< (abs (- a b)) 0.01))
(define (flat-end? side-details)
(let ((shape (assoc-get 'shape side-details)))
(case shape
((flat) #t)
((steep) #f)
(else
(ly:warning (G_ "Unknown end-shape for bend: ~a. \
Using steep ending.") shape)
#f))))
;;; TODO: Maybe use attach-dir instead of direction?
(let*
((dir (ly:grob-property spanner 'direction RIGHT))
(visible-broken-part?
(if (positive? dir) first-broken-spanner? end-broken-spanner?)))
(if (or (unbroken-spanner? spanner)
(visible-broken-part? spanner))
(let* ((left-span (ly:spanner-bound spanner LEFT))
(left-dots (get-dots left-span))
(right-span (ly:spanner-bound spanner RIGHT))
(right-accs (get-accidentals right-span))
(thickness (* (ly:grob-property spanner 'thickness)
(ly:output-def-lookup (ly:grob-layout spanner)
'line-thickness)))
(contour (ly:grob-property spanner 'contour))
(padding (ly:grob-property spanner 'padding 0.5))
(Y-attachment (/ (ly:grob-property spanner 'Y-attachment 0) 2))
(contour-left-y (if (positive? dir)
Y-attachment
(/ (if (pair? contour) (car contour) contour) 2)))
(bound-details (ly:grob-property spanner 'bound-details))
(common-x (ly:grob-common-refpoint right-span
(ly:grob-common-refpoint spanner
left-span X)
X))
(maximum-length (ly:grob-property spanner 'maximum-length +inf.0))
(left-x (+ padding
(max
(interval-end (ly:generic-bound-extent
left-span common-x))
(if
(any
(lambda (dot)
(let
((common-y
(ly:grob-common-refpoint dot spanner Y)))
(close?
(ly:grob-relative-coordinate dot common-y Y)
(+ (ly:grob-relative-coordinate spanner
common-y Y)
contour-left-y))))
left-dots)
(interval-end
(ly:grob-robust-relative-extent (car left-dots)
common-x X))
-inf.0))))
(right-x (- (min
(interval-start (ly:generic-bound-extent
right-span common-x))
(if (ly:grob? right-accs)
(interval-start
(ly:grob-robust-relative-extent right-accs
common-x X))
+inf.0))
padding))
(self-x (ly:grob-relative-coordinate spanner common-x X)))
(if (positive? dir)
(set! right-x (min right-x (+ left-x maximum-length)))
(set! left-x (max left-x (- right-x maximum-length))))
(when (>= left-x right-x)
(ly:grob-warning spanner #f "Zero or negative effective width. \
Consider increasing minimum-length property!"))
(when (number? contour)
(set! contour (list contour)))
(ly:stencil-translate
(make-bend-stencil thickness
(- right-x left-x)
;; contour values are measured in steps = half
staff spaces
(map (lambda (y) (- (/ y 2) Y-attachment)) contour)
dir
(flat-end? (assoc-get 'left bound-details))
(flat-end? (assoc-get 'right bound-details)))
(cons (- left-x self-x) Y-attachment)))
#f)))
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
bendAfter =
#(define-event-function (contour) (number-or-number-list?)
(_i "Create a fall or doit of pitch interval @var{delta}.")
(make-music 'BendAfterEvent
'bend-contour contour))
bendBefore =
#(define-event-function (contour) (number-or-number-list?)
(_i "Create a ... of pitch interval @var{delta}.")
(make-music 'BendBeforeEvent
'bend-contour contour))
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#(define-event-class 'bend-before-event 'music-event)
#(define my-types
'((BendBeforeEvent
. ((description . "A ... jazz articulation.")
(types . (post-event bend-before-event event))))))
#(set!
my-types
(map (lambda (x)
(set-object-property! (car x)
'music-description
(cdr (assq 'description (cdr x))))
(let ((lst (cdr x)))
(set! lst (assoc-set! lst 'name (car x)))
(set! lst (assq-remove! lst 'description))
(hashq-set! music-name-to-property-table (car x) lst)
(cons (car x) lst)))
my-types))
#(set! music-descriptions
(append my-types music-descriptions))
#(set! music-descriptions
(sort music-descriptions alist<?))
#(define (add-grob-definition grob-entry)
(set! all-grob-descriptions
(cons
((@@ (lily) completize-grob-entry)
grob-entry)
all-grob-descriptions)))
#(add-grob-definition
`(BendBefore
. ((minimum-length . 2)
(maximum-length . +inf.0)
(springs-and-rods . ,ly:spanner::set-spacing-rods)
(stencil . ,new::bend::print)
(direction . ,LEFT)
(thickness . 2.0)
(bound-details . ((left . ((shape . flat)))
(right . ((shape . steep)))))
(meta . ((class . Spanner)
(interfaces . (bend-after-interface))
(description . "A grob for displaying
@dfn{...} and @dfn{...}."))))))
\layout {
\context {
\Global
\grobdescriptions #all-grob-descriptions
}
\context {
\Voice
\remove Bend_engraver
\consists #Bend_before_engraver
\consists #Bend_after_engraver
\override BendAfter.direction = #RIGHT
\override BendAfter.bound-details.left.shape = #'flat
\override BendAfter.bound-details.right.shape = #'steep
\override BendAfter.stencil = #new::bend::print
\override BendAfter.springs-and-rods = #ly:spanner::set-spacing-rods
\override BendAfter.minimum-length = 2
\override BendAfter.maximum-length = #+inf.0
}
}
%{
<<
\new Staff {
\repeat unfold 28 e'8
}
\new Staff \relative {
\override BendAfter.minimum-length = 12
\override BendBefore.minimum-length = 8
d'4. \bendAfter 3 es8 d2
e4. \bendAfter 3 es8 d2
e4. es8\bendBefore 1 d2
d4.. f8 \bendBefore -2
}
>>
\new Staff \relative {
\override BendAfter.Y-attachment = 2
\override BendAfter.minimum-length = 20
\override BendBefore.minimum-length = 12
<b \bendAfter #'(-2 2) c d>2. <bes ces des es fes>4
<b d e>2... c8 \bendBefore #'(1 2)
}
% TODO: Warn if bend gets negative length!
\new Staff {
\override BendBefore.minimum-length = 2
es'1
es'4 \bendBefore -3
d' \bendBefore -3
cis' \bendBefore -3
d'
e' \bendBefore -3
}
%}\version "2.24.0"
\include "bend-engravers 2022-01-08.ily"
\relative {
\override Glissando.thickness = 2 % default thickness for bendAfter/bendBefore
g'2 \glissando d'\bendAfter -2.5
f1
\bendBefore -2.5 \tweak bound-details.right.shape #'flat \bendAfter #'(-2.5 0)
f
}
\score {
\layout {
ragged-right = ##f
}
\relative {
\override BendBefore.bound-details.right.shape = #'flat
\override BendAfter.bound-details.right.shape = #'flat
<a' d>4
<c, f> \bendBefore #'(5 -5 4 -4 3 -3 2 -2 1 -1)
\bendAfter #'(-2 3)
<f b>
}
}
doit = \bendAfter 3
scoop = \bendBefore -3
\score {
\layout {
ragged-right = ##t
}
\relative {
c'4\doit d\doit e\doit f\doit
\override BendAfter.minimum-length = 5
c4\doit d\doit e\doit f\doit
\override BendAfter.maximum-length = 1
c4\doit d\doit e\doit f\doit
\bar "."
c\scoop d\scoop e\scoop f\scoop
\override BendBefore.minimum-length = 4
\override BendBefore.maximum-length = 2
c\scoop d\scoop e\scoop f\scoop
}
}
\score {
\relative {
\override BendAfter.minimum-length = 8
\override BendBefore.minimum-length = 4
f'4 r r a \bendBefore -2 % bend should not span rests!
}
}
{
c'4 \bendAfter -2
\override BendAfter.Y-attachment = 1
4 \bendAfter -2
\override BendAfter.Y-attachment = 2
4 \bendAfter -2
\override BendAfter.Y-attachment = 5
4 \bendAfter -2
}
<<
\new Staff {
\repeat unfold 28 e'8
}
\new Staff \relative {
\override BendAfter.minimum-length = 12
\override BendBefore.minimum-length = 8
d'4. \bendAfter 3 es8 d2
e4. \bendAfter 3 es8 d2
e4. es8\bendBefore 1 d2
d4.. f8 \bendBefore -2
}
>>
{
\override BendAfter.minimum-length = 12
c'4\bendAfter -5 <des' f' a>
c'4\bendAfter -5 <des' f' a>
c'1\bendAfter #'(2 -3 4) c'
}
{
\override BendBefore.minimum-length = 10
\override BendAfter.minimum-length = 10
c''4.. e''16 \bendBefore -2
c''4.. \bendAfter 2 es''16
}
%}