]> zdv2.bktei.com Git - BK-2020-03.git/commitdiff
feat(user/flacs_to_m4b.sh):Add script to convert FLAC to M4B develop
authorSteven Baltakatei Sandoval <baltakatei@gmail.com>
Wed, 5 Nov 2025 16:58:26 +0000 (16:58 +0000)
committerSteven Baltakatei Sandoval <baltakatei@gmail.com>
Wed, 5 Nov 2025 16:58:26 +0000 (16:58 +0000)
* Note: Adapated from `user/mp3s_to_mkv.sh`.

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

diff --git a/user/flacs_to_m4b.sh b/user/flacs_to_m4b.sh
new file mode 100755 (executable)
index 0000000..e50b66c
--- /dev/null
@@ -0,0 +1,144 @@
+#!/bin/bash
+# Desc: Converts a directory of flacs into a single m4b file with chapters
+# Usage: flacs_to_m4b.sh [DIR in] [DIR out] [BITRATE]
+# Example: flacs_to_m4b.sh ./dir_source ./dir_output 64k
+# Depends: GNU Coreutils 8.32 (date), ffmpeg, ffprobe
+# Ref/Attrib: [1] FFmpeg Formats Documentation https://ffmpeg.org/ffmpeg-formats.html#toc-Metadata-2
+# Version: 0.0.2
+
+# plumbing
+aac_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_m4b="$dir_out"/output.m4b;
+file_albumart="$dir_out"/albumart.png;
+file_chapters="$dir_tmp"/"$script_rundate"..chapters.txt;
+
+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:
+        flacs_to_m4b.sh [DIR in] [DIR out] [BITRATE]
+
+    EXAMPLE:
+      flacs_to_m4b.sh ./source_dir ./out_dir 64k
+EOF
+} # Display information on how to use this script.
+check_depends() {
+    if ! command -v ffmpeg; then show_usage; die "FATAL:Missing ffmpeg."; fi;
+    if ! command -v ffprobe; then show_usage; die "FATAL:Missing ffprobe."; 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
+get_media_length() {
+    # Use ffprobe to get media container length in seconds (float)
+    # Usage: get_media_length arg1
+    # Input:  arg1: path to file
+    # Output: stdout: seconds (float)
+    # Depends: ffprobe 4.1.8
+    #          BK-2020-03: die()
+    local file_in;
+    file_in="$1";
+    if [[ ! -f $file_in ]]; then
+        die "ERROR:Not a file:$file_in";
+    fi;
+    ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_in";
+} # Get media container length in seconds via stdout
+build_filelist_and_chapters() {
+    # Depends: var dir_tmp     temporary directory
+    #          var dir_in      input dir
+    #          var file_flist  path file list
+    # Output: file: $file_flist     list of flac files for ffmpeg
+    #         file: $file_chapters  chapters file for ffmpeg
+
+    # Change directory to input dir
+    pushd "$dir_in" || die "FATAL:Directory error:$(pwd)";
+
+    local chapter_start=0;
+    local chapter_end;
+    local duration;
+
+
+    # Initialize chapter ffmetadata file. See [1].
+    {
+        echo ";FFMETADATA1";
+    } >> "$file_chapters";
+
+    
+    find "$dir_in" -type f -iname "*.flac" | sort | while read -r line; do
+        local filename="${line#./}";
+        yell "$(printf "file '%s'\n" "$filename")";
+        printf "file '%s'\n" "$filename" >> "$file_flist";
+
+        # Get duration of the current file
+        duration=$(get_media_length "$filename");
+        chapter_end=$(echo "$chapter_start + $duration" | bc -l);
+
+        # Write chapter info
+        {
+            echo "[CHAPTER]";
+            echo "TIMEBASE=1/1000";
+            echo "START=$(echo "scale=0; $chapter_start * 1000" | bc -l)";
+            echo "END=$(echo "scale=0; $chapter_end * 1000" | bc -l)";
+            echo "title=$(basename "$filename" .flac)";
+        } >> "$file_chapters";
+
+        chapter_start=$chapter_end;
+    done
+
+    # Return to original dir
+    popd || die "FATAL:Directory error:$(pwd)";
+}; # build file list and chapters for ffmpeg
+ffmpeg_convert() {
+    # Depends: var dir_tmp
+    #              dir_in
+    #              dir_out
+    # Input: file $file_flist     list of flac files for ffmpeg
+    #        file $file_chapters  chapters file for ffmpeg
+
+    # Change directory to input dir
+    pushd "$dir_in" || die "FATAL:Directory error:$(pwd)";
+
+    # Concatenate flac files into a single WAV file
+    # Convert WAV to aac m4b file
+    ffmpeg -nostdin -f concat -safe 0 -i "$file_flist" -c:a pcm_s24le -rf64 auto -f wav - | \
+        ffmpeg -i - -i "$file_chapters" \
+               -map_metadata 1 -map_chapters 1 \
+               -map 0 -c:a aac -b:a "$aac_bitrate" \
+               "$file_out_m4b";
+
+    # Return to original dir
+    popd || die "FATAL:Directory error:$(pwd)";
+}; # convert flacs to aac m4b via ffmpeg
+save_albumart() {
+    local file
+    file="$(find "$dir_in" -type f -iname "*.flac" | sort | head -n1)";
+    file="$(readlink -f "$file")";
+    ffmpeg -nostdin -i "$file" -an -vcodec copy "$file_albumart";
+}; # save album art from an flac to output dir
+main() {
+    check_depends && yell "DEBUG:check_depends OK";
+    check_plumbing "$@" && yell "DEBUG:check_plumbing OK";
+    build_filelist_and_chapters "$@" && yell "DEBUG:build_filelist_and_chapters OK";
+    ffmpeg_convert "$@" && yell "DEBUG:ffmpeg_convert OK";
+    save_albumart "$@" && yell "DEBUG:save_albumart OK";
+}; # main program
+
+main "$@";
+
+# Author: Steven Baltakatei Sandoval
+# License: GPLv3+