Kernels

What Are Kernels?

Most Legato nodes are executed on a per-block basis. This in general results in better cache performance, SIMD acceleration, etc., but this means that you cannot easily model feedback structures smaller than block size.

Kernels are a feature to help speed up the development process.

They are patches, using only patches in the kernel namespace. These are DSP primitives that operate per sample.

The Executor inside a kernel runs slightly different. Rather than a Khan sorted topography, they walk DFS, then find cycles by seeing if the node has already been registered. Then, an implicit z⁻¹ delay is added at this spot.

For now, this means that you cannot explicitly choose where to insert this delay, but for atleast my usecase, this is fine.

If you use LLMs, a strong pattern is to write the node using the kernel feature, then have an equivalence check for the block algorithm, you may have to account for the unit delay, but there is a good chance such a test can be written and a node generated.

Here is an example kernel node:

kernel comb(fb = 0.6) {
    in audio_in

    audio {
        add { val: 0.0 },
        tap { delay_length: 5, chans: 1 },
        mult { val: $fb }
    }

    audio_in >> add[0]
    add >> tap
    tap >> mult[0]
    mult >> add[1]     // closes the cycle

    { add }
}

patches {
    comb: c {}
}

audio {
    sine { freq: 330.0 }
}

sine >> c.audio_in

{ c }

Performance Impact

The not so fun part, this is much less fast than say a custom node. Custom nodes are likely anywhere from 4-30x faster depending on a few things, i.e CPU cache, if SIMD is possible, etc. You're basically throwing away all of the auto-vectorization and cache locality you can get, and you also have to pay a small enum gather every sample for each individual DSP kernel.

I would suggest prototyping with the kernel feature, and moving away from it if it starts impacting your realtime budget.

Here are some real numbers of an M3 Macbook Air

Implementation4096-sample stereo block @ 48kHzShare of realtime budget
Custom Rust node (plate480)~315 µs~0.4%
Kernel DSL (PLATE_KERNEL)~2.6 ms~3%

Current Limitations

  • Kernel bodies may only contain kernel-capable leaf nodes; no nested patches or kernels, no * N spawning, selectors, pipes, or port slices inside the body. Maybe in the future but I am tired.
  • No parameter arithmetic in the DSL (e.g. you cannot derive 1 - $mix; expose wet and dry separately instead). Might have an eval node or similar soon however.
  • No message routing to interior kernel nodes yet. Set kernel behavior through params at instantiation, if you need something more complex, write a custom node.