理解音频响度:LUFS 标准及其计算实现

一、什么是 LUFS?

LUFS(Loudness Units relative to Full Scale)是音频工程中用于测量感知响度的标准单位。它已成为广播、流媒体和音乐制作领域的行业标准,用于确保不同音频内容具有一致的响度水平。

LUFS 是 ITU-R BS.1770 标准的核心概念,该标准由国际电信联盟制定,旨在解决所谓的"响度战争"问题 - 即不同节目或歌曲之间响度差异过大的现象。

LUFS 的重要性

  1. 一致性体验:确保观众在不同节目间切换时不需要频繁调整音量
  2. 保护听力:防止过度压缩和过大的响度对听众听力造成损害
  3. 提升音质:避免过度压缩导致的音频质量下降
  4. 行业标准:被 Netflix、YouTube、Spotify 等平台广泛采用

二、LUFS 的计算原理

ITU-R BS.1770 标准定义了计算 LUFS 的四个关键步骤:

1. K 加权滤波

K 加权滤波器模拟人耳对不同频率的敏感度,包含两个部分:

• 高频架式滤波器:提升高频部分(约 1500Hz)

• 高通滤波器:衰减低频部分(约 38Hz)
2. 分块处理

音频被分割成固定长度的块(通常为 400ms),相邻块之间有 75% 的重叠。这种重叠处理确保了响度计算的稳定性。
3. 门限处理

计算包含两个门限:

• 绝对门限:-70 LUFS,低于此值的块被忽略

• 相对门限:比超过绝对门限的块的平均响度低 10dB
4. 集成响度计算

只考虑超过两个门限的音频块,计算它们的平均响度值作为最终结果。

三、Python 实现 LUFS 计算

以下是完整的 Python 实现代码,基于 ITU-R BS.1770-4 标准:

python 复制代码
import numpy as np
from scipy import signal
import warnings
import soundfile as sf


