#!/bin/bash # ----------------------------------------------------------------------------- # # App Title: convtoflac.sh # App Version: 2.1.3 # Author: Jared Breland # Homepage: http://www.legroom.net/software # # Script Function: # Convert losslessly compressed audio file to FLAC format, preserving tags # Currently supports FLAC, Monkey's Audio (APE), Shorten, WAV, and WavPack # # Instructions: # Ensure that all programs are properly set in "Setup environment" # # Caveats: # Transcoded files will retain original file name, but use .flac extension # The one exception is for FLAC input files - the original input file will # be renamed _old.flac, and the transcoded file will be named # .flac. # # Requirements: # The following programs must be installed and available # sed (http://sed.sourceforge.net/) # used to handle case sensitivity and tag processing # trash-cli (http://code.google.com/p/trash-cli/)) # used for moving files to trash rather than deleting # flac/metaflac (http://flac.sourceforge.net/) # used to create and tag new FLAC files # alac (http://craz.net/programs/itunes/alac.html) # used to decompress ALAC (Apple Lossles) files # mp4info, part of libmp4v2 (http://resare.com/libmp4v2/) # used to read tags from ALAC (Apple Lossles) files # mac (http://sourceforge.net/projects/mac-port/) # used to decompress APE (Monkey's Audio) files # apeinfo (http://www.legroom.net/software) # used to read tags from APE files # shorten (http://etree.org/shnutils/shorten/) # used to decompress Shorten files # ttaenc (http://www.true-audio.com/) # used to decompress TTA (True Audio) files # wvunpack (http://www.wavpack.com/) # used to decompress WavPack files # ffmpeg (http://www.ffmpeg.org/) # optionally used to decompress ALAC, APE, Shorten, and WavPack files # # Please visit the application's homepage for additional information. # # ----------------------------------------------------------------------------- renice -n 19 $$ # Static variables readonly VERSION="2.1.3" readonly PROG=$(basename $0) # Setup environment TMP='/tmp' DELETE='' OVERWRITE='' USEFFMPEG='' COMPRESS=8 THREADS=1 COPYTAGS=1 FILES=() #COLOR='\E[33;40m\033[1m' #comment these out if your term doesn't support colors COLORWARN='\E[31;40m\033[1m' IFS='@' # Function to display usage information function warning() { echo -ne "Usage: $PROG [-h] [-V] [-d|-m|-p] [-f] [-tN] [-n] [-o] [-cN]\n" echo -ne " $(printf "%${#PROG}s") [ ...]\n" echo -ne "Convert losslessly compressed audio files to FLAC format, preserving tags\n" echo -ne "\nOptions:\n" echo -ne " -h Display this help information\n" echo -ne " -V Display version and exit\n" echo -ne " -d Delete file after conversion\n" echo -ne " -m Move file to trash after conversion\n" echo -ne " -p Prompt to delete file after conversion\n" echo -ne " -f Use ffmpeg instead of default utilities to decode input files\n" echo -ne " Note: Existing tags will not be copied if ffmpeg is used\n" echo -ne " -tN Convert N number of files concurrently; default is 1\n" echo -ne " -n Do not copy existing tags to new FLAC file\n" echo -ne " -o Overwrite existing output FLAC files\n" echo -ne " -cN Set FLAC compression level, where N = 0 (fast) - 8 (best); default is 8\n" echo -ne "\nSupported input formats:\n" echo -ne " Apple Lossless (.m4a)\n" echo -ne " FLAC (.flac)\n" echo -ne " Monkey's Audio (.ape)\n" echo -ne " Shorten (.shn)\n" echo -ne " True Audio (.tta)\n" echo -ne " WAV (.wav)\n" echo -ne " WavPack (.wv)\n" exit } # Function to display colorized output function cecho () { MESSAGE=${1:-"Error: No message passed"} echo -e "${COLOR}${MESSAGE}" tput sgr0 } # Function to display colorized warnings function cwarn () { MESSAGE=${1:-"Error: No message passed"} echo -e "${COLORWARN}${MESSAGE}" tput sgr0 } # Function to determine if variable is an integer function is_int() { return $(test "$1" -eq "$1" > /dev/null 2>&1); } # Function to check for ffmpeg binary function ffmpeg_check() { FFMPEG=$(which ffmpeg 2>/dev/null) if [ ! -e "$FFMPEG" ]; then echo "Error: cannot find ffmpeg binary" MISSING=true fi } # Function to verify that necessary support binaries exist function bincheck() { MISSING='' case $EXT in "ape") MAC=$(which mac 2>/dev/null) APEINFO=$(which apeinfo 2>/dev/null) if [ -n "$USEFFMPEG" ]; then ffmpeg_check else [ ! -e "$MAC" ] && MISSING+='mac, ' [[ -n "$COPYTAGS" && ! -e "$APEINFO" ]] && MISSING+="apeinfo (optional with '-n'), " fi ;; "flac") if [ -n "$USEFFMPEG" ]; then echo "Warning: ffmpeg is not used for FLAC (.flac) files" fi ;; "m4a") ALAC=$(which alac 2>/dev/null) MP4INFO=$(which mp4info 2>/dev/null) if [ -n "$USEFFMPEG" ]; then ffmpeg_check else [ ! -e "$ALAC" ] && MISSING+='alac, ' [[ -n "$COPYTAGS" && ! -e "$MP4INFO" ]] && MISSING+="mp4info (optional with '-n'), " fi ;; "shn") SHORTEN=$(which shorten 2>/dev/null) if [ -n "$USEFFMPEG" ]; then ffmpeg_check elif [ ! -e "$SHORTEN" ]; then MISSING+='shorten, ' fi ;; "tta") TTAENC=$(which ttaenc 2>/dev/null) if [ ! -e "$TTAENC" ]; then if [ -n "$USEFFMPEG" ]; then MISSING+='ttaenc (ffmpeg does not support True Audio (.tta) files), ' else MISSING+='ttaenc, ' fi elif [ -n "$USEFFMPEG" ]; then echo "Warning: ffmpeg is not used for True Audio (.tta) files" fi ;; "wav") if [ -n "$USEFFMPEG" ]; then echo "Warning: ffmpeg is not used for WAV (.wav) files" fi ;; "wv") WVUNPACK=$(which wvunpack 2>/dev/null) if [ -n "$USEFFMPEG" ]; then ffmpeg_check elif [ ! -e "$WVUNPACK" ]; then MISSING+='wvunpack, ' fi ;; esac if [ -n "$MISSING" ]; then echo "Error: cannot find the following binaries: ${MISSING%%, }" exit fi } # Function to parse mp4info output to find tags and convert to VORBISCOMMENT function mp4tags() { TAGS2=${TAGS}.alac $SED -i "/ \w*: /w${TAGS2}" $TAGS $SED -i "s/^ //" $TAGS2 $SED -i "s/: /=/" $TAGS2 $SED -i "s/ of [0-9]\+//" $TAGS2 $SED -i "s/\(.*\)=/\U\1=/" $TAGS2 $SED -i "s/TRACK=/TRACKNUMBER=/;s/YEAR=/DATE=/;s/COMMENTS=/DESCRIPTION=/;s/DISK=/DISCNUMBER=/" $TAGS2 mv $TAGS2 $TAGS } # Function to parse wvunpack output to find tags and convert to VORBISCOMMENT function wvtags() { TAGS2=${TAGS}.wv $SED -i "/ = /w${TAGS2}" $TAGS $SED -i "s/ = /=/" $TAGS2 $SED -i "s/\(.*\)=/\U\1=/" $TAGS2 $SED -i "s/TRACK=/TRACKNUMBER=/;s/YEAR=/DATE=/;s/COMMENT=/DESCRIPTION=/;s/DISK=/DISCNUMBER=/" $TAGS2 mv $TAGS2 $TAGS } # Function to copy tags for supported formats function processtags() { OUTPUT="\nCopying tags for '$FILE'..." TAGS=/tmp/$PROG.$RANDOM.tags if [ "$EXT" == "ape" ]; then $APEINFO -t "$FILE" >$TAGS elif [ "$EXT" == "flac" ]; then $METAFLAC --export-tags-to=$TAGS "$FILE" elif [ "$EXT" == "m4a" ]; then $MP4INFO "$FILE" >$TAGS mp4tags elif [ "$EXT" == "wv" ]; then $WVUNPACK -qss "$FILE" >$TAGS wvtags else OUTPUT+=" tags not supported by for this format\n" return fi if [[ $? -ne 0 || ! -s "$TAGS" ]]; then OUTPUT+="\nWarning: tags could not be read from \"$FILE\"\n" else $METAFLAC --import-tags-from=$TAGS "$NAME.flac" if [[ $? -ne 0 ]]; then OUTPUT+="\nWarning: tags could not be written to \"$NAME.flac\"\n" else OUTPUT+=" complete\n" fi fi rm $TAGS echo -ne "$OUTPUT" } # Function to perform actual transcoding function transcode() { QUIET='' # Use ffmpeg, if requested, to decode input if [ -n "$USEFFMPEG" ] && [ "$EXT" == "ape" -o "$EXT" == "m4a" -o "$EXT" == "shn" -o "$EXT" == "wv" ]; then WAVENAME="/tmp/$(basename $NAME).wav" [ $THREADS -gt 1 ] && QUIET='2>/dev/null' eval $FFMPEG -i \"$FILE\" -f wav \"$WAVENAME.wav\" $QUIET if [ $? -ne 0 ]; then cwarn "\nError: \"$FILE\" could not be converted to a FLAC file." rm "$WAVENAME.wav" exit 1 fi eval $FLAC -$COMPRESS $OVERWRITE -o \"$NAME.flac\" \"$WAVENAME.wav\" $QUIET if [ $? -ne 0 ]; then cwarn "\nError: \"$FILE\" could not be converted to a FLAC file." rm "$WAVENAME.wav" exit 1 fi rm "$WAVENAME.wav" # Otherwise, use dedicated binaries for decoding else # Monkey's Audio input if [ "$EXT" == "ape" ]; then [ $THREADS -gt 1 ] && QUIET='2>/dev/null' eval $MAC \"$FILE\" - -d $QUIET | $FLAC -$COMPRESS $OVERWRITE -s -o "$NAME.flac" - # FLAC input elif [ "$EXT" == "flac" ]; then # Original FLAC file needs to be renamed if [[ -e "${NAME}_old.flac" ]]; then if [ $OVERWRITE ]; then mv -i "$FILE" "${NAME}_old.flac" else echo -e "Error: '${NAME}_old.flac' already exists: could not rename input file" exit 1 fi else mv -i "$FILE" "${NAME}_old.flac" fi FILE="${NAME}_old.flac" [ $THREADS -gt 1 ] && QUIET='-s' $FLAC -d "$FILE" $QUIET -c | $FLAC -$COMPRESS $OVERWRITE -s -o "$NAME.flac" - # ALAC input elif [ "$EXT" == "m4a" ]; then $ALAC -t "$FILE" # .m4a is not a unique extension, so first verify the format if [ $? -ne 0 ]; then echo "ERROR: '$FILE' is not a valid ALAC file" exit 1 fi [ $THREADS -gt 1 ] && QUIET='-s' $ALAC "$FILE" | $FLAC -$COMPRESS $OVERWRITE $QUIET -o "$NAME.flac" - # Shorten input elif [ "$EXT" == "shn" ]; then [ $THREADS -gt 1 ] && QUIET='-s' $SHORTEN -x "$FILE" - | $FLAC -$COMPRESS $OVERWRITE $QUIET -o "$NAME.flac" - # True Audio input elif [ "$EXT" == "tta" ]; then [ $THREADS -gt 1 ] && QUIET='2>/dev/null' eval $TTAENC -d -o - \"$FILE\" $QUIET | $FLAC -$COMPRESS $OVERWRITE -s -o "$NAME.flac" - # WAVE input elif [ "$EXT" == "wav" ]; then [ $THREADS -gt 1 ] && QUIET='-s' $FLAC -$COMPRESS $OVERWRITE $QUIET -o "$NAME.flac" "$FILE" # WavPack input elif [ "$EXT" == "wv" ]; then [ $THREADS -gt 1 ] && QUIET='-q' $WVUNPACK $QUIET "$FILE" -o - | $FLAC -$COMPRESS $OVERWRITE -s -o "$NAME.flac" - fi fi # Abort if transcode failed if [ ${PIPESTATUS[0]} -ne 0 ]; then cwarn "\nError: \"$FILE\" could not be converted to a FLAC file." if [[ "$EXT" == "flac" ]]; then cwarn "Restoring _old file to original name.\n" mv "$FILE" "$NAME.flac" fi exit 1 fi # Copy metadata to transcoded file, but not for ffmpeg decoding if [ $COPYTAGS ]; then processtags fi # Delete old file if requested if [ "$DELETE" == "prompt" ]; then echo -ne "\nDelete \"$FILE\"? " read -e DELPROMPT if [[ "$DELPROMPT" == "y" || "$DELPROMPT" == "Y" ]]; then DELETE=force fi fi OUTPUT="\nConversion complete - " if [ "$DELETE" == "force" ]; then rm "$FILE" OUTPUT+="deleted" elif [ "$DELETE" == "move" ]; then $TPUT "$FILE" OUTPUT+="trashed" else OUTPUT+="kept" fi OUTPUT+=" \"$FILE\"\n\n" echo -ne "$OUTPUT" } # Process arguments if [[ $# -eq 0 ]]; then warning else while [ $# -ne 0 ]; do # Match known arguments if [ "$1" == "-h" ]; then warning elif [ "$1" == "-V" ]; then echo "Version $VERSION" exit elif [ "$1" == "-d" ]; then if [ "$DELETE" == "" ]; then DELETE="force" else echo "Error: Only one deletion option (-d, -m, -p) can be specified)" exit fi elif [ "$1" == "-m" ]; then if [ "$DELETE" == "" ]; then DELETE="move" else echo "Error: Only one deletion option (-d, -m, -p) can be specified)" exit fi elif [ "$1" == "-p" ]; then if [ $THREADS -gt 1 ]; then echo "Error: The -p and -t options cannot be used together" exit fi if [ "$DELETE" == "" ]; then DELETE="prompt" else echo "Error: Only one deletion option (-d, -m, -p) can be specified)" exit fi elif [ "${1:0:2}" == "-c" ]; then COMPRESS=${1:2} is_int "$COMPRESS" if [ $? -ne 0 ] || [ $COMPRESS -lt 0 -o $COMPRESS -gt 8 ]; then echo "Error: You must specify a number between 0-8 for compression (-cN)" exit fi elif [ "${1:0:2}" == "-t" ]; then PROCS=$(grep -c processor /proc/cpuinfo) THREADS=${1:2} is_int "$THREADS" if [ $? -ne 0 ] || [ $THREADS -lt 1 ]; then echo "Error: You must specify the number of threads (-tN)" exit elif [ ${1:2} -gt $PROCS ]; then echo "You specified $THREADS threads, but you only have $PROCS processors." echo "Please specify no more than $PROCS threads." exit elif [ "$DELETE" == "prompt" -a $THREADS -gt 1 ]; then echo "Error: The -p and -t options cannot be used together" exit fi elif [ "$1" == "-n" ]; then COPYTAGS='' elif [ "$1" == "-o" ]; then OVERWRITE='-f' elif [ "$1" == "-f" ]; then USEFFMPEG=true COPYTAGS='' # Anything that's not a known argument gets treated as a file else FILES[${#FILES[*]}]=$1 fi shift done fi # Validate COMPRESS setting if [[ "$COMPRESS" != [0-8] ]]; then echo "Error: FLAC compression level must be between 0 and 8" exit fi # Define and verify core apps exist SED=$(which sed 2>/dev/null) FLAC=$(which flac 2>/dev/null) METAFLAC=$(which metaflac 2>/dev/null) TPUT=$(which trash-put 2>/dev/null) MISSING='' [ ! -e "$SED" ] && MISSING+='sed, ' [ ! -e "$FLAC" ] && MISSING+='flac, ' [ ! -e "$METAFLAC" ] && MISSING+='metaflac, ' [ "$DELETE" == "move" -a ! -e "$TPUT" ] && MISSING+='trash-put, ' if [ -n "$MISSING" ]; then echo "Error: cannot find the following binaries: ${MISSING%%, }" exit fi # Process each passed file sequentially for FILE in ${FILES[@]}; do # Verify file exists if [ ! -e "$FILE" ]; then echo "Error: '$FILE' does not exist" exit 1 fi # Determine file type and base filename NAME=${FILE%.*} EXT=$(echo "${FILE##*.}" | $SED 's/\(.*\)/\L\1/') # Exit if wrong file passed if [[ "$EXT" != "ape" && "$EXT" != "flac" && "$EXT" != "m4a" && "$EXT" != "shn" && "$EXT" != "tta" && "$EXT" != "wav" && "$EXT" != "wv" ]]; then echo "Error: '$FILE' is not a supported input format" exit 1 fi # Verify support binaries bincheck # Transcode file, concurrently up to number of specified threads cecho "\nProcessing '$FILE'...\n" if [ $(jobs | wc -l) -lt $THREADS ]; then transcode & fi while [ $(jobs | wc -l) -ge $THREADS ]; do sleep 0.1 jobs >/dev/null done done # Wait for any remaining processes to finish before exiting wait