Adding Ornamentations to Note Heads (Part 3)

In part 1, we set up a base infrastructure to add an ornament to a note head; then, in part 2, we made it more generic and flexible, in other words, usable. In part 3, we’ll try to tackle collision issues:

collisions.preview


As can be seen in this image, other elements (accidentals, dots, other notes) are not “aware” of the ornaments we’ve added (shown in red), and thus collide with them.

This post may be disappointing, as I have not managed to set up a robust collision avoidance system for ornaments like the one that exists for accidentals, dots, or notes (since that would be far above my head).  Instead we’ll use a hack to avoid collisions with accidentals and dots, by translating (i.e. shifting) them so that they avoid the ornament. This is not completely satisfying because collisions with other note heads may still occur, and also because the engraver has to be moved into a Score context (to make sure that the other grobs it uses have already been built when it needs to use them).

When the ornament is placed on the left side of the note, and there are accidentals, we increase the accidentals’ padding property (by the ornament width) so that the accidentals are shifted to the left. And when the ornament is placed on the right, and there are dots, we horizontally translate (i.e. shift) the dots to the right. Here are the modifications in the context of the function from the last post:

#(define head-ornamentation-engraver
   (make-engraver
    (acknowledgers
     ((note-head-interface engraver note-grob source)
      ;; helper function to create HeadOrnamentation grobs
      (define (make-ornament-grob text direction shift-when-on-line)
        (let ((ornament-grob (ly:engraver-make-grob engraver
                                                    'HeadOrnamentation
                                                    note-grob)))
          ;; use the ornament event text as the grob text property
          (set! (ly:grob-property ornament-grob 'text) text)
          ;; set the grob direction (either LEFT or RIGHT)
          (set! (ly:grob-property ornament-grob 'direction) direction)
          ;; set the shift-when-on-line property using the given value
          (set! (ly:grob-property ornament-grob 'shift-when-on-line)
                shift-when-on-line)
          (ly:pointer-group-interface::add-grob ornament-grob
                                                'elements
                                                note-grob)
          ;; the ornamentation is vertically aligned with the note head
          (set! (ly:grob-parent ornament-grob Y) note-grob)
          ;; compute its font size
          (set! (ly:grob-property ornament-grob 'font-size)
                (+ (ly:grob-property ornament-grob 'font-size 0.0)
                   (ly:grob-property note-grob 'font-size 0.0)))
          ;; move accidentals and dots to avoid collision
          (let* ((ornament-stencil (ly:text-interface::print ornament-grob))
                 (ornament-width (interval-length
                                  (ly:stencil-extent ornament-stencil X)))
                 (note-column (ly:grob-object note-grob 'axis-group-parent-X))
                 ;; accidentals attached to the note:
                 (accidentals (and (ly:grob? note-column)
                                   (ly:note-column-accidentals note-column)))
                 ;; dots attached to the note:
                 (dot-column (and (ly:grob? note-column)
                                  (ly:note-column-dot-column note-column))))
            (cond ((and (= direction LEFT) (ly:grob? accidentals))
                   ;; if the ornament is on the left side, and there are
                   ;; accidentals, then increase padding between note
                   ;; and accidentals to make room for the ornament
                   (set! (ly:grob-property accidentals 'padding)
                         ornament-width))
                  ((and (= direction RIGHT) (ly:grob? dot-column))
                   ;; if the ornament is on the right side, and there
                   ;; are dots, then translate the dots to make room for
                   ;; the ornament
                   (set! (ly:grob-property dot-column 'positioning-done)
                         (lambda (grob)
                           (ly:dot-column::calc-positioning-done grob)
                           (ly:grob-translate-axis! grob ornament-width X))))))
          ornament-grob))
      ;; When the note-head event attached to the note-head grob has
      ;; ornamentation events among its articulations, then create a
      ;; HeadOrnamentation grob
      (for-each
       (lambda (articulation)
         (if (memq 'head-ornamentation-event
                   (ly:event-property articulation 'class))
             ;; this articulation is an ornamentation => make the grob
             ;; (either on LEFT or RIGHT direction)
             (begin
               (if (markup? (ly:event-property articulation 'left-text))
                   (make-ornament-grob
                    (ly:event-property articulation 'left-text)
                    LEFT
                    (ly:event-property articulation 'shift-when-on-line)))
               (if (markup? (ly:event-property articulation 'right-text))
                   (make-ornament-grob
                   (ly:event-property articulation 'right-text)
                   RIGHT
                   (ly:event-property articulation 'shift-when-on-line))))))
         (ly:event-property (ly:grob-property note-grob 'cause) 'articulations))))))

\layout {
  \context {
    \Score    \consists #head-ornamentation-engraver
  }
}

Here is the complete example (collisions-partly-avoided.ly):

collisions-partly-avoided.preview

The collisions with the dot and the accidental are now fixed, but unfortunately the collision with the other note head still remains.

14 thoughts on “Adding Ornamentations to Note Heads (Part 3)

  1. Janek Warchoł

    Extending LilyPond is fascinating! When I read your first post (almost 3 months ago), i was happy that i had managed to roughly understand the code. Now, after i had the opportunity to work on improving \shape (i will write a separate post about that), this code actually looks simple!

    Thanks again for providing this tutorial; i think that it’s a valuable resource for learning how to extend LilyPond.

    And btw, concerning the problems with collision avoidance: do you think that we should implement a generic side-aligning interface that would stack the articulations etc. according to some priority? (similarly to how articulations and other elements are stacked vertically, according to outside-staff-priority).

    Reply
    1. Nicolas Sceaux Post author

      It would be really nice to be able a) to specify that a given grob is horizontally attached to a note (so that its extent is accounted for), and b) to specify how to place the grob: between arpeggio and alteration, between note head and dots, etc. A bit like the break-align-order for time signatures, clefs, bars…

      Reply
  2. ming

    Nicolas,
    After reading all three article in an evening, I learn a lot. Thank you.
    May I enquire if this feasible to attach pitch name with its solfege ( d r m f s l t ) or numbers ( 1 2 3 4 5 6 7 )?
    Emmanuel,
    Ming

    Reply
    1. Nicolas Sceaux

      That’s possible, by making the engraver more versatile.

      #(define head-ornamentation-engraver
         (make-engraver
          (acknowledgers
           ((note-head-interface engraver note-grob source)
            ;; helper function to create HeadOrnamentation grobs
            (define (make-ornament-grob text direction shift-when-on-line)
              (let ((ornament-grob (ly:engraver-make-grob engraver
                                     'HeadOrnamentation
                                     note-grob)))
                ;; use the ornament event text as the grob text property
                (set! (ly:grob-property ornament-grob 'text) text)
                ;; set the grob direction (either LEFT or RIGHT)
                (set! (ly:grob-property ornament-grob 'direction) direction)
                ;; set the shift-when-on-line property using the given value
                (set! (ly:grob-property ornament-grob 'shift-when-on-line)
                      shift-when-on-line)
                (ly:pointer-group-interface::add-grob ornament-grob
                  'elements
                  note-grob)
                ;; the ornamentation is vertically aligned with the note head
                (set! (ly:grob-parent ornament-grob Y) note-grob)
                ;; compute its font size
                (set! (ly:grob-property ornament-grob 'font-size)
                      (+ (ly:grob-property ornament-grob 'font-size 0.0)
                        (ly:grob-property note-grob 'font-size 0.0)))
                ;; move accidentals and dots to avoid collision
                (let* ((ornament-stencil (ly:text-interface::print ornament-grob))
                       (ornament-width (interval-length
                                        (ly:stencil-extent ornament-stencil X)))
                       (note-column (ly:grob-object note-grob 'axis-group-parent-X))
                       ;; accidentals attached to the note:
                       (accidentals (and (ly:grob? note-column)
                                         (ly:note-column-accidentals note-column)))
                       ;; dots attached to the note:
                       (dot-column (and (ly:grob? note-column)
                                        (ly:note-column-dot-column note-column))))
                  (cond ((and (= direction LEFT) (ly:grob? accidentals))
                         ;; if the ornament is on the left side, and there are
                         ;; accidentals, then increase padding between note
                         ;; and accidentals to make room for the ornament
                         (set! (ly:grob-property accidentals 'padding)
                               ornament-width))
                    ((and (= direction RIGHT) (ly:grob? dot-column))
                     ;; if the ornament is on the right side, and there
                     ;; are dots, then translate the dots to make room for
                     ;; the ornament
                     (set! (ly:grob-property dot-column 'positioning-done)
                           (lambda (grob)
                             (ly:dot-column::calc-positioning-done grob)
                             (ly:grob-translate-axis! grob ornament-width X))))))
                ornament-grob))
            ;; When the note-head event attached to the note-head grob has
            ;; ornamentation events among its articulations, then create a
            ;; HeadOrnamentation grob
            (for-each
             (lambda (articulation)
               (if (memq 'head-ornamentation-event
                     (ly:event-property articulation 'class))
                   ;; this articulation is an ornamentation => make the grob
                   ;; (either on LEFT or RIGHT direction)
                   (let ((left-text (if (procedure? (ly:event-property articulation
                                                      'left-text))
                                        ((ly:event-property articulation 'left-text)
                                         note-grob)
                                        (ly:event-property articulation 'left-text)))
                         (right-text (if (procedure? (ly:event-property articulation
                                                       'right-text))
                                         ((ly:event-property articulation 'right-text)
                                          note-grob)
                                         (ly:event-property articulation 'right-text)))
                         (shift (ly:event-property articulation 'shift-when-on-line)))
                     (if (markup? left-text)
                         (make-ornament-grob left-text LEFT shift))
                     (if (markup? right-text)
                         (make-ornament-grob right-text RIGHT shift)))))
             (ly:event-property (ly:grob-property note-grob 'cause) 'articulations))))))
      
      \layout {
        \context {
          \Score
          \consists #head-ornamentation-engraver
        }
      }
      
      %%%
      %%% Event type definition
      %%%
      #(define-music-type HeadOrnamentationEvent (music-event)
         (description . "Print the note name next to the note head")
         (types . (general-music post-event event head-ornamentation-event)))
      
      nn =
      #(make-music
        'HeadOrnamentationEvent
        'shift-when-on-line #t
        'left-text
        (lambda (note-grob)
          #{ \markup\fontsize #-5 \vcenter {
            #(vector-ref #("d" "r" "m" "f" "s" "l" "t")
               (ly:pitch-notename
                (ly:music-property
                 (ly:prob-property
                  (ly:grob-property note-grob 'cause)
                  'music-cause)
                 'pitch)))
            " "
          } #}))
      
      addNoteNames =
      #(define-music-function (parser location music) (ly:music?)
         (music-map
          (lambda (mus)
            (if (eqv? (ly:music-property mus 'name) 'NoteEvent)
                (set! (ly:music-property mus 'articulations)
                      (cons nn (ly:music-property mus 'articulations))))
            mus)
          music))
      
      %%%
      %%% test
      %%%
      
      {
        \override Score.HeadOrnamentation.color = #red
        c'8\nn d'\nn e'\nn f'\nn
        %%
        \addNoteNames { g' a' b' c''}
      }
      Reply
  3. ming

    Nicolas:
    Thank you for the code. I copy and paste to frecobaldi and run and I get the following error on line 88. Then I insert \version “2.19.5” at the beginning and run. Again I get the same error on line 89. I change the version to “1.18.2” and the same error occurs. I then apply the convert.ly and I get the same error again. I don’t know guile or scheme. Your help is highly appreciated.
    Emmanuel,
    Ming.
    >>>>>>> run error log……..
    Starting lilypond-windows.exe 2.19.5 [include_head-ornamentation-engraver.ly]…
    Processing `C:/Users/Tsang/Dropbox/LP_includes/include_head-ornamentation-engraver.ly’
    Parsing…
    C:/Users/Tsang/Dropbox/LP_includes/include_head-ornamentation-engraver.ly:88:2: error: GUILE signaled an error for the expression beginning here
    #
    (define-music-type HeadOrnamentationEvent (music-event)

    fatal error: cannot find music object: HeadOrnamentationEvent
    Unbound variable: define-music-type
    Exited with return code 1.

    Reply
  4. ming

    Nicolas:

    Thank you very much.
    I did as you said it compile fine. Thank you.
    I test the code with different key e.g. \key f\major. I am expecting “F” will have a “d” attached to note head “F” but not a “f”; “G” to “r” not “s” etc. Can the code be modify to do that. I don’t know guile or scheme.
    Emmanuel,
    Ming.

    Reply
    1. Nicolas Sceaux

      Aren’t ‘d r m f s l t’ the short for ‘do ré mi fa sol la si’, that is ‘c d e f g a b’?
      Anyway, if you’d like to change the pitch names, modify the line:

      #(vector-ref #("d" "r" "m" "f" "s" "l" "t")

      which defines the names to be used for ‘c d e f g a b’ pitches (in that order), to e.g.:

      #(vector-ref #("s" "l" "t" "d" "r" "m" "f")

      Reply
  5. ming

    Yes, it is.
    I want this as a moving “do” that is associated with key signature. e.g.
    Nicolas: Thank you for your answer.
    key f\major the scale is (F) f g a bf c d e f
    the scale in solfege is (1) d r m f s l t d as a moving “do” or
    (2) 1 2 3 4 5 6 7 1 as a moving “1”
    I have the following lines coded which produces f s l t d r m f >> the scale (F). I like to see it produces as (1) or (2). Hope I explain it well.
    Emmanuel,
    Ming
    scaleC = { \key c\major
    \override Score.HeadOrnamentation.color = #red
    c’8\ns d’\nn e’\ns f’\nn
    %%
    \addNoteNameN { g’ a’ b’ c”}
    }
    scaleF = { \key f\major
    \override Score.HeadOrnamentation.color = #blue
    \addNoteNameS { f’ g’ a’ b’ c” d” e” f” }
    }
    scaleG = { \key g\major
    \override Score.HeadOrnamentation.color = #blue
    \addNoteNameS { g’ a’ b’ c” d” e” f” g” }
    }

    \new Staff { \scaleC }
    % flat
    \new Staff { \scaleF }
    % sharp
    \new Staff { \scaleG }

    Reply
  6. ming

    Just want to followup:
    notehead ornament for c\major d r m f s l t (or 1 2 3 4 5 6 7) for c-scale c d e f g a b
    How to do this for other scale e.g f\major f-scale: f g a bf c d e to display notehead ornament as d r m f s l t (or 1 2 3 4 5 6 7). As of current code it display f s l t d r m. I like this solfege as a moving “do”. f\major – where f is a “d”; g\major – where g is a “d” . This is for singing. Looking forward to here good news – ie possible to do that.
    Thanks.

    Reply
  7. Gabriel

    Hi Nicolas,

    I’ve been working with your Ornamentation plugin. I added x-position and y-position grob-properties in order to control the position of the text next to the note, and then using them like this:

    (ly:stencil-translate
    text
    (cons
    (- (- x-coord (ly:grob-relative-coordinate me x-ref X))
    (ly:grob-property me 'x-position))
    (- (- y-coord (ly:grob-relative-coordinate me y-ref Y))
    (ly:grob-property me 'y-position))
    ))))

    It works fine. I needed it, because i’m using “brackettips.up” to the left of a note to try to represent a bendBefore, and the default positioning of your “plugin” wasn’t good. It looks like this

    But now, i’m stuck with another problem: i’d want to make the ornament avoid colliding with the bar, like here. Any ideas?

    PD: I don’t know nor GUILE, nor SCHEME, nor Lilys API. I managed to add those properties analyzing and copy-pasting your code.

    PD2: Thank you for your contribution, I’m heavily relying on your work! 😀

    Reply
  8. Bartłomiej

    Hi, Janek Warchoł

    can you paste full working example of your “engraver more versatile” as I still getting errors after replacing code in line #131.

    Thanks.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *