Introduction#

Insertion de code Scheme dans un fichier LilyPond#

Lorsque LilyPond rencontre un caractère # (croisillon), le code qui suit immédiatement est interprété comme une expression Scheme, qui est évaluée.

\version "2.25.8"

maVariable = #(+ 2 2) % maVariable prend la valeur 2 + 2 = 4

\repeat unfold #maVariable { c } % répète la note 4 fois
Résultat../../images/7ce329e98a33f8ad7d8c50bac97200aed69e3e41325a9f8a73fe7b86eb94e6bc.svg

L’exemple ci-dessus montre aussi que les variables définies avec la syntaxe de LilyPond (nom = valeur) sont accessibles depuis le code Scheme. L’inverse fonctionne également :

\version "2.25.8"

#(define maDeuxièmeVariable 42)

\repeat unfold \maDeuxièmeVariable { c }
Résultat../../images/2b81a3949a1fa30162454cb5cf7b884bc055caf1c1df94a76e83ea8f2948c580.svg

Il est également possible d’insérer plusieurs valeurs dans le contexte LilyPond à partir d’une seule expression Scheme, avec l’opérateur #@, qui renvoie toutes les valeurs contenues dans une liste.

À l’intérieur du code Scheme, on peut même repasser en syntaxe LilyPond pour écrire une valeur, en mettant le code LilyPond dans la construction #{ ... #}.

Voici un exemple qui utilise à la fois #@ et #{ ... #} :

\version "2.25.8"

notes = #(list #{ c'4 #}
               #{ e'4 #}
               #{ g'4 #}
               #{ c''4 #})

{
  % Insère toutes les notes dans cette expression séquentielle { ... }
  #@notes
  % Insère toutes les notes dans une expression parallèle << ... >>
  << #@notes >>
}
Résultat../../images/c4ccfb1c97f86813d3310b373dd007a00b2368d15d423c7feededf73f787d576.svg

Fonctions exportées, conventions de nommage#

À l’intérieur de LilyPond, il y a deux catégories de fonctions Scheme. D’une part, Guile définit de nombreuses fonctions de base du langage Scheme pour traiter les chaînes de caractères, les listes, etc. D’autre part, LilyPond a son propre jeu de fonctions qui manipulent les types d’objets qu’elle définit, comme les expressions musicales, les contextes, les markups, etc.

Certaines des fonctions définies par LilyPond ont un nom qui commence par « ly: », par exemple : ly:music-length. En particulier, toutes les fonctions définies en C++ commencent par ly:. C’est aussi le cas pour certaines des fonctions définies en Scheme, mais malheureusement pas toutes. Espérons qu’un jour LilyPond deviendra plus cohérente sur ce point.

Le bac à sable Scheme#

Pour faire des essais avec Scheme, vous pouvez exécuter cette commande dans un terminal :

lilypond scheme-sandbox

Ceci démarre une session interactive de l’interpréteur Guile où les fonctions définies par LilyPond sont disponibles.

Introduction aux fonctions musicales#

Les fonctions musicales, comme \relative, \transpose ou encore \bar, font partie des briques de base du code LilyPond. L’utilisateur a la possibilité de définir ses propres fonctions musicales. Elles sont déclarées avec define-music-function. Pour chaque argument, il faut choisir un type, donné par un prédicat.

\version "2.25.8"

accompagnementSimple =
#(define-music-function (note1 note2 note3)
                        (ly:music? ly:music? ly:music?)
   #{
      \repeat unfold 2 {
        #note1 #note2 #note3 #note2 #note3 #note2
      }
   #})

{
  \time 6/4
  \accompagnementSimple c'8 g' e''
  \accompagnementSimple c'8 g' f''
  \accompagnementSimple d'8 g' f''
  \accompagnementSimple c'8 g' e''
}
Résultat../../images/82fc0ee691f1133cbb40a5d5f862a062fda1ae28505087d7c6954f6ebf12a534.svg

Voici les prédicats de type les plus courants :

Nom du prédicat

Valeurs acceptées

number?

N’importe quel nombre, que ce soit un entier, un nombre à virgule flottante ou encore une fraction

integer?

Un entier

index?

Entier positif

string?

Chaîne de caractères (suite de caractères, comme "abcd")

markup?

Un bloc \markup ; les chaînes de caractères sont également acceptées

boolean ?

Un booléen, #t (vrai, true) ou #f (faux, false)

ly:music?

Une expression musicale

ly:pitch?

Une hauteur (comme les deux premiers arguments de \transpose dans \transpose c des { ... })

