コンテンツにスキップ

第6回 ピッチ・エネルギー・ゼロ交差などの前処理

この回の資料


本文の項目番号とHTMLスライドのページ番号は異なる。

本日の到達目標

  1. ZCR・RMS・ピッチ(F0)の定義と計算方法を説明できる
  2. これらの特徴量による有声・無声・低振幅区間の推定方法と、その限界を説明できる
  3. Pythonで波形と3特徴量(ZCR・RMS・F0)を同時可視化できる
  4. 音声活動検出(VAD)の基本的な仕組みを説明できる
  5. 発展:ZCR・RMS・自己相関がそれぞれ何を測っているかを、式から自分で導ける

進化の視点:声の高さは、体の情報をどこまで運ぶか

基本周波数(F0)は、声帯の長さ・振動に関わる質量・張力などの影響を受け、発話中にも変わる。ただし、低い声なら大きい体だと、個人の体格をそのまま逆算できるわけではない。

成人男女の研究を統合したメタ分析では、同性内での体格との関連はF0よりもフォルマントに基づく声道長の指標の方が強かった。群の平均差と、群内の個人差を分ける必要がある(Pisanski et al., 2014)。

技術への接続:ピッチ抽出で得るのはF0の推定値であり、年齢・性別・体格そのものではない。属性推定に利用するには、別途、対象集団のデータで妥当性や誤差を評価する必要がある。音声の利用目的については第13回で扱う。

問い:F0と体格の関連を検証する際、年齢や性別などの群間差と、同じ群内での関連をどう分けて調べるか。


項目1: フレーム分割と窓掛けの復習

第4回・第5回の復習

多くの音声特徴量は、短い区間(フレーム)ごとに計算する。ただし、フレーム長や窓関数は特徴量ごとに選ぶ。本日のRMS・ZCRは、波形の各フレームから直接求める。

フレーム分割の例(22,050 Hz、フレーム長1,024、ホップ512)

フレーム サンプル位置 前フレームとの重なり
1 0〜1,023 —
2 512〜1,535 50%
3 1,024〜2,047 50%

主要パラメータ(再確認)

パラメータ 例 備考
フレーム長 25 msなら16 kHzで400サンプル 時間長=サンプル数÷サンプリング周波数
ホップサイズ 10 msなら16 kHzで160サンプル 25 ms窓に対して60%の重なり
窓関数 スペクトル分析ではHann窓など スペクトル漏れを軽減する。完全に防ぐわけではない

本日の実装では、低いF0の周期を複数含めるため、22,050 Hzでフレーム長2,048(約92.9 ms)、ホップ512(約23.2 ms)を用いる。RMS・ZCR・F0に同じ時間軸を使うための演習設定であり、常に最適な長さという意味ではない。

短い区間では音声の性質をおおむね一定と近似し、変化を追跡する。窓を長くすると周期の情報を得やすい一方、短い変化を捉えにくくなる。RMSの計算自体に定常性の仮定は必須ではない(RMS公式ドキュメント)。


項目2: ゼロ交差率(ZCR)の定義と計算式

ゼロ交差とは

隣り合うサンプルの符号が変化することを「ゼロ交差」と呼ぶ。例えば符号列「+、+、−、−、+」には2回の交差がある。

ZCRの定義式

ここでは、フレーム内の符号変化の回数をサンプル数で割る、librosaに合わせた定義を用いる:

\[ \text{ZCR}[m] = \frac{1}{2N} \sum_{n=1}^{N-1} \left| s(x_m[n]) - s(x_m[n-1]) \right| \]
  • \(N\):フレーム内のサンプル数(サンプル番号は0〜\(N-1\))
  • \(s(x)\):\(x\geq0\)なら+1、\(x<0\)なら−1とする2値の符号関数
  • 0を正側に含める規約を採用する。librosaには微小値を0とみなす閾値もある

