Quick answer: Pygame has no built-in 3D audio. Compute left/right volume from listener-source vector, call channel.set_volume(left, right). For real 3D, use PyOpenAL.

A top-down shooter wants enemy footsteps to come from their direction. Pygame doesn’t auto-position; you manually compute panning.

Stereo Pan from Position

def play_3d(sound, source_pos, listener_pos):
    dx = source_pos[0] - listener_pos[0]
    dy = source_pos[1] - listener_pos[1]
    dist = (dx*dx + dy*dy) ** 0.5
    if dist == 0:
        left = right = 1.0
    else:
        pan = max(-1.0, min(1.0, dx / dist))
        attenuation = max(0, 1.0 - dist / 500)   # 500px falloff
        left = attenuation * (1.0 - max(0, pan))
        right = attenuation * (1.0 - max(0, -pan))

    ch = sound.play()
    ch.set_volume(left, right)

Pan -1 (full left) to +1 (full right). Distance scales overall volume.

Attenuation Curves

Linear (1 - d/max) is harsh. Try inverse square:

attenuation = 1.0 / (1.0 + (dist / 50) ** 2)

Smoother feel; mimics real-world physics.

Doppler / Pitch Shift

Pygame doesn’t support pitch shift natively. Pre-compute pitched variants with Audacity, swap based on velocity for racing-style Doppler.

For Full 3D

PyOpenAL wraps OpenAL Soft: HRTF, real 3D positioning, occlusion. Heavier than Pygame but professional results.

Verifying

Enemy footsteps audible from their direction. Far enemies quieter. Pan smoothly tracks as enemy moves around listener.

“Pygame gives you channels and volume. You build the 3D math.”

For immersive audio especially (horror, stealth), HRTF via PyOpenAL is worth the dependency — the gameplay feel is dramatically different.