Skip to content

Instantly share code, notes, and snippets.

@greg-randall
Created March 25, 2026 14:06
Show Gist options
  • Select an option

  • Save greg-randall/d5fb71199103d4ea8e311981b781d4ee to your computer and use it in GitHub Desktop.

Select an option

Save greg-randall/d5fb71199103d4ea8e311981b781d4ee to your computer and use it in GitHub Desktop.
Normalizes a folder of audio files to the same volume
import os
from pydub import AudioSegment
from pathlib import Path
import pyloudnorm as pyln
import soundfile as sf
import numpy as np
def process_audio(input_path, target_lufs=-14, threshold_db=-0.1, overwrite=False):
"""
Process audio file by:
1. Converting to WAV for processing
2. Normalizing to target LUFS
3. Applying hard limiter
4. Exporting back to MP3
Args:
input_path (str): Path to the input audio file
target_lufs (float): Target loudness in LUFS
threshold_db (float): Maximum allowed amplitude in dB
overwrite (bool): If True, replace original file
"""
input_path = Path(input_path)
if overwrite:
output_path = input_path
temp_path = input_path.parent / f".temp_{input_path.name}"
else:
output_path = input_path.parent / f"processed_{input_path.name}"
temp_path = output_path
try:
# Load the audio file and immediately convert to WAV in memory
audio = AudioSegment.from_mp3(input_path)
# Get the audio data as numpy array for LUFS processing
samples = np.array(audio.get_array_of_samples())
normalized_samples = samples / (1 << (8 * audio.sample_width - 1))
# Measure LUFS
meter = pyln.Meter(audio.frame_rate)
current_loudness = meter.integrated_loudness(normalized_samples)
# Calculate and apply loudness adjustment
loudness_gain = target_lufs - current_loudness
audio = audio.apply_gain(loudness_gain)
# Apply limiter (still in WAV format)
if audio.dBFS > threshold_db:
reduction_db = threshold_db - audio.dBFS
audio = audio.apply_gain(reduction_db)
# Export to temporary file first
audio.export(str(temp_path), format="mp3", parameters=["-q:a", "0"])
if overwrite:
# Atomic replace of original file
temp_path.replace(output_path)
print(f"Processed {input_path.name}:")
print(f" - Original loudness: {current_loudness:.1f} LUFS")
print(f" - Applied gain: {loudness_gain:.1f} dB")
if audio.dBFS > threshold_db:
print(f" - Limited peaks to {threshold_db} dB")
except Exception as e:
# Clean up temp file if it exists
if temp_path.exists():
temp_path.unlink()
print(f"Error processing {input_path}: {str(e)}")
def process_directory(directory_path, target_lufs=-14, threshold_db=-0.1, overwrite=False):
"""
Process all MP3 files in a directory.
Args:
directory_path (str): Path to the directory containing MP3 files
target_lufs (float): Target loudness in LUFS
threshold_db (float): Maximum allowed amplitude in dB
overwrite (bool): If True, replace original files
"""
directory = Path(directory_path)
if not directory.exists():
print(f"Directory {directory_path} does not exist!")
return
mp3_files = list(directory.glob("*.mp3"))
if not mp3_files:
print("No MP3 files found in the directory!")
return
print(f"Found {len(mp3_files)} MP3 files to process...")
print(f"Target loudness: {target_lufs} LUFS")
print(f"Peak limit: {threshold_db} dB")
print(f"Overwrite mode: {'enabled' if overwrite else 'disabled'}")
print()
for mp3_file in mp3_files:
process_audio(str(mp3_file), target_lufs, threshold_db, overwrite)
print("\nProcessing complete!")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Process audio files with minimal quality loss")
parser.add_argument("directory", help="Directory containing MP3 files")
parser.add_argument("--target-lufs", type=float, default=-14,
help="Target loudness in LUFS (default: -14)")
parser.add_argument("--threshold-db", type=float, default=-0.1,
help="Maximum allowed amplitude in dB (default: -0.1)")
parser.add_argument("--overwrite", action="store_true",
help="Replace original files instead of creating new ones")
args = parser.parse_args()
process_directory(args.directory, args.target_lufs, args.threshold_db, args.overwrite)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment