Skip to content

Migration Guide

Axwabo edited this page Feb 6, 2026 · 6 revisions

Migrate from v1 to v2

v2 introduced audio processors, making resource management easier. It's highly encouraged to use this system.

See the below headings for details.

Tip

For example changes, see this section

Tip

It's recommended to upgrade your project to C# 14 to use extension properties. Set the <LangVersion> property in your csproj.

No More Manual Disposal

It's important to close the file handle when you no longer use the resource. v1 placed this responsibility on the consumer. With v2 this has become easier, you should most likely avoid disposing of resources yourself.

Audio processors and the AudioPlayer encapsulate resources, and automatically dispose of them. This means that you no longer need to store the WaveStream when playing files.

v2 code
using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;

private AudioPlayer _player;

public void Play(string path) => _player.UseFile(path);
v1 code
using NAudio.Wave;
using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;
using SecretLabNAudio.Core.FileReading;

private WaveStream? _stream;

private AudioPlayer _player;

public void Play(string path)
{
    _stream?.Dispose():
    _stream = null;
    _player.SampleProvider = null;
    _stream = CreateAudioReader.Stream(path);
    _player.WithProvider(_stream);
}

If you still want to manage an audio resource yourself, pass isOwned: false to methods that this the parameter. To prevent the AudioPlayer from disposing of your provider, set OwnsProvider to false, or call the WithUnmanagedProvider or WithProviderOwnership extension methods.

Prefer Processors

  • Avoid using WaveStream on its own, use the StreamAudioProcessor
  • For mixing, use the Mixer
  • To play back providers one after another, use the AudioQueue
  • Don't nest providers, use the ProcessorChain

The below table shows which processors you should favor.

Provider Processor
WaveStream StreamAudioProcessor
MixingSampleProvider Mixer
ConcatenatingSampleProvider AudioQueue

Several AudioPlayer extension methods have been removed in favor of processors.

Important

Some new extensions differ in behavior from their old counterparts. See the remarks section in each method's documentation.

v1 method v2
AddMixerInput Mix
RemoveMixerInput Get the Mixer, call Remove
RemoveAllMixerInput RemoveMixerInpupts
RemoveMixerInputsByName RemoveNamedMixerInputs
AddMixerShortClip MixShortClipAnonymous
WithProvider See below
Buffer Create a ProcessorChain
ProviderAs ImmediateProviderAs
DisposeOnDestroy Use an audio processor

WithProvider

If you're supplying an audio processor, call Use - pass isOwned: true if you will manually dispose of the processor yourself.

If your provider is not an audio processor, call the WithUnmanagedProvider method.

Removed Providers

  • LoopingRawSampleProvider - the RawSourceSampleProvider now has a Loop property
  • LoopingWaveProvider - use the StreamAudioProcessor with Loop = true
  • While SampleProviderQueue isn't obsolete, prefer the AudioQueue

(Re)moved Provider Extensions

Tip

Instead of manually creating nested providers, prefer the ProcessorChain or other audio processors.

The methods in SampleProviderExtensions and WaveProviderExtensions have been moved to SecretLabNAudio.Core.Extensions.Providers.NonProcessorExtensions in order to prevent ambiguous matches when using the Extensions namespace.

If you still need to use the old methods, create a:

  • BufferedSampleProvider instead of Buffer(double)
  • SampleProviderQueue and call Enqueue instead of Queue(ISampleProvider)

Examples

Compare v1 and v2 examples. Notice the simplified code in v2.

Music Playback

When the playback ends, the audio player is pooled. Pooling counts as destroying (disables the component), so the stream will be disposed.

v1
using System.IO;
using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;
using SecretLabNAudio.Core.FileReading;
using SecretLabNAudio.Core.Pools;

public static void PlayMusicOneShot(Vector3 position, string path)
{
    if (!File.Exists(path) || !TryCreateAudioReader.Stream(path, out var stream))
        return;
    AudioPlayerPool.Rent(SpeakerSettings.Default, null, position)
        .WithProvider(stream)
        .PoolOnEnd()
        .DisposeOnDestroy(stream);
}
v2
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 (!StreamAudioProcessor.TryCreateFromFile(path, out var processor))
        return;
    AudioPlayerPool.Rent(SpeakerSettings.Default, null, position)
        .Use(processor)
        .PoolOnEnd();
}

Multiple Speakers

Tip

See also (v2): groups

v1

Simply create a SpeakerToy with the same controller ID as the AudioPlayer to mirror its output. The speaker(s) can be placed anywhere, and they can even have different settings.

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

public static AudioPlayer CloneOutput(this AudioPlayer player, IEnumerable<Vector3> positions)
{
    var settings = SpeakerSettings.From(player);
    foreach (var position in positions)
        SpeakerToyPool.Rent(player.Id, settings, null, position);
    return player;
}

var player = AudioPlayerPool.Rent(Config.Settings, position: Config.MainSpeakerLocation)
    .WithProvider(provider)
    .CloneOutput(Config.OtherSpeakerLocations);
