"BOKU"のITな日常

62歳・文系システムエンジニアの”BOKU”は日々勉強を楽しんでます

windows10+Python3.7+「pyaudio」で和音を含むメロディーを鳴らしてみる

pyaudioをインストールしたので、もう少し遊んでみます。

今回は和音交じりのメロディを記述できるように拡張してみます。

f:id:arakan_no_boku:20200707230113p:plain

 

前回やってみたのをベースにします

 

前回、Windows10+python3.7(Anaconda)環境に、pyaudioをインストールして、単音で音階をならすところまでやりました。

arakan-pgm-ai.hatenablog.com

そこで作成した「Audio」クラスを、和音もならせるように拡張しようと思います。

 

使える音階の幅をひろげておこう

 

前回はお試しだったので、1オクターブだけしか音階を作っていません。

和音を使うからには、せめて3オクターブくらいは欲しいので、拡張します。

PyAudioで音階を表現するには、周波数を求める必要があって、A(ラ)の周波数が440Hzとして、これに2の十二乗根を掛け合わせていけば、半音ずつずらした音階の周波数が計算できるってのは、まったく、同じです。

ループを回す回数と、Aのところで0乗にするための数字だけ変更します。

 

   def make_scale(self):
        res = {}
        octave = -1
        for n in range(0, 37):
            hz = self.a_hz * 2 ** ((n - 21) / 12)
            if (n % 12) == 0:
                octave += 1
            name = self.code_names[n % 12] + str(octave)
            print(name, ":", hz)
            res[name] = hz
        return res

確認のためにprint文をいれてます。

C0 : 130.8127826502993
C#0 : 138.59131548843604
D0 : 146.8323839587038
D#0 : 155.56349186104046
E0 : 164.81377845643496
F0 : 174.61411571650194
F#0 : 184.9972113558172
G0 : 195.99771799087463
G#0 : 207.65234878997256
A0 : 220.0
A#0 : 233.08188075904496
B0 : 246.94165062806206
C1 : 261.6255653005986
C#1 : 277.1826309768721
D1 : 293.6647679174076
D#1 : 311.1269837220809
E1 : 329.6275569128699
F1 : 349.2282314330039
F#1 : 369.9944227116344
G1 : 391.99543598174927
G#1 : 415.3046975799451
A1 : 440.0
A#1 : 466.1637615180899
B1 : 493.8833012561241
C2 : 523.2511306011972
C#2 : 554.3652619537442
D2 : 587.3295358348151
D#2 : 622.2539674441618
E2 : 659.2551138257398
F2 : 698.4564628660078
F#2 : 739.9888454232688
G2 : 783.9908719634985
G#2 : 830.6093951598903
A2 : 880.0
A#2 : 932.3275230361799
B2 : 987.7666025122483
C3 : 1046.5022612023945

とりあえず3オクターブ分いけてるっぽいので、次にいきます。

 

PyAudioで和音をならせる様に改良したクラス

 

pyAudioで単音を鳴らすのに「np.sin(np.arange(slen) * t) * gain」みたいにして、Sin派を生成していました。

 

ということは。

普通に考えれば、和音にする=音を重ねる・・ということなので、単音のSin波を合成してやればいいということになります。

イメージとしては音の数だけ。

tone += np.sin(np.arange(slen) * t) * gain

 みたいに足しこんでやって、最後に「tone」を再生すればいいという感じです。

なんですが・・。

実際にやってみると、それだけだと和音(コード)のところで、急に音がでかくなってノイズみたいな音になってしまいます。

なので、今回はtoneに(1 ÷ 重ねる音数)の結果を掛け合わせる方法をとりました。

これが正解なのか?

あんまり情報がないのでよくわかりません。

でも、いろいろ試して一番自然な音量になったのが、この方法なので、そうしてます。

修正したAudioクラスです。

import pyaudio as pa
import numpy as np


