SoundDevice

class SoundDevice[source]

Bases: object

Handles audio playback using ALSA directly (pyalsaaudio).

Same interface as the sounddevice version, but writes to the PCM with writei (RW_INTERLEAVED), so playback starts fast (write into an empty PCM) and stop() can cut it immediately with drop() from another thread – which the sounddevice/PortAudio path could not do on this DAC because it uses mmap.

samplerate

Audio sample rate.

Type:

int

channels

Number of audio channels.

Type:

int

periodsize

Frames per ALSA transfer (latency grain).

Type:

int

periods

Number of periods in the ring buffer.

Type:

int

device

ALSA device string, e.g. ‘hw:2,0’.

Type:

str

error

Last error message.

Type:

str

__init__() None[source]

Initializes the SoundDevice with settings and opens the PCM.

Methods

load(left: Any, right: Any) None[source]

Stops any sound playing, then loads new data ready to be played.

load() cuts whatever is sounding (stop() is idempotent, so it is free if nothing is) and waits until the device is armed again, so the following play() starts immediately. Expect load() to block for roughly the stop delay plus the re-arm (tens of ms) when it interrupts a sound; it does not block when idle.

Parameters:
  • left (Any) – Left channel data (array-like, float in [-1, 1] or None for silence).

  • right (Any) – Right channel data (array-like, float in [-1, 1] or None for silence).

Raises:

ValueError – If inputs are invalid or lengths differ.

play() None[source]

Plays the currently loaded sound.

The same loaded sound may be replayed as many times as wanted; load() is only needed to change the sound. Calling play() before anything has ever been loaded is an error (reported to the alarm queue).

Fast path (nothing is being stopped): just wakes the worker; the PCM is already PREPARED, so the write starts the hardware immediately. The one non-instant case is play() landing while a stop() is still draining – then it waits for that stop to finish, then plays.

stop() None[source]

Asks the worker to stop playback. Non-blocking. A no-op if idle.

Sets _stopping so a play() (or load()) that lands before the stop has finished knows to wait for it instead of racing. The worker clears _stopping (and _playing) once the stop actually completes.

Idempotent on purpose: dropping an idle PCM would move it out of PREPARED into SETUP, and the next play() would pay the ~11.5 ms prepare again. So a stop() with no sound running does nothing.

shutdown() None[source]

Shuts down the worker and closes the PCM, but never closes it in use.

Tolerates a half-built object: if __init__ raised (the PCM failed to open, say), __del__ still calls this and the attributes may not exist yet.

Closing a PCM while another thread sits inside pcm.write() frees, from C, a structure that thread is still using. That can leave the driver’s substream dangling – bad enough that merely reading its /proc status can then take the kernel down. So if the worker will not join, the handle is deliberately leaked: process exit lets the kernel tear it down safely, which is far cheaper than corrupting the driver.

get_sound_from_wav(file: str, gain: float) tuple[numpy.ndarray, numpy.ndarray][source]
create_sound_vec(left: numpy.ndarray, right: numpy.ndarray) numpy.ndarray[source]

Interleaves, ramps and converts float [-1, 1] channels into int32 stereo.

The SOUND_RAMP_MS raised-cosine fade is applied here, so every sound that goes through load() gets it automatically.

Parameters:
  • left (np.ndarray) – Left channel data (float in [-1, 1]).

  • right (np.ndarray) – Right channel data (float in [-1, 1]).

Returns:

Interleaved contiguous int32 stereo data (frames, 2).

Return type:

np.ndarray