Skip to content

Instantly share code, notes, and snippets.

@greg-randall
Created February 19, 2025 00:52
Show Gist options
  • Select an option

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

Select an option

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

Revisions

  1. greg-randall created this gist Feb 19, 2025.
    126 changes: 126 additions & 0 deletions norm.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,126 @@
    """
    This tool normalizes the loudness of MP3 audio files to a consistent level. Here's how to use it:
    Run the script with a directory path: python script.py /path/to/mp3s
    By default, it sets audio to -14 LUFS (loudness units) and prevents clipping at -0.1dB
    It creates new files prefixed with "processed_" unless you use --overwrite
    You can customize the target loudness with --target-lufs and peak limit with --threshold-db
    The tool analyzes each MP3 file, adjusts its volume to the target loudness, applies a limiter to prevent distortion, and saves the processed version.
    """


    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)