v2
using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;

var player = AudioPlayerPool.Rent(Config.Settings, position: Config.MainSpeakerLocation)
    .Use(processor)
    .CloneOutput(Config.OtherSpeakerLocations);

Lobby Music Player

Caution

Do not use null-conditional operators (.?) with Unity objects. Why?

v1
using System.IO;
using LabApi.Events.Handlers;
using LabApi.Loader;
using LabApi.Loader.Features.Plugins;
using NAudio.Wave;
using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;
using SecretLabNAudio.Core.FileReading;
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;
    private WaveStream? _stream;
    
    public override void Enable()
    {
        ServerEvents.WaitingForPlayers += PlayAudio; 
        ServerEvents.RoundStarted += StopAudio; 
    }

    public override void Disable()
    {
        StopAudio();
        ServerEvents.WaitingForPlayers -= PlayAudio;
        ServerEvents.RoundStarted -= StopAudio;
    }
    
    private void PlayAudio()
    {
        _stream?.Dispose();
        var path = Path.Combine(this.GetConfigDirectory().FullName, "lobby.mp3");
        if (!File.Exists(path))
            return;
        _stream = CreateAudioReader.Stream(path);
        _player = AudioPlayer.Create(AudioPlayerPool.NextAvailableId, new SpeakerSettings 
        {
            IsSpatial = false,
            MinDistance = 10000,
            MaxDistance = 15000,
            Volume = 0.2f
        }).WithProvider(_stream.Loop());
    }
    
    private void StopAudio()
    {
        _stream?.Dispose();
        _stream = null;
        if (_player != null)
            _player.Destroy();
    }

}
v2
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, 1, 5);

    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

Register the data store on enable:

using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Events.Handlers;
using LabApi.Features.Stores;

public override void Enable()
{
    CustomDataStoreManager.RegisterStore<SoundtrackStore>();
    PlayerEvents.Joined += OnJoined;
}

private void OnJoined(PlayerJoinedEventArgs ev) => ev.Player.GetDataStore<SoundtrackStore>();
v1 store implementation

[!NOTE] When the new file does not exist, the current track will still be disposed, potentially causing exceptions in the AudioPlayer.

using System.IO;
using LabApi.Features.Stores;
using LabApi.Features.Wrappers;
using NAudio.Wave;
using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;
using SecretLabNAudio.Core.FileReading;
using SecretLabNAudio.Core.Pools;
using SecretLabNAudio.Core.SendEngines;

public sealed class SoundtrackStore : CustomDataStore<SoundtrackStore>
{

    private readonly AudioPlayer _player;

    private WaveStream? _currentTrack;

    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)
    {
        _currentTrack?.Dispose();
        _currentTrack = null;
        if (File.Exists(path) && TryCreateAudioReader.Stream(path, out _currentTrack))
            _player.WithProvider(_currentTrack.Loop());
    }

    protected override void OnInstanceDestroyed()
    {
        _currentTrack?.Dispose();
        _currentTrack = null;
    }

}
v2 store implementation

[!NOTE] The settings have been extracted to a static field for readability (and also to save a few nanoseconds :3).

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 static readonly SpeakerSettings Settings = new()
    {
        IsSpatial = false,
        MaxDistance = 1,
        Volume = 0.2f
    };

    private readonly AudioPlayer _player;

    public SoundtrackStore(Player owner) : base(owner) 
        => _player = AudioPlayer.Create(
            AudioPlayerPool.NextAvailableId,
            Settings,
            owner.GameObject!.transform  // destroy with the owner
        ).WithSendEngine(new SpecificPlayerSendEngine(owner));

    // no exception; the track won't change if the file isn't readable
    public void ChangeTrack(string path) => _player.UseFileSafe(path);

}

Personalization

This example makes the speaker 3D if the player is close to it.

v1
using SecretLabNAudio.Core;
using SecretLabNAudio.Core.Extensions;
using SecretLabNAudio.Core.FileReading;
using SecretLabNAudio.Core.Providers;
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)
{
    if (!ShortClipCache.TryGet("beep", out RawSourceSampleProvider? provider))
        return;
    var audioPlayer = AudioPlayer.Create(AudioPlayerPool.NextAvailableId, Far, null, position);
    var personalization = audioPlayer.AddPersonalization();
    audioPlayer.WithLivePersonalizedSendEngine(personalization, (player, current) => 
    {
        if (Vector3.Distance(player.Position, position) > SpatialRange)
            return null; // default (Far) settings
        return Near;
    })
    .WithProvider(provider.Loop());
}
v2

[!NOTE] In the v1 version, the player isn't created if the clip hasn't been loaded.

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 BeepLoop(Vector3 position)
{
    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;
    });
}

Getting Started

Playing Audio

Advanced

Audio Processors

v1 Guides

Caution

v1 will be out of support soon.

Clone this wiki locally