音声の速さ(長さ)を変えずに高さを変える
アルゴリズム
前回・前々回のプログラムを組み合わせると、『音声の速さを変えずに高さを変える』ことができるようになります。つまり、
周波数をn倍(長さは1/n倍) → 周波数を変えずに長さをn倍
または逆に、
周波数を変えずに長さをn倍 → 周波数をn倍(長さは1/n倍)
とすればいいわけです。
準備
準備段階として、前回・前々回のプログラムをモジュールに書き換えました。関数にしただけで処理内容は変わりません。
import numpy
from scipy import interpolate
def changeSpeed(src, speed):
# 変換後のデータを格納する配列を用意
count = int((len(src)-1)/speed)
dst = numpy.zeros(count, dtype="int16")
# 補間関数を求める
start = time.time()
f = interpolate.interp1d(range(len(src)), src, kind="cubic") # 補間関数の求め方を替える場合はこの行を書き換える
for i in range(0, count):
dst[i] = f(i*speed)
return dst
まず『テープを早回し/スロー再生するような速度変換』です。主要な処理を関数にし、モジュールとして使えるようにしました。後のプログラムでは wavChangeSpeed.py という名前で保存したものとしています。
changeSpeed()メソッドは、srcにリストやnumpy.ndarray型式のサンプリングデータ、speedに現在の何倍の速度にするかを与えれば、numpy.ndarray型式で変換後のサンプリングデータが返却されます。
import numpy
# 二乗誤差平均を求める
def calcerror(a1, a2):
error = 0
for i in range(len(a1)):
try:
error += ((a1[i]-a2[i])**2)/len(a1)
except:
break
return error
# 再生速度変更本体
def timeStretch(src, speed, samplingfreq=44100):
windowsize=int(samplingfreq*0.020) # 『窓』の幅は20ms
xfaderate = 0.2 # クロスフェードで重ねる部分の幅(窓の幅に対する比)は windowsize の20%
count = int( (len(src)-windowsize)/(windowsize*speed) )
dst = numpy.zeros(int(len(src)/speed), dtype="int16")
for i in range(0, count):
src_s = int(i*windowsize*speed)
dst_s = i*windowsize
# 2乗誤差平均がいちばん小さいところに繋げる
if i>0:
src_s0 = src_s
min = 65535**2
for j in range(src_s, int(src_s+windowsize*xfaderate), 1):
error = calcerror(dst[dst_s:dst_s+int(windowsize*xfaderate)], src[j:j+int(windowsize*xfaderate)])
if error<min:
min=error
src_s0 = j
src_s = src_s0
# クロスフェードで繋げる
for i in range(int(windowsize*(1+xfaderate))): # クロスフェードのため、窓の幅よりxfaderate分だけ余分にコピー
try:
if i<windowsize*xfaderate:
dst[dst_s+i] = dst[dst_s+i]*(1-(i/(windowsize*xfaderate))) + src[src_s+i]*(i/(windowsize*xfaderate)) # クロスフェード
else:
dst[dst_s+i] = src[src_s+i] # 他と重ならない部分はそのままコピー
except:
break
return dst
続いて、『音の高さを変えずに速く/遅くする』プログラムです。主要な処理を関数にし、モジュールとして使えるようにしました。後のプログラムでは wavTimeStretch.py というファイル名で保存したものとしています。
timeStretch()メソッドは、srcにリストやnumpy.ndarray型式のサンプリングデータ、speedに現在の何倍の速度にするか、samplingfreqにサンプリング周波数を与えれば、numpy.ndarray型式で変換後のサンプリングデータが返却されます。
とりあえず動かす
では、早速試してみましょう。
import numpy
import os
import wavfile
import wavTimeStretch
import wavChangeSpeed
# 元音声の読み込み
f = wavfile.readfile(os.path.dirname(__file__)+"/ぐそくむし元.wav")
src = f["ldata"]
samplingfreq = int(f["samplingfreq"])
speed = 0.5
dst1 = wavChangeSpeed.changeSpeed(src, speed)
dst2 = wavTimeStretch.timeStretch(dst1, 1/speed, samplingfreq)
wavfile.writefile(numpy.array([dst2]), os.path.dirname(__file__)+"/output.wav", samplingfreq=samplingfreq)
原理は簡単で、changeSpeed() で速度(周波数)を speed 倍にしたあと、timeStretch() で速度(長さの逆数)を 1/speed 倍にしているだけです。
例によって元データはこちら。
※この音声は『VOICEVOX』というソフトウェアを使用して作成しました。サイトはこちら
※音声ライブラリは『VOICEVOX:四国めたん』を使用しています。
では、まず周波数を2倍に。
続いて、周波数を1/2倍に。
このとおり、単語の発音のスピードは同じままで声の高さだけが変化しています。
同じ人間が高い声や低い声を出しているというより、ボイスチェンジャを使ったような声ですね。『使ったような』というか、簡単なボイスチェンジャはこのような原理で声を変換しているのでしょう。ただし逆の変換をすると元の声が復元できてしまうので(変換の途中でデータを省いたり近似値を使ったりするので完全に元と同一ではありませんが)、『身元を隠す』という用途にはイマイチだと思いますが。
音階をつけてみる
これで、元となる音声データを用意すれば、それを任意の高さに変換できるようになりました。50音それぞれの音声データを用意できれば好きなメロディで好きな歌詞を歌わせることもできそうです。
やってみましょう!
音階の基礎知識
各音階の周波数の求め方にはいろいろなやり方があり、微妙に周波数が違うのですが、今回はピアノの調律に採用されている平均律という方法を使います。平均律では、半音ごとに周波数が \(2^{\frac{1}{12}}\) 倍となります。12半音でちょうど周波数の比が2\(2^{\frac{12}{12}}=2\) 倍です。つまり ド → その上のド → そのまた上のド とnオクターヴ違いの音は、周波数が \(2^n\) 倍となります。単純な掛け算だけですべての音の周波数を求めることができ、またどんな調性・和音でもまぁまぁ無難に響くというメリットがあります。
基準をA4(ピアノの鍵盤の中央付近のラの音)=440Hz とし、A4を含む1オクターブ(C4~C5)の周波数を計算すると以下のようになります。
C4 | 261.6255653005986 |
C#4 | 277.1826309768721 |
D4 | 293.6647679174076 |
D#4 | 311.1269837220809 |
E4 | 329.6275569128699 |
F4 | 349.2282314330039 |
F#4 | 369.9944227116344 |
G4 | 391.99543598174927 |
G#4 | 415.3046975799451 |
A4 | 440.0 |
A#4 | 466.1637615180899 |
B4 | 493.8833012561241 |
C5 | 523.2511306011972 |
Python の float の精度で計算した結果をそのまま書いてしまいましたが、小数点以下を四捨五入した値を使用しても実用上はまぁ問題ないでしょう。
素材の用意
いままで音源に使っていたソフトが単音に対応していないようなので(仕様?不具合?それとも僕が何か使い方を間違えているのだろうか…)、今回は五十音単音かつ音程が決まった音源を使います。幸い、ネットを探すといろいろな方が音源を公開して下さっていますので、今回はこちらのサイトで配布されているものを使用します。五十音それぞれの単音を、一定の音程G4=ピアノの中央付近の『ソ』の音で発音しているという、今回の実験には最適なデータです。
サンプルプログラムその1
import numpy
import wavfile
import wavTimeStretch
import wavChangeSpeed
# 変数の初期化
basefreq = 392.0 # 元音声の周波数。G4なので 392Hz としておく
samplingfreq = 44100 # サンプリング周波数のデフォルト値
# 各音階の周波数
scaleFreq = {"C4":262.6, "C#4":277.2, "D4":293.7, "D#4":311.1, "E4":329.6, "F4":349.2, "F#4":370.0,
"G4":392.0, "G#4":415.3, "A4":440.0, "A#4":466.2, "B4":493.9, "C5":523.3}
def makeSyllable(scale):
freq = scaleFreq[scale]
freqRate = freq/basefreq
dst1 = wavChangeSpeed.changeSpeed(src, freqRate)
dst2 = wavTimeStretch.timeStretch(dst1, 1/freqRate, samplingfreq)
return dst2
# 元音声の読み込み
f = wavfile.readfile("あ.wav")
src = f["ldata"]
samplingfreq = f["samplingfreq"]
dst = numpy.array(0, dtype="int16")
dst = numpy.append(dst, makeSyllable("C4"))
dst = numpy.append(dst, makeSyllable("D4"))
dst = numpy.append(dst, makeSyllable("E4"))
dst = numpy.append(dst, makeSyllable("F4"))
dst = numpy.append(dst, makeSyllable("G4"))
dst = numpy.append(dst, makeSyllable("A4"))
dst = numpy.append(dst, makeSyllable("B4"))
dst = numpy.append(dst, makeSyllable("C5"))
wavfile.writefile(numpy.array([dst]), "output.wav", samplingfreq=samplingfreq)
まずは『あ』でドレミファソラシドと歌わせてみます。
各音階の周波数のリストを与えておき、元の周波数(G4=392Hzとしています)との比 freqRate を求め、changeSpeed() で再生速度(周波数)を freqRate 倍にます。そうすると長さが1/freqRate倍になってしまうので、次は timeStretch() で周波数をそのままに再生速度を 1/freqRate 倍して音の長さをもとに戻しています。
結果は…
ちゃんと音階になっています。
サンプルプログラムその2
先ほどのプログラムを改良して、『あ』以外の音も使えるようにします。
import numpy
import wavfile
import wavTimeStretch
import wavChangeSpeed
# 変数の初期化
basefreq = 392.0 # 元音声の周波数。G4なので 392Hz としておく
speed = 120/60 # 演奏のテンポ
samplingfreq = 44100 # サンプリング周波数のデフォルト値
# 各音階の周波数
scaleFreq = {"C4":262.6, "C#4":277.2, "D4":293.7, "D#4":311.1, "E4":329.6, "F4":349.2, "F#4":370.0,
"G4":392.0, "G#4":415.3, "A4":440.0, "A#4":466.2, "B4":493.9, "C5":523.3}
def makeSyllable(scale, char):
freq = scaleFreq[scale]
freqRate = freq/basefreq
dst1 = wavChangeSpeed.changeSpeed(src[char], freqRate)
dst2 = wavTimeStretch.timeStretch(dst1, 1/freqRate, samplingfreq)
return dst2
# 元音声の読み込み
src = {}
f = wavfile.readfile("ど.wav")
src["ど"] = f["ldata"]
f = wavfile.readfile("れ.wav")
src["れ"] = f["ldata"]
f = wavfile.readfile("み.wav")
src["み"] = f["ldata"]
f = wavfile.readfile("ふぁ.wav")
src["ふぁ"] = f["ldata"]
f = wavfile.readfile("そ.wav")
src["そ"] = f["ldata"]
f = wavfile.readfile("ら.wav")
src["ら"] = f["ldata"]
f = wavfile.readfile("し.wav")
src["し"] = f["ldata"]
samplingfreq = f["samplingfreq"]
dst = numpy.array(0, dtype="int16")
dst = numpy.append(dst, makeSyllable("C4", "ど"))
dst = numpy.append(dst, makeSyllable("D4", "れ"))
dst = numpy.append(dst, makeSyllable("E4", "み"))
dst = numpy.append(dst, makeSyllable("F4", "ふぁ"))
dst = numpy.append(dst, makeSyllable("G4", "そ"))
dst = numpy.append(dst, makeSyllable("A4", "ら"))
dst = numpy.append(dst, makeSyllable("B4", "し"))
dst = numpy.append(dst, makeSyllable("C5", "ど"))
wavfile.writefile(numpy.array([dst]), "output.wav", samplingfreq=samplingfreq)
makeSyllable() メソッドを少し改造して、使用する音節を引数で指定できるようにしました。また必要な各音節を読み込むように追加しています。
それでは結果出力をどうぞ。
音節の繋ぎが雑ですが、簡単なプログラムのわりには悪くないです。
Pythonに歌わせてみる
サンプルプログラムその3
では最後に、音符の長さを指定できるようにしましょう。
import numpy
import os
import wavfile
import wavTimeStretch
import wavChangeSpeed
# 変数の初期化
basefreq = 392.0 # 元音声の周波数。G4なので 392Hz としておく
tempo = 120/60 # 演奏のテンポ
samplingfreq = 44100 # サンプリング周波数のデフォルト値
# 各音階の周波数
scaleFreq = {"C4":262.6, "C#4":277.2, "D4":293.7, "D#4":311.1, "E4":329.6, "F4":349.2, "F#4":370.0,
"G4":392.0, "G#4":415.3, "A4":440.0, "A#4":466.2, "B4":493.9, "C5":523.3}
def makeSyllable(scale, length, char):
freq = scaleFreq[scale]
freqRate = freq/basefreq
dst1 = wavChangeSpeed.changeSpeed(src[char], freqRate)
notelength = samplingfreq/(tempo/60) # 1拍(四分音符1つ分)のサンプリングデータ数
notelengthrate = notelength/len(dst1) # 音節の長さを1拍分にするための比例定数
dst2 = wavTimeStretch.timeStretch(dst1, 1/(notelengthrate*length), samplingfreq)
return dst2
# 元音声の読み込み
src = {}
f = wavfile.readfile("ぐ.wav")
src["ぐ"] = f["ldata"]
f = wavfile.readfile("そ.wav")
src["そ"] = f["ldata"]
f = wavfile.readfile("く.wav")
src["く"] = f["ldata"]
f = wavfile.readfile("む.wav")
src["む"] = f["ldata"]
f = wavfile.readfile("し.wav")
src["し"] = f["ldata"]
samplingfreq = f["samplingfreq"]
dst = numpy.array(0, dtype="int16")
dst = numpy.array(0, dtype="int16")
dst = numpy.append(dst, makeSyllable("D4", 0.75, "ぐ"))
dst = numpy.append(dst, makeSyllable("F4", 0.25, "そ"))
dst = numpy.append(dst, makeSyllable("G4", 0.75, "く"))
dst = numpy.append(dst, makeSyllable("F4", 0.25, "む"))
dst = numpy.append(dst, makeSyllable("G4", 1, "し"))
wavfile.writefile(numpy.array([dst]), "output.wav", samplingfreq=samplingfreq)
makeSyllable() メソッドがまた少し複雑になりました。引数で音符の長さ length を指定し、グローバル変数 tempo と合わせて、音節の長さを調節するようになっています。
ちょっと式がややこしくなってきたので一応説明を。
グローバル変数 tempo は楽譜によく書いてある『♩=○○』の値と同じで、1分間に何拍(四分音符が何個)あるのか、を表しています。つまり
tempo/60 は1秒あたりの拍数
notelength = samplingfreq/(tempo/60) は1秒あたりのサンプリングデータ数を1秒あたりの拍数で割っているので、1拍(1音節)あたりのサンプリングデータ数になります。
周波数の変換が終わった時点で、その音節のサンプリングデータ数は len(dst1) なので、これを1拍分のサンプリングデータ数にするには、長さを notelengthrate = notelength/len(dst1) 倍にすれば良いことになります。
さらに、引数 length は1拍(四分音符1個分)の長さを1とした比率を表しています。つまり、
四分音符なら1
付点八分音符なら0.75
八分音符なら0.5
十六分音符なら0.25
等々、です。
これを notelengthrate に掛けた notelengthrate * length がサンプリングデータの個数の比になるので、timeStretch() メソッドでサンプリングデータの個数を notelengthrate * length 倍すれば望みの長さの音節データを得ることが出来ます。ただしtimeStretch() メソッドは、引数として『音の長さの比』ではなく『速度の比』を与えるので、逆数 1/(notelengthrate*length) とする必要があります。
では、結果を聞いてみましょう。
とうとう歌わせたぞ!ボカロなどのソフトを使わずに!
※この曲は NHK Eテレのアニメ『ラブライブ・スーパースター』2021年8月15日放送の第4話『街角ギャラクシー』で視聴者に強いインパクトを与えた劇中歌です。正式な曲タイトルや作曲者は本記事投稿時点では明かされていません。サントラはよ。
まとめ
というわけで、サンプリングデータの周波数変換などについてはこれで一段落です。
今回試作した『歌うソフト』はボカロに較べるとあまりにもショボいことしかできないのですが、基本的な音声サンプリングデータの扱い方の勉強になりました。
コメント