Skip to content

Conversation

@Axwabo
Copy link
Owner

@Axwabo Axwabo commented Jan 3, 2026

This PR adds a new system to manage encapsulated resources, allowing for automatic disposal of resources without the consumer having to store and dispose of everything.

Users are strongly encouraged to migrate to this system instead of manually disposing of audio resources.

Several helper extension methods and properties have been added to simplify code.

There are many breaking changes, however, most functionality should work when using binaries compiled against v1.

Migration Guide

The new wiki and migration guide will be available soon. Important notes:

  • Resources are now managed by the AudioPlayer and audio processors. If you manage a resource yourself, pass false to methods with an isOwned parameter
    • Call WithUnmanagedProvider or WithProviderOwnership, or set OwnsProvider to control whether the AudioPlayer should dispose of the provider
  • Prefer using the IAudioProcessor interface and its existing implementations. See the Processors namespace
    • This interface extends ISampleProvider and IDisposable
    • Use the ProcessorChain class (also available with the ToCompatibleChain extension method) to stack effects while preserving public access to the original provider
    • Use the Mixer class instead of MixingSampleProvider
    • Use the AudioQueue class instead of ConcatenatingSampleProvider
    • Use the StreamAudioProcessor class instead of managing WaveStreams yourself. Factory methods are available via static extensions or the CreateAudioProcessor and TryCreateAudioProcessor classes
  • Many methods have been marked obsolete and will prevent projects from being built. The messages should mostly explain how to migrate each call
  • Call UseFile on an AudioPlayer to have it play a file and dispose of the stream when the provider changes or if the player is destroyed/pooled
  • AudioPlayer extension properties such as CurrentTime and TotalTime will work if the player uses a StreamAudioProcessor/RawSourceSampleProvider, or if there's a single mixer input with an aforementioned type
  • Check out the example changes below to see how some things can be migrated

Important

Obsolete members will be removed after the beta (when v2 is fully released).

Caution

Several AudioPlayer extension methods and properties rely on getting the single mixer input. This means that either the provider itself has to match, or if the provider is a Mixer, there should be exactly one input matching.
Mixer takes precedence over the AudioQueue in such scenarios.

Tip

Upgrade your project to C# 14 (optional, but encouraged). Add <LangVersion>14</LangVersion> into a PropertyGroup in your csproj.
You will need the .NET 10 SDK.

Example Changes

Music Playback

Previous version

using System.IO;
using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;
using SecretLabNAudio.Core.Extensions.Processors;
using SecretLabNAudio.Core.Processors;
using SecretLabNAudio.Core.Pools;

public static void PlayMusicOneShot(Vector3 position, string path)
{
    // if not using C# 14, the line below would use the StreamProcessorExtensions class
    if (!StreamAudioProcessor.TryCreateFromFile(path, out var processor))
        return;
    AudioPlayerPool.Rent(SpeakerSettings.Default, null, position)
        .Use(processor) // the processor is disposed when the player is pooled/destroyed
        .PoolOnEnd();
}
Lobby Music Player

Previous version

using System;
using System.IO;
using LabApi.Events.Handlers;
using LabApi.Loader;
using LabApi.Loader.Features.Plugins;
using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;
using SecretLabNAudio.Core.Pools;

public sealed class LobbyMusic : Plugin
{

    public override string Name => "LobbyMusic";
    public override string Description => "Plays a song in the lobby";
    public override string Author => "Author";
    public override Version Version => GetType().Assembly.GetName().Version;
    public override Version RequiredApiVersion { get; } = new(1, 0, 0);

    private AudioPlayer? _player;

    public override void Enable()
    {
        ServerEvents.WaitingForPlayers += PlayAudio;
        ServerEvents.RoundStarted += StopAudio;
    }

    public override void Disable()
    {
        StopAudio();
        ServerEvents.WaitingForPlayers -= PlayAudio;
        ServerEvents.RoundStarted -= StopAudio;
    }

    private void PlayAudio()
    {
        var path = Path.Combine(this.GetConfigDirectory().FullName, "lobby.mp3");
        var settings = SpeakerSettings.GloballyAudible with {Volume = 0.2f};
        _player = AudioPlayer.Create(AudioPlayerPool.NextAvailableId, settings).UseFile(path, true);
    }

    private void StopAudio()
    {
        if (_player != null)
            _player.Destroy();
    }

}
Soundtracks

Previous version (see the "Store implementation" section)

using LabApi.Features.Stores;
using LabApi.Features.Wrappers;
using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;
using SecretLabNAudio.Core.Pools;
using SecretLabNAudio.Core.SendEngines;

public sealed class SoundtrackStore : CustomDataStore<SoundtrackStore>
{

    private readonly AudioPlayer _player;

    public SoundtrackStore(Player owner) : base(owner) 
        => _player = AudioPlayer.Create(AudioPlayerPool.NextAvailableId, new SpeakerSettings
            {
                IsSpatial = false,
                MaxDistance = 1,
                Volume = 0.2f
            }, owner.GameObject!.transform) // audio player is destroyed with the owner
            .WithSendEngine(new SpecificPlayerSendEngine(owner));

