Four independent “tape heads” orbit a shared circular buffer at rational divisions (e.g., 3/5/7/11 of a bar). Each head has its own tone, feedback, pan, and mute; cross‑feedback ties them into a playable lattice. Quantize to MIDI clock or tap; humanize with micro‑jitter; momentary Flam and Reverse inject accents.
Tide Engine is a playable, polyrhythmic delay instrument. Think of four tape heads circling the same river of audio, occasionally cross-feeding into each other. The goal is a stable, stage-safe system that can still be pushed into chaos on purpose.
Real-time safety rules (audio thread):
- No heap allocs, no locks, no logging, no I/O.
- Bounded work per block; no unbounded loops.
- Denormal-safe processing (flush tiny filter states).
Memory story:
- Long delay storage expects external PSRAM.
- Power-of-two ring sizes keep wrap cheap and deterministic.
- Default ring length is
2^19samples/channel (~11.9s at 44.1kHz, ~2.0 MiB stereo int16).
Suggested first experiment:
- Set Head A to 1/1, Head B to 1/3, mute C/D.
- Set mix around 0.45 and xfeed around 0.25.
- Listen for the braid where the two divisions interlock and start to pull the stereo field.
For the full signal flow, see docs/ARCHITECTURE.md and docs/SIGNAL_FLOW.md. The diagram lives in docs/SIGNAL_FLOW.md.
Teensy 4.x + SGTL5000 (Teensy Audio Library), 44.1 kHz / 128‑sample blocks.
Anything reachable from AudioStream::update() must be real-time safe:
- No heap allocation (
new,malloc,std::vectorgrowth,String). - No locks/mutexes or spin-wait atomics.
- No logging/I/O (Serial/printf) or file access.
- Bounded CPU per block; no unbounded loops.
- Long delay storage expects external PSRAM.
- Use a power-of-two ring length so wrap is a bitmask (
idx & (size - 1)). - Delay buffers are preallocated once at init and never resized.
- Default ring length:
2^19samples/channel (~11.9s at 44.1kHz, ~2.0 MiB stereo int16).
For deeper context, see docs/ARCHITECTURE.md and docs/PARAMS.md.
- Learn the engine in
examples/teaching_rig/teaching_rig.ino(polyrhythmic heads, ping-pong xfeed, flutter-heavy shimmer). The teaching rig now cycles its preset bank at runtime from a button on pin3. - For wiring only, start with
examples/minimal/minimal.ino. - Select your Teensy 4.0/4.1 board and upload.
- Audio in/out via Audio Shield. Adjust
AudioMemory()as needed. teensy/main.cppnow supports tap tempo on pin3; when built with a MIDI-capable Teensy USB type it also follows USB MIDI clock.
- Install PlatformIO Core.
- Build the project:
pio run -e teensy40orpio run -e teensy41. - PlatformIO uses
teensy/main.cppas the entrypoint;examples/minimal/minimal.inostays as a sketch-only demo.
./scripts/lint_audio_thread_allocs.shenforces the no-heap rule in audio-thread code../scripts/smoke_desktop_renderer.shcompiles the desktop renderer, lists the preset bank, and renders an impulse through a preset.
core/— DSP core (engine, heads, clock, xfeed, smoothing, limiter, saturation).core/TidePresetJson.*— desktop preset JSON load/save around the canonicalTidePresetsurface.desktop/— offline desktop tools built directly on the core DSP.teensy/— Teensy adapter (AudioStreamwrapper + entrypoint).examples/— minimal wiring sketch.presets/— starter presets JSON.docs/— control map CSV.scripts/— repo lint checks.
- Core DSP tests run on desktop via PlatformIO:
pio test -e native - See
test/README.mdfor test purpose and regression notes.
- Build:
c++ -std=c++17 -Icore desktop/tide-render.cpp core/*.cpp -o tide-render - List presets:
./tide-render --list-presets - Render an impulse through a preset:
./tide-render --preset-name "Spring Tide" --impulse 128 --tail-seconds 4 --output out.wav - Render a WAV through a preset:
./tide-render --preset-index 1 --input in.wav --output out.wav
- Configure the shared desktop build:
cmake -S . -B build - Build the offline renderer from CMake:
cmake --build build --target tide-render
- The JUCE plugin shell lives in
desktop/juce_plugin/and wraps the sameTideEngineCoreused by the Teensy build and offline renderer. - The current desktop/VST3 manufacturer metadata is
BSSoundStudio. - The plugin embeds
presets/presets.jsonas its default preset bank and can load/save JSON preset banks from the editor. - The plugin can follow the host BPM via the
Tempo Sourcecontrol while still keeping the manualTempoknob as the fallback/internal tempo. - Each head also exposes a plugin-only
Divisionchoice parameter so DAW automation can step through common musical ratios without automating raw numerator/denominator pairs. Transport Phasecan restart modulation/random timing on host play, and optionally on transport jumps/loops, without doing an unbounded delay-buffer clear in the audio thread.- Build with an installed JUCE package or a JUCE checkout:
cmake -S . -B build-vst3 -DTIDE_BUILD_JUCE_PLUGIN=ON -DJUCE_DIR=/path/to/JUCE - Then build the plugin target:
cmake --build build-vst3 --config Release --target TideEngineVST3 - If you prefer a vendored checkout, place JUCE at
extern/JUCE/and omitJUCE_DIR. - Smoke-test the JUCE path locally:
JUCE_DIR=/path/to/JUCE ./scripts/smoke_juce_vst3.sh
MIT — see LICENSE.