Code of the Day
IntermediatePolish and Persistence

Sound in practice

Load and play sound effects with pygame.mixer.Sound, control per-sound volume, and stream background music with pygame.mixer.music.

Game DevIntermediate8 min read
Recommended first
By the end of this lesson you will be able to:
  • Load a Sound object and call play() at the right trigger point
  • Adjust per-sound volume with set_volume()
  • Load, loop, and volume-control background music with pygame.mixer.music

This lesson shows the complete audio API as working code. Because real .wav files are not available in every project, the code uses pygame.sndarray to generate a short sine-wave tone programmatically — this lets you hear audio working without needing external assets.

Pyodide (the in-browser Python runner) does not support pygame's display or mixer system. Read through this code carefully in the browser, then run it locally: pip install pygame numpy followed by python sound_demo.py.

The code

import pygame
import sys
import numpy as np

pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)

SCREEN_W, SCREEN_H = 640, 480
screen = pygame.display.set_mode((SCREEN_W, SCREEN_H))
pygame.display.set_caption("Sound demo")
clock  = pygame.time.Clock()
font   = pygame.font.SysFont(None, 30)


# ── Generate a simple tone as a Sound object ──────────────────────────────────
# In a real project, replace this with:
#   jump_sfx = pygame.mixer.Sound("assets/jump.wav")

def make_tone(frequency=440, duration=0.15, volume=0.4, sample_rate=44100):
    """Return a pygame.mixer.Sound containing a short sine-wave tone."""
    n_samples = int(sample_rate * duration)
    t         = np.linspace(0, duration, n_samples, endpoint=False)
    wave      = (np.sin(2 * np.pi * frequency * t) * 32767 * volume).astype(np.int16)
    sound     = pygame.sndarray.make_sound(wave)
    return sound

jump_sfx = make_tone(frequency=520, duration=0.12)   # higher pitch = jump
coin_sfx = make_tone(frequency=880, duration=0.08)   # bright ding = coin
hit_sfx  = make_tone(frequency=220, duration=0.18)   # low thud = hit

# Volume is 0.0 to 1.0.  Set it once after loading.
jump_sfx.set_volume(0.6)
coin_sfx.set_volume(0.8)
hit_sfx.set_volume(0.5)


# ── Background music ──────────────────────────────────────────────────────────
# In a real project:
#   pygame.mixer.music.load("assets/music.ogg")
#   pygame.mixer.music.set_volume(0.4)
#   pygame.mixer.music.play(-1)     # -1 = loop forever
#
# Skipped here because we have no music file, but the API is three lines.


# ── Simple demo: press keys to trigger SFX ───────────────────────────────────

log = []    # last few triggered events, shown on screen

running = True
while running:
    events = pygame.event.get()
    for event in events:
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                jump_sfx.play()
                log.append("Jump SFX played")
            if event.key == pygame.K_c:
                coin_sfx.play()
                log.append("Coin SFX played")
            if event.key == pygame.K_h:
                hit_sfx.play()
                log.append("Hit SFX played")

    # Keep last 5 messages
    log = log[-5:]

    screen.fill((20, 20, 35))
    screen.blit(font.render(
        "SPACE = jump   C = coin   H = hit", True, (200, 200, 200)), (10, 10))
    for i, msg in enumerate(log):
        screen.blit(font.render(msg, True, (255, 255, 180)), (10, 50 + i * 26))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()
sys.exit()

How each part works

pygame.mixer.init(frequency, size, channels, buffer) configures the mixer before you load any sounds. frequency=44100 is CD quality; size=-16 is 16-bit signed samples; channels=1 is mono (use 2 for stereo); buffer=512 reduces latency. Call this once, before any Sound or music calls.

pygame.mixer.Sound(path) loads the file into memory. The returned object is reusable — calling .play() multiple times creates independent playback instances, so overlapping effects work without extra code.

.set_volume(n) takes a float between 0.0 and 1.0. Set it after loading; it persists until changed. You can also call .set_volume() on each individual .play() return value to control that specific instance.

pygame.mixer.music.load(path) prepares a file for streaming. Unlike Sound.load(), only one music file can be loaded at a time. play(-1) loops indefinitely; play(0) plays once; play(3) loops three additional times after the first play.

When transitioning between scenes, call pygame.mixer.music.stop() before loading the next track. Failing to stop the previous music before loading a new file can cause a brief audio glitch on some platforms.

Where to go next

Next: saving game state — deciding what is worth persisting between sessions and preparing your data for serialisation.

Finished reading? Mark it complete to track your progress.

On this page