Lier une variable par référence plutôt que par valeur en scheme

Bonjour,

Je continue mon cheminement pour automatiser les partitions en fonction des instruments et des clefs.
Grâce à des posts passes, j'ai pu définir une fonction qui génère un \book de façon paramétrique, en indiquant le niveau de transposition que je souhaite. Voici ce que ça donne:

Générateur scheme de \book
bookRenderingBuilder = 
  #(define-void-function (highKey lowKey printKey targetPitch targetAlteration)
    (string? string? string? integer? number?)
    (print-book-with-defaults
     #{
      \book {
        \bookOutputSuffix #(string-join (list printKey defaultInstrumentKey))
        \header {
          instrument = #(string-concatenate (list "Instruments en " printKey))
        }
         \score {
          <<
            \structurePart
            \chordsPart
            \transpose #(ly:make-pitch 0 0) #(ly:make-pitch 0 targetPitch targetAlteration) \choirPart
          >>
          \layout { }
        }
      }
    #}))

L'appel s'effectue comme suit:
\bookRenderingBuilder "treble" "treble_8" "Sib" -1 #FLAT
Question optionnelle intermédiaire: est-ce que vous repérez des mauvaises pratiques dans ce que j'ai fait?
Il faudra que je puisse trouver un mécanisme qui unifie les 3 derniers paramètres, puisqu'on peut déduire -1 #FLAT de "Sib"

Ma question concerne plus particulièrement les 2 premiers paramètres, qui définissent une clef par défaut pour les partitions "voix aiguës" et une clef pour les "voix graves".
La définition dans la déclaration des parties est celle qui suit:

exemple sur une partie de chœur
choirPart = \new ChoirStaff <<
  \new Staff 
  <<
    \clef #defaultHighKey     % <= Positionnement de la clef pour les voix hautes
    \new Voice = "soprano" { \voiceOne \soprano }
    \new Voice = "alto" { \voiceTwo \alto }
  >>
  \new Lyrics \with {
    \override VerticalAxisGroup.staff-affinity = #CENTER
  } \lyricsto "soprano" \verseOne
  \new Staff \with 
  <<
    \clef #defaultLowKey     % <= Positionnement de la clef pour les voix basses
    \new Voice = "tenor" { \voiceOne \tenor }
    \new Voice = "bass" { \voiceTwo \bass }
  >>
>>

Mon problème est que si je fais 2 appels successifs, ex:

\bookRenderingBuilder "treble" "treble_8" "Ut" -1 #FLAT

#(define defaultHighKey "treble")
#(define defaultLowKey "bass")

\bookRenderingBuilder "alto" "alto_8" "Sib" 0 #NATURAL

, alors la transposition fonctionne bien, mais les clefs ne changent pas puisqu'elles ont déjà été interprétées au moment de la déclaration.
(Note: je précise que pour le moment je n'utilise pas les 2 paramètres de clef dans l'appel, j'essaye déjà que ça marche en direct).

Ma question est donc: est-il possible de faire un passage "par référence" plutôt que "par valeur" lorsqu'on définit une expression lilypond+scheme ?
Et si ce n'est pas possible (mon intuition me dicte que c'est bien le cas), alors comment faire? Est-ce qu'il faut mettre un "template" de clef (ex @@HIGH_KEY@@) qu'on remplace lors de l'instanciation (du style s/@@HIGH_KEY@@/#defaultHighKey/g)?
Est-il possible de "reparser" ou "recompiler" une variable?
Y a-t-il une autre bonne pratique correspondant à ce besoin?

Et j'en profite pour reformuler ici une demande qui avait été faite dans un post précédent: est-ce qu'il existe un moyen de se former de façon plus poussée sur la programmation Lilypond+Scheme? J'ai bien compris que la puissance de l'association des 2 est monumentale, mais plonger dans les arcanes de la chose, même lorsqu'on a un passé d'informaticien, n'est vraiment pas évident.

Merci beaucoup, bonne journée à tous.
Emmanuel

Bonjour Emmanuel, Il existe une documentation complète du Scheme sur leur site officiel, il existe même une ébauche en français faite par Jean sur l'intégration de Scheme et Lilypond sur le site de lilypond, mais c'est tout. Autrement dit, pour y parvenir il vaudrait mieux directement devenir développeur occasionnel de lilypond, afin de pratiquer régulièrement et de pouvoir maintenir le code sur la durée. Si votre seul objectif est d'avoir un générateur conditionnel de book, il me paraît plus simple de le faire avec un script système. On peut d'ailleurs envoyer une variable lilypond via la ligne de commande lilypond et ensuite faire une condition en scheme dans le code lilypond. Au plaisir, Ben

Bonjour Ben

Merci pour cette réponse. Comme je l'ai dans de précédents posts, je préfère ne pas passer par des scripts systèmes et tout avoir dans le même "root file".
J'essaye une autre approche: j'ai mis la déclaration des parties dans une fonction. C'est ici que je définis les clefs, donc ça me permet de ne pas avoir à descendre trop profondément dans les définitions.

J'ai donc défini ceci (note: j'ai remonté la transposition dans cette fonction):

Résumé
partsRenderingBuilder = #(define-void-function (highKey lowKey targetPitch targetAlteration)
    (string? string? integer? number?)
    #{
      \new Devnull \structure
      
      \new ChordNames \chordNames
      
      \new ChoirStaff <<
        \new Staff
        \transpose #(ly:make-pitch 0 0) #(ly:make-pitch 0 targetPitch targetAlteration) <<
          \clef #highKey
          \new Voice = "soprano" { \voiceOne \soprano }
          \new Voice = "alto" { \voiceTwo \alto }
        >>
        \new Staff
        \transpose #(ly:make-pitch 0 0) #(ly:make-pitch 0 targetPitch targetAlteration) <<
          \clef #lowKey
          \new Voice = "tenor" { \voiceOne \tenor }
          \new Voice = "bass" { \voiceTwo \bass }
        >>
      >>
    #})

et j'appelle cette fonction dans la fonction englobante bookRenderingBuilder.
bookRenderingBuilder =

Résumé
  #(define-void-function (highKey lowKey printKey targetPitch targetAlteration)
    (string? string? string? integer? number?)
    (print-book-with-defaults
     #{
      \book {
        \bookOutputSuffix #(string-join (list printKey defaultInstrumentKey))
        \header {
          instrument = #(string-concatenate (list "Instruments en " printKey))
        }
        
         \score {
          <<
            #(partsRenderingBuilder highKey lowKey targetPitch targetAlteration)
          >>
          \layout { }
        }
      }
    #}))

La compilation se passe (trop) bien.Le problème que j'ai est que je prends l'avertissement

Avertissement : on passe un partition de durée nulle

Et là je sèche. Pour la fonction générant le book, il fallait appeler en tête la fonction print-book-with-defaults. Est-ce qu'il y a un équivalent pour produire en ligne les parties? Ou bien une façon de produire les variables structurePart, chordsPart et choirPart, que je pourrais utiliser hors du scope de la fonction, dans le bloc \book, ce qui me permettrait au passage de me passer du bookRenderingBuilder?

Je me réponds à moi-même par souci de complétion du thread pour les suivants:
si je remplace
partsRenderingBuilder = #(define-void-function
par
partsRenderingBuilder = #(define-scheme-function

Alors j'obtiens bien une partition mais dans laquelle les 3 parties (structure, chords, choir) sont empilées (comme s'il y avait 3 blocs \score: le bloc structure génère un grand vide en haut, les accords tous ensemble au milieu, et le chœur tout seul en bas).

Si maintenant je remonte le << >> du \score dans la fonction, alors ça fonctionne, j'ai bien toutes les parties qui jouent leur rôle dans 1 seul partition.

