7

Question:

Is there a way to change how the score title appears based on whether it’s on an odd or even page?

Given:

% --- paper.ily
\paper {
  scoreTitleMarkup = \markup \column {
    \fill-line {
      \box \fromproperty #'header:order
      \bold \fromproperty #'header:title
      \null
    }
    \fill-line {
      \null
      \italic \fromproperty #'header:author
    }
  }
}
% --- score.ily
\score {
  <<
    \new Staff \music
  >>
  \header {
    title = \title
    order = \order
    author = \author
  }
}
% --- compile.ly
title = "Good Afternoon"
order = "1"
author = "Santa"
music = {
  % ...
}
\include "./score.ily"

title = "Good Evening"
order = "2"
author = "Elves"
music = {
  % ...
}
\include "./score.ily"

title = "Good Night"
order = "3"
author = "Truman"
music = {
  % ...
}
\include "./score.ily"

Result:

two sheet music pages side-by-side, all score titles look the same

Contrived Example:

(Notice the order and author are on reversed sides on the right-hand page.)

two sheet music pages side-by-side, scores look different on odd and even pages

3 Answers 3

3

It is possible by adapting the implementation of \page-ref.

This is a bit awkward to use since you have to put a uniquely named \label before each markup block that uses \odd-even-markup, and the page number of the label, not the markup, is used for the decision. There may be a way to get the page number without a label, but I didn't find one.

