信号処理でヘリウムボイスに音声変換

f:id:Setoti:20211029223035p:plain:w250

ヘリウムガスを吸い込んだときの特徴的な声はヘリウムボイスと呼ばれています。本記事では、信号処理でヘリウムボイスを擬似的に実現してみました。前回の記事で紹介したケプストラム分析を用いてスペクトル包絡の加工をします。

本記事で主に参考にした本は以下です。

ヘリウムボイスの原理

参考文献 [1] によると、ヘリウムボイスは、ヘリウムガスを吸い込むことで音速が増大し、声道の共鳴周波数が変化することによって生じる現象のようです。

片方が閉じた管で人間の声道をモデル化して考えます。図1のように管の長さが L [m] のとき、波長が  \lambda=4L となる周波数、およびその奇数倍の周波数で共鳴が起こります。

図1:片側が閉じた管で共鳴する波長
図1:片側が閉じた管で共鳴する波長

音速 c [m/s]、周波数 f [Hz]、波長 λ [m] には次式のような関係がありますので、

\displaystyle{
c = f \lambda
}

音速が r 倍になれば、共鳴する周波数は以下のようになります。

\displaystyle{
f = \frac{rc}{4L} n \hspace{1em} (n=1,3,5,\cdots)
}

したがって、音速が増大すれば共鳴周波数(フォルマント周波数)も大きくなるので、ヘリウムボイスはあのような特徴的な音声になります。

ヘリウムボイスの作成方法

ヘリウムボイスを信号処理で擬似的に実現する方法について説明したいと思います。

スペクトル包絡の伸縮

ヘリウムボイスの原理からフォルマント周波数をr倍、つまり対数スペクトル包絡 H[k] を図2のように周波数方向に r 倍伸縮すればヘリウムボイスを実現できます。

図2:対数スペクトル包絡のr倍の伸縮
図2:対数スペクトル包絡のr倍の伸縮

数式で表現すると以下のようになります。

\displaystyle{
\tilde{H}[k]=H\left[\frac{k}{r}\right] 
}

しかし、k/r が整数になることはあまりないため、以下のような線形補間で \tilde{H}[k] を求めます。

\displaystyle{
\tilde{H}[k]=(1-\alpha_k)H[k'] +\alpha_k H[k'+1] 
}

ただし、

\displaystyle{
\begin{align}
\hspace{2em} k' &= \left\lfloor \frac{k}{r} \right\rfloor \\
\hspace{2em} \alpha_k &= \frac{k}{r} - k'
\end{align}
}

となっています。

参考文献 [1] によると、ヘリウムは空気の約3倍の音速ですが、市販のヘリウムガスは安全のため空気が混入しているそうです。そのため、1.5<r<2.0 としたときが実際のヘリウムボイスに近いみたいです。

ヘリウムボイス変換の流れ

音声をヘリウムボイスに変換するためのブロック図を図3に示します。

図3:ヘリウムボイスに変換するブロック図
図3:ヘリウムボイスに変換するブロック図

フレームごとにケプストラム分析で対数スペクトル包絡を求めて、r倍に伸縮しています。抽出していなかった高次のケプストラムについてもFFTして、伸縮した対数スペクトルに加算しています。それから、log の逆関数であるexpによって振幅を再合成します。位相については観測信号の位相をそのまま用いています。

プログラム

音声をヘリウムボイスに変換するソースコードは以下の helium_vc.py となっています。

import soundfile as sf
import numpy as np
from scipy.fft import rfft, irfft
import scipy.signal as sg

# パラメータ
wav_name = "ATR_PM00.wav"  # 読み込むWAVデータの名前
out_name = "soundout.wav"  # 出力するWAVデータの名前
window = "hann"     # 窓関数の種類
N = 1024            # FFT点数
r = 1.5             # スペクトル包絡の伸縮率

# WAVファイルを読み込む
x, fs = sf.read(wav_name)

# 短時間フーリエ変換(STFT)を行う X.shape=(n_bin, n_frame)
_, _, X = sg.stft(x, fs, window=window, nperseg=N)
X_phase = np.angle(X)   # 観測信号の位相
n_bin   = X.shape[0]    # ビン数
n_frame = X.shape[1]    # フレーム数

# 各numpy配列を準備
ceps_l = np.zeros(N)       # 低次のケプストラム用の配列
ceps_h = np.zeros(N)       # 高次のケプストラム用の配列
H_tilde = np.zeros(n_bin)  # 伸縮後のスペクトル包絡用の配列
Y_abs = np.zeros(X.shape, dtype=np.float64) # 出力信号の振幅用の配列
eps = np.finfo(np.float64).eps  # マシンイプシロン

