ENGINE=shuttle · src/engine/shuttle/shuttle_engine.{h,cpp} · class ShuttleEngine

A buffer-based bipolar/reverse varispeed tape - the random-access counterpart to the SD-streaming tape engine. Where tape streams forward-only from the card, shuttle holds audio in SDRAM, so the read pointer is random-access and reverse, freeze, and loop windowing are trivial. The trade is a finite, RAM-capped tape length (currently 30 s per track).

It runs four independent mono tape tracks - two per deck (A/B). All four free-run at their own speeds and sum into the stereo bus; you flip a deck's edit focus between its two tracks with the Rev pad (the edrums mechanism), and re-align all four to a common downbeat with the Seq pad.

The headline control is PITCH as a bipolar capstan-speed knob: noon stops the tape (silence), clockwise plays forward up to +2x, counter-clockwise plays in reverse down to -2x. Unity (+1x) lands off-centre, so the Play pad snaps the focused track to normal speed.

Implementation, architecture, the file map, and dev notes live in docs/dev/shuttle-impl.md.


Controls

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

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

ControlParamId / padScopeEffect
PITCHSpeedper-trackbipolar capstan speed: noon = stop (silence), CW = forward to +2x, CCW = reverse to -2x
POSPosper-trackloop window start (slides the loop through the recording)
SIZESizeper-trackloop window length (full buffer down to a short stutter)
MIXMixper-tracktrack volume
Alt + PITCHAuxper-tracktape-slot select (8 files/deck under /shuttle/) -> loads the slot into RAM
Alt + POSAltPosper-deckpan (equal-power)
Mix faderCrossfadeglobaldeck A/B blend
MOD_AMTModAmpper-tracktape-FX wow/flutter depth (see Tape FX)
MODFREQModSpeedper-tracktape-FX wow/flutter rate
grit + PITCHGritIntensityper-tracktape-FX saturation drive
grit + MIXGritMixper-tracktape-FX saturation character
flux + PITCHFluxIntensityper-tracktape-FX filter cutoff (boots fully open)
flux + MIXFluxMixper-tracktape-FX filter resonance
Play pad-per-deck (focused)toggle rolling; snaps the track to unity (+1x) on engage
Rev pad-per-deckswap which of the deck's two tracks is focused (the other keeps playing)
Alt + Play-per-deck (focused)record the focused track (play XOR record)
Seq pad-globalre-align all four tracks to their loop start (one atomic, declicked gesture)

The four tracks free-run independently, so they drift out of phase (organic tape phasing) - the Seq pad pulls them back to a common downbeat. Because the absolute pots can't physically move, the Play->unity snap holds via the platform's pickup soft-takeover (CapPitchPickup): after a snap, the PITCH pot must be swept across unity before it retakes the speed.

Loop window (POS / SIZE)

Each track plays a window [start, start+length) of its buffer, not always the whole recording:

The varispeed read pointer wraps within the window (both directions); the Seq re-align snaps to the window start.

Audio source: record + load

Tape FX

Each track can run the same tape-FX kernel as the tape engine - wow/flutter (a modulated fractional delay) -> Jiles-Atherton hysteresis saturation -> a post-FX resonant low-pass - shared verbatim from src/engine/tape/tapefx.h. The FX is per-track and addresses the focused track, like every other knob (it repoints on a Rev swap).

Because shuttle already uses POS/SIZE for the loop window (where the tape engine puts drive/character), the FX is driven from this engine's free controls - the two end-of-chain effect pads plus the MOD knobs:

Neutral = bypassed. With drive/character/wow at zero, cutoff fully open and resonance zero (the boot defaults), the kernel is skipped entirely rather than run flat: the wow/flutter delay line imposes a fixed ~25 ms delay even at zero depth and the filter is not bit-identity even fully open, so a "neutral" kernel would not be transparent. Skipping it keeps shuttle's varispeed playback bit-faithful when the FX is untouched, and confines the (non-trivial) Jiles-Atherton cost to tracks that are both rolling and actually using the FX. The summed bus is SoftLimited only while the FX is engaged (saturation + a resonant peak can overshoot 0 dBFS); with the FX off the bus stays a plain linear sum.

Build / test

make -j8 ENGINE=shuttle        # build the firmware
make -C host test-shuttle      # run the headless test (or `make -C host test` for all engines)