Hi Andrew,

Am 23.07.22 um 03:49 schrieb Andrew Bernard:
I know that we can't natively make ties between notes in different voices. I know that there was a Google Summer of Code task that could not be completed.

A few weeks ago, I sent you the following privately (I was too timid to post in on the list):

My idea was to \consist the Tie_engraver to the Staff context not _instead_ of to the Voice context, but _in addition_. Then we have two Tie engravers and need a mechanism by which to tell if a given tie should be collected by the Voice-level Tie_engraver or at Staff level (in order to connect ties between different voices).

During my experiments I re-implemented the Tie_engraver in Scheme; although it turned out that (contrary to my expectations) the necessary adjustments could just as easily have been made in C++, the advantage is that we can test this approach without the need to re-compile a custom LilyPond build.

The attached file (requiring 2.23.6 and above) generates

as easily as:

\new Staff \with { \consists #New_tie_engraver }
{
  <<
    \relative {
      <c''~ c,\to Staff~>4 c8 b a g~ 4
    }
    \\
    \relative {
      s4 c'2 e4
    }
  >>
}

Of course the same mechanism might be implemented for, e.g., the Slur_engraver. But this requires additional work, as the slur positioning mechanism is not quite up to positioning Staff-level slurs correctly.

The attached Scheme Tie_engraver may be used as a drop-in replacement for the standard C++ Tie_engraver; in my local branch, it compiles the full regression test suite without causing differences.)

Lukas
\version "2.23.6"

% TODO: Rename variables for clarity
% TODO: Turn re-implementations of C++ helper functions into exported callbacks

% Not in guile core for 1.8 (remove for Guile2)
#(define (hash-count pred table)
   (count identity (hash-map->list pred table)))

#
(define (hash-non-empty? hash-table)
  ;; For Guile2, simplfy to
  ;; (positive? (hash-count (const #t) hash-table)))
  (pair? (hash-map->list (lambda (key handle) '()) hash-table)))

#
(define (tie-column::add_tie tie-column tie)
;;; TODO: Make callback from C++
  (if (not (grob::has-interface (ly:grob-parent tie Y) 'tie-column-interface))
      (begin
       (if
        (or (null? (ly:spanner-bound tie-column LEFT))
            (> (car (ly:grob-spanned-column-rank-interval tie-column))
               ; THINK: is this exactly equivalent to the C++ original?
               (car (ly:grob-spanned-column-rank-interval tie-column))))
        (begin
         (ly:spanner-set-bound! tie-column LEFT (ly:spanner-bound tie LEFT))
         (ly:spanner-set-bound! tie-column RIGHT (ly:spanner-bound tie RIGHT))))

       (ly:grob-set-parent! tie Y tie-column)
       (ly:pointer-group-interface::add-grob tie-column 'ties tie))))

%{
head-event-alist has the fields:
   '((end-moment . #f)
     (tie-stream-event . #f)
     (tie-articulation-event . #f)
     (tie-from-chord-created . #f)
     (tie . #f)
   )
%}

#
(define (ly:enharmonic-equivalent? p1 p2)
  (= (ly:pitch-tones p1) (ly:pitch-tones p2)))

#
(define (ly:tie::head tie dir)
  (let ((it (ly:spanner-bound tie dir)))
    (if (grob::has-interface it 'note-head-interface)
        it #f)))

#
(define-public (New_tie_engraver context)
  (define (report-unterminated-tie notehead alist)
    ;; give notehead argument in order to simplify use of
    ;; report-unterminated-tie as a proc in hash-for-each
    (if (not (assq-ref alist 'tie-from-chord-created))
        (begin
         (ly:warning (G_ "unterminated tie")) ; TODO: Warn with source position
         (ly:grob-suicide! (assq-ref alist 'tie)))))
  (let
   ((event-processed #f)
    (tie-stream-event #f)   ; corresponds to event_ in C++
    (tie-column #f)
    (now-heads '())
    (heads-to-tie (make-hash-table))
    (ties '())
    (target (ly:context-name context)))

   (define (typeset-tie her)
     ;; this seems not to change anything for "her" if both bounds
     ;; are note heads ???
     (let ((left-head (ly:tie::head her LEFT))
           (right-head (ly:tie::head her RIGHT)))

       (if (not (and left-head right-head))
           (begin
            (ly:warning "lonely tie")
            (if (not left-head)
                (set! left-head right-head)
                (set! right-head left-head))))
       (ly:spanner-set-bound! her LEFT left-head)
       (ly:spanner-set-bound! her RIGHT right-head)))

   (define (tie-notehead engraver head enharmonic?)
     (let ((found #f))
       (hash-for-each
        (lambda (registered-head alist)
          (let*
           ((right-ev (event-cause head))
            (left-head registered-head)
            (left-ev (event-cause left-head)))
           (if (and (not found) left-ev right-ev)
               (let ((p1 (ly:event-property left-ev 'pitch))
                     (p2 (ly:event-property right-ev 'pitch)))
                 (if (and
                      ((if enharmonic? ly:enharmonic-equivalent? equal?) p1 p2)
                      ;; Do not create tie for events split by
                      ;; Completion_heads_engraver
                      (not (ly:event-property left-ev 'autosplit-end #f)))
                     (let*
                      ((tie (assq-ref alist 'tie))
                       (end (assq-ref alist 'end-moment))
                       (tie-event (assq-ref alist 'tie-articulation-event))
                       (cause (if tie-event tie-event
                                  (assq-ref alist 'tie-stream-event)))
                       (cause-direction (ly:event-property cause 'direction #f)))
                      (ly:engraver-announce-end-grob engraver tie cause)
                      (ly:spanner-set-bound! tie RIGHT head)
                      (ly:spanner-set-bound! tie LEFT left-head)
                      (if cause-direction
                          (ly:grob-set-property! tie 'direction cause-direction))
                      (set! ties (cons tie ties))
                      (set! found #t)
                      (hash-remove! heads-to-tie registered-head)

                      (hash-for-each
                       (lambda (other-head alist)
                         (if (equal? (assq-ref alist 'end-moment) end)
                             (hash-set! heads-to-tie other-head
                                        (assq-set! alist 'tie-from-chord-created #t))))
                       heads-to-tie)))))))
        heads-to-tie)
       found))

   (make-engraver
    ((start-translation-timestep translator)
     (if (and (hash-non-empty? heads-to-tie)
              (not (ly:context-property context 'tieWaitForNote #f)))
         (let ((now (ly:context-current-moment context)))
           (hash-for-each
            (lambda (head-event alist)
              (if (ly:moment<? (assq-ref alist 'end-moment) now)
                  (begin
                   (report-unterminated-tie head-event alist)
                   (hash-remove! heads-to-tie head-event))))
            heads-to-tie)))
     (ly:context-set-property! context 'tieMelismaBusy (hash-non-empty? heads-to-tie)))

    (listeners
     ((tie-event engraver event)
      (if (and (not (ly:context-property context 'skipTypesetting #f))
               (eq? (ly:event-property event 'spanner-target 'Voice) target))
          (if (and tie-stream-event
                   (not (equal? tie-stream-event event)))
              (ly:warning "Conflict; discarding tie") ; improve (see stream-event.cc)
              (set! tie-stream-event event)))))

    (acknowledgers
     ((note-head-interface engraver grob source-engraver)
      (set! now-heads (cons grob now-heads))
      (if (not (tie-notehead engraver grob #f))
          (tie-notehead engraver grob #t))
      (if (and (pair? ties) (not tie-column))
          (set! tie-column
                (ly:engraver-make-spanner engraver 'TieColumn (last ties)))) ; is last correct?

      (if tie-column
          (for-each
           (lambda (tie) (tie-column::add_tie tie-column tie))
           ties))))

    ((process-music engraver)
     (if (or tie-stream-event
             (positive?
              (hash-count
               (lambda (head-event alist)
                 (or (assq-ref alist 'tie-articulation-event)
                     (assq-ref alist 'tie-stream-event)))
               heads-to-tie)))
         (ly:context-set-property! context 'tieMelismaBusy #t)))

    ((process-acknowledged engraver)
     (let ((wait (ly:context-property context 'tieWaitForNote #f))
           (new-heads-to-tie '()))
       (if (pair? ties)
           (begin
            (if (not wait)
                (begin
                 (hash-for-each report-unterminated-tie heads-to-tie)
                 (hash-clear! heads-to-tie)))
            (for-each typeset-tie ties)
            (set! ties '())
            (set! tie-column #f)))
       (for-each
        (lambda (head)
          (let ((left-ev #f)
                (left-articulations #f)
                (tie-articulation-event #f))
            (set! left-ev (event-cause head))
            (if (and left-ev
                     ;; no left-ev: may happen for ambitus [?]
                     ;; not a note event: may happen for pitched trills [?]
                     (ly:in-event-class? left-ev 'note-event))
                (begin
                 (set! left-articulations (ly:event-property left-ev 'articulations))
                 (if (not tie-stream-event)
                     (set! tie-articulation-event
                           (find
                            (lambda (ev)
                              (memq 'tie-event (ly:event-property ev 'class)))
                            left-articulations)))
                 ;; TODO: taking the first tie articulation means that
                 ;; there's trouble ahead if we have multiple tie articulations
                 ;; on the same note, headed for different targets. Oh wow.
                 (if
                  (and tie-articulation-event
                       (not (eq? (ly:event-property tie-articulation-event
                                                    'spanner-target 'Voice)
                                 target)))
                  (set! tie-articulation-event #f))

                 (if (and left-ev
                          (or tie-stream-event tie-articulation-event)
                          ;; Do not create tie for events split by
                          ;; Completion_heads_engraver
                          (not (ly:event-property left-ev 'autosplit-end #f)))
                     (let*
                      ((new-tie (ly:engraver-make-spanner
                                 engraver
                                 'Tie
                                 (if tie-articulation-event tie-articulation-event
                                     tie-stream-event)))
                       (new-end-moment
                        (ly:moment-add
                         (ly:context-current-moment context)
                         (ly:event-property left-ev 'length (ly:make-moment 0))
                         ;; TODO: Care for grace (see translator.cc get_event_length)
                         ))
                       (new-head-alist
                        (list
                         ;; STRANGE: Doing this with quasiquoting
                         ;; and (tie-from-chord-created . #f)
                         ;; we have bleeding over from
                         ;; previous score causing
                         ;; tie-from-chord-created to be set to
                         ;; its value in a previous score ...
                         (cons 'tie-stream-event tie-stream-event)
                         (cons 'tie-articulation-event tie-articulation-event)
                         (cons 'end-moment new-end-moment)
                         (cons 'tie-from-chord-created #f)
                         (cons 'tie new-tie))))
                      (set! new-heads-to-tie
                            (cons (cons head new-head-alist)
                                  new-heads-to-tie))
                      (set! event-processed #t)))))))
        ;; reverse now-heads in order to process them
        ;; in the order of creation. This makes sure
        ;; double noteheads in ties get their ties
        ;; in the order requested in
        ;; input/regression/chord-X-align-on-main-noteheads.ly
        ;; e.g. for
        ;; {
        ;;   <e' e'>~ <e' e'>
        ;; }
        (reverse now-heads))

       (if (and (not wait) (pair? new-heads-to-tie))
           (begin
            (hash-for-each report-unterminated-tie heads-to-tie)
            (hash-clear! heads-to-tie)))

       (for-each
        (lambda (new-head-entry)
          (hash-set! heads-to-tie
                     (car new-head-entry)
                     (cdr new-head-entry)))
        new-heads-to-tie)

       (set! now-heads '())))
    ((stop-translation-timestep engraver)
     ;; Discard event only if it has been processed with at least one
     ;; appropriate note.
     (if event-processed
         (set! tie-stream-event #f))
     (set! event-processed #f)))))

to =
#(define-event-function (id event) (key? ly:event?)
   (set! (ly:music-property event 'spanner-target) id)
   event)

\layout {
  \context {
    \Voice
    \remove Tie_engraver
    \consists #New_tie_engraver
  }
}

% -------------------------------------------------------------------- %

\new Staff \with { \consists #New_tie_engraver }
{
  <<
    \relative {
      <c''~ c,\to Staff~>4 c8 b a g~ 4
    }
    \\
    \relative {
      s4 c'2 e4
    }
  >>
}

Reply via email to