Session 4 of 6

Making Music Play

Wire up the media controls so that tapping a song actually plays it.

🎯 By the end of this lesson you will be able to…

Part 1 β€” What is a callback?

Up until now every piece of code you have written has run immediately from top to bottom. But a music player needs to wait β€” wait for the user to tap a song, wait for the song to end, wait for the pause button to be pressed.

A callback is a function that you write, but that you hand to someone else to call later, when something specific happens.

πŸ“¦

Think of it like leaving a note for a delivery driver: "When the parcel arrives, ring the doorbell." You write the instruction, but the driver decides when to carry it out. Your function is the instruction; the device is the delivery driver.

Here is the simplest possible example to show the idea:

def say_hello():
    print("Hello!")

# We pass say_hello to something else β€” note: NO brackets after the name!
# We are handing over the function itself, not calling it right now.
on_song_selected(say_hello)
πŸ’‘ Brackets or no brackets?

say_hello() β€” with brackets β€” calls the function right now.
say_hello β€” without brackets β€” refers to the function as a value that can be passed around. When you register a callback you always use the no-brackets form.

Part 2 β€” Responding when a song is tapped

on_song_selected registers a function that will be called whenever the user taps a song in the list. Your function receives the Track object for the song they chose:

from pypod import on_song_selected, play

def handle_selection(track):
    play(track)

on_song_selected(handle_selection)

There is a neater way to write exactly the same thing, using the decorator syntax. Place @on_song_selected on the line directly above your function definition, and Python registers it for you automatically:

@on_song_selected
def handle_selection(track):
    play(track)
πŸ’‘ What does @ mean?

The @ symbol is just a shortcut. @on_song_selected above a function is exactly the same as writing on_song_selected(handle_selection) below it. Both styles work; the @ style just keeps things tidy.

Part 3 β€” The play, pause, and resume functions

Once a track is playing, you will want to be able to pause and resume it. The play/pause button on the Now Playing screen calls these functions automatically β€” but you can also call them yourself:

from pypod import play, pause, resume, is_playing

# Start playing a track
play(my_track)

# Pause (only does something if a track is playing)
pause()

# Resume (only does something if a track is paused)
resume()

# Check whether something is currently playing
if is_playing():
    print("Music is playing!")

Part 4 β€” Moving to the next song automatically

on_track_ended works in the same way as on_song_selected, except it fires when the current track finishes playing. Register a function there to move to the next track automatically:

from pypod import on_track_ended, next_track

@on_track_ended
def handle_end():
    next_track()       # play the next song in the list

Part 5 β€” Keeping track of the current song

In later lessons you will need to know which track is playing (to look up its lyrics, for example). Store it in a variable each time a new song starts:

current_track = None    # no song selected yet

@on_song_selected
def handle_selection(track):
    global current_track   # we are updating a variable outside this function
    current_track = track
    play(track)
πŸ’‘ What does global do?

Normally, any variable you create or change inside a function belongs only to that function and disappears when the function ends. Writing global current_track tells Python: "I mean the module-level variable, not a local copy". Without it, current_track would only update inside the function, and the rest of your code would never see the change.

Part 6 β€” Putting it all together

main.py
from pypod import (
    Track, add_track,
    play, next_track,
    on_song_selected, on_track_ended,
    list_music_files, music_path, lyrics_path,
    start,
)

# --- functions from previous lessons ---
def parse_filename(filename): ...
def load_lyrics(path): ...

# --- build the track list ---
for filename in list_music_files():
    title, artist = parse_filename(filename)
    track = Track(title=title, artist=artist,
                  file_path=music_path(filename),
                  lyrics=load_lyrics(lyrics_path(title)))
    add_track(track)

# --- callbacks ---
current_track = None

@on_song_selected
def handle_selection(track):
    global current_track
    current_track = track
    play(track)

@on_track_ended
def handle_end():
    next_track()

start()
  1. Add the current_track variable and the two callback functions to your main.py.
  2. Run the code, tap a song β€” it should start playing and switch to the Now Playing screen.
  3. Let the song finish β€” it should move to the next one automatically.
  4. Check that the play/pause button and the skip buttons on the Now Playing page all work.
πŸ† Challenge