
sk-engines
Hype14/100Alternative '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.
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
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 wholeIEngineinput surface is addressable by name (set param,config,cv,gate,midi,pad,fx), state is readable (query empty|mix|route|gateout|usb), andmode testfreezes the physical input path so injected stimulus is the only thing driving the engine.describeemits 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 tomake test. A pytest harness (tools/) plus askdevclient library andskterm.pyREPL drive a flashed device over serial.test_generic.pyis driven entirely bydescribe, 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 routemismatch found by hand.parse_describenow keeps each query's scope. -
Off-target coverage of everything below the USB transport.
host/test_terminal.cppexercises the tokenizer, line assembler, SPSC ring, TX FIFO, value coercion, float formatting (integer decomposition - the firmware links no_printf_float), every verb'sIEnginebinding and exact reply bytes, the error taxonomy, anddescribe, against a recording mock engine undermake test. It also writes the realdescribeblock tobuild/describe_sample.txt, whichtools/test_descriptor.pyparses through the actual host parser - closing the firmware/host loop without a device. -
Per-engine liveness masks (
live_params()/live_configs()) on eleven engines.describelists only what an engine actually implements, and thedescrline reportsmasked=0|1so 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'sset_param/set_configswitch and not yet swept. granular's mask deliberately excludesAltPosandAux, 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 theSRAM_EXECrebalance every engine fits; seedocs/dev/terminal-impl.md. -
ABI tagging for layout-affecting build flags (
src/abi_tag.h).SPK_TERMINALadds a member toCoreUIand virtuals toIEngine;TERM_USBDIAGadds anotherCoreUImember - 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, plusquery usbas 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_EXECrebalanced 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 indocs/dev/terminal-impl.mdis 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, sobootloader-spotykach-v2.bincopies well beyond the old region. The tested lower bound is ~215K, not 300K; an image approaching 300K should still be flash-tested. -
Composite verbs:
resetandpreset save|load <slot>.reset [deck]drives every advertised param toIEngine::param_default()(new virtual, 0.5 unless an engine overrides);presetsnapshots and restores them through in-RAM slots. Both operate on exactly whatdescribeadvertises and reply with a count.resetcloses a real hole in the harness -mode testfroze the panel but left the previous test's writes in place, so tests were independent only by luck;tools/conftest.pynow resets per test. Presets are non-persistent by design (a test snapshots many times a run and should not wear flash) and params only, becauseIEnginehas no config getter -set_configis 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
IEngineinput surface is now reachable from the terminal.terminal-control.mdclaimed the channel drove "the fullIEngineinput surface"; it did not. Addedfx 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 readsquery recorded|capacity|layout|sizetempo <deck>. Two more are reachable by name but deliberately not advertised indescribe:query fit <deck> <fraction>takes an argument the descriptor cannot express, andquery reseed <deck>is a latching read that self-clears - a generic sweep would consume the flag the platform is waiting for.set_aux_activeis left unexposed because the UI re-asserts it everyprocess(). Off-target tested; not yet exercised on hardware. -
get param crossfadereturned 0 on six engines.set_param(ParamId::Crossfade)stores the value and derives the A/B blend gains, butparam()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.
Devicenow 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 inprepare()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
auxselects 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.
IEnginegainsengine_queries()andread_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 anddescribeemission - so an engine writes no parser and dispatch cannot drift from description. The platform's own queries were converted to the same table, sodescribewalks one code path over both halves and a host cannot tell them apart. Wire format gains akindtoken (query <name> <scope> <kind> [labels]), backward compatible - verified by sweeping a device running pre-kindfirmware. Onlysafe(idempotent, side-effect-free) entries are advertised, which makes the generic sweep correct by construction.CapTerminalis 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). Thehandle_commandhook shipped in phase 1 and no engine has ever implemented it - not because the mechanism is wrong, but because it is invisible todescribe, 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 ordinaryquerylines, which is backward compatible and means the existing sweep covers them with no new host code. Entries are taggedsafe = idempotent && side-effect-freerather 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 (tapeloop_ms, radiostation) 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 Areturnsok empty=1andconfig mode A 1returns 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'schangedflag,take_param_reseed's latching read, andtoggle_grit_modereading 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.cppand the Makefile's unconditionalINFS_LOG_TARGET=LOGGER_EXTERNALboth 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_EXTERNALnow defaults on; build a bare Seed or Pod withTERMPORT=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
TERMINALgave a partial rebuild whose stale subset depended on timing. Because these flags change type layout (CoreUI,AppImpl, theIEnginevtable), 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_FSreturns 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. -
describeadvertised parameters that never reach an engine.TempoandKeyIntervallive in the Transport service andModSpeedarrives viaset_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 unitsset_paramtakes. -
config routeandquery routespoke different encodings, soconfig route A 0read back as2and route could not be round-tripped.query routenow reports the selector encodingconfigaccepts anddescribepublishes. -
mode testdid 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-ownedConfigIduntestable. Now gated with the other three consult points;mode runhands control back automatically. -
mode testhad 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=1withUSB_MIDI=1is now a compile error.MidiUsbHandlerclaims the same OTG_HS core as the default terminal port, andUSB_MIDIdefaults on for everyBOOT_QSPIbuild - two USB device stacks on one peripheral. -
Host tooling had never been executed and did not work.
serial.Serial(..., dtr=True)raises (dtris 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.descriptorcould not be imported without pyserial, breaking device-free collection; andmake test-hwinvoked the systempython3, which has neither pyserial nor pytest. All fixed, the last via aPYTHONvariable that prefers the project venv. -
query usbreported init-time state as live. The refresh only ran underUSBDIAG, 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".
- sk-delay-0.6.1-1-g4faeddd.bin151 KB13↓
- sk-reverb-0.6.1-1-g4faeddd.bin181 KB1↓
- sk-edrums-0.6.1-1-g4faeddd.bin160 KB1↓
- sk-csound-0.6.1-1-g4faeddd.bin2.2 MB0↓
- sk-chuck-0.6.1-1-g4faeddd.bin1.3 MB0↓
Show all 21 filesHide 16 files
- sk-mosc-0.6.1-1-g4faeddd.bin298 KB0↓
- sk-graincloud-0.6.1-1-g4faeddd.bin180 KB0↓
- sk-reso-0.6.1-1-g4faeddd.bin180 KB0↓
- sk-softcut-0.6.1-1-g4faeddd.bin175 KB0↓
- sk-shuttle-0.6.1-1-g4faeddd.bin170 KB0↓
- sk-tape-0.6.1-1-g4faeddd.bin169 KB0↓
- sk-bard-0.6.1-1-g4faeddd.bin163 KB0↓
- sk-pstretch-0.6.1-1-g4faeddd.bin161 KB0↓
- sk-voice-0.6.1-1-g4faeddd.bin156 KB0↓
- sk-radio-0.6.1-1-g4faeddd.bin156 KB0↓
- sk-filter-0.6.1-1-g4faeddd.bin155 KB0↓
- sk-glitch-0.6.1-1-g4faeddd.bin154 KB0↓
- sk-qdelay-0.6.1-1-g4faeddd.bin152 KB0↓
Other files-
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 themake distrelease 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=4096for 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. Addssrc/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-cosbreathe + transport ladder →motion::breathe_standby/transport_view, and the per-engine near-miss palette constants (0x00c0ff/0x00aaff≈kCyan, reso's drifted mode hues) unified ontopal::. Also lit previously-dark indicators —led::route_ledson edrums, a non-blankmeter=falsefloor on the Faust engines. All engines build clean; net-new indicators needing per-engine data (ring::valuepickup feedback,led::clock,led::cyclefor 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-executemosc/csound/chuckthat were Make-only — and itsSRAM_EXECgap vs the Makefile is closed to ~0.65%. The Makefile stays canonical. -
gigaverb (
ENGINE=gigaverb): SIZE now slewsroomsizeinstead 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. Onlyroomsizeis 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/) andglitch(src/engine/glitch/) — so any binary of those is GPLv3 while every other engine stays MIT. Documented in a## Licensesection inREADME.md, with per-engineLICENSE/NOTICE.mdand 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 blockingf_openon every slot crossed — so scrubbing the selector no longer stutters the main loop or error-flashes when a load is mid-flight (theradioengine'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 exceedskMaxFrames(silence, not garbage — no effect at today's 96-frame block, well under the 256 cap), and an unused 1 KBkZeroglobal 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 aring::selectorof 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 testrestored 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 acrossdsp//granular//graincloud//transport//hw//ui//memory/used baresize_t/uint*_t/std::sinvia transitive includes only the ARM toolchain supplies; added the missing<cstdint>/<cstddef>/<cmath>(add-only, no vendored code touched); (2) atest_engine_paramslink failure (undefined LEDRing::clear,GranularEnginevtable) that was a compile-cascade — resolved once the sweep letled.ring.cpp/granular_engine.cppcompile again; (3) a use-after-free intest_pstretch.cpp— theHostArenawas scoped to just theinit()call, so it was freed beforeprocess()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 testat 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'scsoundCallocAlignedcalls C11aligned_alloc, whose nano-libc implementation needsposix_memalign(absent undernosys). The SDRAM-pool shim now also--wrapsaligned_allocand serves it from the pool, somake distlinks thecsoundengine again. (docs/dev/csound-impl.md)
- MANIFEST.txt1.1 KB4↓
- sk-tape-0.6.0.bin169 KB3↓
- RELEASE_NOTES.md12 KB2↓
- sk-csound-0.6.0.bin2.2 MB1↓
- sk-chuck-0.6.0.bin1.3 MB1↓
Show all 21 filesHide 16 files
- sk-mosc-0.6.0.bin298 KB1↓
- sk-reverb-0.6.0.bin181 KB1↓
- sk-graincloud-0.6.0.bin180 KB1↓
- sk-reso-0.6.0.bin180 KB1↓
- sk-softcut-0.6.0.bin175 KB1↓
- sk-shuttle-0.6.0.bin170 KB1↓
- sk-bard-0.6.0.bin163 KB1↓
- sk-pstretch-0.6.0.bin160 KB1↓
- sk-edrums-0.6.0.bin159 KB1↓
- sk-voice-0.6.0.bin156 KB1↓
- sk-radio-0.6.0.bin156 KB1↓
- sk-filter-0.6.0.bin155 KB1↓
- sk-glitch-0.6.0.bin154 KB1↓
- sk-qdelay-0.6.0.bin152 KB1↓
- sk-delay-0.6.0.bin151 KB1↓
- SHA256SUMS1.5 KB1↓
-
0.5.1sk-engines 0.5.116 files
Changes since the last Release
Added
-
Real ChucK
MidiInon the bare-metal build (prototype; build-verified, on-hardware wake test pending).libchuck.ais now compiled with ChucK'sMidiIn/MidiOutdevice classes enabled (__DISABLE_MIDI__dropped from all three ChucK define sites —scripts/fetch_chuck.sh, the firmwareMakefile, andpod/Makefile.chuck, kept ABI-consistent), but withmidiio_rtmidi's RtMidi backend stripped: there is no OS MIDI API or callback thread on bare metal, andrtmidi.cpp(166 KB) stays out. A newMidiInManager::inject()feeds bytes straight into ChucK's per-VM MIDI buffer + event-wake path, so a.ckpatch can use the desktop-portableMidiIn 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 newIEngine::handle_midi_message(status, d1, d2)virtual (no-op for engines that only usehandle_midi_note) is driven from the UI MIDI poll (core.ui.midi.cpp) for every representable event;ChuckEngineoverrides it to enqueue raw bytes on a lock-free ring (chuck_midi_in.h), andChuckEngine::process()drains it and injects into device 0 on the run() thread, right beforeck->run(), so a shred blocked onmin => nowis woken that block.ChuckEngine::handle_midi_noteis kept only to map channel→deck for the panel gate-in LED. The vendored-ChucK edits live inscripts/patches/midi_daisy.patch, applied idempotently byscripts/fetch_chuck.sh(becausethirdparty/chuckis gitignored and regenerated). The firmware (make engine-chuck) links with matching ABI. Ported from thedaisy-appssibling project; replication guide indocs/dev/chuck-midi-in-porting.md, and the user-facing capabilities + remaining incompatibilities (noMidiOut, single virtual device / no enumeration, 3-byte messages only) are documented indocs/engines/chuck.md. (SysEx / system-common aren't representable in a 3-byteMidiMsgand 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-moscgives each deck a completeplaits::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 aBOOT_QSPIbuild (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, samestmlib, same PIMPL + arena-placement pattern). User doc:docs/engines/mosc.md; internals, the QSPI build, and the 24-engine order indocs/dev/mosc-impl.md. -
Shared
stmlibfor the Mutable Instruments engines (reso + mosc). Both engines depend on Émilie Gillet'sstmlibsupport library; the two vendored copies were byte-for-byte identical (same upstream, unchanged for years), so they are now a single trimmed copy atsrc/engine/common/thirdparty/stmlib(the union of the Rings and Plaits closures, 19 files), shared via theSTMLIB_TP/STMLIB_INCmake vars. reso's formersrc/engine/reso/thirdparty/stmlibwas removed (its tree is nowringsonly); this is a dedup with no functional change.
Flashing an sk-engines firmware (0.5.1)
Each
.binhere 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).
- With the device in bootloader mode, open https://flash.daisy.audio/
- On the "File Upload" tab, choose your engine binary (sk--0.5.1.bin).
- 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:df11Verify
- 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
csoundandchuckandmoscenginessk-csound-*.binandsk-chuck-*.binandsk-mosc-*.binare 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:leavestep may print a harmlessError 74/ "get_status" message at the end - the write has already succeeded. Ignore it (the Web Programmer does not show this).
-
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
chuckengine, 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.cktext 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-chuckruns a full ChucK VM behindIEngine: the engine creates aChucKinstance, compiles a built-in.ckprogram (aSawOsc -> LPF -> dacdrone readingspeedA/mixA/sizeAglobals), runs the synthesis VM in the audio callback, and applies live panel input through ChucK's global-variable queue (setGlobalFloat). Like Csound it is aBOOT_QSPIbuild (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 upstreamccrma/chuck(pinnedchuck-1.5.5.8) byscripts/fetch_chuck.shwith 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 inChuck_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 indocs/dev/chuck-pod-poc.md; roadmap indocs/dev/chuck-impl.md. -
ChucK SD
.ckpatch bank + Alt+PITCH live switching, hardware-verified on the spotykach. A card ofchuck/0.ck..7.ckprograms 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-framerun()flush, so the old UGens detach fromdacbefore the next patch sporks), then either re-sporks the target patch's cached compiledChuck_VM_Codeor, 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_Contextkeeps its namespace on success), so every tear-down-and-reload strategy leaked — recompile-per-swap,CK_MSG_CLEARVMreset, 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=1build, ring B = live pool usage; ring A = CPU load). A always-on CPU-overrun safeguard mutes a patch whoserun()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 indocs/dev/chuck-impl.md. (Sound-file UGens —SndBuf/WvIn/WvOut— remain unavailable; a planned WAV-over-FatFs bridge forSndBufis scoped as M6 in the dev doc.) -
More
csoundexample 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.csda resonant "acid" voice (a self-oscillatingmoogladderladder filter with anlfo-swept cutoff),4.csda stereo reverb pad (drone + MIDI through a shared bus intoreverbsc, with atanh-limited dry/wet output),5.csda dub echo (a feedback delay line viadelayr/delayw+ a movabledeltapitap), and6.csda self-playing generative line (randomhsample-and-hold picking semitones on eachmetrotick). All follow the established conventions (thespeedA/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 desktopcsound --syntax-check-only. (examples/csound/README.md)
Changed
-
The
dfilterengine has been renamed tofilter. Breaking for build invocations and artifact names so update any scripts or muscle memory:was now make ENGINE=dfiltermake ENGINE=filtermake engine-dfiltermake engine-filtermake -C host test-dfiltermake -C host test-filtersk-dfilter-<version>.binsk-filter-<version>.binThe 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/ thefx_filterFaust namespace), theMakefile/CMakeLists.txt/engine_select.hbuild blocks, the host test,scripts/build_release.py, the control diagram, and the docs (docs/engines/filter.md). Previously releasedsk-dfilter-*.binbinaries (underdist/) 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()incompileCode()---specs=nano.specsships libstdc++ without exceptions, so everystd::__throw_*is a barebl abort; ChucK stringifies float literals viastd::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 theENGINE=chuckbranch; was already in the csound branch). (2) HardFault right after init - the VM was neverstart()ed, soChucK::globals()returnedNULLuntilrun()lazily auto-started the VM inside the audio ISR, racing the main loop. Fixed by calling_ck->start()ininit()(single-threaded, before audio) and null-guardingglobals(). (3) Imprecise BusFault in the SDRAM allocator - the pool (CsoundPool, shared by both QSPI engines) is not reentrant, andsetGlobalFloat'snewon the main loop racedck->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 calledset_paramin 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 ofabort/__assert_funcplusg_chuck_init_stage/g_chuck_init_errorbuffers, readable over SWD) and apod/daisy_qspi.cfgOpenOCD config that flashes (stmqspiIS25LP064A) and attaches over ST-Link without DFU. All four fixes are in the spotykachENGINE=chuckfirmware, 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
.binhere 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).
- With the device in bootloader mode, open https://flash.daisy.audio/
- On the "File Upload" tab, choose your engine binary (sk--0.5.0.bin).
- 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:df11Verify
- 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
csoundandchuckenginessk-csound-*.binandsk-chuck-*.binare 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:leavestep may print a harmlessError 74/ "get_status" message at the end - the write has already succeeded. Ignore it (the Web Programmer does not show this).
-
0.4.0sk-engines 0.4.014 files
Changes since the last Release
Added
-
Csound 7 engine (
ENGINE=csound), verified on hardware.make engine-csoundruns a full Csound 7 instance behindIEngine: 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 aBOOT_QSPIbuild 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'sMidiNoteinstrument from MIDI NoteOn. The static library is the core opcode set only (no plugin opcodes or soundfile I/O), fetched + cross-built from upstreamcsound/csound(pinned tag7.0.0-beta.16) byscripts/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 indocs/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 thegranularengine (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::processsums 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 undersrc/engine/graincloud/thirdparty/; builds at-Os(~97% SRAM_EXEC), 8 grains/deck (conservative - on-device scattered-read cost is unconfirmed; raisekMaxGrainsafter aMETERrun). Host-tested (make -C host test-graincloud-kernel). Seedocs/engines/graincloud.md+docs/dev/graincloud-impl.md.
Flashing an sk-engines firmware (0.4.0)
Each
.binhere 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).
- With the device in bootloader mode, open https://flash.daisy.audio/
- On the "File Upload" tab, choose your engine binary (sk--0.4.0.bin).
- 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:df11Verify
- 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
csoundenginesk-csound-*.binis 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:leavestep may print a harmlessError 74/ "get_status" message at the end - the write has already succeeded. Ignore it (the Web Programmer does not show this).
-
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_paramdiscarded the deck index, so deck A and deck B knobs collapsed onto one control set (chorus). A manifestdeck_modekey 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, onFaustEngine<Traits>with a newdecksdimension (decks==1keeps 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 siblingFaustChainEngine<Traits>template (src/engine/faust/faust_chain.h); the stages get engine-scoped namespaces (fx_<engine>_<stage>) so stage names can't collide. Both advertiseCapDualDeckand need no platform change (the platform already delivers every knob for both decks;set_paramjust honours the deck arg). Shipped with two demos:dfilter(parallel - a resonant low-pass per channel, ~83% SRAM_EXEC) andvoice(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 (onvoicethis 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.txtmarker blocks), registers the kernel(s), and emits the control-diagram spec (deck B drawn as adeckBoverride 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 (
radioengine).make ENGINE=radiobuilds 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 lengthbefore 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 thetapeengine'sSPK_USE_STREAMstreaming 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.txtrebases the resampler, so an unconverted 44.1 kHz card plays at correct pitch.scripts/convert_radio_audio.pyconverts 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) -
radioSD 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 everyNAME.rawon 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_banknow 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.wavstations with per-file sample rate. Banks can now mix headerless.rawand 16-bit-mono PCM.wavstations. A WAV body is int16 like a.raw, so they share one streaming path:RawStreamReader::begin_wavparses the header (a chunk walk validatingfmt=1/16-bit/mono) for the body offset+length and the file's own sample rate, so a 44.1 kHz.wavplays at correct pitch with norate.txt(the rate ratio is per-deck - the WAV header rate, or the globalrate.txtfor raw). Indexing a.wavcosts onef_open+header-parse per file at bank load (a.rawstays a puref_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 todocs/diagrams/(sources) with their rendered SVGs indocs/media/(output), and a newmake diagramstarget renders eachdocs/diagrams/*.d2→docs/media/<name>.svgvia thed2CLI - an incremental pattern rule (a diagram re-renders only when its source changes), with a clear error ifd2isn'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'sre.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.dspowns 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 overflowsSRAM_EXECat-O2(~106%) and the reverb branch now builds atOPT = -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 standaloneENGINE=gigaverbengine is unaffected (only removed from the reverb); the obsoletetest_reverb_gigais dropped, andtest_reverbnow 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 infreeverb.dspfor 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 nowmake faust-engine, and the Faust kernel-compile targetmake faust-genis nowmake 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-enginemake gen-engine GEN_EXPORT=<export-dir>:<name>was added as the gen~ analogue offaust-engine(a thin wrapper overscripts/gen_engine.py; the per-spec batch form staysgen-engines). Both old names (engine-gen,faust-gen) are kept as deprecated aliases so nothing breaks. The layering now reads cleanly:faust-engine/gen-enginegenerate one engine from its source;faust-kernelscompiles all Faust kernels;gen-enginesregenerates 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:
reverbandtaperefactored ontofaust_capture.h. The Faust "bind a kernel's sliders to platform knobs" machinery (CaptureUI : UI+ theBind/Rolestructs) existed in three copies - a full one inreverb_engine.cpp, a trimmed one intape/tapefx.h, and the sharedsrc/engine/faust/faust_capture.hwritten for the generated-Faust path (thechorusdemo). Both hand-written engines now use the shared header and their private copies are deleted, so one zone-capture implementation serves the generator,reverb, andtape(andshuttle, viatapefx.h).reverbkeeps itsKnobenum, which converts to the sharedBind's genericintrole index; the sharedCaptureUIcarries 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 aCaptureUI<bool CaptureDef>template flag, so the hand-written engines instantiateCaptureUI<false>and pay nothing for it - reverb'sSRAM_EXECrises just +288 B (175408 -> 175696 B, 92.3%). Behaviour-identical and re-verified host-green (reverb/tape FX/shuttlesuites), clean target builds ofreverb/tape/shuttle/gigaverb/chorus, and confirmed on hardware (reverbboth algorithms +tapeflashed and verified working).tape's six FX sliders are all native[0,1], so the sharedRole::setlinear-map (0 + v*1 = v) writes the normalized value straight through exactly as the oldCapdid. (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>.mdkeeps the user-facing reference (what the engine is + signature behavior, the control map, routing/display, SD-card prep + file formats, the build/flash commands) anddocs/dev/<name>-impl.mdholds 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 existingtape-impl.md/shuttle-impl.mdabsorbed the moved content (deduped).passthrough(a minimal reference example) is left whole. The convention is recorded indocs/engines/README.md, which also gains the missingradiorow. (docs/engines/*.md,docs/dev/*-impl.md)
-
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:
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
.binhere 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).
- With the device in bootloader mode, open https://flash.daisy.audio/
- On the "File Upload" tab, choose your engine binary (
sk-<engine>-0.3.0.bin). - 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:df11Verify
- 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)