npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

peaknorm

v0.7.0

Published

Normalize audio in media files using EBU R128 (ffmpeg loudnorm)

Readme

peaknorm

npm version CI License TypeScript Bun

Normalize audio loudness in media files using the EBU R128 standard via ffmpeg. Works with video files (video passthrough, audio re-encoded) and audio-only files.

npx peaknorm ./video.mp4

Features

  • EBU R128 two-pass loudnorm — measures integrated loudness, LRA, and true peak, then applies linear normalization with precision
  • Video passthrough — video stream is copied untouched (-c:v copy), only audio is re-encoded
  • In-place processing — overwrite originals safely; optional .bak / folder / suffix backup strategies
  • Real-time progress — per-file progress bar with phase labels (Analyzing / Normalizing) and percentage from ffmpeg
  • Batch folder processing — recursive directory walk with configurable file extensions and sort order
  • Dry-run mode — preview operations without requiring ffmpeg
  • Cancellation — abort long-running operations via AbortSignal
  • Programmatic API — import normalize(), normalizeFile(), or normalizeFolder() directly

Installation

npm install -g peaknorm

Or use directly without installing:

npx peaknorm ./file.mp4

[!IMPORTANT] ffmpeg (≥4.2) must be installed on your system.
macOS: brew install ffmpeg — Ubuntu: sudo apt install ffmpeg — Windows: choco install ffmpeg

CLI Usage

# Normalize a single video file (creates .bak, overwrites original)
peaknorm movie.mp4

# Normalize all media files in a folder (recursive by default)
peaknorm ./videos

# Output to a different directory instead of in-place
peaknorm ./input -o ./output

# Custom loudness target, change audio codec
peaknorm ./files -l -16 --audio-codec aac --audio-bitrate 128k

# Preview without processing (no ffmpeg needed)
peaknorm ./input --dry-run --verbose

All options

  -o, --output <dir>       Output directory (default: same as input, in-place)
  -l, --loudness <num>     Target loudness in LUFS (default: -14)
  --lra <num>              Loudness range in LU (default: 7)
  -tp, --true-peak <num>   True peak limit in dBTP (default: -2)
  --audio-codec <name>     Audio codec (default: libopus)
  --audio-bitrate <str>    Audio bitrate (default: 96k)
  -b, --backup <strategy>  Backup strategy: copy, folder, suffix (default: disabled)
  -r, --recursive          Recurse subdirectories (default: true)
  --no-recursive           Don't recurse subdirectories
  -e, --ext <ext>          File extensions to process (repeatable)
  --ffmpeg-path <path>     Custom ffmpeg binary path
  --dry-run                Preview without processing
  --sort-by <method>       Sort files by: name|mtime (default: name)
  --sort-order <dir>       Sort direction: asc|desc (default: asc)
  --verbose                Verbose output
  -h, --help               Show help
  -v, --version            Show version

Programmatic API

import { normalize, normalizeFile, normalizeFolder } from "peaknorm";
import type {
  NormalizeOptions,
  NormalizeResult,
  BatchResult,
} from "peaknorm";

normalize(input, options?)

Auto-detects whether the input is a file or a folder and processes accordingly.

const batch = await normalize("./input.mp4", {
  loudness: -16,
  onFileProgress: (file, percent, phase) => {
    console.log(`${phase}: ${file} ${percent}%`);
  },
  onFileComplete: (result) => {
    console.log(result.status);
  },
});

console.log(`Done: ${batch.completed}/${batch.total}`);

normalizeFile(path, options?)

Normalize a single file. Returns a NormalizeResult.

const result = await normalizeFile("song.flac", {
  backup: "folder",
  dryRun: true,
});

if (result.status === "completed") {
  console.log(`${result.input} → ${result.output}`);
}

normalizeFolder(path, options?)

Normalize all media files in a directory. Returns a BatchResult.

const batch = await normalizeFolder("./library", {
  extensions: [".flac", ".wav"],
  recursive: true,
  sortBy: "mtime",
  sortOrder: "desc",
  onFileError: (file, err) => {
    console.error(`Skipping ${file}: ${err.message}`);
  },
});

Cancellation

const ac = new AbortController();
setTimeout(() => ac.abort(), 60_000);

const batch = await normalize("./big-folder", { signal: ac.signal });

How it works

Each file goes through a three-stage pipeline:

