fix: clean gitignore for Godot C#

This commit is contained in:
maxwes08
2026-04-17 12:38:11 +02:00
commit 3ac0b6b866
23 changed files with 841 additions and 0 deletions

269
Scripts/ProceduralEngine.cs Normal file
View File

@@ -0,0 +1,269 @@
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;
}
}
}