mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-18 11:19:22 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
324 lines
6.8 KiB
C#
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;
|
|
}
|
|
}
|