17.6. Audio synthesis: oscillators, drums, filters, and effects

Module strudel_synth

17.6.1. Constants

SAMPLE_RATE = 48000

SAMPLE_RATE:int const

TWO_PI = 6.2831855f

TWO_PI:float const

OSC_TURN_DOWN = 0.3f

OSC_TURN_DOWN:float const

VOWEL_NUM_FORMANTS = 5

VOWEL_NUM_FORMANTS:int const

VOWEL_MAKEUP_GAIN = 8f

VOWEL_MAKEUP_GAIN:float const

17.6.2. Enumerations

OscType
Values:
  • osc_sine = 0 - Pure sine wave.

  • osc_sawtooth = 1 - Band-limited sawtooth (poly-BLEP anti-aliased).

  • osc_square = 2 - Band-limited square wave (poly-BLEP anti-aliased).

  • osc_triangle = 3 - Triangle wave derived from the phase.

  • osc_supersaw = 4 - Unison stack of detuned band-limited sawtooths, stereo-spread.

  • osc_pink_noise = 5 - Pink noise via Paul Kellet’s 7-stage cascaded IIR.

  • osc_white_noise = 6 - Uniform white noise.

  • osc_unknown = 7 - Unrecognised sound name — used as a sentinel by to_osc_type.

17.6.3. Structures

PinkNoiseState

struct PinkNoiseState

FormantBiquad
Fields:
  • a0 : double = 0lf - Feed-forward coefficient 0.

  • a1 : double = 0lf - Feed-forward coefficient 1 (0 for BPF).

  • a2 : double = 0lf - Feed-forward coefficient 2 (-a0 for BPF).

  • b1 : double = 0lf - Feedback coefficient 1.

  • b2 : double = 0lf - Feedback coefficient 2.

  • z1 : double = 0lf - Internal delay-line state 1.

  • z2 : double = 0lf - Internal delay-line state 2.

  • gain : float = 1f - Per-formant amplitude scaling applied to the filter output by the vowel filter.

  • active : int = 0 - Non-zero when the filter has valid coefficients and should process samples.

VowelFilter
Fields:
  • formants : FormantBiquad[5] - Five formant bandpass biquads processed in parallel and summed.

  • active : bool = false - True once the filter has been set up for a known vowel; otherwise tick is bypassed by callers.

FormantData
Fields:
  • freqs : float[5] - Centre frequency of each formant in Hz.

  • gains : float[5] - Linear gain of each formant (formant 0 is usually the loudest).

  • qs : float[5] - Q (resonance) of each formant bandpass.

VoiceFX
Fields:
  • bc : ma_bitcrush - Bitcrusher state (bit-depth + sample-rate reduction).

  • ws : ma_waveshaper - Waveshaper state (drive-based soft distortion).

  • dj : ma_djfilter - DJ filter state (single-knob low-pass/high-pass blend).

  • bp : ma_bandpass - Bandpass filter state (centre + Q).

  • ph : ma_phaser - Phaser state (rate/depth/centre/sweep).

  • trem : ma_tremolo - Tremolo state (amplitude modulation rate/depth).

  • comp : ma_compressor - Compressor state (threshold/ratio/knee/attack/release).

  • has_crush : bool = false - Bitcrush is enabled and will be applied to the voice.

  • has_shape : bool = false - Waveshaper is enabled.

  • has_djf : bool = false - DJ filter is enabled (position != 0.5).

  • has_bpf : bool = false - Bandpass filter is enabled.

  • has_phaser : bool = false - Phaser is enabled.

  • has_tremolo : bool = false - Tremolo is enabled.

  • has_compressor : bool = false - Compressor is enabled.

  • active : bool = false - True if any of the above effects is enabled; skip fx_apply entirely when false.

