![見出し画像](https://assets.st-note.com/production/uploads/images/157815413/rectangle_large_type_2_3cbc0e471f09585f949f697b47ced1dc.png?width=1200)
pythonテクニック 演奏機能付きコード譜エディタ
pythonプログラム教室の題材として、tkinterでコード譜エディタを作って、ハモンドっぽい音で演奏する機能を付加してみます。
音を出している裏で、次のコードの音を生成する必要があるので、マルチプロセス、共有メモリを使います。
使うモジュールは、こんな感じ、他にも色々。
tkinter GUI機能
pyaudio
from concurrent.futures.process import ProcessPoolExecutor
from logging import StreamHandler, Formatter, INFO, getLogger
from multiprocessing import Manager
1.プログラム構造
クラス sc77 コード名をもらって、PCMデータを生成
クラス hanko コード譜のGUI、sc77の起動
最下層 マルチプロセス起動、発音プロセス
名前がhankoになってるのは、電子判子ソフトを改造して作ったためです。
クラス hankoがコード1個ごとのpcmデータを生成して、共有メモリ経由で発音プロセスに渡すと、音がでて、その間にクラスsc77が次のコードのPCMを作成しておき、前の音が終わると、今作ったPCMを発音プロセスにわたす、という手順です。
2.画面の起動
self.xwidth = 800
self.ywidth = 600
print('chord diagram editor')
print('version',version_txt,version_date)
self.c0 = tk.Tk()
self.c0.title('Chord Diagram')
self.c0.geometry(str(self.xwidth)+'x'+str(self.ywidth))
ごく当たり前にtkinterのウインドウ起動です。
self.canvas = tk.Canvas(self.fm1,bg='white')
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 拡大許可でpack()
self.c0.update()
self.draw_dia() # 画面描画ルーチン
self.c0.bind('<Configure>',self.resize_dia) # ウィンドウをドラッグされたら
# 呼ぶ。
画面の大きさを可変にしたいので、expand=Trueでcanvasを定義します。
ルート画面のインスタンスself.coにbindを設定して、ドラッグされたら再描画するようにします。
3.再描画
def resize_dia(self,event):
# ウインドウのサイズを変えると飛んでくる。
self.cur_time = time.time()
delta_time = self.cur_time - self.past_time
# print ('dt=',delta_time)
self.past_time = self.cur_time
# ウインドウをドラッグしている間は書き直さない。
if not self.duty_flag:
self.c0.after(1000,self.resize_after) # 1秒後に書き直す設定
# print ('set after')
self.duty_flag = True
ずりずりとドラッグされている最中に毎回、再描画していると固まってしまうので、1秒タイマを仕掛けてそれまでは描画を止めます。
def draw_dia(self):
width = self.canvas.winfo_width()
height = self.canvas.winfo_height()
self.xwidth = self.c0.winfo_width()
self.ywidth = self.c0.winfo_height()
self.xvar.set(self.xwidth)
self.yvar.set(self.ywidth)
self.canvas.create_rectangle(10,10,width*0.8,height-10,tags='rect')
deltay = height/self.numline
deltax = (width*0.6)/self.numcolumn
再描画で、ウインドウの大きさを取得して、相対座標でwidgetを描いていきます。
4.発音のプロセス
def mp_play_chord(sh_dict , sh_status , sh_chunk):
# 発音プロセス
global stream
# 音声デバイス初期化
p = pyaudio.PyAudio()
print ('mp_play_chord pyaudio done.')
stream = p.open(format=pyaudio.paFloat32,
channels=1, rate=44100, output=1)
print ('start mp_play_chord')
while True:
# 発音トリガ待ち
while sh_status.value != 1:
usleep(10)
#
chunkp = sh_chunk[:] # list型で受信
chunk =np.array(chunkp).astype(np.float32) # ndarray型に変換
stream.write(chunk.tostring())
sh_status.value = 2
# endwhile
注意するのは、「stream = p.open(format=pyaudio.paFloat32,」のところで、よくあるpyaudioの例ではpyaudioから入力したデータをそのまま発音する設定ですが、そんなときにはformat=pyaudio.paInt32などになっていますが、今回使うデータはnumpyの三角関数を使うのでpaFloat32となります。
5.マルチプロセス
def start_multi():
sh_status.value = 2
futures = []
with ProcessPoolExecutor(max_workers=2) as executor:
# 発音プロセスを起動
results1 = executor.submit(mp_play_chord,sh_dict,sh_status,sh_chunk)
# 譜面プロセスの起動
results2 = executor.submit(hanko,11)
# shutdown待ち
print ('2 process execute was done.')
while sh_shutdown.value == 0:
usleep(1000000)
# print ('sh_shutdown=',sh_shutdown.value)
print('shutdown...')
results1.cancel()
results2.cancel()
for process in executor._processes.values():
process.kill()
print ('kill',process)
submitで関数をふたつ起動します。
マルチプロセスで起動すると、sys.exit()などでは終了できなくなるので、起動したあとにシャットダウン待ちに入って、共有メモリ経由で終了フラグ受け取って、2つのプロセスをキャンセルします。
6.共有メモリ
if name=="main":
manager = Manager()
sh_chunk = manager.list() # shared list for chunk
sh_dict = manager.dict() # dummy
sh_status = manager.Value('i',0) # shared integer value
sh_shutdown = manager.Value('i',0) # shared interger value
sh_currentbar = manager.Value('i',0)
main()
マルチプロセスで起動したプロセス間の変数やり取りは、 from multiprocessing import Manager
を使います。
list型は普通に使えますが、Value型は、
if sh_status.value != 1:
などと使います。
三角関数の出力は、numpyのarray型なので、一度list型に変換してから共有メモリに格納し、発音側でもとに戻してから使います。
共有メモリのarray型もありますが、固定長になるので使いにくいです。
7.オルガン風のドローバー機能
def hamond(self,frequency,length , rate):
# 3rd:1.25 5th:1.5
self.draw_list = [
[ 1.0 ],
[0.5 , 1.0 , 3.0 ],
[0.5 , 0.75 , 1.0 , 2.0 ,3.0 ],
[0.5 , 0.75 , 1.0 , 2.0 ]
]
self.drawbar = self.draw_list[self.tone]
src = []
for fl in self.drawbar:
src.append( self.sine(frequency * fl , length,rate))
res = np.array([0] * len(src[0]))
for s in src:
res = res + s
res *= 0.5
return res
ドローバー左側4本の周波数は、オクターブ下、下の5度、1度、オクターブ上になってます。
各周波数のsin波を生成して、加算していくだけです。
これをコードの構成音毎にやると、コードの音が出来上がります。
8.BPMの設定
def sine(self,frequency, length, rate):
length = int(length * rate * self.bpm)
factor = float(frequency) * (math.pi * 2) / rate
wave = np.sin(np.arange(length) * factor)
return wave
クラス hankoの画面でBPMを設定すると、下の計算式で係数を得ます。
aa.bpm = 1.0 /(float(self.en1.get()) / 240.0)
サンプリングレートが44.1kHzなので、データを44100個並べると、1秒間発音します。
現状はとりあえず4拍子限定なので、240BPMを基準として,rate=44100 , length=1.0で240bpmになるようにしています。