3

I'm building a music player with Pygame & Tkinter and currently trying to add a working time slider that allows you to jump to a specific point in a song by dragging the slider.

I set the value of the end of the slider ('to' in config) to the length of the current song then update the slider with 'after' as shown below:

def update_timeslider(self, _ = None):
    time = (pygame.mixer.music.get_pos()/1000)
    timeslider.set(time)
    self.after(1000, self.update_timeslider)

This works in moving the slider along with the time of the song but now I'm trying to implement the cue function and having issues. I try to update song position to the value of the slider with

def cue(self, _ = None):
    pygame.mixer.music.set_pos(timeslider.get()) 

Now when I play the song, its very choppy. When I move the slider, it works for a split second before the 'after' function updates but then it jumps back to the position it was before being moved. I tried increasing the refresh rate but it just causes it to jump back faster and remains just as choppy.

Is there a better way to do this?

Full code:

import os
import pygame
import tkinter
from tkinter.filedialog import askdirectory
from tkinter import *
from tkinter import ttk

playlist = [] 
index = 0
paused = False

class Application(tkinter.Tk):
    def __init__(self, parent):
        tkinter.Tk.__init__(self, parent)
        self.minsize(400,400)
        self.parent = parent
        self.main()

    def main(self):
        global v
        global songlabel
        global listbox
        global volumeslider
        global timeslider
        global time_elapsed
        global songlength

        self.configure(background='grey')
        self.grid()
        self.listbox = Listbox(self, width=20, height=25, relief='ridge', bd=3)
        self.listbox.grid(padx=30, pady=15, row=1, columnspan=11, sticky='NSEW')

        v = StringVar()
        songlabel = tkinter.Label(self, textvariable=v, width=30, anchor="n")

        rewbtn = PhotoImage(file="rew.gif")
        stopbtn = PhotoImage(file="stop.gif")
        playbtn = PhotoImage(file="play.gif")
        pausebtn = PhotoImage(file="pause.gif")
        ffbtn = PhotoImage(file="ff.gif")

        prevbutton = Button(self, width=30, height=30, image=rewbtn, anchor='w')
        prevbutton.image = rewbtn
        prevbutton.bind("<Button-1>", self.prevsong)
        prevbutton.grid(row=10, column=0, padx=(30,0), sticky='w')

        playbutton = Button(self, width=30, height=30, image=playbtn, anchor='w')
        playbutton.image = playbtn
        playbutton.bind("<Button-1>", self.play)
        playbutton.grid(row=10, column=1, sticky='w')

        pausebutton = Button(self, width=30, height=30, image=pausebtn, anchor='w')
        pausebutton.image = pausebtn
        pausebutton.bind("<Button-1>", self.pause)
        pausebutton.grid(row=10, column=2, sticky='w')

        stopbutton = Button(self, width=30, height=30, image=stopbtn, anchor='w') 
        stopbutton.image = stopbtn
        stopbutton.bind("<Button-1>", self.stop)
        stopbutton.grid(row=10, column=3, sticky='w')

        nextbutton = Button(self, width=30, height=30, image=ffbtn, anchor='w')
        nextbutton.image = ffbtn
        nextbutton.bind("<Button-1>", self.nextsong)
        nextbutton.grid(row=10, column=4, sticky='w')

        volumeslider = Scale(self, from_=0, to = 1, resolution = 0.01, orient = HORIZONTAL, showvalue = 'yes', command = self.change_vol)
        volumeslider.grid(row=10, column=8, columnspan=3, padx=30, pady=(0,10), sticky='wse')
        volumeslider.set(50)

        timeslider = Scale(self, from_=0, to=100, resolution=1, orient=HORIZONTAL, showvalue = 'no', command=self.cue) 
        timeslider.grid(row=12, column=0, columnspan=11, padx = 30, sticky='wse')
        timeslider.set(0)

        time_elapsed = Label(text="0:00:00")
        time_elapsed.grid(row=13, columnspan=11, padx=(30,0), pady=(0,30), sticky='ws')
        # time_remaining = Label(text="0:00:00")
        # time_remaining.grid(row=13, column = 7, columnspan=5, padx=(0,30), pady=(0,30), sticky='se')

    # FILE OPEN
        self.directorychooser()
        playlist.reverse()
        for items in playlist:
            self.listbox.insert(0, items)
        playlist.reverse()
        self.listbox.bind("<Double-Button-1>", self.selectsong)
        self.listbox.bind("<Return>", self.selectsong)
        songlabel.grid(row = 0, column = 0, columnspan = 10, padx = 55, pady=(10,0), sticky=W+N+E)

    # GRID WEIGHT
        self.grid_columnconfigure(5,weight=1)
        self.grid_columnconfigure(7,weight=1)
        self.grid_rowconfigure(1,weight=1)


    def prevsong(self, event):
        global index

        if index > 0:
            index-=1
            print(index)
        elif index == 0:
            index = len(playlist)-1

        pygame.mixer.music.load(playlist[index])
        self.set_timescale()
        pygame.mixer.music.play()
        self.get_time_elapsed()
        self.update_timeslider()
        self.update_currentsong()


    def play(self, event):
        self.set_timescale()        
        pygame.mixer.music.play()
        self.get_time_elapsed()
        self.update_timeslider()
        self.update_currentsong()


    def pause(self, event): 
        global paused
        if paused == True:
            pygame.mixer.music.unpause()
            paused = False
        elif paused == False:
            pygame.mixer.music.pause()
            paused = True


    def nextsong(self, event):
        global index

        if index < len(playlist)-1:
            index+=1
        elif index == (len(playlist)-1):
            index = 0
        pygame.mixer.music.load(playlist[index])
        self.set_timescale()
        pygame.mixer.music.play()
        self.get_time_elapsed()
        self.update_timeslider()
        self.update_currentsong()


    def stop(self, event):
        pygame.mixer.music.stop()
        v.set("")
        return songlabel


    def selectsong(self, event):
        global index
        global songtime
        global songlength

        idx = self.listbox.curselection()
        index = idx[0]
        pygame.mixer.music.load(playlist[index])

        self.set_timescale()
        pygame.mixer.music.play()
        self.get_time_elapsed()
        # self.get_time_remaining()
        self.update_timeslider()
        self.update_currentsong()


    def change_vol(self, _ = None):
        pygame.mixer.music.set_volume(volumeslider.get())


    def cue(self, _ = None):
        pygame.mixer.music.set_pos(timeslider.get())


    def getsonglen(self):
        s = pygame.mixer.Sound(playlist[index])
        songlength = s.get_length()
        return songlength


    def set_timescale(self):
        songlength = self.getsonglen()
        timeslider.config(to=songlength)    


    def get_time_elapsed(self):
        global time_elapsed
        time = int(pygame.mixer.music.get_pos()/1000)
        m, s = divmod(time, 60)
        h, m = divmod(m, 60)
        clock = "%d:%02d:%02d" % (h, m, s)
        time_elapsed.configure(text=clock)
        self.after(100, self.get_time_elapsed)

    # def get_time_remaining(self):
    #   global time_remaining
    #   time = int(pygame.mixer.music.get_pos()/1000)
    #   songlen = int(self.getsonglen())
    #   rem = songlen - time
    #   m, s = divmod(rem, 60)
    #   h, m = divmod(m, 60)
    #   clock2 = "%d:%02d:%02d" % (h, m, s)
    #   time_remaining.configure(text=clock2)
    #   self.after(100, self.get_time_remaining)


    def update_timeslider(self, _ = None):
        time = (pygame.mixer.music.get_pos()/1000)
        timeslider.set(time)
        self.after(10, self.update_timeslider)


    def update_currentsong(self):
        global index
        global songlabel
        v.set(playlist[index])
        return songlabel


    def directorychooser(self):
        directory = askdirectory()
        os.chdir(directory)
        for files in os.listdir(directory):
            if files.endswith(".flac"):
                realdir = os.path.realpath(files)
                playlist.append(files)
                print(files)
        pygame.mixer.init()
        pygame.mixer.music.load(playlist[0])
        self.update_currentsong()

