Hi everybody!

I think some people might be interested in the files attached.

JSBI1.ly is a sample document (Bach Invention No. 1) that demonstrates the 
syntax.
It includes some insane tempo changes and a bad layout to force pages with a 
variable
number of systems. You'll see that despite of this video and audio keeps 
synchronized.

videohelper.ily includes some code to generate a helper file during the 
lilypond run.

mkvideo is a bash script that generates videos from the lilypond output, 
following
instructions generated during the lilypond run. mkvideo use the following tools

ls, sort, tail, uniq, grep, sed, bc, gs, pdftk, fluidsynth, sox, ffmpeg and the
 /usr/share/sounds/sf2/FluidR3_GM.sf2 sound file.

Put the attached files into a seperate directory, don't forget "chmod 700 
mkvideo",
and execute

    lilypond JSBI1
   ./mkvideo

mkvideo is pretty fast (lilypond time here approximately 1.6 seconds, video
generation time less than 9 seconds on an i4790K system)

cu,
 Knut

\version "2.11.47"
\include "articulate.ly"

% This file demonstrates how to generate sheet music and video from a single
% lilypond source file. Put it into a directory together with videohelper.ily
% and the mkvideo script, then translate it with
%
%    lilypond JSBI1
%    ./mkvideo
%
% mkvideo features:
%
%   1. mkvideo is fast as it runs processes in parallel if possible
%
%   2. mkvideo keeps music and audio synchronized even if there are
%      tempo changes in the music and if the number of systems per
%      page is not constant
%

