Granular Engine
Tier: Algorithms | ComponentType: 31 | Params: 7
32-grain pool granular synthesis engine with circular buffer recording, Hann-windowed grain envelopes, scan position control, and randomized scatter.
Overview
GranularEngine implements real-time granular synthesis by continuously recording input into a circular buffer and spawning overlapping grains that read back from different positions in that buffer. Each grain is a short windowed segment of audio, and the combined output of many simultaneous grains creates the characteristic granular texture -- from smooth, shimmering clouds to fragmented, stuttering effects.
The Density parameter controls how frequently new grains are spawned (in Hz). Grain Size sets each grain's duration. Position controls how far behind the write head grains begin reading, effectively setting the delay between live input and granular output. Scan Rate continuously advances this read position, allowing the grains to slowly move through the buffered audio. Scatter adds random offset to each grain's start position, spreading them across the buffer for a more diffuse texture.
Pitch controls the playback rate of each grain -- values above 1.0 pitch up, below 1.0 pitch down. At extreme settings, grains become very short (high density + small size) or very long (low density + large size), creating textures ranging from metallic resonance to ambient drones.
The grain pool is fixed at 32 slots. When all slots are active, new spawn requests are silently dropped until a grain completes. Output is normalized by \(\sqrt{N}\) where \(N\) is the active grain count, preventing volume spikes from high-density clouds.
File Locations
| Path | |
|---|---|
| Header | Sources/FolioDSP/include/FolioDSP/Algorithms/GranularEngine.h |
| Implementation | Sources/FolioDSP/src/Algorithms/GranularEngine.cpp |
| Tests | Tests/FolioDSPTests/GranularEngineTests.swift |
| Bridge | Sources/FolioDSPBridge/src/FolioDSPBridge.mm (GranularEngineBridge) |
Parameters
| Index | Name | Description | Min | Max | Default Min | Default Max | Default | Unit |
|---|---|---|---|---|---|---|---|---|
| 0 | Grain Size | Duration of each grain | 1.0 | 1000.0 | 10.0 | 500.0 | 80.0 | ms |
| 1 | Density | Grain trigger rate | 0.5 | 200.0 | 1.0 | 100.0 | 20.0 | Hz |
| 2 | Pitch | Playback speed ratio | 0.1 | 8.0 | 0.25 | 4.0 | 1.0 | |
| 3 | Position | Read offset from write head | 0.0 | 10000.0 | 0.0 | 5000.0 | 100.0 | ms |
| 4 | Scan Rate | Position advance speed | -4.0 | 4.0 | -2.0 | 2.0 | 1.0 | |
| 5 | Scatter | Random offset per grain | 0.0 | 1.0 | 0.0 | 1.0 | 0.2 | |
| 6 | Mix | Dry/wet blend | 0.0 | 100.0 | 0.0 | 100.0 | 100.0 | % |
Processing Algorithm
The process() function executes these steps for each input sample:
1. Buffer Recording
The input sample is written to a CircularBuffer<262144> (~5.9 seconds at 44100 Hz):
2. Parameter Smoothing
All seven parameters are smoothed via one-pole ParamSmoother to prevent zipper noise during parameter changes.
3. Scan Position Advance
The scan position advances continuously by the scan rate value each sample:
This creates a slowly moving read window through the buffer. Negative scan rates move backward.
4. Grain Spawning
A spawn counter accumulates at sample rate. When it reaches the spawn interval, a new grain is triggered:
For each new grain, an inactive slot in the 32-grain pool is found. The grain's starting read position is calculated:
where \(\text{posOffset} = \text{position}_{\text{ms}} \cdot f_s \cdot 0.001\). The grain is initialized with:
5. Grain Summation
All active grains are summed. For each grain:
The Hann window provides smooth attack and release for each grain, eliminating clicks at grain boundaries.
6. Grain Advancement
Each grain's state advances per sample:
When \(\text{phase} \geq 1.0\), the grain is deactivated.
7. Normalization
The grain sum is normalized by the square root of the active grain count to prevent volume buildup with high density:
8. Dry/Wet Mix
where \(\text{mix} = \text{mixPct} / 100\).
Core Equations
Where \(N\) is the number of active grains and \(m\) is the normalized mix.
Snapshot Fields
| Field | Type | Range | Unit | Description |
|---|---|---|---|---|
| Input Level | Float | 0--1 | Smoothed input amplitude | |
| Output Level | Float | 0--1 | Smoothed output amplitude | |
| Active Grains | Uint8 | 0--32 | Number of currently active grains | |
| Density | Float | 0.5--200 | Hz | Current grain trigger rate |
| Scan Position | Float | 0--1 | Normalized scan position within buffer | |
| Pitch Ratio | Float | 0.1--8 | Current playback speed ratio |
Implementation Notes
- CircularBuffer<262144> provides ~5.9 seconds of recording at 44100 Hz. The power-of-2 size enables mask-based wrapping (no modulo). Linear interpolation is used for fractional read positions.
- HannWindow uses a 1024-entry lookup table with linear interpolation, shared across all grains for efficient windowing.
- xorshift32 PRNG generates scatter randomness. The random value is mapped to
[0, 1]by dividing by0xFFFFFFFF. For scatter, it is then mapped to[-1, 1]with* 2.0 - 1.0. - Fixed 32-grain pool -- no heap allocation. When all slots are busy, new spawns are silently dropped. This keeps the audio thread real-time safe.
- Level tracking uses exponential smoothing with coefficient 0.01 for display-rate visualization.
- All parameters use
std::atomic<float>for lock-free thread safety. - Snapshot emission is decimated to ~60 fps (every 735 samples at 44.1 kHz).
Equation Summary
y = sum(grain[pos] * hann(env)), 32 grains