Skip to content

Package Management

A resonon package is a directory — usually a git repository — that bundles reusable code (a src/lib.non file) and/or sample kits (a kits/ directory). There’s no registry and no publish step: packages live on GitHub and install with a gh:user/repo shorthand that maps straight to a clone URL.

This guide covers scaffolding projects, the resonon.toml manifest, installing and publishing packages, and the kit.toml sample-kit format. For the runtime CLI (running files, the REPL, the server), see the CLI Reference.

resonon init <name> scaffolds a project in the current directory; resonon new <name> creates a new directory first. Both accept flags that choose the project type:

FlagProject type
(none)Plain project with src/main.non.
--libLibrary with src/lib.non (importable code).
--kitSample-kit project with a kits/ directory.
--nativeNative Rust extension (implies --lib).

Flags combine:

Terminal window
resonon new my-pkg --lib --kit # library plus sample kits
resonon new my-ext --lib --native # library backed by a native extension

Every scaffold writes a resonon.toml manifest, a README.md, an MIT LICENSE, a .gitignore, and initializes a git repository (unless you’re already inside one). A plain project looks like this:

my-project/
resonon.toml
src/
main.non # entry point — run with `resonon run`
README.md
LICENSE
.gitignore

resonon run executes src/main.non from the project root. The generated .gitignore excludes dependencies/, which resonon install restores from the committed resonon.lock, and renders/, which is output rather than source.

The scaffolded src/main.non sets project metadata with builtins that double as your project’s configuration:

project_title("My Project");
project_artist("Your Name");
project_bpm(120);
BuiltinDescription
project_title(string)Set the project title.
project_artist(string)Set the artist name.
project_bpm(bpm, beats_per_cycle?)Set the project tempo in BPM. beats_per_cycle defaults to 4.

project_bpm() establishes the tempo at startup. To change tempo live during a session, use setbpm(bpm, beats_per_cycle?) instead — it takes effect immediately and is ignored while resonon is following an external MIDI clock. See Clock Sync.

resonon.toml declares package metadata and dependencies. It’s created automatically by resonon init / resonon new:

[package]
name = "my-project"
version = "0.1.0"
authors = ["Your Name <email>"]
resonon = "0.10.0"
[dependencies]
drums = { source = "gh:resonon/drum-kits", version = "v1.0" }
synths = { source = "gh:user/synths" }
FieldRequiredDescription
nameYesPackage name.
versionNoPackage version. Informational.
authorsNoList of authors. Informational.
resononNoresonon version the project targets.

resonon is compared against your installed version on every run, at major.minor precision. A project targeting a newer resonon refuses to run — its source may use syntax your version does not have. A project targeting an older one runs with a warning. Patch releases are interchangeable.

Each entry maps a package name to its source. The key is the local package name (the directory it installs into).

FieldRequiredDescription
sourceYesPackage source (gh:user/repo).
versionNoGit tag to pin to (e.g. "v1.0").

What the project draws on that has no upstream to fetch it from — the mirror image of [dependencies], and the reason nothing in it ever takes a source.

FieldRequiredDescription
kitsNoKits that came from outside the project and cannot be fetched.

resonon collect writes these entries for you. See Sharing a Project.

Present only for packages with a native Rust extension. See Native Extensions.

FieldRequiredDescription
crateYesPath to the native Rust crate (relative to the project root).

A key resonon doesn’t read is named, not obeyed and not fatal:

warning: /my-project/resonon.toml has a key that is not a manifest key:
`[package] versoin` — nothing reads this key. Did you mean `version`?

Everything else in the file still applies, and the piece still plays. A typo under [local] must not take your project name and your kit pool down with it mid-set — so it is reported, once, and the rest of the manifest is read as usual. Open resonon.toml in VSCode and the key is underlined where it is written, with a Rename to version quick fix.

