Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
d4d2187
Remove global audio gain reduction
smallketchup82 Mar 20, 2024
c095da6
Preliminary code
smallketchup82 Mar 20, 2024
e7366fb
Not sure
smallketchup82 Mar 21, 2024
ac14b5a
Remove my old test fissure
smallketchup82 Apr 3, 2024
7a3ccf3
Add audio normalization implementation
smallketchup82 Apr 3, 2024
ba4f6e3
Remove global audio adjustment
smallketchup82 Apr 3, 2024
913da37
Initial working implementation of audio normalization
smallketchup82 Apr 3, 2024
5fdbb79
Add background process to process existing maps
smallketchup82 Apr 3, 2024
9f077ae
Code quality
smallketchup82 Apr 3, 2024
6a8d377
CQ
smallketchup82 Apr 3, 2024
19b5ee9
Be a little less verbose
smallketchup82 Apr 3, 2024
2b6919e
Add comment for target_level
smallketchup82 Apr 3, 2024
d2ed3f4
CQ
smallketchup82 Apr 3, 2024
34b95df
Fix memory leaks resulting from loudness normalization
smallketchup82 Apr 3, 2024
1925e70
Add loudness normalization whenever the track changes
smallketchup82 Apr 3, 2024
d3d0cb3
Add background process for calculating loudness values
smallketchup82 Apr 3, 2024
ac0c333
Bump realm version
smallketchup82 Apr 3, 2024
b487d23
Less verbosity
smallketchup82 Apr 3, 2024
5c5d1d7
CQ
smallketchup82 Apr 4, 2024
43e057d
CQ
smallketchup82 Apr 4, 2024
f82d60e
Do volume offset math on demand rather than using a stored value
smallketchup82 Apr 4, 2024
3d9a628
Change target level to -14 LUFS
smallketchup82 Apr 4, 2024
5220f5f
Fix bug with totalcount
smallketchup82 Apr 4, 2024
a3e6eb6
CQ
smallketchup82 Apr 4, 2024
e29e747
CQ
smallketchup82 Apr 4, 2024
c02ce73
Add version info to RealmAccess.cs
smallketchup82 Apr 4, 2024
3b0aac5
CQ
smallketchup82 Apr 4, 2024
47dde89
General codebase overhaul
smallketchup82 Apr 4, 2024
ccb75e8
CQ
smallketchup82 Apr 4, 2024
f1ffd9f
Slightly refactor audio normalization background process
smallketchup82 Apr 4, 2024
f2d9105
Bug fixes
smallketchup82 Apr 4, 2024
50fa394
More bug fixes
smallketchup82 Apr 4, 2024
f2cb249
Fix regressions
smallketchup82 Apr 4, 2024
fc736f2
CQ
smallketchup82 Apr 5, 2024
1f276f4
CQ
smallketchup82 Apr 5, 2024
9ac373c
Fix error catching in `BassAudioNormalization`
smallketchup82 Apr 5, 2024
73ca394
Use Ian's example instead of reinventing the audiofile processor in `…
smallketchup82 Apr 5, 2024
9639e0b
Add easing to make the application of audio normalization less noticable
smallketchup82 Apr 5, 2024
1de1b69
Remove changes to `WorkingBeatmap`
smallketchup82 Apr 7, 2024
a06089d
Fix memory leaks
smallketchup82 Apr 9, 2024
d65fb1a
Clean up BackgroundDataStoreProcessor
smallketchup82 Apr 9, 2024
cb2828a
Make PopulateSet void
smallketchup82 Apr 9, 2024
1633f2e
Fix BackgroundDataStoreProcessor
smallketchup82 Apr 9, 2024
7080626
Make logger also log difficulty name
smallketchup82 Apr 9, 2024
c0c7ae2
Put sleep function call below null check
smallketchup82 Apr 9, 2024
a65d6d1
CQ
smallketchup82 Apr 9, 2024
f65ec52
Patch up docs in AudioNormalization
smallketchup82 Apr 12, 2024
4e1ebc5
Merge branch 'master' into audio-normalization
smallketchup82 Apr 16, 2024
4ef45ff
Make logging a little cooler
smallketchup82 Apr 16, 2024
36db481
Add proper docs
smallketchup82 Apr 19, 2024
82b4102
Remove optionality for BeatmapSetInfo on PopulateSet
smallketchup82 Apr 19, 2024
ebefb70
Improve error handling, nullability, and optionality across the board
smallketchup82 Apr 19, 2024
bdb027a
Fix up log message
smallketchup82 Apr 19, 2024
b848f81
Use Invalidate() instead of GetWorkingBeatmap()
smallketchup82 Apr 19, 2024
249663c
Remove ManagedBass.Enc from osu.Game.csproj
smallketchup82 Apr 19, 2024
a277ac3
Fix up logging and error handling in `BeatmapImporter`
smallketchup82 Apr 19, 2024
8eee827
Another small change to logging in `BeatmapImporter`
smallketchup82 Apr 19, 2024
62645f3
Fix improper cache invalidation
smallketchup82 Apr 21, 2024
fe69efc
Remove extra unnecessary variable in AudioNormalization
smallketchup82 Apr 21, 2024
b50e343
Remove old xmldoc
smallketchup82 Apr 21, 2024
5964a3f
Merge remote-tracking branch 'origin/master' into audio-normalization
smallketchup82 May 10, 2024
db43af4
Remove constants and variables related to global track volume adjustment
smallketchup82 May 10, 2024
2e140c7
Fix up some stuff relating to the maximum loudness and error handling…
smallketchup82 May 10, 2024
ca7e80d
Change the name of functions to match ppy.ManagedBass.Loud
smallketchup82 May 10, 2024
192dc5b
Merge remote-tracking branch 'upstream/master' into audio-normalization
smallketchup82 May 12, 2024
b81277f
Use framework loudness
hwsmm May 11, 2024
e58115f
Remove direct BASS usage in AudioNormalization and use framework coun…
hwsmm May 11, 2024
6f2d873
Add normalization volume bindables in OsuGameBase
hwsmm May 11, 2024
663aa15
Update normalization volume in MusicController
hwsmm May 11, 2024
729f22d
Apply normalization to hitobjects
hwsmm May 11, 2024
baede41
Create AudioNormalizationManager instead of putting in OsuGameBase
hwsmm May 12, 2024
2ce022d
Apply framework-side rename (AudioLoudness to TrackLoudness)
hwsmm May 12, 2024
89aabae
Add docs in AudioNormalizationManager
hwsmm May 12, 2024
6182878
Transform normalized volume along with queued track
hwsmm May 12, 2024
46f11c7
Remove AudioManager dependency from MusicController
hwsmm May 12, 2024
7abcb5f
Fix xmldoc in AudioNormalization
hwsmm May 12, 2024
953d661
Refactor Audio Normalization
hwsmm May 13, 2024
b5ceb48
Add audio normalization setting
hwsmm May 19, 2024
7b9360e
Refator audio normalization 2
hwsmm May 19, 2024
da8a99f
Merge pull request #1 from hwsmm/audio-normalization
smallketchup82 May 20, 2024
477d765
Transfer TrackNormalizeVolume with Track in WorkingBeatmap
hwsmm May 22, 2024
16af168
Merge pull request #2 from hwsmm/audio-normalization
smallketchup82 Jun 11, 2024
ee6abe9
Move gameplay sample volume normalization to DrawableRuleset
hwsmm Jul 5, 2024
fce862a
Merge branch 'master' into audio-normalization
smallketchup82 Oct 16, 2024
3e8c124
Move IntegratedLoudnessInVolumeOffset up for visibility
smallketchup82 Oct 16, 2024
e1a87aa
Code Quality
smallketchup82 Oct 16, 2024
7d53453
Add keywords to setting
smallketchup82 Oct 16, 2024
7bbb6b7
Rename setting to be more descriptive
smallketchup82 Oct 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions osu.Game/Audio/AudioNormalization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using osu.Framework.Audio.Track;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using Realms;

