Session 6 of 6

Sing Along! 🎀

Display time-synced lyrics that scroll as the song plays β€” the finishing touch on your karaoke player.

🎯 By the end of this lesson you will be able to…
πŸ” Recall from Lesson 3 & 5

In Lesson 3 you parsed each lyrics file into a list like:
[(4, "[Intro]"), (79, "Well, oh, they might wear…"), (82, "Or knackered Converse…")]
In Lesson 5 you wrote an on_tick handler that fires every 500 ms with the current elapsed time. Now you will combine those two pieces.

Part 1 β€” Finding the right lyric line

Given that the song is at elapsed = 81 seconds, which lyric should be showing? We need to find the last entry in the list whose timestamp is less than or equal to 81:

lyrics = [
    (4,  "[Intro]"),
    (79, "Well, oh, they might wear classic Reeboks"),
    (82, "Or knackered Converse, or track bottoms stucked in socks"),
]

elapsed = 81

current = ""
for timestamp, text in lyrics:
    if timestamp <= elapsed:
        current = text   # keep updating β€” last one that fits wins

print(current)
# "Well, oh, they might wear classic Reeboks"
# (82 is too late β€” we are only at 81)

The trick is that we keep updating current as long as the timestamp fits. By the end of the loop, current holds the most recent lyric that has been reached.

Part 2 β€” The lookahead trick

Showing the lyric at the exact moment it is sung is a little too late β€” the viewer needs a second or two to read the words before singing them!

The solution is simple: pretend you are a few seconds further ahead when you search the list. We call this value the lookahead:

LOOKAHEAD = 2   # show each lyric 2 seconds before it is sung

current = ""
for timestamp, text in lyrics:
    if timestamp <= elapsed + LOOKAHEAD:
        current = text

With LOOKAHEAD = 2 and elapsed = 80, the search looks for entries up to second 82. That means the line at second 82 will already appear at second 80 β€” two seconds early β€” giving the singer time to read it.

🎀

Try changing LOOKAHEAD to 0, then to 4. Which feels most natural when you are singing along?

Part 3 β€” Wrapping it in a function

As always, if you are going to call the same logic more than once, put it in a function:

def get_current_lyric(lyrics, elapsed):
    """Return the lyric that should be showing at 'elapsed' seconds."""
    current = ""
    for timestamp, text in lyrics:
        if timestamp <= elapsed + LOOKAHEAD:
            current = text
    return current

Part 4 β€” Displaying the lyric on screen

show_lyrics(text) puts any string into the lyrics area on the Now Playing page. Call it inside your tick handler:

from pypod import on_tick, show_timestamp, show_progress, show_lyrics

LOOKAHEAD = 2

@on_tick
def update_display(elapsed):
    show_timestamp(elapsed)
    show_progress(elapsed, 400)

    if current_track is not None and current_track.lyrics:
        lyric = get_current_lyric(current_track.lyrics, elapsed)
        show_lyrics(lyric)

Part 5 β€” The finished main.py

Here is the complete, finished student code β€” the result of all six lessons. Every line should feel familiar now.

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


# ── Lesson 3: load timestamped lyrics ────────────────────────────
def load_lyrics(path):
    entries = []
    try:
        with open(path) as f:
            for line in f:
                parts = line.strip().split(None, 1)
                if len(parts) == 2:
                    minutes, seconds = parts[0].split(":")
                    total = int(minutes) * 60 + int(seconds)
                    entries.append((total, parts[1]))
    except Exception:
        pass
    return entries


# ── Lesson 2: parse filenames from the SD card ───────────────────
def parse_filename(filename):
    stem     = filename[:-4]
    position = stem.rfind("(")
    title    = stem[:position].strip()
    artist   = stem[position + 1 : -1].strip()
    return title, artist


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)


# ── Lesson 4: play music when a song is selected ─────────────────
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()


# ── Lessons 5 & 6: update display and lyrics every tick ──────────
LOOKAHEAD = 2

def get_current_lyric(lyrics, elapsed):
    current = ""
    for timestamp, text in lyrics:
        if timestamp <= elapsed + LOOKAHEAD:
            current = text
    return current

@on_tick
def update_display(elapsed):
    show_timestamp(elapsed)
    show_progress(elapsed, 400)
    if current_track is not None and current_track.lyrics:
        show_lyrics(get_current_lyric(current_track.lyrics, elapsed))


# ── Always last ──────────────────────────────────────────────────
start()
  1. Add LOOKAHEAD, get_current_lyric, and the show_lyrics call to your update_display function.
  2. Play a song that has a lyrics file β€” the lyrics area should now scroll as the song plays.
  3. Try adjusting LOOKAHEAD to 0 and 4 β€” notice the difference.
  4. Try playing a song with no lyrics file β€” the app should still work, just showing nothing in the lyrics area.
πŸ† Final challenge

πŸŽ‰ You have built a karaoke player!

Look back at what you have learned across six sessions:

πŸ“š Concepts covered in this project