Donc je ne sais pas si c'est la bonne façon de faire, ni quelles sont les limitations qui font que le codage précédent ne fonctionnait pas, mais au moins maintenant ça marche, je peux générer une partition avec les bonnes clefs et les bonnes transpositions.

Je mets ici les éléments saillants de mon fichier, pour exemple/aide pour d'autres.

conversion de gamme (C, D, etc.) vers octave\+pitch\+alteration pour ly\:make-pitch
#(define keyToPitch '( ; This list defines the direct required transposition in term of lilypond pitch => we want to transpose in D natural, so increase each pitch from 0 octave, 1 pitch, 0 alteration
("C" . ( 0 0 0))
("C+" . ( 0 8 0))
("C-" . ( 0 -8 0))
("Cb" . ( 0 0 -1))
("Cb+" . ( 0 8 -1))
("Cb-" . ( 0 -8 -1))
("C#" . ( 0 0 1))
("C#+" . ( 0 8 1))
("C#-" . ( 0 -8 1))
("D" . ( 0 1 0))
("D+" . ( 0 9 0))
("D-" . ( 0 -7 0))
("Db" . ( 0 1 -1))
("Db+" . ( 0 9 -1))
("Db-" . ( 0 -7 -1))
("D#" . ( 0 1 1))
("D#+" . ( 0 9 1))
("D#-" . ( 0 -7 1))
("E" . ( 0 2 0))
("E+" . ( 0 10 0))
("E-" . ( 0 -6 0))
("Eb" . ( 0 2 -1))
("Eb+" . ( 0 10 -1))
("Eb-" . ( 0 -6 -1))
("E#" . ( 0 2 1))
("E#+" . ( 0 10 1))
("E#-" . ( 0 -6 1))
("F" . ( 0 3 0))
("F+" . ( 0 11 0))
("F-" . ( 0 -5 0))
("Fb" . ( 0 3 -1))
("Fb+" . ( 0 11 -1))
("Fb-" . ( 0 -5 -1))
("F#" . ( 0 3 1))
("F#+" . ( 0 11 1))
("F#-" . ( 0 -5 1))
("G" . ( 0 -3 0))
("G+" . ( 0 4 0))
("G-" . ( 0 -11 0))
("Gb" . ( 0 -3 -1))
("Gb+" . ( 0 4 -1))
("Gb-" . ( 0 -11 -1))
("G#" . ( 0 -3 1))
("G#+" . ( 0 4 1))
("G#-" . ( 0 -11 1))
("A" . ( 0 -2 0))
("A+" . ( 0 5 0))
("A-" . ( 0 -10 0))
("Ab" . ( 0 -2 -1))
("Ab+" . ( 0 5 -1))
("Ab-" . ( 0 -10 -1))
("A#" . ( 0 -2 1))
("A#+" . ( 0 5 1))
("A#-" . ( 0 -10 1))
("B" . ( 0 -1 0))
("B+" . ( 0 6 0))
("B-" . ( 0 -1 0))
("Bb" . ( 0 -1 -1))
("Bb+" . ( 0 6 -1))
("Bb-" . ( 0 -1 -1))
("B#" . ( 0 -1 1))
("B#+" . ( 0 6 1))
("B#-" . ( 0 -1 1))
))

%% This list defines the needed transposition from instrument's root scale (Ex Bb for clarinets will get D natural as transposition)
#(define transpositorRootScale '(
("C" . ( 0 0 0))
("C+" . ( -1 0 0))
("C-" . ( 1 0 0))
("Cb" . ( 0 0 1))
("Cb+" . ( -1 0 1))
("Cb-" . ( 1 0 1))
("C#" . ( 0 7 0)) ; Instrument plays in C# => need to transpose in B NATURAL
("C#+" . ( -1 7 0)); Instrument plays in C#, 1 octave upper => need to transpose in B, 1 octave lower
("C#-" . ( 1 7 0)); Instrument plays in C#, 1 octave lower => need to transpose in B, 1 octave upper
("D" . ( 0 -1 -1))
("D+" . ( -1 -1 -1))
("D-" . ( 0 -7 0))
("Db" . ( 0 1 -1))
("Db+" . ( 0 9 -1))
("Db-" . ( 0 -7 -1))
("D#" . ( 0 1 1))
("D#+" . ( 0 9 1))
("D#-" . ( 0 -7 1))
("E" . ( 0 2 0))
("E+" . ( 0 10 0))
("E-" . ( 0 -6 0))
("Eb" . ( 0 2 -1))
("Eb+" . ( 0 10 -1))
("Eb-" . ( 0 -6 -1))
("E#" . ( 0 2 1))
("E#+" . ( 0 10 1))
("E#-" . ( 0 -6 1))
("F" . ( 0 3 0))
("F+" . ( 0 11 0))
("F-" . ( 0 -5 0))
("Fb" . ( 0 3 -1))
("Fb+" . ( 0 11 -1))
("Fb-" . ( 0 -5 -1))
("F#" . ( 0 3 1))
("F#+" . ( 0 11 1))
("F#-" . ( 0 -5 1))
("G" . ( 0 -3 0))
("G+" . ( 0 4 0))
("G-" . ( 0 -11 0))
("Gb" . ( 0 -3 -1))
("Gb+" . ( 0 4 -1))
("Gb-" . ( 0 -11 -1))
("G#" . ( 0 -3 1))
("G#+" . ( 0 4 1))
("G#-" . ( 0 -11 1))
("A" . ( 0 -2 0))
("A+" . ( 0 5 0))
("A-" . ( 0 -10 0))
("Ab" . ( 0 -2 -1))
("Ab+" . ( 0 5 -1))
("Ab-" . ( 0 -10 -1))
("A#" . ( 0 -2 1))
("A#+" . ( 0 5 1))
("A#-" . ( 0 -10 1))
("B" . ( 0 -1 0))
("B+" . ( 0 6 0))
("B-" . ( 0 -1 0))
("Bb" . ( 0 -1 -1))
("Bb+" . ( 0 6 -1))
("Bb-" . ( 0 -1 -1))
("B#" . ( 0 -1 1))
("B#+" . ( 0 6 1))
("B#-" . ( 0 -1 1))
))


#(define getTranspositionOctave (define-scheme-function (scaleKey) (string?) (list-ref (assoc-ref keyToPitch scaleKey) 0)))
#(define getTranspositionBasePitch (define-scheme-function (scaleKey) (string?) (list-ref (assoc-ref keyToPitch scaleKey) 1)))
#(define getTranspositionAlteration (define-scheme-function (scaleKey) (string?) (list-ref (assoc-ref keyToPitch scaleKey) 2)))

#(define getReverseTranspositorOctave (define-scheme-function (tranpositorKey) (string?) (list-ref (assoc-ref transpositorRootScale tranpositorKey) 0)))
#(define getReverseTranspositionBasePitch (define-scheme-function (tranpositorKey) (string?) (list-ref (assoc-ref transpositorRootScale tranpositorKey) 1)))
#(define getReverseTranspositionAlteration (define-scheme-function (tranpositorKey) (string?) (list-ref (assoc-ref transpositorRootScale tranpositorKey) 2)))

Attention: ne faites pas une confiance aveugle aux valeurs de pitches définis dans la map !

La fonction qui génère les différentes parties de la partition
partsRenderingBuilder = #(define-scheme-function (highKey lowKey targetPitch)
    (string? string? string?)
    #{
      <<
      \new Devnull \structure
      
      \new ChordNames \chordNames
      
      \new ChoirStaff <<
        \new Staff  \transpose #(ly:make-pitch 0 0) #(ly:make-pitch (getTranspositionOctave targetPitch) (getTranspositionScale targetPitch) (getTranspositionAlteration targetPitch))
        <<
          \clef #highKey
          \new Voice = "soprano" { \voiceOne \soprano }
          \new Voice = "alto" { \voiceTwo \alto }
        >>
        \new Staff \transpose #(ly:make-pitch 0 0) #(ly:make-pitch (getTranspositionOctave targetPitch) (getTranspositionScale targetPitch) (getTranspositionAlteration targetPitch))
        <<
          \clef #lowKey
          \new Voice = "tenor" { \voiceOne \tenor }
          \new Voice = "bass" { \voiceTwo \bass }
        >>
      >>
      >>
    #})

Et enfin

La fonction qui génère le bloc \book
bookRenderingBuilder = 
  #(define-void-function (highKey lowKey printKey targetPitch printableReadingClef)
    (string? string? string? string? string?)
    (print-book-with-defaults
     #{
      \book {
        \bookOutputSuffix #(string-join (list printKey printableReadingClef))
        \header {
          instrument = #(string-concatenate (list "Instruments en " printKey))
        }
        
         \score {
          <<
            #(partsRenderingBuilder highKey lowKey targetPitch)
          >>
          \layout { }
        }
      }
    #}))

Il ne reste plus que l'appel lui-même

\bookRenderingBuilder "treble" "treble_8" "Re" "D" "Clef_Sol"

L'exemple ci-dessus génère une version de la partition dont toutes les portées seront écrites en clef de sol (𝄠 pour les voix graves); les voix mélodiques seront transposées un ton au-dessus.
Donc:

\bookRenderingBuilder "treble" "bass" "Ut" "C" "Clef_Fa_Sol"  % Flûte
\bookRenderingBuilder "treble" "treble_8" "Ut" "C" "Clef_Sol"  % Piano, chœur
\bookRenderingBuilder "treble" "treble_8" "Sib" "D" "Clef_Sol"  % Clarinettes, trompettes
\bookRenderingBuilder "treble" "treble_8" "Mib" "A" "Clef_Sol"  % Saxo
\bookRenderingBuilder "alto" "alto_8" "Ut" "C" "Clef_Alto"  % Violon alto
\bookRenderingBuilder "treble" "treble_8" "Fa" "G" "Clef_Sol"  % Cor

L'exemple ci-dessus produira 6 partitions de la même musique, transposant les notes de façon adéquate pour l'instrument considéré, et avec la clef attendue par le musicien.
Ceci est utile lorsqu'on joue de la musique en groupe (mais pas en orchestre officiel), dans laquelle chaque musicien peut choisir la partie qu'il souhaite jouer.

Notes:

  • D'un point de vue lilypond, cela permet de ne pas multiplier les définitions de \book ou parts et de rendre le code génératif final bien plus lisible que de scroller.
  • L'ajout d'un instrument, quel qu'il soit, ne requiert théoriquement qu'un seul appel en plus.

Auto-commentaires/TODO

  1. Pour le moment je ne sais faire qu'une fonction par type de partitions (chords+choir par exemple). Il serait bien que je puisse définir quelque part les parties qui m'intéressent, si elles seront transposées (dans la pratique des musiciens que je côtoie, les partitions sont transposées, mais pas les accords). L'avantage serait de pouvoir mettre cette fonction générique dans un fichier \include
  2. Pouvoir réduire le nombre de paramètres en déduisant highKey et lowKey de printableReadingKey.
  3. Les fonctions ne prennent pas en compte les instruments exotiques, transposant avec des quarts de ton.
  4. On peut ajouter une table associative qui renvoit la chaîne de transposition à partir du nom de l'instrument ("Clarinet" => "D") et à partir de là récupérer le pitch de transposition.

Soyons clairs: j'ai produit cela avec mes maigres connaissances des bonnes pratiques de la combinaison Guile+Lilypond.
N'hésitez pas s'il y a des hérésies!

En termes de bonnes pratiques, il vaut mieux appeler toplevel-book-handler plutôt que print-book-with-defaults car print-book-with-defaults est la valeur par défaut de toplevel-book-handler mais cette valeur peut être configurée via diverses options.

Plutôt que de travailler avec des paires d'un entier représentant une échelle de la gamme et d'une altération, il est plus simple de travailler avec les objets « hauteur » de LilyPond, qui en prime peuvent s'écrire avec la syntaxe habituelle (par exemple bes, ou sib en \language "français").

(define-scheme-function (...) (...) ...) est la syntaxe pour définir une fonction qui s'appelle avec la syntaxe LilyPond \fonction <argument> ... <argument>. Il se trouve que ces fonctions peuvent aussi s'appeler depuis Scheme avec la syntaxe Scheme (fonction <argument> ... <argument>), mais si c'est pour n'utiliser que la syntaxe Scheme, il suffit de définir une fonction Scheme normale avec (define (fonction ...) ...).

La table qui définit la transposition à appliquer pour passer des hauteurs réelles aux hauteurs lues par les instrumentistes en fonction de la transposition de l'instrument est inutile : plutôt que de calculer par exemple ré à partir de si bémol et transposer de do vers ré, il suffit de transposer de si bémol vers do.

En incorporant quelques autres simplifications, ça donne :

\version "2.24.4"

\language "français"

structure = { s1 }
chordNames = \chordmode { do1 }
soprano = { do''1 }
alto = { mi'1 }
tenor = { sol1 }
bass = { do1 }

#(define (build-parts high-key low-key target-pitch)
   #{
     <<
       \new Devnull \structure
       \new ChordNames \chordNames
       \new ChoirStaff \transpose #target-pitch do <<
         \new Staff <<
           \clef #high-key
           \new Voice = "soprano" { \voiceOne \soprano }
           \new Voice = "alto" { \voiceTwo \alto }
         >>
         \new Staff <<
          \clef #low-key
          \new Voice = "tenor" { \voiceOne \tenor }
          \new Voice = "bass" { \voiceTwo \bass }
         >>
       >>
     >>
   #})

bookRenderingBuilder =
#(define-void-function (high-key low-key print-key target-pitch printable-reading-clef)
   (string? string? string? ly:pitch? string?)
   (toplevel-book-handler
    #{
      \book {
        \bookOutputSuffix #(format #f "~a-~a" print-key printable-reading-clef)
        \header {
          instrument = #(format #f "Instruments en ~a" print-key)
        }
        \score { #(build-parts high-key low-key target-pitch) }
       }
    #}))

\bookRenderingBuilder "treble" "bass" "Ut" do "Clef_Fa_Sol"  % Flûte
\bookRenderingBuilder "treble" "treble_8" "Ut" do "Clef_Sol"  % Piano, chœur
\bookRenderingBuilder "treble" "treble_8" "Sib" sib, "Clef_Sol"  % Clarinettes, trompettes
\bookRenderingBuilder "treble" "treble_8" "Mib" mib "Clef_Sol"  % Saxo
\bookRenderingBuilder "alto" "alto_8" "Ut" do "Clef_Alto"  % Violon alto
\bookRenderingBuilder "treble" "treble_8" "Fa" fa "Clef_Sol"  % Cor
1 « J'aime »

Merci beaucoup Jean! Effectivement c'est beauuuucoup plus simple ! :+1:
(au passage je ne savais pas que la commande \layout {} était optionnelle...)

Il n'y a que pour le bloc \midi qu'il a fallu que je ruse, puisque #(build-parts...) attend un ly:pitch. Mais comme je le génère toujours non transposé, je lui passe juste un (ly:make-pitch -1 0 0), et ça fait la rue Michel (oui je ne sais pas pourquoi, si je lui passe (0 0 0) ça transpose d'une octave vers le bas...).

Un immense merci encore! :slightly_smiling_face:

1 « J'aime »