A file that cannot be parsed at all is a different matter: there is no way to tell what it says, so every command that reads it stops and names the line. resonon install checks this before it fetches anything, so a manifest it could not record the install in leaves nothing behind on disk.

The pkg subcommands have top-level aliases for the common cases:

Full commandAlias
resonon pkg install <source>resonon install <source>
resonon pkg update [name]resonon update [name]
—resonon install (no args — install all from resonon.toml)
—resonon remove <name>

Install from GitHub with the gh:user/repo shorthand, optionally pinning a tag with @:

Terminal window
resonon install gh:resonon/drum-kits
resonon install gh:resonon/drum-kits@v1.0

Where packages install: in the project. Packages go to dependencies/ relative to the project root and are recorded in resonon.toml and resonon.lock, which is what makes the project reproducible somewhere else. There is no machine-wide package directory, and resonon install outside a project is an error rather than a silent install into one — start a project first with resonon new <name> or resonon init.

After install, resonon reports the package type and any kits:

Installed 'drum-kits' [kits] to dependencies/drum-kits
Kits:
808 — Sampler(Kit("drum-kits/808"))
vinyl — Sampler(Kit("drum-kits/vinyl"))

Run resonon install with no arguments to install every dependency listed in resonon.toml.

Dependencies are only half of what a freshly cloned project needs. If its resonon.toml pins a resonon version you do not have, resonon install says so when it finishes and names the command that fixes it:

This project targets Resonon 0.12.0, and 0.11.0 is running.
Switch with: resonon version switch 0.12.0 --install

It reports rather than acts — switching would change what every other shell resolves, and that is not something installing one project’s dependencies should do. See resonon version.

resonon install installs at the lock. For a dependency with no version pin, it checks out the commit recorded in resonon.lock rather than whatever the default branch currently points at — so everyone on the project, and every CI run, gets the same code. If a dependency has drifted away from the lock, resonon install restores it, and says which commit it put it back to:

Restored 'drum-kits' [kits] to dependencies/drum-kits, at locked commit 4f3c1a9e02b7
Kits:
808 — Sampler(Kit("drum-kits/808"))

A version in resonon.toml still wins, since pinning a tag is the more explicit instruction.

resonon update is the command that moves dependencies forward and rewrites the lock. Commit resonon.lock: it is project source, and is what makes installs reproducible across machines. Scaffolded projects track it by default and ignore the restored dependencies/ directory.

resonon pkg list shows installed packages and their types:

Installed packages (dependencies/):
drum-kits [kits]
kit: 808 — Sampler(Kit("drum-kits/808"))
kit: vinyl — Sampler(Kit("drum-kits/vinyl"))
my-lib [code]
studio-tools [code+kits]
kit: piano — Sampler(Kit("studio-tools/piano"))
TypeMeaning
[code]Has src/lib.non (importable code).
[kits]Has a kits/ directory.
[code+kits]Has both.

resonon pkg inspect <name> prints a package’s documentation, code exports with signatures, and kit details:

Package: my-lib [code+kits]
A collection of utility functions and drum kits.
Exports:
fn scaled(value, factor)
let default_bpm = <number>
Kits:
lo-fi — oneshot mode, 12 samples
Usage: Sampler(Kit("my-lib/lo-fi"))

resonon update [name] re-clones dependencies at their pinned tag (or the latest commit when no version is set), rebuilds any native extensions, and regenerates resonon.lock:

Terminal window
resonon update # update all dependencies
resonon update drum-kits # update one

resonon remove <name> drops a dependency from resonon.toml and resonon.lock and deletes its installed directory:

Terminal window
resonon remove drum-kits

A package needs at least one of: a src/lib.non (code) or a kits/ directory (samples). Both can coexist in one repository.

Write src/lib.non with the functions and values you want to share. All top-level fn, let, and class definitions are exported by default; mark items private to keep them internal. /// doc comments appear in resonon pkg inspect, and the first doc-comment block becomes the package description.