OscVoice
Fields:
  • osc_type : OscType = strudel_synth::OscType.osc_sine - Which waveform this voice produces.

  • freq : float = 440f - Base frequency in Hz (before FM and supersaw detune).

  • phase : float = 0f - Main oscillator phase in [0, 1).

  • duration : float = 1f - Total note duration in seconds (release stage extends beyond this).

  • attack : float = 0.001f - ADSR attack time in seconds.

  • decay : float = 0.05f - ADSR decay time in seconds.

  • sustain : float = 0f - ADSR sustain level in [0, 1].

  • release_sec : float = 0.01f - ADSR release time in seconds.

  • samples_elapsed : int = 0 - Number of samples rendered so far; used to compute the envelope time.

  • finished : bool = false - Set to true when the ADSR has fully decayed; scheduler then retires the voice.

  • gain : float = 1f - Effective voice gain (event.gain * event.velocity).

  • pan : float = 0f - Stereo pan: -1 = full left, 0 = centre, +1 = full right.

  • cut : int = 0 - Cut group — non-zero values cut off any previous voice with the same cut value.

  • offset_frames : int = 0 - Sub-chunk onset delay in frames, consumed at the start of the first render chunk.

  • lpf_biquad : ma_sf2_biquad - Low-pass biquad filter state (initialised by osc_voice_init_filters).

  • hpf_biquad : ma_sf2_biquad - High-pass biquad filter state.

  • fm_depth : float = 0f - FM modulation index (0 disables FM).

  • fm_harmonicity : float = 1f - Ratio of modulator frequency to carrier frequency.

  • fm_mod_phase : float = 0f - FM modulator oscillator phase in [0, 1).

  • supersaw_phases : float[7] - Independent phases for each detuned sawtooth voice in the supersaw stack.

  • pink : PinkNoiseState - Pink-noise generator state (Paul Kellet’s 7-stage IIR).

  • white_seed : uint = 0x3039 - LCG seed used by the white-noise generator.

  • reverb_send : float = 0f - Send amount into the reverb bus (driven by Event.room).

  • chorus_send : float = 0f - Send amount into the chorus bus (driven by Event.chorus).

  • delay_send : float = 0f - Send amount into the delay bus (driven by Event.delay_amount).

  • orbit : int = 0 - Orbit index used by the scheduler to route this voice into a shared effect bus.

  • vowel_filter : VowelFilter - Optional vowel formant filter applied after the biquad filters.

  • fx : VoiceFX - Per-voice FX chain (bitcrush/waveshaper/DJ filter/bandpass/phaser/tremolo/compressor).

17.6.4. Pitch conversion

note_to_freq(note: float): float

Convert a MIDI note number to frequency in Hz (A4 = 69 = 440 Hz, middle C = 60).

Arguments:
  • note : float

17.6.5. Noise generators

noise(seed: uint&): float

Linear congruential pseudo-random noise in [-1.0, 1.0]. Advances the seed in place.

Arguments:
  • seed : uint&

17.6.6. Drum renderers

render_bd(duration: float; gain: float): array<float>

Render an 808-style kick drum as a mono buffer at SAMPLE_RATE. Combines a pitched sine body with a fast pitch sweep, a bandpass beater click and a rimshot-like snap, then applies a short room.

Arguments:
  • duration : float

  • gain : float

render_cowbell(duration: float; gain: float): array<float>

Render a cowbell: two detuned square-wave tones through a narrow bandpass, with a second quieter strike 8 ms later.

Arguments:
  • duration : float

  • gain : float

render_cp(duration: float; gain: float): array<float>

Render a hand-clap: a sharp bandpassed noise burst (~1.1 kHz) with a metallic bright edge and a long room tail.

Arguments:
  • duration : float

  • gain : float

render_crash(duration: float; gain: float): array<float>

Render a crash cymbal: lower-pitched bell partials plus a broadband metallic wash with a medium-fast decay.

Arguments:
  • duration : float

  • gain : float

render_hh(duration: float; gain: float): array<float>

Render a closed hi-hat: metallic oscillator bank layered with a short tonal bell (~180 Hz), plus room.

Arguments:
  • duration : float

  • gain : float

render_oh(duration: float; gain: float): array<float>

Render an open hi-hat: the same metallic oscillator bank as hh but with a much slower decay.

Arguments:
  • duration : float

  • gain : float

render_ride(duration: float; gain: float): array<float>

Render a ride cymbal: two bell partials (~340/387 Hz) plus a metallic shimmer, with a long sustain.

Arguments:
  • duration : float

  • gain : float

render_rimshot(duration: float; gain: float): array<float>

Render a rimshot/side-stick: a short woody body (~200 Hz) plus a bandpassed noise snap and a high transient click.

Arguments:
  • duration : float

  • gain : float

render_sd(duration: float; gain: float): array<float>

Render a snare drum as a mono buffer: tonal body (~220/330 Hz) plus high-passed noise for the wires, with a short room tail.

Arguments:
  • duration : float

  • gain : float

render_tambourine(duration: float; gain: float): array<float>

Render a tambourine: high-passed noise with two narrow bandpass jingle peaks (~3.8 kHz and ~8.8 kHz) and a delayed second hit.

Arguments:
  • duration : float

  • gain : float

render_tom(duration: float; gain: float; base_freq: float): array<float>

Render a tom drum at base_freq with a BD-style body + beater click + impulse + resonant-head overtones and a short room. Used for tom_low (~80 Hz) and tom_high (~175 Hz).

Arguments:
  • duration : float

  • gain : float

  • base_freq : float

17.6.7. Oscillator type

to_osc_type(sound: string): OscType

Map a strudel sound name (“sine”, “sawtooth”, …) to an OscType; unknown names return osc_unknown.

Arguments:
  • sound : string

17.6.8. Voice FX chain

fx_apply(fx: VoiceFX; buf: array<float>; n_frames: int)

Apply the enabled effects to an interleaved stereo buffer in place. Order: crush -> shape -> djfilter -> bandpass -> phaser -> tremolo -> compressor (phaser/tremolo modulate the finished tone, compressor is last so it sees the post-FX signal).

Arguments:
  • fx : VoiceFX

  • buf : array<float>

  • n_frames : int

fx_apply_to_samples(event: Event; samples: array<float>; sample_rate: float)

Apply Event-driven effects to a pre-rendered stereo buffer as a one-shot. Effect state is local to this call, so effects reset every render; used for drum/sample voices that don’t stream.

Arguments:
  • event : Event

  • samples : array<float>

  • sample_rate : float

fx_init_from_event(fx: VoiceFX; event: Event; sample_rate: float)

Configure a VoiceFX chain from the relevant Event fields (crush/coarse/shape/djf/bpf/phaser/tremolo/compressor). Leaves everything disabled when no Event field is active, so the chain can stay at fx.active == false.

Arguments:

17.6.9. Oscillator voice

osc_render_chunk(voice: OscVoice; output: array<float>; reverb_send_buf: array<float>; delay_send_buf: array<float>; chorus_send_buf: array<float>; chunkFrames: int)

Render one chunk of the oscillator voice additively into the output buffer and the reverb/delay/chorus send buffers. Hoists invariants out of the inner loop and dispatches to the per-oscillator-type render function.

Arguments:
  • voice : OscVoice

  • output : array<float>

  • reverb_send_buf : array<float>

  • delay_send_buf : array<float>

  • chorus_send_buf : array<float>

  • chunkFrames : int

osc_voice_init_filters(voice: OscVoice; lpf: float; hpf: float; lpq: float; hpq: float)

Configure the voice’s low-pass and high-pass biquads from Event lpf/hpf cutoffs and lpq/hpq resonances. Cutoffs <= 0 or >= Nyquist leave the corresponding filter inactive.

Arguments:
  • voice : OscVoice

  • lpf : float

  • hpf : float

  • lpq : float

  • hpq : float

osc_voice_init_supersaw(voice: OscVoice)

Seed the supersaw unison phases with randomised offsets so the detuned voices don’t start in phase.

Arguments:

17.6.10. Vowel filter

formant_biquad_setup_bpf(bq: FormantBiquad; freq: float; q: float; sample_rate: float)

Compute bandpass coefficients for the given centre frequency and Q. Deactivates the filter if the cutoff is outside the usable range (near 0 or Nyquist).

Arguments:
formant_biquad_tick(bq: FormantBiquad; input: float): float

Process one sample through the biquad and return the output.

Arguments:
vowel_filter_init(vf: VowelFilter; vowel: string)

Configure a VowelFilter for the named vowel, deactivating it if the vowel is unknown.

Arguments:
vowel_filter_tick(vf: VowelFilter; input: float): float

Process one sample through the vowel filter: sum the 5 parallel formant bandpass outputs and apply the makeup gain.

Arguments:
vowel_get_formant_data(vowel: string; data: FormantData): bool

Look up formant parameters for a vowel name (“a”, “e”, “i”, “o”, “u”, “ae”, “aa”, “oe”, “ue”, “y”, “uh”, “un”, “en”, “an”, “on”). Returns false if the vowel is unknown.

Arguments:

17.6.11. Voice rendering

mono_to_stereo(mono: array<float>): array<float>

Upmix a mono buffer to interleaved stereo by duplicating each sample to both channels. Consumes mono (moves and frees it).

Arguments:
  • mono : array<float>

17.6.11.1. render_event_stereo

render_event_stereo(event: Event; duration_sec: float; bank: SampleBank): array<float>

Render an Event into stereo PCM, preferring a sample from the bank when present and falling back to synthesis. Returns raw stereo — gain and pan are applied later by the scheduler.

Arguments:
render_event_stereo(event: Event; duration_sec: float): array<float>