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.