「歌声を合成する」というトピックが載っている唯一無二(?)の本、ということで以下を読んでいます。
https://www.ohmsha.co.jp/book/9784274068942/
前書きでは「プログラミングができれば数学苦手でも大丈夫だよ」というノリですが、普通に数式で殴られ続ける硬派な本です。
4.7が「歌声の合成」となっているわけですが、ディープラーニングではなく、
1984年の論文「The CHANT Project : From the Synthesis of the Singing Voice to Synthesis in General」を元に信号を生成する処理になっています。
書籍のソースコードはCなのですが、それを参考にPythonで実装してみたのが以下です。
(そのまま移植したら生成に数十秒とかかかってしまったので、いろいろnumpyに任せました)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import numpy as np
import math
import random
from scipy.io import wavfile
from functools import reduce
from operator import itemgetter
def fof(center_frequency, bandwidth, amplitude, attack_time, initial_phase, fs, number_of_data_in_points):
beta = np.pi / attack_time
alpha = bandwidth * np.pi
omegac = 2 * np.pi * center_frequency
pip_beta = math.floor(attack_time * fs)
t = np.arange(pip_beta) / fs
s1 = 0.5 * (1 - np.cos(beta * t)) * np.exp(-alpha * t) * np.sin(omegac * t + initial_phase)
t = np.arange(pip_beta + 1, number_of_data_in_points + 1) / fs
s2 = np.exp(-alpha * t) * np.sin(omegac * t + initial_phase)
s = np.concatenate([s1, s2])
cons = amplitude / s.max()
return s * cons
def chant(fs, duration, f0):
pi = np.pi
irandmax = 32767
nfft = 4096
number_of_data = nfft
np_all = (int)(fs * duration)
fmt_id = 1
nformant = 5
max_amp = 32768 * math.pow(10, -3.01 / 20)
cf10 = 980
cf2 = 2450
cf30 = 3920
cf4 = 7000
cf5 = 9000
bw10 = 500
bw2 = 500
bw3 = 500
bw4 = 500
bw5 = 500
a1 = 0.6 * max_amp
a2 = 0.01 * max_amp
a3 = 0.4 * max_amp
a4 = 0.1 * max_amp
a5 = 0.02 * max_amp
atk1 = 0.005
atk2 = 0.003
atk3 = 0.05
atk4 = 0.05
atk5 = 0.05
iphase1 = 0
iphase2 = 1 / 20 * pi
iphase3 = 2 / 20 * pi
iphase4 = 3 / 20 * pi
iphase5 = 4 / 20 * pi
ntaumax = 2000
vibf = 6
av = 0.06
vibcf = 7
avcf = 0.02
vibbw = 7
avbw = 0.02
avrand0 = 0.02
cfrand0 = 0.5
bwrand0 = 0.5
s = np.zeros((5, number_of_data))
y = np.zeros(np_all)
avrand = (np.random.rand(ntaumax) - 0.5) * avrand0
cfrand = (np.random.rand(ntaumax) - 0.5) * cfrand0
bwrand = (np.random.rand(ntaumax) - 0.5) * bwrand0
a = [a1, a2, a3, a4, a5]
atk = [atk1, atk2, atk3, atk4, atk5]
iphase = [iphase1, iphase2, iphase3, iphase4, iphase5]
k = 0
for i in range(ntaumax):
cf1 = (1 + avcf * math.sin(2 * pi * vibcf * k / fs + cfrand[i])) * cf10
cf3 = (1 + avcf * math.sin(2 * pi * vibcf * k / fs + cfrand[i])) * cf30
bw1 = (1 + avbw * math.sin(2 * pi * vibbw * k / fs + bwrand[i])) * bw10
cf = [cf1, cf2, cf3, cf4, cf5]
bw = [bw1, bw2, bw3, bw4, bw5]
for j in range(5):
s[j] = fof(cf[j], bw[j], a[j], atk[j], iphase[j], fs, number_of_data)
f00 = (1 + av * math.sin(2 * pi * vibf * k / fs + avrand[i])) * f0
tau = math.floor(fs / f00)
if k + number_of_data > np_all:
break
y[k:k + number_of_data] += np.sum(s, axis=0)
k += tau
# Deemphasis
a = 0.98
y[0] /= 2
y[1:k] = (a * y[0:k - 1] + y[1:k]) / 2
return np.trim_zeros(y.astype(np.int16))
def fade(y, duration):
fade_ms = 20
fade_size = int(len(y) * fade_ms / (duration * 1000))
fade_filter = np.concatenate([np.linspace(0, 1, fade_size), np.ones(len(y) - fade_size * 2), np.linspace(1, 0, fade_size)])
faded = np.array(y) * fade_filter
return faded
このchant
関数を使っていくつか音声を生成してみました。
あ〜
https://soundcloud.com/user-255261297-724174099/chant-a
1
2
3
4
5
6
fs = 44100
duration = 1.2
f0 = 490
y = fade(chant(fs, duration, f0), duration)
wavfile.write('output/chant_a.wav', fs, y.astype(np.int16))
ドレミファソラシド(ただし全部「あ」)
https://soundcloud.com/user-255261297-724174099/chant-doremi
1
2
3
4
5
6
7
doremi = [261.63, 293.66, 329.63, 349.23, 392.0, 440.0, 493.88, 524.25]
f0_list = doremi
duration_list = [0.4] * 8
score = zip(f0_list, duration_list)
y = reduce(lambda x, score: np.concatenate([x, fade(chant(fs, score[1], score[0]), score[1])]), score, [])
wavfile.write('output/chant_doremi.wav', fs, y.astype(np.int16))
かえるのうたが〜(これも全部「あ」)
https://soundcloud.com/user-255261297-724174099/chant-kaeru
1
2
3
4
5
f0_list = itemgetter(0, 1, 2, 3, 2, 1, 0)(doremi)
duration_list = [0.5] * 6 + [1.0]
score = zip(f0_list, duration_list)
y = reduce(lambda x, score: np.concatenate([x, fade(chant(fs, score[1], score[0]), score[1])]), score, [])
wavfile.write('output/chant_kaeru.wav', fs, y.astype(np.int16))
30年以上前の手法なわけですが、思っていたより人間っぽかったです。