Les fonctions musicales 3. Du code réutilisable

Traduction d’un article de Urs Liska

Dans l’article précédent, nous avons écrit une fonction permettant de coloriser une expression musicale avec différentes couleurs choisies. Nous avons pointé du doigt le problème suivant : nous devions modifier chaque propriété manuellement – et d’ailleurs nous n’avons pas pu les identifier toutes. En répétant la fonction \override, nous avons commis des redondances qui vont à l’encontre des bonnes pratiques de programmation. Aujourd’hui, nous allons faire beaucoup mieux et
apprendre à créer du code réutilisable en factorisant et en utilisant des opérations sur les listes.

La fonction en question était la suivante :

\version "2.18.0"

colorierMusique =
#(define-music-function (parser location ma-couleur ma-musique)
   (color? ly:music?)
   #{
     \temporary \override NoteHead.color = #ma-couleur
     \temporary \override Stem.color = #ma-couleur
     \temporary \override Flag.color = #ma-couleur
     \temporary \override Beam.color = #ma-couleur
     \temporary \override Rest.color = #ma-couleur
     \temporary \override Slur.color = #ma-couleur
     \temporary \override PhrasingSlur.color = #ma-couleur
     \temporary \override Tie.color = #ma-couleur
     \temporary \override Script.color = #ma-couleur
     \temporary \override Dots.color = #ma-couleur
     
     #ma-musique
     
     \revert NoteHead.color
     \revert Stem.color
     \revert Flag.color
     \revert Beam.color
     \revert Rest.color
     \revert Slur.color
     \revert PhrasingSlur.color
     \revert Tie.color
     \revert Script.color
     \revert Dots.color
   #})

Commençons par factoriser le code utilisé plusieurs fois.

Pour chaque élément, nous avons utilisé \temporary \override NNNN.color = #ma-couleur. Nous allons donc factoriser ce code dans une fonction dédiée. Il s’agira d’une fonction qui va nous aider à coder la fonction colorierMusique qui ne sera toutefois pas utilisée directement dans les fichiers source. Chaque utilisation de \override sera encapsulée dans une fonction musicale qui retournera la bonne expression musicale.

colorierSymbole =
#(define-music-function (parser location mon-symbole ma-couleur)
   (symbol? color?)
   #{
     \temporary \override #mon-symbole #'color = #ma-couleur
   #})

Cette fonction prend deux paramètres : le nom d’un symbole (grob = GRaphicalOBject) ainsi qu’une couleur. Nous connaissons déjà le typage de la couleur (color?) et, pour ce qui concerne le symbole, son type est symbol?. Au sein de la fonction, \temporary \override est appliqué au paramètre mon-symbole auquel est appliqué ma-couleur. La fonction sera ainsi appelée : \colorierSymbole NoteHead #red. Notre fonction \colorierMusique comportant déjà un paramètre #ma-couleur, nous appellerons la fonction ainsi : \colorierSymbole NoteHead #ma-couleur.

Pour annuler la couleur, dans la seconde partie de \colorierMusique, nous allons écrire une fonction \decolorierSymbole, identique à \colorierSymbole, mais qui utilisera \revert au lieu de \override. Au final, nous aurons la fonction principale \colorierMusique et ses deux sous-fonctions \colorierSymbole et \decolorierSymbole :

\version "2.18.0"

colorierSymbole =
#(define-music-function (parser location mon-symbole ma-couleur)
   (symbol? color?)
   #{
     \temporary \override #mon-symbole #'color = #ma-couleur
   #})

decolorierSymbole =
#(define-music-function (parser location mon-symbole)
   (symbol?)
   #{
     \revert #mon-symbole #'color
   #})

colorierMusique =
#(define-music-function (parser location ma-couleur ma-musique)
   (color? ly:music?)
   #{
     \colorierSymbole NoteHead #ma-couleur
     \colorierSymbole Stem #ma-couleur
     \colorierSymbole Flag #ma-couleur
     \colorierSymbole Beam #ma-couleur
     \colorierSymbole Rest #ma-couleur
     \colorierSymbole Slur #ma-couleur
     \colorierSymbole PhrasingSlur #ma-couleur
     \colorierSymbole Tie #ma-couleur
     \colorierSymbole Script #ma-couleur
     \colorierSymbole Dots #ma-couleur

     #ma-musique

     \decolorierSymbole NoteHead
     \decolorierSymbole Stem
     \decolorierSymbole Flag
     \decolorierSymbole Beam
     \decolorierSymbole Rest
     \decolorierSymbole Slur
     \decolorierSymbole PhrasingSlur
     \decolorierSymbole Tie
     \decolorierSymbole Script
     \decolorierSymbole Dots
   #})

ma-musique = \relative c' {
  c4. d8 e16 d r cis( d4) ~ | d1 \fermata
}

\relative c' {
  \colorierMusique #blue \ma-musique
  \colorierMusique #red { c4 c } d \colorierMusique #green e\f
  \colorierMusique #magenta \ma-musique

}

ce qui donne le même résultat que dans l’article précédent :

(Click to enlarge)

(Cliquez pour agrandir)

Bon, c’est vrai, notre fonction principale est toujours aussi longue. Mais nous avons fait quelque chose de très important en évitant la répétition du code, ce qui rend la fonction beaucoup plus facilement maintenable : si nous décidons de supprimer \temporary (pour les versions de LilyPond avant 2.18), il suffit de le faire une seule fois et à un seul endroit (\colorierSymbole). Maintenant que nous avons factorisé, toute modification se fait une seule fois ; le changement est répercuté dans la fonction principale à chaque fois que celle-ci est appelée. Et si par exemple nous souhaitons annuler l’effet de la fonction principale partout où nous l’avons appelée dans la source, il suffit de commenter la fonction \colorierSymbole. Mais tout cela n’est qu’un début…

Utiliser une liste comme paramètre

Nous n’avons pas fini de retravailler la fonction, parce que nous devons encore appeler les sous-fonctions pour chaque symbole. L’idéal serait de n’appeler qu’une seule fonction ; il suffirait de lui passer en argument la liste des symboles à traiter. Nous allons donc créer une fonction de ce type :

colorierDesSymboles =
#(define-music-function (parser location ma-liste-de-symboles ma-couleur)
   (symbol-list? color?)
   #{
     
   #})

Cette fonction prend une liste de noms de symboles (grobs) ainsi qu’une couleur, et s’applique sur chaque élément de la liste en utilisant les sous-fonctions déjà écrites. Cette fonction sera alors appelée ainsi :

\colorierDesSymboles #'(NoteHead
                        Stem
                        Flag
                        Beam
                        Rest
                        Slur
                        PhrasingSlur
                        Tie
                        Script
                        Dots
                        DynamicText
                        Accidental) #ma-couleur

Cela permettra de rendre la fonction principale beaucoup plus concise et facile à maintenir. Ajouter des symboles sera simplement fait en ajoutant le nom du symbole dans ma-liste-de-symboles.

#### Itération sur la liste

Autant fabriquer une fonction sur une liste donnée est facile, autant créer la liste elle-même peut vous rendre dingue si vous n’êtes pas habitué à Scheme. Et cela a été mon cas en écrivant cet article – ma contribution sur ce thème s’arrêtera d’ailleurs un jour à cause de cela 😉

Le moyen le plus simple d’itérer en Scheme est l’utilisation de la fonction map – et même les programmeurs d’autres lanagage peuvent facilement la comprendre ! La fonction map prend en paramètres une fonction et une liste de valeurs, et applique la fonction sur chacune de ces valeurs, l’une après l’autre. Elle retourne ensuite la liste des valeurs modifiées.

(map colorierSymbole ma-liste-de-symboles)

passera en revue la liste des symboles (grobs) et les passera en paramètres un par un à la fonction colorierSymbole. La syntaxe est très efficace, mais nous voyons qu’elle ne prend pas en compte le paramètre couleur ; il va falloir creuser encore un peu. En première approche, nous pouvons utiliser la fonction anonyme lambda qui va produire une fonction adaptée qui peut prendre le nombre de paramètres que l’on souhaite.

((lambda (arg) (colorierSymbole arg ma-couleur)) NoteHead)

crée une fonction dont le corps est (colorierSymbole arg ma-couleur). Le paramètre arg est celui qui sera passé dans ce corps. Ici, il sera
remplacé par NoteHead, de sorte que colorierSymbole sera appelé ainsi : (colorierSymbole NoteHead ma-couleur). Cet exemple n’a ici d’autre intérêt que de vous montrer comment fonctionne une fonction anonyme lambda avec différents types d’arguments, valeurs codées en dur ou variables. Cela dit, combinée avec map, elle peut être très efficace.

(map (lambda (arg) (colorierSymbole arg ma-couleur)) ma-liste-de-symboles)

va parcourir la liste ma-liste-de-symboles et les passer un par un à la fonction anonyme lambda. Il y aura donc itération de la fonction colorierSymbole sur la liste des symboles, avec le paramètre ma-couleur. La documentation de Guile traite de map et lambda, mais aussi de Scheme : consultez-la !

Nous n’en avons malheureusement pas encore fini. Comme nous l’avons dit, map n’applique qu’une seule fonction à chaque élément de la liste, et renvoie une nouvelle liste d’expressions musicales. Bien que très utile pour manipuler des listes en général, cela n’est pas utile pour notre cas : nous devons obtenir une seule
expression musicale, alors que map nous fournit une liste d’expressions musicales. Nous ne pouvons donc pas utiliser cet outil, et devons nous tourner vers des méthodes de vraie récursion.

Conclusion provisoire

Aujourd’hui, nous n’avons apparemment pas avancé beaucoup : nous avons factorisé notre code et savons ce qu’il nous faut encore faire. Nous avons exploré une piste qui s’est révélée être une impasse. Bien entendu, je vous en ai parlé pour vous apprendre quelque chose d’utile, et j’espère que cela vous aidera à utiliser Scheme par la suite. Même si map et lambda ne nous ont pas servi ici, il s’agit de deux concepts indispensables pour tout programmeur LilyPond/Scheme, et il est bon de s’y frotter le plus tôt possible.

Le prochain article nous permettra d’atteindre notre but en utilisant un concept fondamental de Scheme : la récursion.


Commentaires après la version anglaise

ming April 2, 2014 at 10:44

Très bons articles pour apprendre à utiliser Scheme dans LilyPond. J’ai lu les 3 premiers articles et ai beaucoup appris : j’attends avec impatience la suite ! Merci, Urs !


Réponse ↓
Urs Liska April 2, 2014 at 16:07
Merci à toi.
En fait, le quatrième article, déjà écrit, fermera la série pour un petit moment. D’abord parce que mes connaissances sont limitées à ces quatre articles (même si l’on pourrait croire le contraire à la fin du dernier article), et puis j’ai à nouveau beaucoup de choses à faire par ailleurs. Mais je serais ravi que d’autres prennent la suite !


Réponse ↓
Caio Barros April 2, 2014 at 11:24

Urs, ressens-tu les ondes d’amour hautes-fréquences que je t’envoie à chaque fois que je lis un de tes articles ? J’apprends tellement ici : continue !

Où puis-je trouver la liste des types disponibles en Scheme ? Par exemple, je suis en train d’écrire une fonction Scheme qui produira nolet num:den à l’endroit voulu uniquement. Mon problème est que je ne sais pas quel type de paramètre ajouter après la déclaration :

tupletFraction = #(define-music-function (parser location tuplet-fraction)
; quel type d'argument déclarer ici ?
(unkonw-argument-type)
#{
\once \override TupletNumber.text = #tuplet-number::calc-fraction-text
\tuplet #tuplet-fraction
#}
)

\relative c' {
% mon tuplet inhabituel ici
c4 c \tupletFraction #5/3 { c16 c c c c } c c4 |
% le tuplet habituel ici
c4 c \tuplet 3/2 { c c c } |
}

