はじめに
julia 言語を使って簡単なボイスチェンジャーを作ったので,その紹介をしてみます.
マイクから入力された音声を,基本周波数とスペクトル包絡をいじってスピーカー等に流すだけのシンプルなものです.
この記事では,julia で声質変換をする方法についてざっくり書きます.
GUIも一応実装していますが,GUIについてはまた別の記事で書きたいなあと思っています.
環境
julia のバージョンと,使用したパッケージのバージョンです.
あまりメンテされていないパッケージも含むので,以下のバージョンじゃないとうまく動かないかもしれません(実装中に少し詰まりました).
julia
: v1.5.1
LibSndFile.jl
: v2.3.0
Observables.jl
: v0.3.1
SampledSignals.jl
: v2.1.0
WORLD.jl
: v0.6.1
声質変換
この記事では,WORLDという音声分析合成システムを使います.
WORLD を julia で使用するためのラッパーとして, WORLD.jl
を使います.
(https://github.com/r9y9/WORLD.jl)
また,WORLD.jl
の関数に渡す各種パラメータは,ほぼドキュメント(https://r9y9.github.io/WORLD.jl/stable/ ) にある例と同じものを使っています.
声のパラメータ
WORLD では,音声の
- 基本周波数(F0)
- スペクトル包絡
- 非周期性指標
という3つのパラメータの推定が可能です.また,これらのパラメータから音声を合成することができます.この記事では,基本周波数とスペクトル包絡を変換し,音声を合成します.
最後に示す文献によれば,F0は声の高さに対応しており,声の質はともかくF0を高くすれば高い声に,低くすれば低い声になります.スペクトル包絡は,喉や口などの特性を示していて,主に音色に影響します.非周期性指標は,声のかすれや雑音などの非周期的な成分を示しています.
音声の読み込み
まず,なんでも良いので適当な音声データを用意します.
ここでは,僕が「こんにちは」と言っている音声を使います(音声は公開しませんが......).
音声データの読み込みには LibSndFile.jl
を使います.
LibSndFile.jl
の README に使い方が書いてありますが,僕の環境ではうまくいかなかったのでちょっと違う方法でいきます.
using LibSndFile
using FileIO
buf = loadstreaming("konnichiha.wav") |> read
x = Float64.(buf.data[:, 1])
fs = Int(buf.samplerate)
読み込んだ直後のデータの型は SampleBuf
型になっていて,このままでは WORLD.jl
に渡せないので,データを Vector{Float64}
型で持っておきます.
F0の推定と変換
dio()
という関数を使ってF0を推定できます.
period = 5.0
dioopt = DioOption(f0floor=71.0, f0ceil=800.0,
channels_in_octave=2.0, period=period, speed=1)
f0, timeaxis = dio(x, fs, dioopt)
これをプロットしてみると,
using Plots
plot(timeaxis, f0)
こんな感じでの F0 の時間変化が見れます.
だいたい100 Hz から150 Hz くらいの音声みたいですね.
また,stonemask()
によって F0 を補正できるみたいなので,やってみると以下のような感じになります.より細かい変化も捉えられているようです.
f0 = stonemask(x, fs, timeaxis, f0)
F0は声の高さに対応しているので,声の質はともかく,F0を高くすれば高い声に,低くすれば低い声になります.
ここでは簡単に,各時刻でのF0をそのまま1.5倍にしてみます.配列の要素全てを1.5倍するだけなので示すまでもないかもしれませんが,一応示しておきます.
f0 *= 1.5
y1が変換前でy2が変換後のF0です.
スペクトル包絡の推定と変換
cheaptrick()
を使ってスペクトル包絡を推定します.
spec = cheaptrick(x, fs, timeaxis, f0)
heatmap(10log10.(spec))
これはスペクトル包絡の時間変化で,色が明るいほどその周波数成分が強いということです.
このあたりの話にはあまり詳しくないので,F0 の変化に合わせて周波数成分の分布も変化していることがわかればヨシとします.
スペクトル包絡は,F0と違ってスペクトル包絡は1フレームあたりに複数の要素が存在するため,その変換はちょっとだけ複雑になります.ここでは簡単に,スペクトル包絡全体を(周波数の方向に)上下に引き延ばしたり縮めたりすることを考えます.
具体的には,スペクトル包絡の各点を$a$倍高いところに移します.ここでは,見てわかりやすいように $a=2$ として引き延ばしてみます.
len = size(spec)[1]
idxs = ifelse(a > 1, len:-1:1, 1:len)
for i in idxs
j = clamp(round(Int,i/a), 1, len)
vp.spec[i, :] = spec[j, :]
end
heatmap(10log10.(spec))
全体的に引き延ばされていると思います.特に明るい色の部分を見比べてみるとわかりやすいと思います.
非周期性指標の推定
非周期性指標はいじりませんが,再合成に必要なので推定しておきます.
d4c()
によって推定できます.
aperiodicity = d4c(x, fs, timeaxis, f0)
heatmap(20log10.(aperiodicity))
音声の合成
以上で推定し変換したパラメータをもとにsynthesis()
で音声を合成し,wavファイルとして保存します.wavファイルの保存にはWAV
というパッケージを使います.LibSndFile
を使ってもできそうですが,ちょっとうまくいかなかったのでWAV
を使いました(うまくやる方法を知ってる人がいたら教えて欲しいです).
using WAV
y = synthesis(f0, spec, aperiodicity, period, fs, len)
wavwrite(y, fs, "converted_konnichiwa.wav")
おわりに
この記事では,F0とスペクトル包絡を変換することで声質変換を行いました.ここで示したような単純な変換では,声の高さや音色を大きく変化させると,不自然でいかにもボイチェンを使いましたというような音声になると思います(個人差もあると思います).僕の場合,声の高さをほんの少し変えるくらいなら,なんとか自然に聞こえるかなあという感じでした.
GUIについては,またそのうち記事を書いてみようと思っています.
参考文献
森勢 将雅, 音声パラメータのデザイン, 日本音響学会誌, 2018, 74 巻, 11 号, p. 608-612, 公開日 2019/05/01, Online ISSN 2432-2040, Print ISSN 0369-4232, https://doi.org/10.20697/jasj.74.11_608, https://www.jstage.jst.go.jp/article/jasj/74/11/74_608/_article/-char/ja