voiceone = \relative c' {
  \set Score.tempoHideNote = ##f
  \tempo 4=80 
  r16 c[d e] f[d e c] g'8[c b^\prall c] d16[g, a b] c[a b g] d'8[g f^\prall g]
  e16[a g f] e[g f a] g[f e d] c[e d f] e[d c b] a[c b d] c[b a g] fis[a g b]
  \tempo 4=60 
  a8[d,] c'8.[^\mordent d16] b[a g fis] e[g fis a] g[b a c] b[d c e]
  d[b32 c d16 g] b,8[^\prall a16 g] g8 r r4 r16 g[a b] c[a b g] fis8^\prall r
  \tempo 4=40 
  r4 r16 a[b c] d[b c a] b8 r r4 r16 d[c b] a[c b d] c8 r r4 r16 e[d c]
  \tempo 4=60 
  b[d cis e] d8[cis d e] f[a, b! cis] d[fis, gis a] b[c] d4 ~ d16[e, fis gis]
  \tempo 4=50 
  a[fis gis e] e'[d c e] d[c b d] c[a' gis b] a[e f d] gis,[f' e d] c8[b16 a]
  a16[a' g f] e[g f a] g2 ~ g16[e f g] a[f g e] f2 ~ f16[g f e] d[f e g]
  \tempo 4=80 
  f2 ~ f16[d e f] g[e f d] e2 ~ e16[c d e] f[d e c] d[e f g] a[f g e] f[g a b]
  c[a b g] c8[g] e[d16 c] c[bes a g] f[a g bes] a[b c e,]
  \set Score.tempoHideNote = ##t
  \tempo 4=65 d [ \tempo 4=50 c' \tempo 4=35 f, \tempo 4=20 b] \tempo 4=60
  <c g e>1^\fermata\arpeggio \bar "|."
}

voicetwo = \relative c {
  \clef "bass" r2 r16 c[d e] f[d e c] g'8[g,] r4 r16 g'[a b] c[a b g] c8[b c d]
  e[g, a b] c[e, fis g] a[b] c4 ~ c16[d, e fis] g[e fis d] g8[b, c d] e[fis g e]
  b8.[c16] d8[d,] r16 g[a b] c[a b g] d'8[g fis g] a16[d, e fis] g[e fis d]
  a'8[d c d] g,16[\clef "treble" g' f e] d[f e g] f8[e f d] e16[a g f] e[g f a]
  g8[f g e] f16[bes a g] f[a g bes] a[g f e] d[f e g] f[e d c] b[d c e] d[c b a]
  gis[b a c] \clef "bass" b8[e,] d'8.[^\mordent e16] c[b a g!] fis[a gis b]
  a[c b d] c[e d f] e8[a, e' e,] a8[a,] r4 r16 e''16[d c] b[d cis e]
  d2 ~ d16[a b c] d[b c a] b2 ~ b16[d c b] a[c b d] c2 ~ c16[g a bes] c[a bes g]
  a8[bes a g] f[d' c bes] a[f' e d] e16[d, e f] g[e f d] e8[c d e] f16[d e f]
  g8[g,] <c c,>1 _\fermata \arpeggio \bar "|."
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% PRINT version A4
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

# ( set-global-staff-size 17.0 )

\book{
    \header {
        composer = "Johann Sebastian Bach (1685-1750)"
        title = "Invention 1"
        opus = "BWV 772"
        tagline     = ##f
    }  
    \paper {
        #(set-paper-size "a4")
        left-margin = 1.5\cm
        line-width = 18\cm
        top-margin = 1.5\cm
        bottom-margin = 1.5\cm
        horizontal-shift = 0\mm
        ragged-bottom = ##f
        ragged-last-bottom = ##f
        print-page-number = ##f
    } 
    \score {
        \new PianoStaff <<
            \set PianoStaff.connectArpeggios = ##t
            \context Staff = "one" << \voiceone >>
            \context Staff = "two" << \voicetwo >>
        >>
        
        \layout{
            indent = 0.0
        }
    }
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% VIDEO version
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#(set! paper-alist (cons '("video" . (cons (* 21 cm) (* 10.5 cm))) paper-alist))

\include "videohelper.ily"

# ( set-global-staff-size 24 )

\book{
   \header {
        title = \markup  \center-column {
          \line \bold \fontsize #5 { Invention 1 } \vspace #.25
          \line \bold \fontsize #2 { Johann Sebastian Bach } \vspace #.5
          \line \medium \fontsize #0 { Video made with the help of Lilypond}
          \line \medium \fontsize #0 { and other open source tools.} \vspace #.5
          \line \medium \fontsize #0 { Demonstrates how to synchronize video and audio even in }
          \line \medium \fontsize #0 { the presence of tempo changes and in case of a variable}
          \line \medium \fontsize #0 { number of systems per page.}
        }
        tagline = ##f
    }
    \paper {
        #(set-paper-size "video")
        left-margin = 1\cm
        line-width = 19.5\cm
        top-margin = 1\cm
        bottom-margin = 1\cm
        horizontal-shift = 0\mm
        ragged-bottom = ##f
        ragged-last-bottom = ##f
        print-page-number = ##f
        max-systems-per-page = #2
    }

    \pageBreak
    
    \score {
        \new PianoStaff <<
            \set PianoStaff.connectArpeggios = ##t
            \context Staff = "one" << \voiceone >>
            \context Staff = "two" << \voicetwo >>    
        >>
        \layout{
            indent = 0.0
        }
    }
} \pdfforvideo

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% MIDI
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

panmidi = { \set Staff.midiInstrument = #"pan flute" 
    \set Staff.midiMinimumVolume = #0.2
    \set Staff.midiMaximumVolume = #0.2
}

pianomidi = { \set Staff.midiInstrument = #"acoustic grand" 
    \set Staff.midiMinimumVolume = #0.2
    \set Staff.midiMaximumVolume = #0.2
}


\book{
    \score { 
        \unfoldRepeats \articulate <<
            \new PianoStaff <<
                \set PianoStaff.connectArpeggios = ##t
                \context Staff = "one" << {\panmidi \voiceone {r1*3 \tempo 1 = 1}} >>
                \context Staff = "two" << {\panmidi \voicetwo} >>    
            >>
        >>
        \midi {   
            \context {
                \Score
                midiMinimumVolume = #0.0
                midiMaximumVolume = #1.0
            }
        }
    }
} \midiforvideo

\book{
    \score { 
        \unfoldRepeats \articulate <<
            \new PianoStaff <<
                \set PianoStaff.connectArpeggios = ##t
                \context Staff = "one" << {\pianomidi \voiceone {r1*3 \tempo 1 = 1}} >>
                \context Staff = "two" << {\pianomidi \voicetwo} >>    
            >>
        >>
        \midi {   
            \context {
                \Score
                midiMinimumVolume = #0.0
                midiMaximumVolume = #1.0
            }
        }
    }
} \midiforvideo
%%%% This file contains code derived from Lilypond's ly/event-listener.ly.
%%%% There the following copyright notice was included:
%%%%
%%%% Copyright (C) 2011--2015 Graham Percival <gra...@percival-music.ca>
%%%%
%%%% Other parts of this file are based on code provide by
%%%% Thomas Morley <thomasmorle...@gmail.com>, see
%%%% 
http://lilypond.1069038.n5.nabble.com/intercepting-implicit-explicit-page-breaks-td194196.html
%%%%
%%%% This is free software: you can redistribute it and/or modify
%%%% it under the terms of the GNU General Public License as published by
%%%% the Free Software Foundation, either version 3 of the License, or
%%%% (at your option) any later version.
%%%%
%%%% This file is distributed in the hope that it will be useful,
%%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%%%% GNU General Public License for more details.
%%%%
%%%% You should have received a copy of the GNU General Public License
%%%% along with LilyPond.  If not, see <http://www.gnu.org/licenses/>.

#(define out (open-output-file "videohelper.notes"))

#(define (format-moment moment)
   (exact->inexact
    (/ (ly:moment-main-numerator moment)
       (ly:moment-main-denominator moment))))

#(define (moment-grace->string moment)
   "Prints a moment without grace note(s) as a float such as
0.25000.  Grace notes are written with the grace duration as a
separate \"dashed\" number, i.e. 0.25000-0.12500.  This allows any
program using the output of this function to interpret grace notes
however they want (half duration, quarter duration?  before beat,
after beat?  etc.)."
   (if
       (zero? (ly:moment-grace-numerator moment))
       (ly:format "~a" (format-moment moment))
       ;; grace notes have a negative numerator, so no "-" necessary
       (ly:format
         "~a~a"
         (format-moment moment)
         (format-moment
                       (ly:make-moment
                        (ly:moment-grace-numerator moment)
                        (ly:moment-grace-denominator moment))))))

#(define (make-output-string-line context values)
   "Constructs a tab-separated string beginning with the
score time (derived from the context) and then adding all the
values.  The string ends with a newline."
   (let* ((moment (ly:context-current-moment context)))
    (string-append
     (string-join
       (append
         (list (moment-grace->string moment))
         (map
             (lambda (x) (ly:format "~a" x))
             values))
       "\t")
     "\n")))


#(define (print-line context . values)
   "Prints the list of values (plus the score time) to a file, and
optionally outputs to the console as well.  context may be specified
as an engraver for convenience."
   (if (ly:translator? context)
       (set! context (ly:translator-context context)))
    (display (make-output-string-line context values) out)
    )

