Humus organism SDK - quickstart
Humus's instruments and effects - organisms, the things that live and grow
in the soil - ship in packs: bundles you can enable, disable, install and
uninstall from Settings ▸ Packs.
Anyone can build and share new ones as .humpack files.
The full tutorial lives in packs/sample/README.md
- a walkthrough of the two commented example organisms (Tremolo, PingPong) covering the DSP contract, manifests, declarative editors, the C ABI and packaging. Start there.
How the system is layered
engine core the host: patcher graph, transport, GUI, Metapad,
automation, recording. Knows NO organism by name -
everything arrives through the Registry + manifests.
│
sdk/include/hum/ the contract (Organism, Transport, Parameter,
Capabilities) + the dsp/ toolkit. Deliberately
dependency-free: no JUCE, no engine headers.
│
packs/ the organisms, all built on the SDK:
├── core/ the plumbing (sound-card + aux I/O, MIDI ports, gains,
│ mixers, buses, crossfader, recorder) - pre-installed
├── humus/ the Humus originals plus the standard workbench
│ (dynamics, EQ, the modulation effects) - pre-installed
├── av/ every visual organism - pre-installed
├── <addon>/ an installable .humpack, downloaded separately
└── sample/ the tutorial pack - always built as a dynamic .humpack
The core stays independent; packs are content. The pre-installed packs (core,
humus, av - the BuiltinPacks.cpp table) are statically linked but register
through the very same Registry + manifest path a dynamic .humpack uses, so
"pre-installed" is a packaging choice, not an architecture one. The GUI app
meets an add-on set only as an installed pack; the dev binaries (hum CLI,
hum_tests) statically carry it so the shipped patches render and the organism
suites stay hermetic.
The dsp/ building blocks
sdk/include/hum/dsp/ is the toolkit packs are made of - plain headers, no
dependencies, safe on the audio thread. Both built-in packs build from these;
your pack should too before writing DSP from scratch:
| Header | Building block |
|---|---|
DspMath.h |
dB↔linear, one-pole smoothCoeff, T60 feedback law, stereo-linked peak detection |
Lfo.h |
phase-accumulator LFO (cycles 0..1): sine/tri/square shapes, phase-offset reads, tempo-syncable period |
EnvelopeFollower.h |
attack/release one-pole envelope follower |
DynamicsCore.h |
gain computers: CompressorCore (soft-knee, dB-domain), LimiterCore, GateCore |
Biquad.h |
RBJ cookbook filter section (LP/HP/BP/peaking/shelves) |
DelayLine.h |
fractional-read circular delay buffer |
Interpolation.h |
Hermite/sinc fractional-position resampling |
BeatGrid.h |
tempo + downbeat anchor mapping (beats ↔ file samples) |
SoundFileBuffer.h |
sound-file decoding into RAM |
LiveWavWriter.h |
lock-free audio-thread WAV recording |
Live reference implementations: the humus pack's Primitives category
(LFO, Follower, Number, Slider, Sig) - each is a single dsp/ block
exposed as a patchable object in ~40 lines, and doubles as the smallest
possible example of a complete organism (DSP class + organism.json +
help text + registration). Richer host integration (recorders, file
transports, DJ decks, MIDI) goes through the interfaces in
hum/Capabilities.h, or one domain of them at a time from hum/caps/
(Audio.h, Files.h, Graph.h, Midi.h, Osc.h, Params.h,
Samples.h, Video.h).
Give your organism a sigil. Implement hum::SigilSource (a tiny vector
draw-list: polylines and dots in the unit square, palette roles instead of
colors) and place a {"type": "sigil"} brick in your editor layout - the
host rasterizes it lo-fi (coarse pixels, 24 fps) with its own theme palette,
so your mark looks native in light and dark alike. Draw the sound, not a
logo: mirror your parameters into relaxed atomics in process() and build
the picture from them (Mineral's spinning gem - one side per partial - is
the reference implementation, packs/humus/organisms/Mineral/).
Quick facts
- SDK surface:
sdk/include/hum/-Organism(the DSP contract),Transport(musical clock + live input),Parameter/Pattern,Capabilities.h(host-integration interfaces, split by domain undercaps/),dsp/helpers, andPackEntry.h(the 4-symbol C ABI,HUM_PACK_ABI= 1). - A pack = manifests (JSON) + one shared library. Code registers factories; manifests carry metadata (schemas, categories, editors, help).
- Installing: Settings ▸ Packs ▸ Install pack copies a
.humpackinto the app's own packs folder. make packbuildsengine/build/humpacks/*.humpackfor every pack in the source tree, including the always-dynamic sample pack.- Compatibility: packs must be built with a host-ABI-compatible toolchain
(Linux: GCC/Clang + libstdc++); the host refuses an
hum_pack_abi()mismatch. - Conformance: the
pack-sdktest suite dlopens every built pack, creates all its classes through the C ABI and processes audio - copy that pattern.
Control pins: sockets and control outlets
Cords carry sound, numbers land on knobs, and a control pin is a knob with a
socket. A parameter that should take a control cord on the canvas says so in
organism.json with "socket": true; the host draws it as a control inlet,
and a cord dropped on it is a Control-with route onto that knob, evaluated in
the audio graph once per block. Nothing in the organism changes: it reads the
parameter as it always did.
An organism that emits control publishes named values through ControlSource
and, to have them drawn as control outlets, implements PinKinds and answers
controlOutlet(i) for each value index. A primitive that only sends control
has no audio pins at all: numAudioInputs() and numAudioOutputs() return 0,
and its process() runs anyway, once per block, to advance its state. Number,
Follower and LFO in the humus pack are the reference implementations;
Sig is the one box that turns a socket back into a signal.
An organism whose audio pins are not all alike can name them through
PortNames (hum/caps/Graph.h), and the patcher shows the name on hover and
opens a small gap wherever the name's group changes. It is dormant: no
organism implements it yet, so every box keeps plain, evenly spaced pins
until one opts in. Opting in is one line per side, through fromGroups:
comma-separated group names, each width pins wide and named L/R when the
width is 2, where a last name ending in * repeats numbered for the rest.
fromGroups("Signal, Key", i, ch_) gives Signal L, Signal R, Key L, Key R;
fromGroups("Main, Strand *", i, 2) gives Main L, Main R, Strand 1 L and on.
Categories, families and the dice
Every class in an organism.json names a category. The picker and the
Help index group by that word, so pick one the user already knows before
inventing one. Underneath, the host folds every category into one of five
families (familyOf in engine/src/core/packs/Categories.cpp), and the family
is what the user sees: the colour of the node's collar and mark, the soil
behind its panel, the tint of its knobs and grids, its chip on the timeline.
The guide page docs/guide/concepts.md says the same to users, and
the family-docs test fails if that page and the code disagree.
| Family | Categories |
|---|---|
| Voice | Instruments, Signal Generators, Players |
| Time | Effects, Filters, Spectral, Dynamics |
| Motion | Control, Primitives, Sequencers |
| Sense | Visual, Meters |
| Utility | Input/Output, Mixers, Buses, Pod, and any word the host does not know |
A category the host has never seen lands in Utility, sand grey. If your
class makes sound, name it an Instrument; if it changes sound, an Effect;
if it moves other things, Control. Sub-categories are written
Effects · Reverbs and keep their parent's family.
Parameters do not roll unless they ask to. "random": true lets the dice
(the one on the panel, and the one on the transport that rolls every
organism at once) move a parameter anywhere between its min and max;
"def-random": true rolls it once when the organism is created. A
parameter whose name says gain, volume, level, trim, master, output or
makeup must never ask, and neither may an envelope stage (attack, decay,
sustain, release, hold): tools/check_random_gain.py fails the battery if
one does, because a roll that turns a patch up, off or into a two-second
swell is a fault, not a surprise. Users can also keep any parameter out of the roll with
Exclude from Random on its right-click menu, and that choice saves with the patch.