class IIRFilter:
    """IIR滤波器实现"""

    def __init__(self, G, Q, fc, rate, filter_type, passband_gain=1.0):
        self.G = G
        self.Q = Q
        self.fc = fc
        self.rate = rate
        self.filter_type = filter_type
        self.passband_gain = passband_gain
        self.b, self.a = self.generate_coefficients()

    def generate_coefficients(self):
        """生成滤波器系数"""
        A = 10 ** (self.G / 40.0)
        w0 = 2.0 * np.pi * (self.fc / self.rate)
        alpha = np.sin(w0) / (2.0 * self.Q)

        if self.filter_type == 'high_shelf':
            b0 = A * ((A + 1) + (A - 1) * np.cos(w0) + 2 * np.sqrt(A) * alpha)
            b1 = -2 * A * ((A - 1) + (A + 1) * np.cos(w0))
            b2 = A * ((A + 1) + (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alpha)
            a0 = (A + 1) - (A - 1) * np.cos(w0) + 2 * np.sqrt(A) * alpha
            a1 = 2 * ((A - 1) - (A + 1) * np.cos(w0))
            a2 = (A + 1) - (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alpha
        elif self.filter_type == 'high_shelf_DeMan':
            K = np.tan(np.pi * self.fc / self.rate)
            Vh = np.power(10.0, self.G / 20.0)
            Vb = np.power(Vh, 0.499666774155)
            a0_ = 1.0 + K / self.Q + K * K
            b0 = (Vh + Vb * K / self.Q + K * K) / a0_
            b1 = 2.0 * (K * K - Vh) / a0_
            b2 = (Vh - Vb * K / self.Q + K * K) / a0_
            a0 = 1.0
            a1 = 2.0 * (K * K - 1.0) / a0_
            a2 = (1.0 - K / self.Q + K * K) / a0_
        elif self.filter_type == 'high_pass_DeMan':
            K = np.tan(np.pi * self.fc / self.rate)
            a0_ = 1.0 + K / self.Q + K * K
            b0 = 1.0
            b1 = -2.0
            b2 = 1.0
            a0 = 1.0
            a1 = 2.0 * (K * K - 1.0) / a0_
            a2 = (1.0 - K / self.Q + K * K) / a0_
        else:
            raise ValueError(f"不支持的滤波器类型: {self.filter_type}")

        return np.array([b0, b1, b2]), np.array([a0, a1, a2])

    def apply_filter(self, data):
        """应用滤波器到音频数据"""
        return self.passband_gain * signal.lfilter(self.b, self.a, data)


class LoudnessMeter:
    """响度计实现"""

    def __init__(self, rate, block_size=0.400):
        self.rate = rate
        self.block_size = block_size
        self._setup_filters()

    def _setup_filters(self):
        """设置DeMan滤波器"""
        self.filters = {
            'high_shelf': IIRFilter(
                G=3.99984385397,
                Q=0.7071752369554193,
                fc=1681.9744509555319,
                rate=self.rate,
                filter_type='high_shelf_DeMan'
            ),
            'high_pass': IIRFilter(
                G=0.0,
                Q=0.5003270373253953,
                fc=38.13547087613982,
                rate=self.rate,
                filter_type='high_pass_DeMan'
            )
        }

    def integrated_loudness(self, data):
        """计算集成响度"""
        # 验证输入数据
        self._validate_audio(data)

        # 处理单声道输入
        if data.ndim == 1:
            data = data.reshape(-1, 1)

        num_channels = data.shape[1]
        num_samples = data.shape[0]

        # 声道增益 (左/右/中:1.0, 环绕:1.41)
        G = [1.0] * num_channels
        if num_channels >= 4:
            G[3] = 1.41  # 左环绕
        if num_channels >= 5:
            G[4] = 1.41  # 右环绕

        # 应用K加权滤波器
        filtered_data = data.copy()
        for ch in range(num_channels):
            for filter_name in ['high_pass', 'high_shelf']:
                filtered_data[:, ch] = self.filters[filter_name].apply_filter(filtered_data[:, ch])

        # 分块处理参数
        samples_per_block = int(self.block_size * self.rate)
        step = int(samples_per_block * 0.25)  # 75%重叠
        num_blocks = (num_samples - samples_per_block) // step + 1

        # 计算每个块的均方值
        z = np.zeros((num_channels, num_blocks))
        for ch in range(num_channels):
            for j in range(num_blocks):
                start = j * step
                end = start + samples_per_block
                block = filtered_data[start:end, ch]
                z[ch, j] = np.mean(block ** 2)

        # 计算每个块的响度
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", category=RuntimeWarning)
            l = np.array([
                -0.691 + 10 * np.log10(np.sum([G[i] * z[i, j] for i in range(num_channels)]))
                for j in range(num_blocks)
            ])

        # 绝对门限处理 (-70 LUFS)
        absolute_threshold = -70
        above_absolute = l >= absolute_threshold

        if not np.any(above_absolute):
            return -np.inf

        # 计算相对门限
        z_avg_abs = np.array([
            np.mean(z[i, above_absolute]) for i in range(num_channels)
        ])
        relative_threshold = -0.691 + 10 * np.log10(
            np.sum([G[i] * z_avg_abs[i] for i in range(num_channels)])
        ) - 10

        # 应用相对门限
        above_both = (l > relative_threshold) & (l > absolute_threshold)

        if not np.any(above_both):
            return -np.inf

        # 计算最终响度
        z_avg_final = np.array([
            np.mean(z[i, above_both]) for i in range(num_channels)
        ])
        LUFS = -0.691 + 10 * np.log10(
            np.sum([G[i] * z_avg_final[i] for i in range(num_channels)])
        )

        return LUFS

    def _validate_audio(self, data):
        """验证音频数据"""
        if not isinstance(data, np.ndarray):
            raise ValueError("输入必须是numpy数组")

        if not np.issubdtype(data.dtype, np.floating):
            raise ValueError("输入必须是浮点类型")

        if data.ndim == 2 and data.shape[1] > 5:
            raise ValueError("音频最多支持5个声道")

        if data.shape[0] < self.block_size * self.rate:
            raise ValueError("音频长度必须大于块大小")


# 读取音频文件
audio_path = "test.wav"
data, samplerate = sf.read(audio_path)

# 确保音频数据是浮点数格式
if data.dtype != np.float32 and data.dtype != np.float64:
    if data.dtype == np.int16:
        data = data.astype(np.float32) / 32768.0
    elif data.dtype == np.int32:
        data = data.astype(np.float32) / 2147483648.0
    elif data.dtype == np.uint8:
        data = (data.astype(np.float32) - 128) / 128.0
    else:
        data = data.astype(np.float32)
        max_val = np.max(np.abs(data))
        if max_val > 1.0:
            data = data / max_val

# 创建响度计
meter = LoudnessMeter(rate=samplerate)

# 计算集成响度
loudness = meter.integrated_loudness(data)
print(f"LUFS响度: {loudness:.2f} LUFS")

四、与Audition一致性验证



五、行业标准参考值

不同平台和场景有不同的 LUFS 目标值:

平台/场景 目标 LUFS 峰值限制
广播 (EBU R128) -23.0 -1.0 dBTP
流媒体 (Spotify) -14.0 -1.0 dBTP
流媒体 (YouTube) -14.0 -1.0 dBTP
流媒体 (Apple Music) -16.0 -1.0 dBTP
播客 -16.0 至 -20.0 -1.0 dBTP
  • 目标 LUFS:表示不同平台或场景下推荐的响度水平。
  • 峰值限制:表示音频信号的最大峰值限制,通常以 dBTP(dB True Peak)表示。