# Granular Timestretch Engine Stereo loop capture and granular playback for **Daisy Seed on a Daisy Pod**. The release example is `example/main.cpp`; `pod/GranularTimestretch` is a separate older implementation, and `vst/` is a separate Rust plug-in port. Changes to one do not automatically update the others. ## Build and install From this directory, with the ARM toolchain and built libDaisy/DaisySP libraries: ```sh make clean make -j4 make program-dfu ``` The output is `build/GranularTimestretchEngine.bin`. This Makefile builds an **internal-flash image at 0x08000000**, not a QSPI application. It replaces any Daisy bootloader previously installed in internal flash. To return to PT32-V or another QSPI application, reinstall the Daisy bootloader first. For ROM DFU mode: connect the Seed USB port with a data cable, hold BOOT, press and release RESET, then release BOOT. Install using the repository’s `firmware-studio` webapp or the [official Daisy programmer](https://electro-smith.github.io/Programmer/). Browser flashing requires WebUSB, normally desktop Chrome or Edge, on HTTPS or localhost. After programming, press RESET if needed. Verify both audio channels at a low monitoring level. ## Pod controls Turn the encoder to cycle five banks. Moving a knob more than 3% from its bank-switch position unlocks it; this is movement pickup, not matching the previous parameter value. Hold Button 1 to capture and loop, release to bypass. Classic forward captures **from the press and processes immediately**. The read head plays the growing recording with the selected pitch, spray, gain and envelope; there is no dry first-pass wait. Reverse, stopped playback and ping-pong grab existing audio ending two frames before the press. Click the encoder to enable/disable USB MIDI clock sync. | Bank / LED color | Knob 1 | Knob 2 | Button 2 | |---|---|---|---| | Time / red | Manual grain: 24–300 ms; synced: whole through 1/32 note | Classic: −1× to +1×, center freeze; ping-pong: 0.5× to 2× | Classic / ping-pong | | Spray / green | Position jitter 0–200 ms, limited internally to half a grain | Stereo width 0–100% | Scan on/off | | Shape / blue | Trapezoid edge: 1–20% | Gain 0.8–1.5× | Hann / trapezoid | | Groove / yellow | Grain-duration jitter up to ±25% of a beat | Reserved | Auto-recapture on/off | | Texture / magenta | Classic-mode overlap 0–100% | Gain 1–2× (0 to about +6 dB) | Reserved | Shape and Texture adjust the **same gain parameter**. Overlap applies to Classic mode; Ping-pong uses one bouncing grain. Zero pitch freezes the read position and can produce silence if it stops at the window edge. Scan moves the loop window while recording continues; with Button 1 released, output is dry. ### LEDs LED 1 is white when fully engaged and blinks magenta while a synced activation is pending. Otherwise it shows the bank’s modifier: red/green for Classic/Ping-pong, green-tinted white for scan, cyan/orange for Hann/trapezoid, bright/dim magenta for auto-recapture, and magenta brightness for overlap. LED 2 meters input above 0.1 amplitude (green → yellow → red), with bright red for input clipping. At low input it shows the bank color dimly. With enabled sync and active clock, white quarter-note flashes overlay the meter. It does not pulse while waiting for clock. ### USB MIDI clock Sync defaults off. With sync enabled and clock present, Time knob 1 selects whole, half, quarter, eighth, sixteenth or thirty-second notes. Loop press/release waits for the next six-clock boundary (1/16 note). Clock is considered lost after 500 ms; pending gestures are then applied immediately. Disabling sync also resolves a pending gesture. MIDI Start resets the clock count. MIDI Stop is not a transport stop for audio; clock timeout and button release govern looping. Detected tempo accepts 40–240 BPM. Synced grains are clamped to 24–2,000 ms, so long divisions at slow tempos saturate at two seconds. Groove jitter changes grain duration on quarter notes, not MIDI event timing. Scan advance is derived from grain duration / beat duration and limited to 0.05–1 frame per audio frame; it is an effect, not a transport or beat-grid alignment guarantee. ## Engine integration ```cpp #include "daisy_pod.h" #include "GranularTimestretchEngine.h" static daisy::DaisyPod hw; static granular::GranularTimestretchEngine engine; static float DSY_SDRAM_BSS audioBuffer[48000 * 30 * 2]; static void AudioCallback(daisy::AudioHandle::InterleavingInputBuffer in, daisy::AudioHandle::InterleavingOutputBuffer out, size_t size) { hw.ProcessAllControls(); if(hw.button1.RisingEdge()) engine.ActivateLoop(); if(hw.button1.FallingEdge()) engine.DeactivateLoop(); engine.ProcessBlock(in, out, size); } int main() { hw.Init(); hw.SetAudioBlockSize(4); granular::EngineConfig config; config.buffer = audioBuffer; config.bufferSize = sizeof(audioBuffer) / sizeof(audioBuffer[0]); config.sampleRate = hw.AudioSampleRate(); if(!engine.Init(config)) return 1; hw.StartAdc(); hw.StartAudio(AudioCallback); for(;;) {} } ``` Compile `src/GranularTimestretchEngine.cpp` alongside your application and include/link DaisySP for its soft clipper. `bufferSize` and `ProcessBlock` size count **interleaved float samples**, not stereo frames. Thirty seconds at 48 kHz uses 2,880,000 floats / 11,520,000 bytes. Sample rate must be finite and 8–192 kHz. Odd buffer capacities round down to a stereo pair. Odd block sizes process complete pairs and leave the final sample untouched. Input/output pointers must be valid; in-place block processing is supported. ### Parameters and threading | Parameter | Accepted range | |---|---| | Grain size | 5–2,000 ms; past captures limited to 80% of recorded frames, forward captures to 80% of buffer capacity | | Pitch rate | −4× to +4× | | Spray | 0–500 ms, effective reach limited to half a grain | | Stereo spray width | 0–1 | | Effect gain | 0.8–2× | | Trapezoid edge | 0.01–0.20 | | Scan advance | 0.05–1 frame/frame | | Classic overlap | 0–1 | `SetParams` and individual setters clamp finite values and substitute defaults for non-finite input. Window and mode setters validate their enums. `ActivateLoop`, `DeactivateLoop`, `ToggleScan` and `SetScanActive` use an atomic **last-command-wins mailbox**, not a FIFO: multiple commands before the next sample overwrite each other. They may be called across threads; call parameter setters, getters and state access on the audio thread or provide external synchronization. `Init` and `ClearBuffer` require processing to be stopped; clearing the large SDRAM buffer is not a real-time operation. Clear resets active, scan and pending command state. The Pod example handles MIDI, controls, LEDs and audio in the audio callback, so its engine access has one owner. The foreground loop idles. Reinitialization with an invalid configuration returns false and leaves an already valid engine intact. ### Signal behavior and limits The recording buffer is stereo. Classic forward anchors at the press and processes the growing recording immediately. Engagement fades into wet audio over 3 ms; release fades back to dry over 10 ms. The first grain has a short, shaped attack rather than a full-grain Hann rise; its falling half and subsequent grains use the selected window. Reverse and ping-pong keep the newest capture edge two frames behind the press, with no zero-crossing search shifting it backward. This reduces discontinuities but does not guarantee click-free stereo output. Hermite interpolation smooths fractional reads; it is not an anti-aliasing filter. Pitch changes can alias. Wet output is soft-clipped; dry bypass is unchanged. Gain compensation is approximate and does not guarantee equal perceived loudness. During forward capture, both the read position and every interpolation tap are bounded to the portion recorded since the press. At speeds above 1× the head can catch the recording frontier; it then wraps through the available portion. This can produce short changing repeats until the slice is filled. Positive spray is limited by the same frontier. The engine cannot play future input. The slice length is pinned while filling; size and scan changes take effect afterward. Classic playback alternates overlapping grains. Ping-pong reflects a single grain at the loop boundaries. Double-precision read positions retain fractional motion late in the buffer. Holding without scan allows recording until the write head approaches the protected loop region, then parks it. After parking, reverse/ping-pong recapture cannot obtain audio that was never recorded; release or start a new forward capture to resume live recording. Scan keeps recording and can eventually encounter overwritten history. Grain-size and spray changes during a long hold still require a hardware listening check. ## Validation ```sh make -C tests test ``` The host regression program uses AddressSanitizer and UndefinedBehaviorSanitizer. It covers bypass, overlap handoff, clear/reinitialize, invalid controls, forward/reverse/fractional playback, and small/partial buffers. See `RELEASE_CHECK.md` for the specific release results and the remaining hardware checks. CPU load, click audibility, MIDI timing under load and end-to-end USB flashing have not been measured by these host tests; do not treat a successful build as a hardware qualification. `BpmSync.h` is an optional standalone tempo helper with a different division table (eighth through 1/128 including dotted/triplets). The release example uses its own USB MIDI logic; `example/main_midi_sync.cpp` is not the default build. ## License See the repository [LICENSE](../../LICENSE) and the dependency licenses.