Python音楽制作 pt.3

楽しすぎてものすごいペースで開発が進んでいます。これPythonサーバーに移植してGUIつけて自分専用のクラウドDAWにするまであるな。

前回からの改良として、作った音をいきなりマスタートラックに上書きするのはやはり良くない。トラック数が増えてくるとミックスいじったりするようになるはずだ。どこかのタイミングで、やっぱり後からトラックごと音量を下げたいとかいうことが出てくるはず。ということでトラック機能をつける。

とりあえず音ネタを増やした。将来的にはこれの一つ上のレイヤーとしてload_drumkitを作って、boombapっぽいドラムやら808やら色々とドラムキットを選べるようにするけど今回はとりあえずこのまま行く。

    def load_drumsound(self, sound='CHH'):
        drumkit = {
            'CRASH': '/content/drive/MyDrive/PyMusic/crash.wav',
            'CHH': '/content/drive/MyDrive/PyMusic/hihat.wav',
            'KICK': '/content/drive/MyDrive/PyMusic/kick.wav',
            'SNARE': '/content/drive/MyDrive/PyMusic/snare.wav',
            'CLAP': '/content/drive/MyDrive/PyMusic/clap.wav'
        }

トラックを作るためにいじるべきは当然hit_sound_in_the_specific_point関数である。ここに新しく引数としてtrack_nameを作る。それにより、track_nameに指定した名前のトラックがなければ新しく作り、あればそこに音を追加することができるようになる。そして最後のplayの際にどの音をミックスして鳴らすか選べばいい。まあこんな感じかな。

    def hit_sound_in_the_specific_point(self, track_name='CHH', sound='CHH', start_beats = [0], volume=1):
        if (volume > 1) or (volume < 0):
            raise ValueError('Volume must be in 0 - 1.')
        audio = self.audio
        sound, fr = self.load_drumsound(sound=sound)
        for pos in start_beats:
            pos = self.find_position(pos)
            blank = self.generate_blank_signal().rename(columns={'y':'sound'}).drop(columns='t')
            blank.loc[pos:pos+fr-1, 'sound'] = sound * volume
            
            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['y'] = audio['y'] + audio['sound']
            audio = audio.drop(columns='sound')
        self.audio = audio

マスターの音(いわゆるaudio[‘y’])には何の信号も追記されないようになったので、この状態でインスタンスを立ち上げて音を追加してもplay()は完全に無音を吐き出すようになる。でもその状態でたとえばいくつかの音を追加してaudioDFのカラム一覧を表示させると、

各音源が独立して保存されていることがわかる。そしてせっかくなら各トラックの中身を可視化したいのでshowをいじって、パラ出し(parallel=True)の時だけ各トラックの中身も吐くように設定する。

    def show(self,parallel=False):
        self.audio.plot(x='t', y='y', ylim=[-1,1])
        if parallel:
            self.audio.drop(columns=['y','metronome']).plot(x='t', ylim=[-1,1],subplots=True)

これで、適当にトラックを追加してあげた状態でaudio.show(parallel=True)を走らせると、

いい感じに吐いてくれるようになった。

ここで、「どうせ音色を指定してるんだから別でtrack_name指定する必要なくない?」という意見もまああるとは思うんだけど、単純に僕の使いやすさとして、クラッシュとハイハットの音はまとめてリバーブやイコライジングをしたいというのがあるので、音色が違ってもトラックを同じにしたいわけです。だからクラッシュとオープン/クローズハイハットを全部まとめてdrm_PERCとしたっていいわけです。

悪くないですね。

んであとはこれをplayの際にミックスしてから再生させればいいわけ。すでにメトロノームはplayの時点で再生するかどうか決めてミックスしてんだから同じことですね。引数soundsでトラック名のリストを渡してあげるイメージ。

    def play(self, sounds=None, metronome=True, isLimit=True):
        if metronome:
            self.audio['y'] = self.audio['y'] + self.audio['metronome']

        if not sounds:
            sounds = self.audio.drop(columns=['t','y','metronome']).columns
        for s in sounds:
            self.audio['y'] = self.audio['y'] + self.audio[s]
        
        if isLimit:
            self.audio.loc[ self.audio['y'] >  1, 'y' ] =  1
            self.audio.loc[ self.audio['y'] < -1, 'y' ] = -1
        display(Audio(self.audio['y'], rate=self.sampling_freq))

こうすると、独立したトラックとしては音は入っているんだけど最終的なマスターには追加されないような音が出てくるので、

audio = AudioDataFrame(bpm = 90, bars = 1, 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.play(sounds=['drm_PERC'], metronome=False)
audio.show(parallel=True)

とかすると、4つの音色でKICK, SNARE, PERCの3つのトラックを作ったけど最終的に鳴らすのはPERCだけみたいなことができる。soundsの指定をしないとそのまま全部の音が鳴る。

やってみて思ったけどyの波形だけデカくて鬱陶しいのでshowの中身を書き換えます。parallel=Trueの場合は全体のプロットは出さずに、subplotの中にyの波形も含めることにします。

def show(self,parallel=False):
        if parallel:
            self.audio.drop(columns=['metronome']).plot(x='t', ylim=[-1,1],subplots=True)
        else:
            self.audio.plot(x='t', y='y', ylim=[-1,1])

うん、だいぶいい感じになった。今回はここまで。

結局まだシンセは作れず。結局ビートの方が楽しいんですよね。たぶんシンセは下手にちょろっと作ったぐらいじゃいい感じにならないので。このままだと次回結局ドラムキットを作る可能性もあるけど、軽くメロの打ち込みできるようにはしたい。

ということで今回の内容は以下のGistに。