What you will learn

This chapter adds an essential dimension to your game: sound. You will discover how to generate sound effects programmatically (without audio files), how to play them on game events, and how to give the player volume control and a mute button.

The concept

Pygame has a dedicated pygame.mixer module for sound. It handles two types of audio:

  • Sound (pygame.mixer.Sound): short sound effects loaded into memory. They can be played simultaneously on multiple channels.
  • Music (pygame.mixer.music): background music streamed from a file. Only one track at a time.

To avoid depending on external audio files, we will generate our sounds by directly creating sine waves. Python's array module lets us create a buffer of audio samples that Pygame can read directly.

Each sound is defined by its frequency (low = bass, high = treble) and its duration.

Step by step

1. Initializing the mixer

We initialize the mixer with specific parameters. A frequency of 44100 Hz is the CD standard. We use a single channel (mono) to simplify sound generation.

python
import array

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

size=-16 means signed 16-bit samples. buffer=512 reduces latency (time between the event and the sound being played).

2. Generating a sound programmatically

Our generate_beep function creates a sine wave. We apply a fade-out to avoid audible clicks at the end of the sound.

python
def generate_beep(frequency=440, duration_ms=100, volume=0.3):
    """Generate a simple sine wave beep sound."""
    sample_rate = 44100
    n_samples = int(sample_rate * duration_ms / 1000)
    buf = array.array("h", [0] * n_samples)
    max_amp = int(32767 * volume)
    for i in range(n_samples):
        t = i / sample_rate
        value = int(max_amp * math.sin(
            2 * math.pi * frequency * t))
        # Apply a short fade-out to avoid clicks
        fade_samples = min(500, n_samples // 4)
        if i >= n_samples - fade_samples:
            value = int(value * (n_samples - i) / fade_samples)
        buf[i] = value
    sound = pygame.mixer.Sound(buffer=buf)
    return sound

32767 is the maximum value for a signed 16-bit integer. The type "h" in array.array represents a signed short (2 bytes). Each sample is a value between -32768 and 32767.

3. Creating sound effects

We create a different sound for each game event. High-pitched sounds are for positive actions, low-pitched sounds for negative ones.

python
snd_paddle = generate_beep(frequency=500, duration_ms=60, volume=0.3)
snd_wall = generate_beep(frequency=300, duration_ms=50, volume=0.2)
snd_brick = generate_beep(frequency=700, duration_ms=80, volume=0.3)
snd_lose_life = generate_beep(frequency=150, duration_ms=300, volume=0.4)
snd_game_over = generate_beep(frequency=100, duration_ms=500, volume=0.5)
snd_win = generate_beep(frequency=880, duration_ms=400, volume=0.4)

4. Playing a sound with volume control

The play_sound function respects the global volume and mute state before playing a sound.

python
def play_sound(sound):
    """Play a sound respecting volume and mute settings."""
    if not sound_muted:
        sound.set_volume(master_volume)
        sound.play()

5. Volume controls

We add keys to control sound: F1 to mute/unmute, +/- to adjust volume.

python
    if event.key == pygame.K_F1:
        sound_muted = not sound_muted
    if (event.key == pygame.K_PLUS
            or event.key == pygame.K_KP_PLUS
            or event.key == pygame.K_EQUALS):
        master_volume = min(1.0, master_volume + 0.1)
    if (event.key == pygame.K_MINUS
            or event.key == pygame.K_KP_MINUS):
        master_volume = max(0.0, master_volume - 0.1)

Key takeaways

Tip: to play background music from a file, use pygame.mixer.music.load("music.ogg") then pygame.mixer.music.play(-1) (-1 to loop).

Common pitfall: do not call mixer.init() before pygame.init(), otherwise the default mixer parameters may not match your generated sounds.

Supported formats: Pygame supports WAV, OGG, and (depending on the version) MP3. OGG is recommended for compatibility.

Pygame functions used:

  • pygame.mixer.init(): initializes the audio system
  • pygame.mixer.Sound(buffer=...): creates a sound from a buffer
  • sound.play(): plays the sound
  • sound.set_volume(v): sets volume (0.0 to 1.0)

Complete code

python
"""Brick Breaker - Chapter 9: Sound and Music"""
import pygame
import sys
import math
import array

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

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60
BG_COLOR = (20, 20, 40)

PADDLE_WIDTH = 100
PADDLE_HEIGHT = 15
PADDLE_SPEED = 8
paddle_x = SCREEN_WIDTH // 2 - PADDLE_WIDTH // 2
paddle_y = SCREEN_HEIGHT - 40

BALL_RADIUS = 8
BALL_BASE_SPEED = 300
BALL_SPEED_INCREMENT = 10

BRICK_ROWS = 6
BRICK_COLS = 10
BRICK_WIDTH = 70
BRICK_HEIGHT = 20
BRICK_PADDING = 5
BRICK_OFFSET_X = 30
BRICK_OFFSET_Y = 50

MAX_LIVES = 3
lives = MAX_LIVES
score = 0
level = 1

STATE_PLAYING = 0
STATE_GAME_OVER = 1
STATE_YOU_WIN = 2
game_state = STATE_PLAYING

master_volume = 0.5
sound_muted = False


def generate_beep(frequency=440, duration_ms=100, volume=0.3):
    """Generate a simple sine wave beep sound."""
    sample_rate = 44100
    n_samples = int(sample_rate * duration_ms / 1000)
    buf = array.array("h", [0] * n_samples)
    max_amp = int(32767 * volume)
    for i in range(n_samples):
        t = i / sample_rate
        value = int(max_amp * math.sin(2 * math.pi * frequency * t))
        # Apply a short fade-out to avoid clicks
        fade_samples = min(500, n_samples // 4)
        if i >= n_samples - fade_samples:
            value = int(value * (n_samples - i) / fade_samples)
        buf[i] = value
    sound = pygame.mixer.Sound(buffer=buf)
    return sound


# Generate sound effects
snd_paddle = generate_beep(frequency=500, duration_ms=60, volume=0.3)
snd_wall = generate_beep(frequency=300, duration_ms=50, volume=0.2)
snd_brick = generate_beep(frequency=700, duration_ms=80, volume=0.3)
snd_lose_life = generate_beep(frequency=150, duration_ms=300, volume=0.4)
snd_game_over = generate_beep(frequency=100, duration_ms=500, volume=0.5)
snd_win = generate_beep(frequency=880, duration_ms=400, volume=0.4)


def play_sound(sound):
    """Play a sound respecting volume and mute settings."""
    if not sound_muted:
        sound.set_volume(master_volume)
        sound.play()


def rainbow_color(row, total_rows):
    hue = int(360 * row / total_rows)
    color = pygame.Color(0)
    color.hsva = (hue, 100, 100, 100)
    return (color.r, color.g, color.b)


def create_paddle_image(width, height):
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    for y in range(height):
        brightness = 200 + int(55 * (1 - y / height))
        pygame.draw.line(surf, (brightness, brightness, brightness),
                         (0, y), (width, y))
    pygame.draw.rect(surf, (0, 0, 0, 0), (0, 0, 3, 3))
    pygame.draw.rect(surf, (0, 0, 0, 0), (width - 3, 0, 3, 3))
    return surf.convert_alpha()


def create_ball_image(radius):
    size = radius * 4
    surf = pygame.Surface((size, size), pygame.SRCALPHA)
    for r in range(radius * 2, radius, -1):
        alpha = int(100 * (1 - (r - radius) / radius))
        pygame.draw.circle(surf, (100, 100, 255, alpha),
                           (size // 2, size // 2), r)
    pygame.draw.circle(surf, (255, 255, 255),
                       (size // 2, size // 2), radius)
    pygame.draw.circle(surf, (255, 255, 255, 200),
                       (size // 2 - 2, size // 2 - 2), radius // 3)
    return surf.convert_alpha()


def create_brick_image(width, height, color):
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    pygame.draw.rect(surf, color, (0, 0, width, height))
    highlight = tuple(min(255, c + 50) for c in color)
    pygame.draw.line(surf, highlight, (1, 1), (width - 2, 1))
    pygame.draw.line(surf, highlight, (1, 1), (1, height - 2))
    shadow = tuple(max(0, c - 50) for c in color)
    pygame.draw.line(surf, shadow, (1, height - 1), (width - 1, height - 1))
    pygame.draw.line(surf, shadow, (width - 1, 1), (width - 1, height - 1))
    return surf.convert_alpha()


def create_bricks():
    bricks = []
    for row in range(BRICK_ROWS):
        color = rainbow_color(row, BRICK_ROWS)
        image = create_brick_image(BRICK_WIDTH, BRICK_HEIGHT, color)
        for col in range(BRICK_COLS):
            x = BRICK_OFFSET_X + col * (BRICK_WIDTH + BRICK_PADDING)
            y = BRICK_OFFSET_Y + row * (BRICK_HEIGHT + BRICK_PADDING)
            brick = {
                "rect": pygame.Rect(x, y, BRICK_WIDTH, BRICK_HEIGHT),
                "color": color,
                "image": image,
                "row": row,
            }
            bricks.append(brick)
    return bricks


def reset_ball():
    global ball_x, ball_y, ball_vx, ball_vy, ball_speed
    ball_x = float(SCREEN_WIDTH // 2)
    ball_y = float(SCREEN_HEIGHT // 2)
    ball_speed = BALL_BASE_SPEED
    ball_vx = ball_speed * math.cos(math.radians(-60))
    ball_vy = ball_speed * math.sin(math.radians(-60))


def score_for_row(row):
    return (BRICK_ROWS - row) * 10


screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 9")
clock = pygame.time.Clock()

hud_font = pygame.font.SysFont("monospace", 18)
big_font = pygame.font.SysFont("monospace", 48, bold=True)
medium_font = pygame.font.SysFont("monospace", 24)
small_font = pygame.font.SysFont("monospace", 14)

paddle_img = create_paddle_image(PADDLE_WIDTH, PADDLE_HEIGHT)
ball_img = create_ball_image(BALL_RADIUS)

bricks = create_bricks()

ball_x = float(SCREEN_WIDTH // 2)
ball_y = float(SCREEN_HEIGHT // 2)
ball_speed = BALL_BASE_SPEED
ball_vx = ball_speed * math.cos(math.radians(-60))
ball_vy = ball_speed * math.sin(math.radians(-60))

use_mouse = False

running = True
while running:
    dt = clock.tick(FPS) / 1000.0

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
            if event.key == pygame.K_m:
                if game_state == STATE_PLAYING:
                    use_mouse = not use_mouse
                else:
                    sound_muted = not sound_muted
            if event.key == pygame.K_F1:
                sound_muted = not sound_muted
            if (event.key == pygame.K_PLUS
                    or event.key == pygame.K_KP_PLUS
                    or event.key == pygame.K_EQUALS):
                master_volume = min(1.0, master_volume + 0.1)
            if (event.key == pygame.K_MINUS
                    or event.key == pygame.K_KP_MINUS):
                master_volume = max(0.0, master_volume - 0.1)
            if event.key == pygame.K_r and game_state != STATE_PLAYING:
                lives = MAX_LIVES
                score = 0
                level = 1
                bricks = create_bricks()
                reset_ball()
                game_state = STATE_PLAYING

    if game_state == STATE_PLAYING:
        if use_mouse:
            mx, _ = pygame.mouse.get_pos()
            paddle_x = mx - PADDLE_WIDTH // 2
        else:
            keys = pygame.key.get_pressed()
            if keys[pygame.K_LEFT] or keys[pygame.K_a]:
                paddle_x -= PADDLE_SPEED
            if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
                paddle_x += PADDLE_SPEED

        paddle_x = max(0, min(paddle_x, SCREEN_WIDTH - PADDLE_WIDTH))

        ball_x += ball_vx * dt
        ball_y += ball_vy * dt

        if ball_x - BALL_RADIUS <= 0:
            ball_x = BALL_RADIUS
            ball_vx = abs(ball_vx)
            ball_speed += BALL_SPEED_INCREMENT
            play_sound(snd_wall)

        if ball_x + BALL_RADIUS >= SCREEN_WIDTH:
            ball_x = SCREEN_WIDTH - BALL_RADIUS
            ball_vx = -abs(ball_vx)
            ball_speed += BALL_SPEED_INCREMENT
            play_sound(snd_wall)

        if ball_y - BALL_RADIUS <= 0:
            ball_y = BALL_RADIUS
            ball_vy = abs(ball_vy)
            ball_speed += BALL_SPEED_INCREMENT
            play_sound(snd_wall)

        if ball_y - BALL_RADIUS > SCREEN_HEIGHT:
            lives -= 1
            if lives <= 0:
                game_state = STATE_GAME_OVER
                play_sound(snd_game_over)
            else:
                play_sound(snd_lose_life)
                reset_ball()

        paddle_rect = pygame.Rect(paddle_x, paddle_y,
                                  PADDLE_WIDTH, PADDLE_HEIGHT)
        ball_rect = pygame.Rect(ball_x - BALL_RADIUS,
                                ball_y - BALL_RADIUS,
                                BALL_RADIUS * 2, BALL_RADIUS * 2)

        if ball_rect.colliderect(paddle_rect) and ball_vy > 0:
            ball_y = paddle_y - BALL_RADIUS
            hit_pos = (ball_x - paddle_x) / PADDLE_WIDTH
            angle = 150 - hit_pos * 120
            rad = math.radians(angle)
            ball_speed += BALL_SPEED_INCREMENT
            ball_vx = ball_speed * math.cos(rad)
            ball_vy = -abs(ball_speed * math.sin(rad))
            play_sound(snd_paddle)

        for brick in bricks[:]:
            if ball_rect.colliderect(brick["rect"]):
                score += score_for_row(brick["row"])
                bricks.remove(brick)
                br = brick["rect"]
                if ball_x >= br.left and ball_x <= br.right:
                    ball_vy = -ball_vy
                else:
                    ball_vx = -ball_vx
                ball_speed += BALL_SPEED_INCREMENT
                play_sound(snd_brick)
                break

        if len(bricks) == 0:
            game_state = STATE_YOU_WIN
            play_sound(snd_win)

    screen.fill(BG_COLOR)

    for brick in bricks:
        screen.blit(brick["image"], brick["rect"])

    screen.blit(paddle_img, (paddle_x, paddle_y))

    ball_offset = BALL_RADIUS * 2
    screen.blit(ball_img, (ball_x - ball_offset, ball_y - ball_offset))

    score_surf = hud_font.render(f"Score: {score}", True, (255, 255, 100))
    screen.blit(score_surf, (10, 10))

    lives_surf = hud_font.render(f"Lives: {lives}", True, (255, 100, 100))
    screen.blit(lives_surf,
                (SCREEN_WIDTH // 2 - lives_surf.get_width() // 2, 10))

    level_surf = hud_font.render(f"Level: {level}", True, (100, 200, 255))
    screen.blit(level_surf,
                (SCREEN_WIDTH - level_surf.get_width() - 10, 10))

    vol_label = "MUTED" if sound_muted else f"Vol: {int(master_volume * 100)}"
    vol_color = (255, 80, 80) if sound_muted else (150, 150, 150)
    vol_surf = small_font.render(vol_label, True, vol_color)
    screen.blit(vol_surf,
                (SCREEN_WIDTH - vol_surf.get_width() - 10, 30))

    if game_state == STATE_GAME_OVER:
        overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT),
                                 pygame.SRCALPHA)
        overlay.fill((0, 0, 0, 150))
        screen.blit(overlay, (0, 0))
        go_text = big_font.render("GAME OVER", True, (255, 60, 60))
        screen.blit(go_text,
                    (SCREEN_WIDTH // 2 - go_text.get_width() // 2,
                     SCREEN_HEIGHT // 2 - 40))
        final_score = medium_font.render(f"Final score: {score}",
                                         True, (255, 255, 255))
        screen.blit(final_score,
                    (SCREEN_WIDTH // 2 - final_score.get_width() // 2,
                     SCREEN_HEIGHT // 2 + 20))
        restart = medium_font.render("Press R to restart",
                                     True, (200, 200, 200))
        screen.blit(restart,
                    (SCREEN_WIDTH // 2 - restart.get_width() // 2,
                     SCREEN_HEIGHT // 2 + 60))

    elif game_state == STATE_YOU_WIN:
        overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT),
                                 pygame.SRCALPHA)
        overlay.fill((0, 0, 0, 150))
        screen.blit(overlay, (0, 0))
        win_text = big_font.render("YOU WIN!", True, (100, 255, 100))
        screen.blit(win_text,
                    (SCREEN_WIDTH // 2 - win_text.get_width() // 2,
                     SCREEN_HEIGHT // 2 - 40))
        final_score = medium_font.render(f"Final score: {score}",
                                         True, (255, 255, 255))
        screen.blit(final_score,
                    (SCREEN_WIDTH // 2 - final_score.get_width() // 2,
                     SCREEN_HEIGHT // 2 + 20))
        restart = medium_font.render("Press R to restart",
                                     True, (200, 200, 200))
        screen.blit(restart,
                    (SCREEN_WIDTH // 2 - restart.get_width() // 2,
                     SCREEN_HEIGHT // 2 + 60))

    info = small_font.render(
        "[M] Mouse/Kbd  [F1] Mute  [+/-] Volume  [R] Restart  [ESC] Quit",
        True, (150, 150, 150))
    screen.blit(info, (10, SCREEN_HEIGHT - 25))

    pygame.display.flip()

pygame.quit()
sys.exit()

Download

Download the source code for this chapter: brick-breaker-ch09.zip