ラウドネス LUFS 測定アルゴリズムの実装

LUFS単位のラウドネス測定プログラムを実装しました。ラウドネスというのはヒトが感じる音の大きさのことをいい、LUFS はデジタル信号におけるラウドネスの単位となります。

本記事では、テレビや音楽ストリーミングサービスで使われているラウドネス測定アルゴリズムについて紹介します。

LUFS について

LUFS(Loudness units relative to full scale)とは放送業界や音楽ストリーミングサービスなどで使われるラウドネスの単位です。

LKFS(Loudness K-weighted relative to full scale)というラウドネス単位も使われますが LUFS と同じ意味です。Wikipedia によれば、元々の名称は LKFS でしたが、EBU(欧州放送連合)がLKFSは科学的命名規則にあってないとかで LUFS にすべきと主張したみたいです[1]。

昔のテレビではチャンネルを変えると音量が急に変わったりしたことで、視聴者が不快に感じることがありました。いまでも Youtube の広告動画とかで爆音になるとき(最近だとYoutubeも音量調整の機能があるのかな?)があるかと思いますが、それと似たようなことがテレビで起きていました。

そこでITU-R(国際電気通信連合無線通信部門)がディジタル放送のラウドネス測定アルゴリズムを国際的に統一するための勧告(ITU-R BS.1770-2)を2011年に作成して、一つの番組の平均ラウドネス値を-24±1 LKFS にするように定めました[2]。

この ITU-R BS.1770 のラウドネス測定アルゴリズムが放送業界以外にも広まり、現在ではSpotifyなどの音楽ストリーミングサービスでも使われています。

ラウドネス LUFS 測定方法

本記事では、ITU-R BS.1770-5に基づくラウドネス測定アルゴリズムを実装したいと思います(正直言って、アルゴリズム自体はITU-R BS.1770-2 から変化はないと思いますが...)。

まず、ラウドネス測定のブロック図は以下となります。

図:ラウドネス測定のブロック図
図:ラウドネス測定のブロック図

BS.1770 には5.1サラウンド方式のブロック図の記載がありますが、2 チャンネルステレオ方式のブロック図にしています。

ラウドネス測定手順は以下です。

  1. K特性フィルタを畳み込む
  2. ゲーティングブロックごとに2乗平均を求める
  3. ゲーティングブロックごとにラウドネスを求める
  4. 絶対ゲーティングによる無音ブロックの除去
  5. 相対ゲーティングによる小さい音ブロックの除去
  6. 残ったブロックから平均ラウドネス値を求める

K特性フィルタ

最初にK特性フィルタを畳み込みます。

K特性フィルタは頭部形状を考慮したフィルタとRLB(修正B特性)フィルタを縦続接続したものとなっており、ブロック図は以下です。

図:K特性フィルタのブロック図
図:K特性フィルタのブロック図

各フィルタ係数の値(サンプリング周波数:48kHz)は以下です。

表:頭部形状を考慮したフィルタ係数
a1 −1.69065929318241
a2 0.73248077421585
b0 1.53512485958697
b1 −2.69169618940638
b2 1.19839281085285
表:RLBフィルタ係数
c1 −1.99004745483398
c2 0.99007225036621
d0 1.0
d1 −2.0
d2 1.0

また、頭部形状を考慮したフィルタ、RLBフィルタ、K特性フィルタの周波数特性は以下のようになります。

図:K特性フィルタの周波数特性
図:K特性フィルタの周波数特性

頭部形状を考慮したフィルタについては頭部を硬質球体上に置き換えた場合の周波数特性となっております。スピーカーから耳に音が届くまでの間、頭部によって音が変化するのでその影響を考慮しています。

RLBフィルタについてはヒトの耳が 100 Hz 以下の音が聴き取りづらくなるという聴覚特性を考慮しています。

ブロックごとの2乗平均

次に信号をスライド幅 100ms、 ブロック長 400ms のゲーティングブロックごとに切り出して、各ブロックで2乗平均を求めます。

図:ゲーティングブロック
図:ゲーティングブロック

2乗平均については以下の式で求めます。

