Skip to content

Instantly share code, notes, and snippets.

@avengerx
Last active November 9, 2025 04:13
Show Gist options
  • Select an option

  • Save avengerx/53f0cabf9ae0bd129d80f2f95d3322af to your computer and use it in GitHub Desktop.

Select an option

Save avengerx/53f0cabf9ae0bd129d80f2f95d3322af to your computer and use it in GitHub Desktop.
Bash script to normalize audio from files using the `ffmpeg` tool
#!/bin/bash
mp3gain="/cygdrive/a/apps/mp3gain/mp3gain.exe"
id3="/cygdrive/a/apps/id3/id3.exe"
concurrent_jobs=4
if [ -z "${1}" ]; then
echo "usage: ${0} <directory> [<directory> ...]"
exit 1
fi
for arg in "${@}"; do
if [ ! -d "${arg}" ]; then
echo "not a directory: ${arg}"
exit 1
fi
done
_ifs="${IFS}"
IFS=$'\n'
filelist=($(find "${@}" -iname "*.mp3"))
IFS="${_ifs}"
iwd="${PWD}"
logfile="${iwd}/${0%.sh}.log"
if [ ${#filelist[@]} -lt 1 ]; then
echo "no mp3 files found in: ${1}"
exit 0
fi
echo "These files will be normalized:"
count=0
for path in "${filelist[@]}"; do
count=$((10#${count} + 1))
echo "- ${path}"
if [ ${count} -ge 10 ]; then
echo "- (Additional $((10#${#filelist[@]} - 10)) files)"
break
fi
done
# In case you want to confirm before starting...
#echo "
#Continue? [yn]"
#read -sn1 resp
#if [ "${resp,}" != "y" ]; then
# echo "no."
# exit 0
#fi
#echo "yes."
echo "Phase 1: normalize in parallel."
for path in "${filelist[@]}"; do
echo "- ${path}"
dir="${path%/*}"
file="${path##*/}"
if [ "$(jobs -p 2>&1 | wc -l)" -ge ${concurrent_jobs} ]; then
echo -n "Waiting background jobs: "
wait -n
retstat="${?}"
if [ ${retstat} -ne 0 ]; then
echo "Error trying to normalize file. aborting."
cd "${iwd}"
if [ "$(jobs -p 2>&1 | wc -l)" -gt 0 ]; then
echo -n "Waiting remaining background jobs: "
wait
echo "done."
fi
echo "Check '${logfile}' for normalize tool output."
exit 1
fi
echo " done."
fi
cd "${dir}"
"${mp3gain}" /u "${file}"
retstat="${?}"
if [ ${retstat} -ne 0 ]; then
echo "Error running mp3gain. aborting."
cd "${iwd}"
exit 1
fi
"${iwd}/normalize.sh" "${file}" 2>&1 >> "${logfile}" &
cd "${iwd}"
done
if [ "$(jobs -p 2>&1 | wc -l)" -gt 0 ]; then
echo -n "Waiting concurrent jobs: "
while [ "$(jobs -p 2>&1 | wc -l)" -gt 0 ]; do
wait -n
echo -n "."
retstat="${?}"
if [ ${retstat} -ne 0 ]; then
echo "Error trying to normalize file. aborting."
cd "${iwd}"
if [ "$(jobs -p 2>&1 | wc -l)" -gt 0 ]; then
echo -n "Waiting remaining background jobs: "
wait
echo "done."
fi
echo "Check '${logfile}' for normalize tool output."
exit 1
fi
done
echo "done."
fi
echo "
Pahse 2: replace files."
for path in "${filelist[@]}"; do
echo "- ${path}"
dir="${path%/*}"
file="${path##*/}"
nfile="${file%.mp3}_.mp3"
cd "${dir}"
if [ ! -f "${nfile}" ]; then
echo "Normalize tool didn't output expected file: ${nfile}"
cd "${iwd}"
exit 1
else
echo -n "Overwriting file with normalized one: "
output="$("${id3}" --duplicate "${file}" "${nfile}" 2>&1)"
retstat="${?}"
if [ ${retstat} -ne 0 ]; then
if [ "${output::36}" == "id3: note: could not read tags from " ]; then
echo -n "no id3 info, "
else
echo "Failed copying over ID3 tag to file.
--- id3.exe output ---
${output}
---
Aborting."
cd "${iwd}"
exit 1
fi
fi
mv "${nfile}" "${file}"
retstat="${?}"
if [ ${retstat} -ne 0 ]; then
echo "Failed overwriting file. Aborting."
cd "${iwd}"
exit 1
else
echo "done."
fi
fi
cd "${iwd}"
done
echo "
All done! Hopefully!"
#!/bin/bash
# Normalize to -14LUFs (youtube, spotify defaults).
target_lufs=-14 # youtube, spotify and other streaming media
ffmpeg="/cygdrive/a/apps/ffmpeg/bin/ffmpeg.exe"
if [ -z "${1}" ]; then
echo "usage: ${0} <mp3 file>"
exit 1
elif [ ! -f "${1}" ]; then
echo "unable to locate mp3 file: ${1}"
exit 1
fi
destfile="${1%.mp3}_.mp3"
echo -n "Analyzing ${1}: "
# infer
output="$("${ffmpeg}" -i "${1}" -af loudnorm=print_format=summary -f null - 2>&1)"
retstat="${?}"
if [ ${retstat} -ne 0 ]; then
echo "failed.
Unable to analyze file. ffmpeg returned non-zero error status ${retstat}.
--- ffmpeg output ---
${output}
---"
exit 1
else
echo "done."
fi
sampleout="Input Integrated: -9.2 LUFS
Input True Peak: +0.1 dBTP
Input LRA: 2.2 LU
Input Threshold: -19.4 LUFS
Output Integrated: -23.5 LUFS
Output True Peak: -11.6 dBTP
Output LRA: 1.7 LU
Output Threshold: -33.6 LUFS
Normalization Type: Dynamic
Target Offset: -0.5 LU
"
vals=($(echo "${output}" | egrep "^Input (Integrated|True Peak|LRA|Threshold): " | sed -E "s/: +/:/;s/^[^:]+://;s/ (LUFS|dBTP|LU)//;s/\r//" | tr '\n' ' '))
input_i="${vals[0]}"
input_tp="${vals[1]}"
input_lra="${vals[2]}"
input_thres="${vals[3]}"
echo "int: ${input_i}
tp: ${input_tp}
lra: ${input_lra}
thres: ${input_thres}
Output file: ${destfile}"
if [[ ! "${input_i}" =~ ^(|\+|-)[0-9]+(|\.[0-9]+)$ || \
! "${input_tp}" =~ ^(|\+|-)[0-9]+(|\.[0-9]+)$ || \
! "${input_lra}" =~ ^(|\+|-)[0-9]+(|\.[0-9]+)$ || \
! "${input_thres}" =~ ^(|\+|-)[0-9]+(|\.[0-9]+)$ ]]; then
echo "One of the required values were not in the expected format."
exit 1
fi
if [ -e "${destfile}" ]; then
dfbak="${destfile//./_}-$(date +%Y%m%d%H%M%S).mp3"
echo -n "Destination file exists, renaming it to: ${dfbak}"
output="$(mv "${destfile}" "${dfbak}" 2>&1)"
retstat="${?}"
if [ ${retstat} -ne 0 ]; then
echo ", failed.
Unable to replace file: ${output}"
exit 1
else
echo ", done."
fi
fi
if [ "${input_i}" == "-inf" -o "${input_tp}" == "-inf" ]; then
echo -n "
File is either silent or doesn't have enough data to be normalized. Copying over: "
output="$(cp "${1}" "${1%.mp3}_.mp3" 2>&1)"
retstat="${?}"
if [ ${retstat} -ne 0 ]; then
echo "failed.
ffmpeg failed to copy file over.
--- cp output ---
${output}
---"
exit 1
else
echo "Done.
Saved to: ${1%.mp3}_.mp3"
fi
else
echo -n "
Applying ${target_lufs} LUFS normalization: "
# apply
output="$("${ffmpeg}" -i "${1}" -af loudnorm=I=${target_lufs}:TP=-1:LRA=11:measured_I=${input_i}:measured_TP=${input_tp}:measured_LRA=${input_lra}:measured_thresh=${input_thres} -q:a 0 "${1%.mp3}_.mp3" 2>&1)"
retstat="${?}"
if [ ${retstat} -ne 0 ]; then
echo "failed.
ffmpeg failed to normalize file.
--- ffmpeg output ---
${output}
---"
exit 1
else
echo "Done.
Saved to: ${1%.mp3}_.mp3"
fi
fi
@avengerx
Copy link
Author

avengerx commented Nov 9, 2025

Am I reinventing the wheel here? I did a lot of research and didn't find any bash script -- or ffmpeg arguments -- for normalizing audio.
I ended up using a set of arguments provided by google search's AI that seemed to do the job just fine. Normalized over 3000 files and the playlist goes out loud well.

I'm using this to normalize old (and new) MP3 files I'm playing in an old Windows 7 machine with WinAmp, also in an old (but loud) stereo. When we're talking about 70+dB of audio intensity on a 50W RMS amplifier, it really matters when some songs are too low or too high.

The above script is meant for individual files' normalization. It won't fit, for instance, when a previous song "connects" to the next, in particular with albums' introductory tracks that are usually some instruments or FX that gets "normalized" really high in respect to their continuation in the actual "track one" of that album.

To automate the normalization of entire discographies, I'm using another script that I'll post and link from here.

Edit: Oops, I've noticed I can just add the second file to the gist itself, so there you go, the "full project" to normalize a whole library of MP3 files. It can probably easily be adjusted to other music/video formats.
Notice the "orchestrator" runs up to concurrent_jobs normalization calls in parallel, helping fill in CPU usage for faster conversion of whole libraries. You may adjust the value according to the host's CPU count for increased speed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment