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.
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.
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.
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.
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.
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 systempygame.mixer.Sound(buffer=...): creates a sound from a buffersound.play(): plays the soundsound.set_volume(v): sets volume (0.0 to 1.0)
Complete code
"""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
