Quick answer: Call pygame.mixer.set_num_channels(32) right after init. Reserve channel 0 for music. Use pygame.mixer.set_reserved(1) so the auto-allocator never steals it. Sound.play with maxtime or a Channel handle gives you precise control over which channel plays which sound.

Boss arena. Twenty enemies firing at once. Hits land but only six of them play sound. Or the music drops out for a second when an explosion fires. Pygame’s default eight mixer channels are not enough for any non-trivial 2D game.

The Symptom

Sounds get clipped, dropped, or simply don’t play in busy moments. The same hit cue plays fine in isolation. No error message; the mixer steals channels silently.

What Causes This

Pygame initializes the mixer with 8 channels. Sound.play() picks the first idle channel. If none are idle, it picks the oldest playing channel, stops it mid-play, and starts the new sound there. Once you have more than 8 simultaneous SFX, every new sound costs an old one.

The Fix

Step 1: Bump the channel count.

import pygame

pygame.init()
pygame.mixer.set_num_channels(32)   # default is 8

Do this immediately after pygame.init() (or specifically pygame.mixer.init() if you initialized subsystems individually). Existing channel handles invalidate when you change the count.

Step 2: Reserve channels for music. Music played via the Sound API (not pygame.mixer.music) should live on its own channel.

MUSIC_CHANNEL = 0

pygame.mixer.set_reserved(1)   # channel 0 is now off-limits to auto-alloc

music_channel = pygame.mixer.Channel(MUSIC_CHANNEL)
music_channel.play(music_sound, loops=-1)

Now Sound.play()—which uses the auto-allocator—won’t pick channel 0, so the explosion can’t silence the music.

Step 3: Prioritize critical sounds. For sounds that absolutely must play (player death, hit confirmations), grab a specific channel:

CRITICAL_CHANNEL = 1

pygame.mixer.set_reserved(2)   # reserve channels 0 and 1

def play_critical(snd):
    pygame.mixer.Channel(CRITICAL_CHANNEL).play(snd)

Critical sounds always preempt whatever was on channel 1. Non-critical sounds never use channel 1.

Manual Channel Assignment

For fine-grained control, ditch Sound.play() and pick the channel yourself:

def play_sfx(snd):
    ch = pygame.mixer.find_channel()
    if ch is None:
        ch = pygame.mixer.find_channel(force=True)
    ch.play(snd)

find_channel() returns the first idle channel or None if all busy. force=True picks the longest-running channel and steals it explicitly — same outcome as Sound.play but explicit.

Mixer Buffer and Latency

If sounds also feel laggy, the buffer size in pygame.mixer.init() is too large. 512 is responsive; 4096 is laggy but reduces underruns. Tune based on the slowest target hardware:

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

Verifying

Print pygame.mixer.get_busy() and get_num_channels() during the dense moment. Busy should plateau below the channel count; if it’s pegged at the limit, raise the count.

“Channels: 32. Reserved: 2 for music and critical. Hits land. Music holds.”

Related Issues

For pygame.mixer.music vs Sound, see music vs sound. For audio crackle, see buffer crackle.

More channels. Reserve music. Critical sounds get their own.