Jump to content

Time mark engraver: Difference between revisions

From LilyPond wiki
m Lemzwerg moved page Time Mark Engraver to Time mark engraver without leaving a redirect
Make the snippet display its result
Line 1: Line 1:
When working with LilyPond’s MIDI output, it can be tedious to find the exact '''elapsed time from the start of the score to an arbitrary point'''—especially when the music contains '''multiple tempo changes'''. Counting measures and doing tempo math is error-prone; inspecting the MIDI file manually is possible but time-consuming.
When working with LilyPond’s MIDI output it can be tedious to find the exact ‘elapsed time from the start of the score to an arbitrary point’ – especially when the music contains multiple tempo changes. Counting measures and doing tempo math is error-prone; inspecting the MIDI file manually is possible but time consuming.


To solve this, Jean wrote a small '''custom engraver''' that allows you to insert '''time marks''' directly into the music. At any point in the score you can add <code>\timeMark</code>, and LilyPond will compute the elapsed time up to that position and display it (2m15s e.g.).
This snippets introduces a Scheme engraver that allows you to insert time marks directly into the music. At any point in the score you can add <code>\timeMark</code>, and LilyPond computes the elapsed time up to that position and displays it (for example, ‘2m15s’).


=== Use case ===
This is useful when you need to
This is useful when you need to:


* align score events with audio/video cues,
* align score events with audio or video cues,
* locate timestamps for MIDI-driven playback or mockups,
* locate timestamps for MIDI-driven playback or mockups,
* find “time since start” at a few key musical points without counting or math,
* find “time since start” at a few musical key positions without counting or math, or
* work reliably across complex tempo maps (rit., accel., metric modulations, etc.).
* work reliably across complex tempo maps (rit., accel., metric modulations, etc.).


\version "2.24.4"
<lilypond version="2.24">
% source: https://lists.gnu.org/archive/html/lilypond-user/2024-03/msg00127.html
% LSR source: https://lists.gnu.org/archive/html/lilypond-user/2024-03/msg00127.html
% credits: 2024 - Jean Abou Samra
% LSR credits: 2024 - Jean Abou Samra
 
