I have been working on a music player app that uses VLC to play songs directly from the internet and has features like seeking and a progress bar. But as the normal tkinter progress bar looks kinda old so I used customtkinter.CTkSlider widget but using this causes buffering. The song plays smoothly on using Tk.Scale.
Here, is the code:
# import external libraries
import vlc
import tkinter as Tk
from tkinter import ttk
import pyautogui
import customtkinter
import pafy
# import standard libraries
import os
from threading import Thread, Event
import time
import platform
import requests
class ttkTimer(Thread):
"""a class serving same function as wxTimer... but there may be better ways to do this
"""
def __init__(self, callback, tick):
Thread.__init__(self)
self.callback = callback
self.stopFlag = Event()
self.tick = tick
self.iters = 0
def run(self):
while not self.stopFlag.wait(self.tick):
self.iters += 1
self.callback()
def stop(self):
self.stopFlag.set()
def get(self):
return self.iters
class Player(Tk.Frame):
"""The main window has to deal with events.
"""
def __init__(self, parent, title=None):
Tk.Frame.__init__(self, parent)
self.parent = parent
if title == None:
title = "tk_vlc"
self.parent.title(title)
# Menu Bar
# File Menu
menubar = Tk.Menu(self.parent)
self.parent.config(menu=menubar)
fileMenu = Tk.Menu(menubar)
fileMenu.add_command(label="Open", underline=0, command=self.OnOpen)
fileMenu.add_command(label="Exit", underline=1, command=_quit)
menubar.add_cascade(label="File", menu=fileMenu)
# The second panel holds controls
self.player = None
self.videopanel = ttk.Frame(self.parent)
self.canvas = Tk.Canvas(self.videopanel).pack(fill=Tk.BOTH,expand=1)
self.videopanel.pack(fill=Tk.BOTH,expand=1)
ctrlpanel = ttk.Frame(self.parent)
pause = ttk.Button(ctrlpanel, text="Pause", command=self.OnPause)
play = ttk.Button(ctrlpanel, text="Play", command=self.OnPlay)
stop = ttk.Button(ctrlpanel, text="Stop", command=self.OnStop)
volume = ttk.Button(ctrlpanel, text="Volume", command=self.OnSetVolume)
pause.pack(side=Tk.LEFT)
play.pack(side=Tk.LEFT)
stop.pack(side=Tk.LEFT)
volume.pack(side=Tk.LEFT)
self.volume_var = Tk.IntVar()
self.volslider = Tk.Scale(ctrlpanel, variable=self.volume_var, command=self.volume_sel,
from_=0, to=100, orient=Tk.HORIZONTAL, length=100)
self.volslider.pack(side=Tk.LEFT)
ctrlpanel.pack(side=Tk.BOTTOM)
ctrlpanel2 = ttk.Frame(self.parent)
self.scale_var = Tk.DoubleVar()
self.timeslider_last_val = ""
self.timeslider = customtkinter.CTkSlider(ctrlpanel2, variable=self.scale_var, command=self.scale_sel,
from_=0, to=1000, orient=Tk.HORIZONTAL) # This causes buffer
self.timeslider.pack(side=Tk.BOTTOM, fill=Tk.X,expand=1)
self.timeslider_last_update = time.time()
ctrlpanel2.pack(side=Tk.BOTTOM,fill=Tk.X)
# VLC player controls
self.Instance = vlc.Instance()
self.player = self.Instance.media_player_new()
self.timer = ttkTimer(self.OnTimer, 1.0)
self.timer.start()
self.parent.update()
#self.player.set_hwnd(self.GetHandle()) # for windows, OnOpen does does this
def Extract(self,topic):
"""Will play video on following topic, takes about 10 to 15 seconds to load"""
url = 'https://www.youtube.com/results?q=' + topic
count = 0
cont = ''
try:
cont = requests.get(url)
except:
print('Error','Cannot Connect.. Internet not connected or invalid URL or id.')
cont = ''
data = cont.content
data = str(data)
lst = data.split('"')
for i in lst:
count+=1
if i == 'WEB_PAGE_TYPE_WATCH':
break
if lst[count-5] == "/results":
print("Error","No video found.")
return "https://www.youtube.com"+lst[count-5]
def OnExit(self, evt):
"""Closes the window.
"""
self.Close()
def OnOpen(self):
"""Pop up a new dialow window to choose a file, then play the selected file.
"""
# if a file is already running, then stop it.
self.OnStop()
fullname = pafy.new(self.Extract(pyautogui.password(mask="", title="Enter Song Name:", text="Enter Song Name:"))).getbest().url
self.Media = self.Instance.media_new(fullname)
self.player.set_media(self.Media)
# set the window id where to render VLC's video output
if platform.system() == 'Windows':
self.player.set_hwnd(self.GetHandle())
else:
self.player.set_xwindow(self.GetHandle()) # this line messes up windows
# FIXME: this should be made cross-platform
self.OnPlay()
# set the volume slider to the current volume
#self.volslider.SetValue(self.player.audio_get_volume() / 2)
self.volslider.set(self.player.audio_get_volume())
def OnPlay(self):
"""Toggle the status to Play/Pause.
If no file is loaded, open the dialog window.
"""
# check if there is a file to play, otherwise open a
# Tk.FileDialog to select a file
if not self.player.get_media():
self.OnOpen()
else:
# Try to launch the media, if this fails display an error message
if self.player.play() == -1:
self.errorDialog("Unable to play.")
def GetHandle(self):
return self.videopanel.winfo_id()
#def OnPause(self, evt):
def OnPause(self):
"""Pause the player.
"""
self.player.pause()
def OnStop(self):
"""Stop the player.
"""
self.player.stop()
# reset the time slider
self.timeslider.set(0)
def OnTimer(self):
"""Update the time slider according to the current movie time.
"""
if self.player == None:
return
# since the self.player.get_length can change while playing,
# re-set the timeslider to the correct range.
length = self.player.get_length()
dbl = length * 0.001
self.timeslider.config(to=dbl)
# update the time on the slider
tyme = self.player.get_time()
if tyme == -1:
tyme = 0
dbl = tyme * 0.001
self.timeslider_last_val = ("%.0f" % dbl) + ".0"
# don't want to programatically change slider while user is messing with it.
# wait 2 seconds after user lets go of slider
if time.time() > (self.timeslider_last_update + 2.0):
self.timeslider.set(dbl)
def scale_sel(self, evt):
if self.player == None:
return
nval = self.scale_var.get()
sval = str(nval)
if self.timeslider_last_val != sval:
self.timeslider_last_update = time.time()
mval = "%.0f" % (nval * 1000)
self.player.set_time(int(mval)) # expects milliseconds
def volume_sel(self, evt):
if self.player == None:
return
volume = self.volume_var.get()
if volume > 100:
volume = 100
if self.player.audio_set_volume(volume) == -1:
self.errorDialog("Failed to set volume")
def OnToggleVolume(self, evt):
"""Mute/Unmute according to the audio button.
"""
is_mute = self.player.audio_get_mute()
self.player.audio_set_mute(not is_mute)
# update the volume slider;
# since vlc volume range is in [0, 200],
# and our volume slider has range [0, 100], just divide by 2.
self.volume_var.set(self.player.audio_get_volume())
def OnSetVolume(self):
"""Set the volume according to the volume sider.
"""
volume = self.volume_var.get()
# vlc.MediaPlayer.audio_set_volume returns 0 if success, -1 otherwise
if volume > 100:
volume = 100
if self.player.audio_set_volume(volume) == -1:
self.errorDialog("Failed to set volume")
def errorDialog(self, errormessage):
"""Display a simple error dialog.
"""
Tk.tkMessageBox.showerror(self, 'Error', errormessage)
def Tk_get_root():
if not hasattr(Tk_get_root, "root"): #(1)
Tk_get_root.root= Tk.Tk() #initialization call is inside the function
return Tk_get_root.root
def _quit():
print("_quit: bye")
root = Tk_get_root()
root.quit() # stops mainloop
root.destroy() # this is necessary on Windows to prevent
# Fatal Python Error: PyEval_RestoreThread: NULL tstate
os._exit(1)
if __name__ == "__main__":
# Create a Tk.App(), which handles the windowing system event loop
root = Tk_get_root()
root.protocol("WM_DELETE_WINDOW", _quit)
player = Player(root, title="tkinter vlc")
# show the player window centred and run the application
root.mainloop()
Kindly help
Regards.
Related
I am using a simple GUI with PyQt5 in which I have 3 buttons to record, stop and play the audio. I have some know-how of the Tkinter GUI, but I am totally confused here in PyQt5. In Tkinter Buttons, I am using commands something like this:
record_btn = Button(voice_rec, text="Record Audio", command=lambda m=1: threading_rec(m))
# Stop button
stop_btn = Button(voice_rec, text="Stop Recording", command=lambda m=2: threading_rec(m))
# Play button
play_btn = Button(voice_rec, text="Play Recording", command=lambda m=3: threading_rec(m))
I tried this in Pyqt5 Buttons, but still not working:
self.record_btn = QPushButton('Record', self)
self.record_btn.clicked.connect(lambda: self.threading_rec(x=1))
self.record_btn.move(100, 70)
self.stop_btn = QPushButton('Stop', self)
self.stop_btn.clicked.connect(lambda: self.threading_rec(x=2))
self.stop_btn.move(200, 70)
self.play_btn = QPushButton('Play', self)
self.play_btn.clicked.connect(lambda: self.threading_rec(x=3))
self.play_btn.move(300, 70)
Here is my complete code:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import pyqtSlot
import sounddevice as sd
import queue
import soundfile as sf
import threading
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'Voice Recorder'
self.left = 400
self.top = 300
self.width = 500
self.height = 200
self.initUI()
# Create a queue to contain the audio data
q = queue.Queue()
# Declare variables and initialise them
recording = False
file_exists = False
# Fit data into queue
def callback(indata, frames, time, status):
q.put(indata.copy())
def threading_rec(x):
if x == 1:
# If recording is selected, then the thread is activated
t1 = threading.Thread(target=record_audio)
t1.start()
elif x == 2:
# To stop, set the flag to false
global recording
recording = False
messagebox.showinfo(message="Recording finished")
elif x == 3:
# To play a recording, it must exist.
if file_exists:
# Read the recording if it exists and play it
data, fs = sf.read("trial.wav", dtype='float32')
sd.play(data, fs)
sd.wait()
else:
# Display and error if none is found
messagebox.showerror(message="Record something to play")
def record_audio(self):
# Declare global variables
global recording
# Set to True to record
recording = True
global file_exists
# Create a file to save the audio
messagebox.showinfo(message="Recording Audio. Speak into the mic")
with sf.SoundFile("trial.wav", mode='w', samplerate=44100,
channels=2) as file:
# Create an input stream to record audio without a preset time
with sd.InputStream(samplerate=44100, channels=2, callback=callback):
while recording == True:
# Set the variable to True to allow playing the audio later
file_exists = True
# write into file
file.write(q.get())
# Button to record audio
record_btn = Button(voice_rec, text="Record Audio", command=lambda m=1: threading_rec(m))
# Stop button
stop_btn = Button(voice_rec, text="Stop Recording", command=lambda m=2: threading_rec(m))
# Play button
play_btn = Button(voice_rec, text="Play Recording", command=lambda m=3: threading_rec(m))
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
record_btn = QPushButton('Record', self)
record_btn.move(100, 70)
stop_btn = QPushButton('Stop', self)
stop_btn.move(200, 70)
play_btn = QPushButton('Play', self)
play_btn.move(300, 70)
button.clicked.connect(self.on_click)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
Someone please help me with this code!
I'll try to answer this in a simple way.
Let's say you have a button like this:
def init_UI(self):
self.sum_button = QPushButton(text='sum')
self.sum_button.clicked.connect(lambda: self.get_sum(int_a, int_b))
def get_sum(self, int_a, int_b):
return int_a + int_b
I have a GUI where the user can click a button named "next set" that allows them to move onto the next task. I wanted to add a timer that starts as soon as they start the application and run the timer until they press the button "next set". When clicked, I want the time elapsed to print and the timer to restart until they press "next set" button again. I would like the timer to start automatically when the code runs. Currently, the "next set" button has two actions, one is to retrieve the next set of images and the other action I am trying to include is to reset the timer and print time elapsed. I also only included part of the code that felt relevant because it is long.
import time
import tkinter as tk
import csv
from pathlib import Path
import PIL.Image
import PIL.ImageDraw
import PIL.ImageTk
MAX_HEIGHT = 500
IMAGES_PATH = Path("Images")
CSV_LABELS_KEY = "New Labels"
CSV_FILE_NAME_KEY = "FolderNum_SeriesNum"
CSV_BOUNDING_BOX_KEY = "correct_flip_bbox"
counter = 0
timer_id = None
class App(tk.Frame):
def __init__(self, master=None):
super().__init__(master) # python3 style
self.config_paths = ["config 1.yaml", "config 2.yaml", "config 3.yaml"]
self.config_index = 0
self.clickStatus = tk.StringVar()
self.loadedImages = dict()
self.loadedBoxes = dict() # this dictionary will keep track of all the boxes drawn on the images
self.master.title('Slideshow')
frame = tk.Frame(self)
tk.Button(frame, text=" Next set ", command=lambda:[self.get_next_image_set(), self.reset()]).pack(side=tk.RIGHT)
tk.Button(frame, text=" Exit ", command=self.destroy).pack(side=tk.RIGHT)
frame.pack(side=tk.TOP, fill=tk.BOTH)
self.canvas = tk.Canvas(self)
self.canvas.pack()
self._load_dataset()
self.reset()
self.start_timer = None
t = time()
t.start()
def start_timer(self, evt=None):
if self._start_timer is not None:
self._start_timer = time.perf_counter()
# global counter
# counter += 1
# label.config(text=str(counter))
# label.after(1000, count)
def reset(self):
if self._start_timer is None:
elapsed_time = time.perf_counter() - self._start_timer
self._start_timer = None
print('Time elapsed (hh:mm:ss.ms) {}'.format(elapsed_time))
def _load_dataset(self):
try:
config_path = self.config_paths[self.config_index]
self.config_index += 1
except IndexError:
return
image_data = loadData(config_path)
# drawing the image on the label
self.image_data = image_data
self.currentIndex = 0
# start from 0th image
self._load_image()
def _load_image(self):
imgName = self.image_data[self.currentIndex]['image_file']
if imgName not in self.loadedImages:
self.im = PIL.Image.open(self.image_data[self.currentIndex]['image_file'])
ratio = MAX_HEIGHT / self.im.height
# ratio divided by existing height -> to get constant amount
height, width = int(self.im.height * ratio), int(self.im.width * ratio)
# calculate the new h and w and then resize next
self.canvas.config(width=width, height=height)
self.im = self.im.resize((width, height))
if self.im.mode == "1":
self.img = PIL.ImageTk.BitmapImage(self.im, foreground="white")
else:
self.img = PIL.ImageTk.PhotoImage(self.im)
imgData = self.loadedImages.setdefault(self.image_data[self.currentIndex]['image_file'], dict())
imgData['image'] = self.img
imgData['shapes'] = self.image_data[self.currentIndex]['shapes']
# for next and previous so it loads the same image adn don't do calculations again
self.img = self.loadedImages[self.image_data[self.currentIndex]['image_file']]['image']
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.img)
self.show_drag_box()
def loadData(fname):
with open(fname, mode='r') as f:
return yaml.load(f.read(), Loader=yaml.SafeLoader)
if __name__ == "__main__":
data = loadData('config 1.yaml')
app = App(data)
app.pack() # goes here
app.mainloop()
I have used datetime instead of time, as subtracting two datetime objects will give an output with hours and minutes included, whereas subtracting two time objects only gives seconds. However, both will work, you may just need to do more reformatting using time.
Read the current time when the application starts and store it. Each time you press the button, subtract your stored time from the current time which gives you your time elapsed. Then simply store your new current time until the next button press. The code below demonstrates this.
import tkinter as tk
import datetime as dt
class TimeButton(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
# Start timer
self.current_time = dt.datetime.today()
# Button
self.next_set = tk.Button(self, text='Next Set', command = self.clicked)
self.next_set.pack()
def clicked(self):
now = dt.datetime.today()
time_elapsed = now - self.current_time
print(time_elapsed)
self.current_time = now
if __name__ == "__main__":
window = tk.Tk()
button = TimeButton(window)
button.pack()
window.mainloop()
I am building a small audio GUI for processing ecological recordings. I want to be able to play a file, and pause it, and play it again from where I paused it. I can get it to play, and pause(stop), but when I hit play again it restarts the audio, not picks up from where it left off.
Pyaudio has the callback function which is what I am trying to implement (see here for working example). That example is pretty much what I want, except where this example has the 'keyboard.Listener' line in the while statement controlling the play/pause, I need to implement the play/pause button functionality from tkinter. I have also threaded the playback so that the GUI does not freeze the buttons, which has added some more complexity for me (I am an ecologist self taught in python, not a computer scientist!).
I have played around with threading.Event() for this as a way to control the stream thread, but I think that will just add additional complexity and leave me at the same problem of restarting from pause location.
Eventually I'd also like to pull out the frame number/time of file when paused, and also plot a progress bar on the tkinter canvas/matplot Figure - part of me says the pyaudio .get_time() embedded within the callback may be able to help with this (i think it returns system time).
Below is a minimum example I could make to get a gui working with where Im at.
import tkinter as tk
from tkinter import ttk
import wave
import pyaudio
import threading
import time
import numpy as np
import datetime
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# gui class
class basic_player():
def __init__(self, root):
# BUILD ROOT
self.root = root
root.title('Playback')
self.audio_file = 'C:/Data/Acoustics/Test/5D8CA5E8.WAV'
self.frame_plot()
self.frame_buttons()
# class globals
self.stream_paused = False
def frame_plot(self):
'''Frame for file plot'''
self.frame_plot = ttk.Frame(self.root, height = 100, width = 500)
self.frame_plot.grid(column = 0, row = 0, sticky = 'nsew', columnspan = 2)
self.frame_plot.grid_propagate(False)
# plot file
self.func_plot_fileplot()
def func_plot_fileplot(self):
'''Plot the main audiofile'''
# create figure to contain plot
# get frame size parameters (update frame parameters first)
self.frame_plot.update()
dpi = self.root.winfo_fpixels('1i')
plotwidth = self.frame_plot.winfo_width() / dpi
plotheight = self.frame_plot.winfo_height() / dpi
# create plot
plot_figure_fileplot_main = Figure(figsize = (plotwidth, plotheight),
dpi = dpi, frameon = False, tight_layout = True)
# get data
with wave.open(self.audio_file, mode = 'rb') as wf:
infile_audio_bytes = wf.readframes(wf.getnframes())
data = np.frombuffer(infile_audio_bytes, dtype = np.int16)
# plot x labels
lst_x_ticks = list(range(0, wf.getnframes(), int(wf.getnframes() / 8))) + [wf.getnframes()]
lst_x_label = [str(datetime.timedelta(seconds = int(sample / wf.getframerate()))) for sample in lst_x_ticks]
# add subplot
plot_figure_fileplot = plot_figure_fileplot_main.add_subplot(111)
plot_figure_fileplot.plot(data, linewidth = 0.25)
# adjust subplot visuals
plot_figure_fileplot.set_xmargin(0)
plot_figure_fileplot.yaxis.set_visible(False)
plot_figure_fileplot.spines['top'].set_visible(False)
plot_figure_fileplot.spines['right'].set_visible(False)
plot_figure_fileplot.spines['left'].set_visible(False)
# labels for plot x axis
plot_figure_fileplot.set_xticks(lst_x_ticks) # set x labels to existing to make sure they find the right spot
plot_figure_fileplot.set_xticklabels(lst_x_label, size = 8)
#create tkinter canvas
self.canvas_plot_figure_main = FigureCanvasTkAgg(plot_figure_fileplot_main, master = self.frame_plot)
self.canvas_plot_figure_main.draw()
#place canvas on tkinter window
self.canvas_plot_figure_main.get_tk_widget().grid(sticky = 'nsew')
def frame_buttons(self):
'''The main frame for the initial window'''
frame_buttons = ttk.Frame(self.root, width = 100)
frame_buttons.grid(column = 0, row = 1, sticky = 'nsew')
btn_play = tk.Button(frame_buttons,
text = 'PLAY',
command = self.buttons_command_play,
state = 'normal',
width = 10)
btn_play.grid(column = 0, row = 0, sticky = 'nsew', padx = 10, pady = 10)
btn_pause = tk.Button(frame_buttons,
text = 'PAUSE',
command = self.buttons_command_playback_pause,
state = 'normal',
width = 10)
btn_pause.grid(column = 1, row = 0, sticky = 'nsew', padx = 10, pady = 10)
def buttons_command_play(self):
''' send play audio function to thread '''
self.stream_paused = False
self.stream_thread = threading.Thread(target = self.play_audio)
self.stream_thread.start()
def play_audio(self):
'''Play audio'''
if self.stream_paused: # this doesnt work.
self.stream.start_stream()
else:
# open file
wf = wave.open(self.audio_file, mode = 'rb')
# instantiate pyaudio
self.pyaudio_init = pyaudio.PyAudio()
# define callback
def callback(in_data, frame_count, time_info, status):
data = wf.readframes(frame_count)
return (data, pyaudio.paContinue)
# open stream using callback
self.stream = self.pyaudio_init.open(format=self.pyaudio_init.get_format_from_width(wf.getsampwidth()),
input = False,
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True,
stream_callback=callback)
self.stream.start_stream()
# start the stream
while self.stream.is_active() and not self.stream_paused:
# this is where the control event needs to work i believe
time.sleep(0.1)
# stop stream
self.stream.stop_stream()
self.stream.close()
wf.close()
def buttons_command_playback_pause(self):
''' Pause the audio '''
if not self.stream_paused:
self.stream_paused = True
else:
pass
## SETUP AND RUN
root = tk.Tk()
basic_player(root)
root.mainloop()
stream_callback is unnecessary here. You could instead create a new thread and run the stream.write() in a loop.
To pause the audio set a flag, and add a condition in the loop. write the stream only if the pause condition is False
Here is an example.
import tkinter as tk
import wave
import pyaudio
import threading
class SamplePlayer:
def __init__(self, master):
frame = tk.Frame(master=master)
frame.pack(expand=True, fill="both")
self.current_lbl = tk.Label(master=frame, text="0/0")
self.current_lbl.pack()
self.pause_btn = tk.Button(master=frame, text="Pause", command=self.pause)
self.pause_btn.pack()
self.play_btn = tk.Button(master=frame, text="Play", command=self.play)
self.play_btn.pack()
self.file = r"sample_wavfile.wav"
self.paused = True
self.playing = False
self.audio_length = 0
self.current_sec = 0
self.after_id = None
def start_playing(self):
p = pyaudio.PyAudio()
chunk = 1024
with wave.open(self.file, "rb") as wf:
self.audio_length = wf.getnframes() / float(wf.getframerate())
stream = p.open(format =
p.get_format_from_width(wf.getsampwidth()),
channels = wf.getnchannels(),
rate = wf.getframerate(),
output = True)
data = wf.readframes(chunk)
chunk_total = 0
while data != b"" and self.playing:
if not self.paused:
chunk_total += chunk
stream.write(data)
data = wf.readframes(chunk)
self.current_sec = chunk_total/wf.getframerate()
self.playing=False
stream.close()
p.terminate()
def pause(self):
self.paused = True
if self.after_id:
self.current_lbl.after_cancel(self.after_id)
self.after_id = None
def play(self):
if not self.playing:
self.playing = True
threading.Thread(target=self.start_playing, daemon=True).start()
if self.after_id is None:
self.update_lbl()
self.paused = False
def stop(self):
self.playing = False
if self.after_id:
self.current_lbl.after_cancel(self.after_id)
self.after_id = None
def update_lbl(self):
self.current_lbl.config(text=f"{self.current_sec}/{self.audio_length}")
self.after_id = self.current_lbl.after(5, self.update_lbl)
def handle_close():
player.stop()
root.destroy()
## SETUP AND RUN
root = tk.Tk()
player = SamplePlayer(root)
root.protocol("WM_DELETE_WINDOW", handle_close)
root.mainloop()
This answer is the same as #Art's answer but I removed the self.after_id variable to simplify the logic a bit:
import tkinter as tk
import threading
import pyaudio
import wave
import time
class SamplePlayer:
def __init__(self, master):
frame = tk.Frame(master=master)
frame.pack(expand=True, fill="both")
self.current_lbl = tk.Label(master=frame, text="0/0")
self.current_lbl.pack()
self.pause_btn = tk.Button(master=frame, text="Pause", command=self.pause)
self.pause_btn.pack()
self.play_btn = tk.Button(master=frame, text="Play", command=self.play)
self.play_btn.pack()
# If you aren't going to use `\`s there is no need for the
# "r" before the start of the string
self.file = r"sample_wavfile.wav"
self.paused = True
self.playing = False
self.audio_length = 0
self.current_sec = 0
def start_playing(self):
""" # I don't have `pyaudio` so I used this to test my answer:
self.audio_length = 200
while self.playing:
if not self.paused:
self.current_sec += 1
time.sleep(1)
return None
# """
p = pyaudio.PyAudio()
chunk = 1024
with wave.open(self.file, "rb") as wf:
self.audio_length = wf.getnframes() / float(wf.getframerate())
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True)
data = wf.readframes(chunk)
chunk_total = 0
while data != b"" and self.playing:
if self.paused:
time.sleep(0.1)
else:
chunk_total += chunk
stream.write(data)
data = wf.readframes(chunk)
self.current_sec = chunk_total/wf.getframerate()
self.playing = False
stream.close()
p.terminate()
def pause(self):
self.paused = True
def play(self):
if not self.playing:
self.playing = True
threading.Thread(target=self.start_playing, daemon=True).start()
if self.paused:
self.paused = False
self.update_lbl()
def stop(self):
self.playing = False
def update_lbl(self):
if self.playing and (not self.paused):
self.current_lbl.config(text=f"{self.current_sec}/{self.audio_length}")
# There is no need to update the label more than 10 times a second.
# It changes once per second anyways.
self.current_lbl.after(100, self.update_lbl)
def handle_close():
player.stop()
root.destroy()
## SETUP AND RUN
root = tk.Tk()
player = SamplePlayer(root)
root.protocol("WM_DELETE_WINDOW", handle_close)
root.mainloop()
There is no need for a self.after_id variable if you can just add if self.playing and (not self.paused) to the code that calls .after. In this case it's the update_lbl method.
I have use VLC Python binding for media Player Gui and I want to add Timer to the Below Slider its like 0.00:0.00 (current video time : total duration of video) how can i add this ??
How to callback the current playing time and display as label below position slider as mention in image ?
Please help for small hint to how to do??
import sys
from PyQt5 import QtCore as qtc
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtGui as qtg
import vlc
import os.path
class MainWindow(qtw.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
##Main framwork
# creating a basic vlc instance
self.instance = vlc.Instance()
# creating an empty vlc media player
self.mediaplayer = self.instance.media_player_new()
self.createUI()
self.isPaused = False
def createUI(self):
base_widget = qtw.QWidget()
base_widget.setLayout(qtw.QHBoxLayout())
notebook = qtw.QVBoxLayout()
base_widget.layout().addLayout(notebook)
self.setCentralWidget(base_widget)
#VideoFrame Loading
self.videoframe = qtw.QFrame()
self.videoframe.setMinimumWidth(950)
self.videoframe.setMinimumHeight(525)
self.palette = self.videoframe.palette()
self.palette.setColor (qtg.QPalette.Window,
qtg.QColor(0,0,0))
self.videoframe.setPalette(self.palette)
self.videoframe.setAutoFillBackground(True)
#Position Slider
self.positionslider = qtw.QSlider(qtc.Qt.Horizontal, self)
self.positionslider.setToolTip("Position")
self.positionslider.setMaximum(100000.0)
self.positionslider.setTickPosition(qtw.QSlider.TicksBelow)
self.positionslider.setTickInterval(2000)
self.positionslider.sliderMoved.connect(self.setPosition)
self.hbuttonbox = qtw.QHBoxLayout()
self.playbutton = qtw.QPushButton("Play")
self.hbuttonbox.addWidget(self.playbutton)
self.playbutton.clicked.connect(self.PlayPause)
#Button Box
self.stopbutton = qtw.QPushButton("Stop")
self.hbuttonbox.addWidget(self.stopbutton)
self.stopbutton.clicked.connect(self.Stop)
#Volume slider
self.hbuttonbox.addStretch(1)
self.volumeslider = qtw.QSlider(qtc.Qt.Horizontal, self)
self.volumeslider.setMaximum(100)
self.volumeslider.setValue(self.mediaplayer.audio_get_volume())
self.volumeslider.setToolTip("Volume")
self.hbuttonbox.addWidget(self.volumeslider)
self.volumeslider.valueChanged.connect(self.setVolume)
notebook.addWidget(self.videoframe)
notebook.addWidget(self.positionslider)
notebook.addLayout(self.hbuttonbox)
#Actions Code
open1 = qtw.QAction("&Open", self)
open1.triggered.connect(self.OpenFile)
exit = qtw.QAction("&Exit", self)
exit.triggered.connect(sys.exit)
menubar = self.menuBar()
filemenu = menubar.addMenu("&File")
filemenu.addAction(open1)
filemenu.addSeparator()
filemenu.addAction(exit)
self.timer = qtc.QTimer(self)
self.timer.setInterval(200)
self.timer.timeout.connect(self.updateUI)
def PlayPause(self):
"""Toggle play/pause status
"""
if self.mediaplayer.is_playing():
self.mediaplayer.pause()
self.playbutton.setText("Play")
self.isPaused = True
else:
if self.mediaplayer.play() == -1:
self.OpenFile()
return
self.mediaplayer.play()
self.playbutton.setText("Pause")
self.timer.start()
self.isPaused = False
def PausePlay(self):
if self.mediaplayer.is_playing():
self.mediaplayer.pause()
self.playbutton.setText("Play")
self.isPaused = True
def Stop(self):
"""Stop player
"""
self.mediaplayer.stop()
self.playbutton.setText("Play")
def OpenFile(self, filename=None):
"""Open a media file in a MediaPlayer
"""
if filename is None or filename is False:
print("Attempt to open up OpenFile")
filenameraw = qtw.QFileDialog.getOpenFileName(self, "Open File", os.path.expanduser('~'))
filename = filenameraw[0]
if not filename:
return
# create the media
if sys.version < '3':
filename = unicode(filename)
self.media = self.instance.media_new(filename)
# put the media in the media player
self.mediaplayer.set_media(self.media)
# parse the metadata of the file
self.media.parse()
# set the title of the track as window title
self.setWindowTitle(self.media.get_meta(0))
# print(vlc.libvlc_media_get_meta(self.media, 6))
# print(vlc.libvlc_media_get_duration(self.media))
# the media player has to be 'connected' to the QFrame
# (otherwise a video would be displayed in it's own window)
# this is platform specific!
# you have to give the id of the QFrame (or similar object) to
# vlc, different platforms have different functions for this
if sys.platform.startswith('linux'): # for Linux using the X Server
self.mediaplayer.set_xwindow(self.videoframe.winId())
elif sys.platform == "win32": # for Windows
self.mediaplayer.set_hwnd(self.videoframe.winId())
elif sys.platform == "darwin": # for MacOS
self.mediaplayer.set_nsobject(int(self.videoframe.winId()))
self.PlayPause()
def setVolume(self, Volume):
"""Set the volume """
self.mediaplayer.audio_set_volume(Volume)
def setPosition(self, position):
"""Set the position
"""
# setting the position to where the slider was dragged
self.mediaplayer.set_position(position / 100000.0)
# the vlc MediaPlayer needs a float value between 0 and 1, Qt
# uses integer variables, so you need a factor; the higher the
# factor, the more precise are the results
# (1000 should be enough)
def updateUI(self):
"""updates the user interface"""
# setting the slider to the desired position
self.positionslider.setValue(self.mediaplayer.get_position() * 100000.0)
if not self.mediaplayer.is_playing():
# no need to call this function if nothing is played
self.timer.stop()
if not self.isPaused:
# after the video finished, the play button stills shows
# "Pause", not the desired behavior of a media player
# this will fix it
self.Stop()
if __name__ == '__main__':
app = qtw.QApplication(sys.argv) #it's required to save a referance to MainWindow
mw = MainWindow()
mw.show()
if sys.argv[1:]:
mw.OpenFile(sys.argv[1])
sys.exit(app.exec_())
#if it goes out of scope ,it will be destroyed
So, Recently I was trying to make an audio player using PyQt5, pygame, and mutagen. The program works pretty fine. But when I'm playing a song and try to quit the program, the program stops responding and the song continues to play. But this doesn't happen when a song is not playing, it works fine then.
Here is the Code:
from PyQt5 import QtWidgets, QtGui
from PyQt5.QtWidgets import QApplication, QMainWindow, QSlider
from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt
import sys
import pygame as pg
from mutagen.mp3 import MP3
import os
import threading
pg.init()
#33206
class window(QMainWindow):
def __init__(self):
super(window, self).__init__()
self.setGeometry(425, 65, 400, 190)
self.setWindowIcon(QtGui.QIcon("icon"))
self.setWindowTitle("MultiMedia Player")
# MenuBar
file = QtWidgets.QAction("&Open Mp3", self)
file.setShortcut("Ctrl + O")
file.triggered.connect(self.open_mp3)
# Quit
quit = QtWidgets.QAction("&Quit", self)
quit.setShortcut("Q")
quit.triggered.connect(self.close_app)
# Add Items
items = QtWidgets.QAction("&Add Items", self)
items.setShortcut("Ctrl + P")
mainmenu = self.menuBar()
filemenu = mainmenu.addMenu("&Open")
filemenu.addAction(file)
add_items = mainmenu.addMenu("&Add Items")
add_items.addAction(items)
filemenu.addAction(quit)
self.flag = 0
self.home()
def home(self):
# colors
black = (13, 13, 13)
light_black = (36, 36, 36)
# Pause Button
self.pause_btn = QtWidgets.QPushButton(self)
self.pause_btn.setText("Pause")
self.pause_btn.setShortcut("p")
self.pause_btn.move(0, 120)
self.pause_btn.clicked.connect(self.pause)
# Play Button
self.play_btn = QtWidgets.QPushButton(self)
self.play_btn.setText("Play")
self.play_btn.setShortcut("Space")
self.play_btn.move(150, 120)
self.play_btn.clicked.connect(self.play)
# Stop Button
self.stop_btn = QtWidgets.QPushButton(self)
self.stop_btn.setText("Stop")
self.stop_btn.setShortcut("s")
self.stop_btn.move(300, 120)
self.stop_btn.clicked.connect(self.stop)
# color for the window
color = QColor(70, 70, 70)
# Volume_Up Button
self.vup_btn = QtWidgets.QPushButton(self)
self.vup_btn.setText("V(+)")
self.vup_btn.setShortcut("+")
self.vup_btn.move(300, 160)
self.vup_btn.clicked.connect(self.volume_up)
# Volume_Down Button
self.vdown_btn = QtWidgets.QPushButton(self)
self.vdown_btn.setText("V(-)")
self.vdown_btn.setShortcut("-")
self.vdown_btn.move(0, 160)
self.vdown_btn.clicked.connect(self.volume_down)
# Seek Slider
self.slider = QSlider(Qt.Horizontal, self)
self.slider.setGeometry(20, 75, 350, 20)
# Volume Slider
self.v_slider = QSlider(Qt.Horizontal, self)
self.v_slider.setGeometry(120, 165, 160, 20)
self.v_slider.setMinimum(0)
self.v_slider.setMaximum(100)
self.v_slider.setValue(70)
self.volume_value = self.v_slider.value()
self.v_slider.valueChanged.connect(self.slider_value_changed)
print(self.v_slider.value() / 100)
def msg(self, title, message):
msg1 = QtWidgets.QMessageBox()
msg1.setWindowIcon(QtGui.QIcon("icon"))
msg1.setWindowTitle(title)
msg1.setText(message)
msg1.setStandardButtons(QtWidgets.QMessageBox.Ok)
msg1.exec_()
def open_mp3(self):
name = QtWidgets.QFileDialog.getOpenFileName(self)
format = os.path.splitext(name[0])
if format[1] == ".mp3":
self.audio = MP3(name[0])
self.duration = self.audio.info.length//1
self.min = int(self.duration // 60)
self.sec = int(self.duration % 60)
self.total_time = str(self.min) + ":" + str(self.sec)
print(self.total_time)
self.slider.setMaximum(self.duration)
self.slider.setMinimum(0)
time = []
time.append(self.total_time)
self.label = QtWidgets.QLabel(self)
self.label.setText(self.total_time)
self.label.setFont(QtGui.QFont("Arial", 9))
self.label.adjustSize()
self.label.move(373, 77)
song = name[0]
pg.mixer.music.load(song)
pg.mixer.music.play(1)
pg.mixer.music.set_volume(self.v_slider.value()/100)
self.label = QtWidgets.QLabel(self)
self.label.setText(song)
self.label.setFont(QtGui.QFont("Arial", 15))
self.label.adjustSize()
self.label.move(0, 36)
self.label.show()
threading_1 = threading.Thread(target=self.cur_time).start()
else:
self.msg("Invalid Format", "Choose A .Mp3 File Only!")
volume_level = pg.mixer.music.get_volume()
print(volume_level)
def cur_time(self):
true = 1
while true == 1:
if self.flag == 0:
self.m_time = pg.mixer.music.get_pos()
self.mm_time = self.m_time * 0.001
self.s_time = self.mm_time // 1
self.slider.setValue(self.s_time)
print(self.s_time)
self.slider.sliderMoved.connect(self.seek_changed)
if self.s_time == -1:
self.slider.setValue(0)
true = 2
if self.flag == 1:
print(self.s_time)
def seek_changed(self):
print(self.slider.value())
pg.mixer.music.set_pos(self.slider.value())
def slider_value_changed(self):
self.volume_value = self.v_slider.value()
pg.mixer.music.set_volume(self.v_slider.value()/100)
def volume_up(self):
self.volume_value = self.volume_value + 10
self.v_slider.setValue(self.volume_value)
if self.volume_value >= 100:
self.volume_value = 100
pg.mixer.music.set_volume(self.v_slider.value() / 100)
print(self.v_slider.value() / 100)
def volume_down(self):
self.volume_value = self.volume_value - 10
self.v_slider.setValue(self.volume_value)
if self.volume_value <= 0:
self.volume_value = 0
pg.mixer.music.set_volume(self.v_slider.value() / 100)
print(self.v_slider.value() / 100)
def pause(self):
pg.mixer.music.pause()
self.flag = 1
def stop(self):
pg.mixer.music.stop()
self.flag = -1
def play(self):
pg.mixer.music.unpause()
self.flag = 0
def close_app(self):
choice = QtWidgets.QMessageBox.question(
self, "QUIT", "You Sure You Wanna Quit?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if choice == QtWidgets.QMessageBox.Yes:
sys.exit()
else:
pass
def items(self):
layout = QtWidgets.QVBoxLayout(self)
song_name = QtWidgets.QFileDialog.getOpenFileName(self)
widget = QtWidgets.QListWidget()
widget.setAlternatingRowColors(True)
widget.setDragDropMode(
QtWidgets.QAbstractItemView.InternalMove)
widget.addItems([str(i) for i in range(1, 6)])
layout.addWidget(widget)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = window()
win.show()
sys.exit(app.exec_())
Thanks In Advance.
The main problem is that you're still having the threading.Thread running, so while the QtApplication is "closed", the program is still alive.
You should really avoid using a while loop to check for the current position, as it will call request that value each time the loop cycles, consuming a lot of unnecessary CPU resources.
Also, you're connecting the sliderMoved signal to seek_changed each time the loops cycles, which is bad.
Use a QTimer instead, which will update the cursor position without overloading the process:
# create a timer in the window __init__
self.cursor_updater = QtCore.QTimer(interval=100, timeout=self.cur_time)
#...
def cur_time(self):
# ignore the update if the user is seeking
if self.slider.isSliderDown():
return
self.slider.setValue(pg.mixer.music.get_pos() * .001)
Then you just need to start the timer everytime the music starts (or unpauses) and stop whenever you stop or pause.
That said, there are other issues with your code.
pygame and Qt run their own event loops, so you can't easily and gracefully quit via sys.exit(), nor their own quit() functions, as it's possible that one or both of them would just hang in their own loop without being able to actually quit, keeping the process running (looping doing almost nothing) and consuming a lot of resources. I'm no expert in using pygame and PyQt but, as far as I know, you can call os._exit(0) instead.
the window closeEvent() should be taken care of, because if the user just closes the window without quitting, there won't be any confirmation dialog and the exit procedure described above won't be called.
pygame.mixer.music.get_pos() "only represents how long the music has been playing; it does not take into account any starting position offsets". So you'll need to keep track of the position whenever you use set_pos() and compute the actual value accordingly.
you should really consider using layouts, or ensure that the window size is fixed, otherwise the user will be able to resize it to a size smaller than the interface is.