理解音频响度:LUFS 标准及其计算实现
一、什么是 LUFS?
LUFS(Loudness Units relative to Full Scale)是音频工程中用于测量感知响度的标准单位。它已成为广播、流媒体和音乐制作领域的行业标准,用于确保不同音频内容具有一致的响度水平。
LUFS 是 ITU-R BS.1770 标准的核心概念,该标准由国际电信联盟制定,旨在解决所谓的"响度战争"问题 - 即不同节目或歌曲之间响度差异过大的现象。
LUFS 的重要性
- 一致性体验:确保观众在不同节目间切换时不需要频繁调整音量
- 保护听力:防止过度压缩和过大的响度对听众听力造成损害
- 提升音质:避免过度压缩导致的音频质量下降
- 行业标准:被 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 标准:
import numpy as np
from scipy import signal
import warnings
import soundfile as sfclass IIRFilter:"""IIR滤波器实现"""def __init__(self, G, Q, fc, rate, filter_type, passband_gain=1.0):self.G = Gself.Q = Qself.fc = fcself.rate = rateself.filter_type = filter_typeself.passband_gain = passband_gainself.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) * alphaa1 = 2 * ((A - 1) - (A + 1) * np.cos(w0))a2 = (A + 1) - (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alphaelif 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 * Kb0 = (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.0a1 = 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 * Kb0 = 1.0b1 = -2.0b2 = 1.0a0 = 1.0a1 = 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 = rateself.block_size = block_sizeself._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_channelsif 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 * stepend = start + samples_per_blockblock = 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 = -70above_absolute = l >= absolute_thresholdif 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 LUFSdef _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.0elif data.dtype == np.int32:data = data.astype(np.float32) / 2147483648.0elif data.dtype == np.uint8:data = (data.astype(np.float32) - 128) / 128.0else: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)表示。