simvx.core.audio_synth

Procedural audio synthesis: oscillators, envelopes, AudioSynth.

Pure-numpy DSP for generating audio at runtime without external assets. The common pattern is “bake then play”: build a synth, render a short clip into an AudioStream, and play it through any AudioStreamPlayer on either backend (desktop / web).

Quick start::

from simvx.core import AudioSynth, Oscillator, ADSR, AudioStreamPlayer

synth = AudioSynth()
synth.add(
    Oscillator.sine(440.0),
    envelope=ADSR(attack=0.01, decay=0.1, sustain=0.6, release=0.2),
)
pluck = synth.bake(duration=0.4)  # → AudioStream

player = AudioStreamPlayer(stream=pluck)
self.add_child(player)
player.play()

Multiple sources mix together in one bake::

synth = AudioSynth()
synth.add(Oscillator.sine(220.0), gain=0.5)              # root
synth.add(Oscillator.sine(330.0), gain=0.3)              # fifth
synth.add(Oscillator.noise.white(), gain=0.1, pan=0.3)   # snare hiss
chord = synth.bake(duration=1.0)

AudioSynth.bake() returns an AudioStream whose backend_data is a float32 interleaved-stereo ndarray. Both MiniaudioBackend (native and legacy) and WebAudioBackend accept that directly: no file I/O, no extra serialization.

For live procedural synthesis with parameter control, see AudioSynth.attach_to() (streams via backend.open_stream and feeds chunks per frame).

Module Contents

Classes

AudioSource

Anything that produces mono float32 audio samples on demand.

WhiteNoise

Uniform-distribution white noise. Deterministic via the bundled RNG seed.

PinkNoise

Approximate pink noise via the Voss-McCartney algorithm.

Oscillator

Namespace for oscillator + noise constructors.

Envelope

Multiplier curve applied over the total duration of a baked clip.

ADSR

Attack / Decay / Sustain / Release.

Linear

Straight-line ramp from start to end over the whole duration.

Exponential

Exponential curve: start * (end/start)^(t/duration).

Filter

Per-source DSP filter applied after the source render + envelope.

LowPass

First-order one-pole low-pass.

HighPass

First-order one-pole high-pass.

AudioSynth

Composes audio sources into a bakeable / streamable synth.

Data

API

simvx.core.audio_synth.log

‘getLogger(…)’

simvx.core.audio_synth.__all__

