#音声処理#DSP#ビブラート#ディレイ変調

知覚できるビブラート/ピッチ揺らぎをどう作るか


「声のデザイン」アプリに、声の表情を足すための「ピッチの揺らぎ」スライダーを付けました。ほんの少し音高を揺らして、棒読みっぽさを和らげ、人間味のあるビブラートを添えたい。

実装自体はすぐできました。純音(サイン波)で検証すると、確かにピッチが周期的に揺れている。数値上は正しく動いている。ところが実際の声に適用してスライダーを最大まで上げても、耳ではほとんど違いが分からない。「パラメータはあるのに聞き分けられない」——DSP でいちばん厄介な種類の失敗です。バグなら直せますが、これは「正しく動いているのに効果が知覚できない」。

この記事は、ディレイ変調でビブラートを作る仕組みと、“効いてる感” を出すための 変調深さの設計 の話です。

前提:ディレイ変調でピッチが動く理由

ピッチを揺らす方法はいくつかありますが、ここでは 時間変化するディレイ(分数遅延) を使いました。コーラスやフランジャーと同じ原理です。

ポイントは、遅延時間そのものではなく、遅延時間の変化率がピッチを動かす ことです。信号を遅らせる量を時間とともに増やしていくと、再生位置がゆっくり後退し、ドップラー的にピッチが下がる。逆に遅延量を減らせばピッチが上がる。遅延量を正弦波で往復させれば、ピッチが上下に揺れる=ビブラートになります。

数式でいえば、瞬時のピッチ変化はおおよそ 1 − d(delay)/dt に比例します。だから 揺れの速さ(変調周波数)と揺れの深さ(最大遅延量)の両方 がビブラートの強さを決めます。

実装:正弦波で遅延量を往復させる

実装(_apply_pitch_variation)はこれだけです。

n = len(wav)
t = np.arange(n) / sr
rate_hz = 4.5
max_delay = (0.0030 * amt) * sr           # 最大 ~3.0ms(100%で±約65セントのビブラート)
delay = max_delay * (0.5 - 0.5 * np.cos(2 * np.pi * rate_hz * t))
idx = np.clip(np.arange(n) - delay, 0, n - 1)
i0 = np.floor(idx).astype(int)
frac = (idx - i0).astype(np.float32)
i1 = np.minimum(i0 + 1, n - 1)
return wav[i0] * (1 - frac) + wav[i1] * frac

順に見ていきます。

  • rate_hz = 4.5ビブラートの速さ。人の自然なビブラートはおおむね 4〜7Hz なので、その下限あたりに置いています。速すぎるとトレモロっぽく、遅すぎると音痴に聞こえるので、この帯は狭い。
  • delay0.5 − 0.5·cos(...) で 0 から max_delay の間を正弦波で往復します。遅延「量」が往復するので、その微分=ピッチが上下に揺れます。
  • idx は「今どのサンプル位置を読むか」。遅延ぶんだけ過去にずれます。位置が整数にならないので、分数遅延 になります。
  • 最後の3行が 線形補間i0(整数部)と i1(次のサンプル)を frac(小数部)で内挿します。分数遅延を実現するにはこの補間が必須で、これを省くと最近傍で読んでジャリついた音になります。

amt(0〜1)は max_delay に線形で効きます。スライダー100%で最大遅延 ~3.0ms、というのが今の設定です。

本題:「聞き分けられない」を深さで解く

最初の実装では、max_delay1.0ms でした。純音での検証は正常。なのに声では効果が知覚できない。

原因は単純で、変調が浅すぎた のです。上の関係から、変調深さは最大 ±約24セント相当でした。セントは半音を100分割した音高単位で、1半音=100セント。24セントは半音の1/4弱です。純音を注意深く聴けば分かりますが、倍音とフォルマントで込み入った実際の声の中に紛れると、この程度の揺れは埋もれて知覚しづらい。「あるのに分からない」の正体はこれでした。

そこで 最大遅延を 1.0ms → 3.0ms に引き上げました。これで100%時のビブラート深さが ±約65セント になり、はっきり “効いてる感” が出ました。git の履歴にもそのまま残っています。

fix: ピッチ揺らぎが体感できない問題。変調深さ 1.0ms→3.0ms(100%で±約65セント)に増加
純音検証で効果自体は正常だったが±24セントと控えめすぎて知覚困難だった。

面白いのは、深くしても「音痴」にはならない ことです。速さ(4.5Hz)が自然なビブラートの範囲に収まっていれば、深さがそこそこ増えても「揺れている表情」として受け取られ、「ピッチが外れている」とは聞こえません。効かせるのは主に深さ、破綻を防ぐのは速さ、という役割分担です。

落とし穴と学び

  • 純音検証は「動いているか」しか教えてくれない。サイン波で正しく揺れていても、それが実素材(声)で 知覚できるか は別問題。DSP は「動作の正しさ」と「知覚される効果」を分けて評価する必要があります。
  • 知覚は非線形。セントという知覚尺度で見ると、±24セントと±65セントは「倍ちょっと」ですが、体感では「分からない」から「はっきり効く」への質的な変化でした。パラメータは物理量ではなく 知覚量で設計する
  • 深さと速さの役割を分ける。効果の強さは深さ(max_delay)で、自然さの範囲は速さ(rate_hz)で担保する。両方を一度に動かすと切り分けができなくなります。
  • 分数遅延には線形補間を必ず入れる。整数サンプルへ丸めると、ジッターとエイリアスでノイズっぽくなり、せっかくのビブラートが濁ります。

まとめ

  • ビブラート=ピッチの揺らぎは、時間変化するディレイ(分数遅延) で作れる。ピッチを動かすのは遅延量そのものではなく 遅延量の変化率
  • 速さ rate_hz は自然なビブラート帯(4〜7Hz)に、揺れの往復は正弦波で、読み出しは 線形補間 で分数遅延にする
  • 「パラメータはあるのに聞き分けられない」の主因は 変調が浅すぎた こと。±24セントでは実素材に埋もれ、±65セントで明確に知覚できた
  • 深さを増やしても速さが自然帯なら「音痴」にはならない。効かせるのは深さ、破綻を防ぐのは速さ
  • 純音での「動作確認」と、実素材での「知覚確認」は別物。パラメータは物理量でなく 知覚量(セント) で設計する
← 記事一覧へ