Sarah
Sarah

Reputation: 617

Playing sound using PyGame mixer does not work expected

I'm doing a project which is making a music player using PyGame and Tkinter. So far I've built the interface and got the player to play two different songs, but I need a piece of advice on how to use PyGame mixer better. My code is:

from tkinter import *
import time, sys
from pygame import mixer

track_names = ['lady maria', 'cleric beast']
current_track = ''


def press(word):
    global track_name
    global current_track
    word = button_text.get()
    if word == 'PLAY':
        update_button_text('PLAY')
        for name in track_names:
            window_2.delete(0, 'end')
            current_track = name

            window_2.configure(state='normal')
            window_2.insert('end', name)

            mixer.init()
            mixer.music.set_volume(100)
            mixer.music.load(f'C:/Users/user/Desktop/python/projects/etc/{name}.mp3')
            mixer.music.play()
            time.sleep(5)

    if word == 'PAUSE':
        update_button_text('PAUSE')
        mixer.music.pause()
        time.sleep(5)

    if word == 'STOP':
        mixer.music.stop()
        time.sleep(5)

    if word == 'NEXT':
        pass

    if word == 'PREVIOUS':
        pass


def update_button_text(word):
    if word == 'PLAY':
        button_text.set('PAUSE')
    elif word == 'PAUSE':
        button_text.set('PLAY')


if __name__ == '__main__':
    # create application window
    app = Tk()

    # title
    app.title("Music Players")

    # geometry
    app.geometry('383x121')

    # background color
    app.configure(bg='orange')

    equation = StringVar()
    window_1 = Label(app, textvariable=equation)
    window_1.grid(columnspan=4, ipadx=100, ipady=10)
    equation.set('music player')

    window_2 = Entry(app, width=30)
    window_2.grid(columnspan=4, ipadx=100, ipady=10)
    window_2.configure(state='disabled')

    window_2.grid_columnconfigure((0, 1, 2), uniform="equal", weight=1)

    # Create buttons
    button_text = StringVar()
    button_text.set("PLAY")
    button1 = Button(app, textvariable=button_text, fg='yellow', bg='purple',
                     command=lambda: press(button_text), height=2, width=1)
    button1.grid(row=2, column=0, sticky="NSEW")

    button2 = Button(app, text='STOP', fg='yellow', bg='purple',
                     command=lambda: press('STOP'), height=2, width=1)
    button2.grid(row=2, column=1, sticky="NSEW")

    button3 = Button(app, text='NEXT', fg='yellow', bg='purple',
                     command=lambda: press('NEXT'), height=2, width=1)
    button3.grid(row=2, column=2, sticky="NSEW")

    button4 = Button(app, text='PREVIOUS', fg='yellow', bg='purple',
                     command=lambda: press('PREVIOUS'), height=2, width=1)
    button4.grid(row=2, column=3, sticky="NSEW")

# start the GUI
app.mainloop()

So when I run the code and click PLAY, it looks like the player is stopped responding. Although it starts playing the music right away, it does not show the title. I have code that inserts the title of the song into Tkinter entry first, and then mixer function comes after but it seems like the mixer function always comes first. This is the part I'm having a problem (I think):

def press(word):
    global track_name
    global current_track
    word = button_text.get()
    if word == 'PLAY':
        update_button_text('PLAY')
        for name in track_names:
            window_2.delete(0, 'end')
            current_track = name

            window_2.configure(state='normal')
            window_2.insert('end', name)

            mixer.init()
            mixer.music.set_volume(100)
            mixer.music.load(f'C:/Users/user/Desktop/python/projects/etc/{name}.mp3')
            mixer.music.play()
            time.sleep(5)

Why is this so? And how should I fix the code to make a better music player? Any advice on this will be greatly appreciated!!

Upvotes: 1

Views: 942

Answers (1)

Kingsley
Kingsley

Reputation: 14926

The problem is your code is blocking the TKinter main event loop as it loops through the audio tracks. Event driven programs cannot have their events blocked, this is why you're seeing the window "lock up" - no re-painting, sizing, closing, etc. The window code that controls these things never gets the messages, and is never able to act on them.

Your application code must never block this loop for more than a split-second.

So what can you do... use a timer or a thread to do your application's work. Timers are easy to use, they simply add a function call-out to a queue in the main loop. After X milliseconds, that function is called.

For example:

window = tk.Tk()

[...]

window.after( 1000, myFunction, arg1 )

Will cause the main event loop to call your myFunction( arg1 ) after 1000 milliseconds. Then inside myFunction, you can again queue a call to myFunction forming a periodic loop.

The PyGame mixer functions already play the music in a separate thread. So in your case, there's no real need to do anything except poll to see if the music has finished. All the "PLAY" code needs to do is start playing the next sound-file, and return.

In the main loop we can create a new timer-function to check to see if the sound-file is still playing, or whether it needs to queue the next one:

