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 - - - -