diff --git a/osu.Framework/Audio/Track/TrackLoudness.cs b/osu.Framework/Audio/Track/TrackLoudness.cs new file mode 100644 index 00000000000..76abac2ca6c --- /dev/null +++ b/osu.Framework/Audio/Track/TrackLoudness.cs @@ -0,0 +1,140 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using ManagedBass; +using ManagedBass.Loud; +using osu.Framework.Audio.Callbacks; +using osu.Framework.Extensions; +using osu.Framework.Logging; + +namespace osu.Framework.Audio.Track +{ + /// + /// Measures loudness of audio samples. + /// + public class TrackLoudness : IDisposable + { + private Stream? data; + + private readonly Task readTask; + + private readonly CancellationTokenSource cancelSource = new CancellationTokenSource(); + + private float? integratedLoudness; + + /// + /// Measures loudness from provided audio data. + /// + /// + /// The sample data stream. + /// The will take ownership of this stream and dispose it when done reading track data. + /// If null, loudness won't get calculated. + /// + public TrackLoudness(Stream? data) + { + this.data = data; + + readTask = Task.Run(() => + { + if (data == null) + return; + + if (Bass.CurrentDevice < 0) + { + Logger.Log("Failed to measure loudness as no bass device is available."); + return; + } + + FileCallbacks fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); + + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Float, fileCallbacks.Callbacks, fileCallbacks.Handle); + + if (decodeStream == 0) + { + Logger.Log($"Bass failed to create a stream while trying to measure loudness: {Bass.LastError}"); + fileCallbacks.Dispose(); + return; + } + + byte[] buffer = ArrayPool.Shared.Rent(20000); + + try + { + int loudHandle = BassLoud.Start(decodeStream, BassFlags.BassLoudnessIntegrated | BassFlags.BassLoudnessAutofree, 0); + + if (loudHandle == 0) + { + Logger.Log($"Failed to start BassLoud: {Bass.LastError}"); + return; + } + + while (Bass.ChannelGetData(decodeStream, buffer, buffer.Length) >= 0) + { + } + + bool gotLevel = BassLoud.GetLevel(loudHandle, BassFlags.BassLoudnessIntegrated, out float integratedLoudness); + + if (!gotLevel) + { + Logger.Log($"Failed to get loudness level: {Bass.LastError}"); + return; + } + + if (integratedLoudness < 0) + this.integratedLoudness = integratedLoudness; + } + finally + { + Bass.StreamFree(decodeStream); + fileCallbacks.Dispose(); + data.Dispose(); + + ArrayPool.Shared.Return(buffer); + } + }, cancelSource.Token); + } + + /// + /// Returns integrated loudness. + /// + public async Task GetIntegratedLoudnessAsync() + { + await readTask.ConfigureAwait(false); + + return integratedLoudness; + } + + /// + /// Returns integrated loudness. + /// + public float? GetIntegratedLoudness() => GetIntegratedLoudnessAsync().GetResultSafely(); + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + cancelSource.Cancel(); + cancelSource.Dispose(); + integratedLoudness = null; + + data?.Dispose(); + data = null; + + disposedValue = true; + } + } + + private bool disposedValue; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +}