using NativeEngine; namespace Sandbox; public enum SoundFormat : byte { PCM16 = 0, PCM8, MP3, ADPCM, }; /// /// A sound resource. /// public partial class SoundFile : Resource, IValid { internal CSfxTable native; internal VSound_t sound; internal static Dictionary Loaded = new(); /// /// Ran when the file is reloaded/recompiled, etc. /// public Action OnSoundReloaded { get; set; } /// /// true if sound is loaded /// public bool IsLoaded => sound.IsValid; /// /// Format of the audio file. /// public SoundFormat Format => sound.format(); /// /// Bits per each sample of this sound file. /// public int BitsPerSample => sound.BitsPerSample(); /// /// Number of channels this audio file has. /// public int Channels => sound.channels(); /// /// Bytes per each sample of this sound file. /// public int BytesPerSample => sound.BytesPerSample(); /// /// Size of one sample, typically this would be "sample size * channel count", but can vary on audio format. /// public int SampleFrameSize => sound.m_sampleFrameSize(); /// /// Sample rate of this sound file, per second. /// public int Rate => sound.m_rate(); /// /// Duration of the sound this sound file contains, in seconds. /// 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(); } 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(); } /// /// Load a new sound from disk. Includes automatic caching. /// /// The file path to load the sound from. /// The loaded sound file, or null if failed. 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; } /// /// Load from PCM. /// internal static unsafe SoundFile Create( string filename, Span 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; } } /// /// Load from WAV. /// public static unsafe SoundFile FromWav( string filename, Span 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 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, }; /// /// Request decompressed audio samples. /// public async Task 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; } }