app = Application(None)
app.mainloop()
mattC
  • 363
  • 2
  • 17
  • What have you done to debug this? It appears that every time you call `update_timeslider` you start a _new_ loop of it calling itself every 10ms without ever cancelling any existing call. You call that function in four different places besides it calling itself. It could be that this function is being called many times more than you think. – Bryan Oakley Jun 18 '17 at 23:32

2 Answers2

3

The problem appears to be that update_timeslider is being called way more times than you think it does.

When you do this:

self.update_timeslider()

... it causes self.update_timeslider to be called 100 times per second.

The problem is that you call that from multiple places in the code, which mean you may ultimately be calling that function 500-1000 times per second or even more. If each call takes a large fraction of a second, you'll end up spending more CPU time updating the slider than you are playing the sound.

When you call after it will return an id. You can use this id to later cancel any existing call before starting a new loop. For example, you might want to do something like this:

class Application(tkinter.Tk):
    def __init__(self, parent):
        ...
        self.after_id = None

    def update_timeslider(self, _ = None):
        if self.after_id is not None:
            self.after_cancel(self.after_id)
            self.after_id = None

        time = (pygame.mixer.music.get_pos()/1000)
        timeslider.set(time)
        self.after_id = self.after(1000, self.update_timeslider)
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
0

Instead of using pygame.mixer.Sound() in your getsonglen() function, use Mutagen for getting the song length.

pygame.mixer.Sound() at first converts the song to a Sound, I exactly don't know what happens, but it probably causes it to use more CPU and that's why the audio gets choppy.

I also faced the same problem, so I used Mutagen for getting the song's length. It worked fine for me.

from mutagen.mp3 import MP3

song = 'your song path'
total_length = MP3(song).info.length

Hope it helps!