Python音楽制作 pt.3:シンセ制作

メロディを作りたい。まずはsin波で遊べればいいので、のんびりやっていこう。シンセ制作って書いてあるけどシンセを作るほどのことはしていない。現状ではあくまで既存の数式を流用しているだけ。

サイン波を鳴らす

ある音を鳴らすには、まずその音を周波数に変換する必要がある。ということで、その変換から。とりあえず細かい変換などは考えずに、まずはA4(ラ)しか鳴らせない形でとにかく全体像を作っていくことにする。A4なのはちょうど440Hzで扱いやすいからですね。

今回はまた馬鹿みたいにplay_synth_in_the_specific_pointという関数名にします。track_nameなどの基本的な考え方や中身は割とhit_sound_in_the_specific_pointに近いですね。

    def play_synth_in_the_specific_point(self, sound='sin', note='A4', track_name='synth', start_beats=[0], lengths=[1], volume=1):
        if (volume > 1) or (volume < 0):
            raise ValueError('Volume must be in 0 - 1.')
        audio = self.audio
        blank = self.generate_blank_signal().rename(columns={'y':'sound'}).drop(columns='t')

今回は減衰とかそういうことは考えずに、とにかく指定した拍から指定した拍まで440Hzのサイン波を鳴らす。それだけが目標。それができればC5, C#5…と音を拡張していくなり、load_synthsound的な関数でシンセの音色を選べるようにするなり、拡張はいくらでもできる。

ここで問題になってくることとして、最終的なサンプリング周波数とシンセ側の周波数の関係の話が当然出てくる。今は44100回のサンプリングで1秒回るようになっている。その状態で440Hzを出すために設定すべきは、

noteFreq = {
            'A4': 440,
        }
frq = int(np.round(self.sampling_freq / noteFreq[note],0))

ですね。

このサンプリング回数で、sin波がradian一周分回ればいいわけなので、numpyのlinspaceを使って

np.linspace(-np.pi, np.pi, frq)

で1周分の横軸が生成できる。もちろんこれを必要な長さ分だけ繰り返せばいいわけだけど、たとえば2.5拍分鳴らすっつったときに綺麗に周期が一致するとは限らないので(というは普通は合わないので)、結局目標の長さ(今回は最長であるblank_signalの長さを埋めることを目標にする)を鳴らすのに何回繰り返せばいいのかを計算しないといけない。余っていいので多めに数える。それを繰り返すのに必要なのはnumpyのresizeを当てればいい。

        len = int(np.floor(self.sampling_freq)/frq)
        x = np.resize(np.linspace(-np.pi, np.pi, frq), len*len)
        synth = self.load_synthsound(sound=sound)
        sound = synth(x) * volume

ここで普通にnp.sin(x)を当てればサイン波の音は出せるわけだが、音を変えたいので柔軟に音色を変えられるよう音色を選択する関数 load_synthsound を挟む。今回はサイン波以外にscipy.signalからsawとsquareも用意した。

    def load_synthsound(self, sound='sin'):
        if sound=='sin':
            return np.sin
        elif sound=='saw':
            return signal.sawtooth
        elif sound=='square':
            return signal.square
        else:
            raise ValueError('Invalid synthsound name is specified.')

あとはどこからどこまで鳴らすかを考えればいい。

for pos, len in zip(start_beats, lengths):
            start = self.find_position(pos)
            end   = self.find_position(pos+len)
blank.loc[start:end, 'sound'] = sound[0:end-start+1]

まずは以前ドラム用に作ったfind_positionを使って、ビートの位置で指定しているstart-endを周波数ベースに変換する。それに基づいて、blank_signalの該当する場所に波形を頭から嵌め込む。

あとはドラムの時と同じように指定したトラックに流し込む。

        audio = audio.join(blank, how='outer').fillna(0)
        if track_name in audio.columns:
            audio[track_name] = audio[track_name] + audio['sound']
        else:
            audio[track_name] = audio['sound']
        audio = audio.drop(columns='sound')
        self.audio = audio