ly:duration?

Une durée (comme l’argument facultatif de \tuplet : \tuplet 3/2 8. { ... })

color?

Une couleur, donnée par chaîne de caractères ou par ses composantes RGB ([documentation](notation:Coloring objects))

list?

Une liste

Introduction aux fonctions de rappel#

Les éléments de la notation musicale, comme les têtes de notes, hampes, silences, etc., sont représentés par des objets appelés « grobs » (pour « objet graphique », « graphical object » en anglais). Les grobs communs sont familiers des LilyPondeurs, qui connaissent la syntaxe \override pour changer leurs propriétés.

Au lieu de définir une propriété à une valeur fixée, on peut la définir à une fonction, que l’on appelle alors « fonction de rappel », et qui calcule la valeur de la propriété. La fonction prend le grob en argument. Elle peut accéder à d’autres propriétés du grob à l’aide de ly:grob-property. Voici une fonction qui dessine des ligatures en soufflet en fonction de leur sens : vers la droite pour une ligature qui monte et vers la gauche pour une ligature qui descend. Le résultat est que la partition indique d’accélérer dès que l’on monte – ce qu’un professeur de musique n’apprécierait sans doute pas.

\version "2.25.8"

#(define (grow-in-up-direction beam)
   (let* ((Y-positions (ly:grob-property beam 'positions))
          (left-position (car Y-positions))
          (right-position (cdr Y-positions)))
     (cond
       ((< left-position right-position)
        RIGHT)
       ((> left-position right-position)
        LEFT)
       (else
        CENTER))))

\relative c' {
  \override Beam.grow-direction = #grow-in-up-direction
  c'16 d e f g f e d c g c g c e g c
}
Résultat../../images/75f5053e6197f292f7249233dbdb0e2586a0a5cd3d2544adb3d5ace0c9d99a66.svg

Affichage de valeurs#

La procédure standard display de Scheme ne convient pas pour afficher des messages de débogage dans LilyPond, car elle écrit sur la sortie standard stdout, alors que LilyPond affiche ses messages sur stderr, ce qui peut mélanger les deux. Mieux vaut utiliser ly:message. Contrairement à display, ly:message prend un premier argument qui est une chaîne de caractères. Les arguments après le premier servent à formater le message comme la procédure format de Guile. Une autre différence est que ly:message insère un saut de ligne automatiquement.

Par exemple, voici comment on pourrait afficher le contenu de la variable positions dans la fonction de rappel de l’exemple ci-dessus :

(ly:message "les positions sont : ~a" positions)

Les spécificateurs de format les plus importants sont ~a et ~s. Le premier affiche les valeurs de la manière la plus agréable possible, comme le ferait display, tandis que le second tente d’afficher d’une manière proche du code Scheme qui serait utilisé pour écrire la valeur. En particulier, ~s met des guillemets autour des chaînes de caractères, alors que ~a ne le fait pas.

Il existe également le spécificateur ~y, qui affiche les structures composites d’une manière plus facile à lire, en les répartissant sur plusieurs lignes avec de l’indentation. Il ne fonctionne toutefois pas avec ly:message.

\version "2.25.8"