ZCRの単位

  • ZCRは無次元。上の例なら \(2/5=0.4\)
  • 1秒あたりの交差回数への換算は \(\text{ZCR}\times f_s\)。F0そのものではない
  • 分母を隣接対の数\(N-1\)にする別の定義もあるため、比較時は規約をそろえる

ZCRの直感的意味

傾向 読み取れることと限界
高い 符号が頻繁に反転する。無声摩擦音や広帯域雑音などで見られる
低い 符号の反転が少ない。有声母音で見られるが、有声であることの証明ではない
完全なゼロ信号 この規約ではZCR=0。小さな雑音を含む非発話区間とは区別する

ZCRはサンプリング周波数や雑音にも依存し、共通の閾値だけで音の種類を確定できない(公式ドキュメント)。

導出①:なぜこの式で「交差の回数」が数えられるのか

式の中身は \(|s(x[n]) - s(x[n-1])|\) である。ここで定義した符号関数は +1 か −1 しか取らないので、隣り合う2サンプルの符号の組み合わせは4通りしかない。

前のサンプル 後のサンプル 差 絶対値
+1 +1 0 0
−1 −1 0 0
+1 −1 +2 2
−1 +1 −2 2

符号が変わらなければ 0、変われば 2 になる。だから総和は交差した回数のちょうど2倍である。先頭の \(1/(2N)\) にある 2 は、この 2 を打ち消すためにある。残る \(1/N\) が「1サンプルあたり何回交差したか」の率にしている。

なぜ周波数の粗い指標になるのか:正弦波 \(\sin(2\pi f t)\) は1周期に2回ゼロを横切る。1秒間に \(f\) 周期あるので、1秒あたりの交差回数は \(2f\) である。したがって

\[\text{ZCR}[m] \times f_s \approx 2f\]

となる(有限長フレームでは端の影響がある)。同じサンプリング周波数なら、純音の周波数が高いほど交差が増える。複合音では成分の振幅や位相にも左右されるため、同じ関係をそのまま当てはめない。

ZCR はピッチの代わりにはならない

上の対応は純音のときの話である。倍音や雑音が重なると交差の回数が変わり、基本周波数との単純な対応は崩れる。ZCR が答えられるのは「速い成分が多いか少ないか」までで、何 Hz かは答えられない。ピッチを知りたいならスライド6の自己相関を使う。

使った数学:符号関数の場合分けと、正弦波の周期とゼロ点の関係(三角関数)。


項目3: RMSとエネルギーの関係

RMSとは(Root Mean Square:二乗平均平方根)

信号の振幅の大きさを要約する指標です。RMS自体はエネルギーではなく、同じフレーム長なら、RMSの二乗がフレームの二乗和(信号エネルギー)に比例します。

\[ \text{RMS}[m] = \sqrt{\frac{1}{N} \sum_{n=0}^{N-1} x_{m}[n]^2} \]
  • \(x_{m}[n]\):フレーム \(m\) の \(n\) 番目のサンプル
  • 大きな音 → 各サンプルの絶対値が大きい → RMSが高い

なぜ「二乗」するか

  • 音声波形はプラスとマイナスを繰り返すため、単純平均すると0に近くなる
  • 二乗することで符号を消し、平均二乗振幅を計算できる
  • その後平方根を取ることで元の単位(振幅)に戻す

デシベル(dB)表現

\[ L[m] = 20 \log_{10}\left(\frac{\max(\text{RMS}[m],\epsilon)}{A_{\mathrm{ref}}}\right) \]

ここで\(A_{\mathrm{ref}}>0\)は基準振幅、\(\epsilon>0\)は対数を計算するための下限値。本日のコードは\(A_{\mathrm{ref}}=1\)を使う。録音サンプル値に対するdBであり、校正された音圧レベル(dB SPL)ではない(公式ドキュメント)。

RMSと低振幅区間の検出

  • 無音区間:RMSがほぼ0(または非常に低いdB値)
  • 発話でも弱い子音は低くなり、非発話の大きな雑音は高くなる
  • 閾値を設けて区間の候補を得られるが、RMSだけで発話の有無は確定できない
導出②:なぜ二乗して、平均して、平方根を取るのか

波形をそのまま平均すると

\[\frac{1}{N}\sum_{n=0}^{N-1} x[n] \;\approx\; 0\]

になる。音声は空気圧の平均からのずれであり、正と負がほぼ同じだけ現れるからである。大きな音でも小さな音でも0に近くなり、指標として使えない。

二乗すれば符号が消える。

\[\frac{1}{N}\sum_{n=0}^{N-1} x[n]^2\]

これは平均二乗である。離散信号の二乗和を\(E=\sum_n x[n]^2\)と定義すると、\(E=N\,\mathrm{RMS}^2\)となる。実際の音の物理的エネルギーとの対応には、録音系の校正なども必要である。ただし単位が振幅の二乗になっているので、波形と並べて読めない。最後に平方根を取ると単位が振幅に戻る。これが RMS である。

絶対値ではいけないのか:\(\frac{1}{N}\sum |x[n]|\) でも符号は消える。実際この量も使われる。二乗は信号エネルギーと結びつき、大きい値をより強く反映する。分散との共通点は二乗を平均することである。ただし、分散は平均との差を二乗し、RMSはサンプル値自体を二乗する。

フレームごとに計算する理由:RMSは非定常な信号や録音全体にも定義できる。ここで短く区切るのは、振幅の時間変化を追うためである。窓を長くすれば値は滑らかになるが、短い変化を見落とす。

使った数学:総和の性質と、平均・二乗平均の定義(統計学Ⅰ第3回)。


項目4: ZCRとRMSによる区間候補の分類

3種類の区間を推定する考え方

ZCRとRMSによる簡単な規則を試す。ただし、出力は正解ラベルではなく候補である。

出力する候補 RMS ZCR 誤りやすい例
低振幅 低 どちらでも 弱い発話を含むことがある
有声らしい 閾値以上 低 周期的な機械音も含み得る
無声らしい 閾値以上 高 非発話の雑音も含み得る

判定アルゴリズム(演習用の規則)

各フレームについて:
  RMSのdB値が閾値未満 → 低振幅
  それ以外でZCRが閾値未満 → 有声らしい候補
  それ以外 → 無声らしい候補

本日のコードの閾値(ZCR=0.1、基準振幅1に対して−30 dB)は動作を試すための設定例。録音レベル・サンプリング周波数・話者・雑音条件によって調整し、正解ラベルと比較して誤りを確かめる。

通常の囁き声は声帯の周期振動が弱い/ないため、F0が安定して得られない場合がある。しかし囁き声も発話であり、無声・非発話・無音は同じ概念ではない。


項目5: ピッチ(基本周波数F0)の概念

ピッチとは

ピッチ(pitch)は知覚される音の高さ。通常の有声音では、声帯振動の繰り返しの周波数である基本周波数F0が主な手がかりになる。知覚量と物理量は区別する。

\[ F_0 = \frac{1}{T_0} \]
  • \(T_0\):基本周期(1回の振動にかかる時間)
  • \(F_0\) が大きい → 高い声
  • \(F_0\) が小さい → 低い声

F0の探索範囲

本日の演習では50〜500 Hzを探索する。これは演習用の設定であり、人間の声の普遍的な範囲ではない。歌声や高い声などは範囲外になり得る。範囲設定は誤推定にも影響するので、音声を聴き、グラフを確認する。

F0の分布には重なりと個人差がある。F0だけから性別・年齢・話している言語を確定することはできない。

フォルマントとの違い(重要!)

基本周波数F0(ピッチの主な手がかり) フォルマント(F1, F2, ...)
物理的実体 声帯の振動周波数 声道の共鳴周波数
変化の要因 声帯の張り(感情・イントネーション) 口・舌・唇の形
音声認識での役割 韻律(イントネーション)解析 音素識別(母音の区別)
話者識別での役割 話者の声域 声道の形状

項目6: 自己相関関数によるピッチ推定

なぜ自己相関関数(ACF)か

