| 1 | #!/usr/bin/env bash |
| 2 | # Strict mode for robust error handling |
| 3 | set -euo pipefail |
| 4 | |
| 5 | # Default variables |
| 6 | INPUT_FILE="" |
| 7 | OUTPUT_FILE="" |
| 8 | CODEC="opus" |
| 9 | ENCODER_TYPE="native" |
| 10 | STREAM_INDEX="0" |
| 11 | BITRATE_PER_CHANNEL=64 # kbps per channel for Opus |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: $(basename "$0") -i <input_media> [OPTIONS] |
| 16 | |
| 17 | A script to extract and encode a specific audio track from a movie file to FLAC or Opus. |
| 18 | |
| 19 | Options: |
| 20 | -i <file> Input media file (video or audio) |
| 21 | -o <file> Output file name (optional; auto-generated if omitted) |
| 22 | -c <codec> Target codec: 'opus' or 'flac' (default: opus) |
| 23 | -e <type> Encoder type: 'native' (ffmpeg) or 'external' (flac/opusenc) (default: native) |
| 24 | -s <index> Audio stream index to select (default: 0, which is the first audio track) |
| 25 | -h Show this help message |
| 26 | |
| 27 | Examples: |
| 28 | $(basename "$0") -i movie.mkv -c opus -e native |
| 29 | $(basename "$0") -i movie.mp4 -c flac -s 1 -o my_custom_audio.flac |
| 30 | EOF |
| 31 | exit 1 |
| 32 | } |
| 33 | |
| 34 | # Parse command line arguments |
| 35 | while getopts "i:o:c:e:s:h" opt; do |
| 36 | case "$opt" in |
| 37 | i) INPUT_FILE="$OPTARG" ;; |
| 38 | o) OUTPUT_FILE="$OPTARG" ;; |
| 39 | c) CODEC=$(echo "$OPTARG" | tr '[:upper:]' '[:lower:]') ;; |
| 40 | e) ENCODER_TYPE=$(echo "$OPTARG" | tr '[:upper:]' '[:lower:]') ;; |
| 41 | s) STREAM_INDEX="$OPTARG" ;; |
| 42 | h|*) usage ;; |
| 43 | esac |
| 44 | done |
| 45 | |
| 46 | # Validate inputs |
| 47 | if [[ -z "$INPUT_FILE" || ! -f "$INPUT_FILE" ]]; then |
| 48 | echo "Error: Input file not found or not specified." |
| 49 | usage |
| 50 | fi |
| 51 | |
| 52 | if [[ "$CODEC" != "opus" && "$CODEC" != "flac" ]]; then |
| 53 | echo "Error: Codec must be 'opus' or 'flac'." |
| 54 | exit 1 |
| 55 | fi |
| 56 | |
| 57 | if [[ "$ENCODER_TYPE" != "native" && "$ENCODER_TYPE" != "external" ]]; then |
| 58 | echo "Error: Encoder type must be 'native' or 'external'." |
| 59 | exit 1 |
| 60 | fi |
| 61 | |
| 62 | # Validate stream index is a number |
| 63 | if ! [[ "$STREAM_INDEX" =~ ^[0-9]+$ ]]; then |
| 64 | echo "Error: Stream index must be a non-negative integer." |
| 65 | exit 1 |
| 66 | fi |
| 67 | |
| 68 | # Check dependencies |
| 69 | command -v ffmpeg >/dev/null 2>&1 || { echo "Error: ffmpeg is required."; exit 1; } |
| 70 | command -v ffprobe >/dev/null 2>&1 || { echo "Error: ffprobe is required."; exit 1; } |
| 71 | |
| 72 | if [[ "$ENCODER_TYPE" == "external" ]]; then |
| 73 | if [[ "$CODEC" == "flac" ]] && ! command -v flac >/dev/null 2>&1; then |
| 74 | echo "Error: 'flac' tool is required for external flac encoding." |
| 75 | exit 1 |
| 76 | fi |
| 77 | if [[ "$CODEC" == "opus" ]] && ! command -v opusenc >/dev/null 2>&1; then |
| 78 | echo "Error: 'opusenc' tool is required for external opus encoding." |
| 79 | exit 1 |
| 80 | fi |
| 81 | fi |
| 82 | |
| 83 | # Determine number of audio channels of the selected audio stream |
| 84 | CHANNELS=$(ffprobe -v error -select_streams "a:${STREAM_INDEX}" -show_entries stream=channels -of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE") |
| 85 | if [[ -z "$CHANNELS" ]]; then |
| 86 | echo "Error: Could not detect audio channels for stream index a:${STREAM_INDEX} in $INPUT_FILE. (Does that track exist?)" |
| 87 | exit 1 |
| 88 | fi |
| 89 | |
| 90 | echo "Detected $CHANNELS channel(s) in audio stream ${STREAM_INDEX}." |
| 91 | |
| 92 | # Calculate Opus bitrate (ignored for lossless FLAC) |
| 93 | TOTAL_BITRATE=$(( CHANNELS * BITRATE_PER_CHANNEL )) |
| 94 | |
| 95 | # Determine output filename if not provided by user |
| 96 | if [[ -z "$OUTPUT_FILE" ]]; then |
| 97 | OUTPUT_FILE="${INPUT_FILE%.*}_track${STREAM_INDEX}.${CODEC}" |
| 98 | fi |
| 99 | |
| 100 | # Handle channel mapping for native Opus (required for >2 channels) |
| 101 | # Family 0 is for mono/stereo. Family 1 is for surround (3 to 8 channels). |
| 102 | MAPPING_FAMILY=0 |
| 103 | if [[ "$CHANNELS" -gt 2 ]]; then |
| 104 | MAPPING_FAMILY=1 |
| 105 | fi |
| 106 | |
| 107 | # Set FFmpeg logging profiles |
| 108 | # Native: Hide banner, only show errors, but keep the progress bar (-stats) |
| 109 | FFMPEG_NATIVE_LOG="-hide_banner -loglevel error -stats" |
| 110 | # External: Completely silent so it doesn't garble the external tool's output |
| 111 | FFMPEG_EXT_LOG="-hide_banner -loglevel quiet" |
| 112 | |
| 113 | echo "Starting encode: Target=$CODEC | Encoder=$ENCODER_TYPE | Stream=a:${STREAM_INDEX} | Output=$OUTPUT_FILE" |
| 114 | |
| 115 | # Execute encoding based on user choices |
| 116 | if [[ "$CODEC" == "opus" ]]; then |
| 117 | echo "Target Bitrate: ${TOTAL_BITRATE}k" |
| 118 | |
| 119 | if [[ "$ENCODER_TYPE" == "native" ]]; then |
| 120 | ffmpeg $FFMPEG_NATIVE_LOG -y -i "$INPUT_FILE" -map "0:a:${STREAM_INDEX}" \ |
| 121 | -c:a libopus -b:a "${TOTAL_BITRATE}k" -vbr on -mapping_family "$MAPPING_FAMILY" \ |
| 122 | "$OUTPUT_FILE" |
| 123 | |
| 124 | elif [[ "$ENCODER_TYPE" == "external" ]]; then |
| 125 | # Pipe 24-bit PCM WAV to opusenc. |
| 126 | ffmpeg $FFMPEG_EXT_LOG -y -i "$INPUT_FILE" -map "0:a:${STREAM_INDEX}" -f wav -c:a pcm_s24le - | \ |
| 127 | opusenc --bitrate "$TOTAL_BITRATE" - "$OUTPUT_FILE" |
| 128 | fi |
| 129 | |
| 130 | elif [[ "$CODEC" == "flac" ]]; then |
| 131 | if [[ "$ENCODER_TYPE" == "native" ]]; then |
| 132 | # Native FLAC encoding |
| 133 | ffmpeg $FFMPEG_NATIVE_LOG -y -i "$INPUT_FILE" -map "0:a:${STREAM_INDEX}" \ |
| 134 | -c:a flac -compression_level 8 \ |
| 135 | "$OUTPUT_FILE" |
| 136 | |
| 137 | elif [[ "$ENCODER_TYPE" == "external" ]]; then |
| 138 | # Pipe 24-bit PCM WAV to external FLAC encoder. |
| 139 | ffmpeg $FFMPEG_EXT_LOG -y -i "$INPUT_FILE" -map "0:a:${STREAM_INDEX}" -f wav -c:a pcm_s24le - | \ |
| 140 | flac -8 - -o "$OUTPUT_FILE" |
| 141 | fi |
| 142 | fi |
| 143 | |
| 144 | echo -e "\nEncoding complete: $OUTPUT_FILE" |
mxsr / Audio Encoder Script
Last active 2 months ago
Revision 842c2101b6301f1be21eb377a9aea13d5a96fdc2