Last active 17 hours ago

Revision 71b092c506b9ca0f19940618af59e703e2532d95

encode_audio.sh Raw
1#!/usr/bin/env bash
2# Strict mode for robust error handling
3set -euo pipefail
4
5# Default variables
6INPUT_FILE=""
7OUTPUT_FILE=""
8CODEC="opus"
9ENCODER_TYPE="native"
10STREAM_INDEX="0"
11BITRATE_PER_CHANNEL=64 # kbps per channel for Opus
12TEMP_OPUS=""
13TEMP_FLAC=""
14
15# Ensure temporary files are cleaned up if the script exits or fails
16cleanup() {
17 if [[ -n "$TEMP_OPUS" && -f "$TEMP_OPUS" ]]; then rm -f "$TEMP_OPUS"; fi
18 if [[ -n "$TEMP_FLAC" && -f "$TEMP_FLAC" ]]; then rm -f "$TEMP_FLAC"; fi
19}
20trap cleanup EXIT
21
22usage() {
23 cat <<EOF
24Usage: $(basename "$0") -i <input_media> [OPTIONS]
25
26A script to extract and encode a specific audio track from a movie file to FLAC, Opus, or both.
27
28Options:
29 -i <file> Input media file (video or audio)
30 -o <file> Output file name (optional; auto-generated if omitted)
31 -c <codec> Target codec: 'opus', 'flac', or 'dual' (default: opus)
32 ('dual' encodes to both and muxes into an .mka file)
33 -e <type> Encoder type: 'native' (ffmpeg) or 'external' (flac/opusenc) (default: native)
34 -s <index> Audio stream index to select (default: 0, which is the first audio track)
35 -h Show this help message
36
37Examples:
38 $(basename "$0") -i movie.mkv -c dual -e native
39 $(basename "$0") -i movie.mp4 -c flac -s 1 -o my_custom_audio.flac
40EOF
41 exit 1
42}
43
44# Parse command line arguments
45while getopts "i:o:c:e:s:h" opt; do
46 case "$opt" in
47 i) INPUT_FILE="$OPTARG" ;;
48 o) OUTPUT_FILE="$OPTARG" ;;
49 c) CODEC=$(echo "$OPTARG" | tr '[:upper:]' '[:lower:]') ;;
50 e) ENCODER_TYPE=$(echo "$OPTARG" | tr '[:upper:]' '[:lower:]') ;;
51 s) STREAM_INDEX="$OPTARG" ;;
52 h|*) usage ;;
53 esac
54done
55
56# Validate inputs
57if [[ -z "$INPUT_FILE" || ! -f "$INPUT_FILE" ]]; then
58 echo "Error: Input file not found or not specified."
59 usage
60fi
61
62if [[ "$CODEC" != "opus" && "$CODEC" != "flac" && "$CODEC" != "dual" ]]; then
63 echo "Error: Codec must be 'opus', 'flac', or 'dual'."
64 exit 1
65fi
66
67if [[ "$ENCODER_TYPE" != "native" && "$ENCODER_TYPE" != "external" ]]; then
68 echo "Error: Encoder type must be 'native' or 'external'."
69 exit 1
70fi
71
72# Validate stream index is a number
73if ! [[ "$STREAM_INDEX" =~ ^[0-9]+$ ]]; then
74 echo "Error: Stream index must be a non-negative integer."
75 exit 1
76fi
77
78# Check dependencies
79command -v ffmpeg >/dev/null 2>&1 || { echo "Error: ffmpeg is required."; exit 1; }
80command -v ffprobe >/dev/null 2>&1 || { echo "Error: ffprobe is required."; exit 1; }
81
82if [[ "$ENCODER_TYPE" == "external" ]]; then
83 if [[ "$CODEC" == "flac" || "$CODEC" == "dual" ]] && ! command -v flac >/dev/null 2>&1; then
84 echo "Error: 'flac' tool is required for external flac/dual encoding."
85 exit 1
86 fi
87 if [[ "$CODEC" == "opus" || "$CODEC" == "dual" ]] && ! command -v opusenc >/dev/null 2>&1; then
88 echo "Error: 'opusenc' tool is required for external opus/dual encoding."
89 exit 1
90 fi
91fi
92
93# Determine number of audio channels of the selected audio stream
94CHANNELS=$(ffprobe -v error -select_streams "a:${STREAM_INDEX}" -show_entries stream=channels -of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE")
95if [[ -z "$CHANNELS" ]]; then
96 echo "Error: Could not detect audio channels for stream index a:${STREAM_INDEX} in $INPUT_FILE. (Does that track exist?)"
97 exit 1
98fi
99
100echo "Detected $CHANNELS channel(s) in audio stream ${STREAM_INDEX}."
101
102# Calculate Opus bitrate (ignored for lossless FLAC)
103TOTAL_BITRATE=$(( CHANNELS * BITRATE_PER_CHANNEL ))
104
105# Determine output filename if not provided by user
106if [[ -z "$OUTPUT_FILE" ]]; then
107 if [[ "$CODEC" == "dual" ]]; then
108 OUTPUT_FILE="${INPUT_FILE%.*}_track${STREAM_INDEX}.mka"
109 else
110 OUTPUT_FILE="${INPUT_FILE%.*}_track${STREAM_INDEX}.${CODEC}"
111 fi
112fi
113
114# Check if output file already exists and abort if it does
115if [[ -e "$OUTPUT_FILE" ]]; then
116 echo "Error: Output file '$OUTPUT_FILE' already exists. Aborting."
117 exit 1
118fi
119
120# Handle channel mapping for native Opus (required for >2 channels)
121# Family 0 is for mono/stereo. Family 1 is for surround (3 to 8 channels).
122MAPPING_FAMILY=0
123if [[ "$CHANNELS" -gt 2 ]]; then
124 MAPPING_FAMILY=1
125fi
126
127# FILTER FIX: Force libopus to see standard 7.1, 5.1, stereo, or mono layouts.
128# This silently fixes the "Invalid channel layout 5.1(side)" error.
129CHANNEL_FORMAT_FILTER="-filter:a aformat=channel_layouts=7.1|5.1|stereo|mono"
130
131# Set FFmpeg logging profiles
132FFMPEG_NATIVE_LOG="-hide_banner -loglevel error -stats"
133FFMPEG_EXT_LOG="-hide_banner -loglevel quiet"
134
135echo "Starting encode: Target=$CODEC | Encoder=$ENCODER_TYPE | Stream=a:${STREAM_INDEX} | Output=$OUTPUT_FILE"
136
137# Execute encoding based on user choices
138if [[ "$CODEC" == "opus" ]]; then
139 echo "Target Bitrate: ${TOTAL_BITRATE}k"
140
141 if [[ "$ENCODER_TYPE" == "native" ]]; then
142 ffmpeg $FFMPEG_NATIVE_LOG -y -i "$INPUT_FILE" -map "0:a:${STREAM_INDEX}" \
143 $CHANNEL_FORMAT_FILTER \
144 -c:a libopus -b:a "${TOTAL_BITRATE}k" -vbr on -mapping_family "$MAPPING_FAMILY" \
145 "$OUTPUT_FILE"
146
147 elif [[ "$ENCODER_TYPE" == "external" ]]; then
148 ffmpeg $FFMPEG_EXT_LOG -y -i "$INPUT_FILE" -map "0:a:${STREAM_INDEX}" \
149 $CHANNEL_FORMAT_FILTER -f wav -c:a pcm_s24le - | \
150 opusenc --bitrate "$TOTAL_BITRATE" - "$OUTPUT_FILE"
151 fi
152
153elif [[ "$CODEC" == "flac" ]]; then
154 if [[ "$ENCODER_TYPE" == "native" ]]; then
155 ffmpeg $FFMPEG_NATIVE_LOG -y -i "$INPUT_FILE" -map "0:a:${STREAM_INDEX}" \
156 $CHANNEL_FORMAT_FILTER \
157 -c:a flac -compression_level 8 \
158 "$OUTPUT_FILE"
159
160 elif [[ "$ENCODER_TYPE" == "external" ]]; then
161 ffmpeg $FFMPEG_EXT_LOG -y -i "$INPUT_FILE" -map "0:a:${STREAM_INDEX}" \
162 $CHANNEL_FORMAT_FILTER -f wav -c:a pcm_s24le - | \
163 flac --best --ignore-chunk-sizes - -o "$OUTPUT_FILE"
164 fi
165
166elif [[ "$CODEC" == "dual" ]]; then
167 echo "Target Opus Bitrate: ${TOTAL_BITRATE}k"
168
169 if [[ "$ENCODER_TYPE" == "native" ]]; then
170 # Map the stream twice. Note we use specific stream filters (-filter:a:0 and -filter:a:1) here
171 # because applying a global -af filter breaks multiple mappings in FFmpeg.
172 ffmpeg $FFMPEG_NATIVE_LOG -y -i "$INPUT_FILE" \
173 -map "0:a:${STREAM_INDEX}" -filter:a:0 "aformat=channel_layouts=7.1|5.1|stereo|mono" -c:a:0 libopus -b:a:0 "${TOTAL_BITRATE}k" -vbr on -mapping_family "$MAPPING_FAMILY" -metadata:s:a:0 title="Opus (${TOTAL_BITRATE}k)" \
174 -map "0:a:${STREAM_INDEX}" -filter:a:1 "aformat=channel_layouts=7.1|5.1|stereo|mono" -c:a:1 flac -compression_level 8 -metadata:s:a:1 title="FLAC (Lossless)" \
175 "$OUTPUT_FILE"
176
177 elif [[ "$ENCODER_TYPE" == "external" ]]; then
178 TEMP_OPUS=$(mktemp --suffix=.opus)
179 TEMP_FLAC=$(mktemp --suffix=.flac)
180
181 echo "[1/3] Encoding Opus externally..."
182 ffmpeg $FFMPEG_EXT_LOG -y -i "$INPUT_FILE" -map "0:a:${STREAM_INDEX}" \
183 $CHANNEL_FORMAT_FILTER -f wav -c:a pcm_s24le - | \
184 opusenc --quiet --bitrate "$TOTAL_BITRATE" - "$TEMP_OPUS"
185
186 echo "[2/3] Encoding FLAC externally..."
187 ffmpeg $FFMPEG_EXT_LOG -y -i "$INPUT_FILE" -map "0:a:${STREAM_INDEX}" \
188 $CHANNEL_FORMAT_FILTER -f wav -c:a pcm_s24le - | \
189 flac --silent --best --ignore-chunk-sizes - -o "$TEMP_FLAC"
190
191 echo "[3/3] Multiplexing tracks into $OUTPUT_FILE..."
192 ffmpeg $FFMPEG_NATIVE_LOG -y -i "$TEMP_OPUS" -i "$TEMP_FLAC" \
193 -map 0:a -map 1:a -c copy \
194 -metadata:s:a:0 title="Opus (${TOTAL_BITRATE}k)" \
195 -metadata:s:a:1 title="FLAC (Lossless)" \
196 "$OUTPUT_FILE"
197 fi
198fi
199
200echo -e "\nEncoding complete: $OUTPUT_FILE"
201