Firmware

sk-engines

Hype14/100

Alternative 'engines' firmware for the Spotykach.

sk-engines is custom firmware for the Synthux Academy Spotykach, by @shakfu. Open source under MIT, latest release 0.6.1 (July 2026), 8 stars on GitHub.

forSynthux Academy Spotykach·by@shakfu

Unclaimed. Is this yours? Sign in with GitHub as @shakfu to claim it.

Downloads
0
Stars
8
Forks
0
Commits
273
Last commit
Download 0.6.1

sk-delay-0.6.1-1-g4faeddd.bin· 151 KB· +20 files

MIT· shakfu/sk-engines·synthux.academy/store/spotykach·downloads come from the creator’s GitHub releases

266
Commits
7
Releases
266 commits and 7 releases in the last year
Activity · last 12 months
7 releases
  1. 0.6.1sk-engines 0.6.1Latest21 files

    Changes since the last release

    Added

    • On-target engine testing over USB-C (TERMINAL=1) — hardware-verified. A bidirectional text/command channel on the rear USB-C CDC port lets a host script drive an engine into a known state, exercise a feature and assert on a property read back, with no knobs, patch cables or MIDI gear. The whole IEngine input surface is addressable by name (set param, config, cv, gate, midi, pad, fx), state is readable (query empty|mix|route|gateout|usb), and mode test freezes the physical input path so injected stimulus is the only thing driving the engine. describe emits the device's own control surface so a host configures itself instead of hardcoding per-engine knowledge. Zero cost when off - the terminal TUs compile to 0 bytes and the default binary is unchanged. (docs/dev/terminal-control.md, docs/dev/terminal-impl.md)

    • make test-hw — the on-target counterpart to make test. A pytest harness (tools/) plus a skdev client library and skterm.py REPL drive a flashed device over serial. test_generic.py is driven entirely by describe, so one file sweeps every engine build; with no device attached the run skips cleanly, so it is safe in a hardware-free pipeline. First real runs: delay's and qdelay's six declared params round-tripped across both decks. (docs/dev/terminal-tools.md)

    • L1 state-query coverage in the generic sweep. Two further describe-driven tests, again with no per-engine knowledge: every advertised query must answer on every deck its scope claims, and any config with a same-named query must be observable through it in the same encoding - the automatic form of the config route / query route mismatch found by hand. parse_describe now keeps each query's scope.

    • Off-target coverage of everything below the USB transport. host/test_terminal.cpp exercises the tokenizer, line assembler, SPSC ring, TX FIFO, value coercion, float formatting (integer decomposition - the firmware links no _printf_float), every verb's IEngine binding and exact reply bytes, the error taxonomy, and describe, against a recording mock engine under make test. It also writes the real describe block to build/describe_sample.txt, which tools/test_descriptor.py parses through the actual host parser - closing the firmware/host loop without a device.

    • Per-engine liveness masks (live_params() / live_configs()) on eleven engines. describe lists only what an engine actually implements, and the descr line reports masked=0|1 so a host sweep can skip engines still on the "all live" default rather than emitting false failures. delay and qdelay are hardware-verified; the rest are derived from each engine's set_param/set_config switch and not yet swept. granular's mask deliberately excludes AltPos and Aux, which its switch names but treats as explicit no-ops - a sweep would otherwise set them and assert on a value that went nowhere. Since the SRAM_EXEC rebalance every engine fits; see docs/dev/terminal-impl.md.

    • ABI tagging for layout-affecting build flags (src/abi_tag.h). SPK_TERMINAL adds a member to CoreUI and virtuals to IEngine; TERM_USBDIAG adds another CoreUI member - so objects built with and without them are not link-compatible, and mixing them silently shifts member offsets and vtable slots. The affected types now live in an inline namespace whose name encodes the flags, turning that into a named link error (undefined reference to spotykach::abi_t1d0::CoreUI::render_leds()) instead of a runtime mystery. Verified by deliberately linking a mismatched object; costs nothing at runtime, as it only changes mangled names.

    • USB bring-up diagnostic (USBDIAG=1). Renders eleven checks - clocks, HSI48, USB clock source, VDD33_USB, transceiver, D+ pullup, VBUS sensing, both D-/D+ pads' alternate function, bus reset, SOF - on the panel LEDs and as a blink code on the Daisy onboard LED, plus query usb as text. Built because a cased unit hides the onboard LED and SWD pads, and the channel that would normally report on itself was the thing that was down. Off by default; the build stays fully runnable.

    • SRAM_EXEC rebalanced 186K -> 300K (alt_sram.lds), which was never a hardware limit. The H750's 512K AXI SRAM was split 186K code / 326K data; code sat at 99.29% (tape + terminal) while ~260K of the data region went unused, and softcut, bard, reverb, reso and granular could not host the terminal at all. Now 300K/212K: every engine fits with margin - worst code 69.93% (granular + terminal), worst data 68.81% (bard + terminal) - and the per-engine fit table in docs/dev/terminal-impl.md is largely moot. Verified on hardware rather than only at link time: a 214812 B granular + terminal image, 24 KB past the old ceiling, boots and enumerates, so bootloader-spotykach-v2.bin copies well beyond the old region. The tested lower bound is ~215K, not 300K; an image approaching 300K should still be flash-tested.

    • Composite verbs: reset and preset save|load <slot>. reset [deck] drives every advertised param to IEngine::param_default() (new virtual, 0.5 unless an engine overrides); preset snapshots and restores them through in-RAM slots. Both operate on exactly what describe advertises and reply with a count. reset closes a real hole in the harness - mode test froze the panel but left the previous test's writes in place, so tests were independent only by luck; tools/conftest.py now resets per test. Presets are non-persistent by design (a test snapshots many times a run and should not wear flash) and params only, because IEngine has no config getter - set_config is write-only. Neither applies its writes atomically with respect to the audio block; noted rather than built, since nothing has been bitten by it.

    • The rest of the IEngine input surface is now reachable from the terminal. terminal-control.md claimed the channel drove "the full IEngine input surface"; it did not. Added fx lock <flux|grit> <deck>, fx gritmode <deck> (an action that returns values - the intensity/mix pair the platform uses to reseed its pickup), seq trig|arm|clear|disarm <deck>, and the state reads query recorded|capacity|layout|sizetempo <deck>. Two more are reachable by name but deliberately not advertised in describe: query fit <deck> <fraction> takes an argument the descriptor cannot express, and query reseed <deck> is a latching read that self-clears - a generic sweep would consume the flag the platform is waiting for. set_aux_active is left unexposed because the UI re-asserts it every process(). Off-target tested; not yet exercised on hardware.

    • get param crossfade returned 0 on six engines. set_param(ParamId::Crossfade) stores the value and derives the A/B blend gains, but param() had no matching case in bard, glitch, pstretch, radio, shuttle, softcut or tape - the setter was copy-pasted between engines and the getter never was. Only the terminal ever reads this param back, so nothing else noticed. Found by the on-target sweep on tape, then generalised. Worth noting granular and graincloud were never affected: they return from a generic _param_cache[id][deck], so a grep for the symbol wrongly flagged them and classifying by mechanism (cache lookup vs if-chain) was what gave the right answer.

    • Host client: one timeout no longer poisons a whole session. The protocol is synchronous, so a late reply from a timed-out command was read as the next command's answer, and every subsequent reply stayed offset - surfacing as nonsense parse errors far from the cause, non-deterministically. Device now drains pending input before each command, so a timeout costs only that command; the default read timeout is 3 s rather than 1 s, since the channel is pumped from the main loop and a streaming engine scanning SD in prepare() stalls replies (a full sweep is ~0.1 s on delay, ~12 s on radio).

    • The generic param sweep no longer assumes exact float round-trip. Quantized params are legitimate - tape's aux selects one of 8 SD slots, so 0.5 reads back 0.5625 - and the descriptor cannot declare a step. It now asserts the param tracks input (two targets, two distinct readings) and is a fixed point (writing back its own reading is stable), which tolerates quantization while still catching a missing getter, and names which of the two failed.

    • Target B built (B1): engine-specific state is now declared, discoverable and swept. IEngine gains engine_queries() and read_engine_query(); an engine declares a static table (name, scope, ValueKind, enum labels, safe) and answers by index, and the platform does the matching, deck validation, reply framing and describe emission - so an engine writes no parser and dispatch cannot drift from description. The platform's own queries were converted to the same table, so describe walks one code path over both halves and a host cannot tell them apart. Wire format gains a kind token (query <name> <scope> <kind> [labels]), backward compatible - verified by sweeping a device running pre-kind firmware. Only safe (idempotent, side-effect-free) entries are advertised, which makes the generic sweep correct by construction. CapTerminal is now derived from a non-empty table instead of hand-set. First users: tape (slot, loopmode, speed - the varispeed actually in effect) and radio (station - which station is really streaming, so a test can assert a seek completed rather than that a knob moved). (docs/dev/terminal-target-b.md)

    • Design: target B redesigned so engine-specific state can actually be used (docs/dev/terminal-target-b.md). The handle_command hook shipped in phase 1 and no engine has ever implemented it - not because the mechanism is wrong, but because it is invisible to describe, makes every engine re-implement matching and error handling, and carries no type information, so the generic sweep cannot see it and every use would need per-engine host code. The proposal replaces the free-form read path with a declared query table (name, scope, kind, enum labels) answered by index, so dispatch and description share one source of truth and drift is structurally impossible; the platform takes over deck validation and reply framing; and engine queries emit as ordinary query lines, which is backward compatible and means the existing sweep covers them with no new host code. Entries are tagged safe = idempotent && side-effect-free rather than sorted into "queries" and "actions" - only safe entries are advertised, so the sweep is correct by construction. The free-form hook stays for side-effecting verbs. Unbuilt; phased with two real users (tape loop_ms, radio station) to validate it by use.

    • The command model is now written down (docs/dev/terminal-control.md). Params/configs, queries and actions, the two axes that separate them (is there a stored value; does calling it change anything), and why a reply does not distinguish an action from a query - pad play A returns ok empty=1 and config mode A 1 returns whether it changed. What matters to tooling is repeatability, which is the sole basis for what the descriptor-driven sweep may call. Includes the three cases in this codebase where the categories leak: set_config's changed flag, take_param_reseed's latching read, and toggle_grit_mode reading like a getter.

    Fixed

    • The terminal was on the wrong USB peripheral. The Spotykach's rear USB-C is wired to OTG_HS (PB14/PB15), not the Seed's own OTG_FS pins - as hw/hardware.cpp and the Makefile's unconditional INFS_LOG_TARGET=LOGGER_EXTERNAL both already implied. The channel had been driving two pins this board connects to nothing, which is why DFU always enumerated on a connector the app could not use. SPK_TERMINAL_PORT_EXTERNAL now defaults on; build a bare Seed or Pod with TERMPORT=int.

    • Build-flag changes could produce a silently corrupt binary. Under GNU Make 3.81 (what macOS ships) mtimes compare at whole-second resolution, so a stamp rewritten in the same second an object was compiled did not out-date it - toggling TERMINAL gave a partial rebuild whose stale subset depended on timing. Because these flags change type layout (CoreUI, AppImpl, the IEngine vtable), the result was mismatched member offsets: a frozen, garbled panel with a working command channel. Stamp recipes now delete the objects outright and are prerequisites of every object.

    • TX staging buffer was reused while a transfer was in flight. CDC_Transmit_FS returns OK once a packet is queued; in non-DMA mode the HAL copies from the caller's buffer later, from the FIFO-empty interrupt. The main loop refilled it well inside a USB frame, corrupting multi-packet replies. Now ping-ponged, swapping only on a successful transmit.

    • Dropped replies were silently swallowed, so a full TX FIFO cost the host a whole reply and read as an unexplained timeout. Now reported as err tx-overflow.

    • describe advertised parameters that never reach an engine. Tempo and KeyInterval live in the Transport service and ModSpeed arrives via set_mod_speed(), so a generic sweep set values that went nowhere and asserted on whatever the engine happened to store. They are no longer advertised (still addressable), which also removes the only non-normalized ranges - their declared display units were never the units set_param takes.

    • config route and query route spoke different encodings, so config route A 0 read back as 2 and route could not be round-tripped. query route now reports the selector encoding config accepts and describe publishes.

    • mode test did not freeze the panel switches. Unlike knobs, CV and gate, switches assert their position every iteration, so any config the terminal set was overwritten within a millisecond - making every panel-owned ConfigId untestable. Now gated with the other three consult points; mode run hands control back automatically.

    • mode test had no way out if a harness died, leaving knobs, CV and gate frozen with no recovery short of a power cycle. Now reverts after 30 s without a command (SPK_TERMINAL_TEST_MODE_TIMEOUT_MS); any command resets the timer.

    • TERMINAL=1 with USB_MIDI=1 is now a compile error. MidiUsbHandler claims the same OTG_HS core as the default terminal port, and USB_MIDI defaults on for every BOOT_QSPI build - two USB device stacks on one peripheral.

    • Host tooling had never been executed and did not work. serial.Serial(..., dtr=True) raises (dtr is a property, not a constructor keyword) so nothing could connect; readline.parse_and_bind("tab: complete") is silently ignored by the libedit backend macOS ships; skdev.descriptor could not be imported without pyserial, breaking device-free collection; and make test-hw invoked the system python3, which has neither pyserial nor pytest. All fixed, the last via a PYTHON variable that prefers the project venv.

    • query usb reported init-time state as live. The refresh only ran under USBDIAG, so in a normal build the host-activity bits could never read anything but zero - the worst possible failure for a field whose job is answering "is the host talking to us".

  2. 0.6.0sk-engines 0.6.021 files

    Changes since the last release

    Added

    • bard engine (ENGINE=bard) — a bookmark-navigated audiobook / storyteller, hardware-verified. Streams long-form spoken-word books from SD (/bard/<shelf>/) with WSOLA pitch-preserving speed, a resume table that remembers where each book left off, auto-marks, and a room reverb. Alt+PITCH selects the shelf, PITCH the book; SIZE = speed, ENV = pitch-keep amount. Now part of the make dist release set. (docs/engines/bard.md, docs/dev/bard-impl.md)

    • pstretch engine (ENGINE=pstretch) — a real-time, clean-room PaulStretch ambient time-smear, hardware-verified. Each deck turns its input into a diffuse spectral wash (FFT → phase-randomize → overlap-add; slowing or freezing the read head stretches time without limit). SIZE = stretch 1×–64×, POS = diffusion, PITCH = ±1-oct grain shift, ENV = tone, MIX = dry/wet, Play = freeze; the per-deck Mode switch picks the Live / Capture / SD-file source. A clean-room MIT reimplementation with a vendored FFT (no CMSIS-DSP). (docs/engines/pstretch.md, docs/dev/pstretch-impl.md)

    • pstretch: 8192-sample default window + per-deck modulation, CV, and gate — hardware-verified. The default analysis window is now 8192 (~171 ms) for a lusher wash (WINDOW=4096 for the lighter one). Adds a per-deck mod LFO (tempo-lockable) routable to diffusion / stretch / tone, additive CV inputs (V/Oct, Size/Pos, Mix, Crossfade), a gate in (re-grab in Capture, freeze-toggle in Live/SD), and CV/gate out — so pstretch doubles as an LFO / tempo-synced clock source. All off by default. (docs/engines/pstretch.md)

    • glitch engine (ENGINE=glitch) — a dual-deck lo-fi / circuit-bent noise voice, host-tested (hardware pending). Each deck runs one of 12 curated algorithms ported from Rob Scape's Noisferatu (buffer glitch, logic-noise, generative blips, rhythmic noise) — the digital-glitch aesthetic the platform's cleaner engines don't cover. Alt+PITCH scrolls the algorithm; SIZE/POS are its two params. GPLv3 (inherits Noisferatu's license), unlike the MIT rest. (docs/engines/glitch.md, docs/dev/glitch-impl.md)

    • qdelay engine (ENGINE=qdelay) — a dub/ambient flavor of the delay, hardware-verified. Keeps the delay engine's whole control grammar and swaps the character palette to Clean / Diffuse / Duck: Diffuse smears repeats through an 8-stage allpass into a reverb-like wash, Duck attenuates the wet while you play so repeats bloom in the gaps. Adds src/dsp/diffuser.h (a JUCE-free port of qdelay's Diffusor). GPLv3 (derived from qdelay), unlike the MIT rest. (docs/engines/qdelay.md, docs/dev/qdelay-impl.md)

    • delay: Reverse on the Rev pad. The delay engine's Rev pad now toggles a per-deck click-free reverse read that composes with all characters and topologies; the play-pad LED shows cyan when reversed. Hardware-verified. (docs/engines/delay.md, docs/dev/delay-impl.md)

    • softcut engine (ENGINE=softcut) — a dual-deck crossfaded overdub looper on monome's softcut-lib, hardware-verified. Four voices (two per deck) that play and record the same loop at once, with click-free crossfades and interpolated overdub — the sound-on-sound tape/shuttle can't do. Record-defines-loop semantics (Alt+Play to record/close), per-voice SD load and save. (docs/engines/softcut.md, docs/dev/softcut-impl.md)

    Changed

    • All own-display engines migrated onto the shared indicator toolkit (src/engine/indicators.h). Following a full-tree audit (indicator-comparison.md §7), the 13 engines that hand-rolled their LED grammar (bard, csound, chuck, softcut, radio, glitch, pstretch, reso, mosc, delay, qdelay, reverb, edrums) plus the Faust engines now call the toolkit instead of duplicating it: the 9 hand-rolled Alt-held selectors → ring::selector/ring::slots, the ~8 copies of the route L/C/R block → led::route_leds, meters/markers → ring::level/ring::playhead, softcut's libm-cos breathe + transport ladder → motion::breathe_standby/transport_view, and the per-engine near-miss palette constants (0x00c0ff/0x00aaff≈kCyan, reso's drifted mode hues) unified onto pal::. Also lit previously-dark indicators — led::route_leds on edrums, a non-blank meter=false floor on the Faust engines. All engines build clean; net-new indicators needing per-engine data (ring::value pickup feedback, led::clock, led::cycle for the LFO engines) are deferred. On-panel appearance not yet hardware-verified. (docs/dev/indicator-comparison.md, src/engine/indicators.h)

    • Synced platform-level changes from upstream Spotykach v1.2.0/v1.2.1. Ported the fork-worthy upstream improvements into the sk-engines platform — external-clock dropout recovery + downbeat re-alignment in Transport, USB MIDI over the rear USB-C (host-built, hardware test pending), the SD-slot selector off-by-one fix, a generic WAV cue-point capability (CapWavCues + wav_cues.h, platform-parsed so any engine can use it), and a Hann-table dedup that trims per-TU copies out of the audio hot path. The granular cue/DSP rewrite is intentionally not chased — the granular engine is the frozen compatibility reference. (docs/dev/1.2.0-upstream-sync.md)

    • CMake build (CMakeLists.txt + Makefile.cmake) brought to full parity with the Makefile. The opt-in CMake path (previously WIP and broken for every engine) now builds the same 20 engines as the canonical Makefile — including the QSPI-execute mosc/csound/chuck that were Make-only — and its SRAM_EXEC gap vs the Makefile is closed to ~0.65%. The Makefile stays canonical.

    • gigaverb (ENGINE=gigaverb): SIZE now slews roomsize instead of writing it raw. Raw writes made the reverb tank's delay taps jump and crackle on every knob step; SIZE now glides to its target (~40 ms one-pole) so turning it is quiet. Only roomsize is slewed. (src/engine/gigaverb/gigaverb_engine.h, src/engine/gen/gen_engine.h)

    • Project licensing is now MIT with documented GPLv3 exceptions. Two new engines add GPLv3 code — qdelay (src/dsp/diffuser.h + src/engine/qdelay/) and glitch (src/engine/glitch/) — so any binary of those is GPLv3 while every other engine stays MIT. Documented in a ## License section in README.md, with per-engine LICENSE/NOTICE.md and SPDX headers.

    Removed

    • Reverb: the parked Freeverb voice (freeverb.dsp + faust_kernel_freeverb.h) is deleted. It was never wired into the engine (active voices are Dattorro plate / Zita hall / Greyhole) and wasn't liked. No build or behaviour change.

    Fixed

    • softcut: SD-slot load-on-release, fresh-take length, and cleanups (from a review pass). Alt+PITCH slot select now previews and loads only on release (committed in prepare() once the deck is free), instead of firing a blocking f_open on every slot crossed — so scrubbing the selector no longer stutters the main loop or error-flashes when a load is mid-flight (the radio engine's settle pattern), and a slot change is ignored while a fresh take is recording. A fresh take's record rate is also frozen at 1× while defining, so its loop length is how long you played rather than the integrated PITCH rate (the closed loop snaps back to unity, matching the Play/overdub engage). Plus: process() zero-fills the output tail if the audio block ever exceeds kMaxFrames (silence, not garbage — no effect at today's 96-frame block, well under the 256 cap), and an unused 1 KB kZero global is removed. The remaining review items — a benign load-while-rolling buffer morph and a one-block loop-bound torn-pair — are accepted as the standard word-atomic scalar-race class. (src/engine/softcut/softcut_engine.{h,cpp})

    • edrums: Alt+PITCH (drum-tone select) now shows a live ring selector. Choosing a drum's tone with Alt+PITCH gave no persistent feedback — only a brief flash, and only when the value actually changed. edrums now tracks the Alt-held state (set_aux_active) and, while Alt is held, draws a ring::selector of all five tones (the current one bright, in the focused drum's colour). Hardware-gated like the rest of the indicator work. (src/engine/edrums/edrums_engine.{h,cpp})

    • make -C host test restored to green (the harness had rotted to where no test built). Three pre-existing issues, all host-only (the ARM firmware was unaffected throughout): (1) a first-party latent-include sweep — 65 headers across dsp//granular//graincloud//transport//hw//ui//memory/ used bare size_t/uint*_t/std::sin via transitive includes only the ARM toolchain supplies; added the missing <cstdint>/<cstddef>/<cmath> (add-only, no vendored code touched); (2) a test_engine_params link failure (undefined LEDRing::clear, GranularEngine vtable) that was a compile-cascade — resolved once the sweep let led.ring.cpp/granular_engine.cpp compile again; (3) a use-after-free in test_pstretch.cpp — the HostArena was scoped to just the init() call, so it was freed before process() ran and the engine's ~1 MB (mmap-backed) input ring dangled → SIGSEGV; the arenas are now pooled to outlive the engines (mirroring the persistent SDRAM arena on device). All 22 suites build and pass.

    • Host test suite builds under Apple clang 21 (macOS Tahoe). Clang 21's stricter front-end broke make -C host test at three host-only spots (a missing <cstddef>, a non-const VLA array bound, and a stale stmlib include path); all 22 host suites pass again. The ARM firmware build was unaffected.

    • csound: undefined reference to 'posix_memalign' at link after the beta17 bump. csound beta17's csoundCallocAligned calls C11 aligned_alloc, whose nano-libc implementation needs posix_memalign (absent under nosys). The SDRAM-pool shim now also --wraps aligned_alloc and serves it from the pool, so make dist links the csound engine again. (docs/dev/csound-impl.md)

  3. 0.5.1sk-engines 0.5.116 files

    Changes since the last Release

    Added

    • Real ChucK MidiIn on the bare-metal build (prototype; build-verified, on-hardware wake test pending). libchuck.a is now compiled with ChucK's MidiIn/MidiOut device classes enabled (__DISABLE_MIDI__ dropped from all three ChucK define sites — scripts/fetch_chuck.sh, the firmware Makefile, and pod/Makefile.chuck, kept ABI-consistent), but with midiio_rtmidi's RtMidi backend stripped: there is no OS MIDI API or callback thread on bare metal, and rtmidi.cpp (166 KB) stays out. A new MidiInManager::inject() feeds bytes straight into ChucK's per-VM MIDI buffer + event-wake path, so a .ck patch can use the desktop-portable MidiIn min; min.recv(msg); idiom (examples/chuck/midi_in.ck) instead of host-specific globals. The full channel-voice MIDI stream is forwarded (real velocity, NoteOff, CC, pitch-bend, aftertouch, program change) plus system realtime (clock/start/continue/stop): a new IEngine::handle_midi_message(status, d1, d2) virtual (no-op for engines that only use handle_midi_note) is driven from the UI MIDI poll (core.ui.midi.cpp) for every representable event; ChuckEngine overrides it to enqueue raw bytes on a lock-free ring (chuck_midi_in.h), and ChuckEngine::process() drains it and injects into device 0 on the run() thread, right before ck->run(), so a shred blocked on min => now is woken that block. ChuckEngine::handle_midi_note is kept only to map channel→deck for the panel gate-in LED. The vendored-ChucK edits live in scripts/patches/midi_daisy.patch, applied idempotently by scripts/fetch_chuck.sh (because thirdparty/chuck is gitignored and regenerated). The firmware (make engine-chuck) links with matching ABI. Ported from the daisy-apps sibling project; replication guide in docs/dev/chuck-midi-in-porting.md, and the user-facing capabilities + remaining incompatibilities (no MidiOut, single virtual device / no enumeration, 3-byte messages only) are documented in docs/engines/chuck.md. (SysEx / system-common aren't representable in a 3-byte MidiMsg and are not forwarded; on-hardware wake test still pending.)

    • mosc engine (ENGINE=mosc) — a dual macro-oscillator on the Mutable Instruments Plaits DSP, hardware-verified. make engine-mosc gives each deck a complete plaits::Voice — the full 24-engine Plaits synthesis voice (virtual analog, waveshaping, FM, wavetable, grain, additive, chord, speech, swarm, noise, particle, string, modal, drums, plus the newer "engine2" models: VA-VCF, phase distortion, 6-op FM, wave terrain, string machine, chiptune) — so the two decks are two independent oscillators. Knobs map Plaits' whole panel: PITCH = note, Alt+PITCH = engine select (24 models), SIZE = harmonics, POS = timbre, ENV = morph, MOD_AMT = LPG decay, MODFREQ = LPG colour, MIX = level. The per-deck Mode switch picks Gate (each trigger strikes the LPG/decay envelope — Play pad / gate-in / MIDI / Seq) vs Drone (LPG bypassed, engine runs open), and the global Routing switch combines the two voices three ways: Stereo (A→L, B→R), DoubleMono (mono sum), and GenerativeStereo (an out/aux spread that uses Plaits' otherwise-discarded auxiliary output for width). The three per-deck CV jacks are wired: V/Oct → note, CV_SIZE_POS → harmonics, CV_MIX → timbre. Like csound/chuck it is a BOOT_QSPI build (the 24-engine voice is ~292 KB of code, too big for the 186 KB SRAM_EXEC, so it executes from QSPI flash) — but unlike them it still synthesises from the platform SDRAM arena (a normal arena engine that merely executes from QSPI), so no SD card or external library is needed; the Plaits DSP is vendored in-tree (src/engine/mosc/thirdparty/plaits). Cloned from the reso engine (same author, same stmlib, same PIMPL + arena-placement pattern). User doc: docs/engines/mosc.md; internals, the QSPI build, and the 24-engine order in docs/dev/mosc-impl.md.

    • Shared stmlib for the Mutable Instruments engines (reso + mosc). Both engines depend on Émilie Gillet's stmlib support library; the two vendored copies were byte-for-byte identical (same upstream, unchanged for years), so they are now a single trimmed copy at src/engine/common/thirdparty/stmlib (the union of the Rings and Plaits closures, 19 files), shared via the STMLIB_TP/STMLIB_INC make vars. reso's former src/engine/reso/thirdparty/stmlib was removed (its tree is now rings only); this is a dedup with no functional change.

    Flashing an sk-engines firmware (0.5.1)

    Each .bin here is a complete firmware for one engine. Flash exactly one at a time.

    Prerequisite

    These app binaries are not standalone: they run under the spotykach bootloader, which must already be installed on the device. Installing the bootloader is a separate, device-level procedure not covered here.

    Step 1: enter bootloader mode

    Both methods below need the device in its bootloader (DFU) mode first: hold Reset ~3s until the bottom pad LEDs breathe white.

    Step 2, option A: Daisy Web Programmer (easiest)

    Needs a WebUSB browser - Chrome or Edge (Firefox and Safari will not work).

    1. With the device in bootloader mode, open https://flash.daisy.audio/
    2. On the "File Upload" tab, choose your engine binary (sk--0.5.1.bin).
    3. Click FLASH.

    Step 2, option B: dfu-util (command line)

    dfu-util -a 0 -s 0x90040000:leave -D sk-<engine>-0.5.1.bin -d ,0483:df11
    

    Verify

    • Confirm the download is intact: shasum -a 256 -c SHA256SUMS
    • Confirm what a binary is: arm-none-eabi-strings <file>.bin | grep '^spotykach ' (prints e.g. spotykach 0.5.1 engine=reverb)

    Note for the csound and chuck and mosc engines

    sk-csound-*.bin and sk-chuck-*.bin and sk-mosc-*.bin are QSPI apps: unlike the other engines they execute in place from QSPI flash (their language runtimes are too big for SRAM), so they are also larger downloads and take longer to flash. Same address and steps as above, with two caveats:

    • They need a bootloader that runs QSPI apps (the spotykach bootloader does); a SRAM-only bootloader cannot run them.
    • With dfu-util, the :leave step may print a harmless Error 74 / "get_status" message at the end - the write has already succeeded. Ignore it (the Web Programmer does not show this).
  4. 0.5.0sk-engines 0.5.015 files

    Changes since the last Release

    The biggest change in this release, and reasons for the minor version bump, is the addition of the chuck engine, which runs the ChucK language and virtual machine on the spotykach. Like the csound engine, chuck is a dsp language and platform and you can program it via .ck text files which are compiled and run at runtime, so the patch — not the firmware — defines the sound. You load programs from the SD card, switch between them live, and play them from the panel knobs.

    Note that we are still exploring the limits of the such powerful engines on the daisy hardware so some complex polyphonic patch which may work on a MacBook may struggle on the daisy platform. Nonetheless, using chuck is very intuitive (and more approachable than the venerable csound). This is one of the examples provided for use with spotykach.

    global float speedA;   // PITCH -> tempo
    global float sizeA;    // SIZE  -> reverb mix
    global float mixA;     // MIX   -> level
    
    SinOsc s => JCRev r => dac;
    
    // a scale (semitone offsets) to pick notes from
    [ 0, 2, 4, 7, 9, 11 ] @=> int hi[];
    
    while( true )
    {
        // controls, read live each step
        (0.05 + mixA * 0.45)  => s.gain;     // MIX  -> level
        (0.02 + sizeA * 0.60) => r.mix;      // SIZE -> reverb amount (dry..wet)
    
        // random note from the scale, across a few octaves
        Std.mtof( 45 + Math.random2(0,3) * 12 + hi[Math.random2(0,hi.size()-1)] ) => s.freq;
    
        // PITCH -> tempo: ~40 ms (fast) up to ~280 ms (slow)
        40.0 + (1.0 - speedA) * 240.0 => float period;
        period::ms => now;
    }
    

    Added

    • ChucK engine (ENGINE=chuck) brought up and running on hardware. make engine-chuck runs a full ChucK VM behind IEngine: the engine creates a ChucK instance, compiles a built-in .ck program (a SawOsc -> LPF -> dac drone reading speedA/mixA/sizeA globals), runs the synthesis VM in the audio callback, and applies live panel input through ChucK's global-variable queue (setGlobalFloat). Like Csound it is a BOOT_QSPI build (libchuck's ~1.1 MB of code can't fit SRAM, so it executes from QSPI flash) with ChucK's STL/heap-heavy allocations routed to a free-capable SDRAM pool (the platform heap stays in SRAM). The static library is the WebChucK source subset + feature defines (no MIDI/network/threads/dlopen), cross-built from upstream ccrma/chuck (pinned chuck-1.5.5.8) by scripts/fetch_chuck.sh with a small shim sysroot standing in for the POSIX/libsndfile userland ChucK assumes. Verified end-to-end on a bare Daisy Pod (audible drone + working knobs; confirmed over SWD running in Chuck_UGen::system_tick / Chuck_VM_Shreduler::advance / Chuck_Globals_Manager::handle_global_queue_messages). The full bring-up - four independent root causes found by attaching a debugger, and the SWD flash/inspect tooling - is in docs/dev/chuck-pod-poc.md; roadmap in docs/dev/chuck-impl.md.

    • ChucK SD .ck patch bank + Alt+PITCH live switching, hardware-verified on the spotykach. A card of chuck/0.ck..7.ck programs turns the engine into a runtime-loadable synth: at boot it auto-loads the lowest-numbered slot (the built-in drone if the card has none); hold Alt and turn PITCH to scroll a per-patch ring selector and release to switch live; anything missing/empty/uncompilable falls back to the built-in so the unit always sounds. The live swap runs on one persistent ChucK VM that is never torn down: a swap stops the current patch's shreds (removeAllShreds() + a one-frame run() flush, so the old UGens detach from dac before the next patch sporks), then either re-sporks the target patch's cached compiled Chuck_VM_Code or, on the patch's first visit, compiles it once and pins the bytecode. This is the fix for a cumulative patch-swap memory leak: ChucK 1.5.5.8 never frees its built-in type system on VM teardown (Chuck_Env::cleanup() frees only the base types behind a // TODO; ~Chuck_Context keeps its namespace on success), so every tear-down-and-reload strategy leaked — recompile-per-swap, CK_MSG_CLEARVM reset, and full VM destroy+recreate (the worst: it re-leaked the entire multi-MB type system every swap until the SDRAM pool exhausted and previously-fine patches began overrunning). With one VM + a compile-once cache, total compiles are bounded by the number of distinct patches, so memory rises once per patch then stays flat across unlimited cycling — confirmed on the cased unit by ear and by a new on-panel SDRAM-pool meter (METER=1 build, ring B = live pool usage; ring A = CPU load). A always-on CPU-overrun safeguard mutes a patch whose run() can't keep up (solid-red rings) so the controls stay alive and you can Alt+PITCH away. The patch bank scan/quantizer is host-tested (make -C host test-chuck-patch). User doc: docs/engines/chuck.md; the full root-cause + fix writeup and the roadmap are in docs/dev/chuck-impl.md. (Sound-file UGens — SndBuf/WvIn/WvOut — remain unavailable; a planned WAV-over-FatFs bridge for SndBuf is scoped as M6 in the dev doc.)

    • More csound example orchestras (examples/csound/3..6.csd). Four new ready-to-copy patches exercising a broader slice of the core opcode set than the original drone / super-saw / poly-lead trio: 3.csd a resonant "acid" voice (a self-oscillating moogladder ladder filter with an lfo-swept cutoff), 4.csd a stereo reverb pad (drone + MIDI through a shared bus into reverbsc, with a tanh-limited dry/wet output), 5.csd a dub echo (a feedback delay line via delayr/delayw + a movable deltapi tap), and 6.csd a self-playing generative line (randomh sample-and-hold picking semitones on each metro tick). All follow the established conventions (the speedA/sizeA/… control channels, instr 1 + instr MidiNote, the global-bus + limiter pattern for the effects) and use only core, table-less opcodes; all seven (0–6) pass desktop csound --syntax-check-only. (examples/csound/README.md)

    Changed

    • The dfilter engine has been renamed to filter. Breaking for build invocations and artifact names so update any scripts or muscle memory:

      wasnow
      make ENGINE=dfiltermake ENGINE=filter
      make engine-dfiltermake engine-filter
      make -C host test-dfiltermake -C host test-filter
      sk-dfilter-<version>.binsk-filter-<version>.bin

      The engine is functionally identical — it is still the generated-Faust parallel (DoubleMono) dual-deck demo (one independent resonant low-pass per channel); only the short name changed. Everything that referenced it was updated: the src/engine/filter/ tree (FilterEngine / SPK_ENGINE_FILTER / the fx_filter Faust namespace), the Makefile / CMakeLists.txt / engine_select.h build blocks, the host test, scripts/build_release.py, the control diagram, and the docs (docs/engines/filter.md). Previously released sk-dfilter-*.bin binaries (under dist/) keep their original names — they record what actually shipped.

    Fixed

    • ChucK no longer aborts/HardFaults at boot - four root causes (the engine was previously linking but dying on hardware with a dark LED). None were ChucK-on-Cortex-M incompatibilities; the platform, bootloader, linker script, and SDRAM were always fine. (1) abort() in compileCode() - --specs=nano.specs ships libstdc++ without exceptions, so every std::__throw_* is a bare bl abort; ChucK stringifies float literals via std::to_string -> vsnprintf("%f"), which without float-printf returns a negative length -> std::string(buf, buf-1) -> __throw_length_error -> abort. Fixed by linking -u _printf_float (added to the ENGINE=chuck branch; was already in the csound branch). (2) HardFault right after init - the VM was never start()ed, so ChucK::globals() returned NULL until run() lazily auto-started the VM inside the audio ISR, racing the main loop. Fixed by calling _ck->start() in init() (single-threaded, before audio) and null-guarding globals(). (3) Imprecise BusFault in the SDRAM allocator - the pool (CsoundPool, shared by both QSPI engines) is not reentrant, and setGlobalFloat's new on the main loop raced ck->run()'s allocations in the audio ISR, corrupting the free list. Fixed with a PRIMASK critical section around every pool op (chuck_alloc.cpp). (4) Dead/noisy knobs - the bare Pod harness called set_param in an unthrottled loop, flooding ChucK's 16384-slot global queue until the VM choked draining it (~100% CPU, main loop starved); reading every block then exposed ADC jitter as zipper noise. Fixed (Pod harness only) by reading knobs once per audio block in the callback with a deadband + one-pole smooth; the spotykach UI is already event-driven (PotMoved -> _apply), so it needs no equivalent. Debugging used new in-firmware crash capture (overrides of abort/__assert_func plus g_chuck_init_stage/g_chuck_init_error buffers, readable over SWD) and a pod/daisy_qspi.cfg OpenOCD config that flashes (stmqspi IS25LP064A) and attaches over ST-Link without DFU. All four fixes are in the spotykach ENGINE=chuck firmware, now hardware-verified on the cased unit (it boots, runs the VM, and makes sound). (docs/dev/chuck-pod-poc.md)

    Flashing an sk-engines firmware (0.5.0)

    Each .bin here is a complete firmware for one engine. Flash exactly one at a time.

    Prerequisite

    These app binaries are not standalone: they run under the spotykach bootloader, which must already be installed on the device. Installing the bootloader is a separate, device-level procedure not covered here.

    Step 1: enter bootloader mode

    Both methods below need the device in its bootloader (DFU) mode first: hold Reset ~3s until the bottom pad LEDs breathe white.

    Step 2, option A: Daisy Web Programmer (easiest)

    Needs a WebUSB browser - Chrome or Edge (Firefox and Safari will not work).

    1. With the device in bootloader mode, open https://flash.daisy.audio/
    2. On the "File Upload" tab, choose your engine binary (sk--0.5.0.bin).
    3. Click FLASH.

    Step 2, option B: dfu-util (command line)

    dfu-util -a 0 -s 0x90040000:leave -D sk-<engine>-0.5.0.bin -d ,0483:df11
    

    Verify

    • Confirm the download is intact: shasum -a 256 -c SHA256SUMS
    • Confirm what a binary is: arm-none-eabi-strings <file>.bin | grep '^spotykach ' (prints e.g. spotykach 0.5.0 engine=reverb)

    Note for the csound and chuck engines

    sk-csound-*.bin and sk-chuck-*.bin are QSPI apps: unlike the other engines they execute in place from QSPI flash (their language runtimes are too big for SRAM), so they are also larger downloads and take longer to flash. Same address and steps as above, with two caveats:

    • They need a bootloader that runs QSPI apps (the spotykach bootloader does); a SRAM-only bootloader cannot run them.
    • With dfu-util, the :leave step may print a harmless Error 74 / "get_status" message at the end - the write has already succeeded. Ignore it (the Web Programmer does not show this).
  5. 0.4.0sk-engines 0.4.014 files

    Changes since the last Release

    Added

    • Csound 7 engine (ENGINE=csound), verified on hardware. make engine-csound runs a full Csound 7 instance behind IEngine: an orchestra (.csd) is compiled and performed at runtime, so the patch defines the sound - swap the .csd, swap the synth, no C++ change. Csound's ~2 MB of code can't fit SRAM, so it is a BOOT_QSPI build that executes from QSPI flash, with Csound's heap in a free-capable SDRAM pool (the platform heap stays in SRAM). It loads an SD patch bank (/csound/0.csd..7.csd, plus a built-in fallback so a card-less unit always sounds) with Alt+PITCH live switching (a lock-free handoff recompiles the patch on the main loop without blocking audio), maps the panel knobs to Csound control channels (chnget), and plays the patch's MidiNote instrument from MIDI NoteOn. The static library is the core opcode set only (no plugin opcodes or soundfile I/O), fetched + cross-built from upstream csound/csound (pinned tag 7.0.0-beta.16) by scripts/fetch_csound.sh. Host-tested (allocator, patch selector, MIDI ring, reload handoff); the full design, bring-up notes, and the one open item (a QSPI-XIP CPU perf pass) are in docs/engines/csound.md + docs/dev/csound-impl.md.

    • Grain cloud engine (ENGINE=graincloud). A polyphonic grain cloud - dozens of grains with independent pitch / pan / position scattered over the recorded buffer in contrast to granular's tape-loop scanning. Built as a self-contained variant of the granular engine (SPK_GRAIN_GF): src/engine/graincloud/ is a copy of the granular tree, so it inherits recording, SD save/load, dual-deck, crossfade, FX and UI verbatim, with only the grain core swapped - Generator::process sums a GrainFlowLib cloud (gf_cloud.{h,cpp}) reading granular's recorded buffer instead of driving the granular voice. Played exactly like granular; the cloud knobs map per deck (POS = centre, SIZE = grain size, PITCH = transpose, ENV = position spray, MODFREQ = density, MOD_AMT = pitch/pan spread, SOS = dry/wet, with direction / playhead-speed / vibrato / glisson on the Mode switch + Alt layers). The de-STL'd GrainflowLib is vendored under src/engine/graincloud/thirdparty/; builds at -Os (~97% SRAM_EXEC), 8 grains/deck (conservative - on-device scattered-read cost is unconfirmed; raise kMaxGrains after a METER run). Host-tested (make -C host test-graincloud-kernel). See docs/engines/graincloud.md + docs/dev/graincloud-impl.md.

    Flashing an sk-engines firmware (0.4.0)

    Each .bin here is a complete firmware for one engine. Flash exactly one at a time.

    Prerequisite

    These app binaries are not standalone: they run under the spotykach bootloader, which must already be installed on the device. Installing the bootloader is a separate, device-level procedure not covered here.

    Step 1: enter bootloader mode

    Both methods below need the device in its bootloader (DFU) mode first: hold Reset ~3s until the bottom pad LEDs breathe white.

    Step 2, option A: Daisy Web Programmer (easiest)

    Needs a WebUSB browser - Chrome or Edge (Firefox and Safari will not work).

    1. With the device in bootloader mode, open https://flash.daisy.audio/
    2. On the "File Upload" tab, choose your engine binary (sk--0.4.0.bin).
    3. Click FLASH.

    Step 2, option B: dfu-util (command line)

    dfu-util -a 0 -s 0x90040000:leave -D sk-<engine>-0.4.0.bin -d ,0483:df11
    

    Verify

    • Confirm the download is intact: shasum -a 256 -c SHA256SUMS
    • Confirm what a binary is: arm-none-eabi-strings <file>.bin | grep '^spotykach ' (prints e.g. spotykach 0.4.0 engine=reverb)

    Note for the csound engine

    sk-csound-*.bin is a QSPI app: unlike the other engines it executes in place from QSPI flash (its Csound runtime is ~2 MB, too big for SRAM), so it is also a larger download and takes longer to flash. Same address and steps as above, with two caveats:

    • It needs a bootloader that runs QSPI apps (the spotykach bootloader does); a SRAM-only bootloader cannot run it.
    • With dfu-util, the :leave step may print a harmless Error 74 / "get_status" message at the end - the write has already succeeded. Ignore it (the Web Programmer does not show this).
  6. 0.3.2sk-engines 0.3.210 files

    Changes in this release

    Added

    • Generated Faust engines can now use both decks (dfilter, voice). The Faust generator (scripts/gen_faust_engine.py) previously emitted only single-deck engines - FaustEngine::set_param discarded the deck index, so deck A and deck B knobs collapsed onto one control set (chorus). A manifest deck_mode key now selects two ways to use both decks as independent banks. parallel (DoubleMono): two instances of one mono kernel, deck A on the left channel and deck B on the right, each with its own knobs, on FaustEngine<Traits> with a new decks dimension (decks==1 keeps the single-deck path byte-for-byte). series (chain): two different kernels wired A->B with deck A driving stage 1 and deck B stage 2 - covering FX->FX and instrument->FX - on a sibling FaustChainEngine<Traits> template (src/engine/faust/faust_chain.h); the stages get engine-scoped namespaces (fx_<engine>_<stage>) so stage names can't collide. Both advertise CapDualDeck and need no platform change (the platform already delivers every knob for both decks; set_param just honours the deck arg). Shipped with two demos: dfilter (parallel - a resonant low-pass per channel, ~83% SRAM_EXEC) and voice (series - a drone oscillator into a resonant filter, the instrument->FX case that also exercises the 0-input chain path, ~84%). Both filters share a control layout: PITCH = cutoff, POS = reso, SIZE = drive, SOS = mix (on voice this is deck B; deck A is the oscillator's Pitch=freq / Size=shape / SOS=level). Each is .dsp(s) + a JSON manifest with no hand-written C++; the generator emits the wrapper, wires the build (Makefile/engine_select.h/CMakeLists.txt marker blocks), registers the kernel(s), and emits the control-diagram spec (deck B drawn as a deckB override for the series case). Host-tested for the property a single-deck engine structurally cannot have: per-deck/per-stage independence (make -C host test-dfilter test-voice). Still hand-written (out of scope): a runtime route/mode switch, per-deck voice allocation, >2 decks. Also hardens the Faust kernel recipe (make faust-kernels) to force UTF-8 (PYTHONUTF8=1) so cyfaust can emit kernels whose library metadata contains non-ASCII characters (e.g. oscillators.lib). (src/engine/faust/{faust_fx.h,faust_chain.h}, scripts/gen_faust_engine.py, src/engine/dfilter/, src/engine/voice/, host/test_dfilter.cpp, host/test_voice.cpp, Makefile, docs/dev/engine-gen.md §9, docs/engine-types/faust.md, docs/engines/{dfilter,voice}.md)

    • Dual virtual RadioMusic (radio engine). make ENGINE=radio builds a streaming sample player modelled on Music Thing Modular's RadioMusic, as a dual instrument: each deck (A/B) is an independent virtual radio browsing one shared SD library of numbered banks (/radio/0../radio/15), blended by the crossfader and placed by the routing switch. Its signature is the free-running virtual playhead - a per-deck monotonic frame clock advances every block, and tuning to a station seeks its file to (clock + START) mod length before streaming forward, so every station sounds as though it kept broadcasting while you were tuned elsewhere (leave one and return and it has advanced). Per deck: PITCH = station tuning (+ V/oct CV), POS = start offset (+ CV), SIZE = 0.5–2x varispeed, ENV = inter-station static (a filtered-noise "tuning hiss" crossfaded in on a switch), MIX = volume, Alt+PITCH = bank (held ring-dot selector), the mix fader blends A/B, the routing switch sets the stereo topology, and the Play pad / gate-in RESET a stopped deck. It reuses the tape engine's SPK_USE_STREAM streaming stack (lock-free per-deck SDRAM ring + main-loop FatFs pump) and adds a headerless raw-16-bit codec (RawStreamReader), a directory scan, and the playhead - so stations stream off SD with no in-RAM length cap (the original recordings run ~25 min / 130 MB; only a tiny per-station name+length index sits in RAM). Stations are signed 16-bit mono PCM at 48 kHz: a headerless .raw (the original RadioMusic format, length = filesize/2) assumes 48 kHz unless an optional one-line /radio/rate.txt rebases the resampler, so an unconverted 44.1 kHz card plays at correct pitch. scripts/convert_radio_audio.py converts or whole-card-mirrors a stock RadioMusic card (resampling 44.1→48 kHz, preserving names). capabilities() = CapOwnDisplay | CapDualDeck | CapAux. Host-tested (make -C host test-radio: the playhead modulo, station/bank quantization, varispeed, static, the codec) and verified on hardware; ~83% SRAM_EXEC. (src/engine/radio/radio_engine.{h,cpp}, src/memory/raw_stream.h, src/engine/istreamdeck.h, src/hw/stream_deck.{h,cpp}, src/engine/engine_select.h, host/test_radio.cpp, scripts/convert_radio_audio.py, Makefile, CMakeLists.txt, Makefile.cmake, docs/engines/radio.md, docs/dev/radio-impl.md)

    • radio SD robustness: macOS metadata filter + anti-stutter guards. Reading an original RadioMusic card copied via Finder, the bank scan indexed the hidden AppleDouble companions (._NAME.raw, ~4 KB of metadata macOS writes next to every NAME.raw on a FAT card) as bogus tiny "stations" that looped a sliver of garbage - a fast stutter, interleaved one-for-one with the real stations as you tuned. scan_bank now drops them three ways: the FAT hidden/system attribute (AM_HID|AM_SYS, which macOS sets on dot-prefixed files), a leading-dot name, and a 32 KB minimum size; the converter skips dotfiles on the source side too. Three further guards stop any re-open-driven stutter, since a (re)open flushes and re-seeds the per-deck ring: station-select hysteresis (a deadband so CV/pot noise near a station boundary can't chatter the target), settle-every-prepare (a target oscillating on a boundary never settles, so the engine holds the last clean station instead of re-opening ~5x/s there), and a debounced RESET that is a no-op on a live deck (a repeated/floating-gate trigger can't flush a playing stream). Verified on hardware (the stutter was field-reported and is resolved). (src/hw/stream_deck.cpp, src/engine/radio/radio_engine.{h,cpp}, scripts/convert_radio_audio.py)

    • radio: 16-bit .wav stations with per-file sample rate. Banks can now mix headerless .raw and 16-bit-mono PCM .wav stations. A WAV body is int16 like a .raw, so they share one streaming path: RawStreamReader::begin_wav parses the header (a chunk walk validating fmt=1/16-bit/mono) for the body offset+length and the file's own sample rate, so a 44.1 kHz .wav plays at correct pitch with no rate.txt (the rate ratio is per-deck - the WAV header rate, or the global rate.txt for raw). Indexing a .wav costs one f_open+header-parse per file at bank load (a .raw stays a pure f_stat); playback cost is identical. The converter gains --format wav. Host-tested (header parse / seek / rewind / rate + rejection of stereo/8-bit/float, and the engine dispatching to the WAV path with the header rate rebasing playback). (src/memory/raw_stream.h, src/hw/stream_deck.{h,cpp}, src/engine/istreamdeck.h, src/engine/radio/radio_engine.{h,cpp}, scripts/convert_radio_audio.py, host/test_radio.cpp)

    • make diagrams: render the d2 diagram sources to SVG. The d2 sources moved to docs/diagrams/ (sources) with their rendered SVGs in docs/media/ (output), and a new make diagrams target renders each docs/diagrams/*.d2 → docs/media/<name>.svg via the d2 CLI - an incremental pattern rule (a diagram re-renders only when its source changes), with a clear error if d2 isn't installed. (Makefile, docs/diagrams/, docs/media/)

    Changed

    • Reverb: legible display + a third all-Faust voice (Greyhole), gen~ gigaverb removed. The reverb was visually dead - the rings lit only with the live output level, so the panel went dark whenever the input was quiet and showed nothing about the patch. render() now draws each ring as three layers at once: the active algorithm as the ring colour (plate blue / hall violet / greyhole teal) over a dim full-ring baseline (never fully dark, readable in silence); the DECAY knob (PITCH) as the bright arc length (turning it gives instant feedback); and the output level as the arc's brightness on top, so the ring pulses with signal and visibly fades out as the tail decays. The three mode L/C/R LEDs - which sit at the Reel/Slice/Drift switch - now show the selected algorithm in its colour (so the 3rd position reads as a distinct teal) instead of the route (route stays legible from the two independent per-deck rings in DoubleMono). The third voice is now Greyhole (Faust's re.greyhole - a modulated diffusion network with a feedback pitch-shifter, a lush evolving/ambient reverb) instead of the gen~ gigaverb, which sounded poor and pulled the gen~ runtime: the 3-position switch maps cleanly to plate/hall/greyhole, all-Faust, with no gen~ dependency. greyhole.dsp owns its dry/wet crossfade and maps all six knob roles (Feedback/Size/Damp/Diffusion/ModDepth/Mix) to a real control. Greyhole is the heaviest voice, so the three-voice build overflows SRAM_EXEC at -O2 (~106%) and the reverb branch now builds at OPT = -Os (as reso does), fitting at 185.5 KB / 97.4% with ~5 KB headroom; its per-block device CPU is unmeasured (verify on hardware). The standalone ENGINE=gigaverb engine is unaffected (only removed from the reverb); the obsolete test_reverb_giga is dropped, and test_reverb now exercises all three voices (its response sums both channels so a stereo-only effect like Greyhole's modulation registers). A lighter Freeverb alternative is kept in freeverb.dsp for an easy swap-back. (src/engine/reverb/{reverb_engine.{h,cpp},greyhole.dsp,freeverb.dsp}, Makefile, host/Makefile, host/test_reverb.cpp, docs/engines/reverb.md, docs/dev/reverb-impl.md)

    • Renamed the engine-generator make targets for backend symmetry. make engine-gen (the Faust new-engine generator) is now make faust-engine, and the Faust kernel-compile target make faust-gen is now make faust-kernels (it compiles .dsp -> faust_kernel_*.h; it never produced engines, and one engine can have several kernels - so the old name misled). A single-engine make gen-engine GEN_EXPORT=<export-dir>:<name> was added as the gen~ analogue of faust-engine (a thin wrapper over scripts/gen_engine.py; the per-spec batch form stays gen-engines). Both old names (engine-gen, faust-gen) are kept as deprecated aliases so nothing breaks. The layering now reads cleanly: faust-engine / gen-engine generate one engine from its source; faust-kernels compiles all Faust kernels; gen-engines regenerates all gen~ engines. Generated <name>_engine.h + kernel header comments, both generator scripts, and the docs were updated. (Makefile, scripts/gen_faust_engine.py, scripts/gen_engine.py, docs/)

    • Shared Faust zone-capture: reverb and tape refactored onto faust_capture.h. The Faust "bind a kernel's sliders to platform knobs" machinery (CaptureUI : UI + the Bind/Role structs) existed in three copies - a full one in reverb_engine.cpp, a trimmed one in tape/tapefx.h, and the shared src/engine/faust/faust_capture.h written for the generated-Faust path (the chorus demo). Both hand-written engines now use the shared header and their private copies are deleted, so one zone-capture implementation serves the generator, reverb, and tape (and shuttle, via tapefx.h). reverb keeps its Knob enum, which converts to the shared Bind's generic int role index; the shared CaptureUI carries the optional "Level" output-gain pin reverb relies on. The slider-default capture the generator needs (to seed its boot param cache) is gated behind a CaptureUI<bool CaptureDef> template flag, so the hand-written engines instantiate CaptureUI<false> and pay nothing for it - reverb's SRAM_EXEC rises just +288 B (175408 -> 175696 B, 92.3%). Behaviour-identical and re-verified host-green (reverb/tape FX/shuttle suites), clean target builds of reverb/tape/shuttle/gigaverb/chorus, and confirmed on hardware (reverb both algorithms + tape flashed and verified working). tape's six FX sliders are all native [0,1], so the shared Role::set linear-map (0 + v*1 = v) writes the normalized value straight through exactly as the old Cap did. (src/engine/faust/faust_capture.h, src/engine/reverb/reverb_engine.cpp, src/engine/tape/tapefx.h, src/engine/faust/faust_fx.h, docs/dev/engine-gen.md, docs/engine-types/faust.md)

    • Engine docs split into user-facing + developer-facing. Each engine's documentation now separates audiences: docs/engines/<name>.md keeps the user-facing reference (what the engine is + signature behavior, the control map, routing/display, SD-card prep + file formats, the build/flash commands) and docs/dev/<name>-impl.md holds the developer-facing material (architecture and internal data flow, the file-by-file map, implementation constants and risks/watch-items, bring-up status, bug writeups, and the roadmap / "what's left"). Each engine doc links to its impl notes near the top. Applied across all engines, roughly halving the engine docs (e.g. tape 254→113, reso 169→65, edrums 245→141, radio 220→131); the existing tape-impl.md/shuttle-impl.md absorbed the moved content (deduped). passthrough (a minimal reference example) is left whole. The convention is recorded in docs/engines/README.md, which also gains the missing radio row. (docs/engines/*.md, docs/dev/*-impl.md)

  7. 0.3.0sk-engines 0.3.09 files

    This is the first firmware release from the sk-engines project. Each engine is its own firmware, reinterpreting the shared spotykach hardware and user interface to its own ends.

    This release ships six engines - four instruments and two effects - each with a distinct character and its own set of features.

    instruments:

    • tape: dual sd-streaming/recording tape deck
    • shuttle: buffer-based bipolar/reverse varispeed tape
    • edrums: four-voice Euclidean drum machine
    • reso: resonator/plucked-string based on rings

    fx:

    • delay: tempo-synchronized stereo delay
    • reverb: stereo reverb with switchable algorithms

    If you discover a bug or have a feature request or suggestion please post an issue in the project's issue tracker.

    Flashing an sk-engines firmware (0.3.0)

    Each .bin here is a complete firmware for one engine. Flash exactly one at a time.

    Prerequisite

    These app binaries are not standalone: they run under the spotykach bootloader, which must already be installed on the device. Installing the bootloader is a separate, device-level procedure not covered here.

    Step 1: enter bootloader mode

    Both methods below need the device in its bootloader (DFU) mode first: hold Reset ~3s until the bottom pad LEDs breathe white.

    Step 2, option A: Daisy Web Programmer (easiest)

    Needs a WebUSB browser - Chrome or Edge (Firefox and Safari will not work).

    1. With the device in bootloader mode, open https://flash.daisy.audio/
    2. On the "File Upload" tab, choose your engine binary (sk-<engine>-0.3.0.bin).
    3. Click FLASH.

    Step 2, option B: dfu-util (command line)

    dfu-util -a 0 -s 0x90040000:leave -D sk-<engine>-0.3.0.bin -d ,0483:df11
    

    Verify

    • Confirm the download is intact: shasum -a 256 -c SHA256SUMS
    • Confirm what a binary is: arm-none-eabi-strings <file>.bin | grep '^spotykach ' (prints e.g. spotykach 0.3.0 engine=reverb)