|
| 1 | +# LFO realization |
| 2 | + |
| 3 | +## Supported profile |
| 4 | + |
| 5 | +enge renders uFor's seconds-clock LFOs for both synths and samplers, selecting |
| 6 | +NumPy or Rust with the instrument's existing `backend` argument. Sine, square, |
| 7 | +and triangle shapes retain the canonical duty-cycle equations, including duty |
| 8 | +zero and one. LFO values are bipolar. Delay and fade-in affect a separate |
| 9 | +activation weight, never the phase clock. |
| 10 | + |
| 11 | +`enge.lfo.lfo_samples(definition, state, start, frames, sample_rate, backend)` |
| 12 | +returns a float64 `(frames, 2)` array containing value and weight. `state` is the |
| 13 | +canonical `ufor.lfo.LFOState`. Frames address the same absolute seconds clock as |
| 14 | +its rational event anchors. Rendering observes state without re-anchoring it. |
| 15 | +Use `ufor.lfo.initial_lfo` and `ufor.lfo.lfo_event` to initialize and apply events; |
| 16 | +serialize the canonical state to restore a standalone source. |
| 17 | + |
| 18 | +Beat clocks require a host tempo mapping and remain explicitly unsupported in |
| 19 | +this audio profile. Named envelopes and other modulation targets remain outside |
| 20 | +this milestone. |
| 21 | + |
| 22 | +## Timing and numerical boundaries |
| 23 | + |
| 24 | +At time `t`, phase is exactly `(state.phase + state.rate * (t-state.at)) mod 1`. |
| 25 | +A rate event advances using the previous rate before installing the next one. |
| 26 | +It preserves phase and activation age. Zero rate freezes phase. Rate changes are |
| 27 | +piecewise constant; continuous rate ramps are not part of uFor's current LFO |
| 28 | +contract. An event affects observations at its time and later. For a fractional |
| 29 | +event time, render the earlier integer observations with the previous state, |
| 30 | +apply the event, then render from the first integer observation at or after it. |
| 31 | + |
| 32 | +Reset policies follow uFor: `free` ignores trigger/transport events, `trigger` |
| 33 | +responds to trigger, and `transport` responds to explicit transport events. An |
| 34 | +explicit reset always resets phase and activation age, retaining current rate. |
| 35 | +A render boundary is never a reset or transport event. |
| 36 | + |
| 37 | +The NumPy reference samples uFor's independent scalar equations. Rust evaluates |
| 38 | +waveforms and activation arithmetic in a detached numerical loop with owned |
| 39 | +input and output arrays. Python resolves rational phase, wrap/duty boundaries, |
| 40 | +and activation boundaries before crossing the binding. Square polarity is |
| 41 | +resolved before converting to float. Triangle spans use a normalized coordinate |
| 42 | +within the selected branch, avoiding cancellation when duty approaches zero or |
| 43 | +one. Complete unobserved cycles are skipped using exact rational reduction. |
| 44 | +The numerical kernel receives arrays, without Pydantic or Fraction operations. |
| 45 | + |
| 46 | +Phase continues during delay and fade-in. At delay's endpoint weight is zero |
| 47 | +when a fade exists, otherwise one. During fade it increases linearly to one. |
| 48 | +Route evaluation applies weight after mapping: addition fades from zero; |
| 49 | +multiplication fades from one. Rust bounds final observations to their declared |
| 50 | +bipolar/unit domains to contain floating-point roundoff at endpoints. |
| 51 | + |
| 52 | +## Instrument ownership and event boundary |
| 53 | + |
| 54 | +LFO names are local to a sound-setting definition. Repeated bindings to one |
| 55 | +named source observe the same state. Voice scope creates a fresh source at the |
| 56 | +voice's start frame. Instrument and part sources use frame zero as their clock |
| 57 | +origin and continue algebraically through silent intervals. A joining voice does |
| 58 | +not send a trigger event to an existing shared source. Part sources are separate |
| 59 | +for each part; instrument instances never share state. Sampler slot/group |
| 60 | +ownership follows uFor validation; shared sampler LFOs belong in instrument |
| 61 | +settings. Instrument and slot modulation still compose per voice before mixing. |
| 62 | + |
| 63 | +Both instrument snapshots include LFO anchors and their owner identities, |
| 64 | +alongside existing control ramps and voice-to-source references. A new onset |
| 65 | +using an old trigger ID cannot reassign a release tail's voice LFO. |
| 66 | + |
| 67 | +Prepared uFor instrument traces currently contain lifecycle and control actions, |
| 68 | +not addressed LFO rate/reset events. Instrument integration therefore realizes |
| 69 | +authored LFO settings and voice/shared ownership. Dynamic rate, trigger, |
| 70 | +transport, and explicit reset events are supported through the standalone |
| 71 | +canonical state API. Exposing those events through prepared instrument traces |
| 72 | +requires a later uFor contract extension; this implementation adds no private |
| 73 | +trace vocabulary and does not reinterpret control observations as LFO events. |
| 74 | + |
| 75 | +## Acceptance |
| 76 | + |
| 77 | +Shared tests compare float arrays before writing at least one second of 48 kHz |
| 78 | +WAV audio, using `atol=1e-10`, `rtol=1e-9`. Coverage includes shapes/duty endpoints, |
| 79 | +fractional delay/fade boundaries, irregular partitions, rational rate events, |
| 80 | +zero rate, all reset policies, exact duty ties, large frame coordinates, scoped |
| 81 | +silent advancement, release tails, reused trigger IDs, and JSON restores. |
| 82 | +Native checks disable scalar reference evaluation and reject invalid span layouts. |
| 83 | + |
| 84 | +The two-second listening regression combines vibrato, tremolo, and a live gain |
| 85 | +ramp: a sine synth on the left and a sampled harmonic tone on the right. Expected |
| 86 | +pitch is integrated independently at higher precision; the sampler oracle uses |
| 87 | +linear interpolation of the supplied asset. Both backends publish verified |
| 88 | +lossless FLAC through reccy's existing atomic-output helper. This establishes |
| 89 | +numerical behavior, not live callback performance. |
| 90 | + |
| 91 | +## Additional work beyond the prompt |
| 92 | + |
| 93 | +None. |
0 commit comments