A thin wrapper for a sounddevice stream to allow signals to "dip in and out" independently of each other.
A DipStream stream, is an OutputStream that allows audio sources to be added, played and removed at any time while the stream is running. This means that it is not necessary to pre-compute the complete mixed output signal, and allows sources to start and stop in response to undefined timing triggers like user input.
pip install -e .
The following snippet shows how DipStream can start and stop playback of several audio signals ("sources") independently of each other.
import numpy as np
import dipstream as ds
samplerate = 48000
# Create two example signals, both 1s long
t = np.linspace(0, 1, int(np.ceil(samplerate * 1)), endpoint=False)
tone_500 = 0.75 * np.sin(2 * np.pi * 500 * t)
tone_1000 = 0.25 * np.sin(2 * np.pi * 1000 * t)
dipstream = ds.DipStream(samplerate=samplerate, channels=2) # use default stereo device
with dipstream:
# Add the 500Hz tone on the left channel and the 1000Hz tone on the right
source_500Hz = dipstream.add(
samplerate=samplerate, data=tone_500, channel_mapping=[1]
)
source_1000Hz = dipstream.add(
samplerate=samplerate, data=tone_1000, channel_mapping=[2]
)
# Start the 500Hz tone, looping until stopped
source_500Hz.start(loop=True)
# After 2 seconds of playback, start the 1000Hz tone
source_1000Hz.start(at_time=source_500Hz.start_time + 2)
# Stop the 500Hz tone 2 seconds after the 1000Hz tone ends
source_500Hz.stop(on_end=source_1000Hz, offset=2)
# Print and example of the timing
print(f" 500Hz: expected 5s, played for {source_500Hz.playback_duration:.6f}s")
print(f"1000Hz: expected 1s, played for {source_1000Hz.playback_duration:.6f}s")
# Remove the signals from the stream
dipstream.remove(source_500Hz)
dipstream.remove(source_1000Hz)Each active source is mixed (+=) into the output on the channels specified. It is necessary to make sure the output mix does not clip (exceed the -1:+1 range) by controlling the amplitude of each source. Currently clipping will throw and error and end the stream.
The output channels which sources are mixed into and played back on can be set using the channel_mapping argument. This does not need to be continuous, for example a stereo source could be mapped to [1, 5]. Mono sources can be mapped to multiple channels, and their data will be reproduced on each one.
All timing in DipStream uses the sounddevice stream time clock. A timestamp can be taken using dipstream.now.
Source start() and stop() functions schedule the start or stop to be handled in the next audio callback (after any delays using the at or ..._with arguments, see their API section). Additionally, the latency of the system means that there will be a delay between scheduling and the actual change on the audio output.
For consistency, the source start_time and stop_time properties use the estimated time that change is seen on the audio output. This is their scheduled time (callback time) + the latency of the system (dipstream.latency).
The start() and stop() functions block until change is estimated on the output. When called without any delay arguments, it should block a little longer than the latency (dipstream.latency).
When called with the at_time time argument, the delay time is reduced by the latency in an attempt to schedule the actual change as close to the target time as possible. When called with one of the on_start or on_end arguments, the calling source will be scheduled based on the target scheduling, so the delta should be one callback duration. In these cases, the blocking should be only a little longer than the intentional delay.
Consequently, if the start and stop are scheduled correctly, the error in playback_duration compared to the expected duration should be kept to a minimum -- in theory a maximum of 1 block duration, although the latency correction is not definite.
Since the effect of latency is accounted for, it should be possible to compare source timing events with other events, such as user input, if they are timestamped using the dipstream.now clock.
Instantiate using DipStream(...), where the arguments match the sounddevice OutputStream arguments. For example DipStream(samplerate=48000, device="ASIO Fireface", channels=8).
Methods:
- Preferably use the
withcontext to manage the lifecycle. Alternatively,start()starts the stream.stop()stops the stream.
add(samplerate: int, data: np.ndarray, channel_mapping: list[int])adds a new audio signal to the stream and returns the source instance.datamust be of type/subtype float and shape (n_samples,) or (n_samples, n_channels).channel_mappingis the mapping between source data channels and output channels and follows the format of the sounddeviceplay()mappingargument. Mono audio can be mapped (repeated on) multiple channels.
remove(source: _Source)removes a source from the stream.clear_sources()removes all sources from the stream.elapsed_between(start: float, end: float)calculates the time between two timestamps.wait_until_time(time: float, sleep: float)sleeps in a loop until the target time.
Properties (readonly):
now: floatis the current sounddevice stream time.samplerate: intis the stream sample rate.channels: intis the number of channels available in the stream.latency: floatis the latency of the stream in secondscurrent_blocksize: intis the blocksize of the audio callback (which could vary between callbacks).
Sources should not be instantiated directly, only by using the add() method the stream.
Methods:
start(at_time: float | None, on_start: _Source | None, on_end: _Source | None, offset: float | None, loop: bool, starting_idx: int, timeout: float)starts the playback of a source, which canloopand start from astarting_idxin the signal. Schedules the start immediately or at a given timeat_time, or based on another source usingon_startandon_end, plus an optionaloffsetduration. See the Timing section for more information.stop(at_time: float | None, on_start: _Source | None, on_end: _Source | None, offset: float | None, timeout: float)stops the playback of a source. Schedules the start immediately or at a given timeat_time, or based on another source usingon_startandon_end, plus an optionaloffsetduration. See the Timing section for more information.
Properties (readonly):
samplerate: intthe sample rate of the source, which must match that of the stream.channel_mapping: list[int]the mapping between source data channels and output channels.start_time: floatthe timestamp of the start of playback, which will be None if it has not yet started.end_time: floatthe timestamp of the end of playback, which will be None if it has not yet ended.data_duration: floatthe duration of the audio signal in seconds (ie, duration of the samples).playback_duration: floatthe duration that the source was played back for (ie, between the start and end timestamps).is_playing: boolis True if the source is currently playing.is_looping: boolis True if the source is currently playing and set to loop.