Skip to main content

Spatial audio engine for Python

Project description

immersive-audio

Real-time 2D positional stereo mixer for Python games. Can handle 50-100 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.

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 — peak ceiling for the output limiter (default 0.7); set None to disable.
  • 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
AudioMixer(sample_rate=44100, chunk_size=1024, master_volume=1.0, limiter_target=0.7) Construct; primes JIT kernels.
start() / stop_stream() Open / close the PortAudio stream.
set_listener(pos) / set_listeners(list) Set listener(s); call every frame.
play(audio, *, volume=1, pan=0, loop=False, pitch=1) Non-positional playback.
play_positional(audio, pos, *, max_dist=6000, volume=1, loop=False, pitch=1, pan_width=2000, volume_curve=…, lowpass_curve=…) Positional playback.
load(path) -> AudioSource Decode + cache without playing.
stop(source) / stop_all() Remove source(s) from the mix.
set_chunk_size(n) Reopen the stream with a new buffer size.

audio is a path (cached) or an AudioSource (used directly). All play methods return the AudioSource.

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

Project details


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.1.tar.gz (433.2 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.1-py3-none-any.whl (8.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: immersive_audio-0.1.1.tar.gz
  • Upload date:
  • Size: 433.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.13

File hashes

Hashes for immersive_audio-0.1.1.tar.gz
Algorithm Hash digest
SHA256 ece509eaa5732a4f3779953a3828a7bcfde0424c26b821cba58cfc5424aad4de
MD5 932bece7c019baa6626f6f172ffc9d59
BLAKE2b-256 1a999b034b1895d720badf51c4fe6f9e0f2da9d847f61c4d718df13f4c909e4c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for immersive_audio-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3d27dfc53de5cd88ef7c5177fc87db2cb5611d3328844951b40f462ec674e47e
MD5 8d667d7a32e3549f3c06c53527a84fed
BLAKE2b-256 0b0b76108eeec54164424d6bc2f2d5ee1d4bce7ecee912ff78e4eaf9b030ed64

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page