Skip to main content

Enge

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.

Authored uFor quantities accept Pint unit strings through reccy's shared parser, for example "48 kHz" for an output rate, "20 ms" for a duration, "1 semitone" for a cents offset, and "-6 dB" for effect gain. Bare numbers retain their documented canonical units. Exact Motion timing stays rational; beats, seconds, frames, and ticks are not implicitly interchangeable. See uFor's unit contract. Preparation and native rendering continue to use numerical values, not Pint Quantity objects. Unit strings do not enable processing features that an engine otherwise rejects. Pint expressions such as "1 ms / 3" are supported. Angles, dB, and pitch intervals are not implicitly interchangeable with dimensionless controls. Patch signals remain dimensionless; their physical-unit extension is deferred.

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. Offline NumPy and Rust renderers accept linear, step, and positive-valued exponential mappings, with activation weights applied after mapping. The persistent synth retains its explicit restriction to direct linear routes. 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. The complete filter chain can run before or after amplitude (envelope, velocity, volume, and changing amplitude controls), then pan/channel routing runs. The default remains before amplitude; 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.

Zero-duration amplitude-envelope phases jump immediately to their endpoints; their curve values do not affect playback. Nonzero-duration phases remain linear. Decoding remains with the caller. Curved envelope phases with nonzero duration, 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.

The native LiveRuntime silences output and latches failure when processing raises an error. A failed block may already have advanced some sources. Recovery requires restore() with a known-good snapshot or a fresh runtime; there is no reset-only recovery. Restore validates every source and effect before changing state, rejects failed snapshots, and discards queued source/effect actions and partially consumed batches from the abandoned timeline. Rejected restoration leaves both state and queued actions untouched. Hosts must resubmit any events they want to apply after restoration.

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.

Run one full suite at a time on a shared development machine, including when verifying uFor or safaz. Prepare the native extension once, then use uv run --no-sync pytest for repeated verification of unchanged Rust code.

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 when a GitHub release is published or when started manually. 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. CI uses the project's worker setting and allows 45 minutes for compilation and the full audio suite; Windows exceeded the previous 25-minute budget.

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.

Metadata

Release files for enge 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for enge 0.2.0
File Size Uploaded
enge-0.2.0.tar.gz 327.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for enge 0.2.0
File Interpreter ABI Platform
enge-0.2.0-cp313-cp313-macosx_11_0_arm64.whl CPython 3.13 CPython 3.13 macOS 11.0+ ARM64 Details

Total release size: 1.0 MB

Release files / enge-0.2.0.tar.gz

Download URL enge-0.2.0.tar.gz
Size 327.2 kB
Tags Source
SHA-256 checksum
How to use checksums
4c9afa3987d52665785367674e10915d91673b8fcca62d31302b9f78d2e77cbd
BLAKE2b-256 checksum
How to use checksums
3f8d1bf3943c93a560a2949d2c4e81caa3c8e0e3926bdff9f404dae27fbfa062
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / enge-0.2.0-cp313-cp313-macosx_11_0_arm64.whl

Download URL enge-0.2.0-cp313-cp313-macosx_11_0_arm64.whl
Size 686.9 kB
Tags CPython 3.13 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
88e263ae855a29ea085ef647f8bd681d9f28c00983439fbd0bf69b186f6642ad
BLAKE2b-256 checksum
How to use checksums
d03a2f5141322966144130086033e40f16e1974353d4767b9aa19444c12562f4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page