$$
z_{i,j} = \frac{1}{N}\sum_{n=0}^{N-1} y_{i,j}[n]^2 \hspace{1em} (i=L,R)
$$

ここで、N はブロックごとのサンプル数、n は各ブロックにおけるサンプル番号、j はブロック番号です。

信号終端の 400ms に満たないゲーティングブロックについては計算に含めません。

ブロックごとのラウドネス

ブロックごとのラウドネス lj [LUFS] を以下のようにして計算します。

$$
l_j = -0.691 + 10\log_{10} \sum_{i} G_{i} \cdot z_{i,j}
$$

各チャンネルの重みづけ Gi についてはスピーカーの位置に応じて変えます。ステレオ方式の場合は GL=1.0、 GR=1.0 ですが、5.1 サラウンド方式の場合は左、右、中央が1.0、左後と右後が 1.41 となります。

0.691 というのはK特性フィルタの 1kHz における値です。0.691を差し引くことで 1kHz のフルスケール正弦波を入力したときにラウドネス値が 0 LUFS となるようにします。

ゲーティング

絶対ゲーティングによって無音区間の除去、相対ゲーティングによって小さい音区間を除去します。

絶対ゲーティングではゲーティングブロックのラウドネスが -70 LUFS 以下の場合、そのゲーティングブロックを除去します。

相対ゲーティングでは絶対ゲーティング後に残ったブロックから平均ラウドネス値を求めて、その値から 10dB 低い値を相対閾値とします。それから、相対閾値以下のゲーティングブロックを除去します。相対閾値 \(\Gamma_r\) [LUFS]の計算式は以下です。

$$
\Gamma_r = -0.691 -10 + 10\log_{10} \sum_{i} G_{i} \cdot \left(\frac{1}{|J_r|}\sum_{J_r}z_{i,j}\right)
$$

ここで、\(J_r\) は絶対ゲーティング後に残ったブロックの集合、\(|J_r|\) は絶対ゲーティング後のブロック数です。

平均ラウドネス値

最後に、ゲーティング後に残ったゲーティングブロックから以下の式で平均ラウドネス値 L [LUFS] を求めます。

$$
L = -0.691 + 10\log_{10} \sum_{i} G_{i} \cdot \left(\frac{1}{|J_g|}\sum_{J_g}z_{i,j}\right)
$$

ここで、\(J_g\) はゲーティング後に残ったブロックの集合、\(|J_g|\) はゲーティング後のブロック数です。

プログラム

ソースコード

LUFS単位のラウドネスを測定するソースコード measure_loudness.py は以下です。

import numpy as np
import soundfile as sf
import scipy.signal as sg
import resampy

# 入力WAVデータ
wav_in_name = "input.wav"

# WAV を読み込む
x, fs = sf.read(wav_in_name, always_2d=True)

# リサンプリング
x_r = resampy.resample(x, fs, 48000, axis=0)

# フィルタ係数
a = np.array([1.0, -1.69065929318241, 0.73248077421585])
b = np.array([1.53512485958697, -2.69169618940638, 1.19839281085285])
c = np.array([1.0, -1.99004745483398, 0.99007225036621])
d = np.array([1.0, -2.0, 1.0])

# 定数
zmin = 1e-10               # 2乗平均の最小
block_len = int(48000 * 0.4)  # ブロック長 400ms
slide     = int(48000 * 0.1)  # スライド長 100ms
length  = x_r.shape[0]     # 信号の長さ
channel = x_r.shape[1]     # チャンネル数

# LR ch の重みづけ
if channel == 1:
    G_LR = [1.0]
elif channel == 2:
    G_LR = [1.0, 1.0]

# K特性フィルタを畳み込む
y = sg.lfilter(b=b, a=a, x=x_r, axis=0)
y = sg.lfilter(b=d, a=c, x=y,   axis=0)

# ゲーティングブロックごとに2乗平均を求める
n_block = (length - block_len) // slide + 1 # ブロック数
z = np.zeros((n_block, channel))
for j in range(n_block):
    z[j,:] = np.mean(y[j*slide:j*slide+block_len,:]**2, axis=0) 

