@maruaudio/dsp/voices/grain-pool plays grains once something else has decided where, at what rate and for how long. A grain reads a mono source table from a start frame at a constant read step (negative reads backwards), is gated by one of the five grain-window curves over its length, and lands in the stereo field through a fixed left/right gain pair. Each frame, every live grain adds into caller-owned per-owner accumulators (an owner is whatever the caller groups grains by, such as a note voice), so each group can take its own envelope.
Parameters
| Call / argument | Coded behavior |
|---|---|
initGrainPool(ownerCapacity) | Declares the positive integer number of owner accumulators, independently of the 128 grain slots |
startGrain(pool, owner, start, step, length, shape, gainLeft, gainRight) | Returns the slot, or −1 for an owner outside capacity, invalid signed32 start/length, or nonfinite f32 step/gains; start and length are floored |
step | Source frames per output frame, stored as f32; negative reads backwards |
shape | Grain-window index, floored; values outside [0, 5) use window 0 |
retuneGrainStep(pool, slot, step) | Changes the rate of a live grain without moving its read head |
grainPoolRenderFrame(pool, table, base, frames, left, right) | Reads table[base + position] for positions in [0, frames); returns the live count, or −1 for invalid source/accumulator geometry without consuming grains |
Lifecycle
initGrainPool(ownerCapacity) allocates the 128-slot arrays and renders the five window curves once (5 × 512 points) → startGrain per scheduled grain (a free slot, else the oldest grain is stolen and steals is counted for the owner's output bridge) → clear the accumulators, then grainPoolRenderFrame once per output frame → stopGrainPoolOwner(pool, owner) when a voice retires → resetGrainPool stops every grain and keeps the rendered curves.
When to use it
The grain engine inside a granular instrument or texture effect, under a scheduler that picks grain start points, rates and pans.
Limits
- At most 128 grains (
GRAIN_POOL_MAX_GRAINS); every loop scans all slots. - Position is recomputed each frame from the integer start and age (plus a fractional origin after a retune), so long sources do not drift in f32.
- Linear interpolation; a frame outside [0, frames) reads 0, and the frame after the last reads as 0 for interpolation.
- The window is read with
setValueCurveAtTime's linear interpolation atage / length. - Both accumulator arrays must cover the declared owner capacity. The source region must be an integer range fully inside the table.
- Table and output arrays are borrowed for one call. During preparation,
isGrainPoolMemoryValidchecks fixed backing and disjointness from each other and private pool storage. Revalidate when replacing a view; keep pool arrays private and clear the accumulators before rendering each frame.