# フレームごとにr培に伸縮したスペクトル包絡を求める
for i in range(n_frame):
    spec_log = np.log(np.abs(X[:,i])+eps)  # 対数変換
    ceps = irfft(spec_log)     # IFFTしてケプストラムを求める
    lifter = 72                # 低次のケプストラムを72点まで抽出
    ceps_l[0:lifter] = ceps[0:lifter]        # 低次の抽出(前半)
    ceps_l[N-lifter+1:] = ceps[N-lifter+1:]  # 低次の抽出(後半)
    ceps_h[lifter:N-lifter+1] = ceps[lifter:N-lifter+1]  # 高次の抽出
    H = np.real(rfft(ceps_l))  # FFTして実部だけ取り出す
    G = np.real(rfft(ceps_h))  # FFTして実部だけ取り出す
    # 対数スペクトル包絡をr倍に伸縮
    for k in range(n_bin):
        k2 = int(k/r)
        alpha = k/r - k2
        if k2 < n_bin-1:
            H_tilde[k] = (1-alpha)*H[k2] + alpha*H[k2+1] # 線形補間
        else:  # k2がn_binを超えた場合 
            H_tilde[k] = np.log(eps)  # -∞ に近いものを代入
    Y_abs[:,i] = np.exp(H_tilde+G) # 振幅スペクトルを求める

# 位相と振幅でスペクトログラムを合成
Y = Y_abs * np.exp(X_phase)

# 逆短時間フーリエ変換(ISTFT)を行う
_, y = sg.istft(Y, fs=fs, window=window, nperseg=N)

# ファイルに書き込む
y = y/np.max(np.abs(y)) # ノーマライズ
sf.write(out_name, y, fs, subtype="PCM_16")

6~11行目:読みこむWAVデータ名、出力するデータ名、窓関数の種類、FFTの点数、スペクトル包絡の伸縮率などを指定しています。

13~14行目:WAVデータを読み込んでいます。

16~20行目短時間フーリエ変換をしています。また、観測信号の位相を取得しています。

22~26行目:あらかじめ各numpy配列を用意しています。

31~38行目:フレームごとにケプストラム分析をして、対数スペクトル包絡 H を求めています。また、高次抽出をして微細構造 G も求めています。子音を発するときなどは、ケプストラムに基本周波数のピークがないので、低次抽出する点数は72点で固定しています。

39~46行目:対数スペクトル包絡を r 倍に伸縮しています。r<1.0 のときは k2(k') にスペクトル包絡がない場合があるので、そのときはとても大きいマイナスの数を代入しています。

47行目:対数スペクトル包絡 H と微細構造 G を足し合わせて、指数関数 exp で振幅スペクトルを求めています。

49~50行目:再合成した振幅と観測信号の位相を用いて、出力信号のスぺクトログラムを求めています。

52~53行目:逆短時間フーリエ変換で波形データに戻しています。

55~57行目:オーバーフローしたときのためにノーマライズして、WAVデータで出力しています。

処理結果

ヘリウムボイスに変換した男性と女性の音声が以下です。窓関数はハン窓、FFT点数は1024点、オーバーラップは1/2、スペクトル包絡の伸縮率は r=1.5 倍としました。一応、r=0.5 としたときの処理結果についても載せました。音速が遅い気体を吸い込んだらおそらくr=0.5のときのような声になると思います。

男声の音声

男声の音声の処理結果(r=1.5)

男声の音声の処理結果(r=0.5)

女声の音声

女声の音声の処理結果(r=1.5)

女声の音声の処理結果(r=0.5)

信号処理によるノイズ(ミュージカルノイズ)はありますが、ヘリウムガスを吸い込んだときのような声になりました。r=0.5のときは、野太い人の声になってますね。

おわりに

本記事ではケプストラム分析を用いて音声をヘリウムボイスに変換しました。よくあるボイスチェンジャーがどのようなアルゴリズムになっているか理解できてよかったです。

参考文献
[1] 川村新、”音声音響信号処理の基礎と実践”、コロナ社、2021.

使用したデータについて
この記事で信号処理した音声は髙橋弘太研究室の話速バリエーション型音声データベース(SRV-DB)で提供されているATR25文の読み上げ音声を使用させていただきました。

【音声データベースを提供している髙橋弘太研修室のページ】
http://www.it.cei.uec.ac.jp/SRV-DB/

お問い合わせフォーム プライバシーポリシー

© 2021 Setoti All rights reserved.