Enge is the shared synth and sampler engine for Ufor instruments. Hosts supply prepared Ufor actions, advance the engine at exact output-frame boundaries, and own transport, device I/O, MIDI/OSC, GUI, output files, and encoding. Sample decoding and preparation can belong to the engine's sampler preparation.
The NumPy reference and Rust backend render held linear-envelope voices with
live amplitude and tuning control routes. It consumes uFor trigger contexts,
smooths controls in their declared scopes, preserves phase through pitch changes,
and snapshots active ramps and release tails. Static tuning and the prepared Hz
offset are applied once. Output is float64 in (frames, channels) order.
Tuney and the offline synth use the same PreparedVoice and VoiceRenderer for
oscillator state, gain, envelopes, minimum hold, release, completion, and explicit
channel routes. Each voice renders float64 (frames, channels) buffers and can
serialize its state for restoration. Completed voices return silence without
advancing their oscillators. OscillatorState retains the phase rounding
correction, and the stateless waveform_samples() keeps the established
sample-position/period interface. Tuney translates its mono/binaural settings
and output-layout policy into routes; note policy and device handling stay local.
The NumPy and Rust sampler core in sampler.py renders decoded
float64 assets with linear interpolation, forward/backward/mirror traversal,
loops, overlaps, live pitch ratios, and fractional effective releases.
PreparedSample owns immutable audio shared by cursors; SampleState serializes
progress, and sample_frames() returns audio plus independent next state.
It passes the traversal vectors and longer
48 kHz WAV regressions for the implemented traversal profile.
RegionPlayer in sample_regions.py plays ordered uFor
sample-region notes in successive output blocks through this sampler;
render_regions() consumes the same player for complete renders. Adjacent
regions of one asset share a cursor run, preserving exact source frames. The
whole tail policy plays each full region; gate stops at its authored gate
end. An optional seam_fade_frames applies a linear fade out and fade in only
where the source cursor jumps, without changing output length. stop() makes
later blocks silent. Looped sustain and a device callback adapter are not
implemented.
OfflineSampler in sample_instrument.py now
consumes prepared uFor sample actions. It applies held linear envelopes,
instrument/slot amplitude and tuning controls, static dB gain, resolved pitch/gain
variation, and explicit channel routes. Selection, sustain, replacement, and
one-shot release decisions come from uFor. Source exhaustion or envelope
completion ends each voice; prepared stops are immediate.
Prepare with sample_instrument.prepare(score, decoded_assets), where the asset
dictionary maps uFor asset IDs to float64 frame/channel arrays, then construct
OfflineSampler(prepared, backend="native") for Rust, or omit the backend for
NumPy, and call advance(actions, start, end). The same selection is available on
SampleVoiceRenderer.start() and sample_frames(). Snapshots
serialize voices and control ramps, and verify the score and decoded-content
fingerprints on restore. Sampler snapshots also retain their backend and reject
restoration into a different backend, including when no voices are active.
Decoded NumPy audio is shared across slots and instances. Each PreparedSample
lazily retains one Rust-owned audio copy, reused by its voices and restored
cursors across calls and engine instances.
Rust computes sample traversal, loop overlap, interpolation, envelopes, gain, and routing while the GIL is released. Python resolves controls and exact rational release splits; frame/index coordinates cross the binding as signed 64-bit integers. The shared conformance suite covers both backends, and direct tests disable Python DSP to prevent a fallback. Modulated sample voices batch a prefix whose declared maximum tuning cannot exhaust the source, then resolve the remaining uncertain frame alone so parameters after exhaustion are not evaluated. This API allocates per call and does not establish live callback deadline guarantees.
Named seconds-clock LFOs drive amplitude, tuning, and filters in both instruments.
They support sine, square, and triangle shapes, delay/fade-in, and voice, part,
or instrument ownership as allowed by uFor. Shared sources continue through
silence, and snapshots retain their exact phase anchors. Standalone
enge.lfo.lfo_samples() also samples canonical uFor rate/reset event states.
Prepared instrument traces do not yet carry addressed LFO events; their LFO
rates remain authored settings.
All four offline engines accept control_interval=1 (a positive integer), as does
render_midi. The default uses full-resolution vectorized controls and route
mappings. Larger intervals interpolate sine LFO values on an event-anchored grid;
constants, linear ramps, envelopes, square/triangle edges, and activation weights
remain exact. Route mappings still operate per sample. Audio-rate LFOs stay at
full rate when they reach the selected control-rate Nyquist frequency. This is
independent of audio buffer size, and snapshots reject a different interval.
See control evaluation and measurements.
Both backends render ordered lowpass, highpass, bandpass, and notch filters with one or two stages. Cutoff and Q follow per-sample control/LFO routes. Each source channel owns trapezoidal integrator state, preserved through parameter changes and snapshots. Filters run before the amplitude envelope and routing; sampler slot/group filters precede instrument filters. Voice completion discards their state without adding a tail. See the revised uFor filter contract and enge's realization.
Decoding remains with the caller. Curved envelopes, equalizers, layer crossfades, delayed or offset sample starts, event bindings, other modulation targets, and fade retirements fail explicitly.
The NumPy and Rust graph FM engine in fm.py renders two through six named sine, square, or triangle operators. Acyclic current-sample edges modulate destination phases; delayed edges read independently retained source-output histories. Operator ratios and tuning, edge indices, carrier level, common amplitude/tuning, and filters support the existing control/LFO routes. Pitch changes retain phase; snapshots retain phase corrections and delayed-edge histories. Carrier completion ends the voice. Rendering is at output rate and permits aliasing.
Load a SynthInstrumentScore containing fm voice definitions, such as
the FM fixture, and prepare its performance with
ufor.synth_trace.prepare. Use fm.OfflineFM(fm.prepare(score)), then the same
advance(actions, start, end), snapshot(), and restore(snapshot) operations
as the other engines. Each engine rejects a score containing another source
profile. Select Rust with fm.OfflineFM(fm.prepare(score), backend="native").
Rust computes both envelopes, operator phases, feedback, filters, gain, and
routing in one call with owned buffers and the GIL released. Snapshots reject
a different backend, including when no voices are active.
fm.fm_samples is the pure numerical boundary: parameter/envelope arrays plus
phase and feedback arrays produce audio and independent next-state arrays.
Validation, models, control evaluation, and rational envelope boundaries stay
outside it; feedback requires a sequential recurrence.
See the FM plan and implementation status and
uFor FM semantics.
The white-noise engine in noise.py accepts uFor NoiseVoice
definitions (noise: "white", mapping pitch_tracking: false). Prepare the score
with noise.prepare(score), construct noise.OfflineNoise(prepared, backend="native") for Rust or omit the backend for NumPy, then use the same
advance, snapshot, and restore operations. See the
noise fixture and
portable noise contract.
Each prepared note carries its own deterministic stream key derived from the performance seed. Block sizes, muting, and snapshot continuation preserve that stream. The source is mono uniform white noise, with existing resonant filters before the amplitude envelope/gain and channel routes. Cutoff, Q, and amplitude support controls and LFOs. Pitch does not affect noise; source tuning is rejected. NumPy generates random samples using vectorized integer arithmetic; Rust owns its buffers and performs the full voice DSP with the GIL released. Neither backend normalizes or clips, and pink/brown noise are not implemented.
Run uv run pytest test/test_noise.py for shared regressions and the two-second
filtered-noise demo. Verified 48 kHz WAVs produce listenable
.pytest_cache/d/audio/noise-demo-numpy.flac and
.pytest_cache/d/audio/noise-demo-native.flac.
Select the Rust backend with OfflineSynth(prepare(score), backend="native")
or VoiceRenderer.start(definition, backend="native"). The default is "numpy";
there is no fallback if the native extension is unavailable. Active-voice snapshots
retain their backend and restore only into that backend. Tuney continues to use
the default NumPy renderer.
Rust computes oscillator phase, waveforms, filter coefficients/state, envelope
samples, gain, and channel mixing. Python resolves uFor actions, control values, and exact rational
envelope boundaries before the call. The Rust kernel owns its working buffers,
releases the GIL during rendering, and returns new audio and state arrays without
mutating its inputs. It contains no Python callbacks or unsafe code. Copying
inputs and allocating output still happen per call, so this is not an
allocation-free device callback API.
Build with uv sync; Rust/Cargo 1.85 or newer is required. Python packaging uses
maturin,
PyO3, and
rust-numpy.
After changing Rust, run uv sync --reinstall-package enge to rebuild the extension.
Run uv run pytest for the shared NumPy/native conformance suite. It uses all
available pytest workers and work-stealing to balance long audio cases. Use
uv run pytest -n 0 to reproduce a failure serially. Native cases are required,
not skipped when compilation is unavailable. cargo fmt --check and
cargo clippy --locked --all-targets -- -D warnings check the Rust source.
Linux, Windows, and macOS use the same Python and Rust engine code. Source builds need Python 3.13 or newer and Rust/Cargo 1.85 or newer. On Windows, use the Rust MSVC toolchain and install Visual Studio Build Tools with the C++ build tools and Windows SDK. On macOS, install the Xcode Command Line Tools; on Linux, install your distribution's compiler build tools. Hosts own audio devices, so the engine does not require a platform-specific audio driver or device library.
FLAC export, listening demos, and their tests require the flac executable on
PATH. The demos encode 32-bit PCM, which requires FLAC 1.4 or newer. Install it
with apt install flac on Debian/Ubuntu or brew install flac on macOS. On
Windows, download the command-line binaries from the
official FLAC releases and add their
Win64 directory to PATH. Engine rendering into NumPy arrays does not require
FLAC.
Rubber Band is optional and statically built from the source bundled by
rubberband-sys; a separately installed Rubber Band library is not needed.
Enabling it adds GPL-licensed code to the native extension. Build it with:
uv sync --frozen --reinstall-package enge --config-settings-package enge:build-args=--features=rubberband
uv run --no-sync pytest --require-rubberband
This build also needs a C++ compiler and libclang. Install libclang-dev on
Debian/Ubuntu, or LLVM on macOS and Windows. On macOS with Homebrew, set
LIBCLANG_PATH to $(brew --prefix llvm)/lib; in Windows PowerShell, set
$env:LIBCLANG_PATH = "$env:ProgramFiles\LLVM\bin" for a default LLVM installation.
See the bindgen build requirements.
To rebuild without Rubber Band, run uv sync --frozen --reinstall-package enge
without the feature setting.
The cross-platform workflow runs only when a GitHub release is published. It builds and runs the full NumPy/native test suite on Linux, Windows, and macOS, both with and without Rubber Band. Its feature-enabled jobs require the Rubber Band tests to run rather than skip when the feature is absent.
The proposed execution contract defines the common
timing, dynamic-control, state, and conformance requirements for Python/NumPy
reference engines and a native implementation. It also records the numerical
function boundaries and verification requirements for a future torch.compile
implementation.
Run uv run pytest test/test_synth_demo.py to generate a 4.25-second stereo demo
at 48 kHz: an arpeggio followed by a held note with smoothed volume and pitch
changes. The regression compares the render against an independently calculated
waveform, writes WAV artifacts, and verifies lossless FLAC encoding using the
flac command (required on PATH). Listen to
.pytest_cache/d/audio/synth-demo-numpy.flac and
.pytest_cache/d/audio/synth-demo-native.flac.
Completed demos are published with Reccy's shared atomic_output helper, so a
failed copy preserves the previous demo. Reccy also supplies atomic publication
for offline rendering.
Run uv run pytest test/test_lfo_demo.py for a two-second vibrato/tremolo demo,
with a sine synth on the left and a sampled harmonic tone on the right. Both
receive a live gain change. Listen to .pytest_cache/d/audio/lfo-demo-numpy.flac
or .pytest_cache/d/audio/lfo-demo-native.flac; the test compares independent
audio oracles and verifies lossless encoding before publishing either file.
Run uv run pytest test/test_filter_demo.py for a two-second cutoff sweep with
LFO-modulated resonance: triangle synth on the left, sampled harmonics on the
right. The test checks an independent matrix-equation oracle before publishing
.pytest_cache/d/audio/filter-demo-numpy.flac and
.pytest_cache/d/audio/filter-demo-native.flac.
Run uv run pytest test/test_fm.py for FM conformance and a one-second demo with
feedback, interrupted timbre smoothing, and independent operator release tails.
The test checks an independent scalar oracle, writes 48 kHz WAV regressions, and
verifies lossless encoding before publishing
.pytest_cache/d/audio/fm-demo-numpy.flac and
.pytest_cache/d/audio/fm-demo-native.flac.
Run uv run python scripts/live-effects.py to render a six-second effects demo:
dry, individually granulated, then shared frozen-granulated oscillator, sampler,
FM, and noise voices. It writes live-effects.flac at 48 kHz; --backend numpy
selects the reference renderer.
For a longer benchmark, run uv run python scripts/bach.py. It renders the
four-minute BWV 578 MIDI adaptation to bwv-578.flac at
48 kHz using FM bass/soprano, triangle tenor, and sampled alto, with subtle
expression, timbre, and pitch automation. --block-size controls render blocks;
--backend native selects Rust for all three engines. MIDI adaptation, reusable
presets, and streaming FLAC rendering live in
enge.midi, enge.presets, and enge.render. Reccy is now a runtime dependency
for atomic output publication.