Pygame mixer causes tkinter to freeze - python

to-speech module to say a list and the audio module I am using is mixer form pygame. When i start playing the audio the tkinter program can not be interacted with and if I move the window the audio stops. I think some sort of threading may fix this but I am not sure how that works.
Problem
Tkinter window freezes.
Goal
To be able to interact with the program when audio is playing.
Code
import tkinter as tk
from gtts import gTTS
from io import BytesIO
import pygame
window = tk.Tk()
window.geometry("800x600")
window.resizable(0, 0)
def speak(text,language="en",accent="com"):
mp3_fp = BytesIO()
phrase = gTTS(text=text,lang=language,tld=accent)
phrase.write_to_fp(mp3_fp)
pygame.init()
pygame.mixer.init()
pygame.mixer.music.load(mp3_fp,"mp3")
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
pygame.time.delay(10)
pygame.event.poll()
def play():
data = list([boy,girl,pink,blue])
for i in data:
speak(i)
window.mainloop()
Code Explanation
The play()passes each value in the dictionary E.g. Boy, Girl to speak() separately where they are played one by one consecutively
In the speak() the audio from the text-to-speech module (gTTS) gets passed to pygame.mixer where it is played when the last word has been said.

First of all you need to post the minimum code necessary to demonstrate your problem. You haven't done that. Fortunately, this is a well-known "problem" and easy to answer without importing four libraries and building an application around what you've provided in order to answer it.
Secondly -- in the code that you have provided, data = list[boy,girl,pink,blue] isn't even proper syntax. It should be data = list(["boy", "girl", "pink", "blue"]). You have to post running code to get the best answers.
Lecture over.
The issue is that conventional unmodified Python runs in a single thread. If you want to know why that is then I invite you to research the GIL (Global Interpreter Lock) for more background.
There's only one thread, and when PyGame is doing something then the thread is busy and TkInter stops responding to input, and vice versa -- when TkInter is in the middle of something you'll find that PyGame stops responding.
You can demonstrate this phenomenon with this:
import tkinter as tk
import time
def delay():
time.sleep(10)
def main():
root = tk.Tk()
tk.Button(root, text="Test Me", command=delay).pack(expand=True, fill=tk.BOTH)
root.mainloop()
if __name__ == "__main__":
main()
When you run this you'll see that the button is depressed, the button stays depressed while the application goes to sleep, and the button doesn't go back to its unclicked status until after it wakes up.
The only way I know of to get around your particular problem is by running TkInter and/or PyGame on separate threads.
You are going to have to read up on Python's Threading() module. You might start here. I've browsed it and it seems to be pretty complete.
Just to demonstrate the difference:
import tkinter as tk
import time
import threading
def delay():
print("Delay started...")
time.sleep(10)
print("... and finished.")
def dispatchDelayToThread():
t = threading.Thread(target=delay)
t.start()
def main():
root = tk.Tk()
tk.Button(root, text="Test Me", command=dispatchDelayToThread).pack(expand=True, fill=tk.BOTH)
root.mainloop()
if __name__ == "__main__":
main()
I didn't even really change the code any! I added a function to dispatch the code I'd already written then changed the button to call the dispatcher instead of the code. Very easy to implement.
Run this and you'll see that the button returns to ready right away. If you run this from the command line you'll see that it prints a line when you enter the thread, and another line when the thread completes. And an even cooler thing is that if you click the button three times in a row you'll get three "starting" message, followed by three "finished" messages shortly afterwards.
To demonstrate this threading using your own code:
import pygame
import tkinter as tk
import io
import gtts
import threading
def speak(text,language="en",accent="com"):
mp3_fp = io.BytesIO()
phrase = gtts.gTTS(text=text,lang=language,tld=accent)
phrase.write_to_fp(mp3_fp)
pygame.init()
pygame.mixer.init()
pygame.mixer.music.load(mp3_fp,"mp3")
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
pygame.time.delay(10)
pygame.event.poll()
def dispatchPlay():
t = threading.Thread(target=play)
t.start()
def play():
data = list(["boy", "girl", "pink", "blue"])
for i in data:
speak(i)
def main():
root = tk.Tk()
root.geometry('300x200')
tk.Button(root, text="This is a clicky button", command=dispatchPlay).pack(expand=True, fill=tk.BOTH)
root.mainloop()
if __name__ == "__main__":
main()
Generally you would have your user interface on one thread, frame updates on another thread if it's a game, sound stuff on yet another thread, networking connection on a thread, etc., with all of them tied together through some kind of messaging system.
Note well, however, that in conventional unmodified Python there is only ever one thread running at any one time! You can have a hundred threads spawned, but only one of them will run at a time. Python sucks at multithreading computationally-intensive tasks, but shines in threading out I/O stuff that you usually just spend your time waiting on.