namespace osu.Game.Audio
{
/// <summary>
/// Audio Normalization Data
/// </summary>
public class AudioNormalization : EmbeddedObject, IEquatable<AudioNormalization>
{
/// <summary>
/// The target level for audio normalization
/// https://en.wikipedia.org/wiki/EBU_R_128
/// </summary>
public const int TARGET_LEVEL = -14;

/// <summary>
/// The integrated (average) loudness of the audio
/// </summary>
public float? IntegratedLoudness { get; init; }

/// <summary>
/// A volume offset that can be applied upon audio to reach <see cref="TARGET_LEVEL"/> (converted from integrated loudness).
/// </summary>
public double? IntegratedLoudnessInVolumeOffset => IntegratedLoudness != null ? TrackLoudness.ConvertToVolumeOffset(TARGET_LEVEL, (float)IntegratedLoudness) : null;

public AudioNormalization()
{
}

/// <summary>
/// Get the audio normalization for a beatmap. The loudness normalization value is stored in the object under <see cref="IntegratedLoudness"/>
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> of the beatmap</param>
/// <param name="beatmapSetInfo">The parent <see cref="BeatmapSetInfo"/> of the <see cref="BeatmapInfo"/> supplied</param>
/// <param name="realmFileStore">The <see cref="RealmFileStore"/> to use in getting the full path of the audio file</param>
public AudioNormalization(BeatmapInfo beatmapInfo, BeatmapSetInfo beatmapSetInfo, RealmFileStore realmFileStore)
{
string audiofile = beatmapInfo.Metadata.AudioFile;

if (string.IsNullOrEmpty(audiofile))
{
Logger.Log("Audio file not found for " + beatmapInfo.Metadata.Title, LoggingTarget.Runtime, LogLevel.Error);
return;
}

string? filepath = beatmapSetInfo.GetPathForFile(audiofile);

if (string.IsNullOrEmpty(filepath))
{
Logger.Log("File path not found for " + audiofile, LoggingTarget.Runtime, LogLevel.Error);
return;
}

using TrackLoudness loudness = new TrackLoudness(realmFileStore.Storage.GetStream(filepath));

float? integratedLoudness = loudness.GetIntegratedLoudness();

if (integratedLoudness == null)
{
Logger.Log("Failed to get loudness level for " + audiofile, LoggingTarget.Runtime, LogLevel.Error);
return;
}

IntegratedLoudness = integratedLoudness;
}

/// <summary>
/// Populate the audio normalization for every <see cref="BeatmapInfo"/> in a <see cref="BeatmapSetInfo"/>.
/// </summary>
/// <remarks>
/// This takes a <see cref="BeatmapInfo"/> and applies the same normalization to every <see cref="BeatmapInfo"/> in the <see cref="BeatmapSetInfo"/> that has the same audio file as the given <see cref="BeatmapInfo"/> to avoid having to calculate the loudness for an audio file multiple times
/// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to clone from</param>
/// <param name="beatmapSetInfo">The <see cref="BeatmapSetInfo"/> to populate</param>
public void PopulateSet(BeatmapInfo beatmapInfo, BeatmapSetInfo beatmapSetInfo)
{
foreach (BeatmapInfo beatmap in beatmapSetInfo.Beatmaps)
{
if (beatmap.AudioNormalization == null && beatmap.AudioEquals(beatmapInfo))
{
beatmap.AudioNormalization = new AudioNormalization
{
IntegratedLoudness = IntegratedLoudness
};
}
}
}


/// <inheritdoc />
public bool Equals(AudioNormalization? other) => other?.IntegratedLoudness != null && IntegratedLoudness != null && Math.Abs((float)(IntegratedLoudness - other.IntegratedLoudness)) < 0.0000001;
}
}
65 changes: 65 additions & 0 deletions osu.Game/Audio/AudioNormalizationManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Configuration;

