10

Please excuse the vague question title. I don't know how what I need is called in Lilypond - the two words (macros/templates) are what I'd call it in programming context.

I have a Lilypond document like this:

title = "title1"
music = { \relative { c c c c } \addlyrics { this is some text } }
% song on one page
\bookpart { \header { title = \title } \score { \music \layout { } } }
% midi file creation
\book { \bookOutputName \title \score { \unfoldRepeats \music \midi { } } }

title = "title2"
music = { \relative { d d d d } \addlyrics { here o -- ther text } }
% song on one page
\bookpart { \header { title = \title } \score { \music \layout { } } }
% midi file creation
\book { \bookOutputName \title \score { \unfoldRepeats \music \midi { } } } 

title = "title3"
music = { \relative { e e e e } \addlyrics { what -- e -- ver text } }
% song on one page
\bookpart { \header { title = \title } \score { \music \layout { } } }
% midi file creation
\book { \bookOutputName \title \score { \unfoldRepeats \music \midi { } } }

There's a lot of code repetition: For each title and musicAndLyrics I have the same code. I want to put that code into a macro/template and reuse that for each pair of title and musicAndLyrics. Like this:

% define macro. NOTE THAT I DO NOT KNOW THE SYNTAX. THIS IS PSEUDOCODE!
macro(foo, bar) =
{
    % song on one page
    \bookpart { \header { title = \foo } \score { \bar \layout { } } }
    % midi file creation
    \book { \bookOutputName \foo \score { \unfoldRepeats \bar \midi { } } }
}

title = "title1"
music = { \relative { c c c c } \addlyrics { this is some text } }
% call macro with above variables as content
macro(title, music)

title = "title2"
music = { \relative { d d d d } \addlyrics { here o -- ther text } }
% call macro with above variables as content
macro(title, music)  

title = "title3"
music = { \relative { e e e e } \addlyrics { what -- e -- ver text } }
% call macro with above variables as content
macro(title, music)

Is there a way to achieve this? If yes, how? How is that concept called in Lilypond? I searched Lilypond snippets that contained "template", "macro", "variable" and "function" in their content or comments, but could find no example where something like \bookpart, \header etc. were used in the way I intend.


Bonus question:

Can we write a macro based on arrays in order to remove the last bit of code repetition? Like this:

% define macro. NOTE THAT I DO NOT KNOW THE SYNTAX.
%    THIS IS (C#/JAVA-based) PSEUDOCODE!
% assumption: the two arrays have the same length.
macro(foo[], bar[]) =
{
  for (int i = 0; i < foo.length; i++)
  {
    % song on one page
    \bookpart { \header { title = \foo[i] } \score { \bar[i] \layout { } } }
    % midi file creation
    \book { \bookOutputName \foo[i] \score { \unfoldRepeats \bar[i] \midi { } } }
  }
}

title[1] = "title1"
music[1] = { \relative { c c c c } \addlyrics { this is some text } }

title[2] = "title2"
music[2] = { \relative { d d d d } \addlyrics { here o -- ther text } }

title[3] = "title3"
music[3] = { \relative { e e e e } \addlyrics { what -- e -- ver text } }

% call macro with above variables as content
macro(title, music)

Side question: Would you put that question somewhere else, like StackOverflow or Tex Stackexchange? Where do you think the chance for a good answer is highest?

3 Answers 3

5

This is not quite the programming-heavy answer you might be looking for, but a workaround that is very easy in LilyPond is to use \include. (See the Learning manual at 3.4.5 Scores and parts and 4.7.3 Style sheets.) You can create a template file that uses your score information, and then reuse that as many times as you need. Just \include it after the code that it is calling:

% File called score.ily

 \bookpart {
   \header {
     title = \title
   }
   \score {
     \music
   }
   \layout {}
 }

% File called compile-this.ly

title = "title1"
music = \relative c' { do re mi }
\include "./score.ily"

title = "title2"
music = \relative c { fa sol la }
\include "./score.ily"
11

Lilypond allows you to create Scheme functions that can process and output Lilypond variables.

Lilypond variables

You can define Lilypond variables to insert a block of code somewhere else. They don't really make this clear in the documentation, but each variable must have one of several allowed types. They can't just be arbitrary text like a macro.

So you can do like this:

title = \markup "Title"
music = { c'4 d'4 c'4 }
lyrics = \lyricmode { Ex -- am -- ple }

\book {
   \header { title = \title }
   \score { { \music } \addlyrics { \lyrics } }
}

Scheme expressions and functions

In pure Scheme, you define expressions that return the body.

(define title "Title")
$> (title) => "Title"

You can define expressions that take arguments and insert them into the body.

(define hello
   (lambda (name)
      (string-append "hello, " name)))
$> (hello "world") => "hello, world"

Scheme functions in Lilypond

Lilypond provides you with the awkward solution of using a highly modified version of Scheme within Lilypond and mixing the two languages' syntax. In Lilypond's version of Scheme, all Scheme expressions are preceded by #. Instead of simple define you "get" to use define-scheme-function. Instead of (define fn (lambda (arg1 arg2) body)) now you write this:

fn = #(define-scheme-function
        (arg1 arg2)
        (type-of-arg1? type-of-arg2?)
        body)

Most of the time for the body you will want to write Lilypond code, not its even crazier Scheme translation, so you can put it in a code block #{ ... #}. Then when you want to insert the arguments, you put a dollar sign in front of them ($arg1).

So here is a way to do more or less what you are asking. Note that you have to be specific about what kind of variables you are giving the function.

\version "2.23"

% Function 'makebook': Make a \book.
%   INPUT:  title (Lilypond \markup expression), 
%           music (Lilypond music expression inside curly braces {})
%           words (Lilypond \lyricmode expression)
%   RETURNS: A \book expression with the given contents.
makebook = 
#(define-scheme-function 
   (title music words)
   (markup? ly:music? ly:music?)
      #{
\book { 
    \header { title = $title } 
    \score { 
        \new Staff 
          <<
            \new Voice { $music }
            \new Lyrics { $words }
          >>
        }
    }
#})

% Lilypond-centered approach 
title = \markup "title1"
music = { c'4 c'4 c'4 c'4 } 
words = \lyricmode { this is some text }

% Call the Scheme function with the Lilypond variables defined above
#(makebook title music words)

Is there a better way?

I learned Scheme so that I could use Lilypond at a more advanced level. I fell in love with Scheme and with functional programming in general. But using Scheme in Lilypond is extremely unpleasant. You aren't really using normal Scheme, you're using their own dialect of it. Scheme itself uses dynamic typing so you can be flexible about arguments. Compared to that, or even more, LaTeX, where you can put just about anything into a macro and do just about anything with it, in Lilypond-Scheme you have do be very fussy about variable types and only certain expressions are allowed. You end up with a messy mishmash. The alternative is a lot of boilerplate Lilypond code, and it's hard to reduce that even using Scheme.

Another approach is to use Scheme itself, or really any other language, to generate your Lilypond code. All you want to do is duplicate blocks of text in the input file. In fact you could use the m4 macro processor to do it:

File lytemplate.m4:

define(makebook,
\version "2.23"
\book { 
    \header { title = $1 } 
    \score { 
        \new Staff 
          <<
            \new Voice { $2 }
            \new Lyrics { $3 }
          >>
        }
    }
)
define(mytitle, \markup "title1")
define(mymusic, { c'4 c'4 c'4 c'4 })
define(mywords, \lyricmode { this is some text })
makebook(mytitle, mymusic, mywords)

Then you can run m4 lytemplate.m4 > lytemplate.ly && lilypond lytemplate.

Response to bonus question about iterating through a list of data

This is a good example of something it seems like you should be able to do with "Scheme-in-Lilypond". I found it easy to do in Scheme but I could not get it to work within Lilypond.

As a Scheme program, this just generates the strings for the Lilypond code. There are various ways you can make a list of your data (here I use an association list) and them map a string-making function over that list. You can run this with the Guile interpreter. (But you might as well use any other language you already know if you're going to go this route.)

(define make-book
  (lambda (title music words)
  "Return a string with the Lilypond code for a book, given title, music, and lyrics"
    (string-append
      "\\book { \\header { title = \\markup \"" title "\" }\n" 
      "\\score { \\new Staff << \\new Voice { " music " }\n" 
      "\\new Lyrics { \\lyricmode { " words " } >> } }\n")))

(define get-song
    (lambda (alist) 
    "Call make-book with the song data from an association list"
    (let ((title (assq-ref alist 'title)) 
          (music (assq-ref alist 'music)) 
          (words (assq-ref alist 'words))) 
      (make-book title music words))))

(define make-songs
  (lambda (ls)
  "Return a string with the Lilypond code for a given list of songs"
    (let ((song-strings (map get-song ls)))
      (string-concatenate song-strings))))

(define songs
  '(((title . "title1")
     (music . "c'4 c'4")
     (words . "word one"))
    ((title . "title2")
     (music . "d'4 d'4")
     (words . "word two"))
    ((title . "title3")
     (music . "e'4 e'4")
     (words . "word three"))))

(format #t (make-songs songs))
5
  • 1
    I just tried your example macro in section Scheme functions in Lilypond. I added \layout { } below the \new Staff ... command in order to get a pdf. It compiles, but creates no pdf. I changed #(makebook title music words) to $(makebook title music words), but then I got error Unbound variable: words. How do I make a pdf from this example?
    – Kjara
    Commented Sep 11, 2021 at 4:32
  • Since you have a lot of experience with scheme and scheme in Lilypond, do you have an answer to my comment under Aaron's answer?
    – Kjara
    Commented Sep 11, 2021 at 4:44
  • That example already makes a PDF, no changes needed. You have to have the # before any Scheme expression. Commented Sep 12, 2021 at 8:41
  • I think the real problem is errors in your Lilypond code. A book part is supposed to be inside a book. lilypond.org/doc/v2.22/Documentation/notation/file-structure Commented Sep 12, 2021 at 8:51
  • 1
    Thanks for this excellent explanation. Much better than the Lilypond Scheme Manual that others linked to, that I have read and explored without finding an answer. I wish I had found your explanation earlier! (In the manual, calling the arguments is done with a #, which doesn't work. Switching to a $ made it.) Thanks again. Commented Dec 20, 2021 at 16:44
2

LilyPond can be extended using Scheme. So you can create a scheme function of appropriate variables to output the sort of code you want.

The LilyPond Scheme Manual is the place to begin.

3
  • I was able to create a function in scheme that returns the bookpart (song on one page) and another function that returns the book (midi file creation). But I was not able to return both in one function. Is that even possible? A function by definition returns ONE value, but \bookpart { ... } \book { ... } are two things.
    – Kjara
    Commented Sep 9, 2021 at 12:21
  • @Kjara I'm working on it myself, but haven't got it yet.
    – Aaron
    Commented Sep 9, 2021 at 12:52
  • 1
    Gather them in a list, and replace # with the list splicing operator #@ . Some examples here: extending-lilypond.readthedocs.io/en/latest/… Commented Sep 11, 2021 at 18:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.