Music Assistant Audio Analysis Provider Architecture: Loudness Measurement Explained

Music Assistant implements loudness analysis through the LoudnessAnalysisProvider class, which uses FFmpeg's ebur128 filter to measure integrated loudness, loudness range, and true-peak values for ReplayGain-compatible volume normalization.

Music Assistant is an open-source media server that requires precise loudness data to enable volume normalization across heterogeneous audio hardware. The loudness measurement system is built as a modular provider within the music-assistant/server repository, implementing the AudioAnalysisProvider interface to analyze PCM streams in real-time. This architecture separates the analysis logic from playback, allowing concurrent loudness computation without blocking the main event loop.

Core Architecture Components

The loudness analysis provider consists of several specialized components that work together to process audio streams and extract EBU R 128-compliant metrics.

LoudnessAnalysisProvider

The LoudnessAnalysisProvider class in music_assistant/providers/loudness_analysis/provider.py serves as the main entry point. It implements the AudioAnalysisProvider interface and manages the lifecycle of loudness analysis sessions. Each session spawns an independent FFmpeg subprocess configured with the ebur128=framelog=verbose filter to compute loudness statistics according to the EBU R 128 standard.

FFmpeg Async Wrapper

The FFMpeg helper class located in music_assistant/helpers/ffmpeg.py provides an asyncio-compatible interface to the ffmpeg binary. It handles:

  • Streaming raw PCM data via stdin
  • Collecting stderr logs in a log_history deque
  • Managing process lifecycle asynchronously

This wrapper enables non-blocking audio processing, allowing the server to handle multiple concurrent analysis sessions.

AudioAnalysisData Model

The AudioAnalysisData class defined in music_assistant/models/audio_analysis.py stores the computed metrics:

  • loudness_integrated: Overall program loudness in LUFS
  • loudness_range: Loudness Range (LRA) in LU
  • true_peak: True peak level in dBTP

ReplayGain Tag Integration

When the write_replaygain_tags configuration option is enabled, the provider writes track-gain metadata back to source files using functions in music_assistant/helpers/tags.py. This enables persistent volume normalization across playback sessions.

Processing Pipeline

The loudness analysis follows a strict five-stage pipeline to ensure accurate measurements and resource cleanup.

1. Session Initialization

When a stream requests analysis, the _start_analysis method validates that volume_normalization_mode is enabled for the player. If normalization is disabled, the analysis is skipped to conserve resources.

For valid sessions, the provider creates an FFmpeg instance with the ebur128 filter and stores it in the _data dictionary under a unique session_id:

ffmpeg = FFMpeg(
    audio_input="-",
    input_format=audio_format,
    output_format=audio_format,
    audio_output="NULL",
    filter_params=["ebur128=framelog=verbose"],
    collect_log_history=True,
    loglevel="info",
)
await ffmpeg.start()
self._data[session_id] = LoudnessSessionData(ffmpeg=ffmpeg)

2. PCM Chunk Feeding

As audio data flows through the system, process_pcm_chunk forwards raw PCM buffers to the FFmpeg process:

await data.ffmpeg.write(pcm_chunk)

The provider enforces a hard cap of MAX_DURATION_SECONDS (600 seconds) to prevent unbounded resource consumption on long-running streams.

3. EOF Handling

When the stream ends or hits the duration limit, _send_eof writes an EOF marker to ffmpeg's stdin:

await data.ffmpeg.write_eof()

This operation is idempotent, ensuring clean process termination even if called multiple times.

4. Log Parsing and Finalization

The _finalize method orchestrates result extraction:

  1. Waits for FFmpeg to exit: await data.ffmpeg.wait()
  2. Retrieves the complete log history from data.ffmpeg.log_history
  3. Parses metrics using _parse_ebur128_metrics
  4. Validates results against safety thresholds:
    • Minimum duration: MIN_DURATION_SECONDS (10 seconds)
    • Minimum loudness: LOUDNESS_MEASUREMENT_MIN_LUFS (filters near-silence)

Valid measurements are rounded and encapsulated in an AudioAnalysisData instance:

metrics = _parse_ebur128_metrics(data.ffmpeg.log_history)
loudness, loudness_range, true_peak = metrics
analysis = AudioAnalysisData(
    loudness_integrated=round(loudness, 2),
    loudness_range=round(loudness_range, 2) if loudness_range else None,
    true_peak=round(true_peak, 2) if true_peak else None,
)

