Le 17/11/2022 à 23:02, Jean Abou Samra a écrit :
Le 17/11/2022 à 23:01, Werner LEMBERG a écrit :
But how do you take into account the constraints from lyrics here?
That is the whole problem.
They should be completely ignored.
In that case, it should be completely doable even in Scheme now.
(I started writing some code yesterday in response to Abraham that
does essentially that, maybe I'll have it finished today).
Aah, nice!

No so fast, wait until it actually works before calling
it nice, otherwise if it doesn't work I'll feel bad :-)




Here you go.

This is a vast refinement of the initial \autoMove function that takes all
lyric words into account simultaneously, finding an optimal solution that
minimizes the distance from each lyric word to its note while respecting
the minimal distances between lyric words as configured by
LyricSpace.minimum-distance / LyricHyphen.minimum-distance.

I used the first algorithm with acceptable complexity that I could
think of, so there may be a simpler solution for this. Nevertheless,
it runs in linear time, so I'm happy enough. (If it were quadratic
or worse, the running time could potentially be problematic if
ly:one-line-auto-height breaking is used, in which case there can
be lots of lyrics on the same line.)

Caveats:

- As said in a previous message, this is the other extreme compared
  to what LilyPond does by default: lyrics are not taken into account
  *at all* into note spacing. In particular, LilyPond may produce
  a page breaking configuration in which there are too many notes on
  the same system to fit the lyrics comfortably. I expect that using
  \break in those cases will often be enough to get an acceptable result,
  but I have zero experience with typesetting music with lyrics.
  (And little actual practical experience with typesetting music at
  all, to be honest.)

- I have barely tested it.

Additional featurelets:

- You can still set LyricText.self-alignment-X. It defines the position
  of the LyricText that the algorithm will consider optimal.

- You can set LyricText.details.strength to tell the algorithm to
  place one specific syllable closer to its note at the expense
  of the others. See the example.

Before someone asks: this is not good to integrate into LilyPond as-is,
because it breaks assumptions that grob implementors and LilyPond in
general make (namely, the assumption that the X-offset of an item is
known before line breaking). One would have to either go for a full
solution as brainstormed in one of my earlier messages, or at least
find a way for an Item to declare that its positioning depends on other
things (akin to the current cross-staff property, but preferably less
invasive).

Hope that helps,
Jean

% Copyright (C) 2022, Jean Abou Samra <j...@abou-samra.fr>
% Placed under the Creative Commons CC0 1.0 Universal license.

\version "2.23.81"

#(ly:set-option 'compile-scheme-code)

#(use-modules (ice-9 match)
              (ice-9 hash-table)
              (oop goops))

%% convenience stuff:

#(define-syntax-rule (transform! lval proc)
   (set! lval (proc lval)))

#(define ->
   (make-procedure-with-setter
    (lambda (instance . path)
      (let loop ((instance instance) (path path))
        (match path
          ((slot)
           (slot-ref instance slot))
          ((slot . rest)
           (loop (slot-ref instance slot)
                 rest)))))
    (lambda (instance . args)
      (let loop ((instance instance) (args args))
        (match args
         ((slot new)
          (slot-set! instance slot new))
         ((slot . rest)
          (loop (slot-ref instance slot)
                rest)))))))



