Ce que vous allez apprendre

Ce chapitre ajoute une dimension essentielle a votre jeu : le son. Vous allez decouvrir comment generer des effets sonores de facon programmatique (sans fichier audio), comment les jouer lors des evenements du jeu, et comment offrir au joueur un controle du volume et un bouton muet.

Le concept

Pygame possede un module pygame.mixer dedie au son. Il gere deux types d'audio :

  • Sound (pygame.mixer.Sound) : courts effets sonores charges en memoire. Ils peuvent etre joues simultanement sur plusieurs canaux.
  • Music (pygame.mixer.music) : musique de fond lue en streaming depuis un fichier. Un seul morceau a la fois.

Pour eviter de dependre de fichiers audio externes, nous allons generer nos sons en creant directement des ondes sinusoidales. Le module array de Python permet de creer un buffer d'echantillons audio que Pygame peut lire directement.

Chaque son est defini par sa frequence (grave = basse frequence, aigu = haute frequence) et sa duree.

Etape par etape

1. Initialiser le mixer

On initialise le mixer avec des parametres specifiques. La frequence de 44100 Hz est le standard CD. On utilise un seul canal (mono) pour simplifier la generation de sons.

python
import array

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

size=-16 signifie des echantillons signes de 16 bits. buffer=512 reduit la latence (temps entre l'evenement et le son joue).

2. Generer un son par programmation

Notre fonction generate_beep cree une onde sinusoidale. On applique un fondu en sortie (fade-out) pour eviter les clics audibles a la fin du son.

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 est la valeur maximale d'un entier signe de 16 bits. Le type "h" dans array.array represente un short signe (2 octets). Chaque echantillon est une valeur entre -32768 et 32767.

3. Creer les effets sonores

On cree un son different pour chaque evenement du jeu. Les sons aigus sont pour les actions positives, les sons graves pour les actions negatives.

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. Jouer un son avec controle du volume

La fonction play_sound respecte le volume global et l'etat muet avant de jouer un son.

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. Controles du volume

On ajoute des touches pour controler le son : F1 pour couper/reactiver le son, +/- pour ajuster le 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)

Points cles a retenir

Astuce : pour jouer une musique de fond depuis un fichier, utilisez pygame.mixer.music.load("musique.ogg") puis pygame.mixer.music.play(-1) (-1 pour boucler).

Piege courant : n'appelez pas mixer.init() avant pygame.init(), sinon les parametres par defaut du mixer pourraient ne pas correspondre a vos sons generes.

Formats supportes : Pygame supporte WAV, OGG et (selon la version) MP3. Le format OGG est recommande pour la compatibilite.

Fonctions Pygame utilisees :

  • pygame.mixer.init() : initialise le systeme audio
  • pygame.mixer.Sound(buffer=...) : cree un son depuis un buffer
  • sound.play() : joue le son
  • sound.set_volume(v) : regle le volume (0.0 a 1.0)

Code complet

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()

Telechargement

Telecharger le code source de ce chapitre : brick-breaker-ch09.zip