有声音は周期的な波形を持ちます。自己相関関数はその周期性を検出するのに適しています。

自己相関関数の定義

\[ R_x[\tau] = \sum_{n=0}^{N-1-\tau} x[n] \, x[n+\tau], \qquad 0\leq\tau<N \]
  • \(\tau\):サンプル単位のラグ。対応する時間差は\(\tau/f_s\)秒
  • \(R_x[0]=\sum_n x[n]^2\):フレームの二乗和(信号エネルギー)
  • 重なるサンプルだけを足す有限長の定義。ラグが大きいほど積の項数が減る

ピッチ推定の手順(基本形)

  1. 低いF0の周期を複数含む長さでフレームを切り出す
  2. 自己相関を計算し、必要に応じて振幅や窓による影響を正規化する
  3. 想定したF0範囲に対応する正のラグで、周期を示すピークを探す
  4. 候補ラグ\(\tau_0\)に対し\(F_0\approx f_s/\tau_0\)と推定する

例えば70〜500 Hzを探索するなら、整数ラグは\(\lceil f_s/500\rceil\)から\(\lfloor f_s/70\rfloor\)までとする。22,050 Hzでは45〜315サンプルである。単に全域の最大値を取ると、ラグ0や短いラグを選びやすい。

導出③:なぜ自己相関のピークが周期の手がかりになるのか

サンプル単位の基本周期を\(P\)とする。理想的な周期信号なら\(x[n+P]=x[n]\)なので、重なり部分では

\[R_x[P]=\sum_{n=0}^{N-1-P} x[n]^2\]

となる。各積が正にそろうため、周期付近にピークが現れやすい。ただし有限長の上式では項数が減るので、一般に\(R_x[P]=R_x[0]\)ではない。周期から外れたラグでは山と谷の対応がずれ、積が打ち消し合いやすい。

ラグ\(P\)サンプルが1周期なら、秒単位の周期\(T_0\)との関係は

\[T_0=\frac{P}{f_s},\qquad F_0=\frac{1}{T_0}=\frac{f_s}{P}\]

である。\(f_s=22{,}050\) Hz、\(P=147\)なら、\(T_0\approx6.67\) ms、\(F_0=150\) Hzになる。実際の周期は整数サンプルに一致しないこともあり、補間が利用される。

オクターブ誤りが起きる仕組み

周期の整数倍でも波形は似るので、\(2P\)にもピークが現れ得る。これを基本周期として選ぶとF0を半分、すなわち1オクターブ低く推定する。必ず誤るという意味ではない。短いラグや倍音の影響による誤りもあり、推定法はこれらを減らす工夫を含む。

使った数学:周期関数、有限和、サンプル数から時間への単位換算。


項目7: YINアルゴリズムの概要

YINアルゴリズムとは

de Cheveigné & Kawahara (2002) が提案した基本周波数推定アルゴリズム。 librosaの librosa.yin() として実装されています。

