ENGINE=tape · src/engine/tape/tape_engine.{h,cpp} · class TapeEngine

A dual streaming tape deck: two independent mono decks (A/B), each playing or recording its own arbitrarily long file on the SD card, removing the in-SDRAM loop-length cap that bounds the other engines (~42 s float / ~84 s int16, kSourceMaxSeconds in src/config.h). A deck is play-XOR-record (no overdub), so the two run like a pair of record decks - play deck A while recording deck B, then play both together and beat-match by ear with each deck's PITCH. It is a linear player/recorder, not a granular scrubber - random SD seek cannot meet the 2 ms audio deadline, so each tape only ever streams forward.

Implementation, architecture, the file map, and the bug/bring-up writeups live in > docs/dev/tape-impl.md.


Audio I/O and routing

The hardware has two mono inputs (A, B; input A is normalled to B when B is unpatched), two mono outputs, and a stereo headphone monitor. The tape engine maps them as two independent decks blended to a stereo bus:

Three knobs/controls place the decks in that bus:

ControlParamId / configEffect
Routing switchConfigId::Routetopology (see below); also lights the L/C/R mode LED
Alt + POS (per deck)AltPosper-deck pan (equal-power, 0 = L, 0.5 = C, 1 = R)
MIX knob (per deck)Mixper-deck playback volume
Mix faderCrossfadeA/B blend (DJ-style: centre = both full, ends = one deck only)

(Bare POS is reserved for a future loop-start control and currently does nothing.)

The routing switch (mirrors the panel L/C/R, granular's int convention):

Total per-deck gain into the bus = MIX volume x mix-fader blend x pan(L/R).


Controls

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

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

ControlAction
Play pad (per deck)play toggle
Alt + Play pad (per deck)record toggle
PITCH (per deck, Speed)varispeed playback (exp2((v-0.5)*2) -> 0.5x..2x, +/-1 octave, pitch+speed linked)
Alt + PITCH (per deck, Aux)tape-slot select (visual selector while held, see below)
Alt + POS (per deck, AltPos)pan (LEFT routing)
MIX (per deck, Mix)playback volume
ENV (per deck, Env)loop mode (4 quadrants, see below)
Mix fader (Crossfade)A/B blend
Routing switch (Route)pan topology
POS (per deck, Pos)tape FX: saturation drive (see Tape FX)
SIZE (per deck, Size)tape FX: character
MOD_AMT (per deck, ModAmp)tape FX: wow/flutter depth
MODFREQ (per deck, ModSpeed)tape FX: wow/flutter rate
Hold grit + PITCH (per deck, GritIntensity)tape FX: filter cutoff (low-pass)
Hold grit + MIX (per deck, GritMix)tape FX: filter resonance

capabilities() = CapOwnDisplay | CapDualDeck | CapAux | CapAltPos. CapAux claims Alt+PITCH (tape-slot select); CapAltPos claims Alt+POS (pan) - both are platform-gated remaps, so non-tape engines are unaffected. It does not advertise CapTapeStorage - the tape engine owns its own SD streaming, so the platform Storage service stays out of the way (it mounts the card at boot and, in the tape build, leaves it mounted for the stream).

Tape slots

Each deck has 8 slots, files /tapes/tape_a_1.wav/tapes/tape_a_8.wav (and tape_b_), so takes are non-destructive and recallable rather than overwriting one fixed file. Alt+PITCH selects the active slot; while Alt is held, the deck's ring shows the 8 slots as evenly-spaced dots with the selected one bright (the same set_aux_active selector seam reso uses for its model picker). Selecting a slot sets the target for the next Play / record - it does not interrupt a deck already playing. Record writes the selected slot (overwriting only that one); Play reads it (amber if empty). The selector shows recorded vs empty slots (selected bright / recorded mid / empty dim). The /tapes/ directory is created on first record. Single-digit slot numbers keep the names 8.3-safe.

To load your own audio into a slot, the file must be mono 32-bit-float WAV at 48 kHz - the engine does no on-device conversion, and a wrong-format file (16-bit / 32-bit-int / stereo / non-48k) is rejected with a strobing amber error LED. Convert source files with scripts/convert_tape_audio.py or the ffmpeg/sox one-liners in docs/preparing-audio.md.


Loop modes (ENV knob, per deck)

The ENV knob picks one of four loop behaviors by quadrant, from fully CCW:

ENVModeBehavior
< 0.25Noneplay once, stop at end
< 0.5Plain loopseamless repeat at full level
< 0.75Faded looprepeat with a ~50 ms fade across the seam (de-click)
>= 0.75Frippertronicseach pass ~0.6x quieter; fades out over ~8 passes, then auto-stops

Loops are the recorded take's length (free-run, not tempo-aligned).

Varispeed

Playback is resampled in the ISR by a 2-frame linear interpolator: the engine advances _speed source frames per output frame and reads fractionally between the two, per deck. Pitch and speed move together (tape-style) - there is no time-stretch. Read-ahead and the SD pump scale with consumption automatically, so faster playback simply drains the ring faster.


Tape FX

Each deck has its own analog-tape effect chain on the playback signal: wow/flutter -> Jiles-Atherton hysteresis/saturation -> resonant low-pass, with the summed two-deck bus soft-limited so resonant peaks and two decks can't clip. POS = saturation drive, SIZE = character, MOD_AMT = wow/flutter depth, MODFREQ = wow/flutter rate; the low-pass rides the grit-modifier pad - hold grit + PITCH for cutoff, grit + MIX for resonance (cutoff boots open, so sweep it down to engage). The DSP/licensing/CPU details are in docs/dev/tape-impl.md.


Build / flash / test

make faust-kernels                                # regenerate the tape-FX kernel (needs the .venv cyfaust)
make engine-tape                                  # clean + build + DFU flash (Make path)
make -f Makefile.cmake ENGINE=tape program-dfu    # CMake path
make -j8 ENGINE=tape                              # build only (~86.9% SRAM_EXEC, incl. tape FX)
make -C host test                                 # host suites incl. test-tape / test-stream (all green)

On hardware, after ~1 s for the SD to mount: pick a slot per deck with Alt+PITCH (ring shows the selector), then Alt+Play(A) records input A and Alt+Play(B) records input B into the selected slots; Play(A) / Play(B) play them back simultaneously (A->L, B->R). Turn each deck's PITCH for varispeed, Alt+POS to pan (LEFT routing), MIX for volume, ENV for the loop mode; the mix fader blends A/B and the routing switch picks the pan topology. Files land under /tapes/ as tape_a_<n>.wav / tape_b_<n>.wav. </content>