一、什么是 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 标准:
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)表示。