# 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.

Canonical: https://maru.audio/docs/dsp-nodes
Documentation index: https://maru.audio/llms.txt

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](https://maru.audio/docs/dsp-fundamentals.md) 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>`:

```ts
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](https://maru.audio/docs/licensing.md)
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:

```json
{
  "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:

```ts
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:

1. **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.
2. **Typed numbers.** Every value the state keeps between calls declares its
   storage: a typed array (`Float64Array`, `Float32Array`, `Int32Array`) or
   `F64` from `maruaudio/dsp/math/precision`. A plain `number` field is an
   error, because the compiler can't give it a fixed native width. A plain
   `number` inside the tick is fine.
3. **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.
4. **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.
5. **Package.** Nothing in the node reaches a browser, timer or network API,
   importing it runs no code (`sideEffects: false`), and its `exports` name 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](https://maru.audio/docs/publishing.md).

## 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](https://maru.audio/docs/licensing.md) for what you own and what you must credit.
