refined
This commit is contained in:
@@ -19,15 +19,20 @@ namespace FluidSim.Components
|
|||||||
public double ConRodLength { get; }
|
public double ConRodLength { get; }
|
||||||
public double CompressionRatio { get; }
|
public double CompressionRatio { get; }
|
||||||
|
|
||||||
// Valve timings
|
// Valve timings (degrees, 0 = TDC compression, 720° full cycle)
|
||||||
public double IVO { get; }
|
public double IVO { get; }
|
||||||
public double IVC { get; }
|
public double IVC { get; }
|
||||||
public double EVO { get; }
|
public double EVO { get; }
|
||||||
public double EVC { get; }
|
public double EVC { get; }
|
||||||
|
|
||||||
// Valve areas
|
// Valve geometry
|
||||||
public double MaxIntakeArea { get; set; } = 0.0005;
|
public double IntakeValveDiameter { get; set; } = 0.030;
|
||||||
public double MaxExhaustArea { get; set; } = 0.0005;
|
public double ExhaustValveDiameter { get; set; } = 0.028;
|
||||||
|
public double IntakeValveLift { get; set; } = 0.005;
|
||||||
|
public double ExhaustValveLift { get; set; } = 0.005;
|
||||||
|
|
||||||
|
public double IntakeValveMaxArea => Math.PI * IntakeValveDiameter * IntakeValveLift;
|
||||||
|
public double ExhaustValveMaxArea => Math.PI * ExhaustValveDiameter * ExhaustValveLift;
|
||||||
|
|
||||||
// Ignition and combustion
|
// Ignition and combustion
|
||||||
public double SparkAdvance { get; set; } = 20.0;
|
public double SparkAdvance { get; set; } = 20.0;
|
||||||
@@ -40,6 +45,12 @@ namespace FluidSim.Components
|
|||||||
public double StoichiometricAFR { get; set; } = 14.7;
|
public double StoichiometricAFR { get; set; } = 14.7;
|
||||||
public double FuelLowerHeatingValue { get; set; } = 44e6;
|
public double FuelLowerHeatingValue { get; set; } = 44e6;
|
||||||
|
|
||||||
|
// Cycle‑to‑cycle randomness
|
||||||
|
/// <summary>Fractional variation in fuel energy (±). 0.05 = ±5%.</summary>
|
||||||
|
public double EnergyVariationFraction { get; set; } = 0.05;
|
||||||
|
/// <summary>Probability of a misfire (0‑1).</summary>
|
||||||
|
public double MisfireProbability { get; set; } = 0.01;
|
||||||
|
|
||||||
// Heat loss
|
// Heat loss
|
||||||
public double CylinderWallArea { get; set; } = 0.02;
|
public double CylinderWallArea { get; set; } = 0.02;
|
||||||
public double HeatTransferCoefficient { get; set; } = 100.0;
|
public double HeatTransferCoefficient { get; set; } = 100.0;
|
||||||
@@ -64,6 +75,10 @@ namespace FluidSim.Components
|
|||||||
private bool combustionActive;
|
private bool combustionActive;
|
||||||
private bool fuelInjected;
|
private bool fuelInjected;
|
||||||
|
|
||||||
|
// per‑cycle randomness
|
||||||
|
private double _energyFactor = 1.0; // applied to FuelLowerHeatingValue this cycle
|
||||||
|
private readonly Random _random = new Random();
|
||||||
|
|
||||||
private const double Gamma = 1.4;
|
private const double Gamma = 1.4;
|
||||||
private const double GasConstant = 287.0;
|
private const double GasConstant = 287.0;
|
||||||
|
|
||||||
@@ -95,6 +110,7 @@ namespace FluidSim.Components
|
|||||||
_ports = new[] { IntakePort, ExhaustPort };
|
_ports = new[] { IntakePort, ExhaustPort };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derived volumes
|
||||||
private double SweptVolume => Math.PI * 0.25 * Bore * Bore * Stroke;
|
private double SweptVolume => Math.PI * 0.25 * Bore * Bore * Stroke;
|
||||||
private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0);
|
private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0);
|
||||||
private double CrankRadius => Stroke / 2.0;
|
private double CrankRadius => Stroke / 2.0;
|
||||||
@@ -113,24 +129,40 @@ namespace FluidSim.Components
|
|||||||
return clearanceVolume + area * x;
|
return clearanceVolume + area * x;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double IntakeValveArea => ValveArea(CrankDeg, IVO, IVC, MaxIntakeArea);
|
private double ValveLift(double thetaDeg, double opens, double closes, double peakLift)
|
||||||
public double ExhaustValveArea => ValveArea(CrankDeg, EVO, EVC, MaxExhaustArea);
|
|
||||||
|
|
||||||
private double ValveArea(double thetaDeg, double opens, double closes, double maxArea)
|
|
||||||
{
|
{
|
||||||
double deg = thetaDeg % 720.0;
|
double deg = thetaDeg % 720.0;
|
||||||
if (deg < 0) deg += 720.0;
|
if (deg < 0) deg += 720.0;
|
||||||
if (deg >= opens && deg <= closes)
|
|
||||||
|
double duration = closes - opens;
|
||||||
|
if (duration <= 0) return 0.0;
|
||||||
|
|
||||||
|
double rampDur = duration * 0.25;
|
||||||
|
double holdDur = duration - 2.0 * rampDur;
|
||||||
|
|
||||||
|
if (deg >= opens && deg < opens + rampDur)
|
||||||
{
|
{
|
||||||
double half = (closes - opens) * 0.5;
|
double t = (deg - opens) / rampDur;
|
||||||
double mid = opens + half;
|
return peakLift * t * t * (3.0 - 2.0 * t);
|
||||||
double frac = 1.0 - Math.Abs(deg - mid) / half;
|
}
|
||||||
frac = Math.Clamp(frac, 0.0, 1.0);
|
else if (deg >= opens + rampDur && deg < opens + rampDur + holdDur)
|
||||||
return maxArea * frac;
|
{
|
||||||
|
return peakLift;
|
||||||
|
}
|
||||||
|
else if (deg >= opens + rampDur + holdDur && deg <= closes)
|
||||||
|
{
|
||||||
|
double t = (deg - (opens + rampDur + holdDur)) / rampDur;
|
||||||
|
return peakLift * (1.0 - t) * (1.0 - t) * (1.0 + 2.0 * t);
|
||||||
}
|
}
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double IntakeValveArea =>
|
||||||
|
Math.PI * IntakeValveDiameter * ValveLift(CrankDeg, IVO, IVC, IntakeValveLift);
|
||||||
|
|
||||||
|
public double ExhaustValveArea =>
|
||||||
|
Math.PI * ExhaustValveDiameter * ValveLift(CrankDeg, EVO, EVC, ExhaustValveLift);
|
||||||
|
|
||||||
private double Wiebe(double angleSinceSpark)
|
private double Wiebe(double angleSinceSpark)
|
||||||
{
|
{
|
||||||
if (angleSinceSpark < WiebeStart) return 0.0;
|
if (angleSinceSpark < WiebeStart) return 0.0;
|
||||||
@@ -147,8 +179,8 @@ namespace FluidSim.Components
|
|||||||
|
|
||||||
double dV = cylinderVolume - prevVolume;
|
double dV = cylinderVolume - prevVolume;
|
||||||
|
|
||||||
// ---- Piston torque ----
|
// Piston torque
|
||||||
double pRel = Pressure - 101325.0; // relative to ambient
|
double pRel = Pressure - 101325.0;
|
||||||
double sinTh = Math.Sin(crankAngleRad);
|
double sinTh = Math.Sin(crankAngleRad);
|
||||||
double cosTh = Math.Cos(crankAngleRad);
|
double cosTh = Math.Cos(crankAngleRad);
|
||||||
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
|
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
|
||||||
@@ -157,13 +189,12 @@ namespace FluidSim.Components
|
|||||||
double torque = pRel * pistonArea * dxdtheta;
|
double torque = pRel * pistonArea * dxdtheta;
|
||||||
Crankshaft.AddTorque(torque);
|
Crankshaft.AddTorque(torque);
|
||||||
|
|
||||||
// Volume work (done BY gas, positive when expanding)
|
|
||||||
cylinderEnergy -= Pressure * dV;
|
cylinderEnergy -= Pressure * dV;
|
||||||
|
|
||||||
double prevDeg = Crankshaft.PreviousAngle * 180.0 / Math.PI % 720.0;
|
double prevDeg = Crankshaft.PreviousAngle * 180.0 / Math.PI % 720.0;
|
||||||
double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0;
|
double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0;
|
||||||
|
|
||||||
// Intake closing: capture trapped air mass (air only!)
|
// ----- Intake closing: capture trapped air mass and compute fuel -----
|
||||||
if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC)
|
if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC)
|
||||||
{
|
{
|
||||||
trappedAirMass = _airMass;
|
trappedAirMass = _airMass;
|
||||||
@@ -171,23 +202,39 @@ namespace FluidSim.Components
|
|||||||
fuelInjected = true;
|
fuelInjected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spark
|
// ----- Spark ignition (once per cycle, with misfire chance) -----
|
||||||
double sparkAngle = 0.0 - SparkAdvance;
|
double sparkAngle = 0.0 - SparkAdvance;
|
||||||
if (sparkAngle < 0) sparkAngle += 720.0;
|
if (sparkAngle < 0) sparkAngle += 720.0;
|
||||||
|
|
||||||
bool crossedSpark = (prevDeg < sparkAngle && currDeg >= sparkAngle) ||
|
bool crossedSpark = (prevDeg < sparkAngle && currDeg >= sparkAngle) ||
|
||||||
(prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle);
|
(prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle);
|
||||||
if (crossedSpark && !combustionActive && fuelInjected)
|
if (crossedSpark && !combustionActive && fuelInjected)
|
||||||
{
|
{
|
||||||
combustionActive = true;
|
// Decide misfire
|
||||||
burnFraction = 0.0;
|
bool misfire = _random.NextDouble() < MisfireProbability;
|
||||||
|
if (misfire)
|
||||||
|
{
|
||||||
|
combustionActive = false; // no combustion this cycle
|
||||||
|
// fuel is not burned – will remain in cylinder and eventually exit as unburned mixture
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
combustionActive = true;
|
||||||
|
burnFraction = 0.0;
|
||||||
|
|
||||||
|
// Energy variation factor for this cycle
|
||||||
|
double range = EnergyVariationFraction;
|
||||||
|
_energyFactor = 1.0 + range * (2.0 * _random.NextDouble() - 1.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combustion progress
|
// ----- Combustion progress -----
|
||||||
if (combustionActive)
|
if (combustionActive)
|
||||||
{
|
{
|
||||||
double angleSinceSpark = currDeg - sparkAngle;
|
double angleSinceSpark = currDeg - sparkAngle;
|
||||||
if (angleSinceSpark < 0) angleSinceSpark += 720.0;
|
if (angleSinceSpark < 0) angleSinceSpark += 720.0;
|
||||||
double newFraction = Wiebe(angleSinceSpark);
|
double newFraction = Wiebe(angleSinceSpark);
|
||||||
|
|
||||||
if (newFraction >= 1.0 || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
|
if (newFraction >= 1.0 || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
|
||||||
{
|
{
|
||||||
newFraction = 1.0;
|
newFraction = 1.0;
|
||||||
@@ -201,14 +248,14 @@ namespace FluidSim.Components
|
|||||||
double dFraction = newFraction - burnFraction;
|
double dFraction = newFraction - burnFraction;
|
||||||
if (dFraction > 0)
|
if (dFraction > 0)
|
||||||
{
|
{
|
||||||
double dQ = fuelMass * FuelLowerHeatingValue * dFraction;
|
double dQ = fuelMass * FuelLowerHeatingValue * _energyFactor * dFraction;
|
||||||
cylinderEnergy += dQ;
|
cylinderEnergy += dQ;
|
||||||
_exhaustMass += fuelMass * dFraction; // burning fuel adds to exhaust
|
_exhaustMass += fuelMass * dFraction;
|
||||||
burnFraction = newFraction;
|
burnFraction = newFraction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heat loss
|
// ----- Heat loss to cylinder walls -----
|
||||||
double dQ_loss = HeatTransferCoefficient * CylinderWallArea *
|
double dQ_loss = HeatTransferCoefficient * CylinderWallArea *
|
||||||
(Temperature - AmbientTemperature) * dt;
|
(Temperature - AmbientTemperature) * dt;
|
||||||
cylinderEnergy -= dQ_loss;
|
cylinderEnergy -= dQ_loss;
|
||||||
@@ -250,7 +297,6 @@ namespace FluidSim.Components
|
|||||||
|
|
||||||
double V = Math.Max(cylinderVolume, 1e-12);
|
double V = Math.Max(cylinderVolume, 1e-12);
|
||||||
|
|
||||||
// Safety clamps
|
|
||||||
double currentP = (Gamma - 1.0) * cylinderEnergy / V;
|
double currentP = (Gamma - 1.0) * cylinderEnergy / V;
|
||||||
if (currentP > MaxPressurePa)
|
if (currentP > MaxPressurePa)
|
||||||
cylinderEnergy = MaxPressurePa * V / (Gamma - 1.0);
|
cylinderEnergy = MaxPressurePa * V / (Gamma - 1.0);
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ namespace FluidSim.Components
|
|||||||
public Port PortA { get; }
|
public Port PortA { get; }
|
||||||
public Port PortB { get; }
|
public Port PortB { get; }
|
||||||
public double Area { get; }
|
public double Area { get; }
|
||||||
public double DampingMultiplier { get; set; } = 1.0;
|
public double DampingMultiplier { get; set; } = 10.0;
|
||||||
public double EnergyRelaxationRate { get; set; } = 0.0; // 1/s
|
public double EnergyRelaxationRate { get; set; } = 5.0; // 1/s
|
||||||
|
|
||||||
private double _ambientPressure = 101325.0;
|
private double _ambientPressure = 101325.0;
|
||||||
public double AmbientPressure
|
public double AmbientPressure
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ namespace FluidSim.Core
|
|||||||
private readonly OrthonormalMixer mixerL, mixerR;
|
private readonly OrthonormalMixer mixerL, mixerR;
|
||||||
private readonly LowPassFilter[] filterL, filterR;
|
private readonly LowPassFilter[] filterL, filterR;
|
||||||
|
|
||||||
public float DryMix { get; set; } = 1.0f;
|
public float DryMix { get; set; } = 1.0f; // direct sound unchanged
|
||||||
public float EarlyMix { get; set; } = 0.5f;
|
public float EarlyMix { get; set; } = 0.12f; // very little early reflection (ground bounce)
|
||||||
public float TailMix { get; set; } = 0.9f;
|
public float TailMix { get; set; } = 0.18f; // subtle diffuse tail
|
||||||
public float Feedback { get; set; } = 0.55f; // safe range 0.7‑0.9
|
public float Feedback { get; set; } = 0.35f; // lower feedback – outdoor doesn't ring
|
||||||
public float DampingFreq { get; set; } = 6000f; // Hz
|
public float DampingFreq { get; set; } = 2500f; // air absorption – high frequencies die quickly
|
||||||
|
|
||||||
public OutdoorExhaustReverb(int sampleRate)
|
public OutdoorExhaustReverb(int sampleRate)
|
||||||
{
|
{
|
||||||
@@ -118,7 +118,7 @@ namespace FluidSim.Core
|
|||||||
public float Process(float drySample)
|
public float Process(float drySample)
|
||||||
{
|
{
|
||||||
var (l, r) = ProcessStereo(drySample);
|
var (l, r) = ProcessStereo(drySample);
|
||||||
return (l + r) * 0.5f;
|
return MathF.Tanh((l + r) * 0.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Helper classes ==========
|
// ========== Helper classes ==========
|
||||||
|
|||||||
@@ -4,141 +4,73 @@ using FluidSim.Core;
|
|||||||
namespace FluidSim.Core
|
namespace FluidSim.Core
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Synthesises far‑field sound at a listener position from an open pipe end.
|
/// Synthesises far‑field exhaust sound using the monopole model
|
||||||
/// Three source mechanisms are combined:
|
/// of Jones (1978). The radiated pressure is proportional to the
|
||||||
/// 1. Monopole – time derivative of mass flow (dominant at low speed / high pulsation).
|
/// time derivative of the mass flow at the pipe exit.
|
||||||
/// 2. Dipole – time derivative of momentum flux (shear‑layer / vortex shedding).
|
|
||||||
/// 3. Jet noise – Lighthill‑type turbulence mixing noise (scales with U^8).
|
|
||||||
///
|
///
|
||||||
/// References:
|
/// Reference:
|
||||||
/// • Lighthill, M.J. (1952) "On Sound Generated Aerodynamically".
|
/// Jones, A.D. (1978) "Noise characteristics and exhaust process
|
||||||
/// • Dowling, A.P. & Williams, J.E.F. (1983) "Sound and Sources of Sound".
|
/// gas dynamics of a small 2-stroke engine", PhD thesis, Univ. Adelaide.
|
||||||
/// • Munjal, M.L. (2014) "Acoustics of Ducts and Mufflers", 2nd ed.
|
|
||||||
/// • Tam, C.K.W. & Auriault, L. (1999) "Jet Mixing Noise from Fine‑Scale Turbulence".
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SoundProcessor
|
public class SoundProcessor
|
||||||
{
|
{
|
||||||
private readonly double dt;
|
private readonly double dt;
|
||||||
private readonly double c0; // ambient speed of sound (m/s)
|
|
||||||
private readonly double rho0; // ambient density (kg/m³)
|
|
||||||
private readonly double r; // listener distance (m)
|
private readonly double r; // listener distance (m)
|
||||||
private readonly double pipeArea; // cross‑sectional area of the pipe end (m²)
|
private readonly double scaleFactor; // 1 / (4π r) (free-field monopole)
|
||||||
|
|
||||||
// ---------- monopole state ----------
|
// ---------- Mass‑flow derivative (identical to original) ----------
|
||||||
private double flowLP;
|
private double flowLP;
|
||||||
private readonly double lpAlpha;
|
private readonly double lpAlpha;
|
||||||
private double prevMassFlowOut;
|
private double prevMassFlowOut;
|
||||||
private double smoothDMdt;
|
private double smoothDMdt;
|
||||||
private readonly double alpha;
|
private readonly double alpha;
|
||||||
|
|
||||||
// ---------- dipole state ----------
|
|
||||||
private double prevMomentumFlux;
|
|
||||||
private double smoothDMomDt;
|
|
||||||
private readonly double dipAlpha;
|
|
||||||
|
|
||||||
// ---------- jet noise state ----------
|
|
||||||
private double jetNoiseSample; // previous random sample (for simple shaping)
|
|
||||||
private readonly double jetTau; // correlation time ≈ D / U_mean
|
|
||||||
|
|
||||||
public float Gain { get; set; } = 1.0f;
|
public float Gain { get; set; } = 1.0f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sampleRate">Audio sample rate (Hz).</param>
|
/// <param name="sampleRate">Audio sample rate (Hz).</param>
|
||||||
/// <param name="listenerDistanceMeters">Distance from the pipe exit to the listener (m).</param>
|
/// <param name="listenerDistanceMeters">Listener distance (m).</param>
|
||||||
/// <param name="pipeDiameterMeters">Internal diameter of the pipe (m).</param>
|
/// <param name="pipeDiameterMeters">Ignored in this model; kept for compatibility.</param>
|
||||||
public SoundProcessor(int sampleRate,
|
public SoundProcessor(int sampleRate,
|
||||||
double listenerDistanceMeters = 1.0,
|
double listenerDistanceMeters = 1.0,
|
||||||
double pipeDiameterMeters = 0.0217) // ~3.7 cm² area
|
double pipeDiameterMeters = 0.0217)
|
||||||
{
|
{
|
||||||
dt = 1.0 / sampleRate;
|
dt = 1.0 / sampleRate;
|
||||||
r = listenerDistanceMeters;
|
r = listenerDistanceMeters;
|
||||||
pipeArea = Math.PI * 0.25 * pipeDiameterMeters * pipeDiameterMeters;
|
scaleFactor = 1.0 / (4.0 * Math.PI * r); // free‑field monopole
|
||||||
|
|
||||||
// Ambient air properties
|
// ---- Smoothing time constants (unchanged) ----
|
||||||
c0 = 340.0;
|
double tau = 0.02; // 2 ms for derivative
|
||||||
rho0 = 1.225;
|
|
||||||
|
|
||||||
// ---- Monopole smoothing ----
|
|
||||||
double tau = 0.002; // 2 ms
|
|
||||||
alpha = Math.Exp(-dt / tau);
|
alpha = Math.Exp(-dt / tau);
|
||||||
|
|
||||||
double tauLP = 0.005; // 5 ms low‑pass on mass flow
|
double tauLP = 0.00001; // 5 ms low‑pass on mass flow
|
||||||
lpAlpha = Math.Exp(-dt / tauLP);
|
lpAlpha = Math.Exp(-dt / tauLP);
|
||||||
|
|
||||||
// ---- Dipole smoothing ----
|
|
||||||
double tauDip = 0.003; // 3 ms
|
|
||||||
dipAlpha = Math.Exp(-dt / tauDip);
|
|
||||||
|
|
||||||
// ---- Jet noise correlation time ----
|
|
||||||
jetTau = Math.Max(0.0005, pipeDiameterMeters / 50.0); // D / U_ref, floor at 0.5 ms
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process one sample. The OpenEndLink provides the instantaneous
|
/// Process one sample. The OpenEndLink provides the instantaneous
|
||||||
/// exit‑plane mass flow, density, velocity, and pressure.
|
/// exit‑plane mass flow.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float Process(OpenEndLink openEnd)
|
public float Process(OpenEndLink openEnd)
|
||||||
{
|
{
|
||||||
double flowOut = openEnd.LastMassFlowRate; // kg/s, positive = leaving pipe
|
double flowOut = openEnd.LastMassFlowRate; // kg/s, positive = leaving pipe
|
||||||
double rhoExit = openEnd.LastFaceDensity; // kg/m³ at exit
|
|
||||||
double uExit = openEnd.LastFaceVelocity; // m/s (axial, positive = leaving)
|
|
||||||
double pExit = openEnd.LastFacePressure; // Pa
|
|
||||||
|
|
||||||
// ============================================================
|
// Low‑pass the mass flow signal
|
||||||
// 1. MONOPOLE – due to unsteady mass addition (Lighthill 1952)
|
|
||||||
// Far‑field pressure: p'(r,t) = (1 / 4πr c0) · dṁ/dt
|
|
||||||
// ============================================================
|
|
||||||
flowLP = lpAlpha * flowLP + (1.0 - lpAlpha) * flowOut;
|
flowLP = lpAlpha * flowLP + (1.0 - lpAlpha) * flowOut;
|
||||||
|
|
||||||
|
// Derivative of the smoothed mass flow
|
||||||
double rawDerivative = (flowLP - prevMassFlowOut) / dt;
|
double rawDerivative = (flowLP - prevMassFlowOut) / dt;
|
||||||
prevMassFlowOut = flowLP;
|
prevMassFlowOut = flowLP;
|
||||||
|
|
||||||
|
// Smooth the derivative
|
||||||
smoothDMdt = alpha * smoothDMdt + (1.0 - alpha) * rawDerivative;
|
smoothDMdt = alpha * smoothDMdt + (1.0 - alpha) * rawDerivative;
|
||||||
double pMono = smoothDMdt / (4.0 * Math.PI * r * c0);
|
|
||||||
|
|
||||||
// ============================================================
|
// Far‑field monopole pressure (free‑field, Jones eq. 2.15 adapted)
|
||||||
// 2. DIPOLE – due to unsteady momentum flux at the exit plane
|
double pressure = smoothDMdt * scaleFactor * Gain;
|
||||||
// Momentum flux: F(t) = ṁ(t) · u(t) = ρ·A·u²
|
|
||||||
// Far‑field (compact, low M): p'(r,θ,t) ≈ (cosθ / 4πr c0) · dF/dt
|
|
||||||
// For on‑axis listener (θ = 0): p'(r,t) ≈ (1 / 4πr c0) · dF/dt
|
|
||||||
// We also include a U⁶ scaling factor relative to a reference velocity.
|
|
||||||
// ============================================================
|
|
||||||
double momentumFlux = Math.Abs(flowOut) * Math.Abs(uExit); // N
|
|
||||||
double rawMomDeriv = (momentumFlux - prevMomentumFlux) / dt;
|
|
||||||
prevMomentumFlux = momentumFlux;
|
|
||||||
smoothDMomDt = dipAlpha * smoothDMomDt + (1.0 - dipAlpha) * rawMomDeriv;
|
|
||||||
double pDipole = smoothDMomDt / (4.0 * Math.PI * r * c0);
|
|
||||||
|
|
||||||
// Dipole efficiency factor: ∝ (U / c0)³ (since Idipole ∝ U⁶, pdipole ∝ U³)
|
// Soft clip to ±1
|
||||||
double Mach = Math.Abs(uExit) / c0;
|
return (float)pressure;
|
||||||
double dipoleEfficiency = Math.Pow(Mach, 3.0);
|
|
||||||
pDipole *= dipoleEfficiency;
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 3. JET NOISE – Lighthill U⁸ mixing noise, band‑pass shaped
|
|
||||||
// rms pressure: p'_jet ~ ρ0 · A / r · U⁴ / c0²
|
|
||||||
// Model as broadband noise with amplitude ∝ U⁴.
|
|
||||||
// A simple first‑order low‑pass filter shapes the spectrum
|
|
||||||
// (cut‑off ≈ Strouhal frequency f ≈ 0.2 · U / D).
|
|
||||||
// ============================================================
|
|
||||||
double Uref = Math.Max(1.0, Math.Abs(uExit)); // avoid division by zero
|
|
||||||
double jetAmplitude = rho0 * pipeArea / r * Math.Pow(Uref / c0, 4.0);
|
|
||||||
|
|
||||||
// Correlation time (sample‑and‑hold style random walk)
|
|
||||||
double alphaJet = Math.Exp(-dt / jetTau);
|
|
||||||
// Generate a new random target each step, filter with alphaJet
|
|
||||||
double randomTarget = (new Random().NextDouble() * 2.0 - 1.0);
|
|
||||||
jetNoiseSample = alphaJet * jetNoiseSample + (1.0 - alphaJet) * randomTarget;
|
|
||||||
double pJet = jetAmplitude * jetNoiseSample;
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Combine contributions (monopole is primary; dipole & jet are
|
|
||||||
// weighted down for realistic mix). Weights can be tuned per engine.
|
|
||||||
// ============================================================
|
|
||||||
double pressure = (3000.0 * pMono) + (0.01 * pDipole) + (0 * pJet);
|
|
||||||
pressure *= Gain;
|
|
||||||
|
|
||||||
// Soft‑clip to ±1
|
|
||||||
return (float)Math.Tanh(pressure);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ namespace FluidSim.Tests
|
|||||||
|
|
||||||
// ---------- Throttle control ----------
|
// ---------- Throttle control ----------
|
||||||
public double Throttle { get; set; } = 0.0;
|
public double Throttle { get; set; } = 0.0;
|
||||||
public double MaxThrottleArea { get; set; } = 6 * Units.cm2; // 2 cm²
|
public double MaxThrottleArea { get; set; } = 3 * Units.cm2; // 2 cm²
|
||||||
|
|
||||||
public override void Initialize(int sampleRate)
|
public override void Initialize(int sampleRate)
|
||||||
{
|
{
|
||||||
@@ -49,37 +49,38 @@ namespace FluidSim.Tests
|
|||||||
solver.CflTarget = 0.9;
|
solver.CflTarget = 0.9;
|
||||||
|
|
||||||
// ---- Crankshaft (external, passed to cylinder) ----
|
// ---- Crankshaft (external, passed to cylinder) ----
|
||||||
crankshaft = new Crankshaft(1000);
|
crankshaft = new Crankshaft(600);
|
||||||
crankshaft.Inertia = 0.05;
|
crankshaft.Inertia = 0.1;
|
||||||
crankshaft.FrictionConstant = 2;
|
crankshaft.FrictionConstant = 2;
|
||||||
crankshaft.FrictionViscous = 0.05;
|
crankshaft.FrictionViscous = 0.04;
|
||||||
|
|
||||||
// ---- Cylinder ----
|
// ---- Cylinder ----
|
||||||
double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2;
|
double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2;
|
||||||
double ivo = 370.0, ivc = 580.0, evo = 120.0, evc = 350.0;
|
double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0;
|
||||||
cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
|
cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
|
||||||
{
|
{
|
||||||
MaxIntakeArea = 3.7 * Units.cm2,
|
IntakeValveDiameter = 30 * Units.mm, // 30 mm
|
||||||
MaxExhaustArea = 3.7 * Units.cm2,
|
IntakeValveLift = 5 * Units.mm, // 5 mm
|
||||||
|
ExhaustValveDiameter = 28 * Units.mm, // 28 mm
|
||||||
|
ExhaustValveLift = 5 * Units.mm // 5 mm
|
||||||
};
|
};
|
||||||
solver.AddComponent(cylinder);
|
solver.AddComponent(cylinder);
|
||||||
|
|
||||||
double pipeDiameter = 2 * Units.cm;
|
double pipeDiameter = 2 * Units.cm;
|
||||||
double pipeArea = Units.AreaFromDiameter(pipeDiameter);
|
double pipeArea = Units.AreaFromDiameter(pipeDiameter);
|
||||||
|
|
||||||
exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.05f };
|
exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f };
|
||||||
intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.05f };
|
intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f };
|
||||||
reverb = new OutdoorExhaustReverb(sampleRate);
|
reverb = new OutdoorExhaustReverb(sampleRate);
|
||||||
|
|
||||||
// ---- Pipes ----
|
// ---- Pipes ----
|
||||||
intakePipeBeforeThrottle = new Pipe1D(0.15, pipeArea, 5);
|
intakePipeBeforeThrottle = new Pipe1D(0.2, pipeArea, 10);
|
||||||
intakeRunner = new Pipe1D(0.1, pipeArea, 5);
|
intakeRunner = new Pipe1D(0.2, pipeArea, 10);
|
||||||
exhaustPipe = new Pipe1D(1.00, pipeArea, 80);
|
exhaustPipe = new Pipe1D(0.5, pipeArea, 50);
|
||||||
solver.AddComponent(intakePipeBeforeThrottle);
|
solver.AddComponent(intakePipeBeforeThrottle);
|
||||||
solver.AddComponent(intakeRunner);
|
solver.AddComponent(intakeRunner);
|
||||||
solver.AddComponent(exhaustPipe);
|
solver.AddComponent(exhaustPipe);
|
||||||
|
|
||||||
// ---- Plenum (5 mL) ----
|
|
||||||
intakePlenum = new Volume0D(5 * Units.mL, 101325.0, 300.0);
|
intakePlenum = new Volume0D(5 * Units.mL, 101325.0, 300.0);
|
||||||
var plenumInlet = intakePlenum.CreatePort();
|
var plenumInlet = intakePlenum.CreatePort();
|
||||||
var plenumOutlet = intakePlenum.CreatePort();
|
var plenumOutlet = intakePlenum.CreatePort();
|
||||||
@@ -95,9 +96,9 @@ namespace FluidSim.Tests
|
|||||||
|
|
||||||
// ---- Throttle orifice (variable area) ----
|
// ---- Throttle orifice (variable area) ----
|
||||||
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
|
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
|
||||||
areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.001, 1))
|
areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.0001, 1))
|
||||||
{
|
{
|
||||||
DischargeCoefficient = 0.1,
|
DischargeCoefficient = 0.2,
|
||||||
UseInertance = false
|
UseInertance = false
|
||||||
};
|
};
|
||||||
solver.AddOrificeLink(throttleOrifice);
|
solver.AddOrificeLink(throttleOrifice);
|
||||||
@@ -170,7 +171,7 @@ namespace FluidSim.Tests
|
|||||||
|
|
||||||
float exhaustDry = exhaustSoundProcessor.Process(exhaustOpenEnd);
|
float exhaustDry = exhaustSoundProcessor.Process(exhaustOpenEnd);
|
||||||
float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd);
|
float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd);
|
||||||
return reverb.Process(intakeDry + exhaustDry);
|
return reverb.Process(exhaustDry + intakeDry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Draw(RenderWindow target)
|
public override void Draw(RenderWindow target)
|
||||||
|
|||||||
Reference in New Issue
Block a user