#(define-class <lyric-variable> ()
   (ideal #:init-keyword #:ideal)
   (extent #:init-keyword #:extent)
   (strength #:init-keyword #:strength)
   (tied-to #:init-value #f)
   (tied-offset #:init-value #f)
   (final #:init-value #f))

#(define (merged-variable! group var)
   (let* ((delta (- (interval-end (-> group 'extent))
                    (interval-start (-> var 'extent))))
          (new
           (make <lyric-variable>
                 #:ideal (/ (+ (* (-> group 'strength)
                                  (-> group 'ideal))
                               (* (-> var 'strength)
                                  (- (-> var 'ideal)
                                     delta)))
                            (+ (-> group 'strength)
                               (-> var 'strength)))
                 #:extent (cons (interval-start (-> group 'extent))
                                (+ (interval-end (-> group 'extent))
                                   (interval-length (-> var 'extent))))
                 #:strength (+ (-> group 'strength)
                               (-> var 'strength)))))
     (set! (-> group 'tied-to) new)
     (set! (-> group 'tied-offset) 0)
     (set! (-> var 'tied-to) new)
     (set! (-> var 'tied-offset) delta)
     new))

#(define (propagate! variables)
   (match variables
     ((var)
      variables)
     ((var group . rest)
      (let ((have-overlap (<= (+ (-> var 'ideal)
                                 (interval-start (-> var 'extent)))
                              (+ (-> group 'ideal)
                                 (interval-end (-> group 'extent))))))
        (if have-overlap
            (let ((merged (merged-variable! group var)))
              (propagate! (cons merged rest)))
            variables)))))

#(define (finalize! variables)
   (define (finalize-one! var)
     (unless (-> var 'final)
       (set! (-> var 'final)
             (if (-> var 'tied-to)
                 (begin
                  (finalize-one! (-> var 'tied-to))
                  (+ (-> var 'tied-to 'final)
                     (-> var 'tied-offset)))
                 (-> var 'ideal)))))
   (for-each finalize-one! variables))

#(define (solve-lyric-spacing-problem! variables)
   (fold
    (lambda (var groups)
      (propagate! (cons var groups)))
    '()
    variables)
   (finalize! variables))

#(define (respace-lyrics! grob)
   (let ((elt-array (ly:grob-object grob 'elements #f)))
     (when elt-array
       (let* ((elts (ly:grob-array->list elt-array))
              (refp (ly:grob-system grob))
              (with-iface (lambda (iface)
                            (filter (lambda (g)
                                      (grob::has-interface g iface))
                                    elts)))
              (words (filter (lambda (word)
                               (interval-sane? (ly:grob-extent word word X)))
                             (with-iface 'lyric-syllable-interface)))
              ;; Includes both LyricHyphen and LyricSpace
              (constraints (with-iface 'lyric-hyphen-interface))
              (variables (map
                          (lambda (word)
                            (let* ((xalign (ly:grob-property word 
'self-alignment-X))
                                   (coord (ly:grob-relative-coordinate word 
refp X))
                                   (orig-ext (ly:grob-extent word word X))
                                   (align-point (interval-index orig-ext 
xalign))
                                   (ideal (+ coord align-point))
                                   (extent (coord-translate orig-ext (- 
align-point)))
                                   (strength (or (assq-ref (ly:grob-property 
word 'details)
                                                           'strength)
                                                 1.0)))
                              (make <lyric-variable>
                                    #:ideal ideal
                                    #:extent extent
                                    #:strength strength)))
                          words))
              (word-to-variable (alist->hashq-table (map cons words 
variables))))
         (for-each
          (lambda (constraint)
            (let ((added (ly:grob-property constraint 'minimum-distance))
                  (left-var (hashq-ref word-to-variable (ly:spanner-bound 
constraint LEFT))))
              (when left-var
                (transform! (-> left-var 'extent)
                            (lambda (e) (cons (interval-start e)
                                              (+ (interval-end e) added)))))))
          constraints)
         (solve-lyric-spacing-problem! variables)
         (for-each
          (lambda (word variable)
            (let* ((xalign (ly:grob-property word 'self-alignment-X))
                   (orig-ext (ly:grob-extent word word X))
                   (align-point (interval-index orig-ext xalign)))
              (ly:grob-translate-axis!
               word
               (- (-> variable 'final)
                  (ly:grob-relative-coordinate word refp X)
                  align-point)
               X)))
          words
          variables)))))

\layout {
  \context {
    \Lyrics
    \override LyricText.extra-spacing-width = #'(+inf.0 . -inf.0)
    \override LyricSpace.springs-and-rods = ##f
    \override LyricHyphen.springs-and-rods = ##f
    \override VerticalAxisGroup.after-line-breaking = #respace-lyrics!
  }
}
\version "2.23.81"

\include "respace-lyrics.ily"

\language "english"

struct = {
  \numericTimeSignature
  \key bf \major
  \time 3/4
  s2.*5 \break
  s2.*5 \break
}

nb = \markup { \small \italic "n.b." }

melody = \relative {
  \clef treble
  \dynamicUp
  R2.*4 |
  d'8\mp ef f4. d8 |
 
  d4 \once \phrasingSlurDashed c2_\(^\nb |
  d8\) ef f4 bf, |
  c2. |
  d8\< ef f4. f8 |
  g8 a bf2\mf |
}

pianoRH = \relative {
  \clef treble
  d''8 ef f4 <f,c'>8 f' |
  << { g8 a bf2 } \\ { <bf, d>2. } >> |
  << { ef4 c8 d ef bf } \\ { g2 g4 } >> |
  << { d'4. ef8 c4 } \\ { <ef, gf>2. } >> |
  <c' f>4 <f, d'>2 |
 
  << { d'4 c } \\ { <ef, gf>2 } >>  ef'8 c' |
  <d, bf'>4 <bf f'>2 |
  <gf d'>4 c ef8 c' |
  <d, bf'>4 f c8 f, |
  << { d'2. } \\ { bf8 a g2 } >> |
}

pianoLH = \relative {
  \clef treble
  bf8 f' ~ f4 a, |
  g8 d' bf' a g d |
  \clef bass
  c,8 g' ef'2 |
  ef,8 bf' c2 |
  bf,8 f' bf4 d |
 
  ef,8 gf bf c ef4 |
  bf,8 f' d'4 f |
  ef,8 c' ef gf ~ gf4 |
  bf,,8 f' d'4 a |
  g8 d' bf a g d |
}

melodyWordsDefault = \lyricmode {
  Would I know my Sav -- ior
  %% The higher the details.strength property, the harder the algorithm
  %% tries to place the lyric syllable close to its ideal position, at the
  %% expense of other lyric syllables nearby.  Try outcommenting this
  %% override to see the effect.
  %\once \override LyricText.details.strength = 100
  Wrapped in swad -- dling
  bands,
  Ly -- ing in a man -- ger bed, Light of hea -- ven ’round His head?
}

#(set-global-staff-size 19)
\paper {
  ragged-last = ##f
  ragged-bottom = ##t
  ragged-right = ##f
  ragged-last-bottom = ##t
  tagline = ##f
}

\layout {
  \context {
    \Lyrics
    \override LyricText.font-size = #0
    \override LyricHyphen.font-size = #-0.5
    \override LyricHyphen.padding = #0.15
    \override LyricHyphen.length = #0.6  %#0.4
    \override LyricHyphen.minimum-length = #0.66
    \override LyricHyphen.minimum-distance = #1 %0.15
    \override LyricHyphen.thickness = 2.0
    \override LyricHyphen.dash-period = 8.0
    \override LyricExtender.minimum-length = #0
    \override LyricExtender.right-padding = #0.5
    \override LyricSpace.minimum-distance = #1
    \override VerticalAxisGroup.nonstaff-relatedstaff-spacing.padding = #1
  }
}

\score {
  <<
    \new Staff <<
      \struct
      \new Voice = "melody" \melody
    >>
    \new Lyrics \lyricsto melody \melodyWordsDefault
    \new PianoStaff <<
      \new Staff = "pianoRH" <<
        \struct
        \pianoRH
      >>
      \new Staff = "pianoLH" <<
        \struct
        \pianoLH
      >>
    >>
  >>
  \layout {}
}

Attachment: tricky-lyrics.pdf
Description: Adobe PDF document

Attachment: OpenPGP_signature
Description: OpenPGP digital signature

Reply via email to