Migrate loudness analyzer to audio analysis provider#3727
Conversation
🔒 Dependency Security Report✅ No dependency changes detected in this PR. |
There was a problem hiding this comment.
Pull request overview
This PR migrates loudness measurement into the existing AudioAnalysisProvider framework, replacing bespoke loudness plumbing with a unified audio-analysis storage and runtime pipeline.
Changes:
- Introduces a new builtin
loudness_analysisaudio analysis provider (FFmpeg ebur128) and routes tag/ReplayGain-derived loudness intostreams.audio_analysis. - Consolidates loudness persistence into the
audio_analysistable (schema 38→39 migration + removal of the legacyloudness_measurementstable). - Adds
loudness_albumtoAudioAnalysisDataand adds optional ReplayGain tag writing support viahelpers.tags.write_replaygain_track_gain.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| music_assistant/providers/opensubsonic/sonic_provider.py | Stores tag/ReplayGain loudness via streams.audio_analysis.set_track_loudness. |
| music_assistant/providers/filesystem_local/init.py | Stores parsed loudness tags via streams.audio_analysis.set_track_loudness for track/audiobook/podcast. |
| music_assistant/providers/loudness_analysis/provider.py | New provider implementing live PCM loudness analysis + nightly background file analysis. |
| music_assistant/providers/loudness_analysis/init.py | Provider setup + config entries for background analysis and ReplayGain tag writing. |
| music_assistant/providers/loudness_analysis/manifest.json | Declares the new builtin audio analysis provider. |
| music_assistant/models/audio_analysis.py | Adds loudness_album to the shared analysis data model. |
| music_assistant/helpers/tags.py | Adds mutagen-based helper to write REPLAYGAIN_TRACK_GAIN back to files. |
| music_assistant/controllers/streams/audio_buffer.py | Switches from the legacy loudness attachment to unified audio analysis startup. |
| music_assistant/controllers/streams/audio_analysis.py | Adds set_track_loudness helper to persist loudness into audio_analysis. |
| music_assistant/controllers/streams/audio.py | Moves loudness hydration to just-in-time lookup from audio_analysis in get_queue_item_stream. |
| music_assistant/controllers/music.py | Bumps DB schema to 39 and migrates loudness rows into audio_analysis, then drops the legacy table. |
| music_assistant/controllers/media/base.py | Updates library removal cleanup to delete audio analysis rows (instead of legacy loudness rows). |
| providers = self.providers | ||
| if not providers: | ||
| return | ||
|
|
||
| for provider in providers: | ||
| candidates = await self._find_tracks_missing_analysis( | ||
| provider.domain, BACKGROUND_SCAN_BATCH_SIZE | ||
| ) |
There was a problem hiding this comment.
[PROBLEM] The nightly background scan iterates every available AudioAnalysisProvider, but most providers don’t override analyze_file (default returns None), so this will still fetch streamdetails for up to 250 tracks per provider and do no work, causing unnecessary DB/IO load each night.
Suggested fix: skip providers that don’t support file-based analysis (e.g., only include providers where provider.__class__.analyze_file is not AudioAnalysisProvider.analyze_file, or add an explicit capability flag/property and filter on it) before calling _find_tracks_missing_analysis/get_stream_details.
Problem
Volume-loudness detection was the last analyzer still hand-rolled in the streams and music controllers, storing measurements in its own
loudness_measurementstable. Every other analyzer already uses theAudioAnalysisProviderpattern. Keeping loudness separate meant duplicated plumbing, a bespoke DB table, and no reuse of the PCM chunk fan-out the new controller already does.Changes
loudness_analysisaudio analysis provider (always enabled) - ebur128 on live PCM via the shared audio-analysis fan-outattach_loudness_analyzerfrom the streams controller andset_loudness/get_loudnessfrom the music controller;filesystem_localandopensubsonicnow usemass.streams.audio_analysis.set_track_loudness(...)loudness_albumfield toAudioAnalysisDataso album-level loudness from tags survives the unified format> -50 LUFS) fromloudness_measurementsintoaudio_analysisunderaa_provider_domain='loudness_analysis'; drops the old tablestreamdetailsmoved to just-in-time inget_queue_item_stream, so a measurement completed during a previous play is picked up without restartstreamdetails.volume_normalization_mode == DISABLED(respects per-player opt-out); the base-class version gating already skips tracks that already have a measurementwrite_replaygain_tagsconfig (default off): writesREPLAYGAIN_TRACK_GAINback to the file (ID3/MP4/Vorbis/APEv2) via a newwrite_replaygain_track_gainmutagen helper