Skip to content

Commit

Permalink
Passthrough bass audio mixer
Browse files Browse the repository at this point in the history
  • Loading branch information
smoogipoo committed Aug 8, 2024
1 parent ad19871 commit 6754f20
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 18 deletions.
2 changes: 1 addition & 1 deletion osu.Framework/Audio/AudioManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 4 additions & 1 deletion osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace osu.Framework.Audio.Mixing.Bass
/// <summary>
/// Mixes together multiple <see cref="IAudioChannel"/> into one output via BASSmix.
/// </summary>
internal class BassAudioMixer : AudioMixer, IBassAudio
internal class BassAudioMixer : AudioMixer, IBassAudio, IBassAudioMixer
{
private readonly AudioManager? manager;

Expand Down Expand Up @@ -100,6 +100,9 @@ protected override void RemoveInternal(IAudioChannel channel)
removeChannelFromBassMix(bassChannel);
}

public BassFlags SampleFlags => BassFlags.SampleChannelStream | BassFlags.Decode;
public BassFlags TrackFlags => BassFlags.Decode;

/// <summary>
/// Plays a channel.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion osu.Framework/Audio/Mixing/Bass/IBassAudioChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ internal interface IBassAudioChannel : IAudioChannel
/// </summary>
bool MixerChannelPaused { get; set; }

new BassAudioMixer Mixer { get; }
new IBassAudioMixer Mixer { get; }
}
}
136 changes: 136 additions & 0 deletions osu.Framework/Audio/Mixing/Bass/IBassAudioMixer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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);

/// <summary>
/// Plays a channel.
/// </summary>
/// <remarks>See: <see cref="ManagedBass.Bass.ChannelPlay"/>.</remarks>
/// <param name="channel">The channel to play.</param>
/// <param name="restart">Restart playback from the beginning?</param>
/// <returns>
/// If successful, <see langword="true"/> is returned, else <see langword="false"/> is returned.
/// Use <see cref="ManagedBass.Bass.LastError"/> to get the error code.
/// </returns>
bool ChannelPlay(IBassAudioChannel channel, bool restart = false);

/// <summary>
/// Pauses a channel.
/// </summary>
/// <remarks>See: <see cref="ManagedBass.Bass.ChannelPause"/>.</remarks>
/// <param name="channel">The channel to pause.</param>
/// <param name="flushMixer">Set to <c>true</c> to make the pause take effect immediately.
/// <para>
/// This will change the timing of <see cref="BassAudioMixer.ChannelGetPosition"/>, so should be used sparingly.
/// </para>
/// </param>
/// <returns>
/// If successful, <see langword="true"/> is returned, else <see langword="false"/> is returned.
/// Use <see cref="ManagedBass.Bass.LastError"/> to get the error code.
/// </returns>
bool ChannelPause(IBassAudioChannel channel, bool flushMixer = false);

/// <summary>
/// Checks if a channel is active (playing) or stalled.
/// </summary>
/// <remarks>See: <see cref="ManagedBass.Bass.ChannelIsActive"/>.</remarks>
/// <param name="channel">The channel to get the state of.</param>
/// <returns><see cref="PlaybackState"/> indicating the state of the channel.</returns>
PlaybackState ChannelIsActive(IBassAudioChannel channel);

/// <summary>
/// Retrieves the playback position of a channel.
/// </summary>
/// <remarks>See: <see cref="ManagedBass.Bass.ChannelGetPosition"/>.</remarks>
/// <param name="channel">The channel to retrieve the position of.</param>
/// <param name="mode">How to retrieve the position.</param>
/// <returns>
/// If an error occurs, -1 is returned, use <see cref="ManagedBass.Bass.LastError"/> to get the error code.
/// If successful, the position is returned.
/// </returns>
long ChannelGetPosition(IBassAudioChannel channel, PositionFlags mode = PositionFlags.Bytes);

/// <summary>
/// Sets the playback position of a channel.
/// </summary>
/// <remarks>See: <see cref="ManagedBass.Bass.ChannelSetPosition"/>.</remarks>
/// <param name="channel">The <see cref="IBassAudioChannel"/> to set the position of.</param>
/// <param name="position">The position, in units determined by the <paramref name="mode"/>.</param>
/// <param name="mode">How to set the position.</param>
/// <returns>
/// If successful, then <see langword="true"/> is returned, else <see langword="false"/> is returned.
/// Use <see cref="P:ManagedBass.Bass.LastError"/> to get the error code.
/// </returns>
bool ChannelSetPosition(IBassAudioChannel channel, long position, PositionFlags mode = PositionFlags.Bytes);

/// <summary>
/// Retrieves the level (peak amplitude) of a channel.
/// </summary>
/// <remarks>See: <see cref="ManagedBass.Bass.ChannelGetLevel(int, float[], float, LevelRetrievalFlags)"/>.</remarks>
/// <param name="channel">The <see cref="IBassAudioChannel"/> to get the levels of.</param>
/// <param name="levels">The array in which the levels are to be returned.</param>
/// <param name="length">How much data (in seconds) to look at to get the level (limited to 1 second).</param>
/// <param name="flags">What levels to retrieve.</param>
/// <returns><c>true</c> if successful, false otherwise.</returns>
bool ChannelGetLevel(IBassAudioChannel channel, [In, Out] float[] levels, float length, LevelRetrievalFlags flags);

/// <summary>
/// Retrieves the immediate sample data (or an FFT representation of it) of a channel.
/// </summary>
/// <remarks>See: <see cref="ManagedBass.Bass.ChannelGetData(int, float[], int)"/>.</remarks>
/// <param name="channel">The <see cref="IBassAudioChannel"/> to retrieve the data of.</param>
/// <param name="buffer">float[] to write the data to.</param>
/// <param name="length">Number of bytes wanted, and/or <see cref="T:ManagedBass.DataFlags"/>.</param>
/// <returns>If an error occurs, -1 is returned, use <see cref="P:ManagedBass.Bass.LastError"/> to get the error code.
/// <para>When requesting FFT data, the number of bytes read from the channel (to perform the FFT) is returned.</para>
/// <para>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 <see cref="F:ManagedBass.DataFlags.Float"/> or DataFlags.Fixed flag).</para>
/// <para>When using the <see cref="F:ManagedBass.DataFlags.Available"/> flag, the number of bytes in the channel's buffer is returned.</para>
/// </returns>
int ChannelGetData(IBassAudioChannel channel, float[] buffer, int length);

/// <summary>
/// Sets up a synchroniser on a mixer source channel.
/// </summary>
/// <remarks>See: <see cref="BassMix.ChannelSetSync(int, SyncFlags, long, SyncProcedure, IntPtr)"/>.</remarks>
/// <param name="channel">The <see cref="IBassAudioChannel"/> to set up the synchroniser for.</param>
/// <param name="type">The type of sync.</param>
/// <param name="parameter">The sync parameters, depending on the sync type.</param>
/// <param name="procedure">The callback function which should be invoked with the sync.</param>
/// <param name="user">User instance data to pass to the callback function.</param>
/// <returns>If successful, then the new synchroniser's handle is returned, else 0 is returned. Use <see cref="P:ManagedBass.Bass.LastError" /> to get the error code.</returns>
int ChannelSetSync(IBassAudioChannel channel, SyncFlags type, long parameter, SyncProcedure procedure, IntPtr user = default);

/// <summary>
/// Removes a synchroniser from a mixer source channel.
/// </summary>
/// <param name="channel">The <see cref="IBassAudioChannel"/> to remove the synchroniser for.</param>
/// <param name="sync">Handle of the synchroniser to remove (return value of a previous <see cref="M:ManagedBass.Mix.BassMix.ChannelSetSync(System.Int32,ManagedBass.SyncFlags,System.Int64,ManagedBass.SyncProcedure,System.IntPtr)" /> call).</param>
/// <returns>If successful, <see langword="true" /> is returned, else <see langword="false" /> is returned. Use <see cref="P:ManagedBass.Bass.LastError" /> to get the error code.</returns>
bool ChannelRemoveSync(IBassAudioChannel channel, int sync);

/// <summary>
/// Frees a channel's resources.
/// </summary>
/// <param name="channel">The <see cref="IBassAudioChannel"/> to free.</param>
/// <returns>If successful, <see langword="true" /> is returned, else <see langword="false" /> is returned. Use <see cref="P:ManagedBass.Bass.LastError" /> to get the error code.</returns>
bool StreamFree(IBassAudioChannel channel);

/// <summary>
/// Adds a channel to the native BASS mix.
/// </summary>
void AddChannelToBassMix(IBassAudioChannel channel);
}
}
115 changes: 115 additions & 0 deletions osu.Framework/Audio/Mixing/Bass/PassthroughBassAudioMixer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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<IBassAudioChannel> activeChannels = new List<IBassAudioChannel>();

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();
}
}
}
6 changes: 3 additions & 3 deletions osu.Framework/Audio/Sample/SampleBass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions osu.Framework/Audio/Sample/SampleBassFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ internal class SampleBassFactory : AudioCollectionManager<AdjustableAudioCompone
/// </summary>
internal readonly Bindable<int> PlaybackConcurrency = new Bindable<int>(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;
Expand Down
11 changes: 7 additions & 4 deletions osu.Framework/Audio/Sample/SampleChannelBass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
Expand Down Expand Up @@ -66,10 +67,12 @@ public override bool Looping
/// Creates a new <see cref="SampleChannelBass"/>.
/// </summary>
/// <param name="sample">The <see cref="SampleBass"/> to create the channel from.</param>
public SampleChannelBass(SampleBass sample)
/// <param name="creationFlags"></param>
public SampleChannelBass(SampleBass sample, BassFlags creationFlags)
: base(sample.Name)
{
this.sample = sample;
this.creationFlags = creationFlags;

relativeFrequencyHandler = new BassRelativeFrequencyHandler
{
Expand Down Expand Up @@ -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.
Expand All @@ -219,15 +222,15 @@ private void ensureChannel() => EnqueueAction(() =>

#region Mixing

private BassAudioMixer bassMixer => (BassAudioMixer)Mixer.AsNonNull();
private IBassAudioMixer bassMixer => (IBassAudioMixer)Mixer.AsNonNull();

bool IBassAudioChannel.IsActive => IsAlive;

int IBassAudioChannel.Handle => channel;

bool IBassAudioChannel.MixerChannelPaused { get; set; } = true;

BassAudioMixer IBassAudioChannel.Mixer => bassMixer;
IBassAudioMixer IBassAudioChannel.Mixer => bassMixer;

#endregion

Expand Down
2 changes: 1 addition & 1 deletion osu.Framework/Audio/Sample/SampleStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 6754f20

Please sign in to comment.