Created
March 25, 2026 14:06
-
-
Save greg-randall/d5fb71199103d4ea8e311981b781d4ee to your computer and use it in GitHub Desktop.
Normalizes a folder of audio files to the same volume
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
| 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