Before we dive into how to batch transcode the audio streams using FFmpeg and bash, I feel that I should mention that there is already a free app called Tdarr which takes care of these issues in a nice fancy GUI.

Now that my guilt has been removed by way of me letting you know there's an easier way, let's get into it. First and foremost, I highly recommend checking out my FFmpeg Examples (and the FFmpeg How-To which is now a main menu link at the top of every page) to give you a head start if you’re not familiar using FFmpeg CLI syntax. The examples should truly put you ahead of most people's ffmpeg skills.

Recently I have been dealing with transcoding media files for my Jellyfin-based streaming website, travisflix.com, and performing ad-hoc media conversions is no longer realistic.

Output Anything Not Using AAC Audio Codec

# Single-liner Example:
find . -mount -depth -maxdepth 2 -regextype posix-extended -regex '.*mkv$|.*mp4$' -type f -exec bash -c 'echo "{}" $(ffprobe -loglevel error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "{}")' 2>/dev/null \; | grep -v 'aac$'

# Multi-liner Example:
find . -mount -depth -maxdepth 2 -regextype posix-extended -regex '.*mkv$|.*mp4$' -type f -exec bash -c \
'echo "{}" $(ffprobe -loglevel error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "{}")' 2>/dev/null \; \
| grep -v 'aac$'

The following script will look in $TEMPDIR for mkv/mp4 files not using the AAC audio codec, and if found, transcodes them into $WORKDIR then moves the final output file back to the original source, overwriting the original file. Test functionality first before running on a large batch.

ffmpeg-transcode-acodec.sh

#!/usr/bin/env bash
#
## DESCRIPTION: Transcode with FFmpeg all (mkv, mp4) media files in specified directory,
##              overwriting the original source files.
##
## AUTHOR:  Travis Runyard
## Revised: 04/18/2020
## URL:     https://blog.travisflix.com/ffmpeg-batch-transcode-audio/

# Exit on first non-zero exit code
set -e
# Set shell options
shopt -s globstar
shopt -u nullglob
# Declare variables (optionally) are indexed arrays
# Only associative arrays require declaration, but standardizing is a good thing
declare -a FILENAME
declare -a LOG

# Modify TEMPDIR and WORKDIR to suit your needs
TEMPDIR="/mnt/pool0/p0ds0smb/temp/ffmpeg"
WORKDIR="$TEMPDIR/.working"
SCRIPT_NAME=$(basename "$0")

usage()
{
  cat 1>&2 <<EOF
Usage: "$SCRIPT_NAME" [options]

-h| --help         Help shows this message.
-d| --directory    Specify the directory to process.
-w| --workdir      Writable working directory for FFmpeg.
                   If you dont specify this option, the
                   subdirectory named "working" will be used
                   within the --directory path or \$TEMPDIR.
EOF
}

while [ "$1" != "" ]; do
    case $1 in
        -d | --directory )  shift
                                TEMPDIR="${1:-$TEMPDIR}"
                                WORKDIR="$WORKDIR"
                                ;;
        -w | --workdir )    shift
                                WORKDIR="${1:-$WORKDIR}"
                                ;;
        -h | --help )           usage
                                exit 0
                                ;;
        * )                     usage
                                exit 1
    esac
    shift
done

cd $TEMPDIR
echo -e "*** DEBUG ***\n\$TEMPDIR = $TEMPDIR\n\$WORKDIR = $WORKDIR"
[ -d "$WORKDIR" ] || mkdir "$WORKDIR"
# Set field separator to newline
OIFS="$IFS"
IFS=$'\n'
FILENAME=($(find . ! -path "*$(basename "$WORKDIR")/*" -regextype posix-extended -regex '.*\.(mkv|mp4)$' -type f -print))
if [ -n "$FILENAME" ]; then
  for f in "${FILENAME[@]}"; do
    LOG+=("$f")
    AUDIOFORMAT=($(ffprobe -loglevel error -select_streams a:0 -show_entries stream=codec_name,channels -of default=nw=1:nk=1 "$f"))
    if ! [ "${AUDIOFORMAT[0]}" = "aac" ]; then
      # Build ffmpeg array within the for loop to populate variables
      args=(
      -nostdin
      -y
      -i "$f"
      -map 0:v:0
      -map 0:a
      -map 0:s?
      -ac "${AUDIOFORMAT[1]}"
      -ar 48000
      -metadata:s:a:0
      language=eng
      -c:v copy
      -c:a aac
      -c:s copy
      "$WORKDIR/$(basename "$f")"
      )
      echo -e "*** DEBUG: ffprobe detected '${AUDIOFORMAT[0]}' in the default audio stream with '${AUDIOFORMAT[1]}' channels\nPreparing to convert audio codec to AAC..." ; sleep 1
      echo -e "*** DEBUG: ffmpeg ${args[@]}"
      ffmpeg "${args[@]}" || break
      echo -e "*** DEBUG: Moving '$WORKDIR/$(basename "$f")' back to source directory name '$(dirname "$f")'"; sleep 1
      mv -ufv "$WORKDIR/$(basename "$f")" "$(dirname "$f")" || break
    fi
  done
  printf '*** DEBUG ***\nPROCESSED:%s\n'
  for i in "${LOG[@]}"; do echo "$i"; done
else
  echo -e "*** DEBUG: No files to process"
fi

If you have any questions/comments regarding this article, click here or scroll down below (login isn't required to post comments and there's no waiting period).

FFmpeg + sed: How to insert clean metadata based on input file name
This post shows you how to extract the movie/show name from the file name and insert it into the title metadata field of MKV & MP4 containers.
FFmpeg Map English Subtitles or Audio Stream Only
Dynamic FFmpeg Subtitle Stream Mapping by Language You can map English subs only by using -map 0:s:m:language:eng. With FFmpeg you can dynamically map streams which match certain criteria such as language that you specify. If you’re looking for the 3-letter language codes go to https:
Supported MP4 Metadata Keys with FFmpeg
The following table lists the supported MP4 container metadata key names in FFmpeg.