sound_rs v0.2.2

04 · Guides

The player

Player owns one output stream for its whole lifetime. Every method below acts on that stream, so starting a track never reopens the device.

import sound_rs

player = sound_rs.Player()                 # opens the default device
player.play_file("first.flac")             # returns immediately
player.wait()

player.play_file("second.mp3")             # no device setup, starts at once
player.stop()
del player                                 # closes the stream
Use it as a context managerwith sound_rs.Player() as player: closes the stream on exit, including when an exception is raised. Without it, the stream closes when the object is garbage collected, which you cannot predict.

Lifecycle

StateWhat it meansLeaves it
idleStream open, nothing loadedplay_file, play_array
playingCallback writes audiopause, stop, end of source
pausedCallback writes silence, position frozenresume, stop
finishedSource exhausted, stream still openplay_*, resume (replays)

Controls

CallEffect
play_file(path)Streams a file from disk. Returns immediately.
play_array(arr, rate)Plays an in-memory array; resampled to the device once, up front.
pause()Stops producing sound and freezes position.
resume()Continues from the paused position, or replays from the start if finished.
stop()Rewinds to zero, keeps the stream open.
seek(seconds)Jumps; works while paused.
wait()Blocks until the source ends (GIL released).
del playerStops the decoder and closes the stream (or use with).

Seeking

Seeking a streamed file is accurate to the frame. Containers can only seek to a packet boundary, so the player reads the packet's real timestamp and drops the difference. A seek to 0.5 in a 16 kHz file lands on frame 8000, not on the nearest packet start.

player.play_file("track.flac")
player.seek(90.5)          # 1:30.5
player.wait()
print(player.position)     # ~ track length, not the seek offset
Seeking past the endRequests beyond the track are clamped to the last frame. position keeps counting from the seek target, so duration and position stay in the same units.

Volume

Applied in the callback, so it changes instantly and does not touch the decoded data. Range is 0 to 4; values outside are clamped. Volume 1.0 is unity.

player.volume = 0.25       # quiet
player.volume = 1.0        # unity

Position and duration

Both are seconds, read from the audio clock, not from wall time. For files the duration comes from the container header and is available right after play_file. For arrays it is the array length divided by the sample rate.

player.play_file("track.mp3")
print(player.duration)     # 213.4
print(player.position)     # 0.0
print(player.is_playing)   # True

Queue

enqueue lines up the next file; when the current one ends the decoder opens it and continues on the same stream. Rate and channel count may differ between tracks.

with sound_rs.Player() as player:
    player.play_file("first.flac")
    player.enqueue("second.mp3")
    player.enqueue("third.wav")
    print(player.queue)        # ['second.mp3', 'third.wav']
    player.wait()              # plays all three

Pushing audio from Python

For synthesis or a network stream, feed samples instead of a file. feed blocks while the buffer is full, so a fast producer cannot exhaust memory. If the buffer cannot drain for five seconds — a paused player, for example — it raises SoundRsError with code="stalled" instead of hanging.

import numpy as np
import sound_rs

player = sound_rs.Player()
player.begin_feed(48000)
for _ in range(100):
    block = (0.2 * np.sin(np.arange(4800) * 0.057)).astype(np.float32)
    player.feed(block)         # returns when the buffer has room
player.end_feed()
player.wait()
Feed at the device rate for clean joinsChunks are converted and resampled independently, so a non-device rate can click at chunk boundaries. Query player.sample_rate and feed that.

Threading and the GIL

Playback and decoding run on Rust threads, and every blocking call releases the GIL. player.wait() in the main thread does not block an asyncio loop running in another thread, and threading.Thread(target=sound_rs.play_audio, ...) works as expected.

One player is one stream. Do not call methods on the same player from several threads at once; create one player per thread instead.

Device selection

import sound_rs

print(sound_rs.list_devices())
player = sound_rs.Player(device="MacBook Pro Speakers")
print(player.device, player.sample_rate, player.channels)

The device is opened once, at construction. If a device disappears mid-playback, the player raises SoundRsError instead of hanging: playback stalls for two seconds without progress, then fails.