YINの主要なステップ

  1. 差分関数(d)の計算 $\(d[\tau] = \sum_{n} \left(x[n] - x[n+\tau]\right)^2\)$ 自己相関の代わりに「信号とそのτ遅延版との差」を使う。

  2. 累積平均正規化差分関数(CMND) $\(d'[\tau] = \begin{cases} 1 & \text{if } \tau = 0 \\ d[\tau] / \left(\frac{1}{\tau}\sum_{j=1}^{\tau} d[j]\right) & \text{otherwise} \end{cases}\)$ 短いラグ付近の自明な小さい差を避け、閾値で周期候補を選びやすくするための正規化。

  3. 閾値による極小値の探索 \(d'[\tau]\) が閾値(librosaの既定値0.1)を下回る最初の局所最小を周期候補とする。該当する谷がないときも、librosa.yin()は最小値の位置からF0候補を返す。

  4. 放物線補間 整数のτを小数点精度に補間して精度向上。

YINとpYINの出力の違い

  • librosa.yin()は各フレームのF0候補を返す。有声フラグは返さず、fmax付近の値も無声を示す特別な値ではない
  • librosa.pyin()は確率的なYINの拡張で、F0・有声フラグ・有声確率を返す。既定では無声と推定した区間のF0はNaNになる
  • 有声推定も誤り得る。非発話の周期音はF0を持ち得るので、F0があることと発話であることは別

以下の実習では、無声区間も可視化するためpYINを使う(YIN/pYIN公式ドキュメント)。


項目8: Python実習(フル実装)

環境設定

# ===== セル1:ライブラリのインストールとインポート =====
!pip install "librosa==0.11.0" soundfile matplotlib numpy -q

import librosa
import librosa.display
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from IPython.display import Audio, display
# ===== セル2:音声読み込みと基本確認 =====

# 音声ファイルの読み込み
# LibriSpeechの英語発話サンプル。自分の録音ファイルにも変更できる
audio_path = librosa.example('libri1')
y, sr = librosa.load(audio_path, sr=22050, duration=10.0)

print(f"サンプリングレート: {sr} Hz")
print(f"音声の長さ: {len(y)/sr:.2f} 秒")
print(f"サンプル数: {len(y)}")

# 音声を再生
display(Audio(y, rate=sr))
# ===== セル3:ZCRの計算と可視化 =====

hop_length = 512     # 約23.2 ms(sr=22050)
frame_length = 2048  # 約92.9 ms。低いF0の周期を複数含める

# ZCRの計算
zcr = librosa.feature.zero_crossing_rate(y,
                                           frame_length=frame_length,
                                           hop_length=hop_length, zero_pos=True)
# zcr の形状:(1, フレーム数)
zcr = zcr[0]  # 次元を削減 → (フレーム数,)

# 時間軸の作成
frames = np.arange(len(zcr))
times = librosa.frames_to_time(frames, sr=sr, hop_length=hop_length)

print(f"ZCRの形状: {zcr.shape}")
print(f"ZCR統計: min={zcr.min():.4f}, max={zcr.max():.4f}, "
      f"mean={zcr.mean():.4f}")

# ZCRを可視化
fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True)

# 波形
librosa.display.waveshow(y, sr=sr, ax=axes[0], alpha=0.6)
axes[0].set_title("音声波形", fontsize=12)
axes[0].set_ylabel("振幅")

# ZCR
axes[1].plot(times, zcr, color='darkorange', linewidth=1.2)
axes[1].axhline(y=0.1, color='red', linestyle='--', linewidth=1, label='閾値 0.1')
axes[1].set_xlabel("時間 (秒)", fontsize=11)
axes[1].set_ylabel("ZCR")
axes[1].set_title("ゼロ交差率(ZCR)", fontsize=12)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
# ===== セル4:RMS(振幅の指標)の計算と可視化 =====

# RMSの計算
rms = librosa.feature.rms(y=y, frame_length=frame_length, hop_length=hop_length)
rms = rms[0]  # 次元を削減 → (フレーム数,)

print(f"RMSの形状: {rms.shape}")
print(f"RMS統計: min={rms.min():.6f}, max={rms.max():.6f}, "
      f"mean={rms.mean():.6f}")

# 基準振幅1.0に対するdBに変換(dB SPLではない)
rms_db = librosa.amplitude_to_db(rms, ref=1.0, top_db=None)

# 可視化
fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True)

# 波形
librosa.display.waveshow(y, sr=sr, ax=axes[0], alpha=0.6)
axes[0].set_title("音声波形", fontsize=12)

# RMS
axes[1].plot(times, rms_db, color='steelblue', linewidth=1.5, label='RMS (dB)')
axes[1].axhline(y=-30, color='red', linestyle='--', linewidth=1, label='閾値 -30 dB')
axes[1].fill_between(times, rms_db, -100, alpha=0.3, color='steelblue')
axes[1].set_xlabel("時間 (秒)", fontsize=11)
axes[1].set_ylabel("RMS (dB)")
axes[1].set_title("RMSレベル(基準振幅1のdB)", fontsize=12)
axes[1].set_ylim(-100, 5)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
# ===== セル5:pYINでF0と有声フラグを推定 =====

