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
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
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
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
Voici les prédicats de type les plus courants :
Nom du prédicat |
Valeurs acceptées |
---|---|
|
N’importe quel nombre, que ce soit un entier, un nombre à virgule flottante ou encore une fraction |
|
Un entier |
|
Entier positif |
|
Chaîne de caractères (suite de caractères, comme |
|
Un bloc |
boolean ? |
Un booléen, |
|
Une expression musicale |
|
Une hauteur (comme les deux premiers arguments de |
|
Une durée (comme l’argument facultatif de |
|
Une couleur, donnée par chaîne de caractères ou par ses composantes RGB ([documentation](notation:Coloring objects)) |
|
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
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
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, commeindent
,page-breaking
ousystem-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 typeSimultaneousMusic
, qui contient deux expressions musicales dont la seconde est unNoteEvent
qui lui-même contient unAbsoluteDynamicEvent
.- Contextes
Les contextes correspondent à différents niveaux d’une partition. Les types de contextes les plus courants sont
Score
,Staff
etVoice
.- 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 leSimultaneous_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...
etRé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.