#!/bin/bash ###################################################################### # Copyright (C) 2016-2017 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. ###################################################################### VERSION=2017-11-16 # FPS=n: video files will use n frames per second # Currently FSP != 25 is broken. It's either ffmpeg or I am too # blind to see my mistake ;-( FPS=25 VIDEORESOLUTION=1280x720 PRESET=veryslow AUDIOBITRATE=128k # TITLETIME: number of seconds the title page shall be visible TITLETIME=4.0 # AFTERTIME AFTERTIME=6.666 # METRONOME: n != 0 enables generation of metronome ticks METRONOME=1 # DEBUG: n != 0 turns on debugging mode (verbose output) DEBUG=0 # CLEAN: n != 0 enables deletion of all temporary files CLEAN=1 # SOUNDFONTB=./kptestpiano.sf2 SOUNDFONTA=/usr/share/sounds/sf2/FluidR3_GM.sf2 SOUNDFONTS="$SOUNDFONTA $SOUNDFONTB" 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 This is mkvideo version $VERSION for i in "$@" do case $i in --noMetro) echo not generating metronome ticks ... METRONOME=0 ;; *) ;; esac done echo checking dependencies ... weneedprog ls sort tail uniq grep sed bc gs pdftk lilypond fluidsynth sox ffmpeg weneeddata $SOUNDFONTS 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 } grep "NOPAGEREPETITIONS" videohelper.notes &> /dev/null if [ $? -eq 0 ]; then echo "No video generation because \noPageRepetitions was used!" exit fi echo checking videohelper.notes ... checknotes "LILYSOURCE" "LILYSOURCE undefined" checknotes "VIDEOSOURCE" "VIDEOSOURCE undefined" checknotes "MIDISOURCE" "MIDISOURCE undefined" checknotes "tempo" "no tempo definition" checknotes "time" "no time definition" 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` eval `grep LASTMOMENT videohelper.notes` grep VIDEORESOLUTION videohelper.notes &> /dev/null if [ $? -eq 0 ]; then eval `grep VIDEORESOLUTION videohelper.notes` fi grep PRESET videohelper.notes &> /dev/null if [ $? -eq 0 ]; then eval `grep PRESET videohelper.notes` fi grep AUDIOBITRATE videohelper.notes &> /dev/null if [ $? -eq 0 ]; then eval `grep AUDIOBITRATE videohelper.notes` fi MIDILIST=`grep MIDISOURCE videohelper.notes | sed -e "s/MIDISOURCE=\([[:print:]]*\).midi/\1/" | sort | uniq | sed ':a;N;$!ba;s/\n/ /g'` 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 if [ $METRONOME -ne 0 ] ; then METROTIME=`grep tempo videohelper.notes | sort -n | head -n 1 | sed -e "s/[0-9.]*[[:space:]]tempo[[:space:]]//"` else METROTIME=0.0 fi TIMENUMERATOR=`grep time videohelper.notes | sort -n | head -n 1 | sed -e "s/[0-9.]*[[:space:]]time[[:space:]]\([0-9][0-9]*\)[[:space:]]\([0-9][0-9]*\)/\1/"` TIMEDENOMINATOR=`grep time videohelper.notes | sort -n | head -n 1 | sed -e "s/[0-9.]*[[:space:]]time[[:space:]]\([0-9][0-9]*\)[[:space:]]\([0-9][0-9]*\)/\2/"` ARVT[1]=$TITLETIME for M in `seq 1 $PAGES`; do ARVT[$((M+1))]=`bc <<<${ARTIMES[$((M+1))]}-${ARTIMES[$M]} | sed -r 's/^(-?)\./\10./'` done ARVT[$MOMENTS]=`bc <<<${ARVT[$MOMENTS]}+$AFTERTIME | sed -r 's/^(-?)\./\10./'` ARVT[2]=`bc <<<"${ARVT[2]}+$METROTIME*($TIMENUMERATOR/$TIMEDENOMINATOR)" | sed -r 's/^(-?)\./\10./'` 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 sleep 0.0250 done } if [ $METRONOME -ne 0 ] ; then echo generating metronome ticks ... FIRSTTEMPO=`echo 60/$METROTIME*4 | bc -l` FIRSTTEMPO=`echo $FIRSTTEMPO/1 | bc` echo "\version \"2.19.55\" \score { \drums { \repeat unfold $TIMENUMERATOR { wbl$TIMEDENOMINATOR } } \midi { \tempo 4 = $FIRSTTEMPO } }" > $TDIR/metronome.ly cd $TDIR lilypond metronome.ly &> /dev/null & cd .. fi echo generating tsilence.wav ... sox -n -r 48000 -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 -o synth.sample-rate=48000 -ln --fast-render=$TDIR/$M-tmp1.wav $SOUNDFONTS $M.midi &> $TDIR/$M-tmp1.wav.log & done echo bursting pdf ... pdftk $VIDEOSOURCE burst output $TDIR/page%d.pdf echo synchronizing ... wait if [ $METRONOME -ne 0 ] ; then fluidsynth -o synth.sample-rate=48000 -ln --fast-render=$TDIR/metronome.wav $SOUNDFONTS $TDIR/metronome.midi &> $TDIR/metronome.wav.log & fi echo "generating `grep page videohelper.notes | wc | sed -e "s/[[:space:]]*\([0-9]*\)[[:space:]]*[[:print:]]*/\1/"` 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=png256 -sOutputFile=- $TDIR/page$((P)).pdf | \ ffmpeg -y -framerate 1/10000 -i - \ -vf scale=$VIDEORESOLUTION -c:v libx264 -tune stillimage -preset ultrafast \ -pix_fmt yuv420p -r $FPS -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 if [ $METRONOME -ne 0 ] ; then limitjobs sox -v `sox $TDIR/metronome.wav -n stat -v 2>&1` $TDIR/metronome.wav $TDIR/metronome-norm.wav &> $TDIR/metronome-norm.wav.log & fi echo synchronizing ... wait if [ $METRONOME -ne 0 ] ; then 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 else for M in $MIDILIST; do ln -s $M-tmp2.wav $TDIR/$M-tmp3.wav done fi 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 ls -1 $TDIR/page*.h264 | grep -o "page[[:print:]]*h264" | sed -e "s/page//" | LANG=clang sort -n | sed -e "s/\([[:print:]]*\)/file page\1/" > $TDIR/concat.txt ffmpeg -y -f concat -i $TDIR/concat.txt -i $TDIR/$M.wav -c:v libx264 -tune animation \ -preset $PRESET -movflags +faststart -bf 2 -flags +cgop -pix_fmt yuv420p -r $FPS -c:a aac -b:a $AUDIOBITRATE -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 -movflags +faststart -c:a aac -b:a $AUDIOBITRATE -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