質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

1回答

4578閲覧

Threadを用いた結果、RuntimeError: main thread is not in main loopが出る

lteru

総合スコア6

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

0クリップ

投稿2021/11/09 00:51

前提・実現したいこと

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): 67 # スレッドの生成と開始 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): 1314 # timerスレッドの初期値 15 self.start_flag = False 16 self.count = 0 17 18 # ウィジットの定義 19 def create_widgets(self): 2021 # メトロノームウィジェット 2223 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 3334 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) 5556 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()

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

前回の質問は解決しましたか?

同種の問題で、前回の回答と同様に、GUI関連はメインスレッドのみにして、
オーディオ部分を別スレッドにすれば、ラグの問題も解消するはずです。

問題点:
GUIが破棄された後も、サブスレッドで破棄済みのGUIへのアクセスが発生する。

メインスレッドでGUIを動かしている状態で、サブスレッドからもGUIを直接操作するのは
スレッドセーフな操作ではありません。

解決策:

今回の問題のみであれば、タイマーにはスレッドではなく
tkinter の提供するタイマー機能 (after 関数) を使って、
同等のタイマー処理を実装することで、メインスレッド側で実行できるので、
スレッド関連のエラーは避けることができます。

音を鳴らすときのラグは別問題で、オーディオ関連の処理を別スレッドにする必要があります。


追記
ソースコードを読んでみて、メトロノーム用途でのタイマー利用だったので、少し追記

GUIの提供するタイマーの精度では不満がある場合であれば、
より高精度のタイマー目的としてスレッドを使うという選択肢はあります。
但し、その場合でもGUI の操作は、スレッド間で通知を行い メインスレッド側で行います。


追記2: エラーの回避策(もし終了時に表示されるエラーだった場合)
スレッドにdaemon=True 引数を指定すると、メインスレッド終了時にサブスレッドも終了するので
根本的な問題解決にはなりませんが、エラー自体は回避できるかもしれません。

投稿2021/11/09 07:42

編集2021/11/09 08:12
teamikl

総合スコア8664

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

lteru

2021/11/09 22:58

丁寧にありがとうございます。 オーディオ部分を別スレッドにするとカーネルが落ちてしまうため劣後していましたが、もう少し考えてみようと思います。
teamikl

2021/11/10 00:10 編集

>オーディオ部分を別スレッドにするとカーネルが落ちてしまう 前回「カーネルが落ちる」と提示されていたコードでは、 オーディオ部分は別になってませんが、別のコードですか? こちらでは再現出来ませんが、問題になりそうな点として プログラムの終了時にstream.close() した後に stream.write() が発生してないか確認して見て下さい。 ==== 問題解決のためには、問題の再現手順の提示も必要です。 例えば、プログラム終了時の問題だった場合、操作方法により挙動が変わっているはずです - ストップボタンを押してからアプリケーションを終了する → サブスレッドは正常終了 - ストップボタンを押さずにアプリケーションを終了する → サブスレッドは動き続ける  破棄済みのリソースをサブスレッドで使おうとしてエラー、というケースが考えられます。 スレッドを安全に終了するには、終了ボタンが押された時の処理 の先頭に self.quitting_flag = False # スレッド側のループを抜ける為のフラグ self.thread1.join() # スレッドの終了を待つ。時間が掛かるとGUIはフリーズするので注意。 サブスレッドを終了させた後に stream.close や master.destroy をすると 一時的な回避策ですが、とりあえず破棄済みのリソースに対するアクセスは避けられます。 根本的な対策としては、メインスレッドで生成・破棄するリソースを サブスレッドから使うと同種の問題が発生しやすいので、 サブスレッド内で扱うリソースはサブスレッド側で生成・破棄すると良いです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問