L’étape de traduction#

Aperçu général#

Dans le contexte de LilyPond, la « traduction » est la partie de la compilation qui fait le pont entre le monde des expressions musicales et celui des grobs et du moteur de rendu. Le temps musical est établi, et les grobs sont non seulement créés, mais également mis en relation (parents, extrémités, pointeurs).

La traduction s’appuie sur plusieurs types d’objets.

Les contextes ont plusieurs fonctions. Ils ont chacun un type de contexte, et forment une hiérarchie des différents niveaux d’une partition. Les types de contexte les plus courants sont Score, StaffGroup, Staff et Voice. Tous sont listés dans la section Contexts de la référence des propriétés internes. Chaque contexte contient un jeu de propriétés.

Les événements sont dérivés des expressions musicales. Ils sont toutefois d’un type Scheme différent, caractérisé par le prédicat ly:stream-event? au lieu de ly:music?. La plupart des expressions musicales sont vouées à être transformées en événements au moment musical où elles apparaissent. Ce n’est par contre généralement pas le cas des expressions qui sont uniquement destinées à en contenir d’autres, comme SequentialMusic. En première approximation, les expressions musicales qui deviennent des événements sont celles dont le nom se termine par Event, comme Notevent, RestEvent ou encore AbsoluteDynamicEvent.

Les événements sont des Probs, donc on dispose des deux fonctions habituelles pour probs : ly:event-property et ly:set-event-property!.

Les itérateurs sont responsables de la gestion du temps musical. Ils avancent dans les expressions musicales qui agissent comme conteneurs à autres expressions musicales – celles dont le nom se termine par Music plutôt que Event – et « diffusent » les expressions musicales qui doivent devenir des événements, en les transformant en événements.

Les graveurs réagissent aux événements diffusés par les itérateurs. Ils peuvent créer des grobs, lire et modifier les propriétés des contextes, et aussi réagir aux grobs créés par d’autres graveurs. Ce sont les acteurs principaux de la traduction. Il existe un graveur pour chaque élément de notation distinct : Note_heads_engraver (têtes de notes), Stem_engraver (hampes), Beam_engraver (ligatures), Staff_symbol_engraver (symbole de portée à cinq lignes), et bien d’autres. Contrairement aux itérateurs, les graveurs peuvent être écrits en Scheme.

Les graveurs ne possèdent pas de propriétés, car on ne les manipule généralement pas en tant qu’objets. Ils communiquent plutôt à travers les propriétés de contexte.

Par convention, le nom d’un itérateur ou d’un graveur est écrit Avec_majuscule_initiale_et_underscores. Quant aux événements, ils n’ont pas vraiment de nom de type. Ils fonctionnent plutôt par classe d’événement que par type d’expression musicale. Les classes d’événements sont semblables aux classes musicales, bien que séparées (mais les deux se recoupent en général).

Contextes#

Propriétés des contextes#

Les contextes contiennent des propriétés. Dans la syntaxe de LilyPond, la manière habituelle de modifier ces propriétés est la commande \set :

\set [NomContexte.]nomPropriété = valeur```

Un \set est une expression musicale de type PropertySet, qui modifie la propriété lorsque l’itérateur Property_iterator agit dessus.

Les contextes sont des Probs, avec les fonctions associées :

(ly:context-property context property [default])
(ly:context-set-property! context property value)

De plus, chaque contexte contient des réglages pour les grobs qui seront créés à partir de lui. La commande pour modifier ces propriétés est \override :

\override [NomContexte.]NomGrob.nom-propriété = valeur```

En Scheme, on dispose des fonctions suivantes pour changer ces réglages :

(ly:context-grob-definition context grob)

Renvoie une alist qui associe des propriétés (symboles) à leurs valeurs par défaut pour le type de grobs type (un symbole) dans le contexte context.

(ly:context-pushpop-property context grob property [value])

Si value est passé, modifie le réglage pour grob.property dans context à cette valeur. C’est l’équivalent d’un \temporary \override.

Sans value, enlève le réglage, ce qui permet à d’autres réglages qui étaient masqués de redevenir visibles. C’est l’équivalent d’un \revert.

Pour des raisons de compatibilité, un \override qui n’est pas marqué \temporary enlève le réglage précédent avant d’établir un nouveau réglage. Ceci signifie qu’après une suite de \override, un \revert ne rétablit pas la valeur en vigueur avant le dernier de ces \override, mais la valeur par défaut globale dans le contexte.

La hiérarchie des contextes#

Les contextes sont ordonnés hiérarchiquement. Le contexte de plus haut niveau, qui englobe tous les autres, est appelé Global. Tous les contextes à l’exception de Global sont contenus dans un contexte parent. Le parent d’un Voice est la plupart du temps un Staff, et celui d’un Staff, un Score. Un Staff pourrait aussi être contenu dans un StaffGroup ou apparenté (PianoStaff, GrandStaff, ChoirStaff).

(ly:context-parent context)

Renvoie le parent de context, ou #f si context est le contexte Global.

(ly:context-find context name)

Recherche et renvoie un contexte avec le nom donné (un symbole) comme ancêtre de context, c’est à dire parent, ou parent du parent, ou …

Renvoie #f si aucun contexte n’est trouvé.

Les alias permettent aux types de contextes moins courants comme TabStaff de s’adapter plus facilement à la hiérarchie des contextes courants comme Staff. Les contextes ayant name comme alias sont aussi recherchés par ly:context-find. Ainsi, un TabStaff peut être renvoyé comme parent Staff pour un TabVoice. Ce mécanisme permet à certains graveurs de fonctionner dans plusieurs types de contextes.

Identifiants de contexte#

Chaque contexte possède un identifiant, qui est accessible avec la fonction ly:context-id. La syntaxe \new Staff = "up" ... règle cet identifiant à "up". Par défaut, l’identifiant est généré automatiquement.

Attention, contrairement à ce que leur nom pourrait laisser penser, les identifiants ne sont pas unique. Par exemple, ce code fonctionne parfaitement :

\version "2.25.8"