#(define (format-tempo engraver event)
   (print-line engraver
               "tempo"
               ( / 60
                (* (ly:event-property  event 'metronome-count)
                (format-moment (ly:duration-length (ly:event-property event 
'tempo-unit)))))))

#(define (format-rest engraver event)
   (print-line engraver
               "rest"
               (format-moment (ly:duration-length
                               (ly:event-property event 'duration)))))

#(define (format-note engraver event)
     (print-line engraver
                 "note"
                 (format-moment (ly:duration-length
                                 (ly:event-property event 'duration)))
                ))

\layout {
  \context {
  \Voice
  \consists #(make-engraver
              (listeners
               (tempo-change-event . format-tempo)
               (rest-event . format-rest)
               (note-event . format-note)))
  }
}

\paper {
        #(define (page-post-process layout pages) 
(print-pages-first-bar-numbers layout pages #t))
}

#(define* (print-pages-first-bar-numbers layout pages #:optional print-to-file)
  (let* ((lines (map (lambda (page) (ly:prob-property page 'lines)) pages))
         ;; list of systems of each pages
         (sys
           (map
             (lambda (line)
               (append-map
                 (lambda (l)
                   (let ((system-grob (ly:prob-property l 'system-grob)))
                     (if (not (null? system-grob))
                         (list system-grob)
                         system-grob))
                   )
                 line))
             lines))
         (sys-moment-location
           (map
             (lambda (m)
               (if (and (not (null? m)) (ly:grob? (car m)))
                   (grob::when (car m))
                   #f))
             sys))
         (formatted-output
           (map
             (lambda (page m)
               (if m
                   (format #f "~a page ~a\n" (format-moment m) page)
                   (format #f "page ~a contains no music\n" page))
                   )
             (iota (length pages) 1 1)
            sys-moment-location))
         )
    (if (not (null? sys-moment-location))
        (begin
           (for-each (lambda (i) (display i out)) formatted-output))
            (for-each display formatted-output)))) 

#(format out "~a~a~a" "LILYSOURCE=" (ly:parser-output-name) ".ly\n")

pdfforvideo = #(define-void-function () () (format out "~a~a~a" "VIDEOSOURCE=" 
current-outfile-name ".pdf\n"))

midiforvideo = #(define-void-function () () (format out "~a~a~a" "MIDISOURCE=" 
current-outfile-name ".midi\n"))
#!/bin/bash

######################################################################
# Copyright (C) 2016 Knut Petersen (knut_peter...@t-online.de)
#
# This is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
######################################################################

FPS=25
TITLETIME=6.0
AFTERTIME=4.0

DEBUG=0
CLEAN=1

FAIL=0

function weneedprog {
 for P in $@; do
   TMP=`which $P 2> /dev/null`
   if [ "x" == "x$TMP"  ]; then
      echo We need $P but could not find it!
      FAIL=$((FAIL+1))
   fi
 done
}

function weneeddata {
 for P in $@; do
   TMP=`ls -q $P 2> /dev/null`
   if [ "x" == "x$TMP"  ]; then
      echo We need $P but could not find it!
      FAIL=$((FAIL+1))
   fi
 done
}

echo checking dependencies ...
weneedprog ls sort tail uniq grep sed bc gs pdftk fluidsynth sox ffmpeg
weneeddata /usr/share/sounds/sf2/FluidR3_GM.sf2 videohelper.notes
if [ $FAIL -ne 0 ]; then
  echo $FAIL missing dependencies, aborting
  exit 1
else
  echo dependencies ok
fi

function checknotes {
  grep "$1" videohelper.notes &> /dev/null
  if [ $? -ne 0 ]; then
     echo Fatal error: $2
     exit 2
  fi
}

echo checking videohelper.notes ...
checknotes "LILYSOURCE" "LILYSOURCE undefined"
checknotes "VIDEOSOURCE" "VIDEOSOURCE undefined"
checknotes "MIDISOURCE" "MIDISOURCE undefined"
checknotes "tempo" "no tempo definition"
checknotes "note" "not a single note event"
checknotes "page 1 contains no music" "no title page defined"
checknotes "[0-9.]* page" "not a single page"
echo videohelper.notes ok

eval `grep LILYSOURCE videohelper.notes`
eval `grep VIDEOSOURCE videohelper.notes`
MIDILIST=`grep MIDISOURCE videohelper.notes | sed -e 
"s/MIDISOURCE=\([[:print:]]*\).midi/\1/" | sort | uniq | sed ':a;N;$!ba;s/\n/ 
/g'`

LASTMOMENT=$(echo `sort -n videohelper.notes | grep 'note\|rest' | tail -n 1 | \
            sed -e 
's/\([0-9.]*\)\([[:space:]]*\)\(note\|rest\)\([[:space:]]*\)\([0-9.]*\)/\1+\5/'`
 | bc -l)
MOMENTLIST="`grep "[0-9.]* page" videohelper.notes | sed -e 
"s/\([0-9.]*\)[[:space:]][[:print:]]*/\1/" | sed ':a;N;$!ba;s/\n/ /g'` 
$LASTMOMENT"
COUNT=1
ARMOM=()
for VAL in $MOMENTLIST; do
    ARMOM[COUNT]=$VAL
    COUNT=$((COUNT+1))
done
if [ $DEBUG -ne 0 ] ; then
   declare -p ARMOM
fi

TEMPOMOMENTLIST=`grep tempo videohelper.notes | sed -e 
"s/\([0-9.]*\)[[:space:]]tempo[[:space:]]\([0-9.]*\)/\1 /" | sed 
':a;N;$!ba;s/\n/ /g'`
COUNT=1
TMMOM=()
for VAL in $TEMPOMOMENTLIST; do
    TMMOM[COUNT]=$VAL
    COUNT=$((COUNT+1))
done
TMMOM[COUNT]=100000.0
if [ $DEBUG -ne 0 ] ; then
    declare -p TMMOM
fi

TEMPOTIMELIST=`grep tempo videohelper.notes | sed -e 
"s/\([0-9.]*\)[[:space:]]tempo[[:space:]]\([0-9.]*\)/\2 /" | sed 
':a;N;$!ba;s/\n/ /g'`
COUNT=1
TTMOM=()
for VAL in $TEMPOTIMELIST; do
    TTMOM[COUNT]=$VAL
    COUNT=$((COUNT+1))
done
TTMOM[COUNT]=0.0
if [ $DEBUG -ne 0 ] ; then
    declare -p TTMOM
fi

TEMPOS=`grep tempo videohelper.notes | wc | sed -e 
"s/[[:space:]]*\([[:digit:]]*\)[[:print:]]*/\1/"`
if [ $DEBUG -ne 0 ] ; then
    echo TEMPOS $TEMPOS
fi

PAGES=`grep page videohelper.notes | wc | sed -e 
"s/[[:space:]]*\([[:digit:]]*\)[[:print:]]*/\1/"`
PAGES=$((PAGES-1))
if [ $DEBUG -ne 0 ] ; then
    echo PAGES $PAGES
fi

MOMENTS=$((PAGES+1))
if [ $DEBUG -ne 0 ] ; then
    echo MOMENTS $MOMENTS
fi

for M in `seq 1 $MOMENTS`;
    do ARTIMES[M]=0.0
done

for T in `seq 1 $TEMPOS`; do
    for M in `seq 1 $MOMENTS`; do
        if (( $(echo "${ARMOM[$M]} > ${TMMOM[$T]}" |bc -l) )); then
            if (( $(echo "${ARMOM[$M]} <= ${TMMOM[$((T+1))]}" |bc -l) )); then
                ARTIMES[$M]=`bc -l 
<<<"${ARTIMES[$M]}+(${ARMOM[$M]}-${TMMOM[$T]})*${TTMOM[$T]}"`
            else
                ARTIMES[$M]=`bc -l 
<<<"${ARTIMES[$M]}+(${TMMOM[$((T+1))]}-${TMMOM[$T]})*${TTMOM[$T]}"`
            fi
        fi
    done
done

for M in `seq 1 $MOMENTS`; do
   ARTIMES[$M]=$(echo $(bc <<< "(${ARTIMES[$M]})*$FPS/1")/$FPS | bc -l)
done

if [ $DEBUG -ne 0 ] ; then
    echo The first page \(title\) will be visible for "$TITLETIME"s.
    echo The last page will be visible for "$AFTERTIME"s after the end of the 
last note/rest.
    for P in `seq 1 $PAGES`; do
            echo "page $((P+1)) from moment ${ARMOM[$P]} to ${ARMOM[$((P+1))]} 
(${ARTIMES[$P]}s to ${ARTIMES[$((P+1))]}s)"
    done
fi

ARVT[1]=$TITLETIME
for M in `seq 1 $PAGES`;
do
    ARVT[$((M+1))]=`bc <<<${ARTIMES[$((M+1))]}-${ARTIMES[$M]}`
done
ARVT[$MOMENTS]=`bc <<<${ARVT[$MOMENTS]}+$AFTERTIME`
if [ $DEBUG -ne 0 ] ; then
    declare -p ARVT
fi

PAGELIST=`grep -o "page [[:digit:]]*" videohelper.notes | sort -n | sed -e 
"s/\(page\) \([[:digit:]]*\)/\1\2/"  | sed ':a;N;$!ba;s/\n/ /g'`

echo generating tsilence.wav ...
sox -n -r 44100 -c 2 -b16 tsilence.wav trim 0.0 $TITLETIME &>/dev/null &

echo generating wav files from midi input ...
for M in $MIDILIST;
do
  fluidsynth -ln --fast-render=$M-tmp1.wav  
/usr/share/sounds/sf2/FluidR3_GM.sf2 $M.midi &>/dev/null &
done

echo bursting pdf ...
pdftk $VIDEOSOURCE burst output page%d.pdf

for P in `seq 1 $MOMENTS`;
do
    echo generating page$((P)).h264, length: ${ARVT[$P]}s
    gs -dBATCH -dNOPAUSE -q -r495.421 -sDEVICE=pnggray -sOutputFile=- 
page$((P)).pdf | \
    ffmpeg -y -framerate 1/100000 -i - \
        -vf scale=1024:512 -c:v libx264 -tune stillimage -preset ultrafast \
        -pix_fmt yuv420p -r 25 -t ${ARVT[$P]} page$((P)).h264 &> /dev/null & 
done

echo synchronizing ...
wait

echo normalizing audio data ...
for M in $MIDILIST;
do
  sox -v `sox $M-tmp1.wav -n stat -v 2>&1` $M-tmp1.wav $M-tmp2.wav &>/dev/null &
done
echo synchronizing ...
wait

echo adding silence to audio data ...
for M in $MIDILIST;
do
  sox tsilence.wav $M-tmp2.wav $M.wav &>/dev/null &
done

echo synchronizing ...
wait

COUNT=0
for M in $MIDILIST;
do
  echo generating $M.mp4 ...
  if [ $COUNT -eq 0 ]; then
     grep -o "page [[:digit:]]*" videohelper.notes | sort -n -k 2 | sed -e 
"s/\(page\) \([[:digit:]]*\)/file \1\2.h264/" >concat.txt
     ffmpeg -y -f concat -i concat.txt -i $M.wav -c:v libx264 -tune stillimage \
            -preset ultrafast -pix_fmt yuv420p -r 25 -shortest $M.mp4 
&>/dev/null
     COUNT=$((COUNT+1))
     FIRSTVIDEO=$M.mp4
  else
     ffmpeg -y -i $FIRSTVIDEO -i $M.wav -c:v copy -map 0:v:0 -map 1:a:0 
-shortest $M.mp4 &>/dev/null &
     COUNT=$((COUNT+1))
  fi
done

echo synchronizing ...
wait

echo removing temporary files ...
if [ $CLEAN -eq 1 ]; then
  for X in $PAGELIST; do
    rm $X.pdf $X.h264
  done
  for X in $MIDILIST; do
    rm $X-tmp1.wav $X-tmp2.wav $X.wav
  done
  rm concat.txt doc_data.txt tsilence.wav
fi
_______________________________________________
lilypond-user mailing list
lilypond-user@gnu.org
https://lists.gnu.org/mailman/listinfo/lilypond-user

Reply via email to