> ## Documentation Index
> Fetch the complete documentation index at: https://docs.soundpiece.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Normalise loudness

> Bring a track to a standard loudness for where it will be played

`adapt.normalise_loudness` takes a track you already have and delivers it at a target loudness, with its peaks kept safely below clipping. Use it when audio from different places needs to play back at a consistent level:

* a podcast episode recorded at a different level from last week's;
* music or voiceover that will sit next to other content on a streaming service, a podcast feed or a broadcast channel;
* a batch of tracks that should all sound equally loud, one after another.

If you are generating or adapting audio with Soundpiece anyway, you may not need this endpoint: `create.new_song`, `create.new_instrumental`, `create.remix_song`, `create.remix_instrumental` and `adapt.auto_cut_down` can normalise their own outputs for free. Add the same `loudness` block to those requests. See [Loudness](/loudness) for how normalisation works on every endpoint.

***

## The request

| Field | Required | What it is |
| - | - | - |
| `source` | yes | The track: `{"url": "https://..."}`, a public link to a WAV, MP3 or FLAC file. |
| `loudness` | no | The loudness to deliver at, as a [preset](#presets-or-a-number) or a number. Leave it out for -14 LUFS. |
| `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. |
| `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. |

Every field, with its exact rules: [submit](/api-reference/adapt/normalise_loudness/submit) and [poll](/api-reference/adapt/normalise_loudness/poll) in the API Reference.

***

## Example

This normalises a podcast episode with the `podcast` preset (-16 LUFS) and saves the result. It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first.

```python normalise.py theme={null}
import soundpiece

result = soundpiece.run("adapt.normalise_loudness", {
    "source": {"url": "https://example.com/audio/episode.mp3"},
    "loudness": {"preset": "podcast"},
})

soundpiece.download(result["output"]["url"], "episode.flac")
measured = result["output"]["loudness"]
print(f"Saved episode.flac at {measured['lufs']} LUFS, true peak {measured['true_peak_dbtp']} dBTP")
```

```javascript normalise.mjs theme={null}
import { run, download } from "./soundpiece.mjs";

const result = await run("adapt.normalise_loudness", {
  source: { url: "https://example.com/audio/episode.mp3" },
  loudness: { preset: "podcast" },
});

await download(result.output.url, "episode.flac");
const measured = result.output.loudness;
console.log(`Saved episode.flac at ${measured.lufs} LUFS, true peak ${measured.true_peak_dbtp} dBTP`);
```

```bash cURL theme={null}
#!/usr/bin/env bash
# Submit, wait and download by hand. Needs curl, jq and uuidgen.
set -euo pipefail
API=https://api.soundpiece.co/v1
AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY"

# 1. Submit. Sending this exact body again (same idempotency_key) is safe.
body=$(jq -n --arg key "$(uuidgen)" '{
  idempotency_key: $key,
  source: { url: "https://example.com/audio/episode.mp3" },
  loudness: { preset: "podcast" }
}')
response=$(curl --silent --show-error --max-time 30 --request PUT "$API/adapt.normalise_loudness" \
  --header "$AUTH" --header "Content-Type: application/json" --data "$body")
result=$(jq -e .normalised_audio <<<"$response") || { echo "$response"; exit 1; }
id=$(jq -r .id <<<"$result")

# 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds.
while [ "$(jq -r .status <<<"$result")" = processing ]; do
  response=$(curl --silent --show-error --max-time 30 "$API/adapt.normalise_loudness?id=$id&wait=true" \
    --header "$AUTH")
  result=$(jq -e .normalised_audio <<<"$response") || { echo "$response"; exit 1; }
done

# 3. Download, or show what went wrong.
if [ "$(jq -r .status <<<"$result")" = failed ]; then
  jq .error <<<"$result"
  exit 1
fi
curl --silent --show-error --fail --output episode.flac "$(jq -r .output.url <<<"$result")"
jq .output.loudness <<<"$result"
echo "Saved episode.flac"
```

***

## The result

`run` returns the finished `normalised_audio` object:

```json theme={null}
{
  "id": "op_01ks2p3hc0etfb59tdwh9edd1r",
  "status": "ready",
  "created_at": "2026-10-10T09:15:02Z",
  "idempotency_key": "3f9a1c2e-5b7d-4e8f-a0b1-c2d3e4f5a6b7",
  "loudness": { "target_lufs": -16.0, "allow_compression": true, "preset": "podcast" },
  "output": {
    "url": "https://cdn.soundpiece.co/...signed...",
    "expires_at": "2026-10-10T09:31:40Z",
    "duration": 184.3,
    "loudness": { "lufs": -16.0, "true_peak_dbtp": -2.1, "compressed": false }
  },
  "error": null
}
```

| Field | What it means |
| - | - |
| `loudness` | What you asked for. `target_lufs` is always filled in, whichever way you asked; `preset` is there only when you named one. |
| `output.url` | A signed link to the normalised audio, as a FLAC file (44.1 kHz). It needs no API key. |
| `output.expires_at` | When the link stops working. Poll the operation again for a fresh one. |
| `output.duration` | The length in seconds. Normalising never changes it. |
| `output.loudness.lufs` | The loudness **measured** on the file you download. |
| `output.loudness.true_peak_dbtp` | The true peak measured on that file. It is always -1 dBTP or lower. |
| `output.loudness.compressed` | `true` if reaching the target meant compressing the dynamics; `false` if only the level changed. |

The measurements are taken from the delivered audio, not copied from your request, so they tell you what really happened. The next section shows the three things that can happen.

***

## Three possible outcomes

Normalising changes a track's level. Turning a track up raises its loudness and its peaks together, and peaks are never allowed above -1 dBTP. Most audio can reach its target long before its peaks get there. Very dynamic audio cannot: its peaks are loud compared with its average level, so they hit the ceiling first. `allow_compression` decides what happens then.

### 1. It lands on the target

Most music and speech, at any sensible target. Only the level changes, and `compressed` is `false`. This is the podcast episode from the example: asked for `{"preset": "podcast"}`, delivered at -16.0 LUFS.

```json theme={null}
"loudness": { "target_lufs": -16.0, "allow_compression": true, "preset": "podcast" },
"output": {
  "url": "https://cdn.soundpiece.co/...signed...",
  "expires_at": "2026-10-10T09:31:40Z",
  "duration": 184.3,
  "loudness": { "lufs": -16.0, "true_peak_dbtp": -2.1, "compressed": false }
}
```

### 2. It is compressed to reach the target

A dynamic recording, such as a quiet orchestral piece with loud climaxes, asked to reach -14 LUFS with compression allowed (the default):

```json theme={null}
{
  "source": { "url": "https://example.com/audio/orchestral.wav" },
  "loudness": { "target_lufs": -14 }
}
```

Turning it up by level alone would push its peaks over the ceiling, so its dynamics are gently compressed. It lands close to the target, typically within about 1.5 LU, and `compressed` is `true`:

```json theme={null}
"loudness": { "target_lufs": -14.0, "allow_compression": true },
"output": {
  "url": "https://cdn.soundpiece.co/...signed...",
  "expires_at": "2026-10-10T09:31:40Z",
  "duration": 247.9,
  "loudness": { "lufs": -14.3, "true_peak_dbtp": -1.0, "compressed": true }
}
```

### 3. It stops short, because you kept the dynamics

The same recording, with compression turned off:

```json theme={null}
{
  "source": { "url": "https://example.com/audio/orchestral.wav" },
  "loudness": { "target_lufs": -14, "allow_compression": false }
}
```

Only the level changes, and it stops when the peaks reach the ceiling. The dynamics are untouched, but the track comes out quieter than you asked for, here at -15.8 LUFS:

```json theme={null}
"loudness": { "target_lufs": -14.0, "allow_compression": false },
"output": {
  "url": "https://cdn.soundpiece.co/...signed...",
  "expires_at": "2026-10-10T09:31:40Z",
  "duration": 247.9,
  "loudness": { "lufs": -15.8, "true_peak_dbtp": -1.0, "compressed": false }
}
```

This is not an error: the operation is `ready`, and `lufs` tells you where the track landed. If the difference matters to you, check `lufs` against your target in your own code, and decide whether to accept it or to resubmit with compression allowed (under a new idempotency key, since it is a different request).

***

## Choosing parameters

### Presets or a number

There are two ways to say what loudness you want:

* **A preset** names a destination: `{"preset": "podcast"}`. Each preset stands for a fixed value that never changes, so your code means the same thing next year.
* **A number** gives the target in LUFS, from `-30` to `-6`: `{"target_lufs": -18}`. Use it when a delivery specification gives you a number that isn't a preset.

Send one or the other, not both: a block with `preset` and `target_lufs` together is refused with a `422`. Both accept `allow_compression`. An empty block, `{}`, or no block at all, means -14 LUFS with compression allowed.

| Preset | Target | What it's for |
| - | - | - |
| `streaming` | -14 LUFS | Music for streaming services and online video, which most of them play back at about this level. |
| `podcast` | -16 LUFS | Podcasts and other spoken-word audio, the level podcast platforms recommend. |
| `broadcast` | -23 LUFS | Television and radio broadcast, the EBU R128 standard. |

### Choosing a target by destination

| Where the audio is going | Send |
| - | - |
| Music streaming services, online video, social media | `{"preset": "streaming"}` (or leave `loudness` out) |
| A podcast feed, an audiobook sample, a voice-led explainer | `{"preset": "podcast"}` |
| European TV or radio (EBU R128) | `{"preset": "broadcast"}` |
| A client's delivery specification | Their number, as `target_lufs` |

If you don't know where the audio will end up, `streaming` is a safe default. Audio that is louder than a platform's playback level is turned down by the platform, and audio that is quieter sounds weak next to everything else.

### Should I allow compression?

* **Leave it on (the default)** when hitting the number matters most: a platform or client will measure the delivery, or it will play right after other audio at that level.
* **Turn it off** when the dynamics matter more than the number: classical music, film score, or anything where the quiet parts should stay quiet. Expect very dynamic audio to come out a little below the target.

For most material it makes no difference, because the target can be reached by a change of level alone.

### The peak ceiling

The true peak is always kept at or below -1 dBTP, and that isn't a setting. It leaves the headroom that MP3 or AAC encoding and playback need to stay free of clipping.

***

## Errors

**When you submit** (the request is refused, nothing is charged):

| Status | `type` | Why |
| - | - | - |
| `422` | `extra_forbidden` | `loudness` has both `preset` and `target_lufs`. Send one. |
| `422` | `literal_error` | `preset` isn't one of `streaming`, `podcast` or `broadcast`. |
| `422` | `less_than_equal` / `greater_than_equal` | `target_lufs` is outside `-30` to `-6`. |
| `422` | `source_too_large` | The source's server says the file is over 50 MB. (When it doesn't say, the size is checked during processing instead.) |
| `422` | `idempotency_key_reused` | The key was already used for a different endpoint. |
| `402` | `insufficient_credits` | Your balance is below 1 credit. |

**While processing** (the operation finishes as `failed`, with an `error.code`):

| Code | Why |
| - | - |
| `source_unreachable` | The source URL couldn't be fetched. |
| `source_format_unsupported` | The file isn't WAV, MP3 or FLAC audio. |
| `source_too_short` | The audio is shorter than 1 second. |
| `source_too_long` | The audio is longer than 5 minutes. |
| `source_too_large` | The file is larger than 50 MB. |
| `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. |

**Source limits for this endpoint:** WAV, MP3 or FLAC, from 1 second to 5 minutes long, and at most 50 MB.

See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`).

***

## Cost

**1 credit** per request, whatever the target and however long the track. Resending a request with the same idempotency key is not charged again.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.