insane engine sound
This commit is contained in:
@@ -17,34 +17,48 @@ namespace FluidSim.Core
|
||||
private double ambientPressure = 101325.0;
|
||||
private double time;
|
||||
|
||||
// Crankshaft
|
||||
private double crankAngle = 0.0;
|
||||
private const double TargetRPM = 4000.0;
|
||||
private double angularVelocity;
|
||||
// ---- 4‑stroke cycle angle (0 … 4π) ----
|
||||
private double cycleCrankAngle = 0.0; // 0 to 4π, then resets
|
||||
private const double TargetRPM = 1000.0;
|
||||
private double angularVelocity; // rad/s of crankshaft
|
||||
|
||||
// Combustion
|
||||
private const double CombustionPressure = 8.0 * 101325.0;
|
||||
private const double CombustionTemperature = 1800.0;
|
||||
// ---- Engine geometry ----
|
||||
private double bore = 0.065; // 65 mm
|
||||
private double stroke = 0.0565; // 56.5 mm → 250 cc
|
||||
private double conRodLength = 0.113; // roughly 2 * stroke
|
||||
private double compressionRatio = 10.0;
|
||||
private double V_disp; // displacement volume
|
||||
private double V_clear; // clearance volume
|
||||
|
||||
// Valve timing
|
||||
private const double ValveOpenStart = 120.0 * Math.PI / 180.0;
|
||||
private const double ValveOpenEnd = 480.0 * Math.PI / 180.0;
|
||||
private const double ValveRampWidth = 30.0 * Math.PI / 180.0;
|
||||
// ---- Combustion ----
|
||||
private const double CombustionPressure = 50.0 * 101325.0;
|
||||
private const double CombustionTemperature = 2500.0;
|
||||
private bool burnInProgress = false;
|
||||
private double burnStartAngle; // cycle angle when ignition began
|
||||
private const double BurnDurationDeg = 40.0;
|
||||
private const double BurnDurationRad = BurnDurationDeg * Math.PI / 180.0;
|
||||
private double targetBurnEnergy;
|
||||
private double totalBurnMass;
|
||||
|
||||
// Pre‑ignition state (compressed fresh charge) for misfire restoration
|
||||
private double preIgnitionMass;
|
||||
private double preIgnitionInternalEnergy;
|
||||
|
||||
// ---- Valve timing ----
|
||||
private const double ValveOpenStart = 120.0 * Math.PI / 180.0; // 120° after TDC power
|
||||
private const double ValveOpenEnd = 480.0 * Math.PI / 180.0; // 480° ≈ 120° after TDC exhaust
|
||||
private const double ValveRampWidth = 30.0 * Math.PI / 180.0; // 30° ramps
|
||||
|
||||
private double maxOrificeArea;
|
||||
|
||||
// Misfire
|
||||
// ---- Misfire ----
|
||||
private Random rand = new Random();
|
||||
private const double MisfireProbability = 0.02;
|
||||
private bool isMisfiring = false;
|
||||
|
||||
// Low‑pass filter for pressure
|
||||
private double lastFilteredPressure;
|
||||
private const double PressureCutoffHz = 50.0;
|
||||
|
||||
// Logging
|
||||
// ---- Logging ----
|
||||
private int stepCount = 0;
|
||||
private const int LogStepInterval = 1000;
|
||||
private const int LogStepInterval = 10000;
|
||||
private int combustionCount = 0;
|
||||
private int misfireCount = 0;
|
||||
|
||||
@@ -53,25 +67,29 @@ namespace FluidSim.Core
|
||||
dt = 1.0 / sampleRate;
|
||||
angularVelocity = TargetRPM * 2.0 * Math.PI / 60.0;
|
||||
|
||||
// Cylinder: 0.5 litre, initially at ambient
|
||||
double cylVolume = 0.5e-3;
|
||||
double initialPressure = ambientPressure;
|
||||
double initialTemperature = 300.0;
|
||||
cylinder = new Volume0D(cylVolume, initialPressure, initialTemperature, sampleRate)
|
||||
// Displacement volume
|
||||
V_disp = (Math.PI / 4.0) * bore * bore * stroke;
|
||||
V_clear = V_disp / (compressionRatio - 1.0);
|
||||
|
||||
// Cylinder (starts at TDC clearance volume with compressed ambient charge)
|
||||
double initialPressure = ambientPressure * Math.Pow(compressionRatio, 1.4); // isentropic compression
|
||||
double initialTemperature = 300.0 * Math.Pow(compressionRatio, 1.4 - 1.0);
|
||||
double initialVolume = V_clear;
|
||||
cylinder = new Volume0D(initialVolume, initialPressure, initialTemperature, sampleRate)
|
||||
{
|
||||
Gamma = 1.4,
|
||||
GasConstant = 287.0
|
||||
};
|
||||
|
||||
// Exhaust pipe: length 2.5 m, radius 2 cm
|
||||
// Exhaust pipe (2.5 m long, 3 cm radius)
|
||||
double pipeLength = 2.5;
|
||||
double pipeRadius = 0.02;
|
||||
double pipeRadius = 0.03;
|
||||
double pipeArea = Math.PI * pipeRadius * pipeRadius;
|
||||
maxOrificeArea = pipeArea;
|
||||
exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 70);
|
||||
exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 100);
|
||||
exhaustPipe.SetUniformState(1.225, 0.0, ambientPressure);
|
||||
|
||||
// Coupling (valve starts closed)
|
||||
// Coupling (valve initially closed)
|
||||
coupling = new PipeVolumeConnection(cylinder, exhaustPipe, true, orificeArea: 0.0);
|
||||
|
||||
solver = new Solver();
|
||||
@@ -79,134 +97,218 @@ namespace FluidSim.Core
|
||||
solver.AddVolume(cylinder);
|
||||
solver.AddPipe(exhaustPipe);
|
||||
solver.AddConnection(coupling);
|
||||
// Use ZeroPressureOpen for strong reflections
|
||||
solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.ZeroPressureOpen, ambientPressure);
|
||||
// Open end with characteristic radiation (softer reflections)
|
||||
solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.OpenEnd, ambientPressure);
|
||||
|
||||
// Sound processor (tuned to pipe length)
|
||||
// Sound processor (keep your carefully tuned gains)
|
||||
soundProcessor = new SoundProcessor(sampleRate, pipeLength, pipeRadius * 2, reverbTimeMs: 200.0f);
|
||||
soundProcessor.MasterGain = 0.02f; // boosted from 0.0008
|
||||
soundProcessor.PressureGain = 4.0f; // boosted from6 0.12
|
||||
soundProcessor.TurbulenceGain = 0.0002f; // reduced from 0.02
|
||||
soundProcessor.MasterGain = 0.0002f;
|
||||
soundProcessor.PressureGain = 10.0f;
|
||||
soundProcessor.TurbulenceGain = 0.00005f;
|
||||
soundProcessor.SetAmbientPressure(ambientPressure);
|
||||
|
||||
lastFilteredPressure = ambientPressure;
|
||||
|
||||
Console.WriteLine("=== EngineScenario (ZeroPressureOpen, boosted gains) ===");
|
||||
// Log startup info
|
||||
Console.WriteLine("=== EngineScenario (improved physics) ===");
|
||||
Console.WriteLine($"Target RPM: {TargetRPM}, Misfire prob: {MisfireProbability * 100:F1}%");
|
||||
Console.WriteLine($"Bore x Stroke: {bore*1000:F0} x {stroke*1000:F0} mm, CR: {compressionRatio:F1}");
|
||||
Console.WriteLine($"Pipe length: {pipeLength} m, fundamental: {340/(4*pipeLength):F1} Hz");
|
||||
Console.WriteLine($"Valve opens at {ValveOpenStart*180/Math.PI:F0}°, closes at {ValveOpenEnd*180/Math.PI:F0}°, ramp {ValveRampWidth*180/Math.PI:F0}°");
|
||||
Console.WriteLine($"Sample rate: {sampleRate} Hz, dt = {dt*1000:F3} ms");
|
||||
Console.WriteLine("Time[s] Crank[°] Valve[%] MassFlow[kg/s] Comb# Misfire");
|
||||
Console.WriteLine("---------------------------------------------------------");
|
||||
Console.WriteLine($"Combustion: {CombustionPressure/101325:F0} bar, {CombustionTemperature} K");
|
||||
Console.WriteLine($"Valve opens at {ValveOpenStart*180/Math.PI:F0}°, closes at {ValveOpenEnd*180/Math.PI:F0}° (ramp {ValveRampWidth*180/Math.PI:F0}°)");
|
||||
Console.WriteLine($"Burn duration: {BurnDurationDeg}°");
|
||||
Console.WriteLine("Time[s] Crank[°] Vol[cc] MassFlow[kg/s] Comb# Misfire");
|
||||
Console.WriteLine("-------------------------------------------------------------");
|
||||
}
|
||||
|
||||
private double ValveOpenRatio(double crankRad)
|
||||
// ---- Piston volume & derivative ----
|
||||
private (double volume, double dvdt) PistonKinematics(double theta)
|
||||
{
|
||||
double cycleAngle = crankRad % (4.0 * Math.PI);
|
||||
double openStart = ValveOpenStart;
|
||||
double openEnd = ValveOpenEnd;
|
||||
// theta = crankshaft angle (0 at TDC of power stroke)
|
||||
double R = stroke / 2.0;
|
||||
double cosT = Math.Cos(theta);
|
||||
double sinT = Math.Sin(theta);
|
||||
double L = conRodLength;
|
||||
|
||||
if (cycleAngle < openStart || cycleAngle > openEnd)
|
||||
// Slider‑crank position relative to TDC
|
||||
double s = R * (1 - cosT) + L - Math.Sqrt(L * L - R * R * sinT * sinT);
|
||||
double V = V_clear + (Math.PI / 4.0) * bore * bore * s;
|
||||
|
||||
// Derivative dV/dθ
|
||||
double sqrtTerm = Math.Sqrt(L * L - R * R * sinT * sinT);
|
||||
double dVdθ = (Math.PI / 4.0) * bore * bore * (R * sinT + (R * R * sinT * cosT) / sqrtTerm);
|
||||
|
||||
double dvdt = dVdθ * angularVelocity; // rad/s → volume change rate
|
||||
return (V, dvdt);
|
||||
}
|
||||
|
||||
// ---- Valve lift (trapezoidal) ----
|
||||
private double ValveOpenRatio(double cycleRad)
|
||||
{
|
||||
// cycleRad: 0 … 4π
|
||||
if (cycleRad < ValveOpenStart || cycleRad > ValveOpenEnd)
|
||||
return 0.0;
|
||||
|
||||
double fullOpenWindow = openEnd - openStart;
|
||||
double closedWindow = 2.0 * ValveRampWidth;
|
||||
if (fullOpenWindow <= closedWindow)
|
||||
return 1.0;
|
||||
double duration = ValveOpenEnd - ValveOpenStart;
|
||||
double ramp = ValveRampWidth;
|
||||
|
||||
double tmid = (openStart + openEnd) / 2.0;
|
||||
double dist = Math.Abs(cycleAngle - tmid);
|
||||
double rampHalf = (fullOpenWindow - closedWindow) / 2.0;
|
||||
if (dist <= rampHalf)
|
||||
return 1.0;
|
||||
double t = (cycleRad - ValveOpenStart) / duration;
|
||||
|
||||
if (t < ramp / duration)
|
||||
return t / (ramp / duration);
|
||||
else if (t > 1.0 - ramp / duration)
|
||||
return (1.0 - t) / (ramp / duration);
|
||||
else
|
||||
{
|
||||
double frac = (dist - rampHalf) / ValveRampWidth;
|
||||
frac = Math.Clamp(frac, 0.0, 1.0);
|
||||
double lift = Math.Cos(frac * Math.PI / 2.0);
|
||||
return lift * lift;
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// ---- Wiebe burn fraction ----
|
||||
private double WiebeFraction(double angleFromIgnition)
|
||||
{
|
||||
if (angleFromIgnition >= BurnDurationRad) return 1.0;
|
||||
double a = 5.0, m = 2.0;
|
||||
double x = angleFromIgnition / BurnDurationRad;
|
||||
return 1.0 - Math.Exp(-a * Math.Pow(x, m + 1));
|
||||
}
|
||||
|
||||
public override float Process()
|
||||
{
|
||||
// Update crank angle
|
||||
crankAngle += angularVelocity * dt;
|
||||
if (crankAngle >= 2.0 * Math.PI)
|
||||
// Advance cycle crank angle
|
||||
cycleCrankAngle += angularVelocity * dt;
|
||||
if (cycleCrankAngle >= 4.0 * Math.PI) // 720°
|
||||
{
|
||||
crankAngle -= 2.0 * Math.PI;
|
||||
cycleCrankAngle -= 4.0 * Math.PI;
|
||||
isMisfiring = rand.NextDouble() < MisfireProbability;
|
||||
}
|
||||
|
||||
// Power stroke at TDC
|
||||
if (crankAngle < angularVelocity * dt && crankAngle >= 0.0)
|
||||
{
|
||||
// ---- Prepare cylinder for new power stroke ----
|
||||
// Fill cylinder with fresh charge at BDC, then compress isentropically to TDC clearance volume.
|
||||
double T_bdc = 300.0; // intake temperature
|
||||
double p_bdc = ambientPressure; // intake pressure
|
||||
double V_bdc = V_clear + V_disp; // volume at BDC (intake valve closing)
|
||||
double freshMass = p_bdc * V_bdc / (287.0 * T_bdc);
|
||||
double freshInternalEnergy = p_bdc * V_bdc / (1.4 - 1.0);
|
||||
|
||||
// Compress isentropically to V_clear
|
||||
double V1 = V_bdc, V2 = V_clear;
|
||||
double gamma = 1.4;
|
||||
double p2 = p_bdc * Math.Pow(V1 / V2, gamma);
|
||||
double T2 = T_bdc * Math.Pow(V1 / V2, gamma - 1);
|
||||
|
||||
// Set compressed state
|
||||
cylinder.Volume = V_clear;
|
||||
cylinder.Mass = freshMass;
|
||||
cylinder.InternalEnergy = p2 * V_clear / (gamma - 1.0); // consistent with pressure/temperature
|
||||
|
||||
// Store pre‑ignition state for misfire recovery
|
||||
preIgnitionMass = cylinder.Mass;
|
||||
preIgnitionInternalEnergy = cylinder.InternalEnergy;
|
||||
|
||||
if (isMisfiring)
|
||||
{
|
||||
double vol = cylinder.Volume;
|
||||
double R = cylinder.GasConstant;
|
||||
double T0 = 300.0;
|
||||
double newMass = ambientPressure * vol / (R * T0);
|
||||
double newInternalEnergy = ambientPressure * vol / (cylinder.Gamma - 1.0);
|
||||
cylinder.Mass = newMass;
|
||||
cylinder.InternalEnergy = newInternalEnergy;
|
||||
// No combustion – just expand from compressed state
|
||||
misfireCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
double volume = cylinder.Volume;
|
||||
double gamma = cylinder.Gamma;
|
||||
double newInternalEnergy = CombustionPressure * volume / (gamma - 1.0);
|
||||
double R = cylinder.GasConstant;
|
||||
double newMass = CombustionPressure * volume / (R * CombustionTemperature);
|
||||
cylinder.InternalEnergy = newInternalEnergy;
|
||||
cylinder.Mass = newMass;
|
||||
// Start Wiebe burn
|
||||
double V = V_clear;
|
||||
targetBurnEnergy = CombustionPressure * V / (gamma - 1.0);
|
||||
double R = 287.0;
|
||||
totalBurnMass = CombustionPressure * V / (R * CombustionTemperature);
|
||||
burnInProgress = true;
|
||||
burnStartAngle = cycleCrankAngle; // now = 0
|
||||
combustionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update valve area
|
||||
double valveOpen = ValveOpenRatio(crankAngle);
|
||||
coupling.OrificeArea = maxOrificeArea * valveOpen;
|
||||
// ---- Combustion progress (if active) ----
|
||||
if (burnInProgress)
|
||||
{
|
||||
double angleFromIgnition = cycleCrankAngle - burnStartAngle;
|
||||
if (angleFromIgnition >= BurnDurationRad)
|
||||
{
|
||||
// Burn complete
|
||||
cylinder.Mass = totalBurnMass;
|
||||
cylinder.InternalEnergy = targetBurnEnergy;
|
||||
burnInProgress = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double fraction = WiebeFraction(angleFromIgnition);
|
||||
// Interpolate between pre‑ignition (compressed charge) and final burned state
|
||||
double gamma = 1.4;
|
||||
double V = cylinder.Volume; // still near clearance
|
||||
double baseEnergy = preIgnitionInternalEnergy;
|
||||
double baseMass = preIgnitionMass;
|
||||
|
||||
cylinder.InternalEnergy = baseEnergy * (1.0 - fraction) + targetBurnEnergy * fraction;
|
||||
cylinder.Mass = baseMass * (1.0 - fraction) + totalBurnMass * fraction;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Update cylinder volume from piston kinematics ----
|
||||
double theta = cycleCrankAngle % (2.0 * Math.PI); // crank angle for piston position
|
||||
var (vol, dvdt) = PistonKinematics(theta);
|
||||
cylinder.Volume = vol;
|
||||
cylinder.Dvdt = dvdt;
|
||||
|
||||
// ---- Valve lift & orifice area ----
|
||||
double lift = ValveOpenRatio(cycleCrankAngle);
|
||||
coupling.OrificeArea = maxOrificeArea * lift;
|
||||
|
||||
// ---- Solver step ----
|
||||
float massFlow = solver.Step();
|
||||
float endPressure = (float)exhaustPipe.GetCellPressure(exhaustPipe.GetCellCount() - 1);
|
||||
|
||||
// Low‑pass filter the pressure (emphasise low frequencies)
|
||||
double rc = 1.0 / (2.0 * Math.PI * PressureCutoffHz);
|
||||
double alpha = dt / (rc + dt);
|
||||
double filteredPressure = alpha * endPressure + (1.0 - alpha) * lastFilteredPressure;
|
||||
lastFilteredPressure = filteredPressure;
|
||||
// ---- Audio (no filter, feed raw pressure) ----
|
||||
float audioSample = soundProcessor.Process(massFlow, endPressure);
|
||||
|
||||
float audioSample = soundProcessor.Process(massFlow, (float)filteredPressure);
|
||||
// Log occasionally
|
||||
time += dt;
|
||||
stepCount++;
|
||||
|
||||
// Logging
|
||||
if (stepCount % LogStepInterval == 0 || (crankAngle < angularVelocity * dt * 2 && !isMisfiring && combustionCount > 0))
|
||||
if (stepCount % LogStepInterval == 0)
|
||||
{
|
||||
Console.WriteLine($"{time,7:F3} {crankAngle * 180.0 / Math.PI,6:F1} " +
|
||||
$"{valveOpen * 100,6:F1} {massFlow,10:F4} " +
|
||||
$"{combustionCount,3} {(isMisfiring ? "X" : "")}");
|
||||
double crankDeg = cycleCrankAngle * 180.0 / Math.PI;
|
||||
double volCC = cylinder.Volume * 1e6; // cc
|
||||
Console.WriteLine($"{time,5:F3} {crankDeg,7:F1}° {volCC,5:F1} {massFlow,14:E4} {combustionCount,4} {misfireCount,4}");
|
||||
}
|
||||
|
||||
return audioSample;
|
||||
}
|
||||
|
||||
// ---- Drawing (unchanged) ----
|
||||
public override void Draw(RenderWindow target)
|
||||
{
|
||||
float winW = target.GetView().Size.X;
|
||||
float winH = target.GetView().Size.Y;
|
||||
float centerY = winH / 2f;
|
||||
|
||||
const float T_ambient = 293.15f;
|
||||
const float T_hot = 1500f;
|
||||
const float T_cold = 0f;
|
||||
const float R = 287.05f;
|
||||
|
||||
float deltaHot = T_hot - T_ambient;
|
||||
float deltaCold = T_ambient - T_cold;
|
||||
|
||||
float NormaliseTemperature(double T)
|
||||
{
|
||||
double t;
|
||||
if (T >= T_ambient)
|
||||
t = (T - T_ambient) / deltaHot;
|
||||
else
|
||||
t = (T - T_ambient) / deltaCold;
|
||||
return (float)Math.Clamp(t, -1.0, 1.0);
|
||||
}
|
||||
|
||||
float cylW = 80f, cylH = 150f;
|
||||
var cylRect = new RectangleShape(new Vector2f(cylW, cylH));
|
||||
cylRect.Position = new Vector2f(40f, centerY - cylH / 2f);
|
||||
double pNorm = (cylinder.Pressure - ambientPressure) / ambientPressure;
|
||||
if (double.IsNaN(pNorm)) pNorm = 0;
|
||||
byte red = (byte)(Math.Clamp(pNorm * 128, 0, 255));
|
||||
byte blue = (byte)(Math.Clamp(-pNorm * 128, 0, 255));
|
||||
cylRect.FillColor = new Color(red, 0, blue);
|
||||
|
||||
double tempCyl = cylinder.Temperature; // Volume0D now has Temperature
|
||||
float tnCyl = NormaliseTemperature(tempCyl);
|
||||
byte redCyl = (byte)(tnCyl > 0 ? 255 * tnCyl : 0);
|
||||
byte blueCyl = (byte)(tnCyl < 0 ? -255 * tnCyl : 0);
|
||||
byte greenCyl = (byte)(255 * (1 - Math.Abs(tnCyl)));
|
||||
cylRect.FillColor = new Color(redCyl, greenCyl, blueCyl);
|
||||
target.Draw(cylRect);
|
||||
|
||||
int n = exhaustPipe.GetCellCount();
|
||||
@@ -215,21 +317,25 @@ namespace FluidSim.Core
|
||||
float dx = pipeLen / (n - 1);
|
||||
float baseRadius = 20f;
|
||||
var vertices = new Vertex[n * 2];
|
||||
float ambientPressure = 101325f;
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
float x = pipeStartX + i * dx;
|
||||
double p = exhaustPipe.GetCellPressure(i);
|
||||
float r = baseRadius * (float)(1.0 + (p - ambientPressure) / ambientPressure);
|
||||
double p = exhaustPipe.GetCellPressure(i);
|
||||
double rho = exhaustPipe.GetCellDensity(i);
|
||||
double T = p / (rho * R);
|
||||
|
||||
float r = baseRadius * 0.1f * (float)(1.0 + (p - ambientPressure) / ambientPressure);
|
||||
if (r < 2f) r = 2f;
|
||||
|
||||
double t = (p - ambientPressure) / ambientPressure;
|
||||
t = Math.Clamp(t, -1.0, 1.0);
|
||||
byte rCol = (byte)(t > 0 ? 255 * t : 0);
|
||||
byte bCol = (byte)(t < 0 ? -255 * t : 0);
|
||||
byte gCol = (byte)(255 * (1 - Math.Abs(t)));
|
||||
float tn = NormaliseTemperature(T);
|
||||
byte rCol = (byte)(tn > 0 ? 255 * tn : 0);
|
||||
byte bCol = (byte)(tn < 0 ? -255 * tn : 0);
|
||||
byte gCol = (byte)(255 * (1 - Math.Abs(tn)));
|
||||
var col = new Color(rCol, gCol, bCol);
|
||||
|
||||
vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col);
|
||||
vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col);
|
||||
vertices[i * 2 + 1] = new Vertex(new Vector2f(x, centerY + r), col);
|
||||
}
|
||||
target.Draw(vertices, PrimitiveType.TriangleStrip);
|
||||
|
||||
Reference in New Issue
Block a user