subsequence.definitions

Project definitions loader — a shared name-to-number vocabulary file.

A music project can keep one small YAML file (any filename — project.yaml, kit.yaml) mapping human names to MIDI numbers, shared between Subsequence and the Subsample sampler. Both tools read the same file, so renumbering a sound or a controller is one edit and both follow on their next reload. The file format is the only contract — neither application depends on the other.

The file is a flat mapping of sections, each mapping names to whole numbers:

notes:

ride_edge_soft: 53 dawn_chorus_pheasant: 60

cc:

sampler_release: 21

channels:

kit: 10 birds: 3

programs:

brushes: 1

nrpn:

filter_env_amount: 1042

Sections and value ranges (inclusive):

  • notes — MIDI note numbers, 0-127.

  • cc — controller numbers, 0-127.

  • channels — MIDI channels as musicians count them, 1-16.

  • programs — program-change numbers as sent on the wire (0-based), 0-127.

  • nrpn — 14-bit NRPN parameter numbers, 0-16383. Subsequence-specific; Subsample ignores this section.

Names must match [a-z][a-z0-9_]* — lowercase letters, digits, underscores; no dots, no leading digit. Unknown top-level sections are silently ignored, so either tool can grow a new section without breaking the other. An empty file, an absent section, and a null section are all valid. Every failure raises ValueError naming the file, section, and offending entry — the same checks, in the same order, as Subsample applies, so a bad file fails the same way in both tools.

Caveats worth knowing:

  • channels values are always user-facing 1-16 (the cross-tool contract). They pass straight into channel= under the default numbering; if your composition sets zero_indexed_channels=True, subtract 1 yourself.

  • YAML silently collapses duplicate keys — the last duplicate wins, and neither tool can detect it.

  • Names resolve exactly as written (lowercase) in drum_note_map / cc_name_map lookups. Merging over a stock map, the later dict wins: {**gm_drums.GM_DRUM_MAP, **defs.notes} lets your names shadow GM ones.

  • A subsequence.load_definitions(...) call at the top of a watched file re-runs on every live reload, so the definitions re-read naturally.

Example

import subsequence

defs = subsequence.load_definitions("project.yaml")
comp = subsequence.Composition(bpm=100)

@comp.pattern(channel=defs.channels["birds"], drum_note_map=defs.notes, cc_name_map=defs.cc)
def birds (b):
        b.hit("dawn_chorus_pheasant", beats=[0, 2.5])
        b.cc("sampler_release", 64)

Module Contents

class subsequence.definitions.Definitions[source]

The name-to-number tables read from a project definitions file.

One plain dict per section, always present — an absent or null section is an empty dict. The dicts merge directly into the existing parameters: notes into drum_note_map=, cc into cc_name_map=, nrpn into nrpn_name_map=, while channels values feed channel= and programs values feed p.program_change().

The dataclass is frozen (attributes cannot be reassigned) but the dicts themselves are ordinary mutable dicts, so they can be merged and extended freely.

Example

defs = subsequence.load_definitions("project.yaml")
defs.channels["birds"]      # 3
defs.notes                  # {"ride_edge_soft": 53, ...}
subsequence.definitions.load_definitions(path: str | pathlib.Path) Definitions[source]

Load and validate a project definitions file.

Reads the YAML file at path and returns a Definitions whose notes / cc / channels / programs / nrpn dicts merge straight into pattern parameters. See the module docstring for the file format, the value ranges, and the shared-vocabulary contract with the Subsample sampler.

Validation is strict inside the sections listed above and lenient outside them: unknown top-level sections are ignored, while a bad name, a non-whole number (including YAML true/false), or an out-of-range value is rejected with an error naming the file, section, and entry.

Parameters:

path – The definitions file, as a path string or pathlib.Path.

Returns:

A Definitions with one name-to-number dict per section.

Raises:

ValueError – If the file is missing, unreadable, or not valid YAML; if the top level or a consumed section is not a mapping; or if a name or value inside a consumed section is invalid. File-system and YAML errors are wrapped so this is the only error type raised.

Example

defs = subsequence.load_definitions("project.yaml")

@comp.pattern(channel=defs.channels["kit"], drum_note_map=defs.notes)
def kit (p):
        p.hit("ride_edge_soft", beats=[1, 3])