Playback and spatial audio¶
The convenience runtime¶
play() opens its default audio session lazily. It reuses clips
loaded from the same resolved path and releases the session automatically at
process exit. Call shutdown() when an application needs
to close it earlier.
File clips use a 64 MiB least-recently-used byte budget. Clips attached to active sounds remain pinned until those sounds stop. Cache policy and explicit eviction remain function-oriented:
from pyalsoft import (
clear_sound_cache,
get_sound_cache_info,
set_sound_cache_limit,
)
set_sound_cache_limit(128 * 1024 * 1024) # None selects an unlimited cache.
print(get_sound_cache_info())
clear_sound_cache("notification.wav")
Clearing an active clip marks it for eviction after its final sound stops. The budget measures decoded OpenAL buffer bytes, not compressed file size. A compressed asset can therefore occupy much more cache memory than its file size, and an active sound is intentionally allowed to keep the cache over budget.
Static audio files¶
Convenience playback, load_audio(),
get_sound_info(), and explicit-session
upload() accept WAV, FLAC, MP3, and Ogg Vorbis files.
Formats are detected from file signatures. Ogg Opus and other Ogg codecs are
reported as unsupported rather than being treated as Vorbis.
| Input | Supported channels | Decoded sample type |
|---|---|---|
| WAV PCM 8-bit | mono through 7.1 standard layouts | UINT8 |
| WAV PCM 16-bit | mono through 7.1 standard layouts | INT16 |
| WAV PCM 24/32-bit or IEEE float | mono through 7.1 standard layouts | FLOAT32 |
| FLAC through 16-bit | mono or stereo | INT16 |
| FLAC above 16-bit | mono or stereo | FLOAT32 |
| MP3 and Ogg Vorbis | mono or stereo | FLOAT32 |
Source sample rates are preserved. SoundInfo.sample_type and bit_depth
describe this decoded PCM representation, not a compressed bitrate. Static
files are decoded completely into memory and uploaded to static OpenAL buffers;
this path is intended for sound assets, not memory-bounded or gapless music
streaming. Use open_stream() with application-provided
PCM chunks when bounded streaming is required.
Control one sound¶
Every VoiceConfig field can also be passed
directly to play(). Direct keywords override the
corresponding field when both forms are used:
from pyalsoft import play
sound = play(
"engine.wav",
gain=0.7,
pitch=1.1,
looping=True,
relative=True,
position=(-2.0, 0.0, -4.0),
reference_distance=1.0,
max_distance=20.0,
rolloff_factor=1.0,
)
sound.position = (2.0, 0.0, -4.0)
sound.pitch = 1.25
sound.seek(3.0)
sound.update(
position=(3.0, 0.0, -2.0),
velocity=(1.0, 0.0, 0.0),
gain=0.6,
)
offset_seconds is the playhead position on the original source-audio
timeline, not elapsed wall-clock time. At pitch=2.0, for example, the offset
advances two source seconds per wall-clock second while duration_seconds
remains unchanged. For sample-accurate work, use offset_frames,
remaining_frames, frame_count, and seek_frames().
rewind() follows OpenAL behavior by moving to the beginning and entering the
INITIAL state. restart() moves to the beginning and immediately plays.
Use PlayingSound.wait() when a thread should
block until playback ends. It returns False instead of raising when an
optional timeout expires:
For sounds attached to the player, UI sounds, and other sources that should not
use position, distance attenuation, directional cones, Doppler shift, or
positional panning, pass spatialize=False to play():
This explicitly disables source spatialization. It does not disable sample-rate
conversion when the source and output-device rates differ. On an HRTF output,
OpenAL Soft can still render the resulting fixed local source through a
front-center HRTF response. The bundled runtime supports this behavior; a
separately installed OpenAL implementation must expose
AL_SOFT_source_spatialize.
When spatialize is omitted or None, OpenAL chooses automatically based on the
source format. Pass True or False only to override that backend decision.
To bypass HRTF virtualization completely, use direct-channel playback:
Direct-channel playback routes stereo channels to the matching outputs without
virtual-speaker rendering. For convenience playback, mono file and
PCM sample frames are duplicated into identical left and
right channels before upload; the returned sound continues to report the
original source format. The normal and stereo-expanded forms of a cached file
occupy separate cache entries. Surround sources are rejected rather than
implicitly downmixed. With an explicit playback session, the
Clip passed to play() must already be stereo. A separately
installed OpenAL implementation must expose AL_SOFT_direct_channels.
Inspect a sound¶
Format and length information is available without opening an audio device:
from pyalsoft import get_sound_info
info = get_sound_info("engine.wav")
print(info.duration_seconds, info.frame_count)
print(info.channels, info.sample_rate, info.bit_depth)
The same immutable SoundInfo is available as sound.info
and for clips uploaded from PCM. Clips uploaded from
BufferData instead expose
BufferInfo as clip.info. When a sound ends,
end_reason distinguishes natural completion, an explicit stop(), runtime
shutdown, and a disconnected device when the backend supports connection
reporting.
Spatial controls¶
The spatial controls describe a sound relative to the playback listener:
| Control | Meaning |
|---|---|
position |
The sound's (x, y, z) location. By default, +X is right, +Y is up, and -Z is forward. |
velocity |
Motion used for Doppler shift. It does not automatically update position. |
direction |
The vector the sound's directional cone points along. (0, 0, 0) makes it omnidirectional. |
relative |
Selects listener-local coordinates instead of world coordinates. |
reference_distance |
The reference point where distance attenuation has unity gain. |
max_distance |
The outer distance used by clamped distance models. |
rolloff_factor |
Scales distance attenuation; 0 disables distance rolloff. |
min_gain, max_gain |
Lower and upper gain clamps. |
cone_inner_angle, cone_outer_angle |
The full angles defining the directional cone. |
cone_outer_gain |
Gain multiplier outside the outer cone. |
Gain is a linear amplitude multiplier: 1.0 is unchanged, 0.5 is about
-6 dB, and 0.0 is silent. Pitch changes playback rate and audible pitch
together; OpenAL does not perform independent time stretching. Prefer mono
sounds for positional audio because OpenAL normally plays stereo sources
without applying 3D position or direction.
Advanced source controls¶
Extension-backed controls live on VoiceConfig so they
work with explicit voices, streams, and convenience sounds. Defaults do not
require optional extensions; PyALSoft checks a capability only when its control
is requested.
They are also accepted directly by play() and exposed as
properties and PlayingSound.update()
keywords. For nullable controls such as distance_model, stereo_angles,
resampler, and super_stereo_width, omission preserves the base or current
configuration while an explicit None clears the override.
from pyalsoft import (
DistanceModel,
SpatializationMode,
VoiceConfig,
list_resamplers,
open_playback,
play,
upload,
)
with open_playback() as playback:
clip = upload(playback, mono_pcm)
resampler = next(value for value in list_resamplers(playback) if value.is_default)
voice = play(
playback,
clip,
VoiceConfig(
position=(8.0, 0.0, -12.0),
distance_model=DistanceModel.EXPONENT_CLAMPED,
radius=2.5,
spatialization=SpatializationMode.ENABLED,
resampler=resampler,
air_absorption_factor=1.0,
room_rolloff_factor=0.7,
),
)
distance_model=None inherits the context model and follows later
set_acoustics() changes. radius describes the
emitter's apparent physical size; it does not replace reference_distance or
max_distance. Stereo angles are ordered left then right and expressed in
radians.
DirectChannelsMode provides three routing
choices: normal virtualization, direct routing that drops unmatched channels,
and direct routing that remixes unmatched channels to available outputs. Direct
routing requires stereo managed audio and cannot be combined with forced
spatialization, stereo angles, or Super Stereo processing. The legacy
direct_channels=True keyword selects DROP_UNMATCHED.
Convenience playback transparently rebuilds mono audio as stereo when direct routing is enabled, and restores the mono form when it is disabled. This works for active sounds and for configuration changes stored until a later restart.
StereoMode.SUPER_STEREO converts an ordinary stereo
signal to an orientable UHJ-derived soundfield. super_stereo_width ranges from
0.0 for a focused front image through 1.0 for the widest supported image.
The stereo mode cannot be changed while its source is playing or paused.
Resamplers are implementation-provided values. Obtain them from
list_resamplers() for the active playback session
instead of constructing a Resampler directly. Air
absorption adds distance-dependent high-frequency attenuation. Room rolloff
attenuates auxiliary effect paths, so it is normally paired with a reverb
EffectSend. Both factors range from 0.0 through
10.0 and require EFX when nonzero.
The complete runnable example is
advanced_sources.py.
Pause a playback device¶
pause_playback_device() suspends device
processing without changing individual voice states. Use
resume_playback_device() to continue. This
is useful while an application is in the background and requires
ALC_SOFT_pause_device. These operations are distinct from pause() and
resume(), which control one voice or stream.
Precise playback timing¶
Three managed queries expose atomic timing pairs for synchronization work:
from pyalsoft import get_playback_clock, get_voice_clock, get_voice_latency
latency = get_voice_latency(playback, voice)
print(latency.offset_seconds, latency.output_latency_ns)
source_clock = get_voice_clock(playback, voice)
print(source_clock.offset_seconds, source_clock.device_time_ns)
device_clock = get_playback_clock(playback)
print(device_clock.device_time_ns, device_clock.output_latency_ns)
The ordinary VoiceStatus offset reports where OpenAL is processing source
audio. VoiceLatency additionally reports how long audio at that offset will
take to reach the physical output. VoiceClock relates a source offset to the
audio device's clock, while PlaybackClock atomically measures that clock and
the device's current output latency. Source offsets retain OpenAL's exact 32.32
fixed-point value as offset_frames_fixed; clock and latency values remain
integer nanoseconds. The offset_frames, offset_seconds, and corresponding
*_seconds properties provide convenient floating-point conversions.
Use delay_seconds or delay_frames to prepend source-timeline silence:
This delay is processed as silent source samples, so pitch and Doppler change
its real-time duration. While the silence is consumed, the voice is PLAYING
and its offset is negative. A delay cannot be combined with offset_seconds or
offset_frames. Both delay values must be non-negative, and delay_frames
cannot exceed 2**31.
For a precise wall-clock start, schedule against the audio-device clock:
clock = get_playback_clock(playback)
start_time_ns = clock.device_time_ns + 500_000_000
scheduled = play(playback, clip, start_time_ns=start_time_ns)
start_time_ns is an absolute device-clock value. A timestamp that has already
passed starts immediately. restart(), PlayingSound.restart(), and
start_stream() accept the same delay and scheduling keywords. These operations
require AL_SOFT_source_start_delay; clock queries additionally require
ALC_SOFT_device_clock.
Apply playback changes together¶
Use defer_updates() when several listener, source, effect, play, or pause
changes must become audible together:
from pyalsoft import Listener, VoiceConfig, defer_updates, set_listener
from pyalsoft import set_voice_config
with defer_updates(playback):
set_listener(playback, Listener(position=(1.0, 0.0, 0.0)))
set_voice_config(playback, voice, VoiceConfig(gain=0.5))
Audio continues rendering with the previous state inside the block, then all
pending changes are processed when the outermost block exits. Nested blocks are
safe. Omit playback to batch changes made through the convenience runtime.
This feature requires AL_SOFT_deferred_updates.
Configure the listener¶
The default runtime's listener and global distance and Doppler behavior can be configured without opening an explicit playback session:
from pyalsoft import (
Acoustics,
DistanceModel,
Listener,
set_acoustics,
set_listener,
update_listener,
)
set_listener(Listener(position=(0.0, 1.7, 0.0)))
set_acoustics(
Acoustics(
distance_model=DistanceModel.INVERSE_CLAMPED,
doppler_factor=1.0,
speed_of_sound=343.3,
)
)
update_listener(position=(2.0, 1.7, 0.0))
get_listener(), get_acoustics(), update_listener(), and
update_acoustics() operate on the convenience runtime by default. Pass an
explicit Playback as the first argument to use that
session instead.
See the runnable
play_file.py
and
move_sine.py
examples.