5. Post-Analysis Tag Writing

If write_replaygain_tags is enabled in the provider manifest (music_assistant/providers/loudness_analysis/manifest.json), the post_analysis method calculates the track gain based on a -18 LUFS reference:

track_gain_db = -18.0 - analysis.loudness_integrated
await write_replaygain_track_gain(streamdetails.path, track_gain_db)

Key Design Characteristics

The Music Assistant audio analysis provider architecture exhibits several critical design patterns that ensure reliability and standard compliance.

Asynchronous Processing

The entire pipeline operates under async/await, preventing analysis tasks from blocking the server's main loop. This allows dozens of concurrent loudness measurements without degrading playback performance.

Log-Based Metric Extraction

Rather than implementing a custom loudness algorithm, the provider relies on FFmpeg's ebur128 filter verbose output. This guarantees strict compliance with the EBU R 128 standard while minimizing code complexity and maintenance overhead.

Session Isolation

Each analysis receives a dedicated FFmpeg subprocess encapsulated in a LoudnessSessionData object. This isolation prevents stream cross-contamination and simplifies resource cleanup.

Fail-Safe Defaults

The provider silently skips analysis for short clips (<10s), disabled normalization, or implausibly low loudness values. These safeguards protect users from extreme gain corrections that could damage audio equipment or hearing.

Integration Example

Below is a typical implementation pattern for client code triggering loudness analysis:


# Initialize analysis session

session_id = await mass.audio_analysis.start(
    streamdetails,
    audio_format=streamdetails.format,
    provider="loudness_analysis",
)

# Stream PCM chunks as they arrive

await mass.audio_analysis.process_pcm_chunk(session_id, pcm_chunk)

# Finalize and retrieve results

analysis = await mass.audio_analysis.finalize(session_id)

if analysis and analysis.loudness_integrated is not None:
    print(f"Loudness: {analysis.loudness_integrated} LUFS")
    print(f"LRA: {analysis.loudness_range} LU")
    print(f"True peak: {analysis.true_peak} dBTP")

Summary

  • Music Assistant implements loudness analysis as a modular provider in music_assistant/providers/loudness_analysis/provider.py, following the AudioAnalysisProvider interface.
  • The LoudnessAnalysisProvider uses FFmpeg's ebur128 filter to compute integrated loudness, loudness range, and true-peak values according to EBU R 128 standards.
  • Session isolation ensures each analysis runs in a separate subprocess with independent resource management and a 600-second maximum duration cap.
  • Safety validations discard measurements from short streams or near-silence content to prevent invalid gain calculations.
  • Optional ReplayGain tag writing persists loudness metadata to source files when write_replaygain_tags is enabled in the provider configuration.

Frequently Asked Questions

How does Music Assistant calculate loudness values?

Music Assistant delegates loudness calculation to FFmpeg's ebur128 filter running in verbose mode. The LoudnessAnalysisProvider streams PCM data to FFmpeg via stdin, captures the stderr logs, and parses them using _parse_ebur128_metrics to extract integrated loudness (LUFS), loudness range (LU), and true-peak (dBTP) values.

What is the maximum duration for loudness analysis?

The provider enforces a hard limit of MAX_DURATION_SECONDS (600 seconds) per analysis session. This prevents unbounded resource usage on long-running streams or radio sources. Additionally, the provider requires a minimum of MIN_DURATION_SECONDS (10 seconds) of audio data to produce valid measurements.

Can Music Assistant write ReplayGain tags to audio files?

Yes. When the write_replaygain_tags configuration option is enabled in the provider manifest, Music Assistant calculates track gain using the formula -18.0 - loudness_integrated and writes the value to the source file's metadata. This functionality is implemented in music_assistant/helpers/tags.py and supports persistent volume normalization across different playback sessions.

Why does the provider skip analysis for some streams?

The provider automatically skips loudness measurement in three scenarios: when the player's volume_normalization_mode is disabled, when the audio duration is less than 10 seconds, or when the measured integrated loudness falls below LOUDNESS_MEASUREMENT_MIN_LUFS. These safeguards prevent resource waste and protect against extreme gain adjustments for near-silent or invalid content.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →