Skip to content

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):

\[\text{buffer}[\text{writePos}] = x\]

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:

\[\text{scanPos} \mathrel{+}= \text{scanRate}\]

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:

\[\text{spawnInterval} = \frac{f_s}{\text{density}_{\text{Hz}}}\]

For each new grain, an inactive slot in the 32-grain pool is found. The grain's starting read position is calculated:

\[\text{readStart} = \text{writePos} - \text{posOffset} + \text{scatter} \cdot \text{posOffset} \cdot \text{random}(-1, 1) + \text{scanPos}\]

where \(\text{posOffset} = \text{position}_{\text{ms}} \cdot f_s \cdot 0.001\). The grain is initialized with:

\[\text{phase} = 0, \quad \text{phaseInc} = \frac{1}{\text{grainSize}_{\text{samples}}}, \quad \text{playbackRate} = \text{pitch}\]

5. Grain Summation

All active grains are summed. For each grain:

\[\text{env} = \text{Hann}(\text{phase})\]
\[\text{grainOut} = \text{buffer}[\text{readPos}] \cdot \text{env}\]
\[\text{grainSum} \mathrel{+}= \text{grainOut}\]

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:

\[\text{readPos} \mathrel{+}= \text{playbackRate}\]
\[\text{phase} \mathrel{+}= \text{phaseInc}\]

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:

\[\text{grainSum}_{\text{norm}} = \frac{\text{grainSum}}{\sqrt{N_{\text{active}}}}\]

8. Dry/Wet Mix

\[y = x \cdot (1 - \text{mix}) + \text{grainSum}_{\text{norm}} \cdot \text{mix}\]

where \(\text{mix} = \text{mixPct} / 100\).

Core Equations

\[\text{env}[k] = \text{Hann}\!\left(\frac{k}{\text{grainSize}}\right)\]
\[y_{\text{wet}} = \frac{1}{\sqrt{N}} \sum_{i=0}^{N-1} \text{buffer}[\text{readPos}_i] \cdot \text{env}_i\]
\[y = x \cdot (1 - m) + y_{\text{wet}} \cdot m\]

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 by 0xFFFFFFFF. 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