Code/Vehicle/VehicleController.Sound.cs
using System.Collections.Generic;
using System;
using Sandbox;
using Sandbox.Audio;

namespace Meteor.VehicleTool.Vehicle;
public partial class VehicleController
{

	[Property, FeatureEnabled( "Sound", Icon = "audio" )] public bool UseAudio { get; set; } = true;
	[Property, Range( 0, 1 ), Feature( "Sound" )] public MixerHandle SoundMixer { get; set; } = Mixer.Master;

	[Property, Feature( "Sound" ), Group( "Engine" )] public GameObject SoundParent { get; set; }
	[Property, Feature( "Sound" ), Group( "Engine" )] public Dictionary<int, SoundFile> AcsendingSounds { get; set; }
	[Property, Feature( "Sound" ), Group( "Engine" )] public Dictionary<int, SoundFile> DecsendingSounds { get; set; }
	[Property, Range( 0, 1 ), Feature( "Sound" ), Group( "Engine" )] public float EngineVolume { get; set; } = 1f;

	[Property, Feature( "Sound" ), Group( "Transmission" )] public SoundFile GearUpSound { get; set; }
	[Property, Feature( "Sound" ), Group( "Transmission" )] public SoundFile GearDownSound { get; set; }
	[Property, Range( 0, 1 ), Feature( "Sound" ), Group( "Transmission" )] public float GearVolume { get; set; } = 1f;

	private Dictionary<int, SoundHandle> AcsendingSoundHandles { get; set; }
	private List<int> AcsendingSoundTimes { get; set; }
	private Dictionary<int, SoundHandle> DecsendingSoundHandles { get; set; }
	private List<int> DecsendingSoundTimes { get; set; }

	[Sync( SyncFlags.Interpolate )] private float SmoothValue { get; set; }
	[Sync( SyncFlags.Interpolate )] private float SmoothVolume { get; set; }

	private void PlayGearShift( SoundFile file )
	{
		if ( !UseAudio || !file.IsValid() )
			return;
		SoundHandle snd = Sound.PlayFile( file );
		snd.Volume = GearVolume;
		snd.TargetMixer = SoundMixer.GetOrDefault();
		snd.Occlusion = true;
		snd.Position = WorldPosition;
	}
	private void OnGearUp() => PlayGearShift( GearUpSound );
	private void OnGearDown() => PlayGearShift( GearDownSound );

	private void RemoveSounds()
	{
		if ( AcsendingSoundHandles is not null )
			foreach ( var item in AcsendingSoundHandles.Values )
				item.Stop();

		if ( DecsendingSoundHandles is not null )
			foreach ( var item in DecsendingSoundHandles.Values )
				item.Stop();
	}
	private void UpdateSound()
	{
		SmoothValue = SmoothValue * (1 - 0.2f) + Engine.OutputRPM * 0.2f;
		SmoothVolume = SmoothVolume * (1 - 0.1f) + Engine.ThrottlePosition * 0.1f;

		var isAcsending = Engine.ThrottlePosition != 0;
		var isRunning = Engine.IsRunning;
		var pos = WorldPosition;
		if ( SoundParent.IsValid() )
			pos = SoundParent.WorldPosition;

		for ( int n = 0; n < AcsendingSoundTimes.Count; n++ )
		{
			var time = AcsendingSoundTimes[n]; // this
			float min = (n == 0) ? -100000f : AcsendingSoundTimes[n - 1]; // prev
			float max = n == (AcsendingSoundTimes.Count - 1) ? 100000f : AcsendingSoundTimes[n + 1]; // next

			float c = MathM.Fade( SmoothValue, min - 10f, time, max + 10 );
			float vol = c * MathM.Map( SmoothVolume, 0f, 1f, 0.5f, 1f ) * EngineVolume;

			SoundHandle soundObject = AcsendingSoundHandles[time];
			soundObject.Volume = isRunning ? isAcsending ? vol : vol * 0.5f : 0f;
			soundObject.Pitch = SmoothValue / time;
			soundObject.Position = pos;
		}

		for ( int n = 0; n < DecsendingSoundTimes.Count; n++ )
		{
			var time = DecsendingSoundTimes[n]; // this
			float min = (n == 0) ? -100000f : DecsendingSoundTimes[n - 1]; // prev
			float max = n == (DecsendingSoundTimes.Count - 1) ? 100000f : DecsendingSoundTimes[n + 1]; // next

			float c = MathM.Fade( SmoothValue, min - 10f, time, max + 10 );
			float vol = c * MathM.Map( SmoothVolume, 0f, 1f, 0.5f, 1f ) * EngineVolume;

			SoundHandle soundObject = DecsendingSoundHandles[time];
			soundObject.Volume = isRunning ? isAcsending ? vol * 0.5f : vol : 0;
			soundObject.Pitch = SmoothValue / time;
			soundObject.Position = pos;
		}

	}

	private async void LoadSoundsAsync()
	{
		AcsendingSoundTimes = [];
		AcsendingSoundHandles = [];
		DecsendingSoundTimes = [];
		DecsendingSoundHandles = [];
		foreach ( KeyValuePair<int, SoundFile> item in AcsendingSounds )
		{
			await item.Value.LoadAsync();
			SoundHandle snd = Sound.PlayFile( item.Value );
			snd.Volume = 0;

			snd.TargetMixer = SoundMixer.GetOrDefault();
			snd.Occlusion = true;
			snd.Distance = 9000;
			snd.Falloff = new Curve( new Curve.Frame( 0f, 1f, 0f, -1.8f ), new Curve.Frame( 0.05f, 0.08f, 3.5f, -3.5f ), new Curve.Frame( 0.2f, 0.04f, 0.16f, -0.16f ), new Curve.Frame( 1f, 0f ) );
			AcsendingSoundTimes.Add( item.Key );
			AcsendingSoundHandles.Add( item.Key, snd );
		}

		foreach ( KeyValuePair<int, SoundFile> item in DecsendingSounds )
		{
			await item.Value.LoadAsync();
			SoundHandle snd = Sound.PlayFile( item.Value );
			snd.Volume = 0;

			snd.TargetMixer = SoundMixer.GetOrDefault();
			snd.Occlusion = true;
			DecsendingSoundTimes.Add( item.Key );
			DecsendingSoundHandles.Add( item.Key, snd );
		}
	}
}