diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 2b55e81788ad..9bb884377461 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -120,6 +120,8 @@ private void load()
bodyPiece.CreateProxy(),
tailContainer.CreateProxy(),
});
+
+ slidingSample.BindAdjustments(Samples);
}
protected override void LoadComplete()
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index e519e51562fc..38372b65957d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -136,6 +136,8 @@ private void load()
foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue;
}, true);
+
+ slidingSample.BindAdjustments(Samples);
}
protected override JudgementResult CreateResult(Judgement judgement) => new OsuSliderJudgementResult(HitObject, judgement);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 8c21e6a6bca1..e72df1f0e172 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -121,6 +121,9 @@ private void load()
});
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
+
+ spinningSample.BindAdjustments(Samples);
+ maxBonusSample.BindAdjustments(Samples);
}
protected override void LoadComplete()
diff --git a/osu.Game/Audio/AudioNormalization.cs b/osu.Game/Audio/AudioNormalization.cs
index d898843845f2..12b352a06a0f 100644
--- a/osu.Game/Audio/AudioNormalization.cs
+++ b/osu.Game/Audio/AudioNormalization.cs
@@ -2,10 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
-using ManagedBass;
-using ManagedBass.Fx;
-using osu.Framework.Audio.Mixing;
+using osu.Framework.Audio.Track;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
@@ -14,9 +11,9 @@
namespace osu.Game.Audio
{
///
- /// Audio Normalization Manager
+ /// Audio Normalization Data
///
- public class AudioNormalization : EmbeddedObject, IAudioNormalization, IEquatable
+ public class AudioNormalization : EmbeddedObject, IEquatable
{
///
/// The target level for audio normalization
@@ -57,9 +54,9 @@ public AudioNormalization(BeatmapInfo beatmapInfo, BeatmapSetInfo beatmapSetInfo
return;
}
- filepath = realmFileStore.Storage.GetFullPath(filepath);
+ using TrackLoudness loudness = new TrackLoudness(realmFileStore.Storage.GetStream(filepath));
- float? integratedLoudness = new BassAudioNormalization(filepath).IntegratedLoudness;
+ float? integratedLoudness = loudness.GetIntegratedLoudness();
if (integratedLoudness == null)
{
@@ -93,57 +90,9 @@ public void PopulateSet(BeatmapInfo beatmapInfo, BeatmapSetInfo beatmapSetInfo)
}
///
- /// Convert integrated loudness to a volume offset
+ /// A volume offset that can be applied upon audio to reach (converted from integrated loudness).
///
- /// The integrated loudness value
- /// The volume offset needed to reach
- public static float IntegratedLoudnessToVolumeOffset(float integratedLoudness) => (float)Math.Pow(10, (TARGET_LEVEL - integratedLoudness) / 20);
-
- ///
- /// Get the loudness values of a and apply the audio normalization effect to an
- ///
- /// The to get the loudness values of
- /// The to apply the effect on
- public static void AddAudioNormalization(BeatmapInfo beatmapInfo, IAudioMixer mixer)
- {
- AudioNormalization? audioNormalizationModule = beatmapInfo.AudioNormalization;
-
- VolumeParameters volumeParameters = new VolumeParameters
- {
- fTarget = audioNormalizationModule?.IntegratedLoudness != null ? IntegratedLoudnessToVolumeOffset((float)audioNormalizationModule.IntegratedLoudness) : 0.8f,
- fCurrent = -1.0f,
- fTime = 0.3f,
- lCurve = 1,
- lChannel = FXChannelFlags.All
- };
-
- addFx(volumeParameters, mixer);
-
- Logger.Log("Normalization Status: " + (audioNormalizationModule?.IntegratedLoudness != null ? $"on ({Math.Round(IntegratedLoudnessToVolumeOffset((float)audioNormalizationModule.IntegratedLoudness) * 100)}%)" : "off"));
- }
-
- ///
- /// Add an effect to a , replacing it if it already exists
- ///
- /// The to add to the
- /// The to add the effect to
- private static void addFx(IEffectParameter effectParameter, IAudioMixer mixer)
- {
- IEffectParameter? effect = mixer.Effects.SingleOrDefault(e => e.FXType == effectParameter.FXType);
-
- if (effect != null)
- {
- int i = mixer.Effects.IndexOf(effect);
- mixer.Effects[i] = effectParameter;
- }
- else
- {
- mixer.Effects.Add(effectParameter);
- }
- }
-
- ///
- public bool Equals(IAudioNormalization? other) => other is AudioNormalization audioNormalization && Equals(audioNormalization);
+ public double? IntegratedLoudnessInVolumeOffset => IntegratedLoudness != null ? TrackLoudness.ConvertToVolumeOffset(TARGET_LEVEL, (float)IntegratedLoudness) : null;
///
public bool Equals(AudioNormalization? other) => other?.IntegratedLoudness != null && IntegratedLoudness != null && Math.Abs((float)(IntegratedLoudness - other.IntegratedLoudness)) < 0.0000001;
diff --git a/osu.Game/Audio/AudioNormalizationManager.cs b/osu.Game/Audio/AudioNormalizationManager.cs
new file mode 100644
index 000000000000..00293d965dc6
--- /dev/null
+++ b/osu.Game/Audio/AudioNormalizationManager.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . 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
+{
+ ///
+ /// Manages audio normalization.
+ ///
+ public class AudioNormalizationManager
+ {
+ ///
+ /// Fallback volume for tracks that failed to measure loudness.
+ ///
+ public const double FALLBACK_VOLUME = 0.8;
+
+ private readonly Bindable audioNormalizationSetting;
+
+ ///
+ /// Samples assigned to hitobjects needs to bind to this bindable to normalize their volume in line with track.
+ ///
+ public readonly BindableDouble SampleNormalizeVolume = new BindableDouble(1.0);
+
+ ///
+ /// Creates a new .
+ ///
+ /// An osu config to get beatmap hitsounds bindable from.
+ /// A bindable for beatmap.
+ public AudioNormalizationManager(OsuConfigManager config, IBindable beatmap)
+ {
+ audioNormalizationSetting = config.GetBindable(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);
+ }
+ }
+}
diff --git a/osu.Game/Audio/BassAudioNormalization.cs b/osu.Game/Audio/BassAudioNormalization.cs
deleted file mode 100644
index 2dd3938f31c3..000000000000
--- a/osu.Game/Audio/BassAudioNormalization.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using ManagedBass;
-using ManagedBass.Loud;
-using osu.Framework.Logging;
-
-namespace osu.Game.Audio
-{
- ///
- /// Audio Normalization Implementation using Bass
- ///
- public class BassAudioNormalization
- {
- ///
- /// The integrated loudness of the audio
- ///
- ///
- /// Applicable range = -70 to 0
Null if the loudness could not be calculated due to an error
- ///
- public float? IntegratedLoudness { get; }
-
- ///
- /// Calculate the integrated loudness of an audio file using Bass
- ///
- /// A path to an audio file
- public BassAudioNormalization(string filePath)
- {
- int decodeStream = Bass.CreateStream(filePath, 0, 0, BassFlags.Decode | BassFlags.Float);
-
- if (decodeStream == 0)
- {
- Logger.Log("Failed to create stream for loudness measurement!\nError Code: " + Bass.LastError, LoggingTarget.Runtime, LogLevel.Error);
- IntegratedLoudness = null;
- Bass.StreamFree(decodeStream);
- return;
- }
-
- int loudness = BassLoud.Start(decodeStream, BassFlags.BassLoudnessIntegrated | BassFlags.BassLoudnessAutofree, 0);
-
- if (loudness == 0)
- {
- Logger.Log("Failed to start loudness measurement!\nError Code: " + Bass.LastError, LoggingTarget.Runtime, LogLevel.Error);
- IntegratedLoudness = null;
- Bass.StreamFree(decodeStream);
- return;
- }
-
- byte[] buffer = new byte[10000];
-
- while (Bass.ChannelGetData(decodeStream, buffer, buffer.Length) >= 0)
- {
- }
-
- float integratedLoudness = 1;
- bool gotLevel = BassLoud.GetLevel(loudness, BassFlags.BassLoudnessIntegrated, ref integratedLoudness);
-
- if (!gotLevel || integratedLoudness > 0)
- {
- Logger.Log("Failed to get loudness level!\nError Code: " + Bass.LastError, LoggingTarget.Runtime, LogLevel.Error);
- IntegratedLoudness = null;
- Bass.StreamFree(decodeStream);
- return;
- }
-
- IntegratedLoudness = integratedLoudness;
-
- bool freedStream = Bass.StreamFree(decodeStream);
- if (!freedStream)
- Logger.Log("Failed to free stream!\nError Code: " + Bass.LastError, LoggingTarget.Runtime, LogLevel.Error);
- }
- }
-}
diff --git a/osu.Game/Audio/IAudioNormalization.cs b/osu.Game/Audio/IAudioNormalization.cs
deleted file mode 100644
index 14cc1cda06e4..000000000000
--- a/osu.Game/Audio/IAudioNormalization.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-
-namespace osu.Game.Audio
-{
- public interface IAudioNormalization : IEquatable
- {
- public float? IntegratedLoudness { get; }
- }
-}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 25159996f3d2..db8ca80be122 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -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;
@@ -111,9 +113,20 @@ public Track LoadTrack()
waveform?.Dispose();
waveform = null;
+ 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 readonly BindableDouble TrackNormalizeVolume = new BindableDouble(AudioNormalizationManager.FALLBACK_VOLUME);
+
+ public void EnableTrackNormlization()
+ => TrackNormalizeVolume.Value = BeatmapInfo.AudioNormalization?.IntegratedLoudnessInVolumeOffset ?? AudioNormalizationManager.FALLBACK_VOLUME;
+
+ public void DisableTrackNormalization() => TrackNormalizeVolume.Value = AudioNormalizationManager.FALLBACK_VOLUME;
+
public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
{
Track.Looping = looping;
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index f4a4c553d8cf..30aac469fff7 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -109,6 +109,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);
@@ -360,6 +362,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.
///
AudioOffset,
+ AudioNormalization,
VolumeInactive,
MenuMusic,
diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs
index 89db60d8a69c..4d38883f6441 100644
--- a/osu.Game/Localisation/AudioSettingsStrings.cs
+++ b/osu.Game/Localisation/AudioSettingsStrings.cs
@@ -64,6 +64,11 @@ public static class AudioSettingsStrings
///
public static LocalisableString AudioOffset => new TranslatableString(getKey(@"audio_offset"), @"Audio offset");
+ ///
+ /// "Normalize Audio"
+ ///
+ public static LocalisableString AudioNormalization => new TranslatableString(getKey(@"audio_normalization"), @"Normalize Audio");
+
///
/// "Play a few beatmaps to receive a suggested offset!"
///
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 96e7d5a603ec..073eb049fc63 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -212,6 +212,8 @@ public virtual string Version
protected SafeAreaContainer SafeAreaContainer { get; private set; }
+ protected AudioNormalizationManager AudioNormalizationManager { get; private set; }
+
///
/// For now, this is used as a source specifically for beat synced components.
/// Going forward, it could potentially be used as the single source-of-truth for beatmap timing.
@@ -365,6 +367,8 @@ private void load(ReadableKeyCombinationProvider keyCombinationProvider, Framewo
dependencies.Cache(previewTrackManager = new PreviewTrackManager(BeatmapManager.BeatmapTrackStore));
base.Content.Add(previewTrackManager);
+ dependencies.Cache(AudioNormalizationManager = new AudioNormalizationManager(LocalConfig, Beatmap));
+
base.Content.Add(MusicController = new MusicController());
dependencies.CacheAs(MusicController);
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index 6a15f6ee35b6..0986c0513ca8 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -16,7 +16,6 @@
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Threading;
-using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Mods;
@@ -64,18 +63,11 @@ public partial class MusicController : CompositeDrawable
[Resolved]
private RealmAccess realm { get; set; }
- [Resolved]
- private AudioManager audioManager { get; set; }
-
protected override void LoadComplete()
{
base.LoadComplete();
- beatmap.BindValueChanged(b =>
- {
- changeBeatmap(b.NewValue);
- AudioNormalization.AddAudioNormalization(b.NewValue.BeatmapInfo, audioManager.TrackMixer);
- }, true);
+ beatmap.BindValueChanged(b => changeBeatmap(b.NewValue), true);
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
}
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
index 2bb5fa983f54..7d73dbd199f0 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
@@ -49,6 +49,11 @@ private void load(AudioManager audio, OsuConfigManager config)
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
+ new SettingsCheckbox
+ {
+ LabelText = AudioSettingsStrings.AudioNormalization,
+ Current = config.GetBindable(OsuSetting.AudioNormalization)
+ }
};
}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 3ce6cc3cef6e..358f79a9a263 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -10,6 +10,7 @@
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ListExtensions;
using osu.Framework.Extensions.ObjectExtensions;
@@ -197,7 +198,7 @@ protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
}
[BackgroundDependencyLoader]
- private void load(IGameplaySettings gameplaySettings, ISkinSource skinSource)
+ private void load(IGameplaySettings gameplaySettings, ISkinSource skinSource, AudioNormalizationManager audioNormalizationManager)
{
positionalHitsoundsLevel.BindTo(gameplaySettings.PositionalHitsoundsLevel);
comboColourBrightness.BindTo(gameplaySettings.ComboColourNormalisationAmount);
@@ -208,6 +209,8 @@ private void load(IGameplaySettings gameplaySettings, ISkinSource skinSource)
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME
});
+ Samples.AddAdjustment(AdjustableProperty.Volume, audioNormalizationManager.SampleNormalizeVolume);
+
CurrentSkin = skinSource;
CurrentSkin.SourceChanged += skinSourceChanged;
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 07270979ce20..5452a648d1d2 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -34,10 +34,6 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-