namespace osu.Game.Audio
{
/// <summary>
/// Manages audio normalization.
/// </summary>
public class AudioNormalizationManager
{
/// <summary>
/// Fallback volume for tracks that <see cref="TrackLoudness"/> failed to measure loudness.
/// </summary>
public const double FALLBACK_VOLUME = 0.8;

private readonly Bindable<bool> audioNormalizationSetting;

/// <summary>
/// Samples assigned to hitobjects needs to bind to this bindable to normalize their volume in line with track.
/// </summary>
public readonly BindableDouble SampleNormalizeVolume = new BindableDouble(1.0);

/// <summary>
/// Creates a new <see cref="AudioNormalizationManager"/>.
/// </summary>
/// <param name="config">An osu config to get beatmap hitsounds bindable from.</param>
/// <param name="beatmap">A bindable for beatmap.</param>
public AudioNormalizationManager(OsuConfigManager config, IBindable<WorkingBeatmap> beatmap)
{
audioNormalizationSetting = config.GetBindable<bool>(OsuSetting.AudioNormalization);

updateNormalization(audioNormalizationSetting.Value, beatmap.Value);
audioNormalizationSetting.BindValueChanged(change => updateNormalization(change.NewValue, beatmap.Value));
beatmap.BindValueChanged(ev => onBeatmapChanged(ev.OldValue, ev.NewValue));
}

private void updateNormalization(bool value, WorkingBeatmap current)
{
if (value)
{
SampleNormalizeVolume.BindTo(current.TrackNormalizeVolume);
current.EnableTrackNormlization();
Logger.Log($"Normalization value: {(int)(current.TrackNormalizeVolume.Value * 100)}%");
}
else
{
SampleNormalizeVolume.UnbindFrom(current.TrackNormalizeVolume);
SampleNormalizeVolume.Value = 1.0;
current.DisableTrackNormalization();
}
}

private void onBeatmapChanged(WorkingBeatmap oldBeatmap, WorkingBeatmap newBeatmap)
{
SampleNormalizeVolume.UnbindFrom(oldBeatmap.TrackNormalizeVolume);
updateNormalization(audioNormalizationSetting.Value, newBeatmap);
}
}
}
18 changes: 18 additions & 0 deletions osu.Game/Beatmaps/BeatmapImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Audio;
using osu.Game.Beatmaps.Formats;
using osu.Game.Collections;
using osu.Game.Database;
Expand Down Expand Up @@ -173,6 +174,23 @@ protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archi
b.Ruleset = realm.Find<RulesetInfo>(b.Ruleset.ShortName) ?? throw new ArgumentNullException(nameof(b.Ruleset));
}