fmin = 50   # 演習用の探索下限。音声に応じて調整する
fmax = 500  # 演習用の探索上限

f0, voiced_flag, voiced_prob = librosa.pyin(
    y, fmin=fmin, fmax=fmax, sr=sr,
    hop_length=hop_length, frame_length=frame_length)
f0_voiced = np.where(voiced_flag, f0, np.nan)

print(f"F0の形状: {f0.shape}")
valid = np.sum(voiced_flag)
total = len(voiced_flag)
print(f"有声と推定したフレーム数: {valid}/{total} ({100*valid/total:.1f}%)")

fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True)
librosa.display.waveshow(y, sr=sr, ax=axes[0], alpha=0.6)
axes[0].set_title("音声波形", fontsize=12)
axes[1].plot(times, f0_voiced, color='crimson', linewidth=1.5,
             label='F0(pYINによる推定)')
axes[1].set_xlabel("時間 (秒)", fontsize=11)
axes[1].set_ylabel("基本周波数 (Hz)")
axes[1].set_title("基本周波数F0:無声と推定した区間はNaN", fontsize=12)
axes[1].set_ylim(0, fmax + 50)
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# ===== セル6:複合可視化(波形 + ZCR + RMS + F0 を同時プロット)=====

fig = plt.figure(figsize=(14, 12))
gs = gridspec.GridSpec(4, 1, hspace=0.45)

# ──────────────── 上段:波形 ────────────────
ax_wave = fig.add_subplot(gs[0])
librosa.display.waveshow(y, sr=sr, ax=ax_wave, alpha=0.7, color='gray')
ax_wave.set_title("① 音声波形(時間領域)", fontsize=12, fontweight='bold')
ax_wave.set_ylabel("振幅")
ax_wave.set_xlabel("")
ax_wave.tick_params(labelbottom=False)

# ──────────────── 2段目:ZCR ────────────────
ax_zcr = fig.add_subplot(gs[1], sharex=ax_wave)
ax_zcr.plot(times, zcr, color='darkorange', linewidth=1.2, label='ZCR')
ax_zcr.axhline(y=0.1, color='red', linestyle='--', linewidth=1,
                 label='ZCRの設定例 (0.1)', alpha=0.8)
ax_zcr.fill_between(times, zcr, 0, alpha=0.25, color='darkorange')
ax_zcr.set_title("② ゼロ交差率(ZCR)", fontsize=12, fontweight='bold')
ax_zcr.set_ylabel("ZCR")
ax_zcr.legend(loc='upper right', fontsize=9)
ax_zcr.grid(True, alpha=0.3)
ax_zcr.tick_params(labelbottom=False)

# ──────────────── 3段目:RMS ────────────────
ax_rms = fig.add_subplot(gs[2], sharex=ax_wave)
ax_rms.plot(times, rms_db, color='steelblue', linewidth=1.5, label='RMS (dB)')
ax_rms.axhline(y=-30, color='red', linestyle='--', linewidth=1,
                 label='低振幅の設定例 (-30 dB)', alpha=0.8)
ax_rms.fill_between(times, rms_db, -100, alpha=0.25, color='steelblue')
ax_rms.set_title("③ RMSレベル(基準振幅1のdB)", fontsize=12, fontweight='bold')
ax_rms.set_ylabel("dB")
ax_rms.set_ylim(-100, 5)
ax_rms.legend(loc='upper right', fontsize=9)
ax_rms.grid(True, alpha=0.3)
ax_rms.tick_params(labelbottom=False)

# ──────────────── 下段:F0 ────────────────
ax_f0 = fig.add_subplot(gs[3], sharex=ax_wave)
ax_f0.plot(times, f0_voiced, color='crimson', linewidth=1.8,
             label='F0 (pYIN)', marker='.', markersize=2)