Existe-t-il une liste des principaux types de paramètres utilisés dans Lilypond ou quelque chose dans le genre ?


Réponse ↓
Urs Liska April 2, 2014 at 16:10

Désolé, je passe la main : je serais intéressé aussi par une réponse. C’est le genre de problème gonflant tant qu’on a pas trouvé de solution…

Je pense toutefois qu’un tel tuplet devrait être exprimé par une paire d’entiers, et appelé ainsi : #'(5 . 3), mais je ne sais pas comment faire après.


Réponse ↓
Urs Liska April 2, 2014 at 16:19
Hé, j’ai trouvé ;o)
Mon intuition était la bonne, mais il faut aussi passer en paramètre la musique :

tupletFraction =
        #(define-music-function (parser location tuplet-fraction music)
           (pair? ly:music?)
           #{
             \once \override TupletNumber.text = #tuplet-number::calc-fraction-text
             \tuplet #tuplet-fraction #music
           #}
           )

        \relative c' {
          % mon tuplet inhabituel ici
          c4 c \tupletFraction #'(5 . 3) { c16 c c c c } c c4 |
          % le tuplet habituel ici
          c4 c \tuplet 3/2 { c c c } |
        }

Réponse ↓
Caio Barros April 3, 2014 at 12:27

Mille mercis !

Cela dit, je n’ai pas bien compris la notion de passer en paramètre la musique. J’ai compris que tu as passé un paramètre music de type ly:music? mais je ne comprends pas à quoi il sert ? Pourquoi faut-il l’ajouter ?


Réponse ↓
Urs Liska April 3, 2014 at 23:28
Si j’ai bien tout compris, la fonction musicale retourne une expression musicale. Dans notre cas, \tupletFraction #'(5 . 3) { c16 c c c c } est l’expression musicale. Tu ne peux pas invoquer la fonction ainsi (\tuplet 5/3) et écrire la musique derrière. Il faut donc passer la musique en paramètre de la fonction, qui la transformera et redonnera une autre expression musicale modifiée.


Réponse ↓
Paul Morris April 2, 2014 at 17:22

Voici la liste des types de paramètres
Cette page est liée avec celle du manuel d’extension


Réponse ↓
Caio Barros April 3, 2014 at 12:28
Merci ! J’étais passé à côté en parcourant la doc…


Réponse ↓
Jay Anderson April 3, 2014 at 04:26
En fait, il est possible d’utiliser map sans récursion explicite :

% enveloppe la liste des éléments musicaux en une seule séquence musicale.
#(define (enveloppe-liste-musique ma-liste-musique)
  (let ((out #{ {} #} ))
    (ly:music-set-property! out 'elements ma-liste-musique)
    out))

colorierDesSymboles =
#(define-music-function (parser location symboles ma-couleur)
  (symbol-list? color?)
  (enveloppe-liste-musique (map (lambda (arg) #{ \colorierSymbole $arg $ma-couleur #}) symboles)))

decolorierDesSymboles =
#(define-music-function (parser location symboles)
  (symbol-list?)
  (enveloppe-liste-musique (map (lambda (arg) #{ \decolorierSymbole $arg #}) symboles)))

La fonction enveloppe-liste-musique comporte des concepts qu’il est nécessaire de détailler pour bien comprendre :
– les variables locales (let (…) …)
– les internes de Lilypond et l’API Scheme

De façon générale, la récursion doit être utilisée le plus modérément
possible. Cela dit, le concept doit être maîtrisé par un utilisateur de
Scheme.

Belle introduction à Scheme et Lilypond ! Bravo !


Réponse ↓
Urs Liska April 3, 2014 at 07:34

Merci pour la suggestion. Pourrais-tu écrire un article sur let et consort ? Bien entendu, tu pourrais continuer mon fil ou citer mes articles. Par exemple à la suite de mes quatre articles, comme approche alternative ?


Réponse ↓

One thought on “Les fonctions musicales 3. Du code réutilisable

  1. Pingback: Les fonctions musicales 4. La récursivité | Scores of Beauty

Leave a Reply

Your email address will not be published. Required fields are marked *