Skip to main content

immersive-audio

Real-time 2D positional stereo mixer for Python games. Can handle hundreds simultanous sounds without jittering, lowpass filtering and volume falloff based on distance, pitch and timescale shifting for slow-mo and panning based on direction. Pure Python + numba; no compiled extension of its own.

Disclaimer

Files are decoded via soundfile (libsndfile), so any libsndfile-supported format works: WAV, FLAC, OGG/Vorbis, AIFF, and a few others. MP3 is not supported. Only .wav has been tested end-to-end; other formats should work but are unverified.

Install

pip install immersive-audio

System libraries required by the audio backends (not pip-installable):

  • PortAudio — backs PyAudio. Debian/Ubuntu: apt install portaudio19-dev; macOS: brew install portaudio. Windows wheels bundle it.
  • libsndfile — backs soundfile; bundled in the soundfile>=0.12 wheels on all three platforms.

Quickstart

import time
from immersiveaudio import AudioMixer

mixer = AudioMixer()
mixer.start()

mixer.play("audio/ui_click.wav")

src = mixer.play_positional("audio/explosion.wav", (500.0, 0.0), max_dist=3000.0)

for x in range(0, 3000, 100):
    mixer.set_listener((float(x), 0.0))
    time.sleep(0.05)

time.sleep(2.0)
mixer.stop_stream()

set_listener(pos) is push-based: call it every frame with the current camera/listener center. Positions are any length-2 iterable — tuple, list, ndarray, pygame.Vector2 all unpack.

Lazy loading

Files are not preloaded. The first play(path) / play_positional(path, ...) / load(path) decodes the file from disk once and caches the decoded PCM array in memory, keyed by path. Every later play of the same path skips disk I/O and decoding — it instantiates a fresh AudioSource from the cached buffer, so concurrent overlapping plays of one file are independent and cheap.

Consequences:

  • No warm-up call needed; first play of each asset pays a one-time decode cost (audible as a hitch only for large files on the audio thread's first touch — decode happens on your calling thread, not the callback, so it does not glitch playback).
  • The cache is unbounded and never evicted. For a fixed asset set this is ideal; if you stream thousands of distinct one-shot files, memory grows monotonically.
  • Pass an AudioSource instead of a path to bypass the cache entirely (e.g. procedurally generated buffers).

Positional model

2D only. Per source, per audio callback, against the nearest listener:

  • Volume = base_volume × volume_curve(dist, max_dist). Default volume_falloff(3.0)1 − (d/max)³, clamped to 0 at d ≥ max_dist.
  • Low-pass cutoff = lowpass_curve(dist, max_dist). Default lowpass_sweep() holds 20 kHz to max_dist/3, log-sweeps to 100 Hz by 2·max_dist/3, flat beyond. Pass lowpass_curve=None to disable.
  • Pan = clip(lateral_offset / pan_width, −1, 1), constant-power. Larger pan_width = less aggressive stereo spread; raise it for loud/wide sources.

Curves are plain callables f(dist, max_dist) -> float, evaluated in Python outside the JIT loop, so custom curves cost nothing structurally:

from immersiveaudio import volume_falloff, lowpass_sweep

mixer.play_positional(
    "audio/engine_hum.wav", (0.0, 0.0),
    max_dist=8000.0,
    loop=True,
    pan_width=4000.0,
    volume_curve=volume_falloff(1.5),
    lowpass_curve=lowpass_sweep(hi_hz=18000, lo_hz=300),
)

Split-screen

Pass every camera center; each source attenuates/pans against whichever listener is closest:

mixer.set_listeners([cam_a_center, cam_b_center])

Global controls

  • mixer.time_scale — playback-rate multiplier applied to all sources (0.3 = slow-mo, 1.0 = normal). Set per frame.
  • mixer.master_volume — linear gain into the mix.
  • mixer.limiter_target — The target audio volume to compress to.
  • Per-source pitch — playback-rate multiplier stacked on time_scale. A random value between 0.9-1.1 produces a nice effect.

API

Call Effect
mixer = AudioMixer(sample_rate=44100, chunk_size=1024, master_volume=1.0, limiter_target=0.7, limiter_attack_ms=5.0, limiter_release_ms=300.0, reverb_feedback=0.84, reverb_damping=0.35, reverb_wet=0.7) Construct; primes JIT kernels.
mixer.start() / mixer.stop_stream() Open / close the PortAudio stream.
mixer.set_listener(pos) / mixer.set_listeners(list) Set listener(s); call every frame.
audioSource = mixer.play(audio, *, volume=1, pan=0, loop=False, pitch=1) Non-positional playback.
audioSource = mixer.play_positional(audio, pos, *, max_dist=6000, volume=1, loop=False, pitch=1, pan_width=2000, volume_curve=…, lowpass_curve=…) Positional playback.
audioSource.set_pos(new_pos) Sets a new position for an already playing audio source.
audioSource = mixer.load(path) Decode + cache without playing. Eliminates the small downtime of playing a sound for the first time but to little gain.
mixer.stop(source) / mixer.stop_all() Remove source(s) from the mix.
mixer.set_chunk_size(n) Reopen the stream with a new buffer size.

| audio is a path (cached) or an AudioSource (used directly).

Threading

Playback runs in the PortAudio callback thread. Your game thread calls set_listener, play*, stop* freely; source-list mutations are lock-guarded with O(1) critical sections, so the callback never blocks meaningfully.

Requirements

Python ≥ 3.10 · numpy ≥ 1.24 · numba ≥ 0.60 · soundfile ≥ 0.12 · PyAudio ≥ 0.2.12

numba is the binding constraint on the numpy version; it caps numpy via its own metadata, so no numpy upper pin is declared here.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

immersive_audio-0.1.2.tar.gz (586.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

immersive_audio-0.1.2-py3-none-any.whl (11.3 kB view details)

Uploaded Python 3

File details

Details for the file immersive_audio-0.1.2.tar.gz.

File metadata

  • Download URL: immersive_audio-0.1.2.tar.gz
  • Upload date:
  • Size: 586.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.12

File hashes

Hashes for immersive_audio-0.1.2.tar.gz
Algorithm Hash digest
SHA256 1e250f737dff687a0405c06ffdc69af4ed26081a1b9131585009f3696f47d6e5
MD5 96428da4acbe3976680b9e3daa856bdd
BLAKE2b-256 77a271ccd32e42ef17715b4fea66cac401a1e4e30dd4f3fd05ee16f51afd8bc8

See more details on using hashes here.

File details

Details for the file immersive_audio-0.1.2-py3-none-any.whl.

File metadata

File hashes

Hashes for immersive_audio-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 db88876f7428067408c50861306e8429887d454573da9c619500bb019b5c1d38
MD5 86e006800656018e4d3f3e6aa7550d92
BLAKE2b-256 3725c19659975d3782becf31914a099a1b64043d07183bddd61383fab8d94ac1

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page