/// my-lib — utility helpers for generative rhythms.
/// The first doc-comment block becomes the package description.
private let lib_name = "my-lib";
/// Scale a value and round to the nearest integer.
fn scaled(value, factor) {
round(value * factor)
}
/// Default tempo for performance templates.
let default_bpm = 120;
/// Print a greeting. Internal — not exported.
private fn greet() {
print(f"Hello from {lib_name}!");
}

Organize WAV files under kits/<kit-name>/. A kit is one subdirectory of kits/:

my-drums/
kits/
808/
bd.wav
sd.wav
hh.wav

If you name files after GM drum abbreviations (bd, sd, hh, …), Resonon auto-detects notes and you can skip kit.toml entirely. For explicit control, add a kit.toml — and then the samples move into a samples/ subdirectory:

my-drums/
kits/
808/
kit.toml
samples/
bd.wav
sd.wav
hh.wav

See Kit Configuration.

Import a package’s code with use "<name>" — the bare package name resolves to its src/lib.non. The alias is the package name with hyphens turned into underscores, since it has to be a valid identifier:

use "studio-tools";
let pattern = studio_tools.swing(42, 0.6);

Load its kits with Sampler(Kit("<name>/<kit>")):

use "std/instruments" { Sampler, Kit };
let drums = AudioTrack("drums");
drums.instrument(Sampler(Kit("studio-tools/lo-fi")));
drums << [bd sd bd sd];
PLAY;

The bare kit name works too — Kit("lo-fi") — since a package’s kits are searched wherever the package itself is. Qualify when two packages ship a kit of the same name, or when you want the source to say which package a sound comes from.

See Modules for the full import system and Samplers for sampler details.

A kit.toml controls how a kit’s samples are mapped and played. Every section is optional — with no kit.toml, resonon auto-detects WAV files in the directory.

Every path a kit.toml names must be relative, must live under samples/, and must not climb out of the kit with ... A kit directory is a unit: that rule is what lets it be copied into a project — by hand or with resonon collect — and still play. A kit that pointed at /Users/you/samples/kick.wav would work on your machine and silently fall apart on anyone else’s. Loading such a kit is an error that names every offending path.

FieldTypeDefaultDescription
nameStringDirectory nameDisplay name for the kit.
authorString—Kit author.
modeString"oneshot"Playback mode: "oneshot" or "melodic".
default_sampleStringSole sampleInitial sample for SamplerMelodic; required when ambiguous.

Oneshot plays each sample at a fixed pitch — standard for drums. Melodic repitches samples relative to each sample’s root note — use it for tonal instruments.

Map sample names to notes in one place instead of repeating note in every entry. Values are note names ("C2", "F#3") or MIDI numbers (36, 42):

[defaults]
bd = "C2"
sd = "D2"
hh = "F#2"

Each key defines a sample:

FieldTypeDefaultDescription
filesArray of strings—WAV paths under samples/ in the kit directory. Multiple files enable round-robin.
noteString or numberAuto-assignedMIDI note that triggers this sample ("C2" or 36).
rootString or number—Root pitch for melodic mode ("C4" or 60).
choke_groupNumber—Samples sharing a group cut each other off.
velocityNumber1.0Default velocity (0.0–1.0).
loop_startNumber—Loop start, normalized (0.0–1.0).
loop_endNumber—Loop end, normalized (0.0–1.0).

A drum kit with a choke group (the open and closed hats cut each other):

[kit]
name = "Lo-Fi Drums"
[samples]
bd = { files = ["samples/kick.wav"] }
sd = { files = ["samples/snare.wav"], note = "D2" }
hh = { files = ["samples/hihat_closed.wav"], note = "F#2", choke_group = 1 }
hh_open = { files = ["samples/hihat_open.wav"], note = "A#2", choke_group = 1 }

Multiple files per sample give automatic round-robin variation:

[samples]
sd = { files = ["samples/snare_1.wav", "samples/snare_2.wav", "samples/snare_3.wav"], note = "D2" }

