acron.dsp

Offline DSP for baked game audio, and the versioned WAV cache it lands in.

The trade this kit serves: an engine with no live audio graph plays baked WAVs, so the synthesis runs once into a cache and playback is gain staging. Everything here is plain Python over array('d') buffers — offline, run once per cache version, deterministic from a seeded acron.noise.RNG, so a given seed renders the same buffer every time on any machine. Signals go in as any sequence of floats (a list, an array, a memoryview); stereo is a (left, right) pair of them.

Conventions worth stating once:

  • Biquad Q is RBJ-cookbook Q (Audio EQ Cookbook, R. Bristow-Johnson), one linear convention for every kind; 0.7071 is the flattest (Butterworth) response, and a q of 0 divides by zero — nothing here interprets Q in dB.

  • Filter a LOOP circularly (loop_filter()): the exact steady state of the periodic input, seamless by construction. It is computed in the time domain — the biquad run from the one initial state that its own period maps back onto itself — which is the same answer an FFT gives (each bin times the response at that bin) in two passes over the loop.

  • Normalise a reverb impulse before convolving (plate_norm()): send gains then mean the same wetness whatever the IR’s seed, length or decay — skipping it renders a plate about twenty times too wet (measured).

  • A stereo plate is applied as its channel mean: the tail of a mono source through (left, right) averaged is, by linearity, the source through (left + right) / 2 — one convolution, not two.

WavBakeCache(root, *[, name, version])

A versioned directory of baked WAVs with a manifest of true levels.

Global Functions

biquad_h(kind: str, f0: float, freqs, fs: float, q: float = 0.7071, gain_db: float = 0.0) → list[complex]

The complex response of a biquad filter — the Audio EQ Cookbook coefficient formulas, S=1 shelves — evaluated at each of freqs.

brown_noise(rng, sec: float, rate: int) → array

Brown-ish noise, heavier at the bottom: a leaky integrator over the seeded stream, one draw per sample, so a given seed renders the same buffer every time.

env_exp(n: int, rate: float, peak: float, attack: float, decay: float) → array

An exponential attack/decay envelope: :data:`ENV_FLOOR` -> peak in attack seconds, back to the floor over decay.

impulse(rng, sec: float, decay: float, rate: int) → tuple[array, array]

A plate-reverb impulse response — decaying white noise, stereo (left, right).

loop_bins(length: int, rate: int, f0: float) → tuple[int, float]

Quantise f0 to a loop’s FFT grid — a loop only loops if every component sits on its grid. Returns (bin, the frequency that bin is).

loop_filter(sig, kind: str, f0: float, fs: float, q: float = 1.0, gain_db: float = 0.0) → array

Filter a LOOP circularly: the exact steady state of the periodic input, and seamless by construction.

The biquad’s zero-input state map over one period is A**n; running the loop once from rest lands on r; the periodic state is the fixed point s = A**n s + r, and the loop run from s is the answer.

pan_gains(pan: float) → tuple[float, float]

Equal-power stereo pan: −1 hard left, +1 hard right.

plate_norm(ir, rate: float)

Equal-power normalisation of a reverb impulse response — see :data:`PLATE_RMS_REF`. Same shape out as in (mono, or a channel tuple).

reverb_loop(sig, ir, send: float) → array

dry + send * (sig (x) ir), circularly — a loop’s steady-state plate. Each IR channel is cut to the loop’s length, as a circular plate must be.

reverb_tail(sig, ir, send: float) → array

dry + send * (sig * ir), linearly — a one-shot and its tail (len(sig) + len(ir) samples).

write_wav(path: Path, data, rate: int, *, scale: float = 1.0) → None

16-bit PCM of data * scale, clipped; data is mono or a (left, right) pair.