--- /dev/null
+#!/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+