# Host a module in React

> Embed a compiled module and its face with a headless React provider, parameter hooks and audio handles.

Canonical: https://maruaudio.com/docs/module-player-react
Documentation index: https://maruaudio.com/llms.txt

Import the headless host from `maruaudio/module-player/react` to play a built
module inside your own React application. You supply the module declaration,
its compiled Release, an optional face and your application controls. The
provider renders its children without adding a playback bar, styles or a
global session. React and React DOM are supplied by your application.

## Add a module and face

Install `maruaudio`, React and React DOM in your application. Build the module
and copy its complete web package into your app's public assets, for example
`public/modules/my-module/`. Keep the Release, Programs, resources and hashed
assets together. The imported declaration must describe that same module.

```tsx
import {
  ModulePlayerProvider,
  ModulePlayerFace,
  useModulePlayer,
  useModuleParameter,
} from "maruaudio/module-player/react"
import { AUDIO_MODULE } from "./my-module"
import MyModuleFace from "./my-module/ui"

const module = {
  definition: AUDIO_MODULE,
  releaseUrl: "/modules/my-module/release.json",
}

function Controls() {
  const { phase, error, controller } = useModulePlayer()
  const drive = useModuleParameter("drive")
  return <>
    <button onClick={() => controller.start().catch(console.error)}>
      {phase === "starting" ? "Loading…" : "Start audio"}
    </button>
    <button onClick={() => void controller.stop()}>Stop</button>
    <input aria-label="Drive" type="range" min={0} max={1} step={0.01}
      value={drive.value}
      onChange={event => drive.setValue(Number(event.target.value))} />
    {error && <p role="alert">{error}</p>}
    <ModulePlayerFace component={MyModuleFace} />
  </>
}

export function App() {
  return <ModulePlayerProvider module={module}>
    <Controls />
  </ModulePlayerProvider>
}
```

Replace the imports and parameter path with your module's exports. A face is
optional: remove `ModulePlayerFace` and draw every control yourself, or use
`useModuleFace()` to pass the standard `AudioModuleFaceProps` into a custom
layout. Import the face's own styles as required by its package. The host
does not import the UI kit, application fonts or global styles.

Mounting does not create an AudioContext or ask for device permissions.
Start audio from a user gesture. An effect also needs an input source; an
instrument needs its note or MIDI input. The face sends edits to the same
host that processes audio.

## Hooks and controls

| API | What it provides |
| --- | --- |
| `useModulePlayer()` | Reactive phase, error, parameter values, preset identity, dirty state and live audio handles, plus the controller |
| `useModulePlayerController()` | Stable imperative handles for event handlers and external integrations |
| `useModuleParameter(path)` | One engine-unit value, its descriptor, `setValue`, `beginEdit` and `endEdit` |
| `useModuleFace()` | Host-bound values, parameters, edit gestures, factory preset library, notes, levels and audio readiness |
| `useModulePlayerSession()` | The canonical session snapshot and its controller for MIDI devices, CC mappings and active notes |
| `useModuleLevels(enabled)` | Opt-in input/output meter sampling while running |

The controller exposes `start`, `stop`, `setParam`, `getParam`, `selectPreset`,
`noteOn`, `noteOff`, `panic`, `setMonitorLevel`, `connectInput`, `loadResource`,
`saveState`, `restoreState`, `send`, `sendMidi` and `onMidiOutput`.
`beginEdit`/`endEdit` report gestures through `options.onParamEdit` for a host
that records automation. `send` is for non-parameter events; write parameters
through `setParam` so the engine and face remain synchronized.

Parameter values use schema engine units, such as hertz or a numeric enum
index. Bounds, steps and taper come from the module schema. The host passes
engine values through the SDK event API; it does not normalize them a second
time. Factory presets apply one
parameter snapshot and retain selection/dirty identity. Saving user presets
or offering generation, camera and file import services requires the owning
application's services; this provider does not advertise absent capabilities.

## Connect an audio source

After Start resolves, `controller.getSnapshot().audio` contains the active
`context`, SDK `host`, output analyser and input analyser (`null` for a module
without audio input). For example, a host
that has already acquired a microphone stream can connect it like this:

```ts
await controller.start()
const { context } = controller.getSnapshot().audio!
const source = context.createMediaStreamSource(stream)
const disconnect = controller.connectInput(source)

// When the input changes or the host closes:
disconnect()
// Stop stream tracks only if this application owns the stream.
```

`connectInput` removes only the connection it owns. The player never stops
caller-owned sources or media tracks. Start an input source such as a buffer
player in your app and connect it in the same way. Monitor level is host gain,
separate from the module's own parameters. Read the output analyser for a
custom visualizer, or use the opt-in meter hook.

Raw MIDI bytes go to `sendMidi`; its boolean result reports whether the
running Program accepts that dialect. Use `noteOn`/`noteOff` for note-based
interaction. `onMidiOutput` subscribes to the Program's emitted batches;
the application chooses how to route those events. Device selection and CC
mapping use the exposed session controller rather than a second MIDI handler
inside the face.

## Supply a Release directly

A module package may already export a parsed Release and its file source.
Pass `{ definition, release, source }` instead of `{ definition, releaseUrl }`.
The source implements the SDK's browser Release file-source contract. URL
loading supports optional `sha256` and `fetchImpl` on the module descriptor.
The SDK verifies artifact/file hashes and chooses the requested available
Program; stale or failed qualification is not silently bypassed.

For an evidence-empty draft, a measuring harness or an intentionally
unqualified JS-only Release, supply an explicit SDK activation policy:

```tsx
<ModulePlayerProvider module={module} options={{
  prequalification: {
    purpose: "unqualified-release",
    artifactId: "program:js",
  },
}}>
  <Controls />
</ModulePlayerProvider>
```

`development` and `qualification` are the other explicit purposes. This
option names an exact Program and applies only to evidence-empty Releases.
Read the active SDK host's `qualificationStatus` for its actual status.

## Ownership, state and teardown

The player creates its own AudioContext on Start and closes it on Stop or
unmount. Pass `options.context` to borrow an application-owned context; the
player resumes it on Start and never closes or suspends it. All connected
nodes must belong to that same context. `options.destination` selects the
output node; `false` leaves the output for your surrounding host to connect.

Keep the declaration, Release and file source stable across renders.
Changing them replaces the player and disposes its old runtime. Inline option
objects and callback updates do not restart an otherwise unchanged host.
React StrictMode effect replay does not dispose a still-mounted player.
Unmount cancels pending loads, releases owned connections and closes owned
audio resources. Independent providers have independent state.

`saveState()` returns the module ID and parameter snapshot, including before
Start. `saveState({ includeProcessor: true })` also requests opaque Processor
bytes from the running Program; it rejects when that Program has no codec.
Opaque Processor bytes are
bound to that Program's artifact identity. `restoreState()` refuses another
module or an incompatible Program. Parameters can be restored before Start;
opaque state needs an active host with the matching codec. The application
owns storage and serialization of returned byte arrays.

For a non-React host, `createModulePlayer(module, options)` exposes the same
controller. Call `dispose()` when finished. A rack authored as a NodeGraphPatch
uses the SDK rack host to connect declared module ports; mounting separate
providers or iframe players does not create that rack.

Serve your app over HTTPS or localhost. Cross-origin payloads need CORS and
appropriate content types. An iframe should embed your complete player page,
with its Start action and permissions, rather than the module's bare
`ui/index.html`.

See [Module UI](https://maruaudio.com/docs/ui.md) for face authoring and [Check, build and
export](https://maruaudio.com/docs/export.md) for producing the module payload.