Related

How to properly close a tkinter window with multiple threads?

I'm writing a script that takes some time to complete, but want to give some feedback to the user showing that the program is actually running. I'm trying to use the progress bar from tkinter because it's simple, but when the window closes, the program continues running in the background.
import sys
from time import sleep
from threading import Thread
import tkinter as tk
import tkinter.ttk as ttk
def work(win):
sleep(10)
win.destroy()
root = tk.Tk()
p = ttk.Progressbar(root, mode="indeterminate")
p.pack()
t = Thread(target=lambda: work(root))
t.start() # start thread
p.start() # start progressbar
root.mainloop()
sys.exit()
Here's essentially what the code does. I've tried putting sys.exit() after the thread closes the window, but that yields the same result.
After some debugging it seems like the program keeps running the main loop of root even though I destroyed it, so how can I make sure the program terminates the script properly?

How does a thread close when using Tkinter

I know this question has been asked numerous times before, but they all use people's code and classes/various functions, etc, so its been a bit difficult to try and follow/understand what's going on. I wanted a more simpler setup to understand what's happening here and how it all works.
When you thread a function, once the function is complete, the thread is closed:
import threading
def fun():
x=0
while x<1000:
x+=1
threading.Thread(target=fun).start()
So I decided to take this idea one step further with Tkinter.
import tkinter as tk
from tkinter import *
from tkinter import messagebox as mb
import threading
def fun():
x=0
while x<10000900:
x+=1
if x == 50:
print(x)
def main():
while True:
fun()
if mb.askquestion('Replay', 'Would you like to play another round?') != 'yes':
root.destroy()
break
root = tk.Tk()
root.geometry('600x600')
threading.Thread(target=main).start()
root.mainloop()
The idea being, that when the main function was broken (based on the user response), the thread would also close automatically. Additionally, so would the Tkinter window. However, when the above script is run, the Tkinter window does close, but the terminal still indicates something, which I assume is the thread. What I don't understand is why in the first case where I use threading, the program ends properly, whereas the 2nd one doesn't.
When you execute root.destroy() you kill the main thread (mainloop) as well as the extra thread running your main funtion. That way the break statement never gets executed - the thread that would execute that statement is abruptly ended.
If you replace root.destroy() with root.after(10, root.destroy) the program acts as expected. This is because you're delaying the call to root.destroy() by some time (10 ms). This delay allows the break statement to be executed since the thread is still alive for 10 ms.

Tkinter - How to get Keypress anywhere on window?

I am trying to get keypresses in Python (2.7.10), bit I had no luck with getch(), as ord(getch()) was returning 255 constantly, so I am now using Tkinter. (I believe that Tkinter is also cross-platform so that should help as i plan to run this script on a Linux device).
I need to be able to get keypresses, even if they are not pressed while the Tkinter window is not active.
Here is my code:
from Tkinter import *
import time, threading
x = "Hi!"
def callback(event):
x = "key: " + event.char
print(x)
def doTk():
root = Tk()
root.bind_all("<Key>", callback)
root.withdraw()
root.mainloop()
thread1 = threading.Thread(target=doTk)
thread1.deamon = True
thread1.start()
I am not reveiving any errors, it is not not registering keypresses. I have also tried this without using threading, but it still does not work.
Please also note that I cannot use raw_input() as I need this to be able to run in the background and still get keypresses.
I am aware that this does not produce a frame, I do not want it to.
Thanks in advance for any help :)
PS: I have looked to other answers on StackOverflow and other sites, but they all either don't work or give solutions where keypresses are only registered when the tkinter frame is active.

label.configure works sometimes why?

Part of my code is as follows:
def get_songs():
label6.configure(text='Wait')
os.system('/home/norman/my-startups/grabsongs')
label6.configure(text='Done')
The label is not updated at the first .configure() but is at the second one.
Except if I cause a deliberate error immediately after the first one at which point it is updated and then the program terminates.
The system call takes about 2 minutes to complete so it isn't as if there isn't time to display the first one.
I am using Python 2.7.6
Does anyone know why please?
I'm going to guess you're using Tkinter. If so, as #albert just suggested, you'll want to call label.update_idletasks() or label.update() to tell Tkinter to refresh the display.
As a very crude example to reproduce your problem, let's make a program that will:
Wait 1 second
Do something (sleep for 2 seconds) and update the text to "wait"
Display "done" afterwards
For example:
import Tkinter as tk
import time
root = tk.Tk()
label = tk.Label(root, text='Not waiting yet')
label.pack()
def do_stuff():
label.configure(text='Wait')
time.sleep(2)
label.configure(text='Done')
label.after(1000, do_stuff)
tk.mainloop()
Notice that "Wait" will never be displayed.
To fix that, let's call update_idletasks() after initially setting the text:
import Tkinter as tk
import time
root = tk.Tk()
label = tk.Label(root, text='Not waiting yet')
label.pack()
def do_stuff():
label.configure(text='Wait')
label.update_idletasks()
time.sleep(2)
label.configure(text='Done')
label.after(1000, do_stuff)
tk.mainloop()
As far as why this happens, it actually is because Tkinter doesn't have time to update the label.
Calling configure doesn't automatically force a refresh of the display, it just queues one the next time things are idle. Because you immediately call something that will halt execution of the mainloop (calling an executable and forcing python to halt until it finishes), Tkinter never gets a chance to process the changes to the label.
Notice that while the gui displays "Wait" (while your process/sleep is running) it won't respond to resizing, etc. Python has halted execution until the other process finishes running.
To get around this, consider using subprocess.Popen (or something similar) instead of os.system. You'll then need to perodically poll the returned pipe to see if the subprocess has finished.
As an example (I'm also moving this into a class to keep the scoping from getting excessively confusing):
import Tkinter as tk
import subprocess
class Application(object):
def __init__(self, parent):
self.parent = parent
self.label = tk.Label(parent, text='Not waiting yet')
self.label.pack()
self.parent.after(1000, self.do_stuff)
def do_stuff(self):
self.label.configure(text='Wait')
self._pipe = subprocess.Popen(['/bin/sleep', '2'])
self.poll()
def poll(self):
if self._pipe.poll() is None:
self.label.after(100, self.poll)
else:
self.label.configure(text='Done')
root = tk.Tk()
app = Application(root)
tk.mainloop()
The key difference here is that we can resize/move/interact with the window while we're waiting for the external process to finish. Also note that we never needed to call update_idletasks/update, as Tkinter now does have idle time to update the display.

Python Tkinter Program is crashing with forget()

In my Program I want to use forget() on a button. Now if I try that, the program crashes. I know that it has something to do with threading but I couldn't find a soultion yet. Thanks in advance. Here is my examplecode:
import Tkinter as tk
import thread
window = tk.Tk()
def ok():
pass
def voice():
button1.forget()
print("If you see this, it works!")
thread.start_new_thread(voice,())
button1=tk.Button(command=ok, text="PRESS")
button1.pack()
window.mainloop()
You can't access tkinter objects from any thread but the thread that creates the object. In other words, you can't call button1.forget() from a thread and expect it to work reliably.
The generally accepted solution is to have your thread(s) write information to a thread-safe queue, and have your GUI thread poll that queue perioducally, pull an item off, and do whatever that item is requesting.
So I solved this problem simply by using the module mtTkinter , which you can find here :
http://tkinter.unpythonic.net/wiki/mtTkinter To use it you only have to write import mtTkinter as Tkinter at the beginning. After that you can use your Tkinter normally. This module changes nothing in Tkinter, it only makes it thread-friendly.
Tkinter is notorious for the fact that its lack of thread safety means that code you have written can sometimes work, and sometimes cause the entire program to hang with no errors produced, which is a pain.
Luckily, Tkinter does have its own measure for dealing with the problem, so to start a a thread with voice in it, just call voice. However, at theend of voice make sure you have made use of the window.after method to call it again later down the line. For example:
import Tkinter as tk
import thread
window = tk.Tk()
def ok():
pass
def voice():
button1.forget()
print("If you see this, it works!")
window.after(10, voice())
voice()
button1=tk.Button(command=ok, text="PRESS")
button1.pack()
window.mainloop(

Categories

Resources