#音声処理#DSP#タイムストレッチ#WSOLA

話速変更でロボット声になる問題 — フェーズボコーダからWSOLAへ


「声のデザイン」アプリを作っていて、話速(話すスピード)を調整するスライダーを付けました。音高(ピッチ)は変えずに、テンポだけ速く/遅くしたい。よくある要件です。

librosa には librosa.effects.time_stretch という、まさにこのための関数があります。1行で済む。最初はそれを使いました。ところが、少しでもスライダーを動かすと出力にうっすらと金属的な反響が乗る。声がわずかに「ロボットっぽく」「エコーがかかったように」濁るのです。元の声は自然なのに、テンポを変えた瞬間に質感が落ちる。

この記事は、その原因が フェーズボコーダの位相のにじみ だったこと、そして WSOLA(Waveform Similarity Overlap-Add) を numpy で自前実装して解消した記録です。

前提:ピッチを保ってテンポだけ変えるとは

音声を単純に間引いたり水増ししたりすると、再生速度と一緒にピッチも動きます(早送りするとキンキン声になるあれです)。これを避けて テンポだけ 変えるのがタイムストレッチです。

代表的な方式は大きく2つあります。

  • フェーズボコーダ(phase vocoder):STFT で周波数領域に変換し、各周波数ビンの位相を進める量を調整して伸縮する。周波数領域で処理する。
  • WSOLA:波形(時間領域)を短いフレームに切り、フレーム同士がなめらかにつながる位置を探して重ね合わせる。時間領域で処理する。

librosa の time_stretch は前者、フェーズボコーダです。

症状:金属的な反響(ロボット声)

フェーズボコーダは各周波数ビンを独立に扱い、位相を「あるべき進み量」に更新していきます。理屈のうえではきれいですが、実際の音声では調波成分(倍音)どうしの位相の関係が少しずつ崩れます。これが 位相のコヒーレンス(位相コヒーレンシー)の喪失 です。

耳には、次のような形で現れます。

  • 金属的・電子的な響き(俗に phasiness と呼ばれる)
  • うっすらとした反響/エコー感
  • 母音の芯がぼやけ、声が「合成っぽく」なる

音楽やパーカッシブな素材では気になりにくいのですが、人の声はフォルマントと倍音構造が命なので、この位相のにじみが「ロボット声」として敏感に聞こえてしまいます。しかも伸縮率を上げるほど悪化する。話速スライダーとは相性が悪い、というのが結論でした。

解決:WSOLA を時間領域で

WSOLA は周波数領域に持ち込まず、波形をそのまま切って貼る 方式です。位相を「計算し直す」のではなく、隣り合うフレームが最も自然につながる貼り付け位置を 相互相関で探す。だから位相のにじみが原理的に起きません。

考え方はこうです。

  1. 出力側は一定間隔(syn_hop)でフレームを並べていく
  2. 入力側からは、伸縮率に応じた間隔(ana_hop = syn_hop × rate)で「理想位置」のフレームを取る
  3. ただし理想位置ちょうどではなく、その周辺 ±tol をずらして、直前に貼ったフレームの続き(自然な連続波形)と最も相似なフレームを探す
  4. 見つけたフレームを Hann 窓で重ね合わせ加算(overlap-add)する

実装(_apply_speed)の心臓部が、この「相似位置の粗探索」です。

ana_hop = int(round(syn_hop * float(rate)))
win = np.hanning(frame).astype(np.float32)
...
# 直前フレームの「自然な続き」= nat
nat = xp[off + prev_ana + syn_hop: off + prev_ana + syn_hop + frame]
best_d, best = 0, -1e18
for d in range(-tol, tol + 1, 8):     # ±tol を粗探索して位相整合
    cand = xp[off + ideal + d: off + ideal + frame + d]
    sc = float(np.dot(cand, nat)) / (float(np.linalg.norm(cand)) + 1e-6)
    if sc > best:
        best, best_d = sc, d
a = ideal + best_d
seg = xp[off + a: off + a + frame]
y[syn:syn + frame] += seg * win
norm[syn:syn + frame] += win

nat が「直前に貼ったフレームの、波形として自然な続き」です。候補フレーム cand を ±tol の範囲でずらしながら、nat との正規化内積(相互相関)が最大になる位置 best_d を探します。要は「波形の周期がそろう場所」で貼り継ぐわけで、これが位相連続性の担保になります。最後に窓関数の重み norm で割って正規化しています(overlap-add の定石)。

パラメータは frame=1024, syn_hop=512, tol=512 です。探索は range(-tol, tol+1, 8) と8サンプル刻みの粗探索にしてあり、サンプル単位の総当たりはしていません。話速調整はリアルタイム性が要るので、音質と速度のバランスを取った刻みです。

トレードオフ:万能ではない

WSOLA にすれば何でも解決、ではありません。手法選択には素材依存のトレードオフがあります。

  • 音声(単一話者の声)には WSOLA が強い。周期性がはっきりしているので相似位置が見つけやすく、位相のにじみが出ない。今回の用途にはこれがベストでした。
  • 一方、複雑な多声・音楽・トランジェントが多い素材ではフェーズボコーダのほうが破綻しにくい こともある。WSOLA は「貼り継ぎ位置」を1つに決める以上、複数の周期が混ざると相似探索が迷い、リズムの揺れやダブリングが出ることがあります。
  • 伸縮率が極端だと、どちらの方式でもアーティファクトは増える。WSOLA も伸ばしすぎれば同じフレームの反復が目立ちます。

「フェーズボコーダはダメで WSOLA が正義」ではなく、声というピッチのはっきりした素材にはWSOLAが向いていた、という素材適合の話です。

落とし穴と学び

  • ライブラリの1行を疑うlibrosa.time_stretch は正しく動いていました。バグではなく、フェーズボコーダという方式の特性(位相のにじみ)が声に合わなかっただけ。「関数が壊れている」ではなく「方式が用途に合わない」を見分けるのが肝でした。
  • 時間領域か周波数領域か、を最初に意識する。金属的な響き=位相由来、と当たりをつけられれば、周波数領域処理(フェーズボコーダ)を疑い、時間領域処理(WSOLA)へ乗り換える、という筋道が立ちます。
  • 自前実装は数十行で済む。WSOLA の中核は「相互相関で貼り継ぎ位置を探して overlap-add する」だけで、numpy だけで書けます。外部依存を1つ減らせたのも副次的な利点でした。
  • 窓とオーバーラップの正規化を忘れない。overlap-add は窓の重み合計 norm で割って初めて振幅が正しくなります。ここを省くと音量がうねります。

まとめ

  • 話速変更で乗る「ロボット声/金属的な反響」の正体は、フェーズボコーダの 位相のにじみ(phasiness) だった
  • 声のようにピッチのはっきりした素材では、位相を計算し直すフェーズボコーダより、波形の相似位置で貼り継ぐ WSOLA が自然
  • WSOLA の核心は「直前フレームの自然な続きと相互相関が最大になる位置を ±tol で探して Hann 窓で overlap-add する」こと。numpy だけで数十行
  • ただし万能ではない。多声・音楽・トランジェント素材ではフェーズボコーダが有利なこともあり、素材に合わせて選ぶ のが正解
  • ライブラリ関数が「壊れている」と疑う前に、その関数が採る 方式(アルゴリズム)が用途に合っているか を確認する
← 記事一覧へ