[‘AudioSource’, ‘Envelope’, ‘Filter’, ‘LowPass’, ‘HighPass’, ‘Oscillator’, ‘WhiteNoise’, ‘PinkNoise’…

class simvx.core.audio_synth.AudioSource[source]

Bases: abc.ABC

Anything that produces mono float32 audio samples on demand.

Subclasses implement render(cursor_samples, n_samples, sample_rate) where cursor_samples is the global sample offset (so periodic waves stay phase-continuous across multiple chunked renders). Instantiating an incomplete subclass raises TypeError.

abstractmethod render(cursor_samples: int, n_samples: int, sample_rate: int) numpy.ndarray[source]
__slots__

()

class simvx.core.audio_synth.WhiteNoise(seed: int | None = None)[source]

Bases: simvx.core.audio_synth.AudioSource

Uniform-distribution white noise. Deterministic via the bundled RNG seed.

Initialization

render(cursor_samples: int, n_samples: int, sample_rate: int) numpy.ndarray[source]
__slots__

()

class simvx.core.audio_synth.PinkNoise(seed: int | None = None)[source]

Bases: simvx.core.audio_synth.AudioSource

Approximate pink noise via the Voss-McCartney algorithm.

Cheap to compute, sounds substantially warmer than white noise. Useful for ambient layers and percussion.

Initialization

render(cursor_samples: int, n_samples: int, sample_rate: int) numpy.ndarray[source]
__slots__

()

class simvx.core.audio_synth.Oscillator[source]

Namespace for oscillator + noise constructors.

Mirrors Web Audio’s OscillatorNode waveform set plus a noise sub-namespace for stochastic sources. All members are static: never instantiate Oscillator itself; call its classmethods directly::

Oscillator.sine(440.0)
Oscillator.square(220.0, duty=0.25)
Oscillator.noise.white(seed=42)
noise: ClassVar[simvx.core.audio_synth._NoiseFactory]

‘_NoiseFactory(…)’

static sine(freq: float, *, phase: float = 0.0) simvx.core.audio_synth.AudioSource[source]
static square(freq: float, *, duty: float = 0.5) simvx.core.audio_synth.AudioSource[source]
static saw(freq: float) simvx.core.audio_synth.AudioSource[source]
static triangle(freq: float) simvx.core.audio_synth.AudioSource[source]
class simvx.core.audio_synth.Envelope[source]

Bases: abc.ABC

Multiplier curve applied over the total duration of a baked clip.

Envelopes don’t know about gate-on/off: they receive the full duration and lay out their shape inside it. For real-time note-on/note-off behaviour, render shorter clips and crossfade in user code (or stream via AudioSynth.attach_to). Instantiating an incomplete subclass raises TypeError.

abstractmethod render_total(total_duration: float, sample_rate: int) numpy.ndarray[source]
__slots__

()

class simvx.core.audio_synth.ADSR(*, attack: float = 0.01, decay: float = 0.05, sustain: float = 1.0, release: float = 0.05)[source]

Bases: simvx.core.audio_synth.Envelope

Attack / Decay / Sustain / Release.

attack, decay, release are in seconds; sustain is a level [0, 1] held for whatever time remains after the attack and decay portions consume their share of total_duration. If the total duration is too short to fit the full ADSR, the release portion truncates from the start of the release phase.

Initialization

render_total(total_duration: float, sample_rate: int) numpy.ndarray[source]
__slots__

()

class simvx.core.audio_synth.Linear(*, start: float = 1.0, end: float = 0.0)[source]

Bases: simvx.core.audio_synth.Envelope

Straight-line ramp from start to end over the whole duration.

Initialization

render_total(total_duration: float, sample_rate: int) numpy.ndarray[source]
__slots__

()

class simvx.core.audio_synth.Exponential(*, start: float = 1.0, end: float = 0.01, power: float = 1.0)[source]

Bases: simvx.core.audio_synth.Envelope

Exponential curve: start * (end/start)^(t/duration).

start and end must be strictly positive (exponential interpolation is undefined through zero). Use Linear for fades to silence.

Initialization

render_total(total_duration: float, sample_rate: int) numpy.ndarray[source]
__slots__

()

class simvx.core.audio_synth.Filter[source]

Bases: abc.ABC

Per-source DSP filter applied after the source render + envelope.

Lighter than bus-level effects (simvx.core.audio_effect.LowPassFilter etc.): these run in numpy inside AudioSynth.bake() and render_chunk(), so the filter shape is baked into the resulting AudioStream and travels with it through any backend.

Use bus effects when you want the filter to apply to everything routed through a bus (mood-driven low-pass underwater scenes, sidechain compression). Use Filter when you want a single source in an AudioSynth to have a specific filter shape baked in (e.g. q1k3’s filtered noise bursts for shotgun blasts). Instantiating an incomplete subclass raises TypeError.

abstractmethod apply(samples: numpy.ndarray, sample_rate: int) numpy.ndarray[source]
__slots__

()

class simvx.core.audio_synth.LowPass(cutoff_hz: float)[source]

Bases: simvx.core.audio_synth.Filter

First-order one-pole low-pass.

a = exp(-2*pi*cutoff_hz / sample_rate) y[k] = a * y[k-1] + (1-a) * x[k]

First-order: -6 dB/octave above cutoff. Use the bus-level simvx.core.audio_effect.LowPassFilter for steeper (2nd-order biquad) cuts.

Initialization

apply(samples: numpy.ndarray, sample_rate: int) numpy.ndarray[source]
__slots__

()

class simvx.core.audio_synth.HighPass(cutoff_hz: float)[source]

Bases: simvx.core.audio_synth.Filter

First-order one-pole high-pass.

a = exp(-2*pi*cutoff_hz / sample_rate) y[k] = a * (y[k-1] + x[k] - x[k-1])

-6 dB/octave below cutoff. Use the bus-level simvx.core.audio_effect.HighPassFilter for steeper biquad cuts.

Initialization

apply(samples: numpy.ndarray, sample_rate: int) numpy.ndarray[source]
__slots__

()

class simvx.core.audio_synth.AudioSynth[source]

Composes audio sources into a bakeable / streamable synth.

Construct, add() one or more (source, envelope, gain, pan) voices, then bake(duration) to render an AudioStream ready to play.

Initialization

add(source: simvx.core.audio_synth.AudioSource, *, envelope: simvx.core.audio_synth.Envelope | None = None, filter: simvx.core.audio_synth.Filter | None = None, gain: float = 1.0, pan: float = 0.0) int[source]

Add a voice. Returns the voice id (index) for later mutation.

filter is an optional per-source DSP filter (e.g. LowPass, HighPass) applied after the source render and envelope but before gain / pan. For bus-wide effects use AudioBus.add_effect.

property voices: list[simvx.core.audio_synth._Voice][source]

Read-only-ish view of the voice list. Mutate elements in place.

clear() None[source]

Remove all voices. The synth becomes silent.

property voice_count: int[source]
set_param(voice_id: int, name: str, value: object) None[source]

Mutate a parameter on a voice’s source. Useful for live tweaks.

Looks up name on the source via setattr. Common targets: freq, duty, phase. Voices with no such attribute raise AttributeError.

render_chunk(cursor_samples: int, n_samples: int, *, sample_rate: int = 48000, channels: int = 2, soft_clip: bool = True) numpy.ndarray[source]

Render n_samples of mixed output starting at the given cursor.

Voices keep phase across consecutive calls (sources receive cursor_samples so periodic waves remain continuous), so this is the canonical “chunk this synth for streaming” API. Envelopes are not applied here: use bake() for one-shot baked clips where the envelope shape spans the whole clip.

Returns a float32 interleaved buffer (n_samples * channels).

attach_to(player, *, chunk_seconds: float = 0.1, sample_rate: int = 48000, channels: int = 2)[source]

Drive player with live synth output as long as the driver lives.

Adds a small _AudioSynthDriver node as a child of player which opens a streaming channel on the active audio backend and feeds chunks of chunk_seconds worth of synth output every process tick.

set_param mutations on the synth take effect at the start of the next chunk (so a 100 ms chunk has up to 100 ms parameter latency). Lower chunk_seconds for more responsive control at the cost of more per-frame work.

Returns the driver node so the caller can remove_child it to stop streaming.

Backend support: works on all three backends. The native ma_engine path uses an ma_pcm_rb ring buffer (default 0.5 s); the legacy path appends to a Python bytearray; the web path posts to an AudioWorkletNode. Underrun is silent padding on all three.

bake(duration: float, *, sample_rate: int = 48000, channels: int = 2, soft_clip: bool = True) simvx.core.audio.AudioStream[source]

Render the synth into an AudioStream of length duration seconds.

All voices mix into a single buffer; the result is stored as a float32 interleaved ndarray (backend_data) on the returned AudioStream. Both desktop and web backends accept this format directly.

soft_clip=True (default) clips the final mix to [-1, +1] so over-mixed voices don’t wrap. Set to False for clean overflow handling upstream (rare).