<<
  \new PianoStaff <<
    \new Staff = "up" { c'8 8 \change Staff = "down" 8 8 }
    \new Staff = "down" { s8 s s s }
  >>
  \new PianoStaff <<
    \new Staff = "up" { c'8 8 \change Staff = "down" 8 8 }
    \new Staff = "down" { s8 s s s }
  >>
>>
Résultat../../images/4c57d8f8c0969cf40b12b2a6c45fa4aff832c668ba5bf2d3eb0e6a38088813bd.svg

Définition d’un type de contextes#

La documentation officielle contient un bon tutoriel sur la manière de définir ses propres contextes, dans la section Définition de nouveaux contextes.

Quelques interfaces de programmation simples pour la traduction#

L’outil ultime pour faire à peu près tout ce que l’on peut imaginer durant l’étape de traduction est l’écriture d’un graveur en Scheme. Néanmoins, cela peut aussi devenir lourd. Avant de nous intéresser aux graveurs, voyons quelques interfaces plus simples à utiliser, bien que plus limitées.

Accéder à un contexte#

Dans une expression musicale, on peut insérer un \applyContext pour appliquer une certaine fonction au contexte dans lequel est interprétée l’expression. Cela est pratique pour modifier une propriété d’une manière qui doit dépendre d’autres propriétés. La fonction \applyContext prend un unique argument, qui est une fonction d’un argument, le contexte. Pour faire que la fonction ne s’applique pas au contexte immédiat mais à l’un de ses ancêtres, il faut appliquer \context à l’expression \applyContext. \context envoie la musique à une contexte trouvé comme ancêtre du contexte immédiat.

Voici comment incrémenter le numéro de mesure de 1 :

\version "2.25.8"

incrementBar =
\context Score
  \applyContext
    #(lambda (context)
       (ly:context-set-property!
         context
         'currentBarNumber
         (1+ (ly:context-property context 'currentBarNumber))))

{
  c'1\break
  c'1\break
  \incrementBar
  c'1
}
Résultat../../images/b31ec38cca9cad4e8b56e6f5279cd05251b12090a6faaa3187340194a7220319.svg

Ajustement de grobs pendant la traduction#

Nous verrons plus tard comment modifier l’apparence et le positionnement des grobs au niveau du moteur de rendu. La fonction \applyOutput est une méthode différente qui s’applique pendant la traduction. Elle est similaire à \applyContext. Elle applique une fonction à certains grobs créés dans un contexte ou dans l’un de ses descendants à un certain moment musical. Elle est utile pour des modifications qui dépendent du contexte. Il y a deux formes de \applyOutput :

\applyOutput Contexte #fonction```
\applyOutput Contexte.Grob #fonction```

Dans la deuxième forme, la fonction est seulement appliqué aux grobs du type Grob, tandis que dans la première, elle est appliquée à tous les grobs.

La fonction est appelée sur trois arguments :

  • Le grob,

  • Le contexte où il a été créé,

  • Le contexte où \applyOutput est utilisée.

Voici un cas où utiliser \applyOutput est pertinent. Cette fonction colorie tous les grobs à la fois.

\version "2.25.8"

colorNotes =
#(define-music-function (color) (color?)
   #{
     \applyOutput Voice
       #(lambda (grob origin-context context)
          (ly:grob-set-property! grob 'color color))
   #})

{
  \colorNotes red
  <e' g' bes'>4
  <e' g' a'>4
  \colorNotes green
  <d' fis' a'>4
}
Résultat../../images/783107af5d9713bce9b9eac3831387cdd7d3f7ead6799e774dfb366803e7e487.svg

Écraser les propriétés de grob modifiées par des graveurs#

La commande \override a pour effet de modifier la valeur par défaut pour une propriété de grob dans un contexte. Néanmoins, il arrive qu’un graveur modifie lui-même la propriété du grob, ce qui rend le \override inopérant. Par exemple, le Note_heads_engraver règle la propriété staff-position des grobs NoteHead qu’il crée en fonction de la hauteur de chaque note. Pour modifier tout de même le réglage de staff-position d’un objet NoteHead, on peut utiliser \overrideProperty. Contrairement à \override, cette commande règle la propriété après que l’objet a été créé et initialisé par son graveur d’origine. En interne, \overrideProperty utilise \applyOutput. La syntaxe de \overrideProperty est semblable à celle de \override, mais sans signe =.

\version "2.25.8"

{
  \overrideProperty NoteHead.staff-position 50
  c'1
}
Résultat../../images/d45594dc3e574dcb2701557e20dda7e0c4341a517c288981a6044ab7c1699750.svg

Écrire un graveur en Scheme#

Fondamentaux#

Les graveurs vivent dans des contextes. Ce sont les définitions de sortie qui contrôlent les graveurs qui s’exécutent dans un contexte. Pour ajouter un graveur, la syntaxe est la suivante :

\layout {
  \context {
    \NomContexte
    \consists Un_graveur
  }
}

Un_graveur est le nom d’un graveur prédéfini parmi ceux listés dans la page Engravers and Performers de la référence des propriétés internes.

La commande \remove est l’inverse de \consists. Elle supprime un graveur d’un contexte où il aurait été activé par défaut.

Pour les graveurs définis en Scheme, il faut remplacer l’argument Un_graveur (un chaîne de caractères) par #Un_graveur (une référence à la variable Un_graveur), en définissant Un_graveur comme une fonction qui prend un contexte et utilise la macro make-engraver pour construire le graveur.

Voici le schéma général d’un graveur. Il comporte plusieurs sections où vous pouvez ajouter du code. On a rarement besoin de toutes les sections  ; un graveur peut tout à fait n’en utiliser que certaines, et les autres sont simplement omises.

#(define (My_engraver context)
   (let (variables...)
     (make-engraver
      ((initialize engraver)
       ...)
      ((start-translation-timestep engraver)
       ...)
      (listeners
       ((event-class-1 engraver event)
        ...)
       ((event-class-2 engraver event)
        ...)
       ...)
      ((pre-process-music engraver)
       ...)
      ((process-music engraver)
       ...)
      (acknowledgers
       ((grob-interface-1 engraver grob source-engraver)
        ...)
       ((grob-interface-2 engraver grob source-engraver)
        ...)
       ...)
      (end-acknowledgers
       ((grob-interface-1 engraver grob source-engraver)
        ...)
        ((grob-interface-2 engraver grob source-engraver)
         ...)
       ...)
      ((process-acknowledged engraver)
       ...)
      ((stop-translation-timestep engraver)
       ...)
      ((finalize engraver)
       ...))))

\layout {
  \context {
    \SomeContext
    \consists #My_engraver
  }
}

Pas de panique ! Le reste de cette section explique chacune des sections du graveur en détail.

Pour donner un premier aperçu, voici un résumé des étapes par lesquelles passe un graveur lorsqu’il est exécuté dans une partition :

initialize

Pour chaque moment de la partition :

start-translation-timestep

Pour chaque événement qui appartient à l’une des classes pour lesquelles le graveur a une méthode dans listeners :

exécuter les listeners correspondants

pre-process-music

process-music

Tant qu’il reste des grobs nouvellement créés :

Pour chaque grob qui possède une interface pour laquelle le graveur a une méthode dans acknowledgers :

exécuter les acknowledgers correspondants

process-acknowledged

stop-translation-timestep

finalize

Les variables dans variables… sont créées dans une clôture (à l’aide de let). Elles sont donc des variables locales du graveur.

Chaque méthode contient une ou plusieurs expressions Scheme, qui sont évaluées dans l’ordre, de manière similaire au corps d’une fonction définie define ou lambda, ou encore d’un let. Les valeurs renvoyées par ces expressions n’ont pas d’importance, car les graveurs fonctionnent par des effets de bord.

Cycle d’une étape temporelle#

La traduction de la musique avance pas à pas dans le temps musical. Tous les graveurs sont synchronisés selon un cycle qu’ils exécutent à chaque « étape temporelle ». Il se passe une « étape temporelle » à chaque moment musical pertinent de la partition. S’il y a des voix simultanées, les étape sont faites de manière à ce que chaque événement puisse trouver place dans une étape temporelle. Schématiquement (x représente une note) :

4 4                      x----------------  x------------------
\tuplet 3/2 { 4 4 4 }    x---------- x------------ x------------

Étapes temporelles       [ étape    ][étape][étape][  étape  ]

Au début d’une étape temporelle, le code contenu dans start-translation-timestep est exécuté dans tous les graveur. De façon symétrique, à la fin d’une étape temporelle, la méthode stop-translation-timestep est exécutée.

Au début de la toute première étape temporelle, la méthode initialize est exécutée. Elle peut être utilisée pour initialiser ce qui a besoin de l’être.

Attention, la toute première étape temporelle est aussi un cas particulier en ce que start-translation-timestep n’y est pas appelée. La raison en est que les contextes sont créés en réponse à des expressions musicales comme \new, et la manière dont ces expressions sont traitées par les itérateurs fait que la création d’un nouveau contexte peut en fait arriver après le début d’une étape temporelle.

La méthode initialize a une contrepartie finalize, appelée à la toute fin de la partition avant que le graveur ne s’arrête. Elle peut être utilisée, par exemple, pour émettre un avertissement si une liaison n’a pas été terminée. La dernière étape temporelle n’a pas le même cas particulier que la première : la méthode stop-translation-timestep y est bien appelée, juste avant finalize.

Réagir aux événements#

Dans la section listeners du graveur se trouvent des méthodes spécifiques à certaines classes d’événements. Quand un événement correspondant à la classe d’une méthode est diffusé par les itérateurs, la méthode est appelée avec pour arguments le graveur lui-même, et l’événement.

(listeners
 ((event-class-1 engraver event)
  ...)
 ((event-class-2 engraver event)
  ...))

La diffusion des événements suit la hiérarchie des contextes. Les événements reçus dans un contexte sont aussi reçus dans tous ses ancêtres. Ainsi, un graveur dans le contexte Voice est approprié pour réagir aux événements de note, tandis qu’un graveur au niveau Staff est approprié pour réagir aux changements d’armure, qui peuvent provenir de n’importe lequel des contextes Voice qui descendent d’un Staff sans que cela ne change le résultat.

Il est important de bien comprendre qu’il ne faut pas créer de grobs dans la section listeners, en raison de l’ordre des événements, qui est arbitraire. Lorsqu’une expression musicale de musique simultanée (SimultaneousMusic, la construction << ... >>) est traitée par les itérateurs, l’ordre dans lequel les événements doivent arriver n’est pas forcément clair, comme l’illustre l’exemple

\version "2.25.8"

\new Staff <<
  { \time 3/4 c'2. }
  \\
  { \override Staff.TimeSignature.color = red c2. }
>>
Résultat../../images/0ba33a3def2bd87429a3fd9b950dc2c802f12644ed8bdb4bd7b2d0c854fa25f7.svg

Dans ce code, en fonction de la manière dont les itérateurs décident de se comporter, l’événement OverrideProperty correspondant au \override peut être diffusé après l’événement TimeSignatureEvent du \time. Quel que soit l’ordre entre les deux, on souhaite que le \override affecte le grob TimeSignature.

C’est la raison pour laquelle les listeners ne doivent être utilisés que pour prendre note des événements qui ont été reçus, ou pour régler des propriétés de contexte. Ensuite, une fois que tous les événements ont été reçus, la méthode process-music est appelée. C’est à ce moment-là, et pas avant, que des grobs peuvent être créés.

Lorsque l’on règle une propriété de contexte, il arrive souvent que cela doive être fait avant process-music, pour que les autres graveurs puissent lire sa valeur finale dans process-music. La plupart du temps, il suffit de régler la propriété dans les listeners. Néanmoins, il existe des cas où cela ne convient pas. Il en est ainsi du cas où le réglage dépend de plusieurs événements à la fois, et non pas d’un seul. Il arrive également que le réglage ne dépende pas du tout des événements, mais qu’il soit trop tôt pour le faire dans start-translation-timestep. En pareil cas, on peut utiliser la méthode pre-process-music, qui est exécutée entre les listeners et process-music. Elle ne sert normalement pas à créer de grobs, mais seulement à des tâches auxiliaires qui affectent les autres graveurs, comme modifier les propriétés des contextes.

Exemple de modification de propriétés dans pre-process-music : Force_chord_at_bar_start_engraver#

Dans un contexte ChordNames où la propriété chordChanges est mise à #t, lorsqu’un accord est identique au précédent, il est supprimé. Ce graveur implémente un entre-deux entre chordChanges = ##t et chordChanges = ##f, à savoir que l’accord redondant est supprimé s’il est au milieu d’une mesure, mais pas si c’est lui qui commence la mesure. À cette fin, le graveur possède trois méthodes :

  • initialize, pour démarrer avec chordChanges à ##t, comme si l’utilisateur avait écrit \set chordChanges = ##t,

  • pre-process-music, pour régler temporairement chordChanges à ##f à chaque fois que la propriété measureStartNow vaut #t (c’est ainsi que le graveur Timing_translator communique le début de chaque mesure),

  • stop-translation-timestep, pour remettre chordChanges à #t.

Ce graveur est si simple qu’il n’a pas besoin de variables locales dans un let autour du make-engraver (c’est rarement le cas).

La partie qui met chordChanges à ##f au début de chaque mesure doit être exécutée dans pre-process-music, et pas avant ni après. Dans start-translation-timestep, il serait trop tôt car le Timing_translator définit measureStartNow dans start-translation-timestep, donc le résultat dépendrait de l’ordre dans lequel les méthodes start-translation-timestep de Timing_translator et Force_chord_at_bar_start_engraver sont exécutées, ce qu’il est fortement conseillé d’éviter. A contrario, dans process-music, il serait trop tard, puisque Chord_name_engraver lit chordChanges dans process-music, donc cela entraînerait le même type de problème d’ordre des graveurs. C’est pourquoi pre-process-music est le moment idéal.

Attention, measureStartNow n’est pas définie à #t au tout début de la pièce. Dans ce cas précis, cela ne pose aucun problème puisque le premier accord est de toute façon toujours visible (il n’y a pas d’accord avant, donc la propriété chordChanges ne fait aucune différence).

\version "2.25.8"

#(define (Force_chord_at_bar_start_engraver context)
   (make-engraver
    ((initialize engraver)
     (ly:context-set-property! context 'chordChanges #t))
    ((pre-process-music engraver)
     (when (ly:context-property context 'measureStartNow #f)
       (ly:context-set-property! context 'chordChanges #f)))
    ((stop-translation-timestep engraver)
     (ly:context-set-property! context 'chordChanges #t))))

ch = \chordmode { c1 g2 g2 g2 c2:7 f2 }

\layout {
  indent = 40
  \context {
    \Score
    \remove System_start_delimiter_engraver
  }
  \context {
    \ChordNames
    \consists Bar_engraver
    \consists Instrument_name_engraver
    \consists Staff_symbol_engraver
    \override StaffSymbol.line-count = 0
    \override BarLine.bar-extent = #'(0 . 2)
  }
}

<<
  \new ChordNames \with {
    instrumentName = "Normal"
  } \ch
  \new ChordNames \with {
    instrumentName = \markup \typewriter chordChanges
    chordChanges = ##t
  } \ch
  \new ChordNames \with {
    instrumentName = \markup { engraver }
    \consists #Force_chord_at_bar_start_engraver
  } \ch
>>
Résultat../../images/320200ba71785a74ab86c1ff09675f2567c9db85aef57fdb6d6be6d3104abe89.svg

Création de grobs#

La fonction principale pour créer des grobs est ly:engraver-make-grob.

(ly:engraver-make-grob engraver grob-type cause)

Crée et renvoie un nouveau grob.

Le type du grob, grob-type, est donné comme un symbole. cause est la cause du grob, qui doit être un événement ou un autre grob, si cela a du sens. Si le grob n’est pas vraiment créé en réaction à une cause (par exemple une barre de mesure), il faut passer la liste vide '() pour cause.

Cependant, il existe aussi une deuxième fonction. Comme vu dans Sortes de grobs, il existe plusieurs sortes de grobs, principalement les grobs ponctuels et étendus. La plupart du temps, un type de grob ne peut être utilisé qu’avec une sorte donnée, mais il existe aussi des types de grob qui peuvent être créés avec les deux sortes, au choix. Le cas le plus commun est celui des grobs dits « adhésifs », qui s’attachent à un autre grob quelconque, tels que les notes de bas de page, les info-bulles ou encore les parenthèses. De très nombreux grobs sont attachés à d’autres grobs (les articulations, par exemple, sont attachées à des têtes de note), mais ce qui rend les grobs adhésifs particuliers est qu’ils peuvent s’attacher à n’importe quel grob, donc en particulier à la fois à un grob ponctuel et à un grob étendu. Pour cette raison, un grob adhésif peut être créé dans les deux sortes, ponctuel ou étendu, en fonction de la sorte du grob auquel il est attaché. Pour ce cas relativement particulier, on dispose d’une fonction spécifique :

(ly:engraver-make-grob engraver grob-type cause)

Crée un grob adhésif de la même sorte que host, et règle ses parents et extrémités pour qu’il s’attache à host.

grob-type doit être un type de grobs adhésifs, c’est à dire qu’il doit avoir l’interface ()[internals:sticky-grob-interface].

Exemple de création de grobs : Tacet_engraver#

Ce graveur ajoute le texte Tacet (en tant qu’objet TextMark) au début de chaque groupe de silences multi-mesures consécutifs. Pour ce faire, il a des listeners pour les événements multi-measure-rest-event (pour détecter les silences multi-mesures), ainsi que pour note-event et rest-event (pour détecter la fin d’un groupe de silences multi-mesures lorsqu’il y a note ou un silence normal, ce qui est nécessaire afin d’éviter d’afficher plusieurs « Tacet » redondants dans le même groupe de silences multi-mesures). TextMark n’est pas un grob adhésif, donc la fonction utilisée pour créer ce grob est ly:engraver-make-grob.

\version "2.25.8"

#(define (Tacet_engraver context)
   (let ((in-tacet #f)
         (mmrest-event #f))
     (make-engraver
      (listeners
       ((multi-measure-rest-event engraver event)
        (set! mmrest-event event))
       ((note-event engraver event)
        (set! in-tacet #f))
       ((rest-event engraver event)
        (set! in-tacet #f)))
      ((process-music engraver)
       (when (and mmrest-event (not in-tacet))
         (set! in-tacet #t)
         (let ((grob (ly:engraver-make-grob engraver 'TextMark mmrest-event)))
           (ly:grob-set-property! grob 'text "Tacet"))))
      ((stop-translation-timestep engraver)
       (set! mmrest-event #f)))))

\new Staff \with {
  \consists #Tacet_engraver
} {
  R1
  c'4 8 8 4 4
  R1*2
  e'4 8 8 4 4
  R1 R1
  g'4 8 8 4 4
}
Résultat../../images/81c86c92f78b5e90c7fd8c7542bfc964050a17e6e8953487c7fe2cd191f7389a.svg

Gestion du temps#

On peut accéder au point du temps musical où la traduction est en train de se faire à l’aide de cette fonction appelée sur le contexte passé en argument au graveur :

(ly:context-current-moment context)

Renvoie le moment courant du temps musical pour context.

Tous les contextes sont synchronisés, donc cette fonction renvoie le même résultat sur un contexte ou sur son parent, par exemple.

Bien sûr, rien n’empêche d’utiliser cette fonction sur le contexte passé à \applyContext. Elle est simplement expliquée ici car elle est généralement la plus utile dans un graveur.

La plupart du temps, la composante d’ornement du moment renvoyé vaut zéro. Cependant, s’il a des notes d’ornement, alors les moments au début et au milieu du groupe de notes d’ornement auront une composante d’ornement (strictement) négative.

Certains événements, comme les événements de note, de silence ou encore de silence multi-mesures, possèdent une durée musicale. Elle n’est reflétée en rien dans la manière dont le graveur reçoit ces événements. Le graveur n’est informé que lorsqu’un événement se produit (commence), mais pas lorsqu’un événement avec une durée se finit. Cependant, le graveur est libre d’utiliser la durée de l’événement, qui est un moment stocké dans sa propriété length.

En théorie, cette possibilité pourrait suffire. Toutefois, une complication apparaît pour les notes d’ornement. Lorsque les itérateurs consomment l’expression musicale \grace { c8 }, l’événement de note émis a pour propriété length un moment dont la partie principale vaut 1/8 et la partie d’ornement 0, car la longueur d’un événement est calculée indépendamment du fait qu’il se trouve dans un groupe de notes d’ornement ou non. C’est pourquoi utiliser simplement (ly:event-property <événement> 'length) risque de causer des soucis si le cas des notes d’ornement n’est pas traité correctement. Pour faciliter la vie du programmeur, la fonction suivante a ajoutée à LilyPond 2.25.1 :

(ly:event-length event [moment])

Renvoie la longueur de l’événement event en supposant qu’il arrive à l’instant moment. Dans le cas le plus commun, la composante d’ornement de moment vaut zéro, et cette fonction renvoie simplement (ly:event-property event 'length). Toutefois, si la composante d’ornement de moment est strictement négative, la durée d’origine de l’événement, (ly:event-property event 'length), est convertie en un moment dont la composante principale vaut zéro et qui possède une composante d’ornement strictement positive.

On peut aussi omettre le paramètre moment, auquel cas cette fonction est simplement un raccourci pour (ly:event-property event 'length).

Il arrive qu’une étape doive être faite dans le temps musical sans pour autant qu’il n’y ait d’événement. Le cas principal est celui des barres de mesure : avec un code comme { c'1*2 }, une barre de mesure est ajoutée au milieu du do, même s’il n’y a aucun événement à ce moment-là. C’est le Timing_translator, l’un des graveurs qui s’exécutent par défaut dans Score, qui signale qu’il faut marquer une étape à ce moment-là à cause de la barre de mesure. Avec LilyPond 2.25.1 ou supérieure, ce mécanisme est également accessible pour les graveurs écrits en Scheme.

(ly:context-schedule-moment context moment)

Demande à ce qu’une étape soit marqué à moment, un moment du temps musical se situant dans le futur, c’est à dire après le moment courant.

Exemple d’utilisation de la longueur d’un événement : Auto_breathe_engraver#

Ce graveur réagit aux événements de silence. Il calcule le moment où chaque silence se termine à l’aide de ly:event-length, et conserve en permanence le moment où le dernier silence qui a été vu doit se terminer. Dans process-music, si ce moment est égal au moment courant du contexte, cela signifie que le silence précédent vient de se terminer sans qu’il n’y ait un nouveau silence pour le remplacer (il aurait une valeur plus grande de moment de fin). Sous cette condition, le graveur affiche un signe de respiration, pour marquer la fin de la phrase musicale. Le signe de respiration est initialisé à l’aide de ly:breathing-sign::set-properties, ce qui correspond à ce que fait le graveur Breathing_sign_engraver défini dans LilyPond.

\version "2.25.8"

#(define (Auto_breathe_engraver context)
   (let ((previous-rest-event #f)
         (previous-rest-end-moment #f))
     (make-engraver
      (listeners
       ((rest-event engraver event)
        (set! previous-rest-event event)
        (let ((current (ly:context-current-moment context)))
          (set! previous-rest-end-moment
                (ly:moment-add current (ly:event-length event current))))))
      ((process-music engraver)
       (when (equal? previous-rest-end-moment (ly:context-current-moment context))
         (let ((grob (ly:engraver-make-grob engraver 'BreathingSign previous-rest-event)))
           (ly:breathing-sign::set-breath-properties grob context 'comma)))))))

\layout {
  \context {
    \Voice
    \consists #Auto_breathe_engraver
    % https://gitlab.com/lilypond/lilypond/-/issues/6273
    \override BreathingSign.extra-spacing-height = #'(-inf.0 . +inf.0)
  }
}

\relative {
  \time 9/8
  \partial 8
  a8 b4 d8 f4. r4 r8
  g,8 a c r4 ees8 g4.
}
Résultat../../images/3fbbd6fd0229faf89006d5d207482d71b53d7cb1b14347b34259ff1fbaf56147.svg

Réagir aux grobs#

Les listeners et process-music sont souvent suffisants pour créer les grobs dont on a besoin. Néanmoins, la traduction n’a pas seulement pour but de créer les grobs, mais aussi de les mettre en réseau. Un objet Dots, par exemple, doit contenir un lien vers l’objet NoteHead auquel il est rattaché. Pour garder LilyPond modulaire, Dots est créé dans un graveur différent de NoteHead. C’est pourquoi il existe un mécanisme pour réagir aux grobs créés par d’autres graveurs, la section acknowledgers.

Chaque graveur réagit aux grobs créés par des graveurs dans les contextes en-dessous de son contexte. Ainsi, un graveur dans un contexte Voice ne réagit qu’aux grobs créés dans ce contexte, tandis qu’un graveur au niveau Staff réagit non seulement aux grobs créés dans ce Staff mais aussi ceux créés dans les Voice qu’il contient.

Le corps de la section acknowledgers suit une structure similaire à celle de listeners :

(acknowledgers
 ((grob-interface-1 engraver grob source-engraver)
  ...)
 ((grob-interface-2 engraver grob source-engraver)
  ...)
 ...)

Chaque méthode dans acknowledgers correspond à une interface, et est appelée avec trois arguments : le graveur, le grob auquel le graveur réagit, et le graveur qui a créé ce grob. On peut utiliser la fonction ly:translator-context pour accéder au contexte dans lequel le graveur d’origine réside.

De même qu’avec les listeners, les méthodes dans acknowledgers ne doivent pas être utilisées pour créer d’autres grobs, mais seulement pour prendre note des grobs reçus. En effet, d’autres graveurs pourraient modifier les propriétés du grob auquel il est réagi dans leurs propres sections acknowledgers. De même qu’il existe process-music pour traiter ce qui a été reçu dans listeners, il existe process-acknowledged pour acknowledgers.

((process-acknowledged engraver)
 ...)

On peut créer de nouveaux grobs dans process-acknowledged. Cela déclenche un nouveau cycle de acknowledgers et process-acknowledged pour que les autres graveurs puissent réagir aux grobs nouvellement créés.

Les graveurs ne réagissent jamais à leurs propres grobs.

Exemple de réaction aux grobs : Balloon_notes_engraver#

Ce graveur ajoute une info-bulle sur chaque note pour indiquer sa hauteur. Puisque les info-bulles – BalloonText – sont des objets adhésifs, ils sont créés avec ly:engraver-make-sticky. Le graveur règle plusieurs propriétés de ces objets BalloonText. En particulier, les propriétés X-offset et Y-offset, normalement héritées d’une expression musicale AnnotateOutputEvent (qui provient d’un appel à \balloonText), sont modifiées pour que les info-bulles s’évitent entre elles s’il y en a plusieurs sur un accord. La propriété annotation-balloon est mise à #f pour supprimer le rectangle autour de chaque note. Les hauteurs sont converties en chaînes de caractères en notation anglo-saxonne (english) à l’aide de la fonction note-name->string.

Notez que la liste note-heads est remise à la liste vide à la fin de process-acknowledged, pour éviter d’ajouter des info-bulles plusieurs fois sur la même tête de note s’il y a plusieurs appels à process-acknowledged.

\version "2.25.8"

#(define (Balloon_notes_engraver context)
   (let ((note-heads '()))
     (make-engraver
      (acknowledgers
       ((note-head-interface engraver grob source-engraver)
        (set! note-heads (cons grob note-heads))))
      ((process-acknowledged engraver)
       (for-each
        (lambda (i note-head)
          (let* ((note-event (event-cause note-head))
                 (pitch (ly:event-property note-event 'pitch))
                 (pitch-string
                   (string-capitalize
                     (note-name->string pitch 'english)))
                 (balloon (ly:engraver-make-sticky engraver
                                                   'BalloonText
                                                   note-head
                                                   note-head)))
            (ly:grob-set-property! balloon 'font-size -3)
            (ly:grob-set-property! balloon 'font-series 'bold)
            (ly:grob-set-property! balloon 'X-offset -2)
            (ly:grob-set-property! balloon 'Y-offset (+ i 0.2))
            (ly:grob-set-property! balloon 'annotation-balloon #f)
            (ly:grob-set-property! balloon 'text pitch-string)
            (ly:grob-set-property! note-head
                                   'extra-spacing-width
                                   '(-3.5 . 0))))
        (reverse! (iota (length note-heads)))
        note-heads)
      (set! note-heads '())))))

\layout {
  \context {
    \Voice
    \consists #Balloon_notes_engraver
  }
}

{
  <e' g' b'>1
  <e' g' a'>1
}
Résultat../../images/ac467fdd4134a26b0aaa76c19cba83f2c1f76b2b750a7e725e32463819a0de7a.svg

Réagir à la fin d’un grob#

Certains grobs, appelés spanners, s’étendent sur une plage de temps, comme les soufflets de crescendo. Leurs extrémités sont donc définies lors de deux étapes temporelles différentes. Souvent, ils sont terminés en réaction à un événement, comme \! pour les soufflets.

En plus de la section acknowledgers, dont les méthodes réagissent à la création d’un grob, il existe la section end-acknowledgers, dont les méthodes réagissent à la fin d’un grob étendu. La fin doit être annoncée par le graveur qui a créé le grob. Un graveur écrit en Scheme est également censé annoncer la fin de chaque grob étendu qu’il crée.

(ly:engraver-announce-end-grob engraver spanner cause)

Annonce la fin du grob étendu spanner aux autres graveurs.

Comme pour ly:engraver-make-grob, l’argument cause est un événement, un autre grob, ou la liste vide.

Exemple de réaction à la fin d’un grob : No_break_during_tie_engraver#

Ce graveur empêche qu’un saut de ligne ne coupe une liaison, ce qu’il fait en réglant la propriété forbidBreak du contexte Score à #t. Prêtez attention à la manière dont la variable tie-in-progress est maintenue. Un saut de ligne est interdit à gauche de la note même si la liaison termine sur cette note, et il ne faut pas interdire un saut à gauche d’une note où commence une liaison. C’est pourquoi les méthodes dans acknowledgers ne font que prendre note des liaisons qui commencent ou se terminent, et ce n’est que dans stop-translation-timestep que leurs résultats sont combinés (ce qui est souvent fait plutôt dans process-acknowledged que dans stop-translation-timestep).

\version "2.25.8"

#(define (No_break_during_tie_engraver context)
   (let ((tie-in-progress #f)
         (acknowledged-start-tie #f)
         (acknowledged-end-tie #f))
     (make-engraver
      ((pre-process-music engraver)
       (when tie-in-progress
         (let ((score (ly:context-find context 'Score)))
           (ly:context-set-property! score 'forbidBreak #t))))
      (acknowledgers
       ((tie-interface engaver grob source-engraver)
        (set! acknowledged-start-tie #t)))
      (end-acknowledgers
       ((tie-interface engraver grob source-engraver)
        (set! acknowledged-end-tie #t)))
      ((stop-translation-timestep engraver)
       (when acknowledged-end-tie
         (set! tie-in-progress #f)
         (set! acknowledged-end-tie #f))
       (when acknowledged-start-tie
         (set! tie-in-progress #t)
         (set! acknowledged-start-tie #f))))))

music = {
  \repeat unfold 20 c'1~ c'1
  c'1
  \repeat unfold 20 c'1~ c'1
}

\new Voice \music
\new Voice \with { \consists #No_break_during_tie_engraver } \music
Résultat../../images/ee770e978e1de0520ec6a9e587f56d21375b11d37e6b5c5ea352c641d43c8ee9.svg

Réglage des parents et extrémités des grobs#

L’une des responsabilités des graveurs lorsqu’ils créent des grobs est de s’assurer que ces grobs ont un parent sur chaque axe, et, dans le cas des grobs étendus, qu’ils ont des extrémités.

À chaque moment musical, les propriétés currentMusicalColumn et currentCommandColumn d’un contexte sont réglées à deux colonnes, l’une dite « musicale » (currentMusicalColumn) et l’autre « non-musicale » (currentCommandColumn). La colonne musicale est utilisée par défaut comme parent sur l’axe X de tous les grobs ponctuels « musicaux » créés dans cette étape temporelle. De même, la colonne non-musicale est utilisée pour les grobs ponctuels « non-musicaux ». La définition précise est celle-ci : un grob ponctuel est non-musical si sa propriété non-musical est mise à #t ou si son parent horizontal est non-musical.

Pour les grobs étendus, les extrémités ne sont pas réglées implicitement. Le graveur doit toujours les régler lui-même. Pour les grobs étendus qui ne commencent et ne se terminent pas sur des grobs précis comme des têtes de note, on peut utiliser soit la colonne musicale, soit la colonne non-musicale. Par défaut, le parent sur l’axe X d’un grob étendu est son extrémité gauche.

Sur l’axe Y, il n’y a pas de différence entre grobs ponctuels et étendus. Le graveur Axis_group_engraver, qui est exécuté par défaut dans certains contextes comme Staff, Lyrics ou Dynamics, crée un grob VerticalAxisGroup pour regrouper verticalement tous les grobs de son contexte, en définissant le parent Y de chaque grob auquel il réagit au VerticalAxisGroup si le grob n’avait pas encore de parent Y. Les grobs créés dans des contextes de plus haut niveau que Staff, comme Score, ont par défaut le grob System de la partition entière pour parent Y.

Exemple de réglage des parents : Auto_stanza_engraver#

Dans une partition vocale, chaque couplet est traditionnellement commencé par \set stanza = "x.", avec x = 1 pour le premier couplet, etc. Ce graveur permet de le faire automatiquement dans les cas les plus courants. Il réagit aux grobs VerticalAxisGroup (caractérisés par la hara-kiri-group-spanner-interface), et pour chaque tel grob qui provient d’un contexte Lyrics, il crée un StanzaNumber avec pour parent ce VerticalAxisGroup (remarquez qu’une alternative serait de créer ces grobs StanzaNumber dans un graveur séparé au niveau Lyrics, ce qui rendrait leur parent réglé automatiquement). Il réagit aussi au grob VerticalAlignment auquel tous ces VerticalAxisGroup appartiennent. À la fin de la première étape temporelle, il lit le tableau de grobs elements du VerticalAlignment. Ce tableau contient les VerticalAxisGroup dans leur ordre vertical, ce qui permet de définir le texte de chaque StanzaNumber avec le bon numéro.

\version "2.25.8"

#(use-modules ((ice-9 hash-table) #:select (alist->hashq-table)))

#(define (Auto_stanza_engraver context)
   (let ((axis-groups '())
         (stanza-numbers '())
         (vertical-alignment #f))
     (make-engraver
      (acknowledgers
       ((hara-kiri-group-spanner-interface engraver grob source-engraver)
        (let ((source-context (ly:translator-context source-engraver)))
          (when (ly:context-find source-context 'Lyrics)
            (set! axis-groups (cons grob axis-groups)))))
       ((align-interface engraver grob source-engraver)
        (set! vertical-alignment grob)))
      ((process-acknowledged engraver)
       (for-each
        (lambda (axis-group)
          (let ((stanza-number
                 (ly:engraver-make-grob engraver 'StanzaNumber axis-group)))
            (ly:grob-set-parent! stanza-number Y axis-group)
            (set! stanza-numbers (cons stanza-number stanza-numbers))))
        axis-groups)
       (set! axis-groups '()))
      ((stop-translation-timestep engraver)
       (when vertical-alignment
         (let ((group-to-stanza (alist->hashq-table
                                 (map (lambda (stanza-number)
                                        (cons (ly:grob-parent stanza-number Y)
                                              stanza-number))
                                      stanza-numbers)))
               (i 1))
           (for-each
            (lambda (group)
              (let ((stanza (hashq-ref group-to-stanza group)))
                (when stanza
                  (let ((i-str (number->string i)))
                    (ly:grob-set-property! stanza 'text (string-append i-str ".")))
                  (set! i (1+ i)))))
            (ly:grob-array->list (ly:grob-object vertical-alignment 'elements #f))))
         (set! vertical-alignment #f))))))

\layout {
  \context {
    \Score
    \consists #Auto_stanza_engraver
  }
}

<<
  \new Voice = melody \fixed c' { c4 c g g a a g2 }
  \new Lyrics \lyricsto melody { Twin -- kle, twin -- kle, lit -- tle star… }
  \new Lyrics \lyricsto melody { When the bla -- zing sun is gone… }
  \new Lyrics \lyricsto melody { Then the tra -- veller in the dark… }
>>
Résultat../../images/c222521dda62fb3a9b0a0688460013a3136b7b1186e781561a436b82fde154d1.svg

Exemple de réglage des extrémités : Align_all_dynamics_engraver#

Ce graveur aligne toutes les nuances verticalement. Cela est plus ou moins équivalent à mettre les nuances dans un contexte Dynamics, sauf que les nuances peuvent continuer d’être entrées dans la mélodie principale, et pas dans une voix séparée avec des silences invisibles.

Ce graveur fonctionne en créant un grob DynamicLineSpanner pour toute la partition, contrairement au Dynamic_align_engraver (exécuté par défaut), qui crée un DynamicLineSpanner pour chaque suite de nuances consécutives.

Les nuances sont ajoutées au DynamicLineSpanner à l’aide de la fonction ly:axis-group-interface::add-element, qui, en plus de régler le parent Y de chaque nuance au DynamicLineSpanner, ajoute un pointeur de la nuance vers le DynamicLineSpanner.

Les extrémités de ce DynamicLineSpanner sont des colonnes non-musicales.

\version "2.25.8"

#(define (Align_all_dynamics_engraver context)
   (let ((line-spanner #f))
     (make-engraver
      ((process-music engraver)
       (when (not line-spanner)
         (set! line-spanner
               (ly:engraver-make-grob engraver 'DynamicLineSpanner '()))
         (let ((column (ly:context-property context 'currentCommandColumn)))
           (ly:spanner-set-bound! line-spanner LEFT column))))
      (acknowledgers
       ((dynamic-interface engraver grob source-engraver)
        (ly:axis-group-interface::add-element line-spanner grob)))
      ((finalize engraver)
       (let ((column (ly:context-property context 'currentCommandColumn)))
         (ly:spanner-set-bound! line-spanner RIGHT column))))))

\layout {
  \context {
    \Voice
    \remove Dynamic_align_engraver
    \consists #Align_all_dynamics_engraver
  }
}

\new Staff \relative {
  \override DynamicLineSpanner.direction = #UP
  c'2\< d4 e |
  c4 e e,2\f |
  g'4\dim a g a |
  c1\p |
}
Résultat../../images/93947b5127aaf4fd15dab7a60951b14cc29ab4a15c91cab43acdc0fea8e2ee80.svg

Suicide de grobs#

Les graveurs avancent pas à pas dans le temps. Ils ne peuvent pas « voir dans l’avenir ». Si un grob est créé et que le graveur réalise plus tard qu’il est superflu, il peut le supprimer, avec la fonction ly:grob-suicide!.

Exemple de suicide de grobs : Voice_line_engraver#

Ce graveur dessine des lignes entre les têtes de note d’une mélodie. Observez l’utilisation de ly:grob-suicide!. Le grob VoiceFollower doit être créé dès qu’une tête de note est vue, sans que le graveur ne sache s’il y aura ensuite une tête de note

De plus, les lignes ne sont pas affichées s’il y a une liaison. Il se trouve que la fin d’une liaison n’est pas annoncée dans l’étape temporelle où elle se passe, mais dans la suivante, ce qui conduit à quelques complications. C’est pour cette raison que ce code utilise un deuxième graveur pour supprimer les lignes qui sont en même temps qu’une liaison. Lorsque l’on écrit un graveur complexe, il peut être difficile de penser à tous les états dans lesquels il peut être. En pareil cas, il peut être utile de séparer le graveur en plusieurs graveurs, chacun avec des invariants plus simples.

Avis de recherche : exemple avec plus d’intérêt musical.

\version "2.25.8"

#(define (Voice_line_engraver context)
   (let (
         ;; Current follower
         (follower #f)
         ;; Note head grob acknowledged
         (note-head #f)
         ;; Moment at which the follower should end. If there is no note head
         ;; at that moment (e.g., because there is a rest), it is removed.
         (expected-end-mom #f))
     (make-engraver
      (acknowledgers
       ((note-head-interface engraver grob source-engraver)
        (set! note-head grob)))
      ((process-acknowledged engraver)
       (when note-head
         ;; End the previous follower on this note head.
         (when follower
           (ly:spanner-set-bound! follower RIGHT note-head)
           (ly:engraver-announce-end-grob engraver follower note-head)
           (set! follower #f)
           (set! expected-end-mom #f))
         ;; Create a new follower starting from this note head.
         (set! follower (ly:engraver-make-grob engraver 'VoiceFollower note-head))
         (ly:spanner-set-bound! follower LEFT note-head)
         ;; Record the moment at which we expect the follower to end.
         (set! expected-end-mom
               (let ((current (ly:context-current-moment context))
                     (note-event (ly:grob-property note-head 'cause)))
               (ly:moment-add current (ly:event-length note-event current))))
         (set! note-head #f)))
      ((stop-translation-timestep engraver)
       ;; If the follower reached its expected end moment without finding
       ;; a note head to end on (e.g., because of a rest), remove it.
       (let ((current (ly:context-current-moment context)))
         (when (and expected-end-mom (moment<=? expected-end-mom current))
           (ly:grob-suicide! follower)
           (set! follower #f)
           (set! expected-end-mom #f)))
       ;; Bookkeeping
       (set! note-head #f))
      ((finalize engraver)
       ;; Remove any unterminated follower
       (when follower
         (ly:grob-suicide! follower))))))

#(define (Remove_voice_line_when_slur_engraver context)
   (let (
         ;; Any voice follower started in this time step
         (started-voice-follower #f)
         ;; Any voice follower ended in this time step
         (ended-voice-follower #f)
         ;; The currently "active" voice follower; a follower is active
         ;; from its start time step included to its end time step excluded.
         (active-voice-follower #f)
         ;; The voice follower that was active at the previous time step.
         (previous-active-voice-follower #f)
         ;; Any slur started in this time step
         (started-slur #f)
         ;; Any slur started in the previous time step
         (previous-started-slur #f)
         ;; Any slur of which we acknowledge the end, meaning that it ended
         ;; in the *previous* time step.
         (previous-ended-slur #f)
         ;; The slur that was active in the previous time step.
         (previous-active-slur #f))
     (make-engraver
      (acknowledgers
       ((line-spanner-interface engraver grob source-engraver)
        ;; Ideally, we'd have a specific interface for VoiceFollower,
        ;; but it doesn't exist.
        (when (eq? 'VoiceFollower (grob::name grob))
          (set! started-voice-follower grob)))
       ((slur-interface engraver grob source-engraver)
        (set! started-slur grob)))
      (end-acknowledgers
       ((line-spanner-interface engraver grob source-engraver)
        (when (eq? 'VoiceFollower (grob::name grob))
          (set! ended-voice-follower grob)))
       ((slur-interface engraver grob source-engraver)
        (set! previous-ended-slur grob)))
      ((stop-translation-timestep engraver)
       ;; Determine if there was an active slur in the previous time step.
       (when previous-ended-slur
         (set! previous-active-slur #f)
         (set! previous-ended-slur #f))
       (when previous-started-slur
         (set! previous-active-slur previous-started-slur))
       ;; If a voice follower and a slur were both active in the previous
       ;; time step, remove the voice follower.
       (when (and previous-active-voice-follower previous-active-slur)
         (ly:grob-suicide! previous-active-voice-follower))
       ;; Determine if there was an active voice follower in this time step
       (when ended-voice-follower
         (set! active-voice-follower #f)
         (set! ended-voice-follower #f))
       (when started-voice-follower
         (set! active-voice-follower started-voice-follower)
         (set! started-voice-follower #f))
       ;; Set previous-* variables for the next time step.
       (set! previous-active-voice-follower active-voice-follower)
       (set! previous-started-slur started-slur)
       (set! started-slur #f)))))


\layout {
  \context {
    \Voice
    \consists #Voice_line_engraver
    \consists #Remove_voice_line_when_slur_engraver
    \override VoiceFollower.style = #'dashed-line
    \slurDashed
  }
}

\relative {
  \override Score.SpacingSpanner.spacing-increment = 4
  d'16( e f8) e d a' d4 a8 |
  bes8 g16( e) a8 f16( d) g8 e16( cis) a8 bes'8~ |
  bes8 g16( e) a,8 cis'8~ cis bes16( g) a,8 e''~ |
  e8 cis16( a) bes( g a f) g( e f d) e( cis d b) |
  cis( a b cis) d( e f g) a( bes c8) c16( d ees8) |
  fis,8 g cis, d gis, r a r |
}
Résultat../../images/0c67ec9b509c4730a0840cb00c3729da8087f7501c41905219442374fbc8e39c.svg