def checkPlaying( main_window ):
    global current_track

    if ( mixer.music.get_busy() == False ):
        # Playing has finished
        print("Sound finished, playing next" )
        track_index, track_name = current_track    
        current_track = ( track_index + 1, '' )     # Advance to next track
        press( 'PLAY' )                             # Start playing

    # Queue the next call to this function
    main_window.after( 250, checkPlaying, main_window )   # call again later

Since it re-queues a timer-call to itself, it needs that initial call in the main function:

# Start the Music Playing Check
app.after( 1000, checkPlaying, app )

The press() function needs to be modified to remove the continuous loop, just playing the current sound-file:

mixer.init()                 
current_track = ( 0, '' )    # track-index and name


def press(word):
    global track_names
    global current_track

    word = button_text.get()
    if word == 'PLAY':
        update_button_text('PLAY')
        track_index, track_name = current_track
        if ( track_index >= len( track_names ) ):  # if out of music, re-start
            track_index = 0

        # Start Playing the current track
        name = track_names[ track_index ]
        current_track = ( track_index, name )
        mixer.music.set_volume(100)
        mixer.music.load(f'C:/Users/user/Desktop/python/projects/etc/{name}.mp3')
        mixer.music.play()
        
        # Update the GUI
        window_2.delete(0, 'end')
        window_2.configure(state='normal')
        window_2.insert('end', name)

And that's it.

Reference Code:

from tkinter import *
import time, sys
from pygame import mixer

mixer.init()

track_names = [ 'car-horn2', 'car-horn', 'cash-register', 'dog-bark', 'duck-quack', 'rain-falling', 'single-ding', 'turkey-gobble' ]
current_track = ( 0, '' )


def press(word):
    global track_names
    global current_track

    word = button_text.get()
    if word == 'PLAY':
        update_button_text('PLAY')
        track_index, track_name = current_track
        if ( track_index >= len( track_names ) ):  # if out of music, re-start
            track_index = 0   

        # Play the current track
        name = track_names[ track_index ]
        current_track = ( track_index, name )
        mixer.music.set_volume(100)
        #mixer.music.load(f'C:/Users/user/Desktop/python/projects/etc/{name}.mp3')
        mixer.music.load(f'{name}.mp3')
        mixer.music.play()

        window_2.delete(0, 'end')
        window_2.configure(state='normal')
        window_2.insert('end', name)

    if word == 'PAUSE':
        update_button_text('PAUSE')
        mixer.music.pause()
        time.sleep(5)

    if word == 'STOP':
        mixer.music.stop()
        time.sleep(5)

    if word == 'NEXT':
        pass

    if word == 'PREVIOUS':
        pass


def checkPlaying( main_window ):
    global track_names
    global current_track

    result = False

    if ( mixer.music.get_busy() == True ):
        # Still playing
        result = True
    else:
        # Playing has finished
        # TODO: Change button states, whatever
        print("Sound finished, playing next" )
        track_index, track_name = current_track
        current_track = ( track_index + 1, '' )
        press( 'PLAY' )        # start next track
        result = False

    # Queue the next call to this function
    main_window.after( 250, checkPlaying, main_window )

    return result


def update_button_text(word):
    if word == 'PLAY':
        button_text.set('PAUSE')
    elif word == 'PAUSE':
        button_text.set('PLAY')


if __name__ == '__main__':
    # create application window
    app = Tk()

    # title
    app.title("Music Players")

    # geometry
    app.geometry('383x121')

    # background color
    app.configure(bg='orange')

    equation = StringVar()
    window_1 = Label(app, textvariable=equation)
    window_1.grid(columnspan=4, ipadx=100, ipady=10)
    equation.set('music player')

    window_2 = Entry(app, width=30)
    window_2.grid(columnspan=4, ipadx=100, ipady=10)
    window_2.configure(state='disabled')

    window_2.grid_columnconfigure((0, 1, 2), uniform="equal", weight=1)

    # Create buttons
    button_text = StringVar()
    button_text.set("PLAY")
    button1 = Button(app, textvariable=button_text, fg='yellow', bg='purple',
                     command=lambda: press(button_text), height=2, width=1)
    button1.grid(row=2, column=0, sticky="NSEW")

    button2 = Button(app, text='STOP', fg='yellow', bg='purple',
                     command=lambda: press('STOP'), height=2, width=1)
    button2.grid(row=2, column=1, sticky="NSEW")

    button3 = Button(app, text='NEXT', fg='yellow', bg='purple',
                     command=lambda: press('NEXT'), height=2, width=1)
    button3.grid(row=2, column=2, sticky="NSEW")

    button4 = Button(app, text='PREVIOUS', fg='yellow', bg='purple',
                     command=lambda: press('PREVIOUS'), height=2, width=1)
    button4.grid(row=2, column=3, sticky="NSEW")

    # Start the Music Playing Check
    app.after( 1000, checkPlaying, app )

# start the GUI
app.mainloop()

Upvotes: 1

Related Questions