2 # Desc: Extracts audio from video files 
   3 # Usage: bk_export_audio.sh [input_dir] ([output_dir]) 
   5 # Depends: bash 5.1.16, GNU Coreutils (8.32) 
   8 max_jobs
="$(nproc --all)"; # max parallel audio conversion jobs 
   9 declare -Ag appRollCall 
# Associative array for storing app status 
  10 declare -Ag fileRollCall 
# Associative array for storing file status 
  11 declare -Ag dirRollCall 
# Associative array for storing dir status 
  13 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr 
  14 die
() { yell 
"$*"; exit 111; } # same as yell() but non-zero exit status 
  15 try
() { "$@" || die 
"cannot $*"; } # runs args as command, reports args if command fails 
  17     # Desc: If arg is a command, save result in assoc array 'appRollCall' 
  18     # Usage: checkapp arg1 arg2 arg3 ... 
  20     # Input: global assoc. array 'appRollCall' 
  21     # Output: adds/updates key(value) to global assoc array 'appRollCall' 
  27         if command -v "$arg" 1>/dev
/null 
2>&1; then # Check if arg is a valid command 
  28             appRollCall
[$arg]="true"; 
  29             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
  31             appRollCall
[$arg]="false"; returnState
="false"; 
  35     #===Determine function return code=== 
  36     if [ "$returnState" = "true" ]; then 
  41 } # Check that app exists 
  43     # Desc: If arg is a file path, save result in assoc array 'fileRollCall' 
  44     # Usage: checkfile arg1 arg2 arg3 ... 
  46     # Input: global assoc. array 'fileRollCall' 
  47     # Output: adds/updates key(value) to global assoc array 'fileRollCall'; 
  48     # Output: returns 0 if app found, 1 otherwise 
  54         if [ -f "$arg" ]; then 
  55             fileRollCall
["$arg"]="true"; 
  56             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi; 
  58             fileRollCall
["$arg"]="false"; returnState
="false"; 
  62     #===Determine function return code=== 
  63     if [ "$returnState" = "true" ]; then 
  68 } # Check that file exists 
  70     # Desc: If arg is a dir path, save result in assoc array 'dirRollCall' 
  71     # Usage: checkdir arg1 arg2 arg3 ... 
  73     # Input: global assoc. array 'dirRollCall' 
  74     # Output: adds/updates key(value) to global assoc array 'dirRollCall'; 
  75     # Output: returns 0 if all args are dirs; 1 otherwise 
  81         if [ -z "$arg" ]; then 
  82             dirRollCall
["(Unspecified Dirname(s))"]="false"; returnState
="false"; 
  83         elif [ -d "$arg" ]; then 
  84             dirRollCall
["$arg"]="true"; 
  85             if ! [ "$returnState" = "false" ]; then returnState
="true"; fi 
  87             dirRollCall
["$arg"]="false"; returnState
="false"; 
  91     #===Determine function return code=== 
  92     if [ "$returnState" = "true" ]; then 
  97 } # Check that dir exists 
  99     # Desc: Displays missing apps, files, and dirs 
 100     # Usage: displayMissing 
 102     # Input: associative arrays: appRollCall, fileRollCall, dirRollCall 
 103     # Output: stderr: messages indicating missing apps, file, or dirs 
 104     # Depends: bash 5, checkAppFileDir() 
 105     local missingApps value appMissing missingFiles fileMissing
 
 106     local missingDirs dirMissing
 
 108     #==BEGIN Display errors== 
 109     #===BEGIN Display Missing Apps=== 
 110     missingApps
="Missing apps  :"; 
 111     #for key in "${!appRollCall[@]}"; do echo "DEBUG:$key => ${appRollCall[$key]}"; done 
 112     for key 
in "${!appRollCall[@]}"; do 
 113         value
="${appRollCall[$key]}"; 
 114         if [ "$value" = "false" ]; then 
 115             #echo "DEBUG:Missing apps: $key => $value"; 
 116             missingApps
