Files
Car-Physics-Godot/Scripts/ProceduralEngine.cs
2026-04-17 12:38:11 +02:00

269 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Godot;
using System;
public partial class ProceduralEngine : AudioStreamPlayer
{
private AudioStreamGeneratorPlayback _playback;
private float _crankAngle = 0f;
private int[] _firingOrder = Array.Empty<int>();
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;
// Lowpass filter for noise (onepole, 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, lowpass 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;
}
}
}