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
;
235 file_in_basename
="$(basename "$file_in")";
236 ffmpeg
-i "$file_in" -vn -acodec copy
"$dir_out"/"$file_in_basename".
"$file_in_ext";
237 } # Create audio file from video file
239 # Desc: Count and return total number of jobs
242 # Output: stdout integer number of jobs
243 # Depends: Bash 5.1.16
244 # Example: while [[$(count_jobs) -gt 0]]; do echo "Working..."; sleep 1; done;
248 job_count
="$(jobs -r | wc -l | tr -d ' ' )";
249 #yell "DEBUG:job_count:$job_count";
250 if [[ -z $job_count ]]; then job_count
="0"; fi;
252 }; # Return number of background jobs
254 # Input: arg1: file : file to check and, if audio, export
255 # arg2: dir_out: output dir
259 aud_format
="$(get_audio_format "$file")"; # Get audio format as file extension string
260 file_basename
="$(basename "$file")"; # Get basename for debugging
261 yell
"DEBUG:file_basename:$file_basename";
262 yell
"DEBUG:aud_format:$aud_format";
265 # Ignore files that return blank $aud_format
266 if [[ -z $aud_format ]]; then
267 yell
"DEBUG:Not an audio file:$file";
271 # Convert video to audio
272 extract_audio_file
"$file" "$aud_format" "$dir_out";
273 }; # One file check and extraction job
275 # Depends: yell(), die(), try()
276 # checkapp(), checkfile(), checkdir(), displayMissing(), showUsage(),
277 # extract_audio_file() get_audio_format()
278 # BK-2020-03: count_jobs v0.0.1
283 # Check argument count
284 if [[ $# -lt 1 ]]; then
286 die
"ERROR:Not enough arguments:$#";
290 checkapp ffmpeg ffprobe
date nproc
;
293 if ! checkdir
"$dir_in"; then
296 die
"ERROR:Missing input directory."
298 if ! checkdir
"$dir_out"; then
299 yell
"NOTICE:Output directory not specified. Creating output directory in current working directory:$script_pwd";
300 timestamp
="$(date +%Y%m%dT%H%M%S%z)"; # iso-8601 without separators
301 dir_out
="$script_pwd"/"$timestamp"..output
;
302 try mkdir
"$dir_out";
306 yell
"DEBUG:dir_in:$dir_in":
307 yell
"DEBUG:dir_out:$dir_out";
308 for file in "$dir_in"/*; do
309 yell
"DEBUG:count_jobs:$(count_jobs)";
310 while [[ "$(count_jobs)" -ge $max_jobs ]]; do sleep 0.1; done; # limit jobs
311 job
"$file" "$dir_out" &
314 # Announce completion
315 while [[ "$(count_jobs)" -gt 0 ]]; do sleep 1; done;
316 printf "\n" 1>&2; yell
"STATUS:Done.";
319 #export -f get_audio_format count_jobs extract_audio_file;
322 # Author: Steven Baltaktei Sandoval