Explicit playback sessions¶
Applications that generate audio, stream it, select a device, or require fully explicit resource lifetimes can use the managed playback API directly:
from pyalsoft import PCM, open_playback, play, release, upload
pcm = PCM(
samples=b"\0\0" * 22_050,
channels=1,
sample_rate=44_100,
)
with open_playback() as playback:
clip = upload(playback, pcm)
voice = play(playback, clip)
# Query or control the voice here.
release(playback, voice)
release(playback, clip)
Calls using an explicit Playback are thread-safe.
PyALSoft serializes each complete operation for that session, including context
activation and managed state changes. Sessions backed by the same loaded OpenAL
library are also serialized because they share process-wide current-context
state; sessions on independent library instances may proceed concurrently.
close_playback() waits for an operation already in
progress and makes later operations fail with
PlaybackClosedError.
Use get_voice_config() to inspect the immutable
configuration currently retained for an explicit voice or stream.
Supported WAV, FLAC, MP3, and Ogg Vorbis files can be decoded and uploaded directly while retaining the same explicit resource ownership:
from pyalsoft import open_playback, play, upload, wait
with open_playback() as playback:
clip = upload(playback, "notification.wav")
voice = play(playback, clip)
wait(playback, voice)
load_audio() exposes the same static decoder when an
application needs the intermediate PCM value. Decoding is
whole-file and preserves the source sample rate. AAC/M4A, Opus, AIFF, metadata
tags, and decoder plugins are not part of this API.
Loop regions¶
Pass a frame range to upload() to repeat only part of a
static clip. The start frame is inclusive and the end frame is exclusive:
from pyalsoft import PCM, open_playback, play, upload
pcm = PCM(samples, channels=1, sample_rate=44_100)
with open_playback() as playback:
clip = upload(
playback,
pcm,
loop_points=(44_100, 88_200),
)
voice = play(playback, clip, looping=True)
Here the first second plays once, then frames 44,100 through 88,199 repeat.
Loop points affect only voices with looping enabled. Omitting them loops the
complete clip as before. A requested range must satisfy
0 <= start < end <= pcm.frame_count and requires AL_SOFT_loop_points.
Extension buffer formats¶
PCM supports unsigned 8-bit, signed 16-bit, float32, and
float64 samples. Standard interleaved mono, stereo, quad, 5.1, 6.1, and 7.1
layouts are accepted where OpenAL defines the combination. The managed upload
checks the required float or multichannel extension before allocating a clip:
import array
from pyalsoft import PCM, SampleType, open_playback, upload
samples = array.array("f", [0.0] * (48_000 * 6)).tobytes()
pcm = PCM(samples, channels=6, sample_rate=48_000, sample_type=SampleType.FLOAT32)
with open_playback() as playback:
clip = upload(playback, pcm)
Use BufferData when a payload needs an exact extension
format rather than a plain PCM layout. BufferFormat
covers the generated binding formats for float and double PCM, LOKI and EXT
IMA ADPCM, Microsoft ADPCM, mu-law and A-law, multichannel audio, Vorbis,
native WAVE, B-format ambisonics, and UHJ. Encoded data includes its decoded
frame_count, so managed duration, seeking, and stream queue timing do not
depend on the compressed byte length:
from pyalsoft import BufferData, BufferFormat, open_playback, upload
data = BufferData(
samples=ima_adpcm_bytes,
format=BufferFormat.MONO_IMA4,
sample_rate=48_000,
frame_count=decoded_frame_count,
block_alignment=samples_per_block,
)
with open_playback() as playback:
clip = upload(playback, data)
The bundled OpenAL Soft runtime supports the EXT IMA4 format used above. The legacy Vorbis, native WAVE, and LOKI formats are available through this API only when a separately installed OpenAL implementation advertises their corresponding extensions; the bundled runtime does not advertise them.
block_alignment configures AL_SOFT_block_alignment. B-format data also
accepts ambisonic_order, AmbisonicLayout, and
AmbisonicScaling; PyALSoft applies the associated
buffer properties before upload. Orders above 3 require explicit ACN layout
and either SN3D or N3D scaling; FuMa layout and scaling remain valid through
order 3. open_stream() accepts the same format and property options. Pass
frame_count= to try_write_stream() for each encoded chunk; fixed-width
chunks infer their frame count from their byte length.
write_stream() is the blocking counterpart to
try_write_stream(). It reclaims processed buffers while waiting and returns
False if its optional timeout expires. After finish_stream(), use
wait() to block until queued audio drains. wait() also
supports static voices and uses portable state polling rather than requiring an
optional native event extension.
These are input-buffer capabilities. ALC_LOKI_audio_channel contributes
legacy channel constants but declares no callable selection function in the
OpenAL registry; those constants remain available through pyalsoft.bindings.
Offline rendering¶
open_offline_playback() creates a managed
session that renders deterministically into memory instead of opening audio
hardware. Clips, voices, streams, listeners, and effects use the same operations
as an ordinary Playback:
from pyalsoft import (
RenderChannelLayout,
RenderConfig,
RenderSampleType,
open_offline_playback,
play,
render_samples,
upload,
)
config = RenderConfig(
sample_rate=48_000,
channels=RenderChannelLayout.STEREO,
sample_type=RenderSampleType.INT16,
)
with open_offline_playback(config) as playback:
clip = upload(playback, pcm)
play(playback, clip)
output = render_samples(playback, 48_000)
render_samples() advances the audio device by exactly the requested number of
frames and returns interleaved bytes in the configured format. Mono, stereo,
quad, 5.1, 6.1, 7.1, and 3D B-format output are supported when the backend
accepts them. B-format output additionally configures ambisonic order, layout,
and scaling through ALC_SOFT_loopback_bformat.
See the runnable
render_offline.py
example, which can optionally write the result to a WAV file.
Device and context configuration¶
Playback devices can be enumerated and passed to
open_playback(). Device and context preferences are
requested with PlaybackConfig; query
PlaybackInfo to see what the audio backend actually
enabled. Requests are hints, so the backend may select a different effective
value. Leaving a field as None preserves its default.
from pyalsoft import (
PlaybackConfig,
PlaybackOutputMode,
get_playback_config,
get_playback_info,
list_hrtf_profiles,
list_playback_devices,
open_playback,
reconfigure_playback,
)
devices = list_playback_devices()
selected = next((device for device in devices if device.is_default), None)
profiles = list_hrtf_profiles(selected)
config = PlaybackConfig(
sample_rate=48_000,
mono_sources=128,
stereo_sources=8,
max_auxiliary_sends=2,
hrtf=True,
hrtf_name=profiles[0] if profiles else None,
output_limiter=True,
output_mode=PlaybackOutputMode.STEREO_HRTF,
)
with open_playback(selected, config=config) as playback:
info = get_playback_info(playback)
print(info.device_name, info.sample_rate, info.output_mode)
print(info.hrtf_status.value, info.hrtf_name)
# Change selected settings while clips, voices, and streams remain valid.
reconfigure_playback(
playback,
PlaybackConfig(sample_rate=44_100, hrtf=False),
)
print(get_playback_config(playback))
# Replace the complete request, returning omitted fields to backend defaults.
reconfigure_playback(
playback,
PlaybackConfig(output_mode=PlaybackOutputMode.STEREO_BASIC),
replace=True,
)
reconfigure_playback() resets the live
device without closing its context or invalidating managed resources. In a
reconfiguration, each None field is an omitted update that preserves the
session's previous request. Pass replace=True to treat the supplied
configuration as the complete new request; its None fields then return to
backend-selected behavior. get_playback_config()
reports the retained request, while
get_playback_info() reports the effective values
negotiated by the backend. The reset may briefly interrupt output. Live
reconfiguration requires ALC_SOFT_HRTF, which is provided by the bundled
OpenAL Soft runtime.
reopen_playback() instead migrates a live session
to another output device while retaining clips, voices, streams, and effect
buses. It preserves the requested configuration except for hrtf_name, which
is cleared because profiles are device-specific. Enumerate the new device's
profiles and call reconfigure_playback() afterward when a named profile is
required.
Connection state is exposed as PlaybackInfo.connected and through
is_playback_connected(). Both return None
when the selected backend lacks ALC_EXT_disconnect.
Device-list events¶
subscribe_device_events() creates a
bounded queue of playback and capture device additions, removals, and default
changes. OpenAL's native callback only enqueues immutable events; application
code calls subscription.next() from its own thread:
from pyalsoft import subscribe_device_events
with subscribe_device_events() as events:
event = events.next(timeout=5.0)
if event is not None:
print(event.type.value, event.device_kind.value, event.name)
Only one system-event owner can use a loaded native OpenAL library at a time.
The queue discards its oldest event when full and exposes the loss count as
dropped_count.
The available requests and observations are:
| Configuration | Playback information | Availability |
|---|---|---|
sample_rate |
sample_rate |
OpenAL core |
refresh_rate |
refresh_rate |
OpenAL core; accepted but ignored by OpenAL Soft |
synchronous |
synchronous |
OpenAL core; accepted but ignored by OpenAL Soft |
mono_sources |
mono_sources |
OpenAL core |
stereo_sources |
stereo_sources |
OpenAL core |
max_auxiliary_sends |
max_auxiliary_sends |
ALC_EXT_EFX |
hrtf and hrtf_name |
hrtf_status and hrtf_name |
ALC_SOFT_HRTF |
output_limiter |
output_limiter |
ALC_SOFT_output_limiter |
output_mode |
output_mode |
ALC_SOFT_output_mode |
Optional-extension requests are omitted when the selected device does not
support the corresponding extension. Their observed values are None, except
for hrtf_status, which is HRTFStatus.UNAVAILABLE. HRTF profile names come
from list_hrtf_profiles(); names are resolved to
the backend's transient numeric identifiers on the same device used to create
the context.
See the runnable
play_sine.py,
loop_points.py,
stream_sine.py,
and
select_device_hrtf.py
examples.