ryanfrancesconi/spfk-loudness
A Swift package for measuring audio loudness according to the [EBU R128](https://tech.ebu.ch/docs/r/r128.pdf) standard. Built on [libebur128](https://github.com/jiixyj/libebur128) with a pure Swift analysis layer using Core Audio for decoding and sample-rate conversion.
Usage
Analyzing a single file
import SPFKLoudness
let result = try LoudnessAnalyzer.analyze(url: audioFileURL)
result.loudnessIntegrated // -24.13 (LUFS)
result.loudnessRange // 1.43 (LU)
result.maxTruePeakLevel // -0.07 (dBTP)
result.maxMomentaryLoudness // -19.51 (LUFS)
result.maxShortTermLoudness // -22.99 (LUFS)Handling short files
Files shorter than 2.5 seconds don't provide enough material for a stable integrated loudness measurement. Pass minimumDuration to loop the audio in-memory until the target length is reached:
let result = try LoudnessAnalyzer.analyze(url: shortFileURL, minimumDuration: 5)Convenience initializer
LoudnessDescription(parsing:) wraps the analyzer with a default 5-second minimum duration and validates the result:
let loudness = try await LoudnessDescription(parsing: audioFileURL)
loudness.isValid // true if at least one metric is non-nil
loudness.stringValue // "I -24.1 LUFS, TP -0.1 dB, LRA 1.4 LU, M -19.5 LU, S -23.0 LU"Averaging across files
let descriptions = try await files.asyncMap { try await LoudnessDescription(parsing: $0) }
let average = descriptions.average
average.loudnessIntegrated // arithmetic mean of integrated valuesEBU R128 Metrics
| Metric | Property | Unit | Description | |--------|----------|------|-------------| | Integrated Loudness | loudnessIntegrated | LUFS | Program loudness over the entire file, with gating | | Loudness Range | loudnessRange | LU | Dynamic range per EBU Tech 3342 | | True Peak | maxTruePeakLevel | dBTP | Maximum inter-sample peak level | | Max Momentary | maxMomentaryLoudness | LUFS | Highest 400 ms loudness window | | Max Short-Term | maxShortTermLoudness | LUFS | Highest 3 s loudness window |
Architecture
SPFKLoudness (Swift)
├── LoudnessAnalyzer.swift — Public API: analyze(url:minimumDuration:)
├── LoudnessDescription+Init.swift — Convenience async init with validation
└── Internal/
├── CallbackContext.swift — Mutable state for the AudioConverter callback
└── AudioConverterCallback.swift — @convention(c) callback: reads audio, feeds ebur128
SPFKLoudnessC (C)
└── r128x/
└── ebur128.c — libebur128 (EBU R128 / ITU BS.1770-4)Processing Pipeline
- File decoding —
ExtAudioFileopens the file and delivers 32-bit float interleaved PCM - Oversampling —
AudioConverterupsamples for true peak detection (4x for ≤48 kHz, 2x for ≤96 kHz, 1x above) - Loudness measurement — Decoded frames are fed to libebur128 in 100 ms chunks; momentary and short-term maxima are tracked per chunk
- True peak detection — Oversampled output is scanned with
vDSP_maxmgvfor vectorized max-magnitude detection - Looping (optional) — If the file is shorter than half the
minimumDuration, the callback seeks back to the start on EOF and continues feeding frames - Validation — Results outside the representable range (±99.99) are set to nil
Supported Formats
Any audio format readable by Core Audio's ExtAudioFile, including WAV, AIF, FLAC, M4A, MP4, MP3, AAC, CAF, and OGG.
Dependencies
| Package | Purpose | |---------|---------| | spfk-audio-base | LoudnessDescription type | | spfk-testing | Test audio resources (test target only) |
Requirements
- Platforms: macOS 13+, iOS 16+
- Swift: 6.2+
About
Spongefork (SPFK) is the personal software projects of Ryan Francesconi. Dedicated to creative sound manipulation, his first application, Spongefork, was released in 1999 for macOS 8. From 2016 to 2025 he was the lead macOS developer at Audio Design Desk.
Package Metadata
Repository: ryanfrancesconi/spfk-loudness
Default branch: main
README: README.md