Moteur de rendu#

Le moteur de rendu est la partie de LilyPond qui positionne et dessine les grobs. C’est aussi dans cette partie que sont déterminés les sauts de ligne et de page.

TOOD : this page still needs more examples.

Interfaces de grobs#

Les interfaces sont faites pour regrouper des grobs qui possèdent des caractéristiques communes. Elles sont semblables aux classes musicales, qui regroupent des types d’expressions musicales. Toutes les interfaces, avec les grobs qu’elles recouvrent, sont listées dans la page Graphical Object Interfaces de la référence des propriétés internes.

Bien qu’il existe une fonction grob::name qui renvoie le type d’un grob, il est préférable de tester si un grob a une certaine interface plutôt que de tester si son type est un certain type.

(grob::has-interface grob interface)

Renvoie un booléen qui dit si grob possède l’interface interface (passée comme symbole).

Fonctions de rappel#

Comprendre les fonctions de rappel#

Les grobs possèdent des propriétés. Les fonctions de base pour lire et modifier les propriétés des grobs suivent le même schéma que pour les probs : ly:grob-property et ly:grob-set-property!. Les types de grob sont listés dans la référence des propriétés internes dans la page (internals:All_Layout_Objects). La page de chaque grob donne les réglages par défaut de ses propriétés.

Cependant, les grobs ne sont pas des probs. Leur système de propriétés est plus riche.

Une propriété d’un grob peut simplement être mise à une valeur qui convient. L’épaisseur d’une hampe, par exemple, ne dépend normalement pas de l’espacement de la page ou d’autres paramètres.

Néanmoins, beaucoup de propriété ne sont pas définies à l’avance, mais calculées dans le moteur de rendu. C’est par exemple le cas de la largeur de beaucoup d’objets. Les barres de mesure créées avec \bar ":|.|:" sont nettement plus larges que les barres de mesure normales. C’est pourquoi la propriété X-extent des grobs BarLine n’est pas réglée à une simple paire de nombres qui conviendrait à toutes les barres de mesure.

Il existe de nombreux exemples similaires où une propriété doit dépendre d’une autre propriété. LilyPond est dotée d’une architecture flexible sur ce point. Au lieu d’avoir un ordre prédéfini pour calculer les propriétés, elle permet à toute propriété d’être calculée au moment où elle est lue. Plus précisément, si une propriété est réglée à une procédure, cette procédure est interprétée comme une fonction de rappel. La procédure est alors appelée avec le grob en argument pour déterminer la valeur de la propriété.

Ce principe élégant est au cœur du moteur de rendu, et le rend très facile à étendre. Tout le placement et le dessin des objets que fait LilyPond est effectué dans des fonctions de rappel qui calculent des propriétés comme stencil et X-extent. Dans l’abstrait, les dépendances entre ces fonctions de rappel forment un immense graphe, qui se résout pas à pas au fur et à mesure que des fonctions de rappel sont déclenchées, lisant à leur tour des propriétés qui déclenchent d’autres fonctions de rappel. Les extensions se font de la même manière, en réglant des propriétés à des fonctions de rappel, avec la commande classique \override, et elles ont accès à tout ce que à quoi les fonctions de rappel définies par LilyPond ont accès, car tout cela est stocké dans des propriétés.

Voici une fonction de rappel simple. Elle change la couleur d’une tête de note en fonction de sa position sur la portée.

\version "2.25.8"

\layout {
  \context {
    \Voice
    \override NoteHead.color =
      #(lambda (grob)
         (case (ly:grob-property grob 'staff-position)
           ((-8) "red")
           ((-7) "blue")
           ((-6) "green")
           ((-5) "orange")
           ((-4) "purple")
           ((-3) "grey")
           (else "black")))
  }
}

\relative {
  \time 3/4
  a8 b cis b a4
  b fis' b,
  c8 d e d c4
  d2.
}
Résultat../../images/c808837bc7b52c8910e3f819bf937d58a3760f2988667742fef6ecdcf41e09c4.svg

Au moment où l’objet NoteHead est affiché, ou bien si une autre fonction de rappel lit sa propriété color avec ly:grob-property, la fonction de rappel est exécutée.

Toutes les propriétés de grobs sont mises en cache, c’est à dire qu’une fois que la fonction de rappel a été exécutée une fois pour calculer la propriété, elle n’est plus jamais exécutée. LilyPond considère que la fonction de rappel a calculé la valeur de la propriété une bonne fois pour toutes, et mémorise cette valeur.

Gardez en tête que contrairement à un ly:context-property par exemple, ‘un ly:grob-property n’est pas une opération complètement anodine, mais peut déclencher une fonction de rappel, qui à son tour déclenchera d’autres fonctions de rappel en cascade. Il faut penser aux effets que ces fonctions de rappel peuvent avoir. Pour l’exemple, supposons la propriété Grob.propriété-A définie à une fonction de rappel qui lit la propriété Grob.propriété-B. Si vous réglez Grob.propriété-B à une fonction de rappel qui elle-même lit Grob.propriété-A, vous obtiendrez une erreur car LilyPond détecte une dépendance cyclique. Un autre exemple est le suicide (voir Suicide de grobs). Après un ly:grob-property, il se peut que le grob ait été supprimé.