    public void ChangeTrack(string path) => _player.UseFileSafe(path); // no exception

}
Personalization

Previous version

Note:
In this version, the player is still created even if the clip wasn't registered.
To use the provider manually obtained from the cache, call the WithUnmanagedProvider extension method.

using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;
using SecretLabNAudio.Core.Pools;
using UnityEngine;

private const float SpatialRange = 10;

private static readonly SpeakerSettings Near = new()
{
    IsSpatial = true,
    MinDistance = 2,
    MaxDistance = SpatialRange + 2,
    Volume = 1
};

private static readonly SpeakerSettings Far = new()
{
    IsSpatial = false,
    MaxDistance = float.MaxValue,
    Volume = 0.1f
};

public static void Beep(Vector3 position)
{
    var audioPlayer = AudioPlayer.Create(AudioPlayerPool.NextAvailableId, Far, null, position)
        .UseShortClip("beep", true)
        .WithLivePersonalizedSendEngine((player, _) => 
    {
        if (Vector3.Distance(player.Position, position) > SpatialRange)
            return null; // default (Far) settings
        return Near;
    });
}

Changes (breaking)

To reduce ambiguity and breaking changes, old methods have been kept, and the ones using new functionality have received different names.

  • Marked the following classes obsolete: LoopingRawSampleProvider LoopingWaveProvider ConditionalOneTimeDisposable
    • RawSourceSampleProvider now has a Loop property
    • Use the StreamAudioProcessor class and set the Loop property (or call WithLoop)
  • Marked SampleProviderExtensions/WaveProviderExtensions and their methods obsolete in favor of audio processors; if absolutely needed, most methods have been moved to SecretLabNAudio.Core.Extensions.Providers.NonProcessorExtensions
    • Queue - use the AudioQueue class
    • Buffer - use a ProcessorChain, or call the BufferedSampleProvider constructor
    • MixWith use a Mixer
  • Marked some AudioPlayerExtensions obsolete:
    • DisposeOnDestroy - encapsulate resources in an audio processor instead, or manage the disposal yourself
    • MixingSampleProvider-related methods - call the new Mix methods
    • WithProvider methods - use audio processors, or call WithUnmanagedProvider if the provider shouldn't be disposed automatically
    • Buffer - use a ProcessorChain
    • ProviderAs - call ImmediateProviderAs
  • Marked RawSampleProviderExtensions.Loop obsolete; call WithLoop
  • Marked WaveStreamExtensions.Loop obsolete; create a StreamAudioProcessor instead

Fixes

  • System.ValueTuple.dll is now included in releases (also as an embedded resource in SecretLabNAudio.dll), this fixes some .ogg files not being able to load
  • SpeakerPersonalization will no longer throw an exception when changing the speaker settings for a disconnected player
  • Speakers returned to the pool will now be unparented
  • ShortClipCache no longer sets the ClipName of the copy to the provided key
  • Fixed some docs

Additions

  • AudioPlayer properties: OwnsProvider AlwaysRead
  • AudioPlayer extension properties:
    • Queue gets the single AudioQueue input
    • Mixer safely casts the SampleProvider
    • CurrentTime TotalTime IsLooping (when there's a single ILoopable or ISeekable input)
  • AudioPlayer extension methods:
    • Resume WithoutProvider WithProviderOwnership
    • Generic methods SourceAs MasterAs SingleInputAs to extract a type of processor
    • Several new Mixer-related methods
    • Loop Restart (when there's a single ILoopable or ISeekable input)
    • Use methods to replace providers
    • Mix methods to add providers to the Mixer
    • WithLivePersonalizedSendEngine overload without the SpeakerPersonalization component (will use the attached component, or add one to the speaker if needed)
  • SpeakerPersonalization extension method ClearAllOverrides
  • Audio processors that dispose of encapsulated resources: AudioQueue Mixer ProcessorChain SampleProviderWrapper StreamAudioProcessor
  • Audio processor extensions namespace
  • CreateAudioProcessor and TryCreateAudioProcessor classes
  • ILoopable and ISeekable interfaces (implemented by StreamAudioProcessor and RawSourceSampleProvider, as well as the now obsolete LoopingRawSampleProvider)
    • LoopingWaveProvider (obsolete) implements ISeekable
  • Static SpeakerSettings.GloballyAudible property
  • Maximum duration support in ShortClipCache
  • ShortClipCache methods: AddAllFromDirectory GetSafe Get
  • WaveFormatExtensions methods: Time Matches
  • Added a README to the demo project

Note

Extension properties are only available in C# 14 (or newer).

Changes (non-breaking)

  • Simplified the release workflow
  • Changed global usings
  • Refactored (almost) all extension methods to be in extension blocks
  • Used XML documentation includes to reduce duplicated lines in some places
  • Simplified the AudioPlayer::OnDestroy Unity event
  • AudioPlayerExtensions.UnsetProviderOnEnd now sets the AlwaysRead property instead of subscribing to NoSamplesRead
  • Bumped LabAPI to 1.1.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants