Am Di., 26. Juli 2022 um 11:48 Uhr schrieb Thomas Morley
<thomasmorle...@gmail.com>:
>
> Am Sa., 23. Juli 2022 um 19:15 Uhr schrieb Jean Abou Samra 
> <j...@abou-samra.fr>:
> >
> >
> >
> > Le 23/07/2022 à 12:49, Lukas-Fabian Moser a écrit :
> > >
> > > 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.)
> > >
> >
> >
> >
> > Interesting, Lukas! Now, this approach fails on cases where
> > ties are physically but not mentally interrupted, as pianists
> > sometimes encounter, like
> >
> > \version "2.23.10"
> >
> > \new Staff <<
> >    \relative {
> >      <%{ tie this %} c' c'>2
> >      c'8 b a g
> >    }
> >    \\
> >    \relative { s2 %{ to this %} c'4 g }
> >    \\
> >    \relative { g16 a c d e a g e f4 d }
> >  >>
> >
> > but this might be rare enough that not catering for it
> > would be good enough?
>
> To make it visible, Jean's example, with Lukas' coding reads:
>
> \new Staff \with { \consists #New_tie_engraver }
> <<
>    \relative {
>      <%{ tie this %} c'\to Staff ~ c'>2
>      c'8 b a g
>    }
>    \\
>    \relative { s2 %{ to this %} c'4 g }
>    \\
>    \relative { g16 a c d e a g e f4 d }
>  >>
>
> and gives the attached output.
>
> We probably need something like NoteColumn.tie-skip similar to 
> 'glissando-skip.
> Though, why not something more general like 'spanner-skip for all
> spanners (usually) terminated at following NoteColumn?
>
> @Lukas
> You use hash-tables in your rewrite of the engraver.
> Does the C++-engraver do so?
> From a users point of view hash-tables are always inconvenient, imho.
> Is the performance advantage really as huge not to use more simple alists?
>
> Many thanks for your work!
>
> Cheers,
>   Harm

@ Jean
I've got your last mail off-list. By accident?

@ Lukas
I have not yet understood all subtleties of your engraver code.
Though, as a proof of concept I implemented a possibility to select
and skip NoteHeads which should not be considered for ending a Tie.
See attachment.

I stumbled across a certain condition. In tie-column::add_tie there is
              (> (car (ly:grob-spanned-column-rank-interval tie-column))
                 (car (ly:grob-spanned-column-rank-interval tie-column)))
How could this ever be true?

Cheers,
  Harm
%% https://lists.gnu.org/archive/html/lilypond-user/2022-07/msg00353.html
%% by Lukas-Fabian Moser

%% Change-log Harm
%%   - drop support for guilev1
%%   - exclude NoteHeads with details.tie-me set to #f
%%   - move some definitions out of engraver
%%   - reformating

\version "2.23.9"

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

#(define (hash-non-empty? hash-table)
  (positive? (hash-count (const #t) hash-table)))

#(define (tie-column::add_tie tie-column tie)
;;; TODO: Make callback from C++
  (when (not (grob::has-interface (ly:grob-parent tie Y) 'tie-column-interface))
    (when (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))))
      (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 (report-unterminated-tie notehead alist)
  ;; give notehead argument in order to simplify use of
  ;; report-unterminated-tie as a proc in hash-for-each
  (when (not (assq-ref alist 'tie-from-chord-created))
    (ly:warning (G_ "unterminated tie")) ; TODO: Warn with source position
    (ly:grob-suicide! (assq-ref alist 'tie))))

#(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)))

    (when (not (and left-head right-head))
      (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-public (New_tie_engraver context)
  (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 (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)))
           (when (and (not found) left-ev right-ev)
             (let ((p1 (ly:event-property left-ev 'pitch))
                   (p2 (ly:event-property right-ev 'pitch))
                   (p-equal? (if enharmonic? ly:enharmonic-equivalent? equal?)))
               (when (and (p-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)
     (when (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)
      (when (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)
       (when (assoc-get 'tie-me (ly:grob-property grob 'details) #t)
         (set! now-heads (cons grob now-heads))
         (when (not (tie-notehead engraver grob #f))
           (tie-notehead engraver grob #t))
         (when (and (pair? ties) (not tie-column))
           (set! tie-column
                 (ly:engraver-make-spanner engraver 'TieColumn (last ties)))) ; is last correct?

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

    ((process-music engraver)
     (when (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 '()))
       (when (pair? ties)
          (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))
            (when (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))
              (set! left-articulations
                    (ly:event-property left-ev 'articulations))
              (when (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.
              (when (and tie-articulation-event
                         (not (eq? (ly:event-property
                                      tie-articulation-event
                                      'spanner-target
                                      'Voice)
                                   target)))
                (set! tie-articulation-event #f))

              (when (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))

       (when (and (not wait) (pair? new-heads-to-tie))
         (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.
     (when 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
  }
}

skipMe = \once \override NoteHead.details.tie-me = ##f

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



\new Staff \with { \consists #New_tie_engraver }
<<
   \relative {
     <%{ tie this %} c'\to Staff ~ c'>2
     \skipMe c8 b a g
   }
   \\
   \relative { s2 %{ to this %} c'2 }
   \\
   \relative { g16 a \skipMe c d e a g e f'4 d }
 >>

Reply via email to