#!/usr/bin/env zsh # use parallel jobs to convert audio files from one format to another source "$HOME/.config/init/helpers" || exit 1 local callstr=$0 usage() { echo "Usage: $callstr [OPTIONS...] ..." echo "Convert multiple audio files to a new format" echo "" echo " \e[1mdescription opt longform arg default\e[0m" echo " output codec -c --codec opus" echo " output bitrate -b --bitrate k (opus:35k, vorbis:50k, mp3:65k, flac:N/A)" echo " output quality -q --quality N/A" echo " output directory -d --dest ." echo " copy metadata -m --metadata false" echo " background job count -j --jobs 4" echo " print this help -h --help" exit 0 } local pid trap_abort() { echo "" abort "process interrupted" } trap 'trap_abort' SIGHUP SIGINT SIGQUIT SIGTERM local codecpat='^(opus|mp3|vorbis|flac)$' local bitratepat='^[1-9][0-9]*k$' local qpatvorbis='^([0-9]|10)$' local qpatmp3='^[0-9]$' local codecstr local codecprintstr local extstr local bitratestr local bitrateset local metastr local dirstr local qstr local jobcount local args local parseerr=$(2>&1 zparseopts -D -E -M -A args \ c: -codec:=c \ b: -bitrate:=b \ q: -quality:=q \ d: -dest:=d -destination:=d \ j: -jobs:=j \ m:: -metadata::=m \ f:: -force::=f \ h -help=h | cut -d ' ' -f 2-) [[ -z $parseerr ]] || abort $parseerr zparseopts -D -E -M -A args \ c: -codec:=c \ b: -bitrate:=b \ q: -quality:=q \ d: -dest:=d -destination:=d \ j: -jobs:=j \ m:: -metadata::=m \ f:: -force::=f \ h -help=h local optarg local opt for opt in ${(@k)args}; do unset optarg [[ -z $args[$opt] ]] || optarg=$args[$opt] case $opt in -c) [[ $optarg =~ $codecpat ]] || \ abort "codec must be one of \`opus\`, \`mp3\`, \`flac\`, or \`vorbis\`" [[ ${optarg} == 'opus' ]] \ && codecprintstr=${optarg} \ && codecstr='libopus' && extstr='opus' \ && [[ -z $bitratestr ]] && bitratestr='35k' [[ ${optarg} == 'mp3' ]] \ && codecprintstr=${optarg} \ && codecstr='libmp3lame' && extstr='mp3' \ && [[ -z $bitratestr ]] && bitratestr='65k' [[ ${optarg} == 'vorbis' ]] \ && codecprintstr=${optarg} \ && codecstr='libvorbis' && extstr='ogg' \ && [[ -z $bitratestr ]] && bitratestr='50k' [[ ${optarg} == 'flac' ]] \ && codecprintstr=${optarg} \ && codecstr='flac' && extstr='flac' \ && [[ -z $bitratestr ]] && bitratestr='' ;; -b) [[ $optarg =~ $bitratepat ]] || \ abort "bitrate must positive integer + k, for kbps" bitratestr=$optarg bitrateset=1 ;; -q) qstr=$optarg ;; -d) dirstr=$optarg ;; -j) [[ $optarg =~ $posunsignedpat ]] || \ abort "job count must be a non-negative integer" jobcount=$optarg ;; -m) if [[ $optarg ]]; then [[ $optarg =~ $boolpat ]] || \ abort "metadata must be a boolean value (e.g. \`true\` or \`false\`)" [[ $optarg =~ $booltpat ]] && metastr=("-map_metadata" "0") \ || metastr=("-map_metadata" "-1") else metastr=("-map_metadata" "0") fi ;; -h) usage ;; esac done [[ $#@ -eq 0 ]] && abort "no input files specified" [[ -z $codecstr ]] && codecstr='libopus' [[ -z $bitratestr ]] && bitratestr='35k' [[ -z $codecprintstr ]] && codecprintstr='opus' [[ -z $extstr ]] && extstr='opus' [[ -z $jobcount ]] && jobcount=4 [[ -z $dirstr ]] && dirstr='.' [[ -z $metastr ]] && metastr=("-map_metadata" "-1") if [[ ! -z $qstr ]]; then [[ -z $bitratset ]] || abort "-b and -q are not compatible" [[ $codecstr == "libopus" ]] && abort "-q cannot be used with opus" [[ $codecstr == "flac" ]] && abort "-q cannot be used with flac" [[ $codecstr == "libvorbis" ]] && [[ ! $qstr =~ $qpatvorbis ]] \ && abort "for vorbis, -q must be in the range [0-10]" [[ $codecstr == "libmp3lame" ]] && [[ ! $qstr =~ $qpatmp3 ]] \ && abort "for mp3, -q must be in the range [0-9]" fi if [[ $codecstr == "flac" ]]; then bitratestr='' [[ -z $bitrateset ]] || abort "-b cannot be used with flac" fi local roots=(${@:t:r}) local uniqroots=(${(u)roots}) [[ $#roots == $#uniqroots ]] \ || abort "input contains files which share a root name (i.e. when extension is stripped)" local filecount=$#@ local f for f in $@; do [[ -f $f ]] || abort "could not read file \`$f\`" done 2>/dev/null mkdir -p $dirstr || abort "could not write to directory \`$dirstr\`" for f in $@; do local dest="$dirstr/${f:t:r}.$extstr" if [[ -e $dest ]]; then printf "destination file \`$dest\` exists\ndelete it? [y/N]: " read -q delprompt print "" [[ $delprompt = "y" ]] || return 0 2>/dev/null rm $dest || abort "could not delete file \`$dest\`" fi done local totalstr="${#@} file" [[ $#@ -ne 1 ]] && totalstr="${totalstr}s" local jobstr=$jobcount [[ $#@ -lt $jobcount ]] && jobstr=$#@ [[ $jobstr -gt 1 ]] && jobstr="${jobstr} jobs" || jobstr="${jobstr} job" coproc { local joblist=() local c while read -d $'\0' c; do [[ $c == "done" ]] && break local f=${c:s/file:/} local dest="$dirstr/${f:t:r}.$extstr" ( 1>/dev/null 2>&1 /dev/null wait $j done echo "done" } repeat $jobcount; do if [[ $#@ -gt 0 ]]; then 1>&p printf "file:%s\0" $@[1] shift fi done while read -p line; do [[ $line == "done" ]] && coproc exit && break echo $line if [[ $#@ -gt 0 ]]; then 1>&p printf "file:%s\0" $@[1] shift else 1>&p printf "done\0" fi done | progress-bar $filecount "using $jobstr to convert $totalstr to $codecprintstr"