Per-kind imports + paired-port stereo
Two declarative metadata fields on a kind extend toBox with the surrounding-environment bookkeeping the generator needs:
imports?: string[]— Faust libraries the kind'stoBoxtemplate depends on, beyondstdfaust.lib.channels: 2on ports — marks paired-port stereo (one L + one R port forming a single stereo signal).
Both shipped post-Phase-4 (PR #303 + #284 respectively). Together they let packs use libraries beyond stdfaust + carry stereo signals through the graph without folding to mono at every hop.
Per-kind imports
Some libfaust-wasm bundled libraries aren't aliased by stdfaust.lib. The big three packs hit on:
tubes.lib— vacuum-tube triode/pentode stage emulations (T1_12AX7,T2_12AX7, etc. — symbols bare, no namespace prefix)tonestacks.lib— guitar/bass amp tonestacks (bassman,jcm800,vox_ac30, etc.)vaeffects.lib— VA-modelled effects (crybaby,autowah)
faust-vintage ships 46 kinds across all three. Without imports plumbing, the only resolution path was to add import("tubes.lib"); etc. to every DSP by hand — fragile + per-DSP.
The imports field lets a kind declare its dependency:
{
"kind": "tube_12ax7_t1",
"imports": ["tubes.lib"],
"toBox": {
"outputs": { "out": "prim('T1_12AX7', input('in'))" }
}
}
The Builder's faustGen walks all node-kinds in the active graph, collects the union of every visited kind's imports, dedupes + sorts, and emits one import("<name>"); line per name in the DSP header right after the default import("stdfaust.lib");:
// Generated from graph — do not edit by hand for now.
import("stdfaust.lib");
import("tonestacks.lib");
import("tubes.lib");
import("vaeffects.lib");
process(in_l, in_r) = …
(Sorted alphabetically for diff-stable output across runs and snapshot tests.) The bare names T1_12AX7, jcm800, crybaby resolve at the DSP's symbol scope.
Rules
- Each entry must end in
.lib. - Names must be resolvable by libfaust — either bundled in libfaust-wasm (the three above +
analyzers.lib/physmodels.lib/quantizers.lib/ etc., all stdfaust-aliased so they don't needimports) or mounted vialibrary.mount(user-shipped.libfiles). stdfaust.libis always emitted by the header — listing it explicitly is a no-op.- Skip the field entirely (or
[]) for kinds whosetoBoxonly touches stdfaust-aliased symbols. Most kinds in thefaust/faustwave-conventions/faust-analysis/faust-reverbs/faust-physical-models/faust-spatialpacks have noimportsfield.
Shipping your own .lib files
Two paths:
faustLibson the pack manifest — ships inline.libsource bytes that the install flow auto-mounts into libfaust's VFS. Use when your pack needs a library NOT already in libfaust-wasm:
{
"packId": "my-pack",
"faustLibs": [{ "name": "mycustom.lib", "source": "<full .lib source as string>" }],
"kinds": [ … ]
}
The pack install flow calls library.mount("mycustom.lib", source) automatically. Subsequent compiles resolve import("mycustom.lib") against your inline source.
- User-mounted
.lib— for libs the user owns directly (Hub Library install / FaustLibrariesPanel mount). Same effect; the pack'simportsfield works the same way against any name in the VFS, regardless of how it got mounted.
Paired-port stereo
Phase 4 (PR #284) introduced the paired-port stereo pattern. Stereo signals carry as two adjacent ports flagged with channels: 2 (a marker, not a literal count — the L + R partner is implicit from the next port in declaration order).
The shape
{
"inputs": [
{ "id": "in_l", "label": "L", "kind": "audio", "channels": 2 },
{ "id": "in_r", "label": "R", "kind": "audio", "channels": 2 }
],
"outputs": [
{ "id": "out_l", "label": "L", "kind": "audio", "channels": 2 },
{ "id": "out_r", "label": "R", "kind": "audio", "channels": 2 }
]
}
Each port keeps its own id (so input('in_l') + input('in_r') in the Box-DSL works exactly like the mono case), but the channels: 2 marker tells the rest of the system:
- Builder UI: the dot rendering switches to the white-ring variant (same diameter, thin outer ring) and the cable stroke-width doubles when both halves are wired.
- Wire validator:
isPairedHalfPort()lets the validator surface a "wire the partner too" hint when only one half of a paired pair is connected, vs. plain mono cable. faustGen: dead-port-def gating already skips emit for ports nothing consumes; for paired ports it tracks both halves independently so a stereo path that only uses L stays mono in the emitted source.
When to use paired-port vs two separate mono ports
Use paired-port (channels: 2) | Use two mono ports |
|---|---|
| The signal is logically one stereo entity that should travel as a unit (e.g. a reverb tail, a panned synth's output) | The kind has two independent mono inputs (e.g. a mixer with two unrelated sources, a sidechain compressor's main + sidechain) |
| Wrong to use only one half | Each half is meaningful on its own |
The kind's toBox likely calls a (2, 2) lib function (re.jpverb, dattorro_rev_default, …) | Each port goes to a different leaf in the Box AST |
faust-spatial's mono_to_stereo is the canonical bridge: ONE mono input, TWO channels: 2 paired outputs. Use it (or a custom variant) to enter the stereo regime from a mono chain.
Worked example — Lush Reverb (Stereo)
{
"kind": "lush_reverb_stereo",
"inputs": [
{ "id": "in_l", "label": "L", "kind": "audio", "channels": 2 },
{ "id": "in_r", "label": "R", "kind": "audio", "channels": 2 }
],
"outputs": [
{ "id": "out_l", "label": "L", "kind": "audio", "channels": 2 },
{ "id": "out_r", "label": "R", "kind": "audio", "channels": 2 }
],
"toBox": {
"defs": {
"jp_pair": "seq(par(input('in_l'), input('in_r')), lib('re', 'jpverb', param('t60'), param('damp'), param('size'), real(0.6), real(0.1), real(2.0), real(1.0), real(1.0), real(1.0), int(200), int(6000)))",
"jp_l": "seq(def('jp_pair'), par(wire(), cut()))",
"jp_r": "seq(def('jp_pair'), par(cut(), wire()))"
},
"outputs": {
"out_l": "plus(times(input('in_l'), minus(real(1), param('mix'))), times(def('jp_l'), param('mix')))",
"out_r": "plus(times(input('in_r'), minus(real(1), param('mix'))), times(def('jp_r'), param('mix')))"
}
}
}
re.jpverb is intrinsically (2, 2) — it takes paired stereo in + emits paired stereo out. par(input('in_l'), input('in_r')) collects the two mono Boxes into one (0, 2) Box; seq(…, lib('re', 'jpverb', …)) chains them into the reverb. Then par(wire(), cut()) peels off only L, par(cut(), wire()) peels off only R, so the per-output dry/wet mix can route per-channel.
Cross-cutting consequences
Once you mark paired ports with channels: 2, the surrounding system honours them:
- The picker + Inspector show paired-port kinds with the stereo-cable hint.
- The wire validator refuses cable patterns that violate paired-port semantics (e.g. crossing L into R mid-pair).
- The
faust-spatialpack is the bundled case study — three kinds (mono_to_stereo,stereo_widener,stereo_panneretc.) that demonstrate the pattern end-to-end. Read it as a template.
A kind that's stereo internally but exposes a (1, 2) shape (one mono in → two mono outs) is NOT paired-port. Use channels: 2 only when both halves are logically a unit.