ax_f0.set_title("④ ピッチ(基本周波数 F0)", fontsize=12, fontweight='bold')
ax_f0.set_ylabel("周波数 (Hz)")
ax_f0.set_xlabel("時間 (秒)", fontsize=11)
ax_f0.set_ylim(0, fmax + 50)
ax_f0.legend(loc='upper right', fontsize=9)
ax_f0.grid(True, alpha=0.3)

fig.suptitle("音声特徴量の複合可視化(波形 / ZCR / RMS / F0)",
              fontsize=14, fontweight='bold', y=1.01)
plt.savefig("features_overview.png", dpi=150, bbox_inches='tight')
plt.show()
print("グラフを features_overview.png に保存しました")
# ===== セル7:ZCRとRMSを使った区間候補の分類 =====

def rule_based_candidates(zcr, rms_db, zcr_threshold=0.1, rms_threshold=-30):
    """
    ZCRとRMSによる教材用の区間候補分類(発話の正解判定ではない)
    Returns:
        labels: 各フレームのラベル
                'low_level' = 低振幅
                'voiced_like' = 有声らしい候補
                'unvoiced_like' = 無声らしい候補
    """
    labels = []
    for z, r in zip(zcr, rms_db):
        if r < rms_threshold:
            labels.append('low_level')
        elif z < zcr_threshold:
            labels.append('voiced_like')
        else:
            labels.append('unvoiced_like')
    return np.array(labels)

labels = rule_based_candidates(zcr, rms_db,
                          zcr_threshold=0.1,
                          rms_threshold=-30)

# 結果の集計
from collections import Counter
count = Counter(labels)
total = len(labels)
print("=== 演習用の区間候補 ===")
for label, cnt in count.items():
    print(f"  {label:10s}: {cnt:4d} フレーム ({100*cnt/total:.1f}%)")

# 可視化(ラベルを色で塗り分け)
label_colors = {'low_level': 'lightgray', 'voiced_like': 'lightgreen', 'unvoiced_like': 'lightsalmon'}

fig, ax = plt.subplots(figsize=(12, 3))
librosa.display.waveshow(y, sr=sr, ax=ax, alpha=0.6, color='gray')

for i, label in enumerate(labels):
    t_start = max(0, times[i] - hop_length / (2 * sr))
    t_end = min(len(y) / sr, times[i] + hop_length / (2 * sr))
    ax.axvspan(t_start, t_end,
                 alpha=0.4,
                 color=label_colors[label],
                 linewidth=0)

# 凡例
from matplotlib.patches import Patch
legend_elements = [Patch(facecolor='lightgreen', alpha=0.6, label='有声らしい候補'),
                    Patch(facecolor='lightsalmon', alpha=0.6, label='無声らしい候補'),
                    Patch(facecolor='lightgray',   alpha=0.6, label='低振幅')]
ax.legend(handles=legend_elements, loc='upper right')
ax.set_title("ZCR + RMS による区間候補(正解ラベルと比較する)", fontsize=12)
ax.set_xlabel("時間 (秒)")
ax.set_ylabel("振幅")
plt.tight_layout()
plt.show()

項目9: 音声活動検出(VAD)の基礎

VAD(Voice Activity Detection)とは

音声区間と非音声区間を自動的に識別する技術です。

音声認識・処理システムにおけるVADの役割

入力音声ストリーム(連続的に入ってくる)
  │
  ▼ VAD
  ├─ 音声区間 → 音声認識エンジンへ送る
  └─ 非音声区間 → スキップ(処理しない)

VADが重要な理由

  1. 計算コスト削減:非発話区間の後段処理を省ける
  2. 認識の支援:非発話の雑音に対する不要な出力を減らせる場合がある。一方、発話を取り逃すと認識も欠落する
  3. 区間の制御:端末内で保存・送信する区間を選ぶ用途に使える。ただし、VADだけで収集や保存が制御されるわけではなく、システム全体の実装による

VADのアプローチ

