#音声処理#DSP#WORLD#失敗談

【失敗から学ぶ】\"息遣い\"はなぜ独立パラメータにできなかったのか


「声のデザイン」アプリに、声質を調整するスライダーを何本か並べました。話速、ピッチの揺らぎ——このあたりは素直に実装できました。そしてもう1本、「息遣いの強さ」。ウィスパー系の甘い声や、吐息混じりの表現を足すためのパラメータです。

結論から書きます。このスライダーは実装できませんでした。 加算、帯域制限ノイズ、クロス合成、WORLD の非周期性増加、その差分加算、高域限定、囁きとのクロスフェード、最後にはモデル側での生成まで——思いつく方式をほぼ全部試して、どれも「ノイズにならない自然な息漏れ」には届かず、最終的に UI からスライダーごと消しました。

なぜダメだったのか。理由は最初から目の前にあったのに、10回近い試行錯誤の果てにようやく言語化できました。息漏れ=気息ノイズは、物理的にノイズと不可分だから です。この記事は、その負けを時系列で正直に記録します。git のコミットログがそのまま失敗の履歴になっているので、それをたどります。

前提:息漏れ声とは何か

まず「息遣い」「息漏れ声(breathy voice)」が音響的に何かを整理します。有声音は声帯の周期振動(=ピッチ)で作られますが、息漏れ声では声門が完全に閉じきらず、周期振動に加えて呼気の乱流ノイズ が混ざります。囁き声はその極端で、周期成分がほぼ消えて 乱流ノイズだけ になった状態です。

つまり息漏れの本体は乱流ノイズそのもの。この一文が、この記事のすべての結論を先取りしています。ただ、当時はまだそう割り切れていませんでした。「うまく整形すれば、ノイズっぽくない上品な息遣いが作れるはずだ」と思っていたのです。

試行の記録

1. ノイズ加算 → 高域ブースト(84cf63a)

最初はガウスノイズを足しました。当然、ただのヒスノイズが乗る。そこで方針転換し、ノイズを足すのをやめて 高域を軽く持ち上げる だけにしました。

sos = butter(2, min(3000.0, sr / 2 * 0.9) / (sr / 2), btype="high", output="sos")
hf = sosfilt(sos, wav).astype(np.float32)
return wav + hf * (0.22 * amt)

ノイズ感は消えました。が、これは単に音が明るくなるだけで、息感が薄い。「息遣い」ではなく「イコライザ」でした。

2. 帯域制限(2〜8kHz)の気息ノイズ(8ea3279)

高域ブーストでは息にならない。ならば呼気の乱流を素直に模そう、と 2〜8kHz に帯域制限したノイズを、音量エンベロープで整形して有声部だけに乗せる 方式にしました。無音部にノイズが乗らないので、ベタなヒスにはならない。

env = np.abs(wav).astype(np.float32)          # 音量エンベロープ(息はしゃべっている所に乗る)
...
noise = sosfilt(lp, sosfilt(hp, noise))       # 2〜8kHz に帯域制限
breath = noise * env * (0.35 * amt)

同一音声で高域(>2kHz)成分の比率が 2.1%→5.8% と、確かに息成分は増えました。数値は動いた。でも耳では、やはり 声とは別レイヤーの雑音 に聞こえる。

3. クロス合成:声の包絡で整形したささやき(d10f696)

平坦なノイズが「別レイヤー」に聞こえるのは、声道の共鳴(フォルマント)を通っていないからだ、と考えました。そこで音声の短時間スペクトル包絡でノイズを整形し、その声のささやき成分 を作って混ぜるクロス合成方式へ。声道の共鳴を通せば、雑音でなく「声に混じる息」に聞こえるはず——理屈は正しい。それでも強めるとヒスっぽさが残りました。

4. WORLD の非周期性(AP)を上げる(438ff68)

ここで大きく方針を変えます。ノイズを「足す」から離れ、声そのものを息っぽく作り直す。音声分析合成の WORLD で信号を F0(ピッチ)・スペクトル包絡(SP)・非周期性(AP)に分解し、非周期性を上げて再合成 する。非周期性を1に寄せるほど、その帯域は周期成分から気息成分へ移る。加算と違い、原理的には「別レイヤー」が生じません。

f0, t = pw.harvest(x, sr)
sp = pw.cheaptrick(x, f0, t, sr)
ap = pw.d4c(x, f0, t, sr)
ap2 = np.clip(ap + (0.45 * amt) * (1.0 - ap), 0.0, 1.0)   # 非周期性を1へ寄せる=気息増
y = pw.synthesize(f0, sp, ap2, sr, frame_period=5.0)

これは筋が良さそうでした。が、新しい問題が次々に出ます。

5. 二重ボコーディングで割れる → 差分だけ加算(f7bc4d9)

入力はもともと Seed-VC が生成した音声です。それを WORLD で丸ごと再合成すると 二重ボコーディング になり、音が割れる。対策として、元の AP と AP を増やした版の 2回合成の差分(=増えた気息成分だけ) を取り出し、クリーンな原音に加算する方式にしました。WORLD 由来の劣化は差分で相殺される、という発想です。