="$missingApps""$key "; 
 120     if [ "$appMissing" = "true" ]; then  # Only indicate if an app is missing. 
 121         echo "$missingApps" 1>&2; 
 124     #===END Display Missing Apps=== 
 126     #===BEGIN Display Missing Files=== 
 127     missingFiles
="Missing files:"; 
 128     #for key in "${!fileRollCall[@]}"; do echo "DEBUG:$key => ${fileRollCall[$key]}"; done 
 129     for key 
in "${!fileRollCall[@]}"; do 
 130         value
="${fileRollCall[$key]}"; 
 131         if [ "$value" = "false" ]; then 
 132             #echo "DEBUG:Missing files: $key => $value"; 
 133             missingFiles
="$missingFiles""$key "; 
 137     if [ "$fileMissing" = "true" ]; then  # Only indicate if an app is missing. 
 138         echo "$missingFiles" 1>&2; 
 141     #===END Display Missing Files=== 
 143     #===BEGIN Display Missing Directories=== 
 144     missingDirs
="Missing dirs:"; 
 145     #for key in "${!dirRollCall[@]}"; do echo "DEBUG:$key => ${dirRollCall[$key]}"; done 
 146     for key 
in "${!dirRollCall[@]}"; do 
 147         value
="${dirRollCall[$key]}"; 
 148         if [ "$value" = "false" ]; then 
 149             #echo "DEBUG:Missing dirs: $key => $value"; 
 150             missingDirs
="$missingDirs""$key "; 
 154     if [ "$dirMissing" = "true" ]; then  # Only indicate if an dir is missing. 
 155         echo "$missingDirs" 1>&2; 
 158     #===END Display Missing Directories=== 
 160     #==END Display errors== 
 161 } # Display missing apps, files, dirs 
 163     # Desc: Display script usage information 
 168     # Depends: GNU-coreutils 8.30 (cat) 
 171         bk_export_audio.sh [DIR in] ([DIR out]) 
 174       bk_export_audio.sh ./videos/ ./exported_audio/ 
 175       bk_export_audio.sh ./videos/ 
 177 } # Display information on how to use this script. 
 179     # Desc: Gets audio format of file as string 
 180     # Usage: get_audio_format arg1 
 183     # Input: arg1: input file path 
 184     # Output: stdout (if valid audio format) 
 185     #         exit code 0 if audio file; 1 otherwise 
 186     # Example: get_audio_format myvideo.mp4 
 187     #   Note: Would return "opus" if full ffprobe report had 'Audio: opus, 48000 Hz, stereo, fltp' 
 188     # Note: Not tested with videos containing multiple video streams 
 189     # Ref/Attrib: [1] https://stackoverflow.com/questions/5618363/is-there-a-way-to-use-ffmpeg-to-determine-the-encoding-of-a-file-before-transcod 
 190     #             [2] https://stackoverflow.com/questions/44123532/how-to-find-out-the-file-extension-for-extracting-audio-tracks-with-ffmpeg-and-p#comment88464070_50723126 
 191     local audio_format file_in
; 
 195     # Return error exit code if not audio file 
 196     ## Return error if ffprobe itself exited on error 
 197     if ! ffprobe 
-v error 
-select_streams a 
-show_entries stream
=codec_name 
-of default
=nokey
=1:noprint_wrappers
=1 "$file_in" 1>/dev
/null 
2>&1; then 
 198         return_state
="false"; 
 202     audio_format
="$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=nokey=1:noprint_wrappers=1 "$file_in")"; # see [1] 
 204     ## Return error if audio format is incorrectly formatted (e.g. reject if contains spaces) 
 205     pattern
="^[[:alnum:]]+$"; # alphanumeric string with no spaces 
 206     if [[ $audio_format =~ 
$pattern ]]; then 
 208         # Report audio format 
 209         echo "$audio_format"; 
 211         return_state
="false"; 
 215     if [[ $return_state = "true" ]]; then 
 220 } # Get audio format as stdout 
 221 extract_audio_file
