Jump to content

Time mark engraver: Difference between revisions

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

Latest revision as of 07:09, 31 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.).

Upgrade notice: This snippet must be updated with the convert-ly script if used with a LilyPond version newer than 2.24.

\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
}