{
  \override Slur.after-line-breaking =
    #(lambda (grob)
       (ly:message
         (format #f "~y" (ly:grob-property grob 'control-points))))
  b'1( b'')
}
Résultat../../images/7d551c611522b22c876a3b96ab7e75cc51c05f5f29ebb77a683c14e81c27ff6c.svg

Cet exemple affiche :

((0.732364943841144 . 1.195004)
 (1.76258769418946 . 3.05939544438528)
 (6.83883925432087 . 5.14594080151585)
 (8.88241194297576 . 4.545004))

Première incursion dans le fonctionnement interne de LilyPond#

Smobs#

Le terme « smob » signifie « Scheme Object » en anglais, soit « objet Scheme ». Le noyau C++ de LilyPond définit de nouveaux types d’objets, comme les hauteurs musicales, en plus des types prédéfinis par Guile (nombres, booléens, etc.). Tous les objets de ces types sont des smobs. Avec chaque type de smobs vient un prédicat. Par exemple, ly:pitch? teste si un objet est du type « hauteur musicale ». De plus, certains types de smobs offrent un constructeur, nommé ly:make-<type>, comme ly:make-pitch. Beaucoup des fonctions exportées par LilyPond prennent des smobs en argument. Par exemple, dans le cas des hauteurs, il existe ly:pitch-notename, ly:pitch-transpose, et bien d’autres.

Probs#

Prob signifie « Property Object », soit « objet à propriétés ». Ce terme désigne une classe d’objets qui contiennent des propriétés. Tous les probs sont des smobs, mais tous les smobs ne sont pas des probs : les hauteurs et les durées, par exemple, ne contiennent pas de propriétés. En revanche, les objets musicaux ou les contextes sont des probs.

Avec chaque type de probs viennent deux fonctions, pour accéder aux propriétés ou les modifier. Elles suivent toutes le même schéma :

(ly:<xxx>-property object property [default])

Accède à une propriété d’un prob. xxx est le type de prob. Ainsi, il existe ly:music-property, ly:event-property, ly:context-property, etc.

Le nom de la propriété est donné comme un symbole. À titre de mise en bouche sur les expressions musicales, testez l’exemple suivant :

#(display (ly:music-property #{ c'8 #} 'pitch))

Lorsque la propriété n’existe pas dans le prob en question, la valeur default est renvoyée, ou bien la liste vide '() si l’argument default n’est pas donné. Attention, contrairement à la plupart des fonctions Scheme standard, la valeur par défaut si default n’est pas défini est bien '(), et non pas #f !

(ly:<xxx>-set-property! object property value)

Définit la propriété property de object à value.

On a donc ly:music-set-property!, ly:event-set-property!, etc.

Glossaire des principaux types d’objets#

Voici quelques types d’objets qui jouent un rôle important dans l’architecture de LilyPond.

Définitions de sortie (output definitions)

Les blocs \layout, \midi et \paper. Ils contrôlent les grandes lignes du processus de compilation. Ils contiennent les définitions des différents contextes (exemple : \context { \Staff \override ... }), ainsi que des paramètres liés aux sauts de lignes, comme indent, page-breaking ou system-system-spacing.

Books, bookparts, scores [termes anglais sans traduction]

Les blocs \book, \bookpart et \score. Les \book sont des conteneurs qui correspondent à un fichier de sortie. Ils peuvent contenir des \score ou des \bookpart. Les \score correspondent aux différentes partitions d’un même ouvrage. Les \bookpart sont un niveau intermédiaire entre \book et \score.

Tous ces blocs contiennent des définitions de sortie. Plus précisément, un \book ou un \bookpart peut contenir des blocs \paper, tandis qu’un \score peut contenir des blocs \layout et \midi. Ces définitions de sorties suivent un principe hiérarchique : les réglages d’un \layout dans \score ont priorité sur les réglages dans un \paper au niveau du \bookpart, qui eux-mêmes ont priorité sur ceux au niveau \book, et ces derniers ont priorités sur les réglages d’un \layout ou \paper en dehors de tout \book.

Expressions musicales

Ces objets omniprésents sont la représentation que LilyPond se fait de la musique saisie par l’utilisateur. Par exemple, << c'8 \\ d16\p >> est une expression musicale de type SimultaneousMusic, qui contient deux expressions musicales dont la seconde est un NoteEvent qui lui-même contient un AbsoluteDynamicEvent.

Contextes

Les contextes correspondent à différents niveaux d’une partition. Les types de contextes les plus courants sont Score, Staff et Voice.

Itérateurs

Ces objets ont pour tâche d’avancer dans la musique, établissant le cours du temps musical. Ainsi, le Sequential_iterator agit sur une expression séquentielle { ... } et avance étape par étape dans ses éléments. Un autre itérateur important est le Simultaneous_music_iterator, à l’œuvre dans la construction parallèle << ... >>, qui avance en crabe dans tous les éléments à la fois.

Événements

Certaines expressions musicales servent de conteneurs qui regroupent expressions musicales. Les autres, les expressions élémentaires, ont pour vocation d’être transformées en événements par les itérateurs. Les notes, les silences, les nuances, les articulations, etc., sont représentées par des événements qui apparaissent à un point précis du temps musical. Il est en effet plus simple pour certaines parties de LilyPond d’agir sur ce flux d’événements plutôt que sur les expressions musicales elles-mêmes.

Traducteurs (translators)

Objets polyvalents qui réagissent aux événements pour créer des grobs et les mettre en réseau. Il y a deux types de traducteurs, les graveurs (engravers), qui agissent pour la sortie graphique, et les interprètes (performers), qui agissent pour la sortie MIDI.

Grobs

Abréviation de « Graphical Objects », soit « objets graphiques ». Les grobs représentent un élément graphique de la notation musicale, comme une tête de note (NoteHead), une hampe (Stem), un point d’augmentation (Dots), etc.

Fonctions de rappel (callbacks)

Les grobs possèdent des propriétés. Une fonction de rappel permet à une propriété d’être calculée à partir d’autres propriétés du grob et d’autres grobs en lien avec ce grob (comme la hampe d’une tête de note). Une fonction de rappel est simplement une fonction qui prend le grob comme argument et renvoie la valeur de la propriété. Un exemple se trouve dans Introduction aux fonctions de rappel.

Stencils

Un stencil est un dessin prêt à être écrit dans le fichier de sortie. La plupart des grobs possèdent un stencil. Ainsi, le stencil d’une hampe est une ligne.

Markups

Ces objets sont des sortes de programmes qui dessinent des éléments graphiques. Un markup peut s‘« exécuter », ce qui donne pour résultat un stencil. En termes techniques, le markup est interprété en un stencil. De nombreux grobs produisent leur stencil via un markup. On peut également insérer des markups directement dans le code, soit comme articulations, comme dans { c'8^\markup \italic { Cresc. poco a poco } }, soit en dehors de tout bloc \score.

Aperçu du fonctionnement interne de LilyPond et des façons de l’étendre#

Traduire du texte en une partition est une tâche complexe. LilyPond la remplit par un processus complexes qui comporte plusieurs grandes étapes. Dans ce qui suit, chaque étape est expliquée à côté du message qu’affiche LilyPond dans la console quand elle commence cette étape.

Analyse syntaxique#

Analyse...

La première étape est la lecture du fichier d’entrée. Au niveau le plus simple, un analyseur lexical (lexer) transforme le fichier en une suite d’éléments appelés « jetons », qui peuvent être des accolades, hauteurs, durées, etc. L’analyseur lexical de LilyPond est écrit avec l’outil Flex.

Ces jetons sont interprétés par l’analyseur syntaxique (parser), qui est écrit avec Bison, et coopère avec l’analyseur lexical en analysant les jetons à mesure qu’ils sont produits. L’analyseur syntaxique définit la syntaxe qui dicte comment les jetons deviennent des expressions musicales, des définitions de sortie, des blocs \book, \score, des markups, etc.

Pendant l’analyse lexicale, les expressions musicales peuvent être transformées par des fonctions musicales, comme \relative et \transpose. L’utilisateur peut définir ses propres fonctions, ce qui constitue un mécanisme d’extension puissant.

Itération#

Interprétation de la musique...

Cette étape se déroule pour chaque bloc \score. Son but est de transformer le flux d’événements en un réseau de grobs. Ainsi, un événement « note » donne lieu à une tête de note (NoteHead), une hampe (Stem), éventuellement une ligature (Beam), etc. On appelle également cette étape « traduction ».

C’est pendant cette étape que les itérateurs, contextes et traducteurs sont créés et jouent leur rôle.

On peut définir ses propres types de contextes dans les définitions de sortie. On peut également écrire ses propres traducteurs en Scheme. En revanche, les itérateurs ne peuvent être écrits qu’en C++.

Positionnement pur#

Pré-traitement des objets graphiques...

L’étape de traduction a pour résultat un réseau de grobs interconnectés. Leur positionnement dépend du choix des sauts de ligne et sauts de page, donc si les choses étaient simples, la prochaine étape serait de calculer les sauts de ligne et de page.

Mais les choses ne sont pas simples, puisque le calcul des sauts de ligne dépend lui-même de l’espace que prend chaque objet, donc de son positionnement.

De manière simplifiée, LilyPond fait ici un premier placement approximatif des objets, dit « pur ». La taille et la position de chaque grob sont estimées.

On peut écrire des fonctions pures en Scheme.

Calcul des sauts de ligne et de page#

Détermination du nombre optimal de page... et Répartition de la musique sur x pages...

Les algorithmes de calcul des sauts de ligne et de page tentent de répartir la musique de façon équilibrée. Certains prennent également en compte d’autres contraintes, comme les tournes de page.

Cette partie est codée en C++ et n’est pas accessible en Scheme.

Positionnement impur#

Dessin des systèmes...

Une fois que les sauts de ligne et de page ont été déterminés, tous les objets peuvent être placés de façon précise, et tous les stencils peuvent être calculés. Cette étape est entièrement accessible en Scheme.

Écriture du fichier de sortie#

Conversion à « document.pdf »...

Pour finir, les stencils sont traduits en langage PostScript et le résultat est converti par l’outil GhostScript en un fichier PDF.