() { 
 222     # Desc: Use ffmpeg to creates audio file from input video file 
 223     # Usage: extract_audio_file arg1 arg2 arg3 
 225     # Input: arg1: input video file path 
 226     #        arg2: desired output file extension 
 227     #        arg3: output dir path 
 228     # Output: audio file at path [arg3]/[arg1].[arg2] 
 229     local file_in file_in_ext dir_out file_in_basename path_out
; 
 233     file_in_basename
="$(basename "$file_in")"; 
 234     path_out
="$dir_out"/"$file_in_basename".
"$file_in_ext"; 
 236     # Skip if output file already exists. 
 237     if [[ -f "$path_out" ]]; then return 1; fi; 
 240     ffmpeg 
-i "$file_in" -vn -acodec copy 
"$path_out"; 
 241 } # Create audio file from video file 
 243     # Desc: Count and return total number of jobs 
 246     # Output: stdout   integer number of jobs 
 247     # Depends: Bash 5.1.16 
 248     # Example: while [[$(count_jobs) -gt 0]]; do echo "Working..."; sleep 1; done; 
 252     job_count
="$(jobs -r | wc -l | tr -d ' ' )"; 
 253     #yell "DEBUG:job_count:$job_count"; 
 254     if [[ -z $job_count ]]; then job_count
="0"; fi; 
 256 }; # Return number of background jobs 
 258     # Input: arg1: file :   file to check and, if audio, export 
 259     #        arg2: dir_out: output dir 
 263     aud_format
="$(get_audio_format "$file")"; # Get audio format as file extension string 
 264     file_basename
="$(basename "$file")"; # Get basename for debugging 
 265     yell 
"DEBUG:file_basename:$file_basename"; 
 266     yell 
"DEBUG:aud_format:$aud_format"; 
 269     # Ignore files that return blank $aud_format 
 270     if [[ -z $aud_format ]]; then 
 271         yell 
"DEBUG:Not an audio file:$file"; 
 275     # Convert video to audio 
 276     extract_audio_file 
"$file" "$aud_format" "$dir_out"; 
 277 }; # One file check and extraction job 
 279     # Depends: yell(), die(), try() 
 280     #     checkapp(), checkfile(), checkdir(), displayMissing(), showUsage(), 
 281     #     extract_audio_file() get_audio_format() 
 282     #   BK-2020-03: count_jobs v0.0.1 
 287     # Check argument count 
 288     if [[ $# -lt 1 ]]; then 
 290         die 
"ERROR:Not enough arguments:$#"; 
 294     checkapp ffmpeg ffprobe 
date nproc
; 
 297     if ! checkdir 
"$dir_in"; then 
 300         die 
"ERROR:Missing input directory." 
 302     if ! checkdir 
"$dir_out"; then 
 303         yell 
"NOTICE:Output directory not specified. Creating output directory in current working directory:$script_pwd"; 
 304         timestamp
="$(date +%Y%m%dT%H%M%S%z)"; # iso-8601 without separators 
 305         dir_out
="$script_pwd"/"$timestamp"..output
; 
 306         try mkdir 
"$dir_out"; 
 310     yell 
"DEBUG:dir_in:$dir_in": 
 311     yell 
"DEBUG:dir_out:$dir_out"; 
 312     for file in "$dir_in"/*; do 
 313         yell 
"DEBUG:count_jobs:$(count_jobs)"; 
 314         while [[ "$(count_jobs)" -ge $max_jobs ]]; do sleep 0.1; done; # limit jobs         
 315         job 
"$file" "$dir_out" & 
 318     # Announce completion 
 319     while [[ "$(count_jobs)" -gt 0 ]]; do sleep 1; done; 
 320     printf "\n" 1>&2; yell 
"STATUS:Done."; 
 323 #export -f get_audio_format count_jobs extract_audio_file; 
 326 # Author: Steven Baltaktei Sandoval