@maruaudio/dsp/voices/fm-core evaluates a phase-modulation operator graph of any size up to 16 operators: per-operator phase and output state, weighted source-to-target routes in radians of phase deviation, and independent carrier gains. Operators are evaluated in descending index order, so a higher-index source contributes its current-sample output, while a lower-index source and a self-route use the previous sample. The product Processor owns parameter mapping, envelopes, note and voice lifecycle and algorithm presets.
Parameters
There are no setters; the graph is passed in borrowed arrays each frame.
| Input | Meaning as coded |
|---|---|
operatorCount of createFmCoreState | Floored, capped at 16 (FM_CORE_MAX_OPERATORS); 0 or less gives 1 |
modulationMatrix | Row-major matrix[source × N + target], radians per unit of source output; negative amounts are routes too |
frequencyHz, envelopes, operatorLevels | Per operator; envelope × level scales an operator before it modulates others |
carrierGains | Per operator; a gain of 0 means not a carrier |
renderFmCoreFrame returns the sum of carrier outputs × gain divided by the number of non-zero carriers, or 0 when there are none.
Lifecycle
createFmCoreState(n) allocates four typed arrays during preparation → buildFmCoreEvaluationOrder(n, matrix, order, positions, scratch) after route edits, outside the audio callback; it returns false for an invalid count or short buffers → renderFmCoreFrame once per frame → resetFmCoreState clears phases and outputs on retrigger or reset. The frame functions allocate nothing. fmCoreMarkIModulationForDestination and fmCoreMarkIMixCarriers do the same sums over Phase 7's f64 per-voice lanes without rounding through f32.
When to use it
Building an FM or PM instrument whose operator count or routing is not one of the fixed tables, while keeping the same timing rules as Phase 7.
Limits
- Mono, one frame per call.
- The evaluation order is always descending operator index; the matrix is only length-checked by
buildFmCoreEvaluationOrder. - Phases are stored as f32 radians and wrapped each frame; operator outputs use
Math.sin.