Humusprivate beta

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 under caps/), dsp/ helpers, and PackEntry.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 .humpack into the app's own packs folder.
  • make pack builds engine/build/humpacks/*.humpack for 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-sdk test 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.