feat(user/mp3s_to_opus.sh):Add script to combine mp3s into an opus
authorSteven Baltakatei Sandoval <baltakatei@gmail.com>
Wed, 26 Apr 2023 11:36:53 +0000 (11:36 +0000)
committerSteven Baltakatei Sandoval <baltakatei@gmail.com>
Wed, 26 Apr 2023 11:36:53 +0000 (11:36 +0000)
- Note: Useful for combining audiobooks split across many mp3 files.

user/mp3s_to_opus.sh [new file with mode: 0755]

diff --git a/user/mp3s_to_opus.sh b/user/mp3s_to_opus.sh
new file mode 100755 (executable)
index 0000000..0af92cc
--- /dev/null
@@ -0,0 +1,94 @@
+#!/bin/bash
+# Desc: Converts a directory of mp3s into a single opus file
+# Usage: mp3s_to_opus.sh [DIR in] [DIR out] [BITRATE]
+# Example: mp3s_to_opus.sh ./dir_source ./dir_output 48k
+# Depends: GNU Coretils 8.32 (date)
+# Version: 0.0.3
+
+# plumbing
+opus_bitrate="$3"; # e.g. "48k"
+script_rundate="$(date +%s)";
+dir_tmp="/dev/shm";
+dir_in="$(readlink -f "$1")";
+dir_out="$(readlink -f "$2")";
+file_flist="$dir_tmp"/"$script_rundate"..flist.txt;
+file_out_opus="$dir_out"/output.opus;
+file_albumart="$dir_out"/albumart.png;
+
+yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
+die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
+must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
+show_usage() {
+    # Desc: Display script usage information
+    # Usage: showUsage
+    # Version 0.0.2
+    # Input: none
+    # Output: stdout
+    # Depends: GNU-coreutils 8.30 (cat)
+    cat <<'EOF'
+    USAGE:
+        mp3s_to_opus.sh [DIR in] [DIR out] [BITRATE]
+
+    EXAMPLE:
+      mp3s_to_opus.sh ./source_dir ./out_dir 48k
+EOF
+} # Display information on how to use this script.
+check_depends() {
+    if ! command -v ffmpeg; then show_usage; die "FATAL:Missing ffmpeg."; fi;
+}; # check dependencies
+check_plumbing() {
+    if [[ $# -ne 3 ]]; then show_usage; die "FATAL:Invalid arg count:$#"; fi;
+    if [[ ! -d "$dir_in" ]]; then show_usage; die "FATAL:Not a dir:$dir_in"; fi;
+    if [[ ! -d "$dir_out" ]]; then mkdir -p "$2"; fi;
+}; # check arguments
+build_filelist() {
+    # Depends: var dir_tmp     temporary directory
+    #          var dir_in      input dir
+    #          var file_flist  path file list
+    # Output: file: $file_flist  list of mp3 files for ffmpeg
+
+    # Change directory to input dir
+    pushd "$dir_in" || die "FATAL:Directory error:$(pwd)";
+    
+    while read -r line; do
+        yell "$(printf "file '%s'\n" "${line#./}")";
+        printf "file '%s'\n" "${line#./}" >> "$file_flist";
+    done < <(find "$dir_in" -type f -iname "*.mp3" | sort);
+
+    # Return to original dir
+    popd || die "FATAL:Directory error:$(pwd)";
+    
+}; # build file list for ffmpeg
+ffmpeg_convert() {
+    # Depends: var dir_tmp
+    #              dir_in
+    #              dir_out
+    # Input: file $file_flist  list of mp3 files for ffmpeg
+
+    # Change directory to input dir
+    pushd "$dir_in" || die "FATAL:Directory error:$(pwd)";
+
+    # Concatenate mp3 files into a single WAV file
+    #   # Convert WAV to 48 kbps opus file
+    ffmpeg -f concat -safe 0 -i "$file_flist" -c:a pcm_s24le -rf64 auto -f wav - | \
+        ffmpeg -i - -c:a libopus -b:a "$opus_bitrate" "$file_out_opus";
+
+    # Return to original dir
+    popd || die "FATAL:Directory error:$(pwd)";
+}; # convert mp3s to opus via ffmpeg
+save_albumart() {
+    local file
+    file="$(find "$dir_in" -type f -iname "*.mp3" | sort | head -n1)";
+    file="$(readlink -f "$file")";
+    ffmpeg -i "$file" -an -vcodec copy "$file_albumart";
+}; # save album art from an mp3 to output dir
+main() {
+    check_depends && yell "DEBUG:check_depends OK";
+    check_plumbing "$@" && yell "DEBUG:check_plumbing OK";
+    build_filelist "$@" && yell "DEBUG:build_filelist OK";
+    ffmpeg_convert "$@" && yell "DEBUG:ffmpeg_convert OK";
+    save_albumart "$@" && yell "DEBUG:save_albumart OK";
+}; # main program
+
+main "$@";
+