┌──────────┐    ┌──────────┐    ┌─────────────┐
│  Probe   │ →  │ Measure  │ →  │  Normalize  │
│ (~200ms) │    │ (Pass 1) │    │  (Pass 2)   │
└──────────┘    └──────────┘    └─────────────┘

| Stage | Action | Progress | |---|---|---| | Probe | ffprobe reads stream info and duration | — (instant) | | Measure | ffmpeg -af loudnorm=print_format=json measures integrated loudness, LRA, true peak | Analyzing [████░░░░] 35% | | Normalize | ffmpeg -c:v copy -af loudnorm=linear=true:measured_I=... applies correction, stream-copies video, re-encodes audio | Normalizing [██████░░] 68% |

[!TIP] If the measured values contain -inf or nan (very quiet or silent content), peaknorm automatically falls back to dynamic loudnorm without linear=true or measured_* parameters.

Backup strategies

On failure, the original is restored from the backup and partial output is deleted.

| Strategy | Behavior | |---|---| | false (default) | No backup created | | copy | file.bak alongside original | | folder | backups/file in a subdirectory | | suffix | Renames original to file.original |

Options reference

NormalizeOptions

| Property | Type | Default | Description | |---|---|---|---| | loudness | number | -14 | Target integrated loudness in LUFS | | lra | number | 7 | Loudness range target in LU | | truePeak | number | -2 | True peak limit in dBTP | | audioCodec | string | "libopus" | Output audio codec | | audioBitrate | string | "96k" | Output audio bitrate | | output | string | — | Output directory (omit for in-place) | | backup | BackupStrategy \| boolean | false | Backup strategy (false to disable) | | recursive | boolean | true | Recurse subdirectories | | extensions | string[] | (see below) | File extensions to process | | ffmpegPath | string | — | Custom ffmpeg binary path | | dryRun | boolean | false | Preview without processing | | sortBy | "name" \| "mtime" | "name" | Sort files by name or modification time | | sortOrder | "asc" \| "desc" | "asc" | Sort direction | | signal | AbortSignal | — | Cancellation signal | | onFileStart | (input, output) => void | — | Called when a file starts | | onFileProgress | (file, percent, phase) => void | — | Progress callback (0–100, "analyzing" or "normalizing") | | onFileComplete | (result) => void | — | Called when a file finishes | | onFileError | (input, error) => void | — | Called when a file errors |

Default extensions: .mp4 .mkv .avi .mov .webm .m4v .ts .mp3 .wav .flac .m4a .ogg .wma .aac .opus

NormalizeResult

interface NormalizeResult {
  input: string;                // Input file path
  output: string;               // Output file path
  status: "completed" | "skipped" | "error";
  error?: string;               // Error message if status is "error"
  backupPath?: string;          // Path to backup file
  inputSizeBytes: number;
  outputSizeBytes: number;
  durationMs: number;           // Processing time in milliseconds
}

BatchResult

interface BatchResult {
  total: number;
  completed: number;
  skipped: number;
  errors: number;
  results: NormalizeResult[];
  durationMs: number;
}

Error handling

Peaknorm defines a typed error hierarchy so you can catch specific failures:

PeaknormError
├── FfmpegNotFoundError   — ffmpeg not on PATH or at custom path
├── FfmpegError           — ffmpeg subprocess failed (exit code + stderr)
├── NormalizeError        — normalization failed for a specific file
├── BackupError           — backup creation or restore failed
└── NoMediaFilesError     — no matching files in the target folder
import { PeaknormError, FfmpegNotFoundError } from "peaknorm";

try {
  await normalize("./input");
} catch (err) {
  if (err instanceof FfmpegNotFoundError) {
    console.error("Please install ffmpeg first");
  } else if (err instanceof PeaknormError) {
    console.error(err.message);
  }
}

[!NOTE] Per-file errors don't fail the batch — the file is marked as error in the results and processing continues with the next file.

Supported formats

Video containers: .mp4 .mkv .avi .mov .webm .m4v .ts

Audio containers: .mp3 .wav .flac .m4a .ogg .wma .aac .opus

Customize with the --ext flag or extensions option.

Development

# Clone and install
git clone https://github.com/zfadhli/peaknorm.git
cd peaknorm
bun install

# Run the CLI from source
bun run dev -- ./file.mp4 --dry-run

# Development commands
bun run check          # Lint + format check (Biome)
bun run typecheck      # TypeScript type check (tsc --noEmit)
bun run test           # Run unit tests
bun run test:integration  # Integration tests (requires ffmpeg)
bun run build          # Build dist/ via tsdown