alchemy-sdk, image 1 from its README
1/3
Tool

alchemy-sdk

Hype13/100

Embedded SDK for Hermetic Modular eurorack modules, built on Electrosmith Daisy.

alchemy-sdk is a tool for the Hermetic Modular Alchemy Lab, by @hermetic-modular. Open source under MIT, latest release v0.12.0 (September 2026), 38 stars on GitHub.

forHermetic Modular Alchemy Lab·by@hermetic-modular

Unclaimed. Is this yours? Sign in with GitHub as @hermetic-modular to claim it.

Downloads
0
Stars
38
Forks
3
Commits
48
Last commit
Get the source

No release files yet. Build from source.

MIT· hermetic-modular/alchemy-sdk·hermeticmodular.com·downloads come from the creator’s GitHub releases

48
Commits
4
Releases
48 commits and 4 releases in the last year
Activity · last 12 months
4 releases
  1. v0.12.0Latest0 files

    Alchemy SDK v0.12.0 | 2026-09-29

    USB diagnostics now share the HostLink connection, including on firmware without presets. Selectors fixed to match across values, rings, and the browser; firmware can refresh its published metadata while connected. Also: corrected codec CV scaling, reliable codec-input trigger detection, a rollover-safe control-loop timebase, and stricter calibration validation.

    Control-loop timebase (#35)

    • Fixed a major issue causing a 50 millisecond stutter every 21 seconds on clock analysis.
    • Added TickTimebase, which accumulates wrapping hardware ticks into microseconds with the wrap behavior expected by the timing APIs.
    • ControlLoop now uses this timebase for CV edge processing and exposes the current poll timestamp through PollTimeUs().
    • Custom clock wiring should use loop.PollTimeUs() inside OnPoll, or its own TickTimebase, instead of passing daisy::System::GetUs() directly to timing consumers.
    • A timebase must be updated before the underlying raw counter completes a full wrap. Initialize it after hardware startup and keep related timing consumers on the same epoch.

    USB diagnostics and Host lifecycle (#40)

    • Added the optional hostlink::Diagnostics extension. Debug(), Info(), Warn(), and Error() write bounded messages without waiting for a USB client. PrintLine() is an Info-level migration alias.
    • Added typed Gauge<float>, Gauge<int32_t>, Gauge<uint32_t>, and Gauge<bool> values. Set() publishes the latest scalar and can run in an audio callback; formatted logging belongs on the main/control thread.
    • Default storage is 32 log records with 160 text bytes each, plus up to 16 registered gauges. Overflow and truncation are reported. Logs are volatile.
    • Added a Host constructor without Presets, for identity, reboot, and protocol extensions without a dummy preset store.
    • The default Host starts at its first Poll(), after setup has attached the module’s declarations. Factory-state capture remains separate from link startup.
    • Added CLI log capture and live-value watching. The web programmer’s Device console uses the same diagnostics protocol.
    • Do not combine HostLink with raw seed.PrintLine() or direct USB writes on the same port.

    Selector consistency (#53; issue #47)

    • Settings rings now render the resolved selection index instead of independently quantizing the knob position.
    • VirtualKnob::Selector(N) uses equal-width bins. Its value count also controls selector-ring selection, independently of builder order.
    • Fixed selected zones being overwritten by inactive zones when several choices share an LED.
    • Auto-derived selector display hints now include disp.bins. The browser uses matching selection boundaries and writes bin centers.
    • Existing display hints without bins, explicit .Disp() overrides, and standalone selector-animation conventions retain their previous interpretation.
    • Settings selections remain bytes and performance values remain normalized floats. Preset offsets, sizes, and schema formats are unchanged by these fixes.

    Manual capacity and control visibility (#54; issues #41 and #43)

    • Increased the module manual limit from eight to sixteen sections.
    • Added Page::HidePot(pot, hidden) and PotHidden(pot).
    • Hidden controls remain in serialization and factory defaults. Visibility affects editor/manual presentation without removing stored values or changing physical pot positions.
    • Unassigned positions are not automatically treated as explicitly hidden controls.

    Runtime descriptor refresh (#55; issue #45)

    • Added Host::RequestDescriptorRefresh() and DescriptorRefreshStatus().
    • Refreshes rebuild auto-derived metadata on the main/control thread. Repeated pending requests coalesce.
    • Field identities and storage layout must remain stable. Factory defaults remain the originally captured defaults rather than becoming the current live state.
    • A rejected refresh retains the previously published descriptor. Correct the declarations and explicitly request another refresh to retry.
    • Added DescriptorRefreshBuffer() for applications whose custom descriptor capacity exceeds the default workspace.
    • Hand-written Descriptor() data and preset-free Hosts do not support this refresh path.
    • Hosts discover changes through descriptor length/CRC and restart descriptor transfers from offset zero.

    Button gesture metadata (#37)

    • Root button descriptions now include structured tap and hold declarations and their help text.
    • Firmware no longer needs duplicate metadata-only actions to expose those gestures.
    • Existing explicit .Action() descriptions remain supported.

    Codec-input triggers (#38)

    • Reworked J1/J2 detection around fast positive transitions through the AC-coupled input path, with hysteresis and ringing rejection.
    • RisingEdge() consumes an atomic latch. Multiple rises between reads coalesce; applications should read once and reuse the result.
    • V2 StartAudio() now accepts an omitted application callback. The SDK still processes trigger inputs and claimed codec CV outputs; unclaimed outputs are cleared.
    • Removed SetTriggerThreshold(). Detection no longer requires threshold configuration.
    • These inputs detect transitions, not sustained gate levels or DC pitch voltage.

    J9/J10 CV conversion (#52; issue #48)

    • Corrected voltage-to-sample polarity and gain for the standard Seed2 DFM DC-coupled output circuit.
    • Nominal conversion is sample = volts / -8.666667, with generated samples bounded to the codec range.
    • Non-finite voltage requests are rejected without replacing the previous target.
    • This is a circuit-based conversion, not per-board calibration. J9/J10 have no ADC readback through this path.
    • Ordinary audio output and the existing CV ownership API are unchanged. Volts() continues to report the requested target, not a measured output.

    Calibration validation (#36)

    • Added checks for reference validity, input noise and saturation, disconnected-output isolation, sweep progression, fitted response, and usable range.
    • Failed measurements are rejected before replacing the stored calibration.
    • Calibration persistence now verifies the written record before reporting success.
    • Stored records receive stronger validity checks before use.
    • Calibration remains specific to J3–J8. It does not calibrate the codec outputs.

    Compatibility and companion web changes

    • Descriptor additions remain optional and dv stays 1.
    • The updated programmer supports selector bins, hidden controls, runtime metadata refresh, diagnostics, and Hosts without presets.
    • Existing firmware retains its supported editor behavior.
    • Applications using SetTriggerThreshold() must remove those calls.
    • Clock code outside ControlLoop must explicitly adopt a suitable timebase; updating the SDK does not rewrite an application’s timestamp source.
  2. v0.11.00 files

    Alchemy SDK v0.11.0 | 2026-09-04

    Parameter locks can now follow the musical clock, shift layers are just pages, and pots no longer drift with ADC noise at idle. Also: reliable lock on fast external clocks, batched CV output (thanks to Nicholas Marrone), and a compile-time guard against braced-list temporaries.

    Parameter locks v2 (#29)

    • Added clocked mode. locks.UseClock(clock_) supplies the clock; settings.UseLocks(locks) adds a persisted Free/Clocked selector on P5. In Clocked mode a finished recording snaps its loop boundary to the nearest musical division and restarts against the clock through tempo changes. Straight and triplet divisions by default; SnapGrid() adds dotted.
    • Slot header 'PLK1' to 'PLK2', 5 bytes to 7. Locks saved under the old header load as empty.
    • Arm threshold lowered from 3% to 0.5%. Clear threshold stays 3%. record_base is now the pre-nudge origin. Added ArmThreshold() and ClearThreshold().
    • Added ExitMode(LockExit::Return): on release the value returns to the recorded origin and pot catch re-arms. LockExit::Latch is the surface default and matches v0.10.0. UseLocks also adds a persisted Return/Latch selector on P6, defaulting to Return. Default settings stack: P1 brightness, P2 free, P3-P4 presets, P5-P6 locks.
    • Added RecordStyle() and PlayStyle() (Solid, Blink, Breathe, LoopPulse, Sweep; red default), ClearSlot(), Freeze(), PlayPhase().
    • Fixed: a trigger release landing under the Settings gate was dropped, so OnButtonUp never ran and the manager kept the hold.
    • Added ParamLock::TakeCleanRelease(): true once per trigger release whose hold armed or cleared nothing. A release under the Settings gate still reaches the manager but never counts as a tap.
    • Behavior documented in docs/param-locks.md.

    Pager navigation bindings (#30)

    • Added Pager(num_pages, num_pots) with .Cycle(btn, pages...), .Shift(btn, page), .Latch(btn, a, b) and .From(pages...). The Pager(b1, n, m) constructor is unchanged and equivalent to .Cycle(b1) over every page. Serialized image and 'PAG0' schema unchanged.
    • Shift layers engage on the press edge. Pot catch re-arms in both directions and runs only on the active page.
    • Added Pager::HoldUsed(). A hold that edited no parameter does not claim its release; a hold that edited one does.
    • Latch pairs occupy one cycle slot and remember the selected member.
    • Added arbitration: presses under or spanning the Settings gate are poisoned, co-pressing two navigation buttons aborts layers, one layer runs at a time, and GoToPage cancels an active layer.
    • Added KnobStorage::ConsumesRelease(), HoldClaimed() and IndicatorColor().
    • ButtonBank suppresses a tap whose release a used hold claimed and resolves its consume handshake through ConsumesRelease(). Hold gestures claim at the release rather than at fire time.
    • ControlLoop paints navigation indicators per button through IndicatorColor(). It previously painted the page tint on button 0 regardless of which button the pager used. A custom KnobStorage must implement PageButton() or override IndicatorColor() to get one.
    • ParamLock banks its record gesture at the trigger's press edge, which fixes a GoToPage during a hold retargeting an in-flight recording. It holds the pager view latch (RetainPage, ReleasePage) and consumes a release only when its trigger carries a release-driven binding.
    • Fixed: a pager button pressed under or across the Settings gate no longer advances the page on release.
    • ConsumeButton() claims this frame's releases and the release of any bound button held when it is called. A claim made with no bound button held is discarded at the end of the frame.
    • Added docs/pages-and-layers.md and tests/host/pager_nav_tests.cpp.

    Pot conditioning (#32)

    • Added a 20 Hz one-pole filter on the V2 pots, applied at the 1 kHz ProcessAllControls() cadence and primed on the first ADC scan.
    • Added a motion gate to pot catch, in a timed UpdateCatch(state, phys, t_ms) overload. A caught pot holds its stored value exactly while asleep, wakes on more than 0.4% of travel, tracks the knob 1:1 while awake, and sleeps after 400 ms within a 0.15% band. Pager and Settings call the timed overload. The untimed overload is unchanged.
    • Added tests/pot_gate_test.cpp.

    ClockFollower (#31)

    • OnPulse() writes to an 8-deep single-producer ring instead of a one-slot mailbox. Pulses are no longer dropped when a poll runs later than the pulse period. Fixes loss of lock on fast external clocks.
    • Phase error is measured at the pulse timestamp through MusicalClock::LastTickUs(). Update() and Tick() may be called in either order.
    • The PI target scales with the number of pulses drained per update.
    • Added tests/clock_follower_test.cpp.

    Batched CV output (#28)

    • Added CvJack::StageVolts() and AlchemyLabV2::FlushCvOutputs(). StageVolts() writes the shared MCP4728 shadow and sets a dirty flag. FlushCvOutputs() latches every channel with one WriteAll and one LDAC pulse, about 430 us, and returns immediately when nothing is staged.
    • On J7 to J10 StageVolts() is identical to SetVolts().
    • SetVolts() is unchanged.
    • Thanks to Nicholas Marrone.

    Builders reject braced-list temporaries (#33)

    • Selector, Labels, Colors, Buttons and Jacks reject a braced list at compile time. The list was a temporary and the retained pointer dangled. Pass a named array.
    • VirtualButton::Colors(ptr, n) no longer defaults n. .Colors(kArray) resolved to this overload without a count, skipping the zone-count check.

    Also

    • Added BrightnessHandle::At() and Settings::RelocateSlot(), which move a settings slot to another page and pot.
    • No descriptor or wire changes. dv stays 1.
  3. v0.10.00 files

    Alchemy SDK v0.10.0 | 2026-08-19

    Modules can now ship their own documentation, and the descriptor build stopped failing quietly. Plus, a new clip indicator light tool. And some errors are fixed.

    Interactive manual support (#21)

    • Firmware can carry its own manual. .Help() attaches prose to knobs, buttons, settings, pages, and jacks; .SeeAlso() makes typed cross-references that resolve and validate at build time.
    • alchemy::Jack (surface/jack.h): pure descriptor metadata for a panel jack: id, name, silk label, signal class, normalling. No runtime behavior, no state, no schema-hash impact.
    • alchemy::Manual (surface/manual.h): module-level tagline, preamble, and long-form sections, plus stock_help for the features the SDK owns (presets, parameter locks, storage, brightness).
    • VirtualButton::GestureHelp(key, md) keys help to the gesture string passed to .Action(); a key matching no declared gesture fails the build.
    • Settings slots gain identity: .Ident(), .Name(), .Help(), labeled Selector, and per-page help.
    • Descriptor rendering no longer depends on the order of calls in main(). Factory defaults are captured before the boot preset loads; the descriptor renders once everything is declared.
    • A failed descriptor build reports its reason over the wire instead of returning zero, so a bad cross-reference surfaces in the host rather than as a module that silently claims to have no descriptor (protocol §5.2).
    • Additive on the wire; dv stays 1. Prose never reaches any schema hash, so editing help text cannot invalidate a saved preset.

    Calibrated VDDA for CV input volts (#22)

    • CV input voltage conversion uses the calibrated VDDA rather than the nominal value.

    ClipIndicator: SDK clipping light (#19)

    • alchemy::ClipIndicator (anims/clip_indicator.h) — audio-fed clip light with a 98 % full-scale threshold, leaky-bucket transient suppression (a one-sample graze no longer flashes the panel), and a 50 ms minimum hold that retriggers while clipping persists. Feed Process() from the audio callback (ISR-safe single-word handoff); everything else is wired by loop.Use(clip) — detection ticks at the poll cadence, pips draw above the perf render.

    • Declares like a VirtualKnob, with good defaults (every pot pip, red, 50 ms):

      static ClipIndicator clip = ClipIndicator()
          .Hold(120.0f)
          .Pots(kPotTopLeft, kPotTopRight)
          .Buttons(kButtonB3);
      

      Also Threshold, Suppression(samples, window_ms), Accent, PipHour, Color, AllPots, and SetEnabled() for a settings toggle. Bespoke loops call Tick(dt_ms) / Draw(panel) directly; Draw(panel, color) serves palette-driven modules.

    • The sample scan runs as integer bit-pattern compares (abs = clear the sign bit; IEEE ordering is monotonic), so the audio-side cost is ~7 integer instructions per checked sample with no FPU flag transfers — and it probes at a stride derived from the suppression config (min_clipped_samples / 2, capped at 16; 4 for the default 8), with each hit counting as one stride of samples. The derivation keeps the suppression contract intact at any setting — a lone sub-stride burst can never reach the trigger — while the scan gets proportionally cheaper as suppression relaxes, and exact when it drops below 4. Non-finite samples (NaN/Inf) count as clipped — a broken signal lights the alarm.

  4. v0.9.00 files

    Alchemy SDK v0.9.0 | 2026-08-14

    The first versioned Alchemy SDK release, trying out a new cadence instead of merging PRs as I please.

    Parameter locks: configurable length (#11)

    • ParamLock<slots, LockLength<seconds, rateHz, storage>> — length, rate, and storage policy are per-module choices; the default is 16 s @ 30 Hz (up from 4s). Guide: docs/param-locks.md.
    • Motion stores as u16 : half the preset bytes per sample, so double the length fits the same preset budget.
    • LockStore::RamOnly keeps a lock surface out of presets entirely if you're playing tight with the preset bundel.

    Buttons: VirtualButton runtime API + ButtonBank surface (#12)

    • One declaration derives zones, labels, the tap-cycle gesture, browser chips, per-zone LEDs, and the preset byte. Buttons self-describe as a kind:"buttons" component; Anchor() renders a button beneath a named field. Guide: docs/buttons.md.
    • Misc fix that snuck into this PR: the root CMakeLists.txt self-locates, so the SDK works under add_subdirectory().

    ControlLoop: CV keeps running through Settings (#14)

    • Fixed stupid issue where CV matrix was skipped when settings was open.

    Pager direct addressing + settings gestures metadata (#17)

    • Pager::GoToPage(page, phys) goes to a target page with pot catch as expected.
    • Settings components advertise gesture-owned pots (gestures: [{page, pot, name}]) so the web tool can show that clearly.

    Named pot position constants (#13)

    • Some nice aliases for the canonical pot positions; examples updated to use them.

    Flash without touching the module

    hostlink-cli can reboot a running module straight into the system bootloader over the same USB connection the web editor uses, so no need to power cycle the rack. Pair it with dfu-util in a Makefile target for one-command flashing:

    .PHONY: program-live
    program-live: all
    	node $(ALCHEMY_DIR)/tools/hostlink-cli/hostlink.mjs reboot bootloader
    	dfu-util -w -a 0 -s $(FLASH_ADDRESS):leave -D $(BUILD_DIR)/$(TARGET_BIN) -d ,0483:$(USBPID)
    

    So: you can now use make program-live to reboot the module over USB and flash new firmware. Just add this to your makefile. This has already been added to the template repo.

    New documentation

    Upgrading from unversioned main

    • Presets survive except recorded lock motion, which resets once ('PLK0' → 'PLK1').
    • Lock configs whose arena exceeds 16 KiB stop compiling until placed deliberately — see docs/param-locks.md. This probably won't impact you.
    • Back compat looks good: older hosts ignore the new descriptor keys; older firmware keeps working against the updated web editor.