# Podcast Loudness Normalizer (LUFS, True Peak) for AI Agents (`ent-e/podcast-loudness-normalizer`) Actor

Loudness normalization API for podcasts, video and broadcast. Send a URL, get MP3, M4A, WAV, FLAC or MP4 at -16, -14, -23 or -24 LUFS with a -1 dBTP true-peak ceiling and a measured before/after report. Batch. No DAW or plugins. MCP, n8n, Make, API. Failed files free.

- **URL**: https://apify.com/ent-e/podcast-loudness-normalizer.md
- **Developed by:** [ENT-E](https://apify.com/ent-e) (community)
- **Categories:** AI, Agents, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 file normalizeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Podcast Loudness Normalizer (LUFS, True Peak) for AI Agents

The Loudness Normalization API from ENT-E turns any podcast, voice or video file URL into a broadcast-grade file at -16, -14, -23 or -24 LUFS, with a true-peak ceiling, so every episode meets platform specs automatically, with no DAW, plugins or ffmpeg setup.

### Loudness normalization API: hit platform LUFS targets in one call

Send one URL, or a whole batch. Pick a preset (`podcast`, `apple`, `spotify`, `youtube`, `ebu-r128`, `atsc-a85`) or set your own target. You get back a normalized MP3, M4A, WAV, FLAC or MP4 file, plus a before/after loudness report you can log, check or show to a client.

How it works:

1. The Actor measures the whole file first (two-pass EBU R128 loudness normalization with FFmpeg's `loudnorm`, linear mode where possible).
2. It applies gain with a 4× oversampled true-peak limiter.
3. It **re-measures the encoded file** and re-renders when the ceiling or target is missed, up to 3 attempts. The row you receive reports the file that was actually delivered.

### What you get

- **Platform presets with fixed targets**:

  | `preset` | Integrated loudness |
  |---|---|
  | `podcast`, `apple` | -16 LUFS |
  | `spotify`, `youtube` | -14 LUFS |
  | `ebu-r128` | -23 LUFS |
  | `atsc-a85` | -24 LUFS |
  | `custom` | your `targetLufs` (-70 to -5) |

  The true-peak ceiling defaults to -1 dBTP (`truePeakDbtp`) and the loudness range to 11 LU (`loudnessRange`).
- **Measured, not assumed.** Every row includes `beforeLufs`, `afterLufs`, `afterTruePeakDbtp`, `gainAppliedDb` and `truePeakCompliant`, plus full `before`/`after` objects with integrated loudness, loudness range and true peak.
- **Output formats**:
  - `mp3` (192 kbps), the default
  - `m4a` (AAC 192 kbps)
  - `wav` (24-bit)
  - `flac`
  - `mp4`, which copies the video stream and re-encodes the audio to AAC, so you can normalize a video podcast without re-rendering the picture
- **Batch.** Use `files` for many episodes in one run. Any setting can be overridden per file.
- **Failed files are free.** A bad URL, a silent file, an audio-free input or an over-length file returns an error row and isn't charged.

### Read this first: true peak beats the target

The true-peak ceiling always takes priority over the loudness target. For loud, very peaky audio, reaching -14 LUFS on the `spotify` or `youtube` preset can be impossible without crossing the -1 dBTP ceiling. In that case the Actor keeps the ceiling and lets the file land a little below -14 LUFS. You still get a clean, non-clipping file, and the row tells you exactly what happened: the measured `afterLufs`, `truePeakCompliant: true`, and `normalizationType: "dynamic"`. No row will claim -14.0 LUFS if the file isn't at -14.0 LUFS. In our tests, integrated loudness typically lands within ±0.5 LU of the target.

### Use cases

#### Normalize audio to LUFS for a podcast feed (-16 LUFS)

Apple Podcasts and most podcast hosts recommend around -16 LUFS. This is the default.

```json
{
  "url": "https://example.com/episodes/ep-142-raw.wav",
  "preset": "podcast",
  "outputFormat": "mp3"
}
```

#### Spotify -14 LUFS and YouTube loudness

```json
{
  "url": "https://example.com/episodes/ep-142-master.wav",
  "preset": "spotify",
  "truePeakDbtp": -1
}
```

For a video podcast going to YouTube, keep the picture and fix only the audio:

```json
{
  "url": "https://example.com/video/ep-142.mp4",
  "preset": "youtube",
  "outputFormat": "mp4"
}
```

#### Apple Podcasts loudness with a stricter true-peak limiter

Some distributors ask for -2 dBTP or lower to leave headroom for lossy encoders.

```json
{
  "url": "https://example.com/episodes/ep-142.wav",
  "preset": "apple",
  "truePeakDbtp": -2,
  "outputFormat": "m4a"
}
```

#### EBU R128 and ATSC A/85 for broadcast delivery

```json
{
  "url": "https://example.com/broadcast/segment-07.wav",
  "preset": "ebu-r128",
  "outputFormat": "wav",
  "sampleRate": 48000
}
```

Use `"preset": "atsc-a85"` for -24 LUFS (US broadcast). For anything else, use `"preset": "custom"` with `"targetLufs": -18`.

#### Batch normalize audio for a whole back catalog

Top-level settings are the defaults, and each item in `files` can override them. Plain URL strings work too.

```json
{
  "preset": "podcast",
  "outputFormat": "mp3",
  "channels": "mono",
  "files": [
    "https://example.com/episodes/ep-001.mp3",
    "https://example.com/episodes/ep-002.mp3",
    { "url": "https://example.com/promos/trailer.wav", "preset": "spotify", "outputFormat": "wav" }
  ]
}
```

Files are processed one after another in the same run. Each one gets its own dataset row.

### Pricing (pay per event)

| Event | Price |
|---|---|
| File normalized (per successful file) | $0.01 |
| Audio minute (per started minute of **input** audio) | $0.008 |
| Actor start (per run, one event per GB of memory, minimum one; default 4 GB = 4 events) | $0.00005 per event |

There are no platform usage fees on top (compute is included). Failed files are free.

**Worked example: a weekly one-hour show**

- One 60-minute episode: $0.01 + 60 × $0.008 = **$0.49**.
- One year of episodes (52) in one batch run: 52 × $0.49 = **$25.48**, plus $0.0002 actor start.
- A 59 min 13 s recording counts as 60 started minutes, so it is also $0.49.
- A 30-second promo: $0.01 + 1 × $0.008 = **$0.018**.

Billing counts **started** minutes of the input file, rounded up, with a minimum of 1. Up to 0.5 s of container padding is ignored, so a 60 min 00.4 s file counts as 60 minutes, while 60 min 01 s counts as 61 ($0.498). Set a spending limit on the run to cap the total; the Actor stops cleanly when the limit is reached.

### Input reference

| Field | Type | Default | Notes |
|---|---|---|---|
| `url` | string | (none) | Direct link to one audio or video file. Any FFmpeg-readable format, up to 2 GB. The first audio stream is normalized. |
| `files` | array | (none) | Batch. Items are `{ "url": ..., <any setting below> }` or plain URL strings. |
| `preset` | string | `podcast` | `podcast`, `apple`, `spotify`, `youtube`, `ebu-r128`, `atsc-a85`, `custom` |
| `targetLufs` | number | (none) | Required for `custom`. -70 to -5. |
| `truePeakDbtp` | number | `-1` | Ceiling, -9 to 0 dBTP (4× oversampled). |
| `loudnessRange` | number | `11` | Target LRA, 1–50 LU. |
| `outputFormat` | string | `mp3` | `mp3`, `m4a`, `wav`, `flac`, `mp4` (`mp4` needs a video input) |
| `sampleRate` | integer | same as input | 8000–192000 Hz. MP3 is capped at 48000. |
| `channels` | string | `keep` | `keep`, `mono`, `stereo` |
| `maxDurationSeconds` | integer | `14400` | Longer inputs are rejected (free). |

You must provide either `url` or `files`.

### Output reference

Each file produces one dataset row. Here is a real 59-minute spoken-word test run (URLs shortened):

```json
{
  "sourceUrl": "https://example.com/episodes/ep-142.mp3",
  "preset": "podcast",
  "outputFormat": "mp3",
  "status": "ok",
  "outputKey": "a1b2c3d4e5f6.mp3",
  "outputUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/a1b2c3d4e5f6.mp3?signature=<sig>",
  "mime": "audio/mpeg",
  "target": { "integratedLufs": -16, "truePeakDbtp": -1, "loudnessRangeLu": 11 },
  "beforeLufs": -24.41,
  "afterLufs": -16.32,
  "afterTruePeakDbtp": -1.32,
  "gainAppliedDb": 8.09,
  "normalizationType": "dynamic",
  "renderPasses": 2,
  "truePeakCompliant": true,
  "input": {
    "format": "mp3",
    "duration": 3553.2,
    "sizeBytes": 28426087,
    "video": null,
    "audio": { "codec": "mp3", "sampleRate": 22050, "channels": 1 }
  },
  "billedMinutes": 60
}
```

Rows also include `outputBytes`, `outputDurationSeconds`, and full `before` and `after` objects (`integratedLufs`, `loudnessRangeLu`, `truePeakDbtp`, `thresholdLufs`). A failed file returns only `sourceUrl`, `preset`, `outputFormat`, `status: "error"` and `error`.

`normalizationType` is `linear` when plain gain was enough. It is `dynamic` when the limiter or range control had to work, which is usually the case for peaky audio or a large gain change.

### Use it from AI agents and automations

**MCP (Claude, Cursor, other MCP clients).** Add `https://mcp.apify.com?tools=ent-e/podcast-loudness-normalizer` as a remote MCP server and sign in with OAuth. Your agent can then call the Actor with a URL and a preset. Long files take a while (roughly 8–9 minutes of processing per hour of audio), so the agent should check the run and read the dataset when it finishes.

**n8n.** Use the Apify node (`@apify/n8n-nodes-apify`):

- **Run an Actor and get dataset** for short clips.
- For full episodes, use **Run an Actor**, then **Get Dataset Items**.

**Make.** Use the Apify app:

- The **Run an Actor** module with *Run synchronously* waits up to 120 s, which suits promos and short clips.
- For episodes, run asynchronously. Use the **Watch Actor Runs** trigger, then **Get Dataset Items**.

**Zapier.** Use the Apify app:

- **Run Actor and Fetch Results** for runs under 5 minutes.
- Otherwise, use **Run Actor**, then the **Finished Actor Run** trigger and **Fetch Dataset Items**.

**API.** Use async for episodes. Start a run, then fetch the dataset when it succeeds:

```bash
curl -X POST "https://api.apify.com/v2/acts/ent-e~podcast-loudness-normalizer/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/episodes/ep-142.wav","preset":"podcast"}'
## then: GET https://api.apify.com/v2/datasets/<defaultDatasetId>/items?token=$APIFY_TOKEN
```

For short clips you can use `/run-sync-get-dataset-items`, which waits up to 5 minutes.

### FAQ

**Do I need a DAW, plugins or ffmpeg?**
No. Send a URL and get a normalized file and a loudness report back. Nothing to install.

**Which preset should I use for Spotify, Apple Podcasts and YouTube?**

- `apple` or `podcast` (-16 LUFS) for podcast feeds and Apple Podcasts.
- `spotify` or `youtube` (-14 LUFS) for music-style streaming targets.
- `ebu-r128` (-23) or `atsc-a85` (-24) for broadcast.

**Why is my file at -14.6 LUFS and not -14?**
The true-peak ceiling always wins. If reaching -14 LUFS would push peaks above `truePeakDbtp`, the Actor stops short so the file doesn't clip on playback. The row shows the measured `afterLufs` and `truePeakCompliant`. If you want the full -14, allow more headroom (for example `"truePeakDbtp": -0.5`) or reduce the peaks in your mix.

**Can I normalize a video podcast?**
Yes. Use `"outputFormat": "mp4"`. The video stream is copied untouched, and only the audio is normalized and re-encoded to AAC.

**Is the loudness measured on the final file?**
Yes. The `after` values come from measuring the encoded output, not from the filter's estimate. If the first render misses the ceiling or the target, it re-renders, and `renderPasses` tells you how many passes it took.

**What isn't supported?**

- Only the first audio stream is processed.
- Files must be direct download URLs (not streaming pages), up to 2 GB and 4 hours by default.
- It doesn't remove noise or EQ. It only sets loudness and peaks.

**Am I charged if a file fails?**
No. Error rows are free. You pay only for files that were normalized and delivered.

### ENT-E media toolkit

Every tool takes a public file URL, needs nothing installed, charges per file, doesn't charge for failures, and works from MCP, n8n, Make, Zapier and the API.

- **[Media Converter for AI Agents (FFmpeg)](https://apify.com/ent-e/media-converter-ffmpeg)**: audio/video to MP3, Whisper-ready 16 kHz WAV, MP4, WebM, GIF or thumbnails, with trim and batch.
- **[Image Converter & Compressor](https://apify.com/ent-e/image-converter-compressor)**: convert to WebP or AVIF, compress to a target file size, resize, strip EXIF/GPS.
- **[Podcast Loudness Normalizer](https://apify.com/ent-e/podcast-loudness-normalizer)**: normalize to -16, -14, -23 or -24 LUFS with a true-peak ceiling, plus a before/after report.
- **[Video to Captions](https://apify.com/ent-e/video-to-captions-whisper)**: SRT, VTT, TXT and word-level JSON from any video or audio URL, with Whisper.
- **[Video to Shorts](https://apify.com/ent-e/video-to-shorts-captions)**: vertical 9:16 clips with burned-in animated captions for TikTok, Reels and Shorts.

# Actor input Schema

## `url` (type: `string`):

Direct, publicly downloadable URL of ONE audio or video file (http/https; redirects followed). Any FFmpeg-readable format: mp3, wav, m4a, aac, flac, ogg, opus, mp4, mov, mkv, webm... Max 2 GB. Must be the media file itself, not a YouTube/Spotify/web page. The first audio stream is normalized. Example: https://download.samplelib.com/mp3/sample-15s.mp3

## `files` (type: `array`):

Optional batch, processed sequentially. Array of objects {"url": string (required), plus any of preset, targetLufs, truePeakDbtp, loudnessRange, outputFormat, sampleRate, channels to override the top-level values for that file}. Plain URL strings are also accepted. Example: \[{"url":"https://example.com/ep1.wav"},{"url":"https://example.com/ep1-clip.mp4","preset":"youtube","outputFormat":"mp4"}]. Failed items return status=error and are not charged.

## `preset` (type: `string`):

Integrated loudness target. podcast/apple = -16 LUFS (Apple Podcasts and most podcast hosts recommend -16 stereo); spotify/youtube = -14 LUFS (streaming platforms); ebu-r128 = -23 LUFS (European broadcast); atsc-a85 = -24 LUFS (US TV); custom = the value in targetLufs. Default: podcast. If the true-peak ceiling cannot be honoured at the target (very peaky audio), the ceiling wins and the result may land slightly below target (see normalizationType=dynamic in the output).

## `targetLufs` (type: `number`):

Integrated loudness target in LUFS, used only when preset=custom (e.g. -18 for audiobooks/ACX-style voice, -19 for mono podcasts). Range -70 to -5. Ignored for other presets.

## `truePeakDbtp` (type: `number`):

Maximum true peak in dBTP (inter-sample peaks, measured with 4x oversampling). Default -1 dBTP (Apple/Spotify/YouTube recommendation). Use -2 for content that will be re-encoded at low bitrates, -1.5 for extra safety. The encoded output is re-measured and re-rendered with a tighter ceiling if lossy encoding pushed it over. Range -9 to 0.

## `loudnessRange` (type: `number`):

Target loudness range (LRA) in LU passed to loudnorm. Default 11 (FFmpeg default; leaves natural dynamics). Lower values (e.g. 7) compress dynamics more for noisy listening environments, but only take effect if the input LRA is larger. Range 1-50.

## `outputFormat` (type: `string`):

Output container/codec. mp3 = MP3 CBR 192 kbps (universal podcast delivery); m4a = AAC 192 kbps; wav = 24-bit PCM (for further editing); flac = lossless; mp4 = keeps the input video stream untouched (no re-encode) and replaces the audio with normalized AAC 192 kbps - requires a video input. Audio formats drop any video. Default: mp3.

## `sampleRate` (type: `integer`):

Optional output sample rate in Hz, e.g. 44100 or 48000. Default: same as the input (mp3 is capped at 48000). Leave empty unless your platform requires a specific rate.

## `channels` (type: `string`):

Output channel layout. keep = same as input (default); mono = downmix to 1 channel (smaller files for voice-only shows); stereo = 2 channels.

## `maxDurationSeconds` (type: `integer`):

Safety cap in seconds on input duration (default 14400 = 4 hours). Longer inputs are rejected with status=error and are not charged. Billing is per started minute of input audio.

## Actor input object example

```json
{
  "url": "https://download.samplelib.com/mp3/sample-15s.mp3",
  "preset": "podcast",
  "truePeakDbtp": -1,
  "loudnessRange": 11,
  "outputFormat": "mp3",
  "channels": "keep",
  "maxDurationSeconds": 14400
}
```

# Actor output Schema

## `results` (type: `string`):

One dataset row per input URL: status, download URL (outputUrl) of the normalized file, target, before/after integrated loudness (LUFS), loudness range (LU), true peak (dBTP), gain applied, or an error message.

## `files` (type: `string`):

Key-value store holding the normalized output files.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "url": "https://download.samplelib.com/mp3/sample-15s.mp3"
};

// Run the Actor and wait for it to finish
const run = await client.actor("ent-e/podcast-loudness-normalizer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "url": "https://download.samplelib.com/mp3/sample-15s.mp3" }

# Run the Actor and wait for it to finish
run = client.actor("ent-e/podcast-loudness-normalizer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "url": "https://download.samplelib.com/mp3/sample-15s.mp3"
}' |
apify call ent-e/podcast-loudness-normalizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ent-e/podcast-loudness-normalizer"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/EgU2Xz9ozdeEtYdKE/builds/tbQYPBUC1uLOyBL0W/openapi.json
