Define a Processor module

Connect a module declaration, realtime Processor, parameters, interface and tests, then publish the project with maru publish.

A Processor module keeps your musical behavior, controls, interface and assets in one editable project. You write target-neutral processing in restricted TypeScript; Maru supplies the host integration and builds the supported outputs. This guide connects the pieces you define before running maru publish.

Start with a complete example

Use the effect starter for audio processing or the instrument starter for a note-driven sound:

bash
maru init my-effect --kind processor-effect
cd my-effect
npm install
maru dev --open

For an instrument, use --kind processor-instrument when creating the project. The effect implements drive and tone stages; the instrument implements eight velocity-sensitive oscillator/ADSR voices with note transitions and release. Both include a schema, presets, a face and a qualification test. Change these working examples together as you develop your own sound.

The Quickstart covers toolchain access. Create a project covers the other starters, including Graph modules that connect existing nodes instead of authoring a Processor.

The pieces you define

PieceStarter locationWhat you define
Project entrypackage.jsonDependencies and maru.entry; development source paths identify the DSP, face and qualification test.
Module declarationsrc/index.tsIdentity, module kind, channel layout, parameter schema, presets, face and processing implementation.
Parameter contractsrc/contract.tsPaths, types, ranges, defaults, response curves and display units.
Processingsrc/dsp/processor.tsPer-instance state, preparation, parameter changes, block processing and reset; events and resources where needed.
Factory soundssrc/factory-bank.tsPreset IDs, names and values keyed by the schema's paths.
Interfacesrc/ui/index.tsxA face bound to the host's parameters and preset selection.
Verificationsrc/qualification.tsRepeatable input/events, observation duration and expected behavior.
Supporting contentProject assets and documentationResources the module uses, a README explaining the tool, and licenses and attribution for included material.

Keep the generated package's dependencies and scripts. Its entry configuration connects the authoring files to the tooling:

json
{
  "maru": {
    "type": "audio-module",
    "entry": "./src/index.ts",
    "development": {
      "sources": {
        "module": "src/index.ts",
        "dsp": "src/dsp/processor.ts",
        "react": "src/ui/index.tsx",
        "qualification": "src/qualification.ts"
      }
    }
  }
}

This is the maru block inside package.json, not a replacement for the rest of that file. Project structure explains authored files and generated output.

Connect the declaration to the Processor

In the effect starter, the declaration has this shape. The imported schema and preset bank are the files the starter creates:

ts
import { defineEffect } from "maruaudio/audio/define"
import { PROCESSOR_EFFECT_SCHEMA } from "./contract"
import { factoryPresets } from "./factory-bank"

export const AUDIO_MODULE = defineEffect({
  id: "com.example.my-effect",
  name: "My Effect",
  channels: 2,
  schema: PROCESSOR_EFFECT_SCHEMA,
  presets: factoryPresets,
  ui: {
    entry: "./ui/index.tsx",
    minSize: { width: 320, height: 420 },
    defaultSize: { width: 680, height: 420 },
    maxSize: { width: 1200, height: 900 },
  },
  implementation: {
    kind: "processor",
    source: "src/dsp/processor.ts",
  },
})

Choose your own stable id before the first publication, replacing the starter's provisional local. identity. It identifies the module across builds and hosts. Use your own namespace instead of the example above.

This example explicitly declares stereo. defineEffect defaults to mono; accepting stereo inside prepare alone does not change its ports. When changing the starter to stereo, also give its qualification input tape two channels. An asymmetric effect can use inputChannels and outputChannels instead.

Use defineInstrument for an instrument. Its starter also declares event and voice bounds. A MIDI tool uses defineMidiModule and declares event capacity; its Processor can run with zero audio channels. Keep ports and bounds aligned with what the processing actually consumes and produces.

Implement the lifecycle

The Processor file default-exports an object satisfying ProcessorDefinition<State> from maruaudio/dsp/processor:

FunctionResponsibility
createState()Allocate bounded per-instance storage. Size rate-dependent storage for the highest supported processing rate.
prepare(state, spec)Check the sample rate, maximum block size and channel layout; prepare coefficients and lengths. Return false for an unsupported configuration.
setParameter(state, path, value)Apply a declared parameter. Floats are numeric, booleans arrive as 0 or 1, and enums as option indices. Return false for an unknown path.
process(state, block)Read and write the current frames using the supplied buffers. Allocate no memory, acquire no locks, perform no I/O and call no browser APIs.
reset(state)Clear transient processing history so the instance behaves like a fresh one. The adapter reapplies schema defaults afterward.

The adapter applies schema defaults after preparation too. Keep defaults in the schema, with the Processor accepting them through setParameter.

