#!/bin/bash # audio2videoWaveformNG # Make head moving over waveform video for any audio file, # overlay over image that must be in the same fodler as audio file, named # like the audio file in question (takes higher order) or generic name like cover.png # usage example: # audio2videoWaveformNG file.flac # required command -v ffmpeg >/dev/null 2>&1 || { >&2 echo "I need ffmpeg" ; exit 1; } command -v ffprobe >/dev/null 2>&1 || { >&2 echo "I need ffprobe" ; exit 1; } command -v lolcat >/dev/null 2>&1 || { >&2 echo "I need lolcat" ; exit 1; } command -v convert >/dev/null 2>&1 || { >&2 echo "I need imagemagick convert" ; exit 1; } command -v composite >/dev/null 2>&1 || { >&2 echo "I need imagemagick composite" ; exit 1; } # functions calc () { bc <<< "scale=5; $*" } round () { awk 'BEGIN{printf "%."'"$1"'"f\n", "'"$2"'"}' } # variables pre=(ffmpeg -y -loglevel error -stats) # How the ffmpeg command should start every time debug="1" # echo various debug/info messages offset="0.005" # seconds < to correct for the ffmpeg cliping the right side of the waveform slightly, pure guesswork. wave_w="3200" # px < tmp image width wave_h="940" # px < tmp image height w="1920" # px < final width, 1920x804 is 21:9 ar. h="804" # px < final height line_size=(2x450) # moving head graphics size #line_color="ccffcc" # hex line_color="bbeebb" # hex #waveform_color="bbeebb" # hex waveform_color="3ba67d" # hex font_color="#bbeebb" # #hex #font_family="Roboto Condensed" # convert -list font | grep FontName # < to check font_family="Agdasima" # convert -list font | grep FontName # < to check font_size="$(calc ${w}/70)" && font_size="$(round 0 "${font_size}")" # font size as ratio of final_width fps="100" # frames per second tmpdir_root="/tmp" # place where script will put tmp files (( debug )) && echo "font size: $font_size" # tmp dir tmp="${tmpdir_root}/waveform_$RANDOM$$" trap '[ -n "$tmp" ] && rm -fr "$tmp"' EXIT mkdir -m 700 "$tmp" || { echo '!! unable to create a tmpdir' >&2; tmp=; exit 1; } (( debug )) && echo "tmp dir is $tmp" # main # main loop while [ $# -gt 0 ]; do file="$1" # info if type roundbox >/dev/null then roundbox "$file" | lolcat else echo "$file" | lolcat fi # get the duration into variable duration="$(ffprobe -i "$file" -show_entries format=duration -v quiet -of csv="p=0")" (( debug )) && echo "duration: $duration (seconds)" # image offset: get me how many pixels i need to pad image with $width, round to whole number offsetPercents="$(calc "($offset/($duration+$offset))*100")" (( debug )) && echo "offsetPercents: $offsetPercents (%)" padded_width="$(calc "($offsetPercents/100)*$wave_w+$wave_w")" padded_width="$(round "0" "$padded_width")" (( debug )) && echo "padded_width: $padded_width (pixels)" # image: waveform without padding waveformPic=(-i "$file" -filter_complex "aformat=channel_layouts=mono,showwavespic=s=${wave_w}x${wave_h}:colors=${waveform_color}, format=rgba" -frames:v 1 "$tmp/waveform_one.png") echo "waveform pic: ${pre[*]} ${waveformPic[*]}" | lolcat ("${pre[@]}" "${waveformPic[@]}") || exit # exe # image: waveform add padding waveformPicPad=(-i "$tmp/waveform_one.png" -vf "pad=${padded_width}:ih:color=black@0" "$tmp/waveform_padded.png") echo "waveform pic padded: ${pre[*]} ${waveformPicPad[*]}" | lolcat ("${pre[@]}" "${waveformPicPad[@]}") || exit # exe # find cover if any, either filebase.png or generic cover.png filename="$(readlink -f "$file")" # absolute baseext=$(basename "${filename}") # file.ext base="${baseext%.*}" # file dir="$(dirname "${filename}")" # dir path1="$dir/${base}.png" path2="$dir/cover.png" if [ -f "$path1" ]; then cover="$path1" elif [ -f "$path2" ]; then cover="$path2" else # make some image for cover makeblack=(-f lavfi -i color=c=black:s="${w}"x"${h}" -frames:v 1 "$tmp/black.png") echo "no cover found, making some black: ${pre[*]} ${video2[*]}" | lolcat ("${pre[@]}" "${makeblack[@]}") || exit # exe cover="$tmp/black.png" fi (( debug )) && echo "cover=$cover" # image magick instead of ffmpeg maybe set -x convert "$cover" -resize "${w}"x"${h}"^ -gravity center -extent "${w}"x"${h}" "$tmp/cover.png" || exit set +x # composite waveform over cover set -x composite -gravity center -compose screen -resize "${w}"x"${h}" "$tmp/waveform_padded.png" "$tmp/cover.png" "$tmp/composite_pre.png" || exit set +x # add text title (( debug )) && echo "base: $base" echo "$base" > "$tmp/text.txt" set -x convert "${tmp}/composite_pre.png" -family "$font_family" -fill "$font_color" -pointsize "$font_size" -compose screen -gravity north -annotate +20+10 @"$tmp/text.txt" "${tmp}/composite.png" || exit set +x # image: make line to be used as head line=(-f lavfi -i color=c="${line_color}":s="${line_size[@]}" -frames:v 1 "$tmp/line.png") echo "make line: ${pre[*]} ${line[*]}" | lolcat ("${pre[@]}" "${line[@]}") || exit # exe # video2: overlay animated head video2=(-r "${fps}" -loop 1 -i "$tmp/composite.png" -i "$tmp/line.png" -i "$file" -filter_complex "[0:v][1:v]overlay=x='if(gte(t,0), -w+(t)*(main_w-overlay_w)/$duration, NAN)':y='(main_h-overlay_h)/2'" -c:v libx264 -preset veryfast -crf 21 -c:a copy -to "$duration" "$tmp/waveform_head.mkv") echo "video2: ${pre[*]} ${video2[*]}" | lolcat ("${pre[@]}" "${video2[@]}") || exit # exe # find nice filename for output baseout="${base}_waveform" while [ -f "$dir/${baseout}.mkv" ] ; do baseout="${base}_waveform.$RANDOM" done (( debug )) && echo "filename: ${baseout}.mkv" # mv to destination echo "${dir}/${baseout}.mkv" | lolcat mv "$tmp/waveform_head.mkv" "${dir}/${baseout}.mkv" shift done