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 while read -r file; do
313 yell
"DEBUG:count_jobs:$(count_jobs)";
314 while [[ "$(count_jobs)" -ge $max_jobs ]]; do sleep 0.01s
; done; # limit jobs
315 job
"$file" "$dir_out" &
316 done < <(find "$dir_in" -type f
);
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