For frequently repeating strophic music, the problem of where to put different words that follow the same music gets harder and harder. For through–composed music, repeats are not a problem as new words will have new music. Aside from eliminating repeats which creates unacceptable redundancy, we have two basic options: displaying the new verse under the same music or displaying extra stanzas under the music.
Displaying the new verse under the same music:
However, as subsequent verses get farther away from the original music, any advantage gained from proximity to the music is quickly lost. Alternatively, the music can be completely separated from the text in stanzas placed at the bottom. The reader either memorises the melody or repeatedly scans up and down to collate the words and music. This approach:
This approach is most common and is for obvious reasons the bane of choral scholars whose eyes must do a lot of work when faced with both an unfamiliar text and an unfamiliar hymn tune to sightread. Both approaches have their pros and cons but to truly create dynamic editions both must be possible. The problem is how to interface one to the other as they are constructed completely differently. Inline lyrics are formatted as such:
{
\relative c' {
\clef "treble_8"
\time 6/8
\partial 8
g8 d'4. d4( e8) f16( e d8) e d4 c8
} \addlyrics {
\set stanza = #"1. "
Be -- a -- ta __ vi -- sce -- ra, Ma
} \addlyrics {
\set stanza = #"2. "
Po -- pu -- lus __ gen -- ti -- um, se
} \addlyrics {
\set stanza = #"3. "
Fer -- men -- ti __ pes -- si -- mi, qui
} \addlyrics {
\set stanza = #"4. "
Par -- tum quem __ de -- stru -- is, Iu
} \addlyrics {
\set stanza = #"5. "
Te sem -- per __ im -- pli -- cas, er
} \addlyrics {
\set stanza = #"6. "
Le -- gis mo -- sa -- y -- ce, clau
} \addlyrics {
\set stanza = #"7. "
So -- lem quem __ li -- bre -- re, dum
}
}
Whereas block lyrics are formatted in columns like this:
\markup {
\fill-line {
\column {
\line { \bold "2."
\column {
"Populus gentium"
"sedens in tenebris..."
}
}
\combine \null \vspace #0.5
\line { \bold "3."
\column {
"Fermenti pessimi"
"qui fecam hauserant..."
}
}
}
\column {
\line { \bold "4."
\column {
"Partum quem destruis"
"Iudea misera..."
}
}
\combine \null \vspace #0.5
\line { \bold "5."
\column {
"Te semper implicas"
"errore patrio..."
}
}
}
\column {
\line { \bold "6."
\column {
"Legis mosayce"
"clausa misteria..."
}
}
\combine \null \vspace #0.5
\line { \bold "7."
\column {
"Solem quem librere"
"dum purus otitur..."
}
}
}
}
}
Clearly if conversion is going to happen, it must be from inline to block lyrics not the other way around because the inline lyrics contain more typesetting information than the block lyrics (hyphens, extenders, etc.) By using the thrust of a Lilypond snippet which converts lyrics into lists of strings as a starting point, I am able to define some functions that, when used together, can convert sets of \lyricmode
lyrics into a dynamic set of block stanzas in an user–definable number of columns.1
The only data that is in the block lyrics that is not encoded in the inline lyrics is the location of line breaks. Before doing anything we must markup up our lyrics with the 'LineBreakEvent
from the snippet:
nl = #(make-music 'LineBreakEvent)
voiceonelyricsvone = \lyricmode {
Be -- a -- ta __ vi -- sce -- ra __ \nl
Ma -- ri -- e __ vir -- gi -- nis. __ \nl
Cu -- ius __ ad __ u -- be -- ra __ \nl
rex ma -- gni __ no -- mi -- nis. __ \nl
Ve -- ste sub al -- te -- ra \nl
vim -- cel -- lans __ no -- mi -- nis. \nl
Dic -- ta -- vit __ fe -- de -- ra \nl
de -- i et __ ho -- mi -- nis.
}
The first step is to convert the marked–up lyrics into pure text. The function below (which has been lifted verbatim from the snippet) extracts the text for each lyric-event
(i.e. each syllable) and places it in a list. If the 'articulations
property contains an 'hyphen-event
then it returns true
along with the syllable, indicating that this syllable needs to be joined to the next syllable to make a word:
#(define linebreakindicator "\\")
#(define (lyrics->list lyrics)
"Return only syllables and hyphens from @code{lyrics}."
(if (ly:music? lyrics)
(cond
((music-is-of-type? lyrics 'lyric-event)
(let* ((art (ly:music-property lyrics 'articulations))
(hyphen?
(not (null?
(filter
(lambda (m)
(music-is-of-type? m 'hyphen-event))
art))))
(text (ly:music-property lyrics 'text)))
(if hyphen? (list text hyphen?) (list text))))
((music-is-of-type? lyrics 'line-break-event)
(list linebreakindicator))
(else
(let ((elt (ly:music-property lyrics 'element))
(elts (ly:music-property lyrics 'elements)))
(if (ly:music? elt)
(lyrics->list elt)
(if (null? elts)
'()
(map
(lambda(x) (lyrics->list x))
elts))))))
'()))
\displayScheme #(lyrics->list voiceonelyricsvone)
yields:
(list (list "Be" #t) (list "a" #t) (list "ta") (list "vi" #t) (list "sce" #t) (list "ra") (list "\\") (list "Ma" #t) (list "ri" #t) (list "e") (list "vir" #t) (list "gi" #t) (list "nis.") (list "\\") (list "Cu" #t) (list "ius") (list "ad") (list "u" #t) (list "be" #t) (list "ra") (list "\\") (list "rex") (list "ma" #t) (list "gni") (list "no" #t) (list "mi" #t) (list "nis.") (list "\\") (list "Ve" #t) (list "ste") (list "sub") (list "al" #t) (list "te" #t) (list "ra") (list "\\") (list "vim" #t) (list "cel" #t) (list "lans") (list "no" #t) (list "mi" #t) (list "nis.") (list "\\") (list "Dic" #t) (list "ta" #t) (list "vit") (list "fe" #t) (list "de" #t) (list "ra") (list "\\") (list "de" #t) (list "i") (list "et") (list "ho" #t) (list "mi" #t) (list "nis."))
This syllable–joining is done in the function reduce-hyphens
which I rewrote from the same snippet to deal more kindly with text. Starting with an empty string, it goes along the list of syllables and appends them to the string, adding a space if the second element in the list is not true
.
#(define (reduce-hyphens text)
(fold (lambda (str prev)
(string-append prev
(if
(and
(= (length str) 2)
(equal? (car (cdr str)) #t)
)
(car str)
(string-append (car str) " ")
)
)
) "" text)
)
\displayScheme #(reduce-hyphens (lyrics->list voiceonelyricsvone))
yields:
"Beata viscera \\ Marie virginis. \\ Cuius ad ubera \\ rex magni nominis. \\ Veste sub altera \\ vimcellans nominis. \\ Dictavit federa \\ dei et hominis. "
Finally, we turn this long string into a list of strings by splitting the string at the line break indicator, \\
:
#(define (verse rawtext)
(map (lambda (a) (string-split (reduce-hyphens a) #\space))
(split-list-by-separator (lyrics->list rawtext)
(lambda (a)
(and (not (null? a)) (equal? (car a) linebreakindicator)))))
)
\displayScheme #(verse voiceonelyricsvone)
gives us:
(list (list "Beata" "viscera" "")
(list "Marie" "virginis." "")
(list "Cuius" "ad" "ubera" "")
(list "rex" "magni" "nominis." "")
(list "Veste" "sub" "altera" "")
(list "vimcellans" "nominis." "")
(list "Dictavit" "federa" "")
(list "dei" "et" "hominis." ""))
Which is ready to plug into a markup function, which we will create in Part II.
Let's first take a look at how the manually marked–up lyrics look under the surface with:
\displayScheme \markup {
\fill-line {
\column {
\line {
\bold "2."
\column {
"Populus gentium"
"sedens in tenebris..."
}
}
}
}
}
which outputs:
(markup
#:line
(#:fill-line
(#:column
(#:line
(#:bold
"2."
#:column
(#:simple
"Populus gentium"
#:simple
"sedens in tenebris..."))))))
Working backwards from this, we need to take the list of strings, put them in a column, prepend a bold number in a line, put all of those in a column and then format them into a line. No mean feat. Furthermore, we should wrap each column in a word–wrap to ensure that when many columns are formatted, long lines wrap around and don't overlap other columns. For each word–wrap, we need to know the column's width. We can use this function I wrote a few months ago to calculate the proportion of line–with for each column:
#(define getlinewidth
(lambda (paper columns)
(let* (
(landscape (ly:output-def-lookup paper 'landscape))
(output-scale (ly:output-def-lookup paper 'output-scale))
(paper-width (ly:output-def-lookup paper 'paper-width))
(paper-height (ly:output-def-lookup paper 'paper-height))
(indent (ly:output-def-lookup paper 'indent))
(plain-line-width (ly:output-def-lookup paper 'line-width))
(plain-left-margin (ly:output-def-lookup paper 'left-margin))
(w (if landscape paper-height paper-width))
(left-margin (if (null? plain-left-margin)
(if (null? plain-line-width)
10
(/ (- w plain-line-width) 2))
plain-left-margin))
(line-width (if (null? plain-line-width)
(- w (* (* (+ columns 1) 2) left-margin))
plain-line-width)))
(/ line-width (+ columns 1))
)))
We can then make each stanza by overriding its line–width with the output from this function, and adding some vertical space to the end:
#(define list-index
(lambda (e lst)
(if (null? lst)
-1
(if (equal? (car lst) e)
0
(if (= (list-index e (cdr lst)) -1)
-1
(+ 1 (list-index e (cdr lst))))))))
#(define (make-stanza n c paper text vspc)
"Format a stanza into a column with a number"
(markup (make-line-markup (list
(markup (#:bold (string-append (number->string (+ n 1)) ".")))
(markup (make-column-markup
(append (map
(lambda (l)
(markup (#:override (cons (quote line-width) (getlinewidth paper c))
(markup (make-wordwrap-markup l)))))
text) (list (markup (#:vspace vspc))))))))))
\displayScheme #(make-stanza 0 3 #{ \paper { } #} (verse voiceonelyricsvone) 0.5)
yields markup that starts:
(markup
#:line
(#:line
(#:line
(#:bold "1.")
#:line
(#:column
(#:line
(#:override
(cons (quote line-width) 32.5)
(#:line
(#:wordwrap
(#:simple "Beata" #:simple "viscera" #:simple ""))))
We do this for all of the stanzas and put them into a list:
#(define (getstanzalist lofl noffset c paper vspc)
"Format list of lists of strings into stanzas"
(map
(lambda (s)
(markup (make-stanza (+ (list-index s lofl) noffset) c paper s vspc
))
) lofl)
)
Finally, we can take a roughly equal number of stanzas and put each set into a column:
#(define-markup-command (stanzacols layout props n off vspc lofl) (integer? integer? number? list?)
(interpret-markup layout props
(make-fill-line-markup
(map
(lambda (p)
(let*
(
(vlist (getstanzalist lofl off n layout vspc))
(floorvs (ceiling (/ (length vlist) n)))
(vsused (* floorvs p))
(vsleft (list-tail vlist (min vsused (length vlist))))
(vshead (if (and (= (+ p 1) n) (< (length vsleft) n)) vsleft (list-head vsleft (min floorvs (length vsleft)))))
)
(make-center-column-markup
vshead))) (iota n))
)
)
)
Use it like this: \markup \stanzacols #3 #0 #1 #(list (verse voiceonelyricsvone) (verse voiceonelyricsvtwo) (verse voiceonelyricsvthree) (verse voiceonelyricsvfour) (verse voiceonelyricsvfive) (verse voiceonelyricsvsix) (verse voiceonelyricsvseven))
Full code:
#(define linebreakindicator "\\")
nl = #(make-music 'LineBreakEvent)
#(define (lyrics->list lyrics)
"Return only syllables and hyphens from @code{lyrics}."
(if (ly:music? lyrics)
(cond
((music-is-of-type? lyrics 'lyric-event)
(let* ((art (ly:music-property lyrics 'articulations))
(hyphen?
(not (null?
(filter
(lambda (m)
(music-is-of-type? m 'hyphen-event))
art))))
(text (ly:music-property lyrics 'text)))
(if hyphen? (list text hyphen?) (list text))))
((music-is-of-type? lyrics 'line-break-event)
(list linebreakindicator))
(else
(let ((elt (ly:music-property lyrics 'element))
(elts (ly:music-property lyrics 'elements)))
(if (ly:music? elt)
(lyrics->list elt)
(if (null? elts)
'()
(map
(lambda(x) (lyrics->list x))
elts))))))
'()))
#(define (reduce-hyphens text)
(fold (lambda (str prev)
(string-append prev
(if
(and
(= (length str) 2)
(equal? (car (cdr str)) #t)
)
(car str)
(string-append (car str) " ")
)
)
) "" text)
)
#(define (verse rawtext)
(map (lambda (a) (string-split (reduce-hyphens a) #\space))
(split-list-by-separator (lyrics->list rawtext)
(lambda (a)
(and (not (null? a)) (equal? (car a) linebreakindicator)))))
)
voiceonelyricsvone = \lyricmode {
Be -- a -- ta __ vi -- sce -- ra __ \nl
Ma -- ri -- e __ vir -- gi -- nis. __ \nl
Cu -- ius __ ad __ u -- be -- ra __ \nl
rex ma -- gni __ no -- mi -- nis. __ \nl
Ve -- ste sub al -- te -- ra \nl
vim -- cel -- lans __ no -- mi -- nis. \nl
Dic -- ta -- vit __ fe -- de -- ra \nl
de -- i et __ ho -- mi -- nis.
}
voiceonelyricsvtwo = \lyricmode {
Po -- pu -- lus __ gen -- ti -- um __ \nl
se -- dens __ in __ te -- ne -- bris. __ \nl
Sur -- git __ ad __ gau -- di -- um __ \nl
par -- tus __ tam __ ce -- le -- bris. __ \nl
Iu -- de -- a te -- di -- um \nl
fo -- vet in __ la -- te -- bris. \nl
Cor ge -- rens __ con -- sci -- um \nl
de -- li -- cet __ fu -- ne -- bris. \nl
}
voiceonelyricsvthree = \lyricmode {
Fer -- men -- ti __ pes -- si -- mi __ \nl
qui fe -- cam __ hau -- se -- rant. __ \nl
Ad __ pa -- nis __ a -- zi -- mi __ \nl
pro -- mi -- sa __ pro -- pe -- rant. __ \nl
Sunt De -- o pro -- xi -- mi \nl
qui lon -- ge __ ste -- te -- rant. \nl
Et hi __ no -- vis -- si -- mi \nl
qui pri -- mi __ fu -- e -- rant. \nl
}
voiceonelyricsvfour = \lyricmode {
Par -- tum __ quem __ de -- stru -- is __ \nl
Iu -- de -- a __ mi -- se -- ra. __ \nl
De __ quo __ nos __ ar -- gu -- es __ \nl
quem do -- cet __ lit -- te -- ra. __ \nl
Si no -- va re -- spu -- is \nl
cre -- de vel __ vet -- te -- ra. \nl
In hoc __ quem __ a -- stru -- is \nl
Chri -- stum con -- si -- de -- ra. \nl
}
voiceonelyricsvfive = \lyricmode {
Te __ sem -- per __ im -- pli -- cas __ \nl
er -- ro -- re __ pa -- tri -- o. __ \nl
Dum __ vi -- am __ in -- di -- cas __ \nl
er -- rans __ in __ in -- vi -- o. __ \nl
In his que pre -- di -- cas \nl
ster -- nis in __ me -- di -- o. \nl
Ba -- ses __ pro -- phe -- ti -- cas \nl
sub e -- van -- ge -- li -- o.
}
voiceonelyricsvsix = \lyricmode {
Le -- gis __ mo -- sa -- y -- ce __ \nl
clau -- sa __ mi -- ste -- ri -- a. __ \nl
Nux __ vir -- ge __ my -- sti -- ce __ \nl
na -- tu -- re __ ne -- sci -- a. __ \nl
A -- qua de si -- li -- ce \nl
co -- lum -- pna __ pre -- vi -- a. \nl
Pro -- lis __ do -- mi -- ni -- ce \nl
Si -- gna sunt __ pro -- pe -- ra.
}
voiceonelyricsvseven = \lyricmode {
So -- lem __ quem __ li -- bre -- re __ \nl
dum pu -- rus __ o -- ti -- tur. __ \nl
In __ au -- ra __ cer -- ne -- re __ \nl
vi -- sus __ non __ pa -- ti -- tur. __ \nl
Cer -- nat a la -- te -- re \nl
dum re -- per -- cu -- ti -- tur. \nl
Al -- vus __ pu -- er -- pe -- re \nl
qua to -- tus __ clau -- di -- tur.
}
#(define list-index
(lambda (e lst)
(if (null? lst)
-1
(if (equal? (car lst) e)
0
(if (= (list-index e (cdr lst)) -1)
-1
(+ 1 (list-index e (cdr lst))))))))
#(define getlinewidth
(lambda (paper columns)
(let* (
(landscape (ly:output-def-lookup paper 'landscape))
(output-scale (ly:output-def-lookup paper 'output-scale))
(paper-width (ly:output-def-lookup paper 'paper-width))
(paper-height (ly:output-def-lookup paper 'paper-height))
(indent (ly:output-def-lookup paper 'indent))
(plain-line-width (ly:output-def-lookup paper 'line-width))
(plain-left-margin (ly:output-def-lookup paper 'left-margin))
(w (if landscape paper-height paper-width))
(left-margin (if (null? plain-left-margin)
(if (null? plain-line-width)
10
(/ (- w plain-line-width) 2))
plain-left-margin))
(line-width (if (null? plain-line-width)
(- w (* (* (+ columns 1) 2) left-margin))
plain-line-width)))
(/ line-width (+ columns 1))
)))
#(define (make-stanza n c paper text vspc)
"Format a stanza into a column with a number"
(markup (make-line-markup (list
(markup (#:bold (string-append (number->string (+ n 1)) ".")))
(markup (make-column-markup
(append (map
(lambda (l)
(markup (#:override (cons (quote line-width) (getlinewidth paper c))
(markup (make-wordwrap-markup l)))))
text) (list (markup (#:vspace vspc))))))))))
#(define (getstanzalist lofl noffset c paper vspc)
"Format list of lists of strings into stanzas"
(map
(lambda (s)
(markup (make-stanza (+ (list-index s lofl) noffset) c paper s vspc
))
) lofl)
)
#(define-markup-command (stanzacols layout props n off vspc lofl) (integer? integer? number? list?)
(interpret-markup layout props
(make-fill-line-markup
(map
(lambda (p)
(let*
(
(vlist (getstanzalist lofl off n layout vspc))
(floorvs (ceiling (/ (length vlist) n)))
(vsused (* floorvs p))
(vsleft (list-tail vlist (min vsused (length vlist))))
(vshead (if (and (= (+ p 1) n) (< (length vsleft) n)) vsleft (list-head vsleft (min floorvs (length vsleft)))))
)
(make-center-column-markup
vshead))) (iota n))
)
)
)
\markup \stanzacols #3 #0 #1 #(list (verse voiceonelyricsvone) (verse voiceonelyricsvtwo) (verse voiceonelyricsvthree) (verse voiceonelyricsvfour) (verse voiceonelyricsvfive) (verse voiceonelyricsvsix) (verse voiceonelyricsvseven))