6. 低域が濁る(ガラガラ)→ 高域限定(2287e39)

今度は低域(調波)まで非周期化してしまい、声が ガラガラに濁る。気息は本来高域に多いので、非周期性を上げるのを >1.5kHz に限定し、低域の有声はクリーンに保つことにしました。濁りと割れは解消。ただしコミットメッセージにこう書いています。

効果は控えめになる(後処理の限界)。

このあたりから、「後処理でやれることの天井」がはっきり見え始めます。強くすれば濁り、濁りを消せば効かない。両立しない。

7. 強める(1110567)→ 囁きとのクロスフェード(e767575)

副作用を承知で強めに振ってみる(高域カット1.0kHz・強度0.85)。次に、高域限定 AP 増は弱すぎ、部分的な AP 増は濁る、という板挟みを避けるため、全帯域を非周期化した「その声の囁き」を作り、原音とクロスフェード する方式へ。100%で完全な囁き声、中間で息漏れ声。各成分はクリーンで濁らない。

ここまでで、後処理系の方式はほぼ出し尽くしました。共通する敗因は一つ。息を強く効かせようとするほどノイズ性が増し、ノイズ性を抑えると息が効かない。

8. 諦めて、モデル側で生成させる(e7c6438)

後処理で息を「足す/作り替える」のが限界なら、モデル自身に息漏れ声を生成させれば ボコーダがクリーンに描いてくれるのでは、と考えました。Seed-VC の参照プロンプト(cref)を、息遣い量に応じて WORLD 囁き版へブレンドし、モデルに息漏れ声そのものを合成させる。

# 参照プロンプトを息漏れ(囁き)版へブレンド → モデルが息漏れ声を生成
cref = (cref * (1.0 - 0.85 * b) + _world_whisper(cref, sr) * (0.85 * b)).astype(np.float32)

検証すると有声率は 0.64→0.11(@100%)まで下がり、スペクトル平坦度(flatness)は 0.028 で横ばい——つまり ノイズ性は増えずクリーンなまま息漏れが増えた。数値上はいちばん良い。後処理で生成段階の反映に切り替えたため、A/B 比較のために torch.manual_seed(1234) で生成を決定論化までしました。

それでも——強い息漏れは、やはり息漏れらしいノイズ感を伴う。当たり前でした。モデルにクリーンな息漏れを描かせても、息漏れの本体が乱流ノイズである以上、「ノイズのない強い息漏れ」というもの自体が存在しない。

9. 撤去(85bbcd0)

そこで結論を出しました。

息漏れ/囁きは呼気の乱流ノイズそのもので、後処理・モデル側いずれも
「ノイズ無しの強い息漏れ」は実現不能と結論。
息遣いスライダーを高度な調整から除去(話速・ピッチ揺らぎは維持)。

スライダーを UI から外し、バックエンドを実験前の状態へ戻しました。話速(WSOLA)とピッチ揺らぎは残しています。息遣いだけが、負けました。

なぜ最初から見抜けなかったのか

敗因は技術ではなく 前提の立て方 でした。「息遣い」という UI ラベルが、頭の中で “上品でノイズのない息感” という達成不能な目標に化けていたのです。音響的に定義すれば、息漏れ=気息ノイズ。ノイズを消したら息も消える。これは実装の巧拙ではなく物理です。加算だろうが、非周期性だろうが、モデル生成だろうが、同じ壁に別の角度からぶつかっていただけでした。

もう一つの学びは、中間指標が良くなっても目的が達成されるとは限らない こと。高域比率、有声率、スペクトル平坦度——数字はどれも「改善」していました。でも「ノイズに聞こえない自然な息遣い」という、そもそも自己矛盾した目標には近づけなかった。指標が良くなるほど、達成不能なゴールを追っている事実が見えにくくなります。

まとめ

  • 息漏れ声・囁き声の本体は 呼気の乱流ノイズ そのもの。「ノイズのない息遣い」は音響的に自己矛盾で、実装の問題ではなく物理の問題だった
  • 試した方式は一通り:ノイズ加算 → 高域ブースト → 帯域制限ノイズ → クロス合成 → WORLD非周期性増加 → 差分加算 → 高域限定 → 囁きクロスフェード → モデル側生成。どれも「強くすると濁る/ノイズ化、抑えると効かない」の板挟みに帰着した
  • WORLD の非周期性やモデル側生成は 中間指標(有声率・平坦度)を改善 したが、達成不能なゴールには近づけなかった。指標の改善は目的の達成を意味しない
  • 最終判断は スライダーの撤去。効かせられない/不自然なパラメータを残すより、正直に消すほうがプロダクトとして誠実だった
  • 敗因の本質は UI ラベルが達成不能な目標を暗黙に定義していた こと。機能を作る前に、その言葉を音響的に定義し、物理的に成立するかを確かめるべきだった
← 記事一覧へ