#!/bin/bash ###################################################################### # Copyright (C) 2016 Knut Petersen (address@hidden) # # 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=n: video files will use n frames per second FPS=25 # TITLETIME: number of seconds the title page shall be visible TITLETIME=6.0 # AFTERTIME AFTERTIME=4.0 # PRESET: x264 preset to be used by ffmpeg for final output PRESET=veryslow PRESET=ultrafast # DEBUG: n != 0 turns on debugging mode (verbose output) DEBUG=0 # CLEAN: n != 0 enables deletion of all temporary files 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 usleep bc gs pdftk lilypond 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 TDIR=`mktemp -d mkvideo-XXXXX` 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 METROTIME=`grep tempo videohelper.notes | sort -n | head -n 1 | sed -e "s/[0-9.]*[[:space:]]tempo[[:space:]]//"` 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` ARVT[2]=`bc <<<${ARVT[2]}+$METROTIME` 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'` MAXJOBS=2 if [ -e "/proc/cpuinfo" ]; then if [ -r "/proc/cpuinfo" ]; then MAXJOBS=$((`grep processor /proc/cpuinfo | wc | sed -e "s/[[:space:]]*\([[:digit:]]*\)[[:space:]]*[[:print:]]*/\1/"`+1)) fi fi echo we decided to use up to $MAXJOBS parallel jobs ... function limitjobs { while [ `jobs | wc | sed -e "s/[[:space:]]*\([[:digit:]]*\)[[:space:]]*[[:print:]]*/\1/"` -ge $MAXJOBS ]; do usleep 250 done } echo generating metronome ticks ... FIRSTTEMPO=`echo 60/$METROTIME*4 | bc -l` FIRSTTEMPO=`echo $FIRSTTEMPO/1 | bc` echo "\version \"2.19.47\" \score { \drums { wbl4 4 4 4 } \midi { \tempo 4 = $FIRSTTEMPO } }" > $TDIR/metronome.ly cd $TDIR lilypond metronome.ly &> /dev/null & cd .. echo generating tsilence.wav ... sox -n -r 44100 -c 2 -b16 $TDIR/tsilence.wav trim 0.0 $TITLETIME &> $TDIR/sox-tsilence.log & echo generating wav files from midi input ... for M in $MIDILIST; do limitjobs fluidsynth -ln --fast-render=$TDIR/$M-tmp1.wav /usr/share/sounds/sf2/FluidR3_GM.sf2 $M.midi &> $TDIR/$M-tmp1.wav.log & done echo bursting pdf ... pdftk $VIDEOSOURCE burst output $TDIR/page%d.pdf echo synchronizing ... wait fluidsynth -ln --fast-render=$TDIR/metronome.wav /usr/share/sounds/sf2/FluidR3_GM.sf2 $TDIR/metronome.midi &> $TDIR/metronome.wav.log & echo "generating temporary h264 files ... " for P in `seq 1 $MOMENTS`; do limitjobs if [ $DEBUG -ne 0 ] ; then echo -n "page$P.h264, length: ${ARVT[$P]}s; " else echo -n "$P " fi gs -dBATCH -dNOPAUSE -q -r495.421 -sDEVICE=pnggray -sOutputFile=- $TDIR/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]} $TDIR/page$((P)).h264 &> $TDIR/ffmpeg-page$P.pdf-h264.log & done echo echo synchronizing ... wait echo normalizing audio data ... for M in $MIDILIST; do limitjobs sox -v `sox $TDIR/$M-tmp1.wav -n stat -v 2>&1` $TDIR/$M-tmp1.wav $TDIR/$M-tmp2.wav &> $TDIR/sox-$M-tmp1-tmp2.log & done limitjobs sox -v `sox $TDIR/metronome.wav -n stat -v 2>&1` $TDIR/metronome.wav $TDIR/metronome-norm.wav &> $TDIR/metronome-norm.wav.log & echo synchronizing ... wait echo adding metronome wav to audio data ... for M in $MIDILIST; do limitjobs sox $TDIR/metronome-norm.wav $TDIR/$M-tmp2.wav $TDIR/$M-tmp3.wav &> $TDIR/sox-$M-tmp2-tmp3.log & done echo synchronizing ... wait echo adding silence to audio data ... for M in $MIDILIST; do limitjobs sox $TDIR/tsilence.wav $TDIR/$M-tmp3.wav $TDIR/$M.wav &> $TDIR/sox-$M-tmp2-wav.log & 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/" > $TDIR/concat.txt ffmpeg -y -f concat -i $TDIR/concat.txt -i $TDIR/$M.wav -c:v libx264 -tune stillimage \ -preset $PRESET -pix_fmt yuv420p -r 25 -shortest $M.mp4 &> $TDIR/ffmpeg-$M.mp4.log COUNT=$((COUNT+1)) FIRSTVIDEO=$M.mp4 else limitjobs ffmpeg -y -i $FIRSTVIDEO -i $TDIR/$M.wav -c:v copy -map 0:v:0 -map 1:a:0 -shortest $M.mp4 &> $TDIR/ffmpeg-$M.mp4.log & COUNT=$((COUNT+1)) fi done echo synchronizing ... wait echo removing temporary files ... if [ $CLEAN -eq 1 ]; then echo $TDIR | grep "mkvideo-" &> /dev/null if [ $? -eq 0 ]; then rm -r $TDIR fi fi