class Audio:

    def __init__(self, bpm=30):
        self.audio = pa.PyAudio()
        self.a_hz = 440
        self.smpl_rate = 44100
        self.bpm = bpm
        self.code_names = (
            "C",
            "C#",
            "D",
            "D#",
            "E",
            "F",
            "F#",
            "G",
            "G#",
            "A",
            "A#",
            "B")
        self.stream = self.audio.open(
            format=pa.paFloat32,
            channels=1,
            rate=self.smpl_rate,
            frames_per_buffer=1024,
            output=True)

    def __tone(self, hz, note, gain=1.0):
        slen = int(note * self.smpl_rate)
        t = float(hz[0]) * np.pi * 2 / self.smpl_rate
        tone = np.sin(np.arange(slen) * t) * gain
        if (len(hz) > 1):
            for i in range(1, len(hz)):
                t = float(hz[i]) * np.pi * 2 / self.smpl_rate
                tone += np.sin(np.arange(slen) * t) * gain
        return tone * (1 / len(hz))

    # 音符
    def make_note(self):
        self.note1 = 60 / (self.bpm * 4)
        notes = []
        notes.append(self.note1)
        notes.append(self.note1 / 2)
        notes.append(self.note1 / 4)
        notes.append(self.note1 / 8)
        return notes

    # 音階
    def make_scale(self):
        res = {}
        octave = -1
        for n in range(0, 37):
            hz = self.a_hz * 2 ** ((n - 21) / 12)
            if (n % 12) == 0:
                octave += 1
            name = self.code_names[n % 12] + str(octave)
            res[name] = hz
        return res

    def play(self, hz, note, gain=1.0, repeat=1):
        for j in range(repeat):
            self.stream.write(
                self.__tone(
                    hz, note, gain).astype(
                    np.float32).tostring())

変更したのは「__tone」メソッドです。

短音と和音(コード)でメソッドを変えるようなことはしたくなかったので、音階の周波数(hz)をリストで受け取るようにして、単音か和音(長さが2以上)で処理を分岐するようにしました。

 def __tone(self, hz, note, gain=1.0):
        slen = int(note * self.smpl_rate)
        t = float(hz[0]) * np.pi * 2 / self.smpl_rate
        tone = np.sin(np.arange(slen) * t) * gain
        if (len(hz) > 1):
            for i in range(1, len(hz)):
                t = float(hz[i]) * np.pi * 2 / self.smpl_rate
                tone += np.sin(np.arange(slen) * t) * gain
        return tone * (1 / len(hz))

これで、和音もならせるようになったはずです。

 

一発つかってみよう

 

キーCの王道コード進行である「FーGーEmーAm」をやってみます。

構成音はこちら。

  • F::ファ・ラ・ド
  • G:ソ・シ・レ
  • Em:ミ:ソ・シ
  • Am:ラ・ド・ミ

タン・タタ・タン・タタのシンプルなリズムにします。

せっかくなので、4小節くらい繰り返すようにして、和音を構成する1音だけオクターブを上げたり下げたりして変化をつける感じでやってみます。

ソースはこんな感じ。

a = Audio()
scales = a.make_scale()
notes = a.make_note()
a.play([scales["G1"]], notes[1])
a.play([scales["B0"]], notes[1])
for i in range(4):
    if((i % 2) == 1):
        a.play([scales["F1"], scales["A1"], scales["C1"]], notes[0])
        a.play([scales["F1"], scales["A1"], scales["C1"]], notes[1], repeat=2)
        a.play([scales["G1"], scales["B1"], scales["D1"]], notes[0])
        a.play([scales["G1"], scales["B1"], scales["D1"]], notes[1], repeat=2)
        a.play([scales["E1"], scales["G1"], scales["B1"]], notes[0])
        a.play([scales["E1"], scales["G1"], scales["B1"]], notes[1], repeat=2)
        a.play([scales["A1"], scales["C1"], scales["E1"]], notes[0])
        a.play([scales["A1"], scales["C1"], scales["E1"]], notes[1], repeat=2)
    else:
        a.play([scales["F1"], scales["A1"], scales["C2"]], notes[0])
        a.play([scales["F1"], scales["A1"], scales["C2"]], notes[1], repeat=2)
        a.play([scales["G1"], scales["B1"], scales["D2"]], notes[0])
        a.play([scales["G1"], scales["B1"], scales["D2"]], notes[1], repeat=2)
        a.play([scales["E1"], scales["G1"], scales["B0"]], notes[0])
        a.play([scales["E1"], scales["G1"], scales["B0"]], notes[1], repeat=2)
        a.play([scales["A1"], scales["C2"], scales["E1"]], notes[0])
        a.play([scales["A1"], scales["C2"], scales["E1"]], notes[1], repeat=2

鳴らしてみると、結構いい感じです。

王道コード進行ですしね。

これだけでも、コードをいろいろ変えてみたら、結構遊べました(笑)

今回はこんなところで。

ではでは。