Files
sbox-public/engine/Sandbox.Engine/Resources/Sound/SoundFile.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

324 lines
6.8 KiB
C#

using NativeEngine;
namespace Sandbox;
public enum SoundFormat : byte
{
PCM16 = 0,
PCM8,
MP3,
ADPCM,
};
/// <summary>
/// A sound resource.
/// </summary>
public partial class SoundFile : Resource, IValid
{
internal CSfxTable native;
internal VSound_t sound;
internal static Dictionary<string, SoundFile> Loaded = new();
/// <summary>
/// Ran when the file is reloaded/recompiled, etc.
/// </summary>
public Action OnSoundReloaded { get; set; }
/// <summary>
/// true if sound is loaded
/// </summary>
public bool IsLoaded => sound.IsValid;
/// <summary>
/// Format of the audio file.
/// </summary>
public SoundFormat Format => sound.format();
/// <summary>
/// Bits per each sample of this sound file.
/// </summary>
public int BitsPerSample => sound.BitsPerSample();
/// <summary>
/// Number of channels this audio file has.
/// </summary>
public int Channels => sound.channels();
/// <summary>
/// Bytes per each sample of this sound file.
/// </summary>
public int BytesPerSample => sound.BytesPerSample();
/// <summary>
/// Size of one sample, typically this would be "sample size * channel count", but can vary on audio format.
/// </summary>
public int SampleFrameSize => sound.m_sampleFrameSize();
/// <summary>
/// Sample rate of this sound file, per second.
/// </summary>
public int Rate => sound.m_rate();
/// <summary>
/// Duration of the sound this sound file contains, in seconds.
/// </summary>
public float Duration => sound.Duration();
public override bool IsValid => native.IsValid;
// Can be played
public bool IsValidForPlayback => IsValid && native.IsValidForPlayback();
private SoundFile( CSfxTable native )
{
if ( native.IsNull ) throw new Exception( "SoundFile pointer cannot be null!" );
this.native = native;
}
~SoundFile()
{
Dispose();
}
internal static void Init()
{
Shutdown();
Loaded = new Dictionary<string, SoundFile>();
}
internal static void Shutdown()
{
if ( Loaded != null )
{
foreach ( var file in Loaded.Values )
{
file.Dispose();
}
}
}
internal void Dispose()
{
native = default;
sound = default;
}
internal void OnReloadInternal()
{
SoundHandle.StopAll( native );
sound = default;
}
internal void OnReloadedInternal()
{
if ( native.IsValid )
sound = native.GetSound();
OnSoundReloaded?.Invoke();
}
/// <summary>
/// Load a new sound from disk. Includes automatic caching.
/// </summary>
/// <param name="filename">The file path to load the sound from.</param>
/// <returns>The loaded sound file, or null if failed.</returns>
public static SoundFile Load( string filename )
{
ThreadSafe.AssertIsMainThread( "SoundFile.Load" );
if ( !filename.EndsWith( ".vsnd", StringComparison.OrdinalIgnoreCase ) )
filename = System.IO.Path.ChangeExtension( filename, "vsnd" );
if ( Loaded.TryGetValue( filename, out var soundFile ) )
return soundFile;
if ( Mounting.Directory.TryLoad( filename, Mounting.ResourceType.Sound, out object sound ) && sound is SoundFile s )
return s;
var soundFilePointer = g_pSoundSystem.PrecacheSound( filename );
if ( soundFilePointer.IsNull )
return null;
soundFile = new SoundFile( soundFilePointer );
Loaded[filename] = soundFile;
soundFile.SetIdFromResourcePath( filename );
return soundFile;
}
/// <summary>
/// Load from PCM.
/// </summary>
internal static unsafe SoundFile Create( string filename, Span<byte> data, int channels, uint rate, int format, uint sampleCount, float duration, bool loop )
{
fixed ( byte* pData = data )
{
var sfx = g_pSoundSystem.CreateSound( filename, channels, (int)rate, format, (int)sampleCount, duration, loop, (IntPtr)pData, data.Length );
if ( sfx.IsNull )
return null;
var soundFile = new SoundFile( sfx );
Loaded[filename] = soundFile;
soundFile.SetIdFromResourcePath( filename );
g_pSoundSystem.PreloadSound( sfx );
return soundFile;
}
}
/// <summary>
/// Load from WAV.
/// </summary>
public static unsafe SoundFile FromWav( string filename, Span<byte> data, bool loop )
{
ThreadSafe.AssertIsMainThread( "SoundFile.FromWav" );
if ( !filename.EndsWith( ".vsnd", StringComparison.OrdinalIgnoreCase ) )
filename = System.IO.Path.ChangeExtension( filename, "vsnd" );
if ( Loaded.TryGetValue( filename, out var soundFile ) )
return soundFile;
if ( data.Length <= 0 )
throw new ArgumentException( "Invalid data" );
var soundData = SoundData.FromWav( data );
var pcmData = soundData.PCMData ?? throw new ArgumentException( "Invalid WAV" );
var format = 0;
if ( soundData.Format == 1 )
{
if ( soundData.BitsPerSample == 8 ) format = 1;
else if ( soundData.BitsPerSample == 16 ) format = 0;
}
else if ( soundData.Format == 2 )
{
format = 3;
}
return Create( filename, pcmData, soundData.Channels, soundData.SampleRate, format, soundData.SampleCount, soundData.Duration, loop );
}
// this is a fucking mess
// TODO: Document. What's the difference beetween preloading here and precaching in Load()? What does this do that Load() doesn't?
public async Task<bool> LoadAsync()
{
if ( native.IsNull ) return false;
if ( sound.IsValid ) return true;
g_pSoundSystem.PreloadSound( native );
RealTimeSince timeout = 0;
// We need to wait until the sound has loaded
while ( !native.IsValidForPlayback() )
{
await Task.Yield();
if ( !native.IsValid ) return false;
if ( timeout > 3 )
{
return false;
}
}
sound = native.GetSound();
return true;
}
public void Preload()
{
if ( native.IsNull ) return;
if ( sound.IsValid ) return;
g_pSoundSystem.PreloadSound( native );
sound = native.GetSound();
}
internal enum CacheStatus
{
NotLoaded = 0,
IsLoaded,
ErrorLoading,
};
/// <summary>
/// Request decompressed audio samples.
/// </summary>
public async Task<short[]> GetSamplesAsync()
{
if ( native.IsNull )
return null;
g_pSoundSystem.PreloadSound( native );
RealTimeSince timeout = 0;
// We need to wait until the sound has loaded before trying to load the source
while ( !native.IsValidForPlayback() )
{
await Task.Yield();
if ( timeout > 10 )
{
return null;
}
}
timeout = 0;
using ( var mixer = native.CreateMixer() )
{
// Failed to create mixer -> bail to prevent native crash
if ( mixer.IsNull ) return null;
while ( !mixer.IsReadyToMix() )
{
await Task.Yield();
if ( timeout > 10 )
{
return null;
}
continue;
}
return GetSamples();
}
}
unsafe short[] GetSamples()
{
int sampleCount = native.GetSampleCount();
if ( sampleCount == 0 )
{
return null;
}
// TODO: do something better than allocating an array each time?
var samples = new short[sampleCount];
fixed ( short* memory = &samples[0] )
{
if ( !native.GetSamples( (IntPtr)memory, (uint)sampleCount ) )
return null;
}
return samples;
}
}