前提・実現したいこと
Pythonを用いて、PCキーボードを用いた演奏アプリを作っています。
・PCのキープレスと周波数を対応させ、キーを押すたびに異なる音が出力されるようにしています。
・キーイベントの検出とsin波のグラフ作成にtkinterを用いて、音の出力にpyaudioを用いています。
上記に加えて、画面上にテンポをカウントしてくれるメトロノームのような機能をつけようと思っています。
試したこと
メインループでグラフのプロットと音の出力を行うため、メトロノーム機能はスレッドで実施しようと考えました。
メインスレッドとサブスレッド間のやり取りにはstart_flagを用意し、メインウィンドウ上のstartとstopボタンが押されるとself.start_flagが切り替わり、その通知を受けてスレッド上でtimerメソッドが時間をカウントするようにしました。
Python
1 2#=== アプリケーションの定義 3class Synth_application(tk.Frame): 4 # 初期設定 5 def __init__(self, master=None): 6… 7 # スレッドの生成と開始 8 self.thread1 = threading.Thread(target=self.timer) 9 self.thread1.start() 10 11 # 初期値 12 def start_up(self, gain=0.25, rate=44100, chunk_size=1024): 13… 14 # timerスレッドの初期値 15 self.start_flag = False 16 self.count = 0 17 18 # ウィジットの定義 19 def create_widgets(self): 20… 21 # メトロノームウィジェット 22… 23 self.start_button = tk.Button(self.metronom_innerBox, text="start") 24 self.start_button.grid(column=0, row=0) 25 self.stop_button = tk.Button(self.metronom_innerBox, text="stop") 26 self.stop_button.grid(column=0, row=1) 27 self.metronom_label = tk.Label(self.metronom_innerBox, font=("", 20)) 28 self.metronom_label.grid(column=1, row=0, padx=20, rowspan=2) 29 30 self.start_button.bind("<ButtonPress>", self.start_button_click) 31 self.stop_button.bind("<ButtonPress>", self.stop_button_click) 32 33… 34 35 # スタートボタンが押された時の処理 36 def start_button_click(self, event): 37 self.count = 0 38 self.start_flag = True 39 40 # ストップボタンが押された時の処理 41 def stop_button_click(self, event): 42 self.start_flag = False 43 44 # タイマー 45 def timer(self): 46 while not self.quitting_flag: 47 if self.start_flag: 48 self.metronom_label.config(text=self.count) 49 self.count += 1 50 if self.count > 4: 51 self.count = 1 52 53 import time 54 time.sleep(60/self.bpm) 55… 56 57root = tk.Tk() 58app = Synth_application(master=root) 59app.mainloop()
発生している問題・エラーメッセージ
上記コードでアプリ自体は動くのですが、下記のエラーが発生します。
また、その影響かメインスレッドで音を鳴らすとタイマーの数字の変わる時間が遅延します。
スレッドを加えていないときは下記エラーは出ませんでした。
RuntimeError: main thread is not in main loop
作成したコード
Python
1# ライブラリのインポート 2import tkinter as tk 3import numpy as np 4import pyaudio 5import matplotlib.pyplot as plt 6from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg 7from matplotlib.figure import Figure 8import threading 9 10 11#=== アプリケーションの定義 12class Synth_application(tk.Frame): 13 # 初期設定 14 def __init__(self, master=None): 15 # メインウィンドウの設定 16 super().__init__(master) 17 self.master = master 18 self.master.title('Synth_Application') 19 self.master.resizable(width=False,height=False) 20 self.master.geometry('700x570') 21 22 # sin波の初期値設定 23 self.start_up() 24 25 # ウィジットをつくる 26 self.create_widgets() 27 28 # pyaudioの開始 29 self.p = pyaudio.PyAudio() 30 self.stream = self.p.open(format=pyaudio.paFloat32, 31 channels=1, 32 rate=self.rate, 33 frames_per_buffer=self.chunk_size, 34 output=True) 35 36 self.master.protocol("WM_DELETE_WINDOW", self.quit_app) 37 38 # スレッドの生成と開始 39 self.thread1 = threading.Thread(target=self.timer) 40 self.thread1.start() 41 42 # 初期値 43 def start_up(self, gain=0.25, rate=44100, chunk_size=1024): 44 global freq 45 46 freq = 261.26 47 self.duration = 0.5 48 self.bpm = 100 49 self.gain = gain 50 self.rate = rate 51 self.chunk_size = chunk_size 52 53 self.start_pos = 0 54 self.end_pos = self.start_pos + self.duration * self.rate 55 56 self.t = np.arange(self.start_pos, self.end_pos) / self.rate 57 self.sin_t = self.gain * np.sin(2 * np.pi * 0 * self.t) 58 59 # timerスレッドの初期値 60 self.start_flag = False 61 self.quitting_flag = False 62 self.count = 0 63 64 65 # ウィジットの定義 66 def create_widgets(self): 67 self.canvas_frame = tk.Frame(self.master, width=700, height=250, bg="#000080") 68 self.canvas_frame.grid(column=0, row=0) 69 self.canvas_frame.grid_propagate(0) 70 self.canvas_frame.grid_anchor(tk.CENTER) 71 72 self.option_frame = tk.Frame(self.master, width=700, height=320, bd = 5, relief = tk.GROOVE) # New 73 self.option_frame.grid(column=0, row=1) 74 self.option_frame.grid_propagate(0) 75 self.option_frame.grid_anchor(tk.CENTER) 76 77 # グラフウィジット 78 self.chunk_labelFrame = tk.LabelFrame(self.canvas_frame, text="WaveForm", fg='white', bg='#444', relief=tk.FLAT) 79 self.chunk_labelFrame.grid(column=0, row=0) 80 self.chunk_innerBox = tk.Frame(self.chunk_labelFrame) 81 self.chunk_innerBox.grid(column=0, row=0) 82 83 fig = Figure(figsize=(6.5,2), dpi=100, tight_layout=True, facecolor='#F0F0F0') 84 axes_wave = fig.add_subplot(1, 1, 1) 85 axes_wave.set_ylim([-(self.gain + 0.05), (self.gain + 0.05)]) 86 self.line_wave, = axes_wave.plot(self.sin_t[:500]) 87 88 self.canvas = FigureCanvasTkAgg(fig, master=self.chunk_innerBox) 89 self.canvas.get_tk_widget().pack(padx=10, pady=10) 90 self.canvas.get_tk_widget().bind('<Any-KeyPress>', self.press_key) 91 92 # BPMスケールバーウィジェット 93 self.scale_labelFrame = tk.LabelFrame(self.option_frame, text="BPM", fg='white', bg='#444', relief=tk.FLAT) # New 94 self.scale_labelFrame.grid(column=0, row=1, padx=20, sticky=tk.N) 95 self.scale_innerBox = tk.Frame(self.scale_labelFrame, width=250, height=50) 96 self.scale_innerBox.grid(column=0, row=0) 97 self.scale_innerBox.grid_propagate(0) 98 self.scale_innerBox.grid_anchor(tk.CENTER) 99 100 self.bpm_scale = tk.DoubleVar() 101 self.bpm_scale.set(100) 102 self.bpm_scalebar = tk.Scale(self.scale_innerBox, 103 variable=self.bpm_scale, 104 from_=10, 105 to=200, 106 resolution=2, 107 orient=tk.HORIZONTAL, 108 length=150, 109 command=self.reset_bpm) 110 self.bpm_scalebar.grid(column=0,row=0, padx=100, pady=20) 111 112 # メトロノームウィジェット 113 self.metronom_labelFrame = tk.LabelFrame(self.option_frame, text="Metronom", fg='white', bg='#444', relief=tk.FLAT) 114 self.metronom_labelFrame.grid(column=0, row=2, padx=20) 115 self.metronom_innerBox = tk.Frame(self.metronom_labelFrame, width=250, height=70) 116 self.metronom_innerBox.grid(column=0, row=0) 117 self.metronom_innerBox.grid_propagate(0) 118 self.metronom_innerBox.grid_anchor(tk.CENTER) 119 120 self.start_button = tk.Button(self.metronom_innerBox, text="start") 121 self.start_button.grid(column=0, row=0) 122 self.stop_button = tk.Button(self.metronom_innerBox, text="stop") 123 self.stop_button.grid(column=0, row=1) 124 self.metronom_label = tk.Label(self.metronom_innerBox, font=("", 20)) 125 self.metronom_label.grid(column=1, row=0, padx=20, rowspan=2) 126 127 self.start_button.bind("<ButtonPress>", self.start_button_click) 128 self.stop_button.bind("<ButtonPress>", self.stop_button_click) 129 130 def reset_bpm(self, bpm): 131 self.bpm = self.bpm_scale.get() # New 132 self.DURATION = { 133 'L4': (60 / self.bpm * 4) / 4, # 四分音符 134 } 135 136 # sin波の作成 137 def create_sinwave(self, duration, freq): 138 self.duration = duration 139 self.freq = freq 140 141 self.start_pos = self.end_pos 142 self.end_pos = self.start_pos + duration * self.rate 143 self.t = np.arange(self.start_pos, self.end_pos) / self.rate 144 self.sin_t = self.gain * np.sin(2 * np.pi * freq * self.t) 145 146 def press_key(self, event): 147 global freq 148 self.key = event.keysym 149 if self.key == 'a': 150 freq = FREQ_SCALE['ド/C4'] 151 self.create_sinwave(self.duration, freq) 152 self.play() 153 elif self.key == 's': 154 freq = FREQ_SCALE['レ/D4'] 155 self.create_sinwave(self.duration, freq) 156 self.play() 157 158 self.reset_sinwave() 159 160 161 def play(self): 162 self.stream.write(self.sin_t.astype(np.float32).tobytes()) 163 164 def reset_sinwave(self): 165 self.line_wave.set_ydata(self.sin_t[:500]) 166 self.canvas.draw() 167 168 # 終了ボタンが押された時の処理 169 def quit_app(self): 170 self.stream.close() 171 self.master.destroy() 172 173 # スタートボタンが押された時の処理 174 def start_button_click(self, event): 175 self.count = 0 176 self.start_flag = True 177 178 # ストップボタンが押された時の処理 179 def stop_button_click(self, event): 180 self.start_flag = False 181 182 # タイマー 183 def timer(self): 184 while not self.quitting_flag: 185 if self.start_flag: 186 self.metronom_label.config(text=self.count) 187 self.count += 1 188 if self.count > 4: 189 self.count = 1 190 191 import time 192 time.sleep(60/self.bpm) 193 194# パラメータ 195FREQ_SCALE = { 196 'ド/C4': 261.626, 197 'レ/D4': 293.665 198} 199 200root = tk.Tk() 201app = Synth_application(master=root) 202app.mainloop()
回答1件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/11/09 22:58
2021/11/10 00:10 編集