# 絶対ゲーテイング
l_tmp = np.sum(np.maximum(zmin, z*G_LR), axis=1) 
l = -0.691 + 10.0 * np.log10(l_tmp)
abs_gate = (l > -70.0) # 絶対閾値を超える要素は True

# 相対ゲーテイング
z_abs = np.zeros_like(z)  # z と同じ形のNumpy配列作成
for i in range(channel):  # 絶対閾値以下の要素は 0 にする
    z_abs[:,i] = z[:,i] * abs_gate
z_mean_r = np.sum(z_abs, axis=0)/np.sum(abs_gate)
rel_thd_tmp = np.sum(z_mean_r*G_LR)
rel_thd = -0.691 -10.0 + 10.0 * np.log10(rel_thd_tmp) # 相対閾値
rel_gate = (l > rel_thd) # 相対閾値を超える要素は True

# 平均ラウドネス値を算出
gate = abs_gate & rel_gate
z_gate = np.zeros_like(z)  # z と同じ形のNumpy配列作成
for i in range(channel):   # 絶対閾値と相対閾値以下の要素は 0 にする
    z_gate[:,i] = z[:,i] * gate
z_mean_g = np.sum(z_gate, axis=0)/np.sum(gate)
Loudness_tmp = np.sum(z_mean_g*G_LR)
Loudness = -0.691 + 10.0 * np.log10(Loudness_tmp)

# ラウドネスを表示
print("Loudness: {0:.2f} [LUFS]".format(Loudness))

10行目:モノラルとステレオどちらも対応できるように always_2d=True でnumpy配列の次元数が常に2次元となるようにします。

13行目:12~16行目のフィルタ係数は fs=48kHz 用のため、fsが48kHzとなるようにリサンプリングします。

45~46行目:np.log10 にゼロを入力しないように、np.maximum を使用して入力値は最小で zmin=10^(-10) とします。

実行方法

(1) プログラムを実行するディレクトリにソースコード(measure_loudness.py)と入力 WAV データを格納する。

(2) ソースコード6~7行目の入力データ名を修正する。

# 入力WAVデータ
wav_in_name = "input.wav"

(3) 以下のコマンドで python を実行することで、 WAV データのラウドネスが表示される。

$ python measure_loudness.py
Loudness: -14.1 [LUFS]

pyloudnormとの比較

作成したプログラムとpythonのライブラリである pyloudnorm で出力される値を比較しました。

pyloudnormを使ったソースコード pyloudnorm_example.py は以下をクリックして確認できます。

pyloudnorm_example.py (クリックで展開)
import soundfile as sf
import pyloudnorm as pyln

data, rate = sf.read("input.wav")
meter = pyln.Meter(rate) # create BS.1770 meter
loudness = meter.integrated_loudness(data) # measure loudness

print(loudness)

約9分間の朗読と約1分間の音楽のラウドネス測定結果は以下の表のようになります。

表:ラウドネス測定結果
CH 数 ライブラリ 自作
朗読 モノ -24.08 LUFS -24.05 LUFS
音楽 ステレオ -16.37 LUFS -16.35 LUFS
せとち
測定結果は小数点第一位まで自作とライブラリは同じなので、実装に問題はないかな。

おわりに

本記事ではラウドネス測定アルゴリズムについて紹介しました。

個人的には、ラウドネス測定に使用するK特性フィルタはテレビに特化したフィルタだなと思いました。音楽をヘッドホンやイヤホンで聴取する場合、頭部形状を考慮したフィルタは必要ないですし、RLBフィルタについても高域については考えていないため、音楽用のフィルタも用意したほうが良いのでは?と感じます。

まあ、LUFS がスタンダードに使われているので、いまさら新しいアルゴリズムを採用するのは難しいのかなとも思います。

参考文献
[1] 英語版 Wikipedia,  “LUFS”,  https://en.wikipedia.org/wiki/LUFS,  (参照2024-05-14).
[2] 大串健吾,  ”音響聴覚心理学”,  誠信書房,  2019.
[3] ITU,  "BS.1770 : Algorithms to measure audio programme loudness and true-peak audio level",  https://www.itu.int/rec/R-REC-BS.1770,  (参照2024-05-19).