Skip to content

Releases: shakfu/sk-engines

sk-engines 0.5.1

Choose a tag to compare

@shakfu shakfu released this 25 Jun 06:52

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).

sk-engines 0.5.0

Choose a tag to compare

@shakfu shakfu released this 22 Jun 07:44

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:

    was now
    make ENGINE=dfilter make ENGINE=filter
    make engine-dfilter make engine-filter
    make -C host test-dfilter make -C host test-filter
    sk-dfilter-<version>.bin sk-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 cov...

Read more

sk-engines 0.4.0

Choose a tag to compare

@shakfu shakfu released this 18 Jun 19:17

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).

sk-engines 0.3.2

Choose a tag to compare

@shakfu shakfu released this 15 Jun 18:15

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/*.d2docs/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}, `Make...
Read more

sk-engines 0.3.0

Choose a tag to compare

@shakfu shakfu released this 13 Jun 10:05

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)