foreach (BeatmapInfo beatmapInfo in beatmapSet.Beatmaps)
{
if (beatmapInfo.AudioNormalization != null) continue;

AudioNormalization audioNormalization = new AudioNormalization(beatmapInfo, beatmapSet, Files);

if (audioNormalization.IntegratedLoudness == null)
{
Logger.Log($"Failed to calculate audio normalization values for {beatmapInfo.Metadata.Title} [{beatmapInfo.DifficultyName}]", LoggingTarget.Runtime, LogLevel.Error);
continue;
}

beatmapInfo.AudioNormalization = audioNormalization;
beatmapInfo.AudioNormalization.PopulateSet(beatmapInfo, beatmapSet);
Logger.Log($"Processed loudness values for {beatmapInfo.Metadata.Title} [{beatmapInfo.DifficultyName}]");
}

validateOnlineIds(beatmapSet, realm);

bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0);
Expand Down
3 changes: 3 additions & 0 deletions osu.Game/Beatmaps/BeatmapInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Collections;
using osu.Game.Database;
Expand Down Expand Up @@ -41,6 +42,8 @@ public class BeatmapInfo : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo, IEquat

public BeatmapMetadata Metadata { get; set; } = null!;

public AudioNormalization? AudioNormalization { get; set; }

[JsonIgnore]
[Backlink(nameof(ScoreInfo.BeatmapInfo))]
public IQueryable<ScoreInfo> Scores { get; } = null!;
Expand Down
26 changes: 26 additions & 0 deletions osu.Game/Beatmaps/WorkingBeatmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
Expand Down Expand Up @@ -111,9 +113,32 @@ public Track LoadTrack()
waveform?.Dispose();
waveform = null;

