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

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 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, then applies linear normalization with precision
  • Video passthrough — video stream is copied untouched (-c:v copy), only audio is re-encoded
  • In-place processing with backup — overwrite originals safely with .bak / folder / suffix backup strategies
  • Real-time progress bar — shows Analyzing (Pass 1) and Normalizing (Pass 2) phases with percentage
  • Batch folder processing — recursive directory walk, configurable file extensions
  • Dry-run mode — preview operations without ffmpeg installed
  • Programmatic API — import normalize() in any TypeScript/Bun/Node project (Hono, Elysia, etc.)

Installation

# Install globally
npm install -g peaknorm

# Or use directly
npx peaknorm ./file.mp4

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

CLI Usage

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

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

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

# Preview what would be done (no ffmpeg needed)
peaknorm ./input --dry-run --verbose

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: copy, folder, suffix (default: copy)
  --no-backup              Disable backup entirely
  -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
  --verbose                Verbose output
  -h, --help               Show help
  --version                Show version

Programmatic API

import { normalize, normalizeFile, normalizeFolder } from "peaknorm";
import {
  PeaknormError,
  FfmpegNotFoundError,
  NormalizeError,
} from "peaknorm";

normalize(input, options?)

Auto-detects file or folder.

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,
  onFileError: (file, err) => {
    console.error(`Skipping ${file}: ${err.message}`);
  },
});

AbortSignal support

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 two-pass pipeline:

1. Probe     ffmpeg -i input → parse Duration + Stream info (~200ms)
             ↓
2. Analyze   ffmpeg -i input -af loudnorm=print_format=json -f null -
             → measures input_i, input_lra, input_tp, input_thresh, offset
             ↓
3. Normalize ffmpeg -i input -c:v copy -af loudnorm=linear=true:measured_*...
             -c:a libopus -b:a 96k output

Step details

| Phase | What happens | Progress shown | |---|---|---| | Starting | File path resolved, backup created | Starting filename... | | Analyzing | Pass 1 — ffmpeg measures integrated loudness, LRA, true peak | ████░░░░ 35% Analyzing | | Normalizing | Pass 2 — ffmpeg applies linear normalization with measured values, stream-copies video, re-encodes audio | ██████░░ 68% Normalizing |

Backup strategies

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

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

Types

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 (omitted = in-place) | | backup | BackupStrategy \| boolean | "copy" | Backup strategy (false to disable) | | recursive | boolean | true | Recurse subdirectories | | extensions | string[] | — | File extensions to process (default: 15 common media types) | | ffmpegPath | string | — | Custom ffmpeg binary path | | dryRun | boolean | false | Preview without processing | | signal | AbortSignal | — | Cancellation signal | | onFileStart | (input, output) => void | — | Called when a file starts | | onFileProgress | (file, percent, phase) => void | — | Progress callback (percent 0–100, phase is "analyzing" or "normalizing") | | onFileComplete | (result) => void | — | Called when a file finishes | | onFileError | (input, error) => void | — | Called when a file errors |

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 (if created)
  inputSizeBytes: number;
  outputSizeBytes: number;
  durationMs: number;      // Processing time
}

BatchResult

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

Error handling

Peaknorm defines a hierarchy of error classes:

PeaknormError
├── FfmpegNotFoundError   — ffmpeg not on PATH or at custom path
├── FfmpegError           — ffmpeg subprocess failed (exit code + stderr tail)
├── NormalizeError        — normalization failed for a specific file
├── BackupError           — backup creation/restore failed
└── NoMediaFilesError     — no matching files found in the given folder

All errors extend PeaknormError, which extends Error. Catch broadly or specifically:

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);
  }
}

Per-file errors don't fail the batch — the file is marked as error in the results and processing continues.

Supported formats

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

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

Development

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

# Dev workflow
bun run dev -- ./file.mp4 --dry-run  # Run CLI from source
bun run check                         # Lint + format
bun run typecheck                     # TypeScript check
bun run test                          # Run tests
bun run build                         # Build dist/

# Integration tests (requires ffmpeg)
bun run test:integration