ProcessorBlock uses planar buffers: a channel starts at channel * channelStride + frameOffset, and this call covers frameCount frames. Write only that region. The host owns the buffers and event timing; an event-only Processor writes no audio output.

Compose processing from focused maruaudio/dsp/... imports and your own DSP helpers. Keep compiler, browser and plugin-format choices outside the algorithm. DSP fundamentals explains I/O, state, units and the reusable building blocks, with category references for the toolkit. For the installed version's detailed contract, run:

bash
maru guide read processor-authoring restricted-typescript

Make controls, presets and state agree

Declare each control once in the schema, then implement its path in setParameter. Put its response curve in the descriptor's taper, so face controls, MIDI CC and host automation agree. Factory preset values use those same paths and valid ranges.

The face receives AudioModuleFaceProps from the host. Use the starter's faceParamBinding, edit gestures, UI-kit controls and FaceModuleNavbar to connect controls and preset selection. The host owns live parameter state, device access and audio execution. See Module UI.

Add optional contracts only when the tool needs them:

  • Notes and MIDI: implement the appropriate handleEvent, handleMidi and output-event functions and declare the required capacities. Keep voice handling and musical event transformations in the Processor.
  • Prepared resources: declare bounded implementation.resources slots and implement loadResource. The host loads and prepares content outside the realtime callback.
  • Durable content beyond parameters: declare state: { version, maxBytes } and implement both saveState and restoreState. Parameters are restored separately; test the two together.
  • Host services: declare the face capabilities needed for sign-in, generation or other services. Use the host bridge for those operations. Integration reference maps the entry points.

The versioned CLI guides cover these contracts in more detail:

bash
maru guide read parameters-and-schema presets-and-state
maru guide read midi-and-event-modules assets-and-resources

Verify the musical tool

Keep src/qualification.ts aligned with the declaration. For the effect starter, the input is 48 kHz with one array per input channel. Each array has (warmupQuanta + measuredQuanta) * 128 samples. The starter's 2 warmup and 30 measured quanta therefore need 4096 samples per channel. Its event tape exercises parameter changes; update it as the product changes.

Run the checks and use the workbench to exercise the complete tool:

bash
npm run typecheck
maru module validate
maru module check
maru module check . --portability

Test controls, preset recall, reset and saved state. For instruments, include overlapping notes, release, voice stealing and live control changes. Listen to the result as well as measuring it. Add module-specific tests for behavior the starter does not cover. Check, build and export explains offline rendering, levels, build reports and native verification.

Prepare and publish

Before uploading, confirm these parts of your project:

  • The entry resolves to the intended declaration and Processor; the identity, channel layout, schema, presets and referenced files agree.
  • The README describes the sound, controls, setup and supported targets. Included code and assets have appropriate rights, licenses and attribution.
  • Dependencies can be installed outside your checkout. For a source upload, replace workspace: and file: dependencies with distributable packages.
  • The listing license, source visibility and maru.share reuse settings match how you want to distribute the work. Reusable DSP source also needs the package export required by that sharing mode; keep the CLI's sharing checks.
  • Your machine has the required toolchain. maru module doctor reports missing tools. Select the Programs and targets you have tested.

Run a dry run with the same build options you intend to publish:

bash
maru publish --dry-run
maru login
maru publish

The default project publication builds a development JavaScript release. For a qualified JavaScript and WebAssembly release, use maru publish --qualify --programs js,wasm --dry-run, then the same command without --dry-run. Qualification needs the corresponding compiler and browser tools. Native plugin builds remain a separate target workflow.

Project publication uploads source by default so you can open it in Studio. Source is private unless already shared or explicitly made public. Use --source-visibility public with a suitable source license to share it, or --no-source to publish only the built release. A source-closed listing license requires the build-only path. This upload choice is separate from optional source/ inside a .maru bundle.

The command prints the publication reference and page URL. Keep that reference with your test results. Publishing covers release preparation and updates; Build and publish in CI extends the workflow to automation.

Find more examples

The two generated Processor starters are the smallest complete runnable examples. For other musical structures, inspect the examples and library available through your installed CLI:

bash
maru module examples
maru module list
maru module spec <slug>
maru module params <slug>
maru module files <slug>
maru module read <slug> src/index.ts

Read the declaration first to identify Processor versus Graph, then follow its implementation source, contract, face and qualification files. Source access and permission to reuse an existing module depend on its author's settings and license. Use the public maruaudio/... imports from your starter when adapting examples that use workspace package names.

For the architectural reason behind restricted TypeScript, read Restricted TypeScript in a DSP kernel. For a port from another plugin framework, see Constraints for plugin developers.