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