Skip to content

Modules & Persistence

krach sessions can be captured, serialized, and replayed. The module system turns a live session into a frozen specification (ModuleIr) that can be saved, loaded, shared, and composed.

Capture & instantiate

# Build a session
bass = kr.node("bass", bass_fn, gain=0.3)
verb = kr.node("verb", reverb_fn, gain=0.3)
bass >> (verb, 0.4)
bass @ kr.seq("A2", "D3", None, "E2").over(2)
kr.tempo = 128

# Capture the entire state as frozen IR
ir = kr.capture()

# Later: replay on a fresh mixer
kr.load(ir)

capture() returns a ModuleIr — a frozen, serializable snapshot of everything: nodes (with their DspGraph IR), routing, patterns, controls, transport, and mute state.

load(ir) replays it: creates nodes, connects routing, plays patterns, sets controls.

Save & recall (in-memory scenes)

kr.save("verse")       # capture() + store by name
kr.save("chorus")

kr.recall("verse")     # clear + instantiate the saved scene
kr.recall("chorus")

kr.scenes              # → ["verse", "chorus"]

JSON serialization

import json

# Serialize
ir = kr.capture()
data = ir.to_dict()
json.dump(data, open("session.json", "w"))

# Deserialize
from krach.ir.module import ModuleIr
data = json.load(open("session.json"))
ir = ModuleIr.from_dict(data)
kr.load(ir)

The serialized format embeds DspGraphs inline — the full signal computation IR for each node. When you load and instantiate, the DSP is re-transpiled to FAUST and JIT-compiled. Nodes with identical DspGraphs (same graph_key) share compiled binaries.

Export to Python script

kr.export("~/sessions/my_jam.py")

Generates a self-contained Python script that recreates the session:

# Generated by kr.export()
import json
from krach.pattern.pattern import Pattern
from krach.pattern.serialize import dict_to_pattern_node

with kr.batch():
    kr.node("bass", bass_fn, gain=0.3)
    kr.node("verb", reverb_fn, gain=0.3)
kr.connect("bass", "verb", level=0.4)
kr.tempo = 128
# ... patterns, controls, etc.

Tracing proxy

For programmatic module construction without starting audio:

proxy = kr.trace()
proxy.node("kick", "faust:kick", gain=0.8)
proxy.node("hat", "faust:hat", gain=0.3)
proxy.send("kick", "verb", level=0.2)
proxy.play("kick", kr.hit() * 4)
proxy.tempo = 128

ir = proxy.build()       # → ModuleIr (no audio started)
kr.load(ir)       # now play it

The proxy records calls as ModuleIr without connecting to the engine. Useful for building reusable module templates.

@kr.module decorator

Define reusable modules as traced functions. The first parameter is a ModuleProxy — it records calls without starting audio:

@kr.module
def drums(m, tempo=128):
    m.node("kick", "faust:kick", gain=0.8)
    m.node("hat", "faust:hat", gain=0.3)
    m.send("kick", "hat", level=0.2)
    m.play("kick", kr.hit() * 4)
    m.tempo = tempo
    m.outputs("kick", "hat")

ir = drums(tempo=140)  # → ModuleIr (no audio)

The decorated function returns a frozen ModuleIr. Call it with any extra arguments — the proxy parameter is injected automatically.

ModuleHandle

kr.instantiate(ir, prefix) replays a ModuleIr with namespaced nodes and returns a ModuleHandle:

d = kr.instantiate(drums_ir, "drums")

# Operator DSL — delegates to first declared input/output
d >> verb          # route output to verb
bass >> d          # route bass into input
d @ pattern        # play on first input
d["kick/cutoff"] = 1200  # control access

# Properties
d.input            # → NodeHandle for first declared input
d.output           # → NodeHandle for first declared output
d.nodes            # → {"kick": NodeHandle, "hat": NodeHandle}
d.prefix           # → "drums"

All node names are prefixed: "kick" becomes "drums/kick". The existing / path addressing works naturally.

kr.scene(name) and kr.load(ir)

kr.scene(name) retrieves a saved scene by name (returns ModuleIr):

kr.save("verse")
ir = kr.scene("verse")   # → ModuleIr

kr.load(ir) replays a ModuleIr onto the mixer (session replay). It flattens any sub_modules automatically:

kr.load(ir)  # creates nodes, routing, patterns, transport

Module composition with m.sub()

Compose modules by nesting them inside a @kr.module definition:

@kr.module
def kit(m):
    m.node("kick", "faust:kick", gain=0.8)
    m.node("hat", "faust:hat", gain=0.3)
    m.outputs("kick", "hat")

@kr.module
def full_band(m):
    m.node("bass", "faust:bass", gain=0.5)
    drums = m.sub("drums", kit())  # nest kit as "drums/*"
    m.send(drums.output("kick"), "bass", level=0.3)
    m.outputs("bass")

ir = full_band()
band = kr.instantiate(ir, "band")
# Nodes: band/bass, band/drums/kick, band/drums/hat

m.sub(prefix, ir) returns a SubModuleRef with .input(name) and .output(name) for validated path strings. Typos are caught at trace time, not instantiation time.

prefix_ir() and flatten() (advanced)

For manual composition without the decorator:

from krach.ir.module import prefix_ir, flatten

# Namespace all names in an IR
namespaced = prefix_ir(drums_ir, "drums")
# "kick" → "drums/kick", controls and routes follow

# Resolve sub_modules into flat nodes
flat = flatten(parent_ir)
# Recursively prefixes and merges all sub_module nodes

prefix_ir(ir, prefix) rewrites all node names, routes, patterns, controls, and automations with the prefix. flatten(ir) recursively resolves sub_modules into a single flat IR (parent-wins for transport settings).