본문 바로가기
Python

[Python] EBU R128 기준으로 오디오 라우드니스 정규화하기 (배치 처리)

by teamnova 2025. 9. 18.
728x90

 TTS/ASR 데이터는 파일마다 소리가 들쑥날쑥하면 성능이 흔들릴 수 있습니다.

EBU R128 (LUFS) 표준으로 라우드니스를 맞춰주면 학습/추론 안정성이 올라갑니다.

아래 스크립트는 폴더 단위로 .wav 파일을 읽어 타깃 LUFS(-23, -18, -16 등)에 맞춰 정규화해서 저장합니다 

 

 

1. 먼저 필요한 라이브러리를 설치해줍니다 

pip install soundfile numpy pyloudnorm

 

 

2. 라우드니스 정규화 배치 스크립트 

import os
import argparse
import numpy as np
import soundfile as sf
import pyloudnorm as pyln

def loudness_normalize(y, sr, target_lufs=-18.0, tp_limit_db=-1.0, soft_limit=True):
    """
    EBU R128 기반 라우드니스 정규화.
    - target_lufs: 원하는 통합 라우드니스(LUFS)
    - tp_limit_db: True Peak 상한(dBFS). soft_limit=True면 살짝 리미팅
    """
    meter = pyln.Meter(sr, block_size=0.400)  # 400ms 블록 권장
    loudness = meter.integrated_loudness(y)
    gain_db = target_lufs - loudness
    y_norm = pyln.normalize.loudness(y, loudness, target_lufs)

    if soft_limit:
        # True Peak 초과 시 간단 소프트 리미팅(시그모이드 계열)
        peak = np.max(np.abs(y_norm))
        tp_limit = 10 ** (tp_limit_db / 20.0)
        if peak > tp_limit:
            # 스케일 후 완만한 컴프
            ratio = tp_limit / peak
            y_norm = y_norm * ratio
            # 추가로 아주 얕은 소프트클립(선택)
            y_norm = np.tanh(y_norm * 1.2)

    # -1.0 ~ 1.0 범위 보장
    y_norm = np.clip(y_norm, -1.0, 1.0)
    return y_norm, loudness, target_lufs, gain_db

def process_file(in_path, out_path, target_lufs=-18.0, tp_limit_db=-1.0, soft_limit=True):
    y, sr = sf.read(in_path, always_2d=False)
    # 스테레오/모노 모두 지원: 내부적으로 float32로 캐스팅 권장
    if y.dtype != np.float32:
        y = y.astype(np.float32)

    # 채널별/합성 라우드니스 처리: 간단하게 스테레오는 mid/side 고려 없이 합성으로 처리
    if y.ndim == 2:  # (samples, channels)
        # 채널 평균으로 라우드니스 계산 → 동일 게인 적용
        y_mono = y.mean(axis=1)
        y_norm_mono, l_in, l_tgt, gain_db = loudness_normalize(
            y_mono, sr, target_lufs, tp_limit_db, soft_limit
        )
        gain = 10 ** (gain_db / 20.0)
        y_norm = (y * gain).astype(np.float32)
        # 리미터 처리 재적용
        peak = np.max(np.abs(y_norm))
        tp_limit = 10 ** (tp_limit_db / 20.0)
        if soft_limit and peak > tp_limit:
            ratio = tp_limit / peak
            y_norm = y_norm * ratio
            y_norm = np.tanh(y_norm * 1.2).astype(np.float32)
        loudness_out = pyln.Meter(sr).integrated_loudness(y_norm.mean(axis=1))
    else:
        y_norm, l_in, l_tgt, gain_db = loudness_normalize(
            y, sr, target_lufs, tp_limit_db, soft_limit
        )
        y_norm = y_norm.astype(np.float32)
        loudness_out = pyln.Meter(sr).integrated_loudness(y_norm)

    os.makedirs(os.path.dirname(out_path), exist_ok=True)
    sf.write(out_path, y_norm, sr, subtype="PCM_16")

    return {
        "in_lufs": l_in if y.ndim == 1 else None,
        "out_lufs": loudness_out,
        "target_lufs": target_lufs,
        "gain_db": gain_db if y.ndim == 1 else None
    }

def main():
    ap = argparse.ArgumentParser(description="Batch EBU R128 loudness normalize for WAV files")
    ap.add_argument("--in_dir", required=True, help="입력 폴더")
    ap.add_argument("--out_dir", required=True, help="출력 폴더")
    ap.add_argument("--target_lufs", type=float, default=-18.0, help="목표 LUFS (예: -23 방송, -16/-14 팟캐스트)")
    ap.add_argument("--tp_limit_db", type=float, default=-1.0, help="True Peak 상한(dBFS)")
    ap.add_argument("--no_soft_limit", action="store_true", help="소프트 리미팅 비활성화")
    args = ap.parse_args()

    print(f"[INFO] target={args.target_lufs} LUFS, TP limit={args.tp_limit_db} dBFS, soft_limit={not args.no_soft_limit}")
    os.makedirs(args.out_dir, exist_ok=True)

    for name in os.listdir(args.in_dir):
        if not name.lower().endswith(".wav"):
            continue
        in_path = os.path.join(args.in_dir, name)
        out_path = os.path.join(args.out_dir, name)
        try:
            info = process_file(
                in_path,
                out_path,
                target_lufs=args.target_lufs,
                tp_limit_db=args.tp_limit_db,
                soft_limit=not args.no_soft_limit
            )
            print(f"[OK] {name}: → out {info['out_lufs']:.2f} LUFS (target {info['target_lufs']})")
        except Exception as e:
            print(f"[ERR] {name}: {e}")

if __name__ == "__main__":
    main()

 

 

권장 세팅 값입니다. 

- 방송/내래이션 스타일: -23 LUFS

- 팟캐스트/대화체(조금 더 크게): -16 LUFS, True Peak -1 dBFS