Il peut parfois être utile, notamment à des fins de débogage, d’obtenir la valeur d’une propriété sans exécuter les fonctions de rappel. C’est possible en utilisant ly:grob-property-data au lieu de ly:grob-property. Si la valeur de la propriété a été fixée dès le départ, ou si la propriété était mise à une fonction de rappel qui a déjà été déclenchée et a calculé la valeur, ly:grob-property-data renvoie cette valeur, mais si la propriété est mise à une fonction de rappel qui n’a pas encore été déclenchée, c’est cette fonction de rappel qui sera renvoyée.

Il est peu idiomatique de modifier directement les propriétés de grob avec ly:grob-set-property!. Réfléchissez à deux fois avant de le faire, car cela peut introduire une dépendance à l’ordre d’exécution des fonctions de rappel. Supposons que la fonction de rappel A lise la propriété B, et que la fonction de rappel C modifie la propriété B. Si A se trouve être exécutée avant C, il est trop tôt pour voir la valeur de B que C lui donnera. La propriété A est donc calculée avec la mauvaise valeur de B. C’est pourquoi il vaut souvent mieux écrire une fonction de rappel pour chaque propriété que l’on veut modifier plutôt qu’une seule fonction de rappel qui modifie plusieurs propriétés à la fois.

De nombreuses fonctions de rappel prédéfinies sont associées à des interfaces. Cela se reflète dans leur nom, par exemple ly:side-position-interface::y-aligned-side. Les fonctions de rappel qui sont vraiment spécifiques à un type de grob ont dans leur nom ce type, comme ly:note-head::calc-stem-attachment.

Utilisation de grob-transformer#

La fonction grob-transformer permet d’écrire une fonction de rappel qui modifie la valeur par défaut d’une propriété. Elle s’utilise avec (grob-transformer propriété transformateur). propriété est le nom de la propriété modifiée, et transformateur est une fonction qui prend deux arguments, le grob, et la valeur par défaut.

\version "2.25.8"

sharpenBeams =
\override Beam.positions =
  #(grob-transformer 'positions
     (lambda (grob original)
       (let ((left-Y (car original))
             (right-Y (cdr original)))
         (cons
           (+ left-Y (* 2.0 (- left-Y right-Y)))
           (+ right-Y (* 2.0 (- right-Y left-Y)))))))

\relative {
  c'8[ d] e[ c] c[ g'] c'[ c,]
  \sharpenBeams
  c,8[ d] e[ c] c[ g'] c'[ c,]
}
Résultat../../images/b8059cd94695780ba63f5f2df5fbc494592622653c7871269679c7b075349d3b.svg

Pointeurs vers des grobs#

Comprendre les pointeurs vers des grobs#

Il existe un second type de propriétés de grobs, séparé des propriétés normales, auxquelles on accède avec ly:grob-object au lieu de ly:grob-property. Ces « propriétés d’objet », à ne pas confondre avec les « propriétés d’objet » de Guile qui sont quelque chose de complètement différent, servent à relier les grobs entre eux. Par exemple, une tête de note, objet NoteHead, contient dans sa « propriété d’objet » stem le grob Stem qui représente la hampe affichée à côté de la tête de note. Inversement, une hampe, objet Stem contient dans sa « propriété d’objet » note-heads un tableau des grobs NoteHead de l’accord sur lequel la hampe est accrochée, et utilise ces têtes de notes pour calculer sa longueur. Ces liens sont mis en place par les graveurs, à l’aide de la fonction ly:grob-set-object!.

Comme expliqué dans Clonage des grobs pour suivre les sauts de ligne, après que les sauts de ligne sont calculés, de nombreux grobs sont clonés en plusieurs grobs correspondant à des systèmes différents. Lorsque ce processus est terminé, il reste à rendre les systèmes indépendants. C’est la raison pour laquelle les propriétés « d’objet » sont séparées des propriétés normales. Toutes les propriétés susceptibles de contenir des grobs sont des propriétés d’objet. Elles sont modifiées lors d’un processus appelé « substitution post-séparation-des-systèmes » (break substitution). Concrètement, si vous accédez à la propriété d’objet staff-symbol d’un objet après la séparation des systèmes, vous obtiendrez un StaffSymbol qui ne s’étend que sur le système de l’objet, alors que si vous le faites avant la séparation des systèmes, vous obtenez un long StaffSymbol qui s’étend sur toute la pièce, et qui est voué à être séparé en plusieurs clones, un pour chaque système, lors de la séparation des systèmes.

Dans la référence des propriétés internes, les propriétés d’objet se trouvent dans la section Internal backend properties. Attention cette section contient aussi des propriétés normales !

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

Renvoie/modifie la propriété d’objet property dans grob.

ly:grob-object renvoie default si la propriété d’objet n’est pas défini, ou bien la liste vide si default n’est pas fourni.

Les propriétés d’objet n’ont jamais de valeur par défaut (elles contiennent d’autres grobs, qui ne sont pas créés avant d’analyser la partition, donc on ne peut logiquement pas leur donner de valeurs par défaut à l’avance). C’est pourquoi elles n’apparaissent jamais sur la page d’un grob dans la référence des propriétés internes, mais seulement sur les pages de ses interfaces.

Dans l’exemple suivant, une fonction de rappel détecte si une tête de note possède une altération accidentelle, et la colorie dans ce cas en rouge, pour rappeler l’altération à l’interprète.

\version "2.25.8"

\relative {
  \override Accidental.color = "red"
  \override NoteHead.color =
    #(lambda (grob)
       (if (ly:grob-object grob 'accidental-grob #f)
           "red"
           "black"))
  c'16 d e fis g fis e cis e2
}
Résultat../../images/18fec2cd1323d7aab711620ab5d86982faeafb87986e786b4b2a5d5eeb9afd69.svg

Tableaux de grobs#

Beaucoup de ces « propriétés d’objet » sont des tableaux de grobs. Pour des raisons techniques ayant à voir avec le noyau C++ de LilyPond, ces propriétés ne sont pas des tableaux du type de tableaux fourni par Guile, mais d’un type spécialement dédié aux tableaux de grobs. Même s’il existe des interfaces Scheme pour manipuler les tableaux de ce type, le moyen le plus simple reste de convertir un tel tableau en une liste, et si besoin convertir une liste modifiée en tableau. (Renseignez-vous sur Internet pour connaître la différence entre listes et tableaux en informatique.)

(ly:grob-array-length grob-array)

Renvoie la longueur du tableau.

(ly:grob-array-ref grob-array index)

Accède à l’élément à l’indice index dans le tableau grob-array. Les indices commencent à zéro.

(ly:grob-array->list grob-array)

Convertit le tableau grob-array en une liste.

(ly:grob-list->grob-array lst)

Convertit la liste Scheme de grobs lst en un tableau de grobs.

Dans cet exemple, les hampes sont dessinées dans un style inhabituel où elles ne recouvrent que l’étendue des notes de l’accord auquel elles sont attachées, à l’aide du tableau de grobs note-heads.

\version "2.25.8"

{
  <c' e' g' c'' g'' c'''>
  \override Stem.length =
    #(lambda (grob)
       (let* ((note-heads (ly:grob-array->list (ly:grob-object grob 'note-heads)))
              (head-positions (map (lambda (head)
                                     (ly:grob-property head 'staff-position))
                                   note-heads)))
         (- (apply max head-positions)
            (apply min head-positions))))
  \override Stem.thickness = 4
  q
}
Résultat../../images/c4064d63cf03a42bcf190909bc675075244104c8fd8fff83be7f5a2dd23e6846.svg

Chaîne de causes#

La propriété cause d’un grob contient un événement, ou un autre grob, ou la liste vide. Elle est réglée au moment où le grob est créé avec ly:engraver-make-grob.

Cette propriété peut être utilisée pendant l’étape de traduction comme dans le moteur de rendu. C’est notamment grâce à elle que fonctionne le « point-and-click ». Dans certains éditeurs comme Frescobaldi, cliquer sur un objet dans le PDF déplace le curseur dans la source vers l’endroit du code où l’objet a été créé. En interne, si la propriété cause d’un grob est un événement (mais pas si c’est un autre grob), LilyPond ajoute un lien hypertexte du grob vers l’emplacement de sa cause (ou plutôt de l’expression musicale dont elle provient) dans le code source.

(event-cause grob)

Retrace l’événement qui a causé grob

Si la cause de grob est un événement, il est renvoyé. Sinon, si c’est un grob, la cause de ce grob est lue, et si c’est un événement, il est renvoyé, sinon, etc., jusqu’à trouver un événement.

Suicide de grobs#

Il est possible, dans une fonction de rappel, de supprimer un grob. Voici quelques exemples où cela est utile :

  • Lorsque la dernière note d’un soufflet est la première note sur son système, la dernière partie du soufflet, celle qui est sur ce système, est supprimée par défaut (voir aussi Propriétés factices).

  • Les accolades de début de système sont supprimées si elles sont plus petites que leur propriété collapse-height.

  • En général, un grob peut être supprimé si une erreur ou anomalie est détectée (après avoir émis un message avec ly:programming-error) et qu’il n’y a rien d’utile à faire du grob.

(ly:grob-suicide! grob)

Supprime grob.

Lorsque cette fonction est appelée, le grob perd toutes ses propriétés. Tout appel à ly:grob-property renvoie la valeur par défaut donnée comme troisième argument, ou '() si cet argument n’est pas fourni. Étant donné que les fonctions de rappel sont stockées dans les propriétés, cela signifie aussi que plus aucune fonction de rappel de cet objet ne s’exécutera.

(grob::is-live? grob)

Renvoie un booléen qui est #t si le grob a été supprimé avec ly:grob-suicide!.

Sortes de grobs#

Les grobs sont divisés en plusieurs sortes, qui se distinguent par le rôle joué dans l’espacement, ainsi que l’interaction avec les sauts de ligne. En C++, cela se traduit par deux sous-classes de la classe Grob, qui sont Item, pour les grobs ponctuels (« items »), et Spanner, pour les grobs étendus (« spanners »). Les grobs ponctuels sont fixés à une position horizontale précise. On peut citer les têtes de notes, les hampes, les barres de mesure, … Les grobs étendus vont d’une position horizontale à une autre. Ils possèdent deux extrémités (« bounds »), qui sont des grobs ponctuels. Leur parent sur l’axe X est le plus souvent leur extrémité gauche. Quelques exemples de grobs étendus sont les ligatures, les soufflets, ou encore les liaisons. La sorte d’un grob se reconnaît à ses interfaces. Les grobs ponctuels possèdent la item-interface, alors que les grobs étendus possèdent la spanner-interface.

(ly:spanner-bound spanner direction)
(ly:spanner-set-bound! spanner direction item)

Renvoie/modifie l’extrémité de l’objet étendu spanner dans la direction donnée par direction (LEFT = gauche, RIGHT = droite).

Les colonnes sont un sous-type des grobs ponctuels. Ce sont des grobs abstraits, invisibles, qui regroupent les grobs ponctuels sur une même position horizontale. L’espacement horizontal à l’intérieur d’une colonne est normalement facile à calculer ; ainsi, « calculer l’espacement » signifie essentiellement déterminer l’espacement entre les colonnes. La partition se construit sur une alternance de colonnes dits « musicales », de type PaperColumn, et « non-musicales », de type NonMusicalPaperColumn. À chaque moment, il y a une colonne musicale et une non-musicale. Les colonnes musicales contiennent les grobs ponctuels liés directement aux notes, comme les têtes de note, les hampes, ou les articulations, qui sont appelés des grobs ponctuels musicaux. Quant aux colonnes non-musicales, on y retrouve logiquement les grobs ponctuels non-musicaux, qui comprennent les clefs, les métriques, les armures, ou encore les barres de mesure.

Clonage des grobs pour suivre les sauts de ligne#

La présence de sauts de ligne nécessite souvent d’afficher plusieurs fois le même grob. C’est par exemple le cas des clefs :

\version "2.25.8"

\paper {
  ragged-right = ##t
}

{
  c'1
  \break
  \clef bass
  c1
}
Résultat../../images/a4ee8b9ff75395341c29661fc5a87445cceab4243714f26113f971114f13b81a.svg

et également des soufflets :

\version "2.25.8"

\paper {
  ragged-right = ##t
}

\relative {
  c'\< d e g
  \break
  c g e f
  \break
  g-- a-- g2->\!
}
Résultat../../images/3264cf3f412d5ec12d94de2042f81c2749a5ba144736445ccc75e18ee68af63b.svg

LilyPond le fait en créant plusieurs clones de ces grobs, qui héritent au départ des propriétés de l’original, mais sont indépendants des autres clones.

La manière de réagir aux sauts de ligne est l’un des points qui distinguent grobs ponctuels et étendus.

Les grobs étendus peuvent être arbitrairement longs. Ils sont divisés en autant de clones qu’il n’en faut pour avoir un clone par système présent entre le début et la fin du grob. Les extrémités des clones sont ajustées : si un saut de ligne divise un grob, les extrémités de chacun des deux morceaux à cet endroit deviennent la colonne non-musicale sur laquelle le saut se fait. Comme il y a beaucoup de configurations possibles pour les sauts de ligne, donc beaucoup de manières de diviser un grob étendu, la division n’est faite qu’après le calcul final des sauts de ligne.

Concernant les grobs ponctuels, la manière dont ils réagissent aux sauts de lignes varie entre grobs ponctuels musicaux et non-musicaux. Ceux qui sont musicaux ne sont jamais clonés – on imagine mal une tête de note se diviser en plusieurs versions sur différents systèmes. A contrario, les grobs ponctuels non-musicaux sont clonés, ce qui leur vaut le nom alternatif de grobs ponctuels divisibles (« breakable »). Chacun se divise en exactement trois grobs, l’original et deux clones. Ces trois grobs sont caractérisés par une « direction de division », qui vaut LEFT en fin de système, CENTER en milieu de système, et RIGHT en début de système. On peut s’imaginer l’original au milieu de sa colonne divisible (NonMusicalPaperColumn), et deux clones autour de lui, à gauche et à droite. Si un saut de ligne tombe sur cette colonne, l’original au milieu disparaît et les deux clones sont placés l’un à la fin du système, l’autre au début du système suivant.

../_images/item-breaking.svg

Même lorsqu’un grob ponctuel est cloné, tous les clones ne sont pas forcément visibles. Un contre-exemple typique est celui des numéros de mesure, qui ne sont normalement visibles qu’en début de système. Ce comportement est contrôlé par la propriété break-visibility. Elle se présente sous la forme d’un vecteur de trois booléens, qui correspondent aux trois directions de division LEFT, CENTER et RIGHT. Si la composante de break-visibility pour la direction de division d’un grob ponctuel vaut #f, ce grob est supprimé (voir Suicide de grobs).

(ly:grob-original grob)

Renvoie l’original de grob. Pour un grob ponctuel, c’est la version du grob dont la direction est CENTER. Pour un grob étendu, c’est le grob d’origine avant la séparation.

(ly:item-break-dir item)

Renvoie la direction du grob ponctuel item.

(ly:spanner-broken-into spanner)

Renvoie la liste des clones du grob étendu spanner, s’il est séparé. Pour obtenir les autres clones d’un clone, utilisez cette fonction sur l’original.

Notez qu’il n’y a pas d’interface pour obtenir les clones d’un grob ponctuel.

TODO : unbroken-or-last-broken-spanner?, etc.

Dessin des objets#

Le tracé d’un objet est représenté par une valeur de type « dessin », en anglais stencil. Un markup est une sorte de programme qui renvoie un dessin.

Dessins (stencils)#

Le dessin de chaque objet est contenu dans sa propriété stencil. En interne, la représentation des dessins est proche du code émis en PostScript ou SVG.

Il y a deux dessins spéciaux, empty-stencil et point-stencil. Le premier, comme son nom l’indique, est un dessin complètement vide. Ses étendues sur les deux axes sont l’intervalle vide empty-interval. Il ne prend normalement aucune place. point-stencil est un stencil dont les étendues sur chaque axe sont '(0.0 . 0.0), un intervalle à un seul point. Ce dessin peut, contrairement à empty-stencil, prendre de la place.

Les deux ont des cas d’utilisation différents. empty-stencil est la seule manière de supprimer complètement l’espace que pourrait prendre un objet. Cela est souvent souhaitable, mais pas toujours. Par exemple, les liaisons de tenue ne fonctionnent pas sur les têtes de note dont le dessin est mis à empty-stencil (et causent un crash, voir le bug 6040).

{
  \omit NoteHead
  e'1~ e'1
}

Au lieu de #f (valeur à laquelle \omit règle le stencil, équivalente à empty-stencil), la tête de note doit avoir son stencil réglé à point-stencil pour que la liaison ait un endroit où s’attacher.

\version "2.25.8"

{
  \override NoteHead.stencil = #point-stencil
  e'1~ e'1
}
Résultat../../images/1e1e3a95597cc15f0359d97ec79b0b7735cf113216af49bb63d3653ecade3846.svg

Il existe de nombreuses fonctions liées aux dessins, surtout utiles dans les fonctions de rappel pour la propriété stencil. Toutes sont documentées dans la page Scheme functions de la référence des propriétés internes (rechercher les fonctions nommées ly:stencil-... ou stencil-...). Voici les plus importantes.

(ly:stencil-add stencil1 stencil2 ...)

Forme un dessin composite qui combine plusieurs dessins.

(ly:stencil-translate-axis stencil amount axis)

Renvoie une copie du dessin, décalée de amount sur l’axe axis.

(ly:stencil-extent stencil axis)

Renvoie l’étendue de stencil sur l’axe axis.

(ly:stencil-aligned-to stencil axis side)

Décale stencil selon l’axe axis pour l’aligner sur la direction side.

Par exemple, pour axis = X et side = LEFT, si le stencil d’origine a l’étendue horizontale '(-2 . 3), le résultat a l’étendue horizontale '(0 . 5).

side n’est pas forcément -1 ou 1, cette fonction fait une interpolation. Par exemple, 0 recentre le dessin.

(ly:stencil-outline stencil outline)

Renvoie stencil avec des étendues et un contour pris depuis le dessin outline.

Les étendues de dessin sont utilisées pour la plupart des étendues de grob. Le contour est utilisé par les lignes d’horizon pour éviter les collisions. Vous pouvez activer la propriété show-horizontal-skylines ou show-vertical-skylines d’un objet pour visualiser ses lignes d’horizon.

{
  \override Score.PaperColumn.show-horizontal-skylines = ##t
  c'^"Hi"
}
(make-line-stencil width start-X start-Y end-X end-Y)

Renvoie un dessin de ligne entre les points (start-X, start-Y) et (end-X, end-Y) avec l’épaisseur width.

(make-filled-box-stencil X-extent Y-extent)

Renvoie un dessin de rectangle avec les étendues X-extent et Y-extent.

(make-circle-stencil radius thickness filled)

Renvoie un dessin de cercle de rayon radius et d’épaisseur thickness. Si filled vaut #t, le cercle est rempli à l’intérieur (c’est donc un disque).

Markups#

Les markups sont la plupart du temps construits avec la syntaxe \markup :

\markup \bold \center-column { Arranged by }

Il existe une deuxième possibilité parfois utile. À chaque commande de markups \qqchose correspond une fonction Scheme make-qqchose-markup. Une deuxième façon d’écrire l’exemple ci-dessus est donc :

\markup #(make-bold-markup (make-center-column-markup '("Arranged" "by")))

Le prédicat qui est si une valeur est un markup se nomme markup?. Il renvoie aussi #t sur les chaînes de caractères, qui sont la forme la plus simple que peuvent prendre les markups.

La commande pour markups \stencil prend un dessin en argument et renvoie un \markup qui s’évalue toujours en ce dessin.

\markup \stencil #(make-oval-stencil 4 3 0.1 #t)

Les markups sont différents des dessins. On peut voir un markup comme une sorte de programme qui construit un dessin. L’acte d‘« exécuter » le markup pour en faire un dessin est le processus d‘« interprétation » du markup. L’interprétation demande deux ingrédients :

  • Une définition de sortie,

  • Des propriétés.

C’est la fonction interpret-markup qui se charge d’interpréter un markup. Elle prend comme arguments la définition de sortie, les propriétés, et le markup, et renvoie un dessin.

La chaîne vide "" est toujours interprétée à empty-stencil. Vous pouvez aussi utiliser l’alias empty-markup. Il n’existe pas point-markup, mais avec (make-null-markup), vous obtenez un markup qui s’interprète à point-stencil.

On peut définir de nouvelles commandes pour markups à l’aide de la macro define-markup-command. La manière de l’utiliser ressemble à define-music-function, sauf que le nom de la commande est à l’intérieur même du define-markup-command (pas de commande = #(define-markup-command ...)), et il y a aussi deux paramètres en plus, layout (définition de sortie) et props (propriétés). Une commande pour markups doit obligatoirement renvoyer un dessin.

\version "2.25.8"

#(define-markup-command (strong-emphasis layout props arg) (markup?)
   (interpret-markup layout props
     (make-bold-markup
       (make-italic-markup
         (make-underline-markup
           arg)))))

\markup \strong-emphasis "Very important!"
Résultat../../images/ef431ed36d265edd688b28d04888214a5745f03fb38046e3901e9881fae41d75.svg

Le mot-clé #:properties est un moyen pratique de lire des propriétés se trouvant dans l’argument props. Voici un exemple tiré de la documentation officielle :

\version "2.25.8"

#(define-markup-command (double-box layout props text) (markup?)
  #:properties ((inter-box-padding 0.4)
                (box-padding 0.6))
  "Draw a double box around text."
  (interpret-markup layout props
    (markup #:override `(box-padding . ,inter-box-padding) #:box
            #:override `(box-padding . ,box-padding) #:box text)))

\markup \override #'(inter-box-padding . 0.2) \double-box "Erik Satie"
Résultat../../images/a26ac54c19043feabc1b5099b6b98b3c8db5f091165b642b25b8e0fd98ac3163.svg

La fonction grob-interpret-markup prend un grob et un markup. Elle interprète le markup avec la définition de sortie et les propriétés du grob. Cette fonction permet de comprendre pourquoi les markups existent en tant que « programmes qui dessinent ». Le même markup, interprété par deux grobs différents, peut produire des dessins différents, en fonction des propriétés. Voici un code qui fonctionne :

\version "2.25.8"

{
  \override TextScript.thickness = 10
  c'^\markup \draw-line #'(10 . 10)
}
Résultat../../images/5746607adae6f8562e3d999a22504a655cc84961a60bd9cc25a2faed1314d9c7.svg

Cela peut sembler surprenant, puisque TextScript n’a pas d’interface pour la propriété thickness. Mais puisque ses propriétés sont utilisées pour interpréter le markup, \draw-line est bien influencée par la propriété.

Voici un exemple où grob-interpret-markup est utilisée dans une fonction de rappel pour la propriété stencil afin d’afficher une métrique avec un slash.

\version "2.25.8"

#(define (slashed-time-signature-stencil grob)
   (let* ((fraction (ly:grob-property grob 'fraction))
          (num (number->string (car fraction)))
          (den (number->string (cdr fraction))))
     (ly:stencil-aligned-to
       (grob-interpret-markup grob
         (make-line-markup (list num "/" den)))
       Y
       CENTER)))

\layout {
  \context {
    \Staff
    \override TimeSignature.stencil = #slashed-time-signature-stencil
  }
}

{ c'1 }
Résultat../../images/67a09983066a4466fa30dadcfe9c6e7cd755fda159a01d80bf1e6cf64ca67dc1.svg

Il est fréquent pour les fonctions de rappel pour la propriété stencil d’interpréter un markup. La fonction de rappel prédéfinie ly:text-interface::print interprète le markup contenu dans la propriété text du grob. De nombreux grobs l’utilisent pour leur propriété stencil. C’est aussi un moyen pratique de changer le dessin d’un grob en utilisant un markup.

\version "2.25.8"

{
  \tweak stencil #ly:text-interface::print
  \tweak text \markup \circle G
  \tweak thickness 2
  g'1
}
Résultat../../images/521ecf0dc20cafa1a39e68e83fede4ab6ed36a57898cd5ab983749c04f4aa74e.svg

Dans l’exemple ci-dessus, remarquer que la propriété thickness affecte la tête de note, même si la référence des propriétés internes ne dit à aucun endroit que NoteHead lit la propriété thickness.

Considérations d’espacement#

Directions et axes#

Certaines fonctions ont des paramètres qui représentent un axe, horizontal ou vertical. Par convention, 0 est l’axe horizontal et 1 l’axe vertical. Pour rendre le code plus compréhensible, les constantes X et Y sont définies respectivement à 0 et 1.

Beaucoup de propriétés représentent une direction, haut ou bas, gauche ou droite, parfois milieu. Dans la logique de LilyPond, une « direction » ne dépend pas de l’axe. Il y a exactement trois directions possibles, qui sont -1, 0 et 1.

  • -1 : gauche, bas, début

  • 0 : milieu

  • 1 : droite, haut, fin

Le prédicat ly:dir? vérifie si son argument est une direction.

Les constantes liées aux directions sont LEFT, RIGHT, DOWN, UP et CENTER.

Notez que certaines propriétés, bien qu’elles soient au départ des directions, ne sont pas restreintes à ces trois valeurs, mais interpolent lorsqu’elles reçoivent des valeurs à mi-chemin entre deux directions. C’est notamment le cas de self-alignment-X.

Intervalles#

Un intervalle représente une étendue. Par exemple, l’étendue des grobs est donnée comme un intervalle. En Scheme, les intervalles sont des paires de nombres.

L’intervalle vide est défini comme (+inf.0 . -inf.0), qui peut aussi s’écrire avec la constante empty-interval. Attention, ce n’est pas la même chose qu’un intervalle à un seul point, comme (0 . 0) !

Les fonctions auxiliaires suivantes manipulent des intervalles.

(interval-start interval)
(interval-end interval)

Alias de car et cdr, pour rendre le code plus lisible.

(interval-bound interval direction)

Renvoie le car ou cdr de interval, en fonction de direction (car = début = LEFT/DOWN, cdr = fin = RIGHT/UP).

(interval-empty? interval)

Renvoie vrai si l’intervalle est vide. L’intervalle vide usuel empty-interval est vide, mais aussi l’intervalle (1 . -1) par exemple.

(interval-length interval)

Renvoie la longueur de l’intervalle.

(interval-center interval)

Renvoie le point au milieu de l’intervalle.

(interval-index interval position)

Interpole entre le début et la fin de l’intervalle selon position : -1 pour le début, 1 pour la fin, 0 pour le milieu, et tout autre nombre pour les valeurs intermédiaires.

(interval-sane? interval)

Vérifie si cet intervalle ne contient ni valeurs infinies ni valeurs non-numériques, et a ses extrémités dans le bon ordre (carcdr).

Les fonctions auxiliaires suivantes construisent de nouveaux intervalles :

(interval-scale interval factor)

Multiplie le début et la fin de interval par factor.

(interval-widen interval amount)

Renvoie l’intervalle avec une longueur de amount de plus de chaque côté.

(interval-union interval1 interval2)

Renvoie l’union des deux intervalles.

(interval-intersection interval1 interval2)

Renvoie la partie commune aux deux intervalles.

(reverse-interval interval)

Inverse le début et la fin d’un intervalle.

(add-point interval point)

Agrandit interval d’un côté si nécessaire pour qu’il inclue point.

(symmetric-interval x)

Renvoie l’intervalle de -x à x.

(ordered-cons a b)

Renvoie l’intervalle avec les extrémités a et b. Le plus petit des deux est choisi pour l’extrémité gauche.

Coordonnées, étendues et points de référence#

Chaque grob a un parent sur chaque axe. Les fonctions liées aux parents sont :

(ly:grob-parent grob axis)
(ly:grob-set-parent! grob axis)

Renvoie ou définit le parent de grob sur l’axe axis (X ou Y).

Les propriétés X-offset et Y-offset d’un grob contiennent son décalage par rapport à son parent sur chaque axe. X-extent et Y-extent sont ses étendues horizontale et verticale, par rapport à lui-même.

Étant donné que toutes les coordonnées sont relatives, il faut, pour calculer l’écart entre deux grobs X et Y, choisir un point de référence Z tel qu’il soit possible de calculer la coordonnée de X par rapport à Z et celle de Y par rapport à Z. Concrètement, Z est un ancêtre commun de X et Y. Les fonctions suivantes servent à rechercher des points de référence et à calculer une coordonnée par rapport à un point de référence.

(ly:grob-common-refpoint grob1 grob2 axis)

Renvoie un point de référence commun pour grob1 et grob2 sur l’axe axis, ou #f s’il n’y en a pas.

Un point de référence commun est un grob qui est un ancêtre des deux grobs.

(ly:grob-relative-coordinate grob refpoint axis)
(ly:grob-extent grob refpoint axis)

Calcule la coordonnée/l’étendue de grob sur l’axe axis par rapport à refpoint, qui doit obligatoirement être un point de référence commun.

En général, il faut prendre des précautions avec les coordonnées sur l’axe vertical. Elles sont calculées relativement tardivement. Les demander trop tôt risque de provoquer des dépendances cycliques. Le sujet délicat du positionnement sur l’axe vertical est abordé dans la section Unpure-pure containers.

À l’inverse, les choses sont plus simples sur l’axe horizontal, où il est rarement problématique de demander l’étendue d’un objet ponctuel, ou l’étendue d’un objet étendu après la séparation des systèmes (mais, logiquement, un objet étendu ne peut normalement pas calculer son étendue horizontale avant la séparation des systèmes).

Unités de mesure#

Les deux paramètres principaux liés à la taille de la portée sont l’espace de portée (espace entre deux lignes) et l’épaisseur des lignes de la portée. Les distances sont souvent exprimées en espaces de portée afin qu’il soit possible de leur donner des valeurs par défaut qui fonctionnent pour toutes les tailles de portées. Les épaisseurs de ligne sont en général exprimées comme multiples de l’épaisseur des lignes de la portée. La valeur de l’espace de portée comme de l’épaisseur de portée sont des propriétés du StaffSymbol. On peut les régler séparément.

La portée à laquelle appartient un grob (si cela a du sens pour ce grob) se trouve dans sa « propriété d’objet » staff-symbol.

(ly:staff-symbol-staff-space staff-symbol)
(ly:staff-symbol-line-thickness staff-symbol)

Ces fonctions donnent les unités que l’on est amené à manipuler lorsque l’on utilise des propriétés d’un grob qui sont exprimées en espaces de portée ou épaisseurs de portée. Elles accèdent au staff-symbol et lisent sa propriété staff-space/line-thickness, et multiplient cette valeur par un facteur qui se trouve dans la définition de sortie.

Propriétés factices#

Il y a une poignée de propriétés qui n’ont pas de valeur intéressantes. Ce sont des propriétés factices, qui sont utilisées seulement pour déclencher une fonction de rappel à un point particulier du processus de placement. On les utilise en les réglant à une fonction de rappel qui a des effets de bord. Ce sont :

  • before-line-breaking et after-line-breaking, appelées respectivement avant et après la séparation des systèmes. Une valeur courante pour after-line-breaking est ly:spanner::kill-zero-spanned-time, qui supprime un clone, par exemple d’un soufflet, s’il est le dernier clone et arrive sur la première note du système.

    \version "2.25.8"
    
    {
      \override Hairpin.to-barline = ##f
      c'1\< \break c'1\! \break
      \once \override Hairpin.after-line-breaking = #'()
      c'1\< \break c'1\!
    }
    
    Résultat../../images/19fa88f08fe3207224927b6b3cf74a0785e204c394780b841df7273ce89b58cf.svg
  • positioning-done, appelée pour déclencher l’espacement de certains objets. Par exemple, ly:stem::calc-positioning-done déplace certaines des têtes de notes lorsqu’un accord comporte des intervalles de seconde, pour éviter les collisions. Chaque tête de note lit la propriété positioning-done de son objet Stem associé lorsqu’elle a besoin de connaître son placement. Comme les fonctions de rappel ne sont jamais appelées plus d’une fois, le positionnement n’est fait qu’une seule fois.

  • springs-and-rods, appelée juste après before-line-breaking. Son but est d’ajouter des contraintes d’espacement horizontal entre les colonnes, flexibles (les springs, « ressorts ») ou fixes (les rods, « tiges »).

Unpure-pure containers#

XXX to be rewritten when I have done my wholesale restructuring of purity.

TODO : add examples. TODO : explain why pure functions must be pure (to avoid dependency on callback order).

The problem#

Unpure-pure containers are meant to address a thorny issue. Observe this input, with kneed beams between staves :

<<
  \new Staff = "up" \relative c'' {
    a8 g f e d c \change Staff = "down" b a
    \change Staff = "up" c' b a g c c \change Staff = "down" c,, c
  }
  \new Staff = "down" {
    \clef bass
    s1
    s1
  }
>>

To determine how many systems can be placed on the page, the page breaker would like to know how tall the system is. This depends on whether the beams are kneed or not – if they are, more space should be allocated for the beam.

On the other hand, the page breaker may go for a tightly packed configuration, perhaps because a small number of systems was requested by the user through systems-per-page. In this case, beams should rather not be kneed, because kneed beams take up more space.

Similar trouble is run into with text scripts.

filler = \relative c' { c8 e g c g c g e }

\relative c' {
  \repeat unfold 4 \filler
  c4^\markup "Crescendo poco a poco" d e f
  \mark "Tempo primo"
  g^"Rallentando ma non troppo" f e d
  \repeat unfold 20 \filler
}

In this example, observe how the musical indications avoid each other on the second system, which is tall.

The page breaker could have chosen this configuration instead :

filler = \relative c' { c8 e g c g c g e }

\relative c' {
  \repeat unfold 4 \filler
  c4^\markup "Crescendo poco a poco" d e f
  \break
  \mark "Tempo primo"
  g^"Rallentando ma non troppo" f e d
  \repeat unfold 20 \filler
}

Because the break separates the two measures with colliding text scripts, the second system is now smaller, while the first system got slightly taller. This may in turn influence page breaking decisions : a page break could be inserted between the first and the second system (assuming more music before) to space pages more evenly.

Both of these examples show how a cyclic dependency can occur between calculating the spacing inside a system and choosing breaks.

Page and line breaking rely on scoring different configurations, attributing them penalties on criteria such as cramped spacing. It is infeasible, however, to score all possible configurations – that would make the compilation gigantically slower. Other classic constraint optimization techniques are not workable, either. There simply are too many parameters to take into account.

This is why LilyPond has heuristic algorithms instead. The goal of pure properties is to provide estimates of certain spacing parameters before line breaking in order to help the breaking algorithms make informed decisions. Afterwards, when one configuration is finally settled on, the properties are computed for real.

The art of writing pure callbacks is to make the best estimates possible while ensuring no cyclic dependencies, including corner cases. This is very difficult.

It has to be noted that purity is concerned with all Y axis positioning. There is no concept of pure X-offset or X-extent because, by design, positioning constraints on the X axis are determined in advance (springs and rods). The operation of page breakers is to try a spacing that seems sensible on the X axis and see how it looks like on the Y axis using pure properties.

Writing an unpure-pure container#

To begin with, an unpure-pure container is only needed if the callback needs different code paths for pure and unpure positioning. For instance, many stencil callbacks do not depend on line breaking, such as the one for NoteHead. Consequently, there is no unpure-pure container involved even though the callback is triggered before line breaking. When this differentiation is required, the callback is replaced with an unpure-pure container containing two callbacks.

(ly:make-unpure-pure-container unpure-function pure-function)

Construct an unpure-pure container.

unpure-function is a regular callback.

pure-function takes three arguments : the grob, a start column, and an end column. It is meant to estimate the property for a potential broken part of the grob between start and end. For items, these parameters are mandatory, but they are not meaningful : an item should only check that it is between these columns. A spanner should try to make use of the start and end parameters for better estimates.

In a more advanced form, unpure-function can take n + 1 arguments, the grob and other arguments. In this case, pure-function takes n + 3 arguments, the grob, start and end column, and the other arguments. The property can then be read using ly:unpure-call or ly:pure-call, passing these arguments. They may be used for estimates as well.

(ly:unpure-pure-container-unpure-part container)
(ly:unpure-pure-container-pure-part container)

Extract either part of container.

(ly:unpure-call data grob . rest)
(ly:pure-call data grob start end . rest)

Call a procedure with arguments. If the data is an unpure-pure container, extract its unpure or pure part to get the procedure. If data is already a procedure, use it directly.

Further reading on purity#