A melodic kit repitches relative to each root; use loop points for sustaining sounds:

[kit]
name = "Tape Piano"
mode = "melodic"
default_sample = "piano"
[samples]
piano = { files = ["samples/piano_c4.wav"], root = "C4" }
pad = { files = ["samples/pad_c3.wav"], root = "C3", loop_start = 0.1, loop_end = 0.95 }

SamplerMelodic(Kit("Tape Piano")) loads piano immediately. A melodic kit with exactly one sample may omit default_sample; a kit with several samples must declare it. Each selectable melodic sample names exactly one file—multiple files are round-robin material for the drum sampler. The selected file and its root must be valid when the device is constructed, so broken presets fail with an error instead of producing a silent instrument.

When a sample has no explicit note, resonon resolves it in order:

  1. Explicit note in the [samples] entry.
  2. A [defaults] mapping.
  3. The GM drum mapping below (by sample name).
  4. Sequential assignment from C2 (MIDI 36).
NameAliasesNote
bdkick, bassC2 (36)
sdsnareD2 (38)
rsrim, rimshotC#2 (37)
hhhihat, hat, chF#2 (42)
hh_openhihat_open, ohA#2 (46)
cpclapD#2 (39)
lttom, tom_lowA2 (45)
mttom_midB2 (47)
httom_high, tom_hiD3 (50)
crashcyC#3 (49)
ride—D#3 (51)
cbcowbellG#3 (56)
mamaracasA#4 (70)
clclaveD#5 (75)
lcconga_lowE4 (64)
mcconga_midD#4 (63)
hcconga_highD4 (62)

With no kit.toml, each .wav becomes a sample named after its filename, notes come from the table above (or sequentially from C2), and the mode defaults to oneshot — so a minimal kit is just a folder of WAV files.

A --native project adds a Rust crate under native/, a built-dylib directory lib/, and a [native] section in resonon.toml. Build it with:

Terminal window
resonon build # build for the current platform
resonon build --dist # platform-named output for binary distribution

Writing native extensions — the resonon-ext crate, the #[ext_fn] macro, and calling into Rust from .non — is covered in Going Native.

Packages publish by pushing to GitHub — gh:user/repo maps directly to https://github.com/user/repo.git. There’s no registry or publish command:

Terminal window
cd my-package
git init
git add .
git commit -m "Initial commit"
gh repo create my-package --public --source=. --push

Others then install it:

Terminal window
resonon install gh:yourname/my-package

Versioning. Tag releases with git tags. Consumers pin a tag in resonon.toml:

[dependencies]
my-package = { source = "gh:yourname/my-package", version = "v1.0" }

…or at install time with resonon install gh:yourname/my-package@v1.0. Without a pin, install clones the latest commit on the default branch, and resonon.lock records the exact commit so the install is reproducible.

“Module ‘x’ not found. Searched: …” — The message lists every directory that was looked in, tagged with why each one was searched. Check the package is installed (resonon pkg list) and that you’re using the repository name, not the [kit].name from kit.toml. The bare name (use "drum-kits") resolves to the package’s src/lib.non, and binds drum_kits — a hyphen cannot be part of a name.

“Name collision: ‘x’ is both …” — A package bound a name something else in your file already had. A warning, not an error; see Troubleshooting.

“no lib.non or kits/ found” after install — The repository doesn’t follow the package layout. Ensure code is at src/lib.non or kit WAVs are under kits/<kit-name>/.

Samples not loading — Confirm the kit directory holds .wav files (not .mp3/.flac) and that the path has both parts: Sampler(Kit("package-name/kit-name")).

  • CLI Reference — the runtime CLI (run, REPL, server).
  • Configuration — where each kind of setting lives, and the ~/.resonon layout.
  • Modules — the use import system.
  • Samplers — loading and playing sampler kits.
  • Going Native — writing native Rust extensions.