Last active
November 9, 2025 04:13
-
-
Save avengerx/53f0cabf9ae0bd129d80f2f95d3322af to your computer and use it in GitHub Desktop.
Bash script to normalize audio from files using the `ffmpeg` tool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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!" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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_jobsnormalization 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.