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

# Auto-duck

> Mix a voiceover over music, turning the music down while the voice speaks

`adapt.auto_duck` takes a music track and a voice recording and delivers one finished mix, with the music turned down ("ducked") whenever the voice is speaking so every word stays clear. Use it for adverts, explainers, podcast intros, narrated video and anywhere else a voice talks over music.

You don't need to balance the two recordings first. In one step:

1. the voice is levelled, so it sits at the same level whether it was recorded quietly or loudly;
2. the music is placed underneath it, at a level that leaves room for the speech;
3. the music is ducked while speech is detected, and comes back up when it stops;
4. the mix is delivered at a target loudness: -14 LUFS unless you ask for another, with its true peak at or below -1 dBTP.

***

## The request

| Field | Required | What it is |
| - | - | - |
| `music` | yes | The music: `{"url": "https://..."}`, a public link to a WAV, MP3 or FLAC file. |
| `voice` | yes | The speech, in the same form. It must be a different file from `music`. |
| `loudness` | no | The loudness to deliver the mix at, as a preset or a number. Leave it out for -14 LUFS. See [Loudness](/loudness). |
| `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/auto_duck/submit) and [poll](/api-reference/adapt/auto_duck/poll) in the API Reference.

***

## Example

This mixes a voiceover over a music bed for a podcast intro, delivered with the `podcast` preset (-16 LUFS). It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first.

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

mix = soundpiece.run("adapt.auto_duck", {
    "music": {"url": "https://example.com/audio/music.mp3"},
    "voice": {"url": "https://example.com/audio/voiceover.mp3"},
    "loudness": {"preset": "podcast"},
})

soundpiece.download(mix["output"]["url"], "mix.flac")
print(f"Saved mix.flac ({mix['output']['duration']} seconds)")
```

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

const mix = await run("adapt.auto_duck", {
  music: { url: "https://example.com/audio/music.mp3" },
  voice: { url: "https://example.com/audio/voiceover.mp3" },
  loudness: { preset: "podcast" },
});

await download(mix.output.url, "mix.flac");
console.log(`Saved mix.flac (${mix.output.duration} seconds)`);
```

```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,
  music: { url: "https://example.com/audio/music.mp3" },
  voice: { url: "https://example.com/audio/voiceover.mp3" },
  loudness: { preset: "podcast" }
}')
response=$(curl --silent --show-error --max-time 30 --request PUT "$API/adapt.auto_duck" \
  --header "$AUTH" --header "Content-Type: application/json" --data "$body")
result=$(jq -e .ducked_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.auto_duck?id=$id&wait=true" \
    --header "$AUTH")
  result=$(jq -e .ducked_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 mix.flac "$(jq -r .output.url <<<"$result")"
echo "Saved mix.flac"
```

***

## The result

`run` returns the finished `ducked_audio` object:

```json theme={null}
{
  "id": "op_01ks2pthpgf4qs4fr84g98pbsk",
  "status": "ready",
  "created_at": "2026-10-10T09:15:02Z",
  "idempotency_key": "9b2e4c1a-7d3f-4a5b-8c6d-0e1f2a3b4c5d",
  "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": 94.2,
    "loudness": { "lufs": -16.0, "true_peak_dbtp": -1.8, "compressed": false }
  },
  "error": null
}
```

| Field | What it means |
| - | - |
| `loudness` | The loudness the mix was delivered at. `target_lufs` is always filled in; `preset` is there only when you named one. |
| `output.url` | A signed link to the mix, 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 mix's length in seconds: the length of the **longer** of your two inputs. |
| `output.loudness` | Measured on the mix you download: `lufs`, `true_peak_dbtp`, and whether the dynamics were `compressed` to reach the target. |

The source URLs are never echoed back.

***

## Choosing parameters

* **Length.** The mix is as long as the longer input. The shorter one is padded with silence at its end, so nothing is cut off. To end the music with the voice, trim the music to length first, for example with [`adapt.auto_cut_down`](/guides/adapt/auto_cut_down).
* **Loudness.** Pick the preset for where the mix is going: `streaming` (-14 LUFS, the default) for online video and social media, `podcast` (-16 LUFS) for podcast feeds, `broadcast` (-23 LUFS) for EBU R128 television and radio. Or send `{"target_lufs": ...}` for another number between -30 and -6. The [loudness normalisation guide](/guides/adapt/normalise_loudness#choosing-a-target-by-destination) has a fuller table.
* **`allow_compression`** works as it does everywhere: on by default, so the mix lands on the target; set it to `false` to keep the dynamics untouched, at the cost of landing a little quieter when the audio is very dynamic. See [Loudness](/loudness#allow_compression).
* **Inputs.** A clean voice recording, without its own music under it, gives the best result. The music can be any finished track or bed; you don't need to turn it down beforehand.

***

## Errors

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

| Status | `type` | Why |
| - | - | - |
| `422` | `same_source` | `music` and `voice` are the same file: the same URL, or two URLs recognisably serving the same file. Reported at `["body", "voice", "url"]`. |
| `422` | `extra_forbidden` | `loudness` has both `preset` and `target_lufs`. Send one. |
| `422` | `source_too_large` | A 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` | A source URL couldn't be fetched. |
| `source_format_unsupported` | A file isn't WAV, MP3 or FLAC audio. |
| `source_too_short` | An input is shorter than 1 second. |
| `source_too_long` | An input is longer than 5 minutes. |
| `source_too_large` | A file is larger than 50 MB. |
| `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. |

For a `source_*` error, `error.source` says which input it concerns, `"music"` or `"voice"`, when that is known:

```json theme={null}
"error": {
  "code": "source_unreachable",
  "message": "The source URL could not be reached.",
  "source": "voice"
}
```

**Source limits for this endpoint:** each input 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, normalisation included. 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.