TrackNormalizeVolume = new BindableDouble(AudioNormalizationManager.FALLBACK_VOLUME);
track.AddAdjustment(AdjustableProperty.Volume, TrackNormalizeVolume);

return track;
}

// Normalization is here because it's not so good to apply normalization in global state through MusicController or TrackStore.
// Each beatmap has its own track and its own track loudness value.
public BindableDouble TrackNormalizeVolume { get; private set; }

public void EnableTrackNormlization()
{
if (TrackNormalizeVolume == null)
throw new InvalidOperationException($"{nameof(TrackNormalizeVolume)} is not available. You must call {nameof(LoadTrack)} before calling this.");

TrackNormalizeVolume.Value = BeatmapInfo.AudioNormalization?.IntegratedLoudnessInVolumeOffset ?? AudioNormalizationManager.FALLBACK_VOLUME;
}

public void DisableTrackNormalization()
{
if (TrackNormalizeVolume == null)
throw new InvalidOperationException($"{nameof(TrackNormalizeVolume)} is not available. You must call {nameof(LoadTrack)} before calling this.");

TrackNormalizeVolume.Value = AudioNormalizationManager.FALLBACK_VOLUME;
}

public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
{
Track.Looping = looping;
Expand Down Expand Up @@ -144,6 +169,7 @@ public virtual bool TryTransferTrack([NotNull] WorkingBeatmap target)
if (BeatmapInfo?.AudioEquals(target.BeatmapInfo) != true || Track.IsDummyDevice)
return false;

target.TrackNormalizeVolume = TrackNormalizeVolume;
target.track = Track;
return true;
}
Expand Down
3 changes: 3 additions & 0 deletions osu.Game/Configuration/OsuConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ protected override void InitialiseDefaults()

SetDefault(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1);

SetDefault(OsuSetting.AudioNormalization, true);

// Input
SetDefault(OsuSetting.MenuCursorSize, 1.0f, 0.5f, 2f, 0.01f);
SetDefault(OsuSetting.GameplayCursorSize, 1.0f, 0.1f, 2f, 0.01f);
Expand Down Expand Up @@ -355,6 +357,7 @@ public enum OsuSetting
/// This is added to the audio track's current time. Higher values will cause gameplay to occur earlier, relative to the audio track.
/// </summary>
AudioOffset,
AudioNormalization,

VolumeInactive,
MenuMusic,
Expand Down
67 changes: 67 additions & 0 deletions osu.Game/Database/BackgroundDataStoreProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.API;
Expand Down Expand Up @@ -84,6 +85,7 @@ protected override void LoadComplete()
convertLegacyTotalScoreToStandardised();
upgradeScoreRanks();
backpopulateMissingSubmissionAndRankDates();
processAudioNormalization();
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
{
if (t.Exception?.InnerException is ObjectDisposedException)
Expand All @@ -96,6 +98,71 @@ protected override void LoadComplete()
});
}

/// <summary>
/// Go through every beatmap and calculate the audio normalization values if they are missing
/// </summary>
private void processAudioNormalization()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow the example set by every other method in this file...

  • Storing the IDs to process ahead of time.
  • Iterating through those stored IDs.
  • Not nesting .Write() inside a .Run().

