Learning Goals
3 minBy the end of this lesson you can:
- Place a music file in the
music/folder and loop it withmusic.play("track"). - Adjust playback volume with
music.set_volume()and mute/unmute on a key press. - Fade the music out smoothly with
music.fadeout(seconds)when the game ends.
Warm-Up · Sound Effects Recap
5 minLast lesson you played one-shot sound effects with sounds.name.play(). What does this snippet do? Predict before reading on:
def on_mouse_down(pos, button): if score_rect.collidepoint(pos): sounds.chime.play() else: sounds.thud.play()
Show the answer
If the click lands inside score_rect, a chime plays. Any other click plays a thud. The else is the catch-all. Music is different — it keeps playing by itself in the background without needing an event.
New Concept · The music Object
12 minThink of music as a CD player built into Pygame Zero. You press play once — it loops forever on its own. You turn the volume knob. You press stop or fade. That is all there is to it.
Folder layout
Music files live in a music/ folder next to your Python file:
my_game/
├── mygame.py
└── music/
├── theme.ogg
└── gameover.oggPygame Zero supports .ogg and .mp3 for music (unlike sounds, MP3 works here). Use .ogg when possible — it is smaller and plays reliably everywhere.
Starting and stopping
# Start looping the theme track immediately music.play("theme") # Stop it instantly music.stop() # Fade out over 3 seconds (nice for game-over screens) music.fadeout(3)
Pass the filename without the extension. "theme" plays music/theme.ogg (or music/theme.mp3).
Volume control
# 0.0 = silent, 1.0 = full volume music.set_volume(0.4) # comfortable background level
Call set_volume any time — inside update(), in an event hook, or at the start of the file. Volume persists until you change it again.
Worked Example · Mute Button
12 minMei Ling wants background music that the player can toggle on and off by pressing M. Save as music_demo.py:
# music_demo.py — looping music with mute toggle import pgzrun WIDTH = 480 HEIGHT = 300 TITLE = "Music Demo" is_muted = False def on_key_down(key): global is_muted if key == keys.M: is_muted = not is_muted if is_muted: music.set_volume(0.0) else: music.set_volume(0.5) def draw(): screen.fill("midnightblue") label = "Music: OFF (press M)" if is_muted else "Music: ON (press M)" screen.draw.text(label, center=(240, 150), fontsize=28, color="white") music.play("theme") # music/theme.ogg loops automatically music.set_volume(0.5) pgzrun.go()
The global keyword lets the key handler write to is_muted which lives outside the function. The label updates each frame so the player always knows the current state.
Try It Yourself
13 minAdd two more key bindings: pressing Up increases the volume by 0.1 (max 1.0), pressing Down decreases it by 0.1 (min 0.0). Display the current volume on screen.
Hint
volume = 0.5 def on_key_down(key): global volume if key == keys.UP: volume = min(1.0, volume + 0.1) music.set_volume(volume) elif key == keys.DOWN: volume = max(0.0, volume - 0.1) music.set_volume(volume)
When the player presses G (simulating a game-over), call music.fadeout(2) and display "Game Over!" in red in the centre of the screen.
Hint
game_over = False def on_key_down(key): global game_over if key == keys.G: game_over = True music.fadeout(2) def draw(): screen.fill("black") if game_over: screen.draw.text("Game Over!", center=(240, 150), fontsize=40, color="red")
Mini-Challenge · Debug Arjun's Track Switch
8 minArjun wants the game to play a calm theme at the start and switch to an intense track when the player presses SPACE. His music never switches. Find the bug:
# arjun_music.py — buggy
import pgzrun
WIDTH = 400
HEIGHT = 300
intense = False
def on_key_down(key):
if key == keys.SPACE:
intense = True # Bug is here
music.play("intense")
def draw():
screen.fill("black")
label = "INTENSE" if intense else "CALM"
screen.draw.text(label, center=(200, 150), fontsize=36, color="white")
music.play("calm")
pgzrun.go()Show the fix
# arjun_music.py — fixed import pgzrun WIDTH = 400 HEIGHT = 300 intense = False def on_key_down(key): global intense # Fix: declare intense as global so the function can write to it if key == keys.SPACE: intense = True music.play("intense") def draw(): screen.fill("black") label = "INTENSE" if intense else "CALM" screen.draw.text(label, center=(200, 150), fontsize=36, color="white") music.play("calm") pgzrun.go()
Without global intense, the assignment intense = True creates a local variable that vanishes when the function ends. The outer intense stays False, so the label never updates.
It works if…
pressing SPACE changes the label to INTENSE and the music track switches
Recap
3 minPlace an .ogg or .mp3 file in the music/ folder. Call music.play("filename") once — Pygame Zero loops it forever. Use music.set_volume(0.0–1.0) to adjust loudness, andmusic.fadeout(seconds) for a smooth ending. Only one track plays at a time.
Vocabulary Card
- music.play("name")
- Starts looping a file from
music/. Replaces any currently playing track. - music.set_volume(v)
- Sets volume from 0.0 (silent) to 1.0 (full). Can be called at any time.
- music.fadeout(s)
- Smoothly fades the music out over s seconds, then stops.
- music.stop()
- Stops playback immediately with no fade.
Homework
4 minFind a short loopable music track on freesound.org(search "loop" or "ambient"). Download it as .ogg and save it as music/theme.ogg. Write a program that displays the game title and the current volume level on screen. The player pressesM to mute and Up/Down arrow keys to change volume in steps of 0.1. Save as music_controls.py and bring a screenshot to the next class.
Sample · music_controls.py
# music_controls.py — volume control demo import pgzrun WIDTH = 480 HEIGHT = 300 TITLE = "Music Controls" volume = 0.5 muted = False def on_key_down(key): global volume, muted if key == keys.M: muted = not muted music.set_volume(0.0 if muted else volume) elif key == keys.UP: volume = min(1.0, round(volume + 0.1, 1)) if not muted: music.set_volume(volume) elif key == keys.DOWN: volume = max(0.0, round(volume - 0.1, 1)) if not muted: music.set_volume(volume) def draw(): screen.fill("black") screen.draw.text("My Awesome Game", center=(240, 100), fontsize=36, color="gold") status = f"Volume: {volume:.1f} {'[MUTED]' if muted else ''}" screen.draw.text(status, center=(240, 180), fontsize=22, color="white") music.play("theme") music.set_volume(volume) pgzrun.go()
Using round(..., 1) avoids floating-point drift like 0.30000000000000004. Your track name and colours are your own.