using System; using FluidSim.Components; using FluidSim.Utils; using FluidSim.Interfaces; using SFML.Graphics; using SFML.System; namespace FluidSim.Core { public class EngineScenario : Scenario { private Solver solver; private Crankshaft crankshaft; private EngineCylinder engineCyl; private Pipe1D exhaustPipe; private PipeVolumeConnection coupling; private SoundProcessor soundProcessor; private OutdoorExhaustReverb reverb; private Port exitPort = new Port(); private double dt; private double pipeArea; private const double AmbientPressure = 101325.0; private double time; private int stepCount = 0; private const int LogInterval = 10000; // Throttle 0..1 public double Throttle { get; set; } = 0.0; // start with a light idle throttle // ---- Realistic combustion parameters ---- private const double FullLoadPeakPressure = 70.0 * 101325.0; // 15 bar // ---- Idle speed governor ---- private const double TargetIdleRPM = 800.0; // rad/s = RPM * π/30, we'll convert public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; // ---- Crankshaft: inertia + friction that gives ~800 RPM at idle ---- crankshaft = new Crankshaft(initialRPM: 600.0) // start a bit low { Inertia = 0.005, // slightly heavier flywheel FrictionConstant = 0.8, // static friction FrictionViscous = 0.01 // viscous (linear with RPM) }; // ---- Pipe: add a tiny bit of damping to prevent unrealistic shocks ---- double pipeLength = 2; double pipeRadius = 0.1; pipeArea = Math.PI * pipeRadius * pipeRadius; exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 60); exhaustPipe.SetUniformState(1.225, 0.0, AmbientPressure); exhaustPipe.DampingMultiplier = 5; exhaustPipe.EnergyRelaxationRate = 50; // ---- Cylinder ---- engineCyl = new EngineCylinder(crankshaft, bore: 0.065, stroke: 0.0565, compressionRatio: 10.0, pipeArea: pipeArea, sampleRate: sampleRate); // ---- Coupling ---- coupling = new PipeVolumeConnection(engineCyl.Cylinder, exhaustPipe, true, orificeArea: 0.0); // ---- Solver ---- solver = new Solver(); solver.SetTimeStep(dt); solver.AddVolume(engineCyl.Cylinder); solver.AddPipe(exhaustPipe); solver.AddConnection(coupling); solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.OpenEnd, AmbientPressure); // ---- Sound processor (stable version) ---- soundProcessor = new SoundProcessor(sampleRate, pipeRadius * 2); soundProcessor.Gain = 0.00001f; // ---- Reverb ---- reverb = new OutdoorExhaustReverb(sampleRate); // Church: vast, highly reflective, bright reverb.DryMix = 1.0f; // always full dry signal reverb.EarlyMix = 0.5f; // distinct early reflections from distant walls reverb.TailMix = 0.9f; // huge tail, almost as loud as the dry sound reverb.Feedback = 0.9f; // long decay – roughly 3 s reverb time (with current delay lengths) reverb.DampingFreq = 6000f; // bright: high‑frequency energy stays for a long time reverb.MatrixCoeff = 0.5f; // default orthogonal mix Console.WriteLine("=== EngineScenario (Stable) ==="); Console.WriteLine($"Crankshaft inertia: {crankshaft.Inertia}"); Console.WriteLine($"Pipe: {pipeLength} m, fundamental: {340/(4*pipeLength):F1} Hz"); } public override float Process() { // ---- RPM governor: adjust throttle to maintain idle when no user input ---- double currentRPM = crankshaft.AngularVelocity * 60.0 / (2.0 * Math.PI); double throttle = Math.Clamp(Throttle, 0.05, 1.0); // never let it drop below a tiny value // ---- Target combustion pressure ---- double targetPressure = throttle * FullLoadPeakPressure; engineCyl.TargetPeakPressure = targetPressure; // ---- Simulate one timestep ---- engineCyl.Step(dt); crankshaft.Step(dt); coupling.OrificeArea = engineCyl.OrificeArea; solver.Step(); // ---- Update exit port with safety clamps ---- UpdateExitPort(); // ---- Generate audio ---- float dry = soundProcessor.Process(exitPort); float wet = reverb.Process(dry); time += dt; stepCount++; return wet; } private void UpdateExitPort() { int last = exhaustPipe.GetCellCount() - 1; double p = exhaustPipe.GetCellPressure(last); double rho = exhaustPipe.GetCellDensity(last); double vel = exhaustPipe.GetCellVelocity(last); // Clamp density to physically possible values if (rho < 0.01) rho = 0.01; // never let it approach zero if (rho > 50.0) rho = 50.0; // never let it become absurd // Clamp velocity to ± 500 m/s (safe subsonic) vel = Math.Clamp(vel, -500.0, 500.0); double outflowMassFlow = rho * vel * pipeArea; // Clamp exit pressure to sensible range (0.1 – 20 bar) p = Math.Clamp(p, 1e4, 2e6); exitPort.Pressure = p; exitPort.Density = rho; exitPort.Temperature = p / (rho * 287.05); exitPort.MassFlowRate = -outflowMassFlow; exitPort.SpecificEnthalpy = 0.0; } 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 tempCyl = engineCyl.Cylinder.Temperature; float tnCyl = NormaliseTemperature(tempCyl); byte rC = (byte)(tnCyl > 0 ? 255 * tnCyl : 0); byte bC = (byte)(tnCyl < 0 ? -255 * tnCyl : 0); byte gC = (byte)(255 * (1 - Math.Abs(tnCyl))); cylRect.FillColor = new Color(rC, gC, bC); target.Draw(cylRect); int n = exhaustPipe.GetCellCount(); float pipeStartX = 120f, pipeEndX = winW - 60f; float pipeLen = pipeEndX - pipeStartX; float dx = pipeLen / (n - 1); float baseRadius = 20f; var vertices = new Vertex[n * 2]; float ambPress = 101325f; for (int i = 0; i < n; i++) { float x = pipeStartX + i * dx; double p = exhaustPipe.GetCellPressure(i); double rho = exhaustPipe.GetCellDensity(i); double T = p / (rho * R); float r = baseRadius * 0.3f * (float)(1.0 + (p - ambPress) / ambPress); if (r < 2f) r = 2f; 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 + 1] = new Vertex(new Vector2f(x, centerY + r), col); } target.Draw(vertices, PrimitiveType.TriangleStrip); } } }