-
Notifications
You must be signed in to change notification settings - Fork 0
Migration Guide
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.
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.
- Avoid using
WaveStreamon its own, use theStreamAudioProcessor - 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 |
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.
-
LoopingRawSampleProvider- theRawSourceSampleProvidernow has aLoopproperty -
LoopingWaveProvider- use theStreamAudioProcessorwithLoop = true - While
SampleProviderQueueisn't obsolete, prefer theAudioQueue
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:
-
BufferedSampleProviderinstead ofBuffer(double) -
SampleProviderQueueand callEnqueueinstead ofQueue(ISampleProvider)
Compare v1 and v2 examples. Notice the simplified code in v2.
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();
}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);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();
}
}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);
}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;
});
}- π Home
- πΌ Digital Audio Basics
- π Examples
- π¦ Supported Formats
- β¬οΈ Migrating from v1
- π AudioPlayer
- πΎ Short Clips
- πΏ Streaming From Disk
- ποΈ Speaker Groups
- π Sample Providers
- β»οΈ Pooling
- π© SendEngines
- π§ Personalizing Speakers
- π Monitoring Output
- βοΈ AudioQueue
- πΆ Mixer
- ποΈ ProcessorChain
Caution
v1 will be out of support soon.