using SFML.Audio; using SFML.System; using System; namespace Car_simulation { public class EngineSound : SoundStream { // Audio properties - smaller buffer for less latency private const uint SAMPLE_RATE = 44100; private const ushort CHANNEL_COUNT = 2; // Stereo private const float BUFFER_DURATION = 0.05f; // 10ms instead of 50ms! // Engine sound properties - NO SMOOTHING for instant response private volatile float _currentRPM = 800f; // volatile for thread safety private volatile float _currentThrottle = 0f; private float _volume = 0.3f; private bool _isPlaying = false; // Harmonic series - DIRECT RPM TO FREQUENCY private float[] _harmonicRatios = { 1f, 2f, 4f, 6f }; private float[] _harmonicAmplitudes = { 1f, 0.3f, 0.1f, 0.05f }; private float[] _harmonicPhases = new float[4]; // Engine configuration - for direct RPM calculation public int CylinderCount { get; set; } = 4; public float FiringFrequencyMultiplier => CylinderCount / 2f; // 4-stroke engines // For RPM to frequency mapping private float _rpmToHzFactor; private Random _random = new Random(); public EngineSound() { Initialize(CHANNEL_COUNT, SAMPLE_RATE); // Calculate direct conversion factor // RPM to Hz: (RPM / 60) × (Cylinders / 2) for 4-stroke _rpmToHzFactor = (1f / 60f) * (CylinderCount / 2f); // Initialize phases for (int i = 0; i < _harmonicPhases.Length; i++) { _harmonicPhases[i] = (float)(_random.NextDouble() * 2 * Math.PI); } } // CALL THIS FROM YOUR PHYSICS THREAD - INSTANT UPDATE public void SetEngineState(float rpm, float throttle) { // NO LOCK, NO SMOOTHING - DIRECT ASSIGNMENT _currentRPM = rpm; _currentThrottle = throttle; // Volume based on throttle (instant) _volume = 0.1f + 0.4f * throttle; } public void StartSound() { if (!_isPlaying) { Play(); _isPlaying = true; } } public void StopSound() { if (_isPlaying) { Stop(); _isPlaying = false; } } protected override bool OnGetData(out short[] samples) { // SMALLER BUFFER: 10ms instead of 50ms int sampleCount = (int)(SAMPLE_RATE * BUFFER_DURATION) * 2; // *2 for stereo samples = new short[sampleCount]; // Get current values ONCE per buffer (not per sample) float rpm = _currentRPM; float throttle = _currentThrottle; float volume = _volume; // DIRECT RPM TO FREQUENCY - NO SMOOTHING float baseFrequency = rpm * _rpmToHzFactor; // (RPM/60) × (cylinders/2) // Pre-calculate harmonic frequencies float[] harmonicFrequencies = new float[_harmonicRatios.Length]; float[] phaseIncrements = new float[_harmonicRatios.Length]; for (int h = 0; h < _harmonicRatios.Length; h++) { harmonicFrequencies[h] = baseFrequency * _harmonicRatios[h]; phaseIncrements[h] = harmonicFrequencies[h] * 2f * MathF.PI / SAMPLE_RATE; } // Calculate roughness factor float roughness = 0.02f * throttle; // Generate sound for (int i = 0; i < sampleCount; i += 2) { float sampleValue = 0f; // Sum all harmonics for (int h = 0; h < _harmonicRatios.Length; h++) { sampleValue += MathF.Sin(_harmonicPhases[h]) * _harmonicAmplitudes[h]; _harmonicPhases[h] += phaseIncrements[h]; if (_harmonicPhases[h] > 2f * MathF.PI) _harmonicPhases[h] -= 2f * MathF.PI; } // Add roughness sampleValue += (float)(_random.NextDouble() * 2 - 1) * roughness; // Apply volume sampleValue *= volume; // Clamp and convert sampleValue = Math.Clamp(sampleValue, -1f, 1f); short sample = (short)(sampleValue * 32767); // Stereo samples[i] = sample; samples[i + 1] = sample; } return true; } protected override void OnSeek(Time timeOffset) { // Reset phases for (int i = 0; i < _harmonicPhases.Length; i++) { _harmonicPhases[i] = (float)(_random.NextDouble() * 2 * Math.PI); } } } }