DSP nodes
What a DSP node is, the maru.dsp block that describes one, how maru dsp check validates it, and the rules for publishing measurements against other implementations.
A DSP node is one signal-processing algorithm with a stable name, a page and a contract: a one-pole filter, a compressor's detector, a reverb. Plugins are built from nodes. You import a node into your module's Processor, and the DSP library is the set of nodes Maru ships.
A node is TypeScript in the shape the compiler can turn into small native
functions: free functions over a state record (init, set<Name>, tick,
reset), where the state is allocated once and the tick allocates nothing.
Because a host compiles a node into the plugin that imports it, a node is
always TypeScript. Code in C, C++, Rust or another language can't be composed
that way. A native build is a plugin, not a node: publish it as a plugin.
Using a node
Each node's page gives its import line. In a project made with maru init,
Maru's nodes import from maruaudio/dsp/<category>/<leaf>:
import { biquadNodeTick, initBiquadNode } from "maruaudio/dsp/filters/biquad"
The page address mirrors the import: /dsp/@maru/filters-biquad is
maruaudio/dsp/filters/biquad. A node's slug is always its category and its
leaf name joined by a hyphen.
Check the node's license and provenance before reusing its source. Maru's copyright fallback says Source available · all rights reserved; an import example does not grant reuse rights. See Licensing for research preview terms and third-party licenses.
The maru.dsp block
A node project is a folder with a package.json whose maru block holds
dsp. This is a complete one-pole low-pass:
{
"name": "@you/filters-one-pole",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"exports": { ".": "./src/one-pole.ts" },
"maru": {
"dsp": {
"slug": "filters-one-pole",
"category": "filters",
"entry": "src/one-pole.ts",
"purpose": "A one-pole low-pass filter whose cutoff is the exact digital -3 dB point.",
"lifecycle": "initOnePole allocates one Float64Array of three slots; resetOnePole clears the history.",
"realtime": "onePoleTick reads and writes the state's slots in place; it allocates, locks and calls nothing.",
"parameters": [
{ "path": "cutoff", "label": "Cutoff", "type": "float", "min": 10, "max": 20000, "default": 1000, "units": "Hz" }
],
"ports": [
{ "id": "in", "label": "In", "direction": "input", "kind": "audio", "channels": 1 },
{ "id": "out", "label": "Out", "direction": "output", "kind": "audio", "channels": 1 }
],
"ratePolicy": { "mode": "host-native" },
"dependencies": [],
"license": "MIT",
"provenance": [],
"example": "examples/one-pole.demo.ts",
"readme": "README.md"
}
}
}
| Field | What it says |
|---|---|
slug, category | The node's name, <category>-<leaf>. The leaf is the entry's file name, so src/one-pole.ts in filters is filters-one-pole. |
entry | The TypeScript file that defines the node. |
purpose, lifecycle, realtime | What it does, what state it keeps and who resets it, and what its audio path allocates, locks or calls. The node page shows these. |
parameters | Each control. Its path names the setter: cutoff is setCutoff(state, value). |
ports, ratePolicy | Audio and control inputs and outputs, and whether the node runs at the host's rate. |
dependencies | Other nodes it imports, as @owner/<category>-<leaf>, optionally pinned with @<version>. |
license, provenance | An SPDX license or UNLICENSED, and a notice for each algorithm or piece of code you ported, naming its source and license. |
example, readme | An optional demo, and the README the node page shows: what the formula does, its parameters and units, and how to use it. |
The entry for that block keeps its state in one typed array:
export interface OnePoleState {
/** [coefficient, history, sampleRate] */
readonly slots: Float64Array
}
export function onePoleTick(state: OnePoleState, input: number): number {
const slots = state.slots
const next = slots[1] + slots[0] * (input - slots[1])
slots[1] = next
return next
}
Check a node
maru dsp check validates a node folder and prints what a publication would
attach as evidence. It runs from a Maru source checkout
(pnpm maru dsp check ./filters-one-pole), because it uses the checkout's
compiler. Each step either passes or names what to fix:
- The block. The fields above, the entry and README files, a setter for each parameter and one tick function. A slug that doesn't match the category and leaf, a missing README or an entry that isn't TypeScript stops here.
- Typed numbers. Every value the state keeps between calls declares its
storage: a typed array (
Float64Array,Float32Array,Int32Array) orF64frommaruaudio/dsp/math/precision. A plainnumberfield is an error, because the compiler can't give it a fixed native width. A plainnumberinside the tick is fine. - Realtime. The tick is compiled as a realtime entry. If it allocates, grows a container, blocks or calls something unproven, the compiler refuses it and names the line. The compiled tick then runs while allocator and lock calls are counted, from the first sample.
- Targets. The node is built for each target this computer supports (C, LLVM native and WebAssembly) and its output is compared, sample by sample, with the TypeScript running in Node. Each target is reported as matching, failed with the compiler's message, or not available here.
- Package. Nothing in the node reaches a browser, timer or network API,
importing it runs no code (
sideEffects: false), and itsexportsname the entry.
Add --json for the full report and the evidence records. --no-build skips
the realtime and target steps for a quick check of the block and the typed
numbers; it never reports a pass.
Publishing a node under your own handle isn't available: maru publish
recognizes a node folder, says so and uploads nothing. Modules and sampler
banks publish as described in Publishing.
Publishing measurements against other implementations
A node page can show timings beside other implementations of the same job. You may publish timings that you measured yourself for your node and for open-source comparables, if you name each comparable's exact version or commit, build flags and license, link its repository, and use the same fixture, work count, timing boundary, statistic, precision, channel layout, process phase and host on both sides. If any of these differ, show the numbers side by side as a non-comparison, without a ratio or "faster than". Do not paste or excerpt a comparable's source code. Do not publish timings, rankings or comparisons of closed commercial products unless you have read their license and it allows it; many commercial licenses forbid publishing benchmark results. Maru removes comparison rows that do not meet these rules.
A node that ports a published algorithm says so in provenance; see
Licensing for what you own and what you must credit.