using Godot; using System; public partial class ProceduralEngine : AudioStreamPlayer { private AudioStreamGeneratorPlayback _playback; private float _crankAngle = 0f; private int[] _firingOrder = Array.Empty(); private float[] _combustionEnvelope; // Persistent phases for engine order harmonics private float _phase05 = 0f, _phase1 = 0f, _phase2 = 0f, _phase4 = 0f; // Intake resonance filter state private float _intakeFilterState = 0f, _intakeFilterPrev = 0f; private float _currentIntakeResFreq = 100f; // smoothed resonance frequency private float _targetIntakeResFreq = 100f; // Noise generator state (simple LCG) private uint _noiseState = 123456789; public float CurrentRPM { get; set; } public float CurrentThrottle { get; set; } public float CurrentCombustionPower { get; set; } public float RumbleIntensity { get; set; } = 1.0f; // 0=off, 1=normal, >1=extra rumble private int _cylinderCount = 1; public int CylinderCount { get => _cylinderCount; set { if (value != _cylinderCount && value >= 1 && value <= 12) { _cylinderCount = value; UpdateFiringOrder(); } } } public string EngineLayout { get; set; } = "inline"; public bool IsCrossplane { get; set; } = true; // Tunable parameters public float BaseEngineVolume { get; set; } = 0.4f; public float IntakeRoarAmount { get; set; } = 0.25f; public float IntakeHissAmount { get; set; } = 0.08f; // reduced, now noise public float ExhaustRumbleAmount { get; set; } = 0.3f; public float ExhaustCrackleAmount { get; set; } = 0.1f; public override void _Ready() { Play(); _playback = (AudioStreamGeneratorPlayback)GetStreamPlayback(); PrecomputeCombustionEnvelope(); UpdateFiringOrder(); } private void UpdateFiringOrder() { // (unchanged – same as your code) if (_cylinderCount == 1) _firingOrder = new int[] { 0 }; else if (_cylinderCount == 2) _firingOrder = new int[] { 0, 1 }; else if (_cylinderCount == 3) _firingOrder = new int[] { 0, 2, 1 }; else if (_cylinderCount == 4) _firingOrder = new int[] { 0, 2, 3, 1 }; else if (_cylinderCount == 5) _firingOrder = new int[] { 0, 1, 3, 4, 2 }; else if (_cylinderCount == 6) _firingOrder = new int[] { 0, 4, 2, 5, 1, 3 }; else if (_cylinderCount == 8) { if (IsCrossplane && EngineLayout == "v") _firingOrder = new int[] { 0, 7, 3, 2, 5, 4, 6, 1 }; else if (!IsCrossplane && EngineLayout == "v") _firingOrder = new int[] { 0, 7, 1, 6, 3, 4, 2, 5 }; else if (EngineLayout == "flat") _firingOrder = new int[] { 0, 7, 1, 6, 3, 4, 2, 5 }; else { _firingOrder = new int[8]; for (int i = 0; i < 8; i++) _firingOrder[i] = i; } } else if (_cylinderCount == 10) _firingOrder = new int[] { 0, 9, 4, 6, 1, 7, 2, 8, 3, 5 }; else if (_cylinderCount == 12) _firingOrder = new int[] { 0, 11, 3, 8, 1, 10, 5, 6, 2, 9, 4, 7 }; else { _firingOrder = new int[_cylinderCount]; for (int i = 0; i < _cylinderCount; i++) _firingOrder[i] = i; } } private void PrecomputeCombustionEnvelope() { const int steps = 128; _combustionEnvelope = new float[steps]; float peakPhase = 0.26f; float decay = 12f; for (int i = 0; i < steps; i++) { float phase = (float)i / (steps - 1) * Mathf.Pi; if (phase < peakPhase) _combustionEnvelope[i] = phase / peakPhase; else _combustionEnvelope[i] = Mathf.Exp(-decay * (phase - peakPhase)); } } public override void _Process(double delta) => FillBuffer(); public void ResetAudioState() { _crankAngle = 0f; _phase05 = _phase1 = _phase2 = _phase4 = 0f; _intakeFilterState = _intakeFilterPrev = 0f; _currentIntakeResFreq = _targetIntakeResFreq = 100f; _noiseState = 123456789; } public void PrewarmBuffer() => FillBuffer(); // Simple white noise generator (xorshift style) private float GetNoise() { _noiseState ^= _noiseState << 13; _noiseState ^= _noiseState >> 17; _noiseState ^= _noiseState << 5; return (_noiseState % 65536) / 32768.0f - 1.0f; } private void FillBuffer() { if (_playback == null || _firingOrder.Length == 0) return; float sampleRate = ((AudioStreamGenerator)Stream).MixRate; float dt = (float)GetProcessDeltaTime(); int framesToFill = (int)(sampleRate * dt); int framesAvailable = _playback.GetFramesAvailable(); int pushCount = Mathf.Min(framesToFill, framesAvailable); float cycleRad = Mathf.Tau * 2f; float radPerSec = (CurrentRPM / 60f) * Mathf.Tau; float rpmNorm = Mathf.Clamp(CurrentRPM / 7000f, 0f, 1f); float loadNorm = Mathf.Clamp(CurrentCombustionPower / 150000f, 0f, 1f); float crankFreq = CurrentRPM / 60f; // Engine order frequencies and delta phases float deltaPhase05 = (crankFreq * 0.5f) / sampleRate * Mathf.Tau; float deltaPhase1 = crankFreq / sampleRate * Mathf.Tau; float deltaPhase2 = (crankFreq * 2f) / sampleRate * Mathf.Tau; float deltaPhase4 = (crankFreq * 4f) / sampleRate * Mathf.Tau; // Smooth intake resonance frequency to avoid filter pops _targetIntakeResFreq = Mathf.Clamp(crankFreq * (_cylinderCount / 2f) * 0.8f, 60f, 400f); _currentIntakeResFreq = Mathf.Lerp(_currentIntakeResFreq, _targetIntakeResFreq, 0.01f); float intakeOmega = 2f * Mathf.Pi * _currentIntakeResFreq / sampleRate; float intakeQ = 2.5f; float intakeAlpha = Mathf.Sin(intakeOmega) / (2f * intakeQ); float intakeA0 = 1f + intakeAlpha; float intakeA1 = -2f * Mathf.Cos(intakeOmega); float intakeA2 = 1f - intakeAlpha; float intakeB0 = intakeAlpha; float intakeB1 = 0; float intakeB2 = -intakeAlpha; // Low‑pass filter for noise (one‑pole, cutoff 2000 Hz) float noiseCutoff = 2000f / sampleRate; float noiseA = Mathf.Exp(-Mathf.Tau * noiseCutoff); float noiseB = 1f - noiseA; float filteredNoise = 0f; for (int i = 0; i < pushCount; i++) { float sample = 0f; // ---- 1. Combustion pulses (unchanged) ---- float clatterPulse = Mathf.Max(0, Mathf.Sin(_phase05)); float clatter = GetNoise() * Mathf.Pow(clatterPulse, 10.0f) * 0.02f; clatter *= (1.0f - rpmNorm * 0.5f); // Quieter as oil pressure/RPM rises sample += clatter; for (int idx = 0; idx < CylinderCount; idx++) { int cyl = _firingOrder[idx]; float cylinderOffset = (cycleRad / CylinderCount) * cyl; float jitter = GetNoise() * 0.02f; float cylPhase = (_crankAngle + cylinderOffset + jitter) % cycleRad; if (cylPhase < Mathf.Pi) { float t = cylPhase / Mathf.Pi; int envIdx = (int)(t * (_combustionEnvelope.Length - 1)); float pulse = _combustionEnvelope[envIdx]; float powerStroke = pulse * loadNorm * (0.8f + 0.2f * rpmNorm); sample += powerStroke; } // ---- 2. Intake pulses (per cylinder) ---- if (cylPhase > Mathf.Pi * 3f) { float t = (cylPhase - Mathf.Pi * 3f) / Mathf.Pi; float pulse = Mathf.Sin(t * Mathf.Pi); float throttleFactor = Mathf.Pow(CurrentThrottle, 0.5f); float airRush = pulse * throttleFactor * IntakeRoarAmount; // === FIXED HISS: filtered noise instead of pure sine === // Generate white noise, low‑pass filter it float rawNoise = GetNoise(); filteredNoise = noiseA * filteredNoise + noiseB * rawNoise; float hiss = filteredNoise * IntakeHissAmount * throttleFactor * (0.3f + 0.7f * rpmNorm); airRush += hiss; // =================================================== sample += airRush; } // ---- 3. Exhaust pulses (unchanged) ---- if (cylPhase >= Mathf.Pi && cylPhase < Mathf.Tau) { float t = (cylPhase - Mathf.Pi) / Mathf.Pi; float rumble = Mathf.Sin(t * Mathf.Pi * 4f) * (1f - t); float exhaustAmp = ExhaustRumbleAmount * (0.3f + 0.7f * rpmNorm); sample += rumble * exhaustAmp; bool overrun = CurrentThrottle < 0.1f && CurrentRPM > 2000f; if (overrun && t > 0.7f) { float crackle = Mathf.Sin(t * Mathf.Pi * 20f) * (1f - t) * 0.5f; sample += crackle * ExhaustCrackleAmount * rpmNorm; } } if (cylPhase < 0.1f) { sample += GetNoise() * 0.03f * loadNorm; } } sample = Mathf.Tanh(sample * (1.0f + loadNorm * 0.5f)); // ---- 4. Global intake resonance (unchanged, but with smoothed frequency) ---- float intakeResonance = Mathf.Sin(_crankAngle * (_currentIntakeResFreq / crankFreq) * 2f); float filteredRes = intakeB0 * intakeResonance + intakeB1 * _intakeFilterPrev + intakeB2 * _intakeFilterState - intakeA1 * _intakeFilterPrev - intakeA2 * _intakeFilterState; filteredRes /= intakeA0; _intakeFilterState = _intakeFilterPrev; _intakeFilterPrev = filteredRes; sample += filteredRes * IntakeRoarAmount * 0.5f * CurrentThrottle * rpmNorm; // ---- 5. Engine order harmonics (unchanged) ---- float roughnessFactor = (EngineLayout == "v" && IsCrossplane) ? 1.2f : 1.0f; float harmonic = 0f; harmonic += Mathf.Sin(_phase05) * 0.08f * rpmNorm * (1f - loadNorm) * roughnessFactor; harmonic += Mathf.Sin(_phase1) * 0.25f * rpmNorm; harmonic += Mathf.Sin(_phase2) * 0.12f * rpmNorm; harmonic += Mathf.Sin(_phase4) * 0.06f * rpmNorm; sample += harmonic; sample = Mathf.Clamp(sample * BaseEngineVolume, -0.95f, 0.95f); _playback.PushFrame(Vector2.One * sample); // Update phases _phase05 = (_phase05 + deltaPhase05) % Mathf.Tau; _phase1 = (_phase1 + deltaPhase1) % Mathf.Tau; _phase2 = (_phase2 + deltaPhase2) % Mathf.Tau; _phase4 = (_phase4 + deltaPhase4) % Mathf.Tau; _crankAngle = (_crankAngle + radPerSec / sampleRate) % cycleRad; } } }