269 lines
11 KiB
C#
269 lines
11 KiB
C#
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;
|
||
|
||
// 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;
|
||
}
|
||
}
|
||
} |