diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index f6c366271d..28c6433e7e 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -265,7 +265,7 @@ public AudioMixer CreateAudioMixer(string identifier = default) => private AudioMixer createAudioMixer(AudioMixer fallbackMixer, string identifier) { - var mixer = new BassAudioMixer(this, fallbackMixer, identifier); + var mixer = new PassthroughBassAudioMixer(this, fallbackMixer, identifier); AddItem(mixer); return mixer; } diff --git a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs index a564a6b20a..0514eeaf5b 100644 --- a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs +++ b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs @@ -14,7 +14,7 @@ namespace osu.Framework.Audio.Mixing.Bass /// /// Mixes together multiple into one output via BASSmix. /// - internal class BassAudioMixer : AudioMixer, IBassAudio + internal class BassAudioMixer : AudioMixer, IBassAudio, IBassAudioMixer { private readonly AudioManager? manager; @@ -100,6 +100,9 @@ protected override void RemoveInternal(IAudioChannel channel) removeChannelFromBassMix(bassChannel); } + public BassFlags SampleFlags => BassFlags.SampleChannelStream | BassFlags.Decode; + public BassFlags TrackFlags => BassFlags.Decode; + /// /// Plays a channel. /// diff --git a/osu.Framework/Audio/Mixing/Bass/IBassAudioChannel.cs b/osu.Framework/Audio/Mixing/Bass/IBassAudioChannel.cs index 4e6905b183..12e8722fd2 100644 --- a/osu.Framework/Audio/Mixing/Bass/IBassAudioChannel.cs +++ b/osu.Framework/Audio/Mixing/Bass/IBassAudioChannel.cs @@ -26,6 +26,6 @@ internal interface IBassAudioChannel : IAudioChannel /// bool MixerChannelPaused { get; set; } - new BassAudioMixer Mixer { get; } + new IBassAudioMixer Mixer { get; } } } diff --git a/osu.Framework/Audio/Mixing/Bass/IBassAudioMixer.cs b/osu.Framework/Audio/Mixing/Bass/IBassAudioMixer.cs new file mode 100644 index 0000000000..5641dee430 --- /dev/null +++ b/osu.Framework/Audio/Mixing/Bass/IBassAudioMixer.cs @@ -0,0 +1,136 @@ +// 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.Runtime.InteropServices; +using ManagedBass; +using ManagedBass.Mix; + +namespace osu.Framework.Audio.Mixing.Bass +{ + internal interface IBassAudioMixer + { + BassFlags SampleFlags { get; } + BassFlags TrackFlags { get; } + + void Add(IAudioChannel channel); + + /// + /// Plays a channel. + /// + /// See: . + /// The channel to play. + /// Restart playback from the beginning? + /// + /// If successful, is returned, else is returned. + /// Use to get the error code. + /// + bool ChannelPlay(IBassAudioChannel channel, bool restart = false); + + /// + /// Pauses a channel. + /// + /// See: . + /// The channel to pause. + /// Set to true to make the pause take effect immediately. + /// + /// This will change the timing of , so should be used sparingly. + /// + /// + /// + /// If successful, is returned, else is returned. + /// Use to get the error code. + /// + bool ChannelPause(IBassAudioChannel channel, bool flushMixer = false); + + /// + /// Checks if a channel is active (playing) or stalled. + /// + /// See: . + /// The channel to get the state of. + /// indicating the state of the channel. + PlaybackState ChannelIsActive(IBassAudioChannel channel); + + /// + /// Retrieves the playback position of a channel. + /// + /// See: . + /// The channel to retrieve the position of. + /// How to retrieve the position. + /// + /// If an error occurs, -1 is returned, use to get the error code. + /// If successful, the position is returned. + /// + long ChannelGetPosition(IBassAudioChannel channel, PositionFlags mode = PositionFlags.Bytes); + + /// + /// Sets the playback position of a channel. + /// + /// See: . + /// The to set the position of. + /// The position, in units determined by the . + /// How to set the position. + /// + /// If successful, then is returned, else is returned. + /// Use to get the error code. + /// + bool ChannelSetPosition(IBassAudioChannel channel, long position, PositionFlags mode = PositionFlags.Bytes); + + /// + /// Retrieves the level (peak amplitude) of a channel. + /// + /// See: . + /// The to get the levels of. + /// The array in which the levels are to be returned. + /// How much data (in seconds) to look at to get the level (limited to 1 second). + /// What levels to retrieve. + /// true if successful, false otherwise. + bool ChannelGetLevel(IBassAudioChannel channel, [In, Out] float[] levels, float length, LevelRetrievalFlags flags); + + /// + /// Retrieves the immediate sample data (or an FFT representation of it) of a channel. + /// + /// See: . + /// The to retrieve the data of. + /// float[] to write the data to. + /// Number of bytes wanted, and/or . + /// If an error occurs, -1 is returned, use to get the error code. + /// When requesting FFT data, the number of bytes read from the channel (to perform the FFT) is returned. + /// When requesting sample data, the number of bytes written to buffer will be returned (not necessarily the same as the number of bytes read when using the or DataFlags.Fixed flag). + /// When using the flag, the number of bytes in the channel's buffer is returned. + /// + int ChannelGetData(IBassAudioChannel channel, float[] buffer, int length); + + /// + /// Sets up a synchroniser on a mixer source channel. + /// + /// See: . + /// The to set up the synchroniser for. + /// The type of sync. + /// The sync parameters, depending on the sync type. + /// The callback function which should be invoked with the sync. + /// User instance data to pass to the callback function. + /// If successful, then the new synchroniser's handle is returned, else 0 is returned. Use to get the error code. + int ChannelSetSync(IBassAudioChannel channel, SyncFlags type, long parameter, SyncProcedure procedure, IntPtr user = default); + + /// + /// Removes a synchroniser from a mixer source channel. + /// + /// The to remove the synchroniser for. + /// Handle of the synchroniser to remove (return value of a previous call). + /// If successful, is returned, else is returned. Use to get the error code. + bool ChannelRemoveSync(IBassAudioChannel channel, int sync); + + /// + /// Frees a channel's resources. + /// + /// The to free. + /// If successful, is returned, else is returned. Use to get the error code. + bool StreamFree(IBassAudioChannel channel); + + /// + /// Adds a channel to the native BASS mix. + /// + void AddChannelToBassMix(IBassAudioChannel channel); + } +} diff --git a/osu.Framework/Audio/Mixing/Bass/PassthroughBassAudioMixer.cs b/osu.Framework/Audio/Mixing/Bass/PassthroughBassAudioMixer.cs new file mode 100644 index 0000000000..cc920677dc --- /dev/null +++ b/osu.Framework/Audio/Mixing/Bass/PassthroughBassAudioMixer.cs @@ -0,0 +1,115 @@ +// 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.Collections.Generic; +using ManagedBass; + +namespace osu.Framework.Audio.Mixing.Bass +{ + internal class PassthroughBassAudioMixer : AudioMixer, IBassAudio, IBassAudioMixer + { + private readonly List activeChannels = new List(); + + public PassthroughBassAudioMixer(AudioManager manager, AudioMixer? fallbackMixer, string identifier) + : base(fallbackMixer, identifier) + { + } + + public override void AddEffect(IEffectParameter effect, int priority = 0) + { + } + + public override void RemoveEffect(IEffectParameter effect) + { + } + + public override void UpdateEffect(IEffectParameter effect) + { + } + + protected override void AddInternal(IAudioChannel channel) + { + if (!(channel is IBassAudioChannel bassChannel)) + return; + + if (bassChannel.Handle == 0) + return; + + activeChannels.Add(bassChannel); + + if (ManagedBass.Bass.ChannelIsActive(bassChannel.Handle) != PlaybackState.Stopped) + ManagedBass.Bass.ChannelPlay(bassChannel.Handle); + } + + protected override void RemoveInternal(IAudioChannel channel) + { + if (!(channel is IBassAudioChannel bassChannel)) + return; + + if (bassChannel.Handle == 0) + return; + + activeChannels.Remove(bassChannel); + ManagedBass.Bass.ChannelPause(bassChannel.Handle); + } + + public void UpdateDevice(int deviceIndex) + { + } + + public BassFlags SampleFlags => BassFlags.Default; + public BassFlags TrackFlags => BassFlags.Default; + + public bool ChannelPlay(IBassAudioChannel channel, bool restart = false) + { + if (channel.Handle == 0) + return false; + + return ManagedBass.Bass.ChannelPlay(channel.Handle, restart); + } + + public bool ChannelPause(IBassAudioChannel channel, bool flushMixer = false) + => ManagedBass.Bass.ChannelPause(channel.Handle); + + public PlaybackState ChannelIsActive(IBassAudioChannel channel) + => ManagedBass.Bass.ChannelIsActive(channel.Handle); + + public long ChannelGetPosition(IBassAudioChannel channel, PositionFlags mode = PositionFlags.Bytes) + => ManagedBass.Bass.ChannelGetPosition(channel.Handle, mode); + + public bool ChannelSetPosition(IBassAudioChannel channel, long position, PositionFlags mode = PositionFlags.Bytes) + => ManagedBass.Bass.ChannelSetPosition(channel.Handle, position, mode); + + public bool ChannelGetLevel(IBassAudioChannel channel, float[] levels, float length, LevelRetrievalFlags flags) + => ManagedBass.Bass.ChannelGetLevel(channel.Handle, levels, length, flags); + + public int ChannelGetData(IBassAudioChannel channel, float[] buffer, int length) + => ManagedBass.Bass.ChannelGetData(channel.Handle, buffer, length); + + public int ChannelSetSync(IBassAudioChannel channel, SyncFlags type, long parameter, SyncProcedure procedure, IntPtr user = default) + => ManagedBass.Bass.ChannelSetSync(channel.Handle, type, parameter, procedure, user); + + public bool ChannelRemoveSync(IBassAudioChannel channel, int sync) + => ManagedBass.Bass.ChannelRemoveSync(channel.Handle, sync); + + public bool StreamFree(IBassAudioChannel channel) + { + ManagedBass.Bass.ChannelStop(channel.Handle); + return ManagedBass.Bass.StreamFree(channel.Handle); + } + + public void AddChannelToBassMix(IBassAudioChannel channel) + { + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + foreach (var channel in activeChannels.ToArray()) + ManagedBass.Bass.ChannelPause(channel.Handle); + activeChannels.Clear(); + } + } +} diff --git a/osu.Framework/Audio/Sample/SampleBass.cs b/osu.Framework/Audio/Sample/SampleBass.cs index 5052a2cd5e..42b4d559a0 100644 --- a/osu.Framework/Audio/Sample/SampleBass.cs +++ b/osu.Framework/Audio/Sample/SampleBass.cs @@ -12,9 +12,9 @@ internal sealed class SampleBass : Sample public override bool IsLoaded => factory.IsLoaded; private readonly SampleBassFactory factory; - private readonly BassAudioMixer mixer; + private readonly IBassAudioMixer mixer; - internal SampleBass(SampleBassFactory factory, BassAudioMixer mixer) + internal SampleBass(SampleBassFactory factory, IBassAudioMixer mixer) : base(factory.Name) { this.factory = factory; @@ -25,7 +25,7 @@ internal SampleBass(SampleBassFactory factory, BassAudioMixer mixer) protected override SampleChannel CreateChannel() { - var channel = new SampleChannelBass(this); + var channel = new SampleChannelBass(this, mixer.SampleFlags); mixer.Add(channel); return channel; } diff --git a/osu.Framework/Audio/Sample/SampleBassFactory.cs b/osu.Framework/Audio/Sample/SampleBassFactory.cs index 3b863a68b3..65ebb72568 100644 --- a/osu.Framework/Audio/Sample/SampleBassFactory.cs +++ b/osu.Framework/Audio/Sample/SampleBassFactory.cs @@ -32,12 +32,12 @@ internal class SampleBassFactory : AudioCollectionManager internal readonly Bindable PlaybackConcurrency = new Bindable(Sample.DEFAULT_CONCURRENCY); - private readonly BassAudioMixer mixer; + private readonly IBassAudioMixer mixer; private NativeMemoryTracker.NativeMemoryLease? memoryLease; private byte[]? data; - public SampleBassFactory(byte[] data, string name, BassAudioMixer mixer) + public SampleBassFactory(byte[] data, string name, IBassAudioMixer mixer) { this.data = data; this.mixer = mixer; diff --git a/osu.Framework/Audio/Sample/SampleChannelBass.cs b/osu.Framework/Audio/Sample/SampleChannelBass.cs index f08f5baa49..b8beb0ff4f 100644 --- a/osu.Framework/Audio/Sample/SampleChannelBass.cs +++ b/osu.Framework/Audio/Sample/SampleChannelBass.cs @@ -11,6 +11,7 @@ namespace osu.Framework.Audio.Sample internal sealed class SampleChannelBass : SampleChannel, IBassAudioChannel { private readonly SampleBass sample; + private readonly BassFlags creationFlags; private volatile int channel; /// @@ -66,10 +67,12 @@ public override bool Looping /// Creates a new . /// /// The to create the channel from. - public SampleChannelBass(SampleBass sample) + /// + public SampleChannelBass(SampleBass sample, BassFlags creationFlags) : base(sample.Name) { this.sample = sample; + this.creationFlags = creationFlags; relativeFrequencyHandler = new BassRelativeFrequencyHandler { @@ -199,7 +202,7 @@ private void ensureChannel() => EnqueueAction(() => if (hasChannel) return; - BassFlags flags = BassFlags.SampleChannelStream | BassFlags.Decode; + BassFlags flags = creationFlags; // While this shouldn't cause issues, we've had a small subset of users reporting issues on windows. // To keep things working let's only apply to other platforms until we know more. @@ -219,7 +222,7 @@ private void ensureChannel() => EnqueueAction(() => #region Mixing - private BassAudioMixer bassMixer => (BassAudioMixer)Mixer.AsNonNull(); + private IBassAudioMixer bassMixer => (IBassAudioMixer)Mixer.AsNonNull(); bool IBassAudioChannel.IsActive => IsAlive; @@ -227,7 +230,7 @@ private void ensureChannel() => EnqueueAction(() => bool IBassAudioChannel.MixerChannelPaused { get; set; } = true; - BassAudioMixer IBassAudioChannel.Mixer => bassMixer; + IBassAudioMixer IBassAudioChannel.Mixer => bassMixer; #endregion diff --git a/osu.Framework/Audio/Sample/SampleStore.cs b/osu.Framework/Audio/Sample/SampleStore.cs index a47a9f15af..6d7ce65644 100644 --- a/osu.Framework/Audio/Sample/SampleStore.cs +++ b/osu.Framework/Audio/Sample/SampleStore.cs @@ -49,7 +49,7 @@ public Sample Get(string name) this.LogIfNonBackgroundThread(name); byte[] data = store.Get(name); - factory = factories[name] = data == null ? null : new SampleBassFactory(data, name, (BassAudioMixer)mixer) { PlaybackConcurrency = { Value = PlaybackConcurrency } }; + factory = factories[name] = data == null ? null : new SampleBassFactory(data, name, (IBassAudioMixer)mixer) { PlaybackConcurrency = { Value = PlaybackConcurrency } }; if (factory != null) AddItem(factory); diff --git a/osu.Framework/Audio/Track/TrackBass.cs b/osu.Framework/Audio/Track/TrackBass.cs index 55088f81d8..9817f14b43 100644 --- a/osu.Framework/Audio/Track/TrackBass.cs +++ b/osu.Framework/Audio/Track/TrackBass.cs @@ -20,6 +20,7 @@ namespace osu.Framework.Audio.Track { public sealed class TrackBass : Track, IBassAudio, IBassAudioChannel { + private readonly BassFlags creationFlags; private Stream? dataStream; /// @@ -66,10 +67,12 @@ public sealed class TrackBass : Track, IBassAudio, IBassAudioChannel /// /// The sample data stream. /// A name identifying the track internally. + /// /// If true, the track will not be fully loaded, and should only be used for preview purposes. Defaults to false. - internal TrackBass(Stream data, string name, bool quick = false) + internal TrackBass(Stream data, string name, BassFlags creationFlags, bool quick = false) : base(name) { + this.creationFlags = creationFlags; ArgumentNullException.ThrowIfNull(data); relativeFrequencyHandler = new BassRelativeFrequencyHandler @@ -179,7 +182,7 @@ private int prepareStream(Stream data, bool quick) Bass.ChannelSetDevice(stream, bass_nodevice); tempoAdjustStream = BassFx.TempoCreate(stream, BassFlags.Decode | BassFlags.FxFreeSource); Bass.ChannelSetDevice(tempoAdjustStream, bass_nodevice); - stream = BassFx.ReverseCreate(tempoAdjustStream, 5f, BassFlags.Default | BassFlags.FxFreeSource | BassFlags.Decode); + stream = BassFx.ReverseCreate(tempoAdjustStream, 5f, BassFlags.Default | BassFlags.FxFreeSource | creationFlags); Bass.ChannelSetAttribute(stream, ChannelAttribute.TempoUseQuickAlgorithm, 1); Bass.ChannelSetAttribute(stream, ChannelAttribute.TempoOverlapMilliseconds, 4); @@ -439,7 +442,7 @@ protected override AudioMixer? Mixer } } - private BassAudioMixer bassMixer => (BassAudioMixer)Mixer.AsNonNull(); + private IBassAudioMixer bassMixer => (IBassAudioMixer)Mixer.AsNonNull(); bool IBassAudioChannel.IsActive => !IsDisposed; @@ -447,7 +450,7 @@ protected override AudioMixer? Mixer bool IBassAudioChannel.MixerChannelPaused { get; set; } = true; - BassAudioMixer IBassAudioChannel.Mixer => bassMixer; + IBassAudioMixer IBassAudioChannel.Mixer => bassMixer; #endregion diff --git a/osu.Framework/Audio/Track/TrackStore.cs b/osu.Framework/Audio/Track/TrackStore.cs index 495e3f0713..c5830f2371 100644 --- a/osu.Framework/Audio/Track/TrackStore.cs +++ b/osu.Framework/Audio/Track/TrackStore.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Audio.Mixing; +using osu.Framework.Audio.Mixing.Bass; using osu.Framework.IO.Stores; namespace osu.Framework.Audio.Track @@ -47,7 +48,7 @@ public Track Get(string name) if (dataStream == null) return null; - TrackBass trackBass = new TrackBass(dataStream, name); + TrackBass trackBass = new TrackBass(dataStream, name, ((IBassAudioMixer)mixer).TrackFlags); mixer.Add(trackBass); AddItem(trackBass);