Python: GUI freezing problem of thread using tkinter and sounddevice - python

I have a code for handle audio data from a sound device.
My code construct the GUI through tkinter and handles audio data through the sounddevice when the button is pressed.
I succeeded in handle audio data in real-time process using thread class.
When I press the Start button, the input sound from the microphone is perfectly output to the speaker.
However, there is a problem with the stop button.
when I press the stop button, my code try to kill the thread, but there is a GUI freezing occurs. And the thread doesn't die.
I made several attempts based on a lot of information in stack overflow, but all failed.
Please review my code and give me some advice.
This is my Code:
import sounddevice as sd
import numpy as np
import tkinter as tk
from tkinter import ttk
from threading import Thread
class StreamThread(Thread):
def __init__(self):
super().__init__()
self.input_device_index = 0
self.output_device_index = 4
self.BLOCK_SHIFT = 128
self.SAMPLING_RATE = 16000
self.BLOCK_LEN = 512
self.SOUND_DEVICE_LATENCY = 0.2
def run(self):
with sd.Stream(device=(self.input_device_index, self.output_device_index),
samplerate=self.SAMPLING_RATE, blocksize=self.BLOCK_SHIFT,
dtype=np.float32, latency=self.SOUND_DEVICE_LATENCY,
channels=1, callback=self.callback):
input() # Input start
def callback(indata, outdata, frames, time, status):
outdata[:] = indata
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Please Help Me")
self.geometry("400x300")
self.resizable(0, 0)
start_button = tk.Button(self, overrelief="solid", width=15,
command=lambda: start_button_clicked(),
text="Start", repeatdelay=1000, repeatinterval=100)
start_button.grid(column=0, row=5)
stop_button = tk.Button(self, overrelief="solid", width=15,
command=lambda: stop_button_clicked(),
text="Stop", repeatdelay=1000, repeatinterval=100)
stop_button.grid(column=0, row=6)
def start_button_clicked():
stream_thead.start()
def stop_button_clicked():
# this is problem point
if stream_thead.isAlive():
sd.CallbackStop()
sd.CallbackAbort()
stream_thead.join()
if __name__ == "__main__":
stream_thead = StreamThread()
stream_thead.daemon = True # set Daemon thread
app = App()
app.mainloop()

There are issues in your code:
use console input() in a GUI application. What I understand you use input() to put the thread task in a waiting state. Suggest to use threading.Event.wait() instead.
sd.CallbackStop() and sd.CallbackAbort() cannot break input(). Use threading.Event.set() to break threading.Event.wait().
missing self argument in def callback(indata, ...). It should be def callback(self, indata, ...).
Below is modified code to fix the above issues:
...
from threading import Thread, Event
...
class StreamThread(Thread):
...
def run(self):
self.event = Event()
with sd.Stream(device=(self.input_device_index, self.output_device_index),
samplerate=self.SAMPLING_RATE, blocksize=self.BLOCK_SHIFT,
dtype=np.float32, latency=self.SOUND_DEVICE_LATENCY,
channels=1, callback=self.callback) as self.stream:
#input() # Input start
self.event.wait()
def terminate(self):
self.stream.abort() # abort the stream processing
self.event.set() # break self.event.wait()
def callback(self, indata, outdata, frames, time, status):
outdata[:] = indata
...
def stop_button_clicked():
if stream_thread.is_alive():
stream_thread.terminate()
stream_thread.join()
...

Related

How to open tk window in tkinter in another thread (tkinter isn't thread safe)

I want to write a program using Tkinter GUI, which performs a certain task in a thread. When an error occurs during the execution of the task, the program should pop up an error-window, for example using tkMessageBox. Of course the program crashes when it should pop up the messagebox from the new thread, since Tkinter isn't thread safe, but I hope that there is a solution to this problem.
Here is an easy example of a not working code (edited):
from tkinter import *
import tkMessageBox
from threading import *
import time
class App:
def __init__(self, master):
frame = Frame(master)
frame.pack()
self.button = Button(frame, text = "Task", command = self.thread_task)
self.button.pack(side=LEFT)
def thread_task(self):
thread = Thread(target = self.task)
thread.start()
def task(self):
#perform task...
time.sleep(1) #Just as a filler in the code
#command to open an error popup, e.g. tkMessageBox.showerror("Error", "Problem occured")
root = Tk()
app = App(root)
root.mainloop()
You can use try-except inside a thread.
Here is an example based on your code (edited to work with Python 2):
from Tkinter import *
import tkMessageBox
from threading import *
class App:
def __init__(self, master):
frame = Frame(master)
frame.pack()
self.button = Button(frame, text="Task", command=self.thread_task)
self.button.pack(side=LEFT)
def thread_task(self):
thread = Thread(target=self.task)
thread.start()
def task(self):
try:
time.sleep(1)
self.button["text"] = "Started"
self.button["state"] = "disabled"
raise ValueError # or anything else
except:
tkMessageBox.showerror("Error", "Problem occured")
self.button["text"] = "Task"
self.button["state"] = "normal"
if __name__ == "__main__":
root = Tk()
app = App(root)
root.mainloop()

Using Tkinter button to stop an iterating function

I have created a GUI with a "stop" button. When the GUI is ran, another module is called that contains a while loop for a background function. The stop button would be used to pass a variable to the loop to stop it. However, when the module containing the loop is called the GUI freezes. I have considered using the library "threading" but cannot find any tkinter specific content. Any advice or small example of how you would create the code would help a lot.
Here is a basic GUI with 2 buttons that can start and stop a thread which increments a variable count.
I let you try it :
import tkinter as tk
import threading
import time
class GUI(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.title("test")
self.button_start = tk.Button(self, text="Start", command=self.start_thread)
self.button_start.pack()
self.button_stop = tk.Button(self, text="Stop", command=self.stop_thread)
self.button_stop.pack()
self.count = 0
self.continue_thread = True
def start_thread(self):
self.count = 0
self.continue_thread = True
self.t1 = threading.Thread(target = self.counter)
self.t1.daemon = True # With this parameter, the thread functions stops when you stop the main program
self.t1.start()
def stop_thread(self):
self.continue_thread = False
self.t1.join()
def counter (self):
while self.continue_thread:
print("i =", self.count)
self.count += 1
time.sleep(1)
if __name__ == "__main__":
app = GUI()
app.mainloop()
This is just a sample program to illustrate how to kill a running thread.
import threading
import time
def run():
while True:
print('thread running')
global stop_threads
if stop_threads:
break
if __name__=="__main__":
stop_threads = False
t1 = threading.Thread(target = run) ##background loop
t1.start()
time.sleep(1)
#while clicking on the button in GUI kill the thread like this
stop_threads = True
t1.join()
print('thread killed')

python script runs after x seconds but when tkinter code inserted it runs only once

What is the best way to repeatedly execute a function every x seconds in Python?
i have tried the solutions posted on above links but none helped me to achieve the desired result.
the code above prints "Doing stuff..." on console many times as per seconds mentioned i.e. 5 but when i add the line of window() which is a tkinter code for displaying a message the code runs just once and not anytime again .
please help . i want to run the tkinter code again and again on specific time as per system clock but now i am just trying to execute it after x amounts of seconds .
any help would really mean a lot to me.Thanks
search for tkinter .after method.
This will alow you to run a command every x seconds.
The problem your tkinter code runs only once, is since its set up first and then goes into a loop, (root.mainloop()) , hence never returning to your code to display anything again.
Example : tkinter: how to use after method
I think you need thread and queue...let me show a little demo.
I've set time.sleep(1) per seconds in the thead class.
In this matter you get two advantages, first repet your funcion any times
you desire and second your program never freeze it self.
import tkinter as tk
import threading
import queue
import datetime
import time
class MyThread(threading.Thread):
def __init__(self, queue,):
threading.Thread.__init__(self)
self.queue = queue
self.check = True
def stop(self):
self.check = False
def run(self):
while self.check:
x = "Doing stuff.. "
y = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
msg = x+y
time.sleep(1)
self.queue.put(msg)
class App(tk.Frame):
def __init__(self,):
super().__init__()
self.master.title("Hello World")
self.master.protocol("WM_DELETE_WINDOW",self.on_close)
self.queue = queue.Queue()
self.my_thread = None
self.init_ui()
def init_ui(self):
self.f = tk.Frame()
w = tk.Frame()
tk.Button(w, text="Start", command=self.launch_thread).pack()
tk.Button(w, text="Stop", command=self.stop_thread).pack()
tk.Button(w, text="Close", command=self.on_close).pack()
w.pack(side=tk.RIGHT, fill=tk.BOTH, expand=0)
self.f.pack(side=tk.LEFT, fill=tk.BOTH, expand=0)
def launch_thread(self):
if (threading.active_count()!=0):
self.my_thread = MyThread(self.queue)
self.my_thread.start()
self.periodiccall()
def stop_thread(self):
if(threading.active_count()!=1):
self.my_thread.stop()
def periodiccall(self):
self.checkqueue()
if self.my_thread.is_alive():
self.after(1, self.periodiccall)
else:
pass
def checkqueue(self):
while self.queue.qsize():
try:
ret = self.queue.get(0)
msg = "%s"%(ret)
print(msg)
except queue.Empty:
pass
def on_close(self):
if(threading.active_count()!=1):
self.my_thread.stop()
self.master.destroy()
if __name__ == '__main__':
app = App()
app.mainloop()

Calling Tkinter class from another class (Python on Raspberry Pi)

I am stumped as to why the Display class isn't running in the following code. I call it using Display(root) in main(). The main class runs just fine. I don't get any errors in terminal.
#!/usr/bin/python
import alsaaudio as aa
import audioop
import Tkinter as tk
import tkFont
class Display():
def __init__(self, parent):
self.parent = parent
self._geom = '200x200+0+0'
parent.geometry("{0}x{1}+0+0".format(parent.winfo_screenwidth(), parent.winfo_screenheight()))
parent.overrideredirect(1)
parent.title('Listen')
parent.configure(background='#000000')
parent.displayFont = tkFont.Font(family="Unit-Bold", size=150)
def printMessage(self, parent, messageString):
self.message = tk.Message(self.parent, text=messageString, bg="#000000", font=parent.displayFont, fg="#777777", justify="c")
self.message.place(relx=.5, rely=.5, anchor="c")
def main():
root = tk.Tk()
window = Display(root)
# Set up audio
data_in = aa.PCM(aa.PCM_CAPTURE, aa.PCM_NONBLOCK, 'hw:1')
data_in.setchannels(2)
data_in.setrate(44100)
data_in.setformat(aa.PCM_FORMAT_S16_LE)
data_in.setperiodsize(256)
while True:
# Read data from device
l,data = data_in.read()
if l:
# catch frame error
try:
max_vol=audioop.rms(data,2)
scaled_vol = max_vol//4680
print scaled_vol
window.printMessage(root, scaled_vol)
except audioop.error, e:
if e.message !="not a whole number of frames":
raise e
root.mainloop()
if __name__ == '__main__':
main()
Also, both classes run perfectly separately. I'm just having problems using them together.
I think it's because there is an infinite loop (while True) that does not allow the execution to reach to Tk event loop root.mainloop().
Using threads might help to solve this issue. You can run the data input loop in a separate thread, and run Tk event loop in the main thread as it is.
Update: I ran the code (Python 2.7.6 on Linux 3.13.0-24-generic #47-Ubuntu SMP, pyalsaaudio==0.8.2) and it was showing the data. However it could be possible that using threads on different platforms behave differently. I'm having doubts about threads in this case now, because in the comments you mentioned that even the print statement is not being called. Another approach is not to use threads and the while True loop, but to use the mainloop from Tkinter itself.
Also please not that Display.printMessage is creating a new message widget on each call. This will end up with many message widgets showing (and occupying memory). To avoid this, I'd suggest using Tkinter variables, as used on the second code example using Tkinter mainloop.
Solution 1 Using threads (minimum change to code in the question):
#!/usr/bin/python
import alsaaudio as aa
import audioop
import Tkinter as tk
import tkFont
from threading import Thread, Event
class Display():
def __init__(self, parent):
self.parent = parent
self._geom = '200x200+0+0'
parent.geometry("{0}x{1}+0+0".format(parent.winfo_screenwidth(), parent.winfo_screenheight()))
parent.overrideredirect(1)
parent.title('Listen')
parent.configure(background='#000000')
parent.displayFont = tkFont.Font(family="Unit-Bold", size=150)
def printMessage(self, messageString):
self.message = tk.Message(self.parent, text=messageString, bg="#000000", font=self.parent.displayFont, fg="#777777", justify="c")
self.message.place(relx=.5, rely=.5, anchor="c")
def setup_audio(window, stop_event):
data_in = aa.PCM(aa.PCM_CAPTURE, aa.PCM_NONBLOCK, 'hw:1')
data_in.setchannels(2)
data_in.setrate(44100)
data_in.setformat(aa.PCM_FORMAT_S16_LE)
data_in.setperiodsize(256)
while not stop_event.is_set():
# Read data from device
l,data = data_in.read()
if l:
# catch frame error
try:
max_vol=audioop.rms(data,2)
scaled_vol = max_vol//4680
print scaled_vol
window.printMessage(scaled_vol)
except audioop.error, e:
if e.message !="not a whole number of frames":
raise e
def main():
root = tk.Tk()
window = Display(root)
stop_event = Event()
audio_thread = Thread(target=setup_audio, args=[window, stop_event])
audio_thread.start()
try:
root.mainloop()
finally:
stop_event.set()
audio_thread.join()
if __name__ == '__main__':
main()
Changes:
Moved reading sound loop into a separate function that accepts a threading Event to signal stop the loop
Removed the parent arg from Display.printMesssage() definition and use self.parent instead. So in the setup_autdio function when calling the printMessage(), there is no need to pass in root as an argument anymore.
Solution 2 Using Tkinter mainloop, and run all in the main thread (note that I also changed the Display.printMessage() class, mainly for performance improvements):
#!/usr/bin/python
import alsaaudio as aa
import audioop
import Tkinter as tk
import tkFont
class Display():
def __init__(self, parent):
self.parent = parent
self._geom = '200x200+0+0'
parent.geometry("{0}x{1}+0+0".format(parent.winfo_screenwidth(), parent.winfo_screenheight()))
parent.overrideredirect(1)
parent.title('Listen')
parent.configure(background='#000000')
parent.displayFont = tkFont.Font(family="Unit-Bold", size=150)
# using a Tkinter variable to display different messages, on the same widget
self.messageVar = tk.StringVar()
self.message = tk.Message(self.parent, textvar=self.messageVar, bg="#000000", font=self.parent.displayFont, fg="#777777", justify="c")
self.message.place(relx=.5, rely=.5, anchor="c")
def printMessage(self, messageString):
self.messageVar.set(messageString)
def update_ui_from_audio(data_in, window):
l, data = data_in.read()
if l:
# catch frame error
try:
max_vol=audioop.rms(data,2)
scaled_vol = max_vol//4680
print scaled_vol
window.printMessage(scaled_vol)
except audioop.error, e:
if e.message !="not a whole number of frames":
raise e
def main():
root = tk.Tk()
window = Display(root)
# Set up audio
data_in = aa.PCM(aa.PCM_CAPTURE, aa.PCM_NONBLOCK, 'hw:1')
data_in.setchannels(2)
data_in.setrate(44100)
data_in.setformat(aa.PCM_FORMAT_S16_LE)
data_in.setperiodsize(256)
def audio_read_loop():
update_ui_from_audio(data_in, window)
root.after(1, audio_read_loop)
root.after(1, audio_read_loop)
root.mainloop()
if __name__ == '__main__':
main()
Changes:
Move the code that reads data from PCM device, and updates the UI into a separate function named update_ui_from_audio
Display.printMessage uses a StringVar to update the message, so only 1 message widget is used.
Replace the while True loop, with another function that calls the update_ui_from_audio function, and asks Tkinter to call it again in a milisecond (hence, acting like a loop).

How to connect a progress bar to a function?

I'm trying to connect a progress bar to a function for my project.
This is what I have so far but im pretty sure it does nothing:
def main():
pgBar.start()
function1()
function2()
function3()
function4()
pgBar.stop()
Here is the code where I make my progress bar if that helps at all:
pgBar = ttk.Progressbar(window, orient = HORIZONTAL, length=300, mode = "determinate")
pgBar.place(x=45, y=130)
I have been doing some research and understand that the tkinter window freezes when running a function or something like that. Is there a way I could "unfreeze" the window at the end of each function that is called inside the main one?
Since tkinter is single threaded, you need another thread to execute your main function without freezing the GUI. One common approach is that the working thread puts the messages into a synchronized object (like a Queue), and the GUI part consumes this messages, updating the progress bar.
The following code is based on a full detailed example on ActiveState:
import tkinter as tk
from tkinter import ttk
import threading
import queue
import time
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.queue = queue.Queue()
self.listbox = tk.Listbox(self, width=20, height=5)
self.progressbar = ttk.Progressbar(self, orient='horizontal',
length=300, mode='determinate')
self.button = tk.Button(self, text="Start", command=self.spawnthread)
self.listbox.pack(padx=10, pady=10)
self.progressbar.pack(padx=10, pady=10)
self.button.pack(padx=10, pady=10)
def spawnthread(self):
self.button.config(state="disabled")
self.thread = ThreadedClient(self.queue)
self.thread.start()
self.periodiccall()
def periodiccall(self):
self.checkqueue()
if self.thread.is_alive():
self.after(100, self.periodiccall)
else:
self.button.config(state="active")
def checkqueue(self):
while self.queue.qsize():
try:
msg = self.queue.get(0)
self.listbox.insert('end', msg)
self.progressbar.step(25)
except Queue.Empty:
pass
class ThreadedClient(threading.Thread):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
for x in range(1, 5):
time.sleep(2)
msg = "Function %s finished..." % x
self.queue.put(msg)
if __name__ == "__main__":
app = App()
app.mainloop()
Since the original example on ActiveState is a bit messy IMO (the ThreadedClient is quite coupled with the GuiPart, and things like controlling the moment to spawn the thread from the GUI are not as straightforward as they could be), I have refactored it and also added a Button to start the new thread.
To understand the 'freezing' you need to understand mainloop(). Calling this method starts the tkinter event loop. The main thread is responsible for this loop. Therefore, when your work intensive function runs in the main thread, it is also interfering with the mainloop. To prevent this you can use a secondary Thread to run your function. It's recommended that secondary threads are not given access to tkinter objects. Allen B.Taylor, author of mtTkinter, states:
The problems stem from the fact that the _tkinter module attempts to
gain control of the main thread via a polling technique when
processing calls from other threads.
If it succeeds, all is well. If it fails (i.e., after a timeout), the
application receives an exception with the message: "RuntimeError:
main thread is not in main loop".
You can have the secondary thread put information into a Queue. Then have a function that checks the Queue every x milliseconds, within the mainloop, via the after() method.
First, decide what you want the value of the Progressbar's maximum option to be.
This is the Progressbar's maximum indicator value (how many units are required to fill the Progressbar).
For example, you could set maximum=4 and then put the appropriate indicator value into the Queue after each of your four functions. The main thread can then retrieve these values (from the Queue) to set the progress via a tkinter.IntVar().
(Note that if you use progbar.step(), the Progressbar resets to 0 (empty) at the end, instead of reaching 4 (completely filled).)
Here's a quick look at how you can use a tkinter.IntVar() with a Progressbar:
int_var = tkinter.IntVar()
pb_instance = ttk.Progressbar(root, maximum=4)
pb_instance['variable'] = int_var
pb_instance.pack()
# completely fill the Progressbar
int_var.set(4)
# get the progress value
x = int_var.get()
Here's an example based on your own (renamed the "main" function "arbitrary"):
import time
import threading
try: import tkinter
except ImportError:
import Tkinter as tkinter
import ttk
import Queue as queue
else:
from tkinter import ttk
import queue
class GUI_Core(object):
def __init__(self):
self.root = tkinter.Tk()
self.int_var = tkinter.IntVar()
progbar = ttk.Progressbar(self.root, maximum=4)
# associate self.int_var with the progress value
progbar['variable'] = self.int_var
progbar.pack()
self.label = ttk.Label(self.root, text='0/4')
self.label.pack()
self.b_start = ttk.Button(self.root, text='Start')
self.b_start['command'] = self.start_thread
self.b_start.pack()
def start_thread(self):
self.b_start['state'] = 'disable'
self.int_var.set(0) # empty the Progressbar
self.label['text'] = '0/4'
# create then start a secondary thread to run arbitrary()
self.secondary_thread = threading.Thread(target=arbitrary)
self.secondary_thread.start()
# check the Queue in 50ms
self.root.after(50, self.check_que)
def check_que(self):
while True:
try: x = que.get_nowait()
except queue.Empty:
self.root.after(25, self.check_que)
break
else: # continue from the try suite
self.label['text'] = '{}/4'.format(x)
self.int_var.set(x)
if x == 4:
self.b_start['state'] = 'normal'
break
def func_a():
time.sleep(1) # simulate some work
def func_b():
time.sleep(0.3)
def func_c():
time.sleep(0.9)
def func_d():
time.sleep(0.6)
def arbitrary():
func_a()
que.put(1)
func_b()
que.put(2)
func_c()
que.put(3)
func_d()
que.put(4)
que = queue.Queue()
gui = GUI_Core() # see GUI_Core's __init__ method
gui.root.mainloop()
If all you want is something that indicates to the user that there is activity
you can set the Progressbar's mode option to 'indeterminate'.
The indicator bounces back and forth in this mode (the speed relates to the maximum option).
Then you can call the Progressbar's start() method directly before starting the secondary thread;
and then call stop() after secondary_thread.is_alive() returns False.
Here's an example:
import time
import threading
try: import tkinter
except ImportError:
import Tkinter as tkinter
import ttk
else: from tkinter import ttk
class GUI_Core(object):
def __init__(self):
self.root = tkinter.Tk()
self.progbar = ttk.Progressbar(self.root)
self.progbar.config(maximum=4, mode='indeterminate')
self.progbar.pack()
self.b_start = ttk.Button(self.root, text='Start')
self.b_start['command'] = self.start_thread
self.b_start.pack()
def start_thread(self):
self.b_start['state'] = 'disable'
self.progbar.start()
self.secondary_thread = threading.Thread(target=arbitrary)
self.secondary_thread.start()
self.root.after(50, self.check_thread)
def check_thread(self):
if self.secondary_thread.is_alive():
self.root.after(50, self.check_thread)
else:
self.progbar.stop()
self.b_start['state'] = 'normal'
def func_a():
time.sleep(1) # simulate some work
def func_b():
time.sleep(0.3)
def func_c():
time.sleep(0.9)
def func_d():
time.sleep(0.6)
def arbitrary():
func_a()
func_b()
func_c()
func_d()
gui = GUI_Core()
gui.root.mainloop()
→ Progressbar reference
You must be using:
self.pgBar.step(x)
where 'x' is the amount to be increased in progressbar.
for this to get updated in your UI you have to put
self.window.update_idletasks() after every self.pgBar.step(x) statement

Categories

Resources