ENGINE=pstretch · src/engine/pstretch/pstretch_engine.{h,cpp} · class PstretchEngine

A real-time PaulStretch ambient time-smear. PaulStretch (Nasca Octavian Paul) turns audio into a diffuse, evolving spectral wash by FFT-ing large overlapping windows, randomizing the phases (keeping the magnitudes), and overlapping back to time. Advancing the analysis read head slowly - or freezing it - stretches and smears time without limit.

Each deck (A/B) runs an independent stretcher over its input channel (A = left, B = right, like the delay engine); the two are blended by the crossfader and placed by the routing switch. This is a clean-room reimplementation written from the published algorithm description (not derived from the GPL PaulStretch / PaulXStretch sources), so it stays MIT, and it is self-contained - a small vendored FFT, no external DSP library.

Implementation, the source modes (live / capture / SD-file), and the design notes live in docs/dev/pstretch-impl.md.


Controls (per deck)

Pstretch control surface
Pstretch control surface - open full size download PDF

_Generated from docs/diagrams/controls/pstretch.json via make diagrams._

ControlParamId / configEffect
SIZE (Size)STRETCH amount, 1x..64x (exponential) - how slowly the read head crawls. More = more smear.
POS (Pos)DIFFUSION - 0 = clean window resynthesis, 1 = full phase-randomized PaulStretch wash.
PITCH (Speed)pitch shift of the grain, +/- 1 octave.
ENV (Env)tone - a one-pole low-pass to soften the wash (open at max).
MIX (Mix)dry/wet.
Mode switch (Mode)SOURCE select (per deck). The 3-position switch is silkscreened (top to bottom) Reel / Slice / Drift - legacy labels from the stock granular firmware; here they map Reel = Capture, Slice = Live, Drift = SD-file (stream a clip from /pstretch). See below.
Play padFREEZE the read head - an infinite, evolving drone on the current spot. Works in any source.
Rev padRE-GRAB the ring at the current instant - refreshes the Capture snapshot (Capture mode only; no-op in Live/SD).
Alt+PITCH (Aux)CLIP select (SD source) - pick which clip in /pstretch this deck streams. Takes effect live if the deck is already streaming, else on the next switch to SD. Hold Alt to show the selector.
Alt+POS (AltPos)SCRUB (SD source) - seek the stream playhead to a position in the clip (debounced, so a sweep opens where you land; works while frozen to audition spots).
Cycle (ModSpeed)MOD rate - the per-deck LFO speed (~0.03..8 Hz). Alt+Cycle locks the rate to the tempo (musical divisions).
Glow (ModAmp)MOD depth - 0 = modulation off (the engine is un-modulated until you raise this).
Mod Type switch (ModType/LfoShape)LFO shape (sine / triangle) or Follow (input-envelope follower).
Size/Pos mod switch (StartModOn/SizeModOn)MOD target: Pos = diffusion, Size = stretch, both = tone (pitch is modulated via the V/Oct CV jack).
CV in (cv_voct/cv_size_pos/cv_mix/cv_crossfade)additive CV: V/Oct -> pitch, Size/Pos -> stretch, Mix -> dry/wet, Crossfade -> A/B blend.
Gate inin Capture, re-grab the ring; in Live/SD, toggle freeze - rhythmic re-sampling / stutter from a clock.
Mod CV out (process_cv)each deck's LFO as a 0..1 CV (free-runs as a usable modulation source even at Glow=0).
Gate out (gate_out_triggered)a pulse on every LFO cycle - a tempo-synced clock/reset out when the LFO is clock-synced.
Mix fader (Crossfade)A/B blend of the two decks.
Routing switch (Route)stereo topology (below).

Source modes - what the stretch acts on

The stretch is orthogonal to its source - the analysis ring is just "what's being read," and the per-deck Mode switch selects what fills it. That switch is a 3-position toggle silkscreened (top to bottom) Reel / Slice / Drift; those are legacy labels from the stock granular firmware and do not describe pstretch's use. Here they select the source:

Switch positionLabelSource
topReelCapture
middleSliceLive
bottomDriftSD-file

Live and Capture are pure live effects (no files); SD streams a clip. All three respond to SIZE / PITCH / DIFFUSION / ENV / MIX and to Freeze identically. The two decks are independent, so you can run one Live while the other streams an SD clip and blend them on the crossfader.

SD-file source

Put 16-bit mono .wav (or .raw, assumed 48 kHz) clips in a flat /pstretch folder on the card (8.3 names, at least ~32 KB each, up to 32 clips); the folder is scanned once and sorted alphabetically, so Alt+PITCH position N is the Nth clip by name (zero-pad any numeric names). Both decks stream independently.

Because the stretch makes the read head crawl (it consumes the source at input_rate / stretch, ~1 KB/s at 50x), the clip is streamed slowly from the card - never fully loaded into RAM - so an arbitrarily long file plays for hours. Off-rate clips (e.g. 44.1 kHz) are pitch-corrected from the .wav header, so they play at native pitch rather than sharp. The card mounts a moment after boot and the engine keeps rescanning until a clip streams, so a slow or late-inserted card self-heals; if SD is selected but the folder is empty/unreadable the pad LED turns red (vs magenta when streaming) as a diagnostic.

Modulation, CV, and gate

The engine self-animates and patches into a modular rig. All of this is per deck and independent of the source (Live / Capture / SD), and everything is off by default - the un-modulated engine is unchanged until you raise Glow or patch a jack.

Routing / stereo image

Display

Per deck the play LED and a ring marker show the stretch amount and the state colour: green = live, amber = capture (looping a grab), magenta = SD (streaming a clip), red = SD selected but no clips found, cyan = frozen (takes priority). While Alt is held on a streaming SD deck, the ring shows a dot per clip with the selected one bright, instead of the stretch marker. The routing switch lights the mode L/C/R indicator.


Notes

WINDOW=8192WINDOW=4096
CPU avg41.5%33.3%
CPU max87.6 -> 88.2 -> 91.3%63.47 -> 63.57 -> 63.76%
CPU min2.00%1.93%
SRAM (312K)97.40%82.01%
SRAM_EXEC (200K)94.38%94.35%
make test-hw30 passed30 passed

Read the max column, not the average - and read it as a sequence. At 8192 the peak was still CLIMBING through the sample (87.6 -> 91.3, +3.7 pp), i.e. it had not converged and 91.3% is not the ceiling, just the worst block seen in six passive seconds. At 4096 it converged immediately (+0.3 pp drift) and reads like an actual bound. So the real 8192 headroom is smaller than "100 - 91" suggests and is not yet known; driving both decks hard will push it further. Neither figure is a worst case - both were measured passive.


Build / flash

make -j8 ENGINE=pstretch      # build (8192 window; WINDOW=4096 for the lighter build)
                              # uses linker/alt_sram_pstretch.lds automatically - pstretch does NOT
                              # link against the default 300K/212K split (see that file's header)
make ENGINE=pstretch program-dfu
make engine-pstretch          # one-shot: clean + build + flash (device in DFU mode)
make -C host test             # host suites incl. test-pstretch