コーラスっぽい音声を合成(ディープラーニングではない)

May 8, 2020

「歌声を合成する」というトピックが載っている唯一無二(?)の本、ということで以下を読んでいます。

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年以上前の手法なわけですが、思っていたより人間っぽかったです。