Files
sbox-public/engine/Sandbox.Engine/Systems/Audio/SoundHandle.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

541 lines
11 KiB
C#

using Sandbox.Audio;
using System.Collections.Concurrent;
using System.IO;
namespace Sandbox;
/// <summary>
/// A handle to a sound that is currently playing. You can use this to control the sound's position, volume, pitch etc.
/// </summary>
[Expose]
public partial class SoundHandle : IValid, IDisposable
{
static ConcurrentQueue<SoundHandle> removalQueue = new();
static ConcurrentQueue<SoundHandle> addQueue = new();
static HashSet<SoundHandle> active = new();
CSfxTable _sfx;
internal AudioSampler sampler;
int _ticks;
Transform _transform = Transform.Zero;
static SoundHandle _empty;
/// <summary>
/// RealTime that this sound was created
/// </summary>
internal float _CreatedTime;
/// <summary>
/// An empty, do nothing sound, that we can return to avoid NREs
/// </summary>
internal static SoundHandle Empty
{
get
{
if ( _empty is null )
{
_empty = new SoundHandle();
}
return _empty;
}
}
/// <summary>
/// Position of the sound.
/// </summary>
public Vector3 Position
{
get => _transform.Position;
set => _transform.Position = value;
}
/// <summary>
/// The direction the sound is facing
/// </summary>
public Rotation Rotation
{
get => _transform.Rotation;
set => _transform.Rotation = value;
}
/// <summary>
/// This sound's transform
/// </summary>
public Transform Transform
{
get => _transform;
set => _transform = value;
}
/// <summary>
/// Volume of the sound.
/// </summary>
public float Volume { get; set; } = 1.0f;
/// <summary>
/// A debug name to help identify the sound
/// </summary>
public string Name { get; set; }
/// <summary>
/// How 3d the sound should be. 0 means no 3d, 1 means fully
/// </summary>
[Range( 0, 1 )]
public float SpacialBlend { get; set; } = 1.0f;
/// <summary>
/// How many units the sound can be heard from.
/// </summary>
public float Distance { get; set; } = 15_000f;
/// <summary>
/// The falloff curve for the sound.
/// </summary>
public Curve Falloff { get; set; } = new Curve( new( 0, 1, 0, -1.8f ), new( 0.05f, 0.22f, 3.5f, -3.5f ), new( 0.2f, 0.04f, 0.16f, -0.16f ), new( 1, 0 ) );
/// <summary>
/// The fadeout curve for when the sound stops.
/// </summary>
public Curve Fadeout { get; set; } = new Curve( new( 0, 1 ), new( 1, 0 ) );
[Obsolete( "This is not used anymore" )]
public float Decibels { get; set; } = 70.0f;
/// <summary>
/// Pitch of the sound.
/// </summary>
public float Pitch { get; set; } = 1.0f;
/// <summary>
/// Whether the sound is currently playing or not.
/// </summary>
public bool IsPlaying => !IsStopped;
/// <summary>
/// Whether the sound is currently paused or not.
/// </summary>
public bool Paused { get; set; } = false;
/// <summary>
/// Sound is done
/// </summary>
public bool Finished { get; set; }
/// <summary>
/// Enable the sound reflecting off surfaces
/// </summary>
[System.Obsolete]
public bool Reflections { get; set; }
/// <summary>
/// Allow this sound to be occluded by geometry etc
/// </summary>
public bool Occlusion { get; set; } = true;
/// <summary>
/// The radius of this sound's occlusion, allow for partial occlusion
/// </summary>
public float OcclusionRadius { get; set; } = 32.0f;
/// <summary>
/// Should the sound fade out over distance
/// </summary>
public bool DistanceAttenuation { get; set; } = true;
/// <summary>
/// Should the sound get absorbed by air, so it sounds different at distance
/// </summary>
public bool AirAbsorption { get; set; } = true;
/// <summary>
/// Should the sound transmit through walls, doors etc
/// </summary>
public bool Transmission { get; set; } = true;
/// <summary>
/// Which mixer do we want to write to
/// </summary>
public Mixer TargetMixer { get; set; }
/// <summary>
/// How many samples per second?
/// </summary>
public int SampleRate { get; private init; }
/// <summary>
/// Keep playing silently for a second or two, to finish reverb effect
/// </summary>
internal RealTimeUntil TimeUntilFinished { get; set; }
/// <summary>
/// Keep playing until faded out
/// </summary>
internal RealTimeUntil TimeUntilFaded { get; set; }
/// <summary>
/// Have we started fading out?
/// </summary>
internal bool IsFading { get; set; }
/// <summary>
/// True if the sound has been stopped
/// </summary>
public bool IsStopped
{
get
{
if ( !IsValid ) return true;
return false;
}
}
[Obsolete( "Use Time instead" )]
public float ElapsedTime => Time;
/// <summary>
/// The current time of the playing sound in seconds.
/// Note: for some formats seeking may be expensive, and some may not support it at all.
/// </summary>
public float Time
{
get
{
if ( IsStopped ) return 0.0f;
if ( sampler is null ) return 0.0f;
return SampleRate > 0 ? sampler.SamplePosition / (float)SampleRate : 0.0f;
}
set
{
if ( sampler is null ) return;
sampler.SamplePosition = (int)(value * SampleRate);
}
}
public void Stop( float fadeTime = 0.0f )
{
if ( Finished || IsFading ) return;
if ( fadeTime > 0.0f )
{
TimeUntilFaded = fadeTime;
IsFading = true;
return;
}
Finished = true;
}
/// <summary>
/// Place the listener at 0,0,0 facing 1,0,0.
/// </summary>
public bool ListenLocal { get; set; }
/// <summary>
/// If true, then this sound won't be played unless voice_loopback is 1. The assumption is that it's the
/// local user's voice. Amplitude and visme data will still be available!
/// </summary>
public bool Loopback { get; set; }
/// <summary>
/// Measure of audio loudness.
/// </summary>
public float Amplitude { get; set; }
bool _destroyed = false;
internal readonly Scene Scene;
internal SoundHandle( CSfxTable soundHandle )
{
_sfx = soundHandle;
Scene = Game.ActiveScene;
SampleRate = _sfx.GetSound().m_rate();
TryCreateMixer();
addQueue.Enqueue( this );
_CreatedTime = RealTime.Now;
}
// an empty soundhandle
internal SoundHandle()
{
SampleRate = 48000;
_destroyed = true;
_CreatedTime = RealTime.Now;
}
~SoundHandle()
{
Dispose();
}
/// <summary>
/// Return true if this has no mixer specified, so will use the default mixer
/// </summary>
/// <returns></returns>
internal bool WantsDefaultMixer() => TargetMixer is null;
/// <summary>
/// Return true if we want to play on this mixer. Will return true if we have no
/// mixer specified, and the provided mixer is the default.
/// </summary>
internal bool IsTargettingMixer( Mixer mixer )
{
if ( _destroyed ) return false;
if ( WantsDefaultMixer() && Mixer.Default == mixer ) return true;
if ( TargetMixer is null ) return false;
if ( string.IsNullOrEmpty( mixer.Name ) ) return false;
// Compare names instead of mixers, because they may have deserialized etc
return TargetMixer.Name == mixer.Name;
}
public bool IsValid => !_destroyed;
void TryCreateMixer()
{
if ( sampler is not null )
return;
var ptr = _sfx.CreateMixer();
if ( ptr.IsNull )
{
// Did we fail because the resource failed to load? Just mark complete or get stuck in hell (feedback is provide on load failure)
if ( _sfx.FailedResourceLoad() )
{
Finished = true;
}
return;
}
sampler = new AudioSampler( ptr );
}
public void Dispose()
{
lock ( this )
{
if ( _destroyed )
return;
GC.SuppressFinalize( this );
_destroyed = true;
_sfx = default;
DisposeSources();
MainThread.QueueDispose( sampler );
sampler = null;
removalQueue.Enqueue( this );
if ( LipSync.Enabled )
LipSync.DisableLipSync();
}
}
/// <summary>
/// This is called on the main thread for all active voices
/// </summary>
void TickInternal()
{
if ( _destroyed )
return;
if ( Finished )
{
Dispose();
return;
}
UpdateFollower();
TryCreateMixer();
UpdateSources();
_ticks++;
}
/// <summary>
/// Called to push changes to a sound immediately, rather than waiting for the next tick.
/// You should call this if you make changes to a sound.
/// </summary>
[System.Obsolete( "This no longer needs to exist" )]
public void Update()
{
}
/// <summary>
/// Before we're added to the active list, we need to get some stuff straight
/// </summary>
void OnActive()
{
}
static void TickQueues()
{
while ( addQueue.TryDequeue( out var h ) )
{
h.OnActive();
active.Add( h );
}
while ( removalQueue.TryDequeue( out var h ) )
{
active.Remove( h );
}
}
static void TickVoices()
{
foreach ( var handle in active )
{
if ( !handle.IsValid ) continue;
handle.TickInternal();
}
}
internal static void TickAll()
{
lock ( active )
{
TickQueues();
TickVoices();
}
}
internal static void StopAll( float fade, Mixer mixer = null )
{
lock ( active )
{
TickQueues();
var handles = mixer is null ? active : active.Where( x => x.TargetMixer == mixer );
foreach ( var handle in handles )
{
if ( !handle.IsValid() ) continue;
handle.Stop( fade );
}
}
}
internal static void Shutdown()
{
lock ( active )
{
foreach ( var handle in active )
{
if ( !handle.IsValid() ) continue;
handle.Dispose();
}
}
}
internal static void StopAllWithParent( GameObject parent, float fade )
{
lock ( active )
{
TickQueues();
foreach ( var handle in active.Where( x => x.Parent == parent ) )
{
if ( !handle.IsValid() ) continue;
handle.Stop( fade );
}
}
}
internal static void StopAll( CSfxTable sfx )
{
lock ( active )
{
foreach ( var handle in active.Where( x => x._sfx == sfx ) )
{
if ( !handle.IsValid() ) continue;
handle.Stop();
}
}
}
internal static void FlushCreatedSounds()
{
lock ( active )
{
TickQueues();
}
}
public static void GetActive( List<SoundHandle> handles )
{
lock ( active )
{
foreach ( var handle in active )
{
if ( !handle.IsValid() ) continue;
if ( handle._ticks == 0 ) continue;
if ( handle.Paused ) continue;
handles.Add( handle );
}
}
}
[ConCmd( "list_sound_handles", Help = "Prints a summary of active sound handles to the console. Use \"list_sound_handles 2\" to see more info." )]
private static void LogActiveHandles( int level = 1 )
{
var handles = new List<SoundHandle>();
GetActive( handles );
var writer = new StringWriter();
var mixerGroups = handles
.GroupBy( x => x.TargetMixer )
.OrderBy( x => x.Key?.Name );
writer.WriteLine( $"Total active sound handles: {handles.Count}" );
writer.WriteLine();
foreach ( var mixerGroup in mixerGroups )
{
writer.WriteLine( $"Mixer \"{mixerGroup.Key?.Name ?? "Default"}\": {mixerGroup.Count()}" );
var soundGroups = mixerGroup.GroupBy( x => x.Name );
foreach ( var soundGroup in soundGroups )
{
writer.WriteLine( $" Sound \"{soundGroup.Key}\": {soundGroup.Count()}" );
if ( level < 2 ) continue;
foreach ( var soundHandle in soundGroup )
{
writer.WriteLine( $" Handle {soundHandle._sfx.self:x}:" );
writer.WriteLine( $" {nameof( IsPlaying )}: {soundHandle.IsPlaying}" );
writer.WriteLine( $" {nameof( Time )}: {soundHandle.Time}" );
writer.WriteLine( $" {nameof( Volume )}: {soundHandle.Volume}" );
writer.WriteLine( $" {nameof( Distance )}: {soundHandle.Distance}" );
writer.WriteLine( $" {nameof( ListenLocal )}: {soundHandle.ListenLocal}" );
writer.WriteLine( $" {nameof( Position )}: {soundHandle.Position}" );
}
}
writer.WriteLine();
}
Log.Info( writer.ToString() );
}
}