関数をまとめると、

    def play_synth_in_the_specific_point(self, sound='sin', note='A4', track_name='synth', start_beats=[0], lengths=[1], volume=1):
        noteFreq = {
            'G1': 48.999,
            'A1': 55,
            'C2': 65.406,
            'G2': 97.999,
            'A2': 110.000,
            'B2': 123.471,
            'C3': 130.813,
            'D3': 146.832,
            'E3': 164.814,
            'G3': 195.998,
            'A3': 220.000,
            'C4': 261.626,
            'D4': 293.665,
            'E4': 329.628,
            'G4': 391.995,
            'A4': 440,
            'B4': 493.883,
            'C5': 523.251,
            'D5': 587.330,
            'E5': 659.255,
            'F5': 698.456,
            'G5': 783.991
        }
        if (volume > 1) or (volume < 0):
            raise ValueError('Volume must be in 0 - 1.')
        audio = self.audio

        blank = self.generate_blank_signal().rename(columns={'y':'sound'}).drop(columns='t')
        frq = int(np.round(self.sampling_freq / noteFreq[note],0))
        len = int(np.floor(self.sampling_freq)/frq)

        x = np.resize(np.linspace(-np.pi, np.pi, frq), len*len)
        synth = self.load_synthsound(sound=sound)
        sound = synth(x) * volume

        for pos, len in zip(start_beats, lengths):
            start = self.find_position(pos)
            end   = self.find_position(pos+len)
            
            blank.loc[start:end, 'sound'] = sound[0:end-start+1]
        audio = audio.join(blank, how='outer').fillna(0)
        if track_name in audio.columns:
            audio[track_name] = audio[track_name] + audio['sound']
        else:
            audio[track_name] = audio['sound']
        audio = audio.drop(columns='sound')
        self.audio = audio

こんな感じかな。

せっかくなので辞書に付け足しておく。細かい音の変換はまた考えるので今は一旦一覧にしておく。

        noteFreq = {
            'G1': 48.999,
            'A1': 55,
            'C2': 65.406,
            'G2': 97.999,
            'A2': 110.000,
            'B2': 123.471,
            'C3': 130.813,
            'D3': 146.832,
            'E3': 164.814,
            'G3': 195.998,
            'A3': 220.000,
            'C4': 261.626,
            'D4': 293.665,
            'E4': 329.628,
            'G4': 391.995,
            'A4': 440,
            'B4': 493.883,
            'C5': 523.251,
            'D5': 587.330,
            'E5': 659.255,
            'F5': 698.456,
            'G5': 783.991
        }

メロディを組んでみる

音も鳴るようになったので、今作ったシンセ3音でマルチトラックにしてみるなら、

audio = AudioDataFrame(bpm = 90, bars = 1, sub=True)

audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='A3', start_beats=[0, 2],lengths=[0.5, 0.5],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='E3', start_beats=[0.5],lengths=[0.25],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='G3', start_beats=[1.25],lengths=[0.2],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='A3', start_beats=[2],lengths=[0.2],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='C4', start_beats=[2.75],lengths=[0.2],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='D4', start_beats=[3.25],lengths=[0.2],volume=0.1)

audio.play_synth_in_the_specific_point(track_name='SYNTH1',sound='saw', note='A4', start_beats=[0],lengths=[1],volume=0.02)
audio.play_synth_in_the_specific_point(track_name='SYNTH1',sound='saw', note='C5', start_beats=[0],lengths=[1],volume=0.02)
audio.play_synth_in_the_specific_point(track_name='SYNTH1',sound='saw', note='E5', start_beats=[0],lengths=[1],volume=0.02)

audio.play_synth_in_the_specific_point(track_name='SYNTH2',sound='square', note='A4', start_beats=[0,0.75,1.5,1.75],lengths=[0.15,0.15,0.15,0.15,0.15,0.15],volume=0.05)

audio.play(metronome=False)
audio.show(parallel=True)

波形にすると、

面白くなってきました。

ただ、音の高さをずらすだけで同じ音色を鳴らすような場合、今のコードでいうとたとえば以下の部分、

audio.play_synth_in_the_specific_point(track_name='SYNTH1',sound='saw', note='A4', start_beats=[0],lengths=[1],volume=0.02)
audio.play_synth_in_the_specific_point(track_name='SYNTH1',sound='saw', note='C5', start_beats=[0],lengths=[1],volume=0.02)
audio.play_synth_in_the_specific_point(track_name='SYNTH1',sound='saw', note='E5', start_beats=[0],lengths=[1],volume=0.02)

これは3行に分ける必要はない。無意味すぎる。

ということでplay_chord_in_the_specific_pointという感じでまとめる。さっきのplay_synth_in_the_specific_point関数のnoteをリスト形式で複数流し込むだけの関数なので、指定した音をループで順に設定していく非常にシンプルな関数になった。

    def play_chord_in_the_specific_point(self, sound='sin', notelist=None, track_name='synth', start_beats=[0], lengths=[1], volume=1):
        for n in notelist:
            self.play_synth_in_the_specific_point(track_name=track_name, sound=sound, note=n, start_beats=start_beats, lengths=lengths, volume=volume)

