# Beats and tempo in a module

> Find beats, downbeats and onsets in audio: the model-free onset tools you can run anywhere, the host's beat tracker, and how to bring a beat grid into your module.

Canonical: https://maru.audio/docs/beats-and-tempo-in-a-module
Documentation index: https://maru.audio/llms.txt

Knowing where the beats are lets a module slice a loop, sync a delay, quantise
a transcription or line up a sample with the song. It is part of
[AI tasks in your module](https://maru.audio/docs/ai-tasks.md). For what beat tracking gets
right and wrong, read [Follow the beat](https://maru.audio/help/follow-the-beat.md) in
the Help Center.

| You want | Use | Runs |
| --- | --- | --- |
| Where sounds start (onsets), in your own analysis | `spectralFlux` and `pickPeaks` from `@maruaudio/audio-sdk/analysis/novelty` | Anywhere: pure functions over feature frames |
| Section boundaries in a clip | `footeNovelty` from the same subpath | Anywhere |
| Beats and downbeats of a song | The host beat tracker, today offered with stem separation (`trackRhythm`) | The host's worker |
| A beat grid from an app or server | `analyze_audio` with `analyses: ["rhythm"]` | Maru's job queue |

## Onsets without a model

Onset detection does not need a model. Compute log-band levels for each frame
(`@maruaudio/audio-sdk/analysis/log-bands`), take the spectral flux, then pick
peaks:

```ts
import { spectralFlux, pickPeaks } from "@maruaudio/audio-sdk/analysis/novelty"

const flux = spectralFlux(bandLevelsDb, frames, bands)
const onsetFrames = pickPeaks(flux, { minDistance: 4, threshold: 0.5 })
```

These run outside the audio callback in a face, a worker or a build script.
Inside a Processor, prefer the realtime analysis leaves in `@maruaudio/dsp`.

## Beats from the host

The host's beat tracker is exposed today on the separation surface: a face that
declares `audio.separation/v1` can call `separation.trackRhythm?.("mix")` or
`("drums")` and read `separation.state.rhythm`:

```ts
const rhythm = face.separation?.state.rhythm
// rhythm.beats and rhythm.downbeats are times in seconds
// rhythm.modelLabel names the tracker that produced them
```

`exportRhythm?("mid")` gives the person the grid as a MIDI file. Check that
`trackRhythm` exists before offering the action; a host without the tracker
omits it.

A dedicated beat-grid task for modules that do not separate stems is planned.
Until then, a module that needs beats from the host declares the separation
capability, as StemSep does.

## Current implementation

The beat tracker is [Beat This](https://maru.audio/models/beat-this). It returns beats and
downbeats; it does not return a single tempo or a time signature, so derive
those from the beat spacing if you need them.

## From an app or server

```ts
const beats = await submit({
  type: "analyze_audio",
  source,
  analyses: ["rhythm"],
  modelId: "beat-this/small0-onnx",
})
```

`rhythm` must be requested on its own, with that model id. Other analyses such
as `tempo`, `loudness` and `key` go in a separate `analyze_audio` job. See
[Add generative audio to your app](https://maru.audio/docs/generative-features.md#supply-audio-to-a-task).

## Ask your agent

```text
Read the Maru docs page "Beats and tempo in a module", then read StemSep's
source with `maru module files stemsep`, looking for how its face calls
trackRhythm. Add a "slice on beats" action to my sampler that uses the beat
grid when the host offers one and falls back to onset detection otherwise.
```