@smallketchup82 smallketchup82 Apr 5, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with 1 and 2 is that the way that the loops work don't allow for calculating what to process ahead of time. If it were possible, I would have totalcount be only what needs to be processed rather than the number of total difficulties a user has. The loop populates other difficulties then skips over them in the proceeding loop. This behaviour would be hard to replicate in another loopn since it relies on the difficulties being populated during the loop. Sure It can be done but it would be hacky at best.

3rd point was done to have the beatmaps update instantly instead of being deferred until after everything is calculated. I did try to avoid nesting because common sense states that it's a bad idea, but well, the loop seems to break if I try this instead

diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs
index fc38f3532c..d50d86b97d 100644
--- a/osu.Game/Database/BackgroundDataStoreProcessor.cs
+++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs
@@ -119,37 +119,38 @@ private void processAudioNormalization()
             var notification = showProgressNotification(beatmapsCount, "Verifying loudness level for beatmaps", "all loudness levels have been verified");
             int processedCount = 0;
 
+            IQueryable<BeatmapSetInfo> beatmaps = null!;
             realmAccess.Run(r =>
             {
-                r.Refresh();
+                beatmaps = r.All<BeatmapSetInfo>().Where(b => !b.DeletePending);
+            });
 
-                foreach (BeatmapSetInfo beatmapSetInfo in r.All<BeatmapSetInfo>().Where(b => !b.DeletePending))
+            foreach (BeatmapSetInfo beatmapSetInfo in beatmaps)
+            {
+                foreach (BeatmapInfo beatmapInfo in beatmapSetInfo.Beatmaps)
                 {
-                    foreach (BeatmapInfo beatmapInfo in beatmapSetInfo.Beatmaps)
-                    {
-                        if (notification?.State == ProgressNotificationState.Cancelled)
-                            break;
-
-                        updateNotificationProgress(notification, processedCount, beatmapsCount);
-                        processedCount++;
+                    if (notification?.State == ProgressNotificationState.Cancelled)
+                        break;
 
-                        sleepIfRequired();
-                        if (beatmapInfo.AudioNormalization != null) continue;
+                    updateNotificationProgress(notification, processedCount, beatmapsCount);
+                    processedCount++;
 
-                        realmAccess.Write(realm =>
-                        {
-                            AudioNormalization audioNormalization = new AudioNormalization(beatmapInfo, beatmapSetInfo, new RealmFileStore(realmAccess, storage!));
-                            beatmapInfo.AudioNormalization = audioNormalization;
-                            realm.Add(audioNormalization.PopulateSet(beatmapInfo, beatmapSetInfo), true);
-                        });
-                        beatmapManager.GetWorkingBeatmap(beatmapInfo, true);
-                    }
+                    sleepIfRequired();
+                    if (beatmapInfo.AudioNormalization != null) continue;
 
-                    Logger.Log($"Processed audio normalization for {beatmapSetInfo.Metadata.Title}");
+                    realmAccess.Write(realm =>
+                    {
+                        AudioNormalization audioNormalization = new AudioNormalization(beatmapInfo, beatmapSetInfo, new RealmFileStore(realmAccess, storage!));
+                        beatmapInfo.AudioNormalization = audioNormalization;
+                        realm.Add(audioNormalization.PopulateSet(beatmapInfo, beatmapSetInfo), true);
+                    });
+                    beatmapManager.GetWorkingBeatmap(beatmapInfo, true);
                 }
 
-                r.Refresh();
-            });
+                Logger.Log($"Processed audio normalization for {beatmapSetInfo.Metadata.Title}");
+            }
+
+            realmAccess.Run(r => r.Refresh());
 
             Logger.Log("Finished processing audio normalization readings");
             completeNotification(notification, processedCount, beatmapsCount);

So to me, it's either everything gets deferred and we wrap the entire function in a RealmAccess.Run, or we go with this.

@smallketchup82 smallketchup82 Apr 9, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an update since the memory leak fixes:
My argument to point 1 and 2 stand.
While I still want to avoid deferring the realm updates until everything is complete (this will take a LONG time for big libraries, so being able to quit but save progress would be useful), I do understand that this is more efficient. For the time being I've decided to just defer everything, but If its possible not to, I would prefer that. Also the diff shown still does not work.