ということでドラムも含めて鳴らしてみると、

audio = AudioDataFrame(bpm = 90, bars = 2, sub=True)
audio.hit_sound_in_the_specific_point(track_name='drm_PERC',sound='CRASH', start_beats=[0], volume=0.1)
audio.hit_sound_in_the_specific_point(track_name='drm_PERC',sound='CHH', start_beats=[0, 0.25, 0.5, 0.75,0.8,0.85,0.9,0.95, 1, 1.25, 1.5, 1.75, 2, 2.125, 2.25, 2.5, 2.75, 3, 3.15, 3.3, 3.45, 3.6], volume=0.2)
audio.hit_sound_in_the_specific_point(track_name='drm_KICK',sound='KICK', start_beats=[0, 0.5, 2.5, 2.75], volume=0.3)
audio.hit_sound_in_the_specific_point(track_name='drm_SNARE',sound='SNARE', start_beats=[1.75, 2.25, 3], volume=0.2)
audio.hit_sound_in_the_specific_point(track_name='drm_PERC',sound='CHH', start_beats=[i/4+4 for i in range(16)], volume=0.2)
audio.hit_sound_in_the_specific_point(track_name='drm_SNARE',sound='SNARE', start_beats=[4, 4.75, 5, 5.5, 6.25, 6.5, 7, 7.5, 7.625, 7.75], volume=0.2)
audio.hit_sound_in_the_specific_point(track_name='drm_CLAP',sound='CLAP', start_beats=[1, 3, 5, 7], volume=0.15)

audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='A3', start_beats=[0, 4],lengths=[0.5, 0.5],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='E3', start_beats=[0.5],lengths=[0.25],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='G3', start_beats=[1.25],lengths=[0.2],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='A3', start_beats=[2],lengths=[0.2],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='C4', start_beats=[2.75],lengths=[0.2],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='D4', start_beats=[3.25],lengths=[0.2],volume=0.1)

audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='C4', start_beats=[5],lengths=[0.5],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='D4', start_beats=[6],lengths=[0.5],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='E4', start_beats=[7],lengths=[0.5],volume=0.1)
audio.play_synth_in_the_specific_point(track_name='BASS',sound='sin', note='G3', start_beats=[7.5],lengths=[0.35],volume=0.1)

audio.play_chord_in_the_specific_point(track_name='SYNTH',sound='saw', notelist=['A4','C5','E5','G5'], start_beats=[0],lengths=[1],volume=0.05)
audio.play_synth_in_the_specific_point(track_name='SYNTH',sound='saw', note='A4', start_beats=[4.00],lengths=[0.25],volume=0.05)
audio.play_synth_in_the_specific_point(track_name='SYNTH',sound='saw', note='C5', start_beats=[4.25],lengths=[0.25],volume=0.05)
audio.play_synth_in_the_specific_point(track_name='SYNTH',sound='saw', note='E5', start_beats=[4.50],lengths=[0.25],volume=0.05)
audio.play_synth_in_the_specific_point(track_name='SYNTH',sound='saw', note='A4', start_beats=[4.75],lengths=[0.50],volume=0.05)
audio.play_synth_in_the_specific_point(track_name='SYNTH',sound='saw', note='G4', start_beats=[5.25],lengths=[0.50],volume=0.05)
audio.play_synth_in_the_specific_point(track_name='SYNTH',sound='saw', note='A3', start_beats=[5.75],lengths=[0.25],volume=0.05)

audio.play_synth_in_the_specific_point(track_name='SYNTH2',sound='square', note='A4', start_beats=[0,0.75,1.5,1.75],lengths=[0.15,0.15,0.15,0.15,0.15,0.15],volume=0.05)
audio.play_synth_in_the_specific_point(track_name='SYNTH2',sound='square', note='A3', start_beats=[4,4.75,5.5,5.75],lengths=[0.15,0.15,0.15,0.15,0.15,0.15],volume=0.05)

audio.play(metronome=False)
audio.show(parallel=True)

ただのありがちな波形を使っているだけなので音はまだ頼りないけど、作りとしてはいい感じになってきました。各打ち込みをもう少しメタ的に関数にまとめていければそれらしくなっていくはず。

次あたりでそろそろディレイでも作ってみようかと思っている。