アプローチ 特徴 代表例
単純な規則 エネルギー等の閾値から候補を得る 本日の分類は原理を試す簡略例
統計的 周波数帯域の特徴をGMMで比較するなど WebRTC VAD
深層学習 学習した音声区間の判別モデル Silero VAD、pyannoteなど

WebRTC VAD(実用的なライブラリ)

# pip install webrtcvad
import webrtcvad
# aggressiveness: 0(穏やか)〜 3(厳格)
vad = webrtcvad.Vad(2)

WebRTC VADは16-bitモノラルPCM、8/16/32/48 kHz、10/20/30 msのフレームを受け取る。上の22,050 Hzの浮動小数点波形をそのまま渡せない。上のコードは初期化のみ(Pythonラッパー公式README、WebRTCのGMM実装)。

項目10: 演習—特徴量からの推理

演習テーマ

「どんな音が候補になるか。特徴量だけでは何が決まらないかを考えよう」

以下は22,050 Hzでの模式例。dBは振幅1を基準とし、F0のNaNはpYINが無声と推定したことを表す。音が存在しないという意味ではない。

演習の進め方

  1. 以下の3つの特徴量パターンを見て、どんな音声か推測する
  2. 自分のペースで考えてから、チャットや共有ドキュメントに回答を書き込む
  3. 余裕があればクラス全体で共有する(Zoom参加またはメール提出でも可)

ケース1:特徴量パターン

ZCR:全体的に低い(0.05〜0.08)が、時々0.3以上に急上昇
RMS:高いゾーンと低いゾーンが規則的に交互に現れる
F0 :有声区間では130〜180Hz程度で安定

ヒント:RMSの高低の繰り返し、低めのZCRは何を示す?


ケース2:特徴量パターン

ZCR:常に高い(0.2〜0.35)
RMS:非常に低い(-50dB 以下)
F0 :ほとんど NaN(未検出)

ヒント:エネルギーが低く、ゼロ交差が多い…


ケース3:特徴量パターン

ZCR:低い区間と高い区間が混在
RMS:高い値が持続(-10〜-15dB 程度)
F0 :200〜280Hz の有声区間と、NaN 区間が交互

ヒント:F0が200〜280Hz程度、有声と無声が混在…


解答例(授業終了後に公開)

ケース 考えられる候補 この情報だけでは決まらないこと
ケース1 周期的な音と摩擦的な音を含む発話など 性別・言語・文章の読み上げかどうか
ケース2 小さな広帯域雑音、弱い無声子音など 音源がファンかどうか、発話が含まれないか
ケース3 周期的な区間と非周期的な区間を含む発話など 性別・年齢・言語

各候補を区別するには、元の音声、スペクトログラム、周期性の推定結果などを照合する。特徴量だけで一つの状況に確定しない。


項目11: まとめと次回予告

本日のまとめ

特徴量 定義・推定 読み取れること
ZCR 符号変化の回数÷サンプル数 符号の反転の頻度。単独では有声性を確定しない
RMS 二乗平均平方根 録音信号の振幅。RMSの二乗は平均二乗振幅
F0 周期の逆数。実習ではpYINで推定 有声区間の周期性。推定誤りや範囲外に注意
フォルマント 声道の共鳴に対応するスペクトル包絡の山 母音の音色など。F0の倍音の山とは区別する

RMSが低い区間にも発話が、RMSが高い区間にも雑音があり得る。ZCRによる区間候補、有声性推定、VADの発話区間推定は目的が異なる。

librosaで使った主な関数

librosa.feature.zero_crossing_rate()  # ZCR
librosa.feature.rms()                 # RMS
librosa.pyin()                        # F0・有声フラグ・有声確率
librosa.amplitude_to_db()             # 基準振幅に対するdB

次回(第7回)予告

HMM・GMMによる系列モデルの基礎を学び、自己教師あり表現学習との違いにつなげます。

予習課題

  • 「HMM(隠れマルコフモデル)」を調べ、「状態」「遷移確率」「出力確率」の3概念を理解しておく
  • 今日の複合可視化コードを自分の声(録音)に適用してみる

参考文献・資料