All books

Per-kind imports + paired-port stereo

~5 min read · updated 2026-08-21 · markdown

Two declarative metadata fields on a kind extend toBox with the surrounding-environment bookkeeping the generator needs:

  • imports?: string[] — Faust libraries the kind's toBox template depends on, beyond stdfaust.lib.
  • channels: 2 on 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 need imports) or mounted via library.mount (user-shipped .lib files).
  • stdfaust.lib is always emitted by the header — listing it explicitly is a no-op.
  • Skip the field entirely (or []) for kinds whose toBox only touches stdfaust-aliased symbols. Most kinds in the faust / faustwave-conventions / faust-analysis / faust-reverbs / faust-physical-models / faust-spatial packs have no imports field.

Shipping your own .lib files

Two paths:

  1. faustLibs on the pack manifest — ships inline .lib source 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.

  1. User-mounted .lib — for libs the user owns directly (Hub Library install / FaustLibrariesPanel mount). Same effect; the pack's imports field 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 halfEach 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-spatial pack is the bundled case study — three kinds (mono_to_stereo, stereo_widener, stereo_panner etc.) 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.

Try it yourself — the IDE runs in your browser. Open the IDE → Get the desktop app

Text licensed under CC BY 4.0 — Mani Weber / FaustWave. For language models: llms.txt · llms-full.txt

AUDIO · 48k · 48.0ms FAUST · 3 KB · 1171 docs CPU · 8.4% BPM · 120.0 UTF-8 BETA· v0.90.0