Releases: shakfu/sk-engines
Release list
sk-engines 0.5.1
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 .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).
- 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: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: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).
sk-engines 0.5.0
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-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 .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 cov...
sk-engines 0.4.0
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 .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).
- 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: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: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).
sk-engines 0.3.2
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}, `Make...
sk-engines 0.3.0
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 .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).
- 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: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)