#(define (Custom_engraver!! context)
#(define (Elapsed_time_engraver context)
    (define (format-time seconds) ; ex.: 2m15s
  (define (format-time seconds) ;; example: 2m15s
      (let ((minutes (euclidean-quotient seconds 60))
    (let ((minutes (euclidean-quotient seconds 60))
            (rest (euclidean-remainder seconds 60)))
          (rest (euclidean-remainder seconds 60)))
        (string-append (if (zero? minutes) "" (format #f "~am" minutes))
      (string-append (if (zero? minutes) "" (format #f "~am" minutes))
                      (format #f "~as" (round rest)))))
                      (format #f "~as" (round rest)))))
    (let ((wholes-per-minute 15)
 
          (last-time ZERO-MOMENT)
  (let ((wholes-per-minute 15)
          (total-time 0)
        (last-time ZERO-MOMENT)
          (marks '()))
        (total-time 0)
      (make-engraver
        (marks '()))
      ((process-music engraver)
    (make-engraver
        (let* ((new-time (ly:context-current-moment context))
      ((process-music engraver)
              (time-delta (ly:moment-main (ly:moment-sub new-time last-time)))
      (let* ((new-time (ly:context-current-moment context))
              (new-wholes-per-minute
              (time-delta (ly:moment-main
                (and=> (ly:context-property context 'tempoWholesPerMinute #f)
                          (ly:moment-sub new-time last-time)))
                        ly:moment-main)))
              (new-wholes-per-minute
          (set! total-time
              (and=> (ly:context-property context
                (+ total-time (* 60 (/ time-delta wholes-per-minute))))
                                          'tempoWholesPerMinute #f)
          (set! last-time new-time)
                      ly:moment-main)))
          (when new-wholes-per-minute
        (set! total-time
            (set! wholes-per-minute new-wholes-per-minute))))
              (+ total-time (* 60 (/ time-delta wholes-per-minute))))
      (acknowledgers
        (set! last-time new-time)
        ((text-mark-interface engraver grob source-engraver)
        (when new-wholes-per-minute
        (set! marks (cons grob marks))))
          (set! wholes-per-minute new-wholes-per-minute))))
      ((process-acknowledged engraver)
 
        (for-each (lambda (grob)
      (acknowledgers
                    (when (assq-ref (ly:grob-property grob 'details) 'time-mark)
      ((text-mark-interface engraver grob source-engraver)
                      (ly:grob-set-property! grob 'text (format-time  
        (set! marks (cons grob marks))))
total-time))))
 
                  marks)
      ((process-acknowledged engraver)
        (set! marks '())))))
      (for-each (lambda (grob)
                  (when (assq-ref (ly:grob-property grob 'details)
\layout {
                                  'time-mark)
  \context {
                    (ly:grob-set-property!
    \Score
                      grob 'text (format-time total-time))))
    \consists #Custom_engraver!!
                marks)
  }
      (set! marks '())))))
}
 
 
timeMark = \tweak details.time-mark ##t \tweak color "red" \textEndMark  
\layout {
"Abracadabra"
  \context {
    \Score
{
    \consists #Elapsed_time_engraver
  c'1
  }
  \timeMark
}
  \tempo 4 = 120
 
  c'4 8. 16 2
% The argument of the `\textEndMark` here is just a placeholder
  \timeMark
% that the engraver replaces with a time stamp (because the
  \tempo 4 = 180
% `time-mark` subproperty is set).
  c'2 2
timeMark = \tweak details.time-mark ##t
  \timeMark
          \tweak color "red"
  \repeat unfold 180 c'4
          \textEndMark ""
  \timeMark
 
}
{
  c'1 | \timeMark
  \tempo 4 = 120 c'4 8. 16 2 | \timeMark
  \tempo 4 = 180 \repeat unfold 4 c'2 | \timeMark
  \repeat unfold 180 c'4 | \timeMark
}
</lilypond>
 
[[Category:Automatic notation]]
[[Category:Contexts and engravers]]
[[Category:Editorial annotations]]
[[Category:Midi]]
[[Category:Scheme]]
[[Category:Snippet]]

Revision as of 11:38, 27 December 2025

When working with LilyPond’s MIDI output it can be tedious to find the exact ‘elapsed time from the start of the score to an arbitrary point’ – especially when the music contains multiple tempo changes. Counting measures and doing tempo math is error-prone; inspecting the MIDI file manually is possible but time consuming.

This snippets introduces a Scheme engraver that allows you to insert time marks directly into the music. At any point in the score you can add \timeMark, and LilyPond computes the elapsed time up to that position and displays it (for example, ‘2m15s’).

This is useful when you need to

  • align score events with audio or video cues,
  • locate timestamps for MIDI-driven playback or mockups,
  • find “time since start” at a few musical key positions without counting or math, or
  • work reliably across complex tempo maps (rit., accel., metric modulations, etc.).

\version "2.24"

% LSR source: https://lists.gnu.org/archive/html/lilypond-user/2024-03/msg00127.html
% LSR credits: 2024 - Jean Abou Samra

#(define (Elapsed_time_engraver context)
   (define (format-time seconds) ;; example: 2m15s
     (let ((minutes (euclidean-quotient seconds 60))
           (rest (euclidean-remainder seconds 60)))
       (string-append (if (zero? minutes) "" (format #f "~am" minutes))
                      (format #f "~as" (round rest)))))

   (let ((wholes-per-minute 15)
         (last-time ZERO-MOMENT)
         (total-time 0)
         (marks '()))
     (make-engraver
      ((process-music engraver)
       (let* ((new-time (ly:context-current-moment context))
              (time-delta (ly:moment-main
                           (ly:moment-sub new-time last-time)))
              (new-wholes-per-minute
               (and=> (ly:context-property context
                                           'tempoWholesPerMinute #f)
                      ly:moment-main)))
         (set! total-time
               (+ total-time (* 60 (/ time-delta wholes-per-minute))))
         (set! last-time new-time)
         (when new-wholes-per-minute
           (set! wholes-per-minute new-wholes-per-minute))))

      (acknowledgers
       ((text-mark-interface engraver grob source-engraver)
        (set! marks (cons grob marks))))

      ((process-acknowledged engraver)
       (for-each (lambda (grob)
                   (when (assq-ref (ly:grob-property grob 'details)
                                   'time-mark)
                     (ly:grob-set-property!
                      grob 'text (format-time total-time))))
                 marks)
       (set! marks '())))))


\layout {
  \context {
    \Score
    \consists #Elapsed_time_engraver
  }
}

% The argument of the `\textEndMark` here is just a placeholder
% that the engraver replaces with a time stamp (because the
% `time-mark` subproperty is set).
timeMark = \tweak details.time-mark ##t
           \tweak color "red"
           \textEndMark ""

{
  c'1 | \timeMark
  \tempo 4 = 120 c'4 8. 16 2 | \timeMark
  \tempo 4 = 180 \repeat unfold 4 c'2 | \timeMark
  \repeat unfold 180 c'4 | \timeMark
}