{
var filestorage = new RealmFileStore(realmAccess, storage);
int beatmapsCount = 0;
int beatmapsToProcess = 0;

realmAccess.Run(r =>
{
foreach (BeatmapSetInfo beatmapSetInfo in r.All<BeatmapSetInfo>().Where(b => !b.DeletePending))
{
beatmapsCount += beatmapSetInfo.Beatmaps.Count;
beatmapsToProcess += beatmapSetInfo.Beatmaps.Count(b => b.AudioNormalization == null);
}
});

if (beatmapsToProcess == 0)
{
Logger.Log("No audio normalization processing required.");
return;
}

var notification = showProgressNotification(beatmapsCount, "Verifying loudness level for beatmaps", "all loudness levels have been verified");
int processedCount = 0;

realmAccess.Write(r =>
{
foreach (BeatmapSetInfo beatmapSetInfo in r.All<BeatmapSetInfo>().Where(b => !b.DeletePending))
{
foreach (BeatmapInfo beatmapInfo in beatmapSetInfo.Beatmaps)
{
if (notification?.State == ProgressNotificationState.Cancelled)
break;

updateNotificationProgress(notification, processedCount, beatmapsCount);
processedCount++;

if (beatmapInfo.AudioNormalization != null) continue;

sleepIfRequired();

AudioNormalization audioNormalization = new AudioNormalization(beatmapInfo, beatmapSetInfo, filestorage);

if (audioNormalization.IntegratedLoudness == null)
{
Logger.Log($"Failed to get loudness level for {beatmapSetInfo.Metadata.Title} [{beatmapInfo.DifficultyName}]", LoggingTarget.Runtime, LogLevel.Error);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this does not check if the audio file plays correctly, so if the user has any beatmaps with a broken audio file, this will attempt to process them, fail, then notify the user of the failure. This happens on every startup.

Maybe store a default value like TARGET_LEVEL, or a NaN, for integrated loudness in this case, while reminding user of the failure?

@smallketchup82 smallketchup82 Apr 2, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a solution where we log the error, then set a non-standard value for the loudness, and filter them out from the loop could work to stop the processor from continuously attempting to process these broken audio tracks on startup.

The issue then becomes, how will we know if the audio file for that beatmap has been fixed if the processor will gloss over it every time it processes the map?

I think it would be better to use the non-standard value as a sort of "silence" indicator to which if the beatmap is processed again, and fails to do so, it won't alert the user of the failure. That way we don't annoy the user with failure notifications on every startup, but also continue to process the audio files in case they've been replaced with a working version.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest creating a Realm model for looking up normalization parameters from audio file hash, and calculate when the hash exists but the corresponding parameters isn't. Or, if it seems too complex, a quick hack is to recalculate ever beatmap update, in BeatmapUpdater.Process(), like how star rating is treated.

continue;
}

beatmapInfo.AudioNormalization = audioNormalization;
audioNormalization.PopulateSet(beatmapInfo, beatmapSetInfo);
((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmapSetInfo);
Logger.Log($"Processed audio normalization for {beatmapSetInfo.Metadata.Title} [{beatmapInfo.DifficultyName}]");
}
}

r.Refresh();
});

Logger.Log("Finished processing audio normalization readings");
completeNotification(notification, processedCount, beatmapsCount);
}

/// <summary>
/// Check whether the databased difficulty calculation version matches the latest ruleset provided version.
/// If it doesn't, clear out any existing difficulties so they can be incrementally recalculated.
Expand Down
3 changes: 2 additions & 1 deletion osu.Game/Database/RealmAccess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ public class RealmAccess : IDisposable
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
/// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user.
/// 44 2024-10-16 Add AudioNormalization to BeatmapInfo.
/// </summary>
private const int schema_version = 43;
private const int schema_version = 44;

/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
Expand Down
Loading