Since the size of the markup is calculated before the page number is known, the odd and even markup have to be the same size (or they're padded to the same size). That probably won't be a problem in this particular use case.



\version "2.24.3"


#(define-markup-command (delayed-markup layout props factory) (procedure?)

   "Use the dimensions of the markup returned by (factory #t) for layout,
    and the markup returned by (factory #f), called later, for rendering."

   (let* ((gauge-stencil (interpret-markup layout props (factory #t)))
          (x-ext (ly:stencil-extent gauge-stencil X))
          (y-ext (ly:stencil-extent gauge-stencil Y)))
     (ly:stencil-outline
      (ly:make-stencil
       `(delay-stencil-evaluation
         ,(delay (ly:stencil-expr
                   (interpret-markup layout props
                     (make-with-dimensions-markup x-ext y-ext (factory #f))))))
       x-ext y-ext)
      (make-filled-box-stencil x-ext y-ext))))


#(define (layout-lookup-label layout label)

   "The weird label-lookup logic from page-ref."

   (define (assoc-name-get name ls)
     (do ((ls ls (cdr ls)) (result '() result))
         ((null? ls) result)
       (if (and (car ls) (eq? name (assoc-get 'name (cdar ls))))
           (set! result (cons (car ls) result)))))

   (let* ((table (ly:output-def-lookup layout 'label-page-table))
          (alist-table (ly:output-def-lookup layout 'label-alist-table))
          (retrieve-id (and (list? alist-table)
                            (let ((entry (assoc-name-get label alist-table)))
                              (and (pair? entry) (caar entry))))))
     (and (list? table)
          (assoc-get (or retrieve-id label) table))))


#(define-markup-command (page-ref-generic layout props label factory) (symbol? procedure?)

   "Use the dimensions of the markup returned by (factory #t) for layout, and
    the markup returned by (factory pagenum) for rendering, where pagenum is
    the page number of the given label or #f if the label wasn't found."

   (interpret-markup layout props
     (make-delayed-markup-markup
       (lambda (dry-run)
         (factory (or dry-run (layout-lookup-label layout label)))))))


%#(define-markup-command (page-ref layout props label gauge default) (symbol? markup? markup?)
%
%   "A reimplementation of page-ref in terms of page-ref-generic."
%
%   (interpret-markup layout props
%     (make-page-ref-generic-markup label
%       (lambda (page-number)
%         (if (boolean? page-number)
%             (if page-number gauge default)
%             (number-format (ly:output-def-lookup layout 'page-number-type) page-number))))))


#(define-markup-command (odd-even-markup layout props label ifodd ifeven) (symbol? markup? markup?)

   "Select ifodd or ifeven markup depending on the page number of the label.
    If label lookup fails, display ifodd and ifeven superimposed."

   (interpret-markup layout props
     (make-page-ref-generic-markup
       label
       (lambda (page)
         (if (boolean? page)
             (make-combine-markup ifodd ifeven)
             (if (odd? page) ifodd ifeven))))))


\label #'a  \markup \odd-even-markup #'a "odd" "even"  \pageBreak
\label #'b  \markup \odd-even-markup #'b "odd" "even"  \pageBreak
\label #'c  \markup \odd-even-markup #'c "odd" "even"  \pageBreak

To use common title markup for all scores, you could do something like this. (Edit: see Lazy's answer for a way to avoid the \remember-page-number and score-label = #last-remembered-page-label boilerplate.)

last-remembered-page-label = ##f

remember-page-number =
  #(define-music-function () ()
     (set! last-remembered-page-label (gensym))
     #{ \label #last-remembered-page-label #} )


#(define-markup-command (title-odd-even-markup layout props ifodd ifeven) (markup? markup?)

   "like \\odd-even-markup, but gets the label from the 'score-label' header property."

   (interpret-markup layout props
     (make-odd-even-markup-markup (chain-assoc-get 'header:score-label props) ifodd ifeven)))


\paper {
  scoreTitleMarkup = \markup \title-odd-even-markup "odd" "even"
}


\remember-page-number

\score {
  \header { score-label = #last-remembered-page-label }
  { c'1 }
}


\pageBreak


\remember-page-number

\score {
  \header { score-label = #last-remembered-page-label }
  { d'1 }
}
3
  • It is a clever hack, but you need to understand that this is not exactly the behavior we‘d expect. The thing is that this is not "markup depending on page number", but "a stencil depending on page number", which is probably fine for something like page number (not much going on), but if you use e.g. something like "a" \column { "b" "b" "b" } for this you‘ll see why this is still an (albeit quite clever) hack. For the means of the OP probably a usable solution.
    – Lazy
    Commented Dec 16, 2023 at 9:11
  • I’ve posted a way to get rid of the boilerplate in a separate answer (cannot put this as comment).
    – Lazy
    Commented Dec 16, 2023 at 9:49
  • @Lazy I added a link to your answer and a mention of the sizing problem.
    – benrg
    Commented Dec 16, 2023 at 18:58
2

The problem here is the order things are done. At the time this markup is created Lilypond does not know on what page it is. Rather it places all of these things on a single page and then breaks this page depending on the content.

So with the current state of affairs this is not possible. Some of the developers dream of turning these markups into grobs, which would potentially allow for a later callback changing that stuff.

Note that the header markups are not affected by this, as they need to be spaced out, but else the content cannot depend on the page, as the page would depend on the content.

But how do things like TeX do this (e.g. for references, tocs, ...)? These employ a two step approach, writing the page numbers into a separate file which then provides the number on the next run. This could be implemented around Lilypond.

For example use code like this

#(use-modules (ice-9 textual-ports))

pageref =
#(define-scheme-function (ref) (string?)
   (let ((refs (format #f "~a.pagerefs" input-file-name))
         (filler (markup #:box #:bold (format #f "%.~a.%" ref))))
     (if (file-exists? refs)
         (let* ((content (call-with-input-file refs get-string-all))
                (content (string-split content #\newline))
                (content (map (lambda (x) (string-split x #\:)) content))
                (content (filter (lambda (x) (not (or (null? x) (equal? (car x) "")))) content))
                (content (map (lambda (x) (cons (car x) (cadr x))) content))
                (page-nr (assoc-get ref content filler)))
           page-nr)
         filler)))


\markup\pageref "A"
\pageBreak
\markup\pageref "B"

and create a wrapper in whatever language you like that

  1. Runs Lilypond with svg option into a tempdir
  2. Searches the pages for %\.(\w+)\.%
  3. Enters the found pairs into a file [inputfile].pagerefs in the format label:page
  4. Reruns Lilypond with the intended format

For the above case that file would contain

A:1
B:2
4
  • I’m accepting the answer not necessarily because I comprehend it all, but because the pretty clear answer is, “No, you can’t. At least not yet.” As far as your example is concerned, do you have handy either more documentation on that, other examples, or discussions in the mailing list? It’s fine if you don’t; I at least have a direction to search.
    – Neal
    Commented Dec 15, 2023 at 15:49
  • \page-ref manages to do something very similar, so I think it's possible.
    – benrg
    Commented Dec 16, 2023 at 1:23
  • Please critique my answer.
    – benrg
    Commented Dec 16, 2023 at 2:09
  • @Neal While this answer still stands, checkout benrg’s idea, which could just work out for you. Do not forget to mark his reply as accepted if it works out for you.
    – Lazy
    Commented Dec 16, 2023 at 9:51
2

Adding to @benrq’s interesting approach:

You do not need to manually label everything and set score-labels. Instead you can change the score-handler to something that does these things for you:

#(define (collect-scores-for-book-with-label score)
  (let* ((label (gensym))
         (header (ly:score-header score))
         (header (if (null? header) #{\header {}#} header)))
    (module-define! header 'score-label label)
    (ly:score-set-header! score header)
    (collect-music-for-book #{ \label #label #})
    (collect-scores-for-book score)))

#(set! toplevel-score-handler collect-scores-for-book-with-label)

\paper {
  scoreTitleMarkup = \markup \title-odd-even-markup "a" "b"
  ragged-bottom = ##t
}

\score {
  { c'1 }
}

\pageBreak

\score {
  { d'1 }
}

Probably one would also like to adapt the other toplevel handlers, but you get the idea.

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.