Engine working
This commit is contained in:
228
Core/Junction.cs
228
Core/Junction.cs
@@ -1,228 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluidSim.Components;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Zero‑dimensional junction connecting multiple pipe ends.
|
||||
/// The coupling conditions are mass conservation and equality of
|
||||
/// stagnation enthalpy (Bernoulli invariant) for all branches,
|
||||
/// following Reigstad (2014, 2015). A root‑finding method (Brent)
|
||||
/// solves for the common junction pressure.
|
||||
/// </summary>
|
||||
public class Junction
|
||||
{
|
||||
public struct Branch
|
||||
{
|
||||
public Pipe1D Pipe;
|
||||
public bool IsLeftEnd;
|
||||
}
|
||||
|
||||
private readonly List<Branch> _branches = new List<Branch>();
|
||||
public IReadOnlyList<Branch> Branches => _branches;
|
||||
|
||||
// Last resolved state (for audio / monitoring)
|
||||
public double LastJunctionPressure { get; private set; }
|
||||
public double[] LastBranchMassFlows { get; private set; } = Array.Empty<double>();
|
||||
|
||||
public Junction() { }
|
||||
|
||||
public void AddBranch(Pipe1D pipe, bool isLeftEnd)
|
||||
{
|
||||
_branches.Add(new Branch { Pipe = pipe, IsLeftEnd = isLeftEnd });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Solve the junction for one sub‑step. Uses Brent's method to find
|
||||
/// the pressure p* that satisfies sum(mdot) = 0 with stagnation enthalpy equality.
|
||||
/// </summary>
|
||||
public void Resolve(double dtSub)
|
||||
{
|
||||
int nb = _branches.Count;
|
||||
if (nb < 2)
|
||||
throw new InvalidOperationException("Junction requires at least 2 branches.");
|
||||
|
||||
// Gather interior states and areas
|
||||
var rho = new double[nb];
|
||||
var u = new double[nb];
|
||||
var p = new double[nb];
|
||||
var area = new double[nb];
|
||||
var isLeft = new bool[nb];
|
||||
double gamma = 1.4;
|
||||
|
||||
double pMin = double.MaxValue, pMax = double.MinValue;
|
||||
for (int i = 0; i < nb; i++)
|
||||
{
|
||||
var branch = _branches[i];
|
||||
(double ri, double ui, double pi) = branch.IsLeftEnd
|
||||
? branch.Pipe.GetInteriorStateLeft()
|
||||
: branch.Pipe.GetInteriorStateRight();
|
||||
rho[i] = ri; u[i] = ui; p[i] = pi;
|
||||
area[i] = branch.Pipe.Area;
|
||||
isLeft[i] = branch.IsLeftEnd;
|
||||
|
||||
if (pi < pMin) pMin = pi;
|
||||
if (pi > pMax) pMax = pi;
|
||||
}
|
||||
|
||||
// We solve for pStar that makes totalMassFlow(pStar) = 0.
|
||||
// The function: totalMassFlow = sum( sign_i * rhoStar_i * uStar_i * A_i )
|
||||
// where for each branch:
|
||||
// - Riemann invariant: J = u + 2c/(γ-1) for right end, J = u - 2c/(γ-1) for left end.
|
||||
// - uStar = J ∓ 2cStar/(γ-1) (depending on direction)
|
||||
// - Isentropic relation: rhoStar = rho_i * (pStar / p_i)^{1/γ}
|
||||
// - cStar = sqrt(γ pStar / rhoStar)
|
||||
// We require stagnation enthalpy equality: h0 = h + u^2/2 = constant across junction.
|
||||
// Hence for each branch we compute the specific total enthalpy:
|
||||
// hStar = (γ/(γ-1)) * pStar/rhoStar, h0_star = hStar + 0.5 uStar^2.
|
||||
// We enforce that all h0_star are equal. Mass conservation then determines pStar.
|
||||
// This is a scalar root‑finding problem.
|
||||
|
||||
// Bracket the solution: pressure must lie between min and max of branch pressures (expanded a bit)
|
||||
double a = Math.Max(100.0, pMin * 0.1);
|
||||
double b = Math.Min(1e7, pMax * 10.0);
|
||||
if (a >= b) { a = 100.0; b = 1e7; }
|
||||
|
||||
Func<double, double> f = pStar =>
|
||||
{
|
||||
double totalMdot = 0.0;
|
||||
double h0Ref = 0.0;
|
||||
bool first = true;
|
||||
for (int i = 0; i < nb; i++)
|
||||
{
|
||||
double g = gamma;
|
||||
double gm1 = g - 1.0;
|
||||
double rhoI = rho[i], uI = u[i], pI = p[i];
|
||||
double cI = Math.Sqrt(g * pI / rhoI);
|
||||
double J = isLeft[i] ? uI - 2.0 * cI / gm1 : uI + 2.0 * cI / gm1;
|
||||
|
||||
double pratio = Math.Max(pStar / pI, 1e-6);
|
||||
double rhoStar = rhoI * Math.Pow(pratio, 1.0 / g);
|
||||
double cStar = Math.Sqrt(g * pStar / rhoStar);
|
||||
double uStar = isLeft[i] ? J + 2.0 * cStar / gm1 : J - 2.0 * cStar / gm1;
|
||||
|
||||
double hStar = (g / gm1) * pStar / rhoStar;
|
||||
double h0 = hStar + 0.5 * uStar * uStar;
|
||||
|
||||
if (first)
|
||||
{
|
||||
h0Ref = h0;
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Equality of stagnation enthalpy: ideally h0 == h0Ref.
|
||||
// We incorporate a penalty to enforce this.
|
||||
}
|
||||
|
||||
// Mass flow into junction: sign convention = positive if fluid leaves pipe into junction.
|
||||
double sign = isLeft[i] ? -1.0 : 1.0; // left end: positive u is into pipe, so into junction is -u
|
||||
double mdot_i = sign * rhoStar * uStar * area[i];
|
||||
totalMdot += mdot_i;
|
||||
}
|
||||
|
||||
// Additional term to enforce equal stagnation enthalpies? For simplicity, we only enforce mass conservation here,
|
||||
// because with the Riemann invariants and a common pressure, the stagnation enthalpies are automatically equal
|
||||
// if the junction is isentropic? Actually, with a common pressure and isentropic relations from each branch,
|
||||
// each branch has its own entropy (p/ρ^γ = const), so h0 may differ. The correct condition is mass conservation + equality of h0.
|
||||
// To solve both, we would need to vary pStar and a common h0? In Reigstad's formulation, the system yields
|
||||
// mass conservation as the determinant, and pStar is found from that equation, with the assumption that the junction
|
||||
// itself does not introduce entropy. The typical implementation uses the Riemann invariants and mass conservation only.
|
||||
// We'll stick to mass conservation for now.
|
||||
return totalMdot;
|
||||
};
|
||||
|
||||
double pStar = BrentsMethod(f, a, b, 1e-6, 100);
|
||||
LastJunctionPressure = pStar;
|
||||
LastBranchMassFlows = new double[nb];
|
||||
|
||||
// Apply ghost states and record mass flows
|
||||
for (int i = 0; i < nb; i++)
|
||||
{
|
||||
double g = gamma, gm1 = g - 1.0;
|
||||
double rhoI = rho[i], uI = u[i], pI = p[i];
|
||||
double cI = Math.Sqrt(g * pI / rhoI);
|
||||
double J = isLeft[i] ? uI - 2.0 * cI / gm1 : uI + 2.0 * cI / gm1;
|
||||
|
||||
double pratio = Math.Max(pStar / pI, 1e-6);
|
||||
double rhoStar = rhoI * Math.Pow(pratio, 1.0 / g);
|
||||
double cStar = Math.Sqrt(g * pStar / rhoStar);
|
||||
double uStar = isLeft[i] ? J + 2.0 * cStar / gm1 : J - 2.0 * cStar / gm1;
|
||||
|
||||
double sign = isLeft[i] ? -1.0 : 1.0;
|
||||
double mdot = sign * rhoStar * uStar * area[i];
|
||||
LastBranchMassFlows[i] = mdot;
|
||||
|
||||
if (isLeft[i])
|
||||
_branches[i].Pipe.SetGhostLeft(rhoStar, uStar, pStar);
|
||||
else
|
||||
_branches[i].Pipe.SetGhostRight(rhoStar, uStar, pStar);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Simple Brent's method root finder.</summary>
|
||||
private static double BrentsMethod(Func<double, double> f, double a, double b, double tol, int maxIter)
|
||||
{
|
||||
double fa = f(a), fb = f(b);
|
||||
if (fa * fb >= 0)
|
||||
return (a + b) / 2.0; // fallback
|
||||
|
||||
double c = a, fc = fa;
|
||||
double d = b - a, e = d;
|
||||
|
||||
for (int iter = 0; iter < maxIter; iter++)
|
||||
{
|
||||
if (Math.Abs(fc) < Math.Abs(fb))
|
||||
{
|
||||
a = b; b = c; c = a;
|
||||
fa = fb; fb = fc; fc = fa;
|
||||
}
|
||||
double tol1 = 2 * double.Epsilon * Math.Abs(b) + 0.5 * tol;
|
||||
double xm = 0.5 * (c - b);
|
||||
if (Math.Abs(xm) <= tol1 || fb == 0.0)
|
||||
return b;
|
||||
|
||||
if (Math.Abs(e) >= tol1 && Math.Abs(fa) > Math.Abs(fb))
|
||||
{
|
||||
double s = fb / fa;
|
||||
double p, q;
|
||||
if (a == c)
|
||||
{
|
||||
p = 2.0 * xm * s;
|
||||
q = 1.0 - s;
|
||||
}
|
||||
else
|
||||
{
|
||||
q = fa / fc;
|
||||
double r = fb / fc;
|
||||
p = s * (2.0 * xm * q * (q - r) - (b - a) * (r - 1.0));
|
||||
q = (q - 1.0) * (r - 1.0) * (s - 1.0);
|
||||
}
|
||||
if (p > 0) q = -q; else p = -p;
|
||||
s = e; e = d;
|
||||
if (2.0 * p < 3.0 * xm * q - Math.Abs(tol1 * q) && p < Math.Abs(0.5 * s * q))
|
||||
{
|
||||
d = p / q;
|
||||
}
|
||||
else
|
||||
{
|
||||
d = xm; e = d;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
d = xm; e = d;
|
||||
}
|
||||
|
||||
a = b; fa = fb;
|
||||
if (Math.Abs(d) > tol1)
|
||||
b += d;
|
||||
else
|
||||
b += Math.Sign(xm) * tol1;
|
||||
fb = f(b);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ namespace FluidSim.Core
|
||||
/// from the isentropic expansion to ambient pressure, using the pipe's entropy,
|
||||
/// and the outgoing Riemann invariant. This avoids a density jump at flow reversal.
|
||||
/// Supersonic outflow extrapolates the interior state.
|
||||
/// Now includes air fraction tracking: incoming air is fresh (AF=1), outgoing uses interior pipe AF.
|
||||
/// </summary>
|
||||
public class OpenEndLink
|
||||
{
|
||||
@@ -34,34 +35,34 @@ namespace FluidSim.Core
|
||||
? Pipe.GetInteriorStateLeft()
|
||||
: Pipe.GetInteriorStateRight();
|
||||
|
||||
double airFracInt = IsLeftEnd
|
||||
? Pipe.GetInteriorAirFractionLeft()
|
||||
: Pipe.GetInteriorAirFractionRight();
|
||||
|
||||
double gamma = Gamma;
|
||||
double gm1 = gamma - 1.0;
|
||||
double cInt = Math.Sqrt(gamma * pInt / Math.Max(rhoInt, 1e-12));
|
||||
double pAmb = AmbientPressure;
|
||||
|
||||
// Riemann invariants
|
||||
double J_plus = uInt + 2.0 * cInt / gm1;
|
||||
double J_plus = uInt + 2.0 * cInt / gm1;
|
||||
double J_minus = uInt - 2.0 * cInt / gm1;
|
||||
|
||||
double rhoGhost, uGhost, pGhost;
|
||||
double rhoGhost, uGhost, pGhost, airFracGhost;
|
||||
|
||||
// ---- Subsonic branch (used for both outflow and inflow) ----
|
||||
// Isentropic expansion to ambient pressure using pipe's entropy
|
||||
double s = pInt / Math.Pow(rhoInt, gamma); // entropy constant
|
||||
double rhoIso = Math.Pow(pAmb / s, 1.0 / gamma);
|
||||
double cIso = Math.Sqrt(gamma * pAmb / Math.Max(rhoIso, 1e-12));
|
||||
double uIso = IsLeftEnd
|
||||
? (J_minus + 2.0 * cIso / gm1)
|
||||
: (J_plus - 2.0 * cIso / gm1);
|
||||
: (J_plus - 2.0 * cIso / gm1);
|
||||
|
||||
// Check for supersonic outflow: if the isentropic velocity exceeds the speed of sound,
|
||||
// the flow is supersonic and we extrapolate the interior state.
|
||||
// Check for supersonic outflow
|
||||
bool supersonic = IsLeftEnd
|
||||
? (uInt <= -cInt) // left end: outflow is when u < -c
|
||||
: (uInt >= cInt); // right end: outflow is when u > c
|
||||
? (uInt <= -cInt)
|
||||
: (uInt >= cInt);
|
||||
|
||||
// Extra check: if the isentropic velocity is supersonic in the outflow direction,
|
||||
// also treat as supersonic (this can happen when the interior pressure is very high).
|
||||
if (!supersonic)
|
||||
{
|
||||
if (IsLeftEnd)
|
||||
@@ -74,27 +75,42 @@ namespace FluidSim.Core
|
||||
{
|
||||
// Supersonic outflow – extrapolate interior
|
||||
rhoGhost = rhoInt;
|
||||
uGhost = uInt;
|
||||
pGhost = pInt;
|
||||
uGhost = uInt;
|
||||
pGhost = pInt;
|
||||
airFracGhost = airFracInt; // whatever is leaving
|
||||
}
|
||||
else
|
||||
{
|
||||
// Subsonic flow – use the isentropic state
|
||||
// Subsonic flow – use isentropic state to ambient
|
||||
rhoGhost = rhoIso;
|
||||
uGhost = uIso;
|
||||
pGhost = pAmb;
|
||||
uGhost = uIso;
|
||||
pGhost = pAmb;
|
||||
|
||||
// Determine if inflow or outflow
|
||||
bool isInflow = IsLeftEnd ? (uIso >= 0) : (uIso <= 0); // positive u means into pipe from left end? Wait: left end: u>0 means flow to the right, into pipe. Right end: u>0 means flow to the right, out of pipe. Let's use mass flow sign later.
|
||||
// More straightforward: if using the isentropic state, the ghost velocity direction indicates flow. For inflow (ambient to pipe), airFraction = 1.0; for outflow, airFraction = interior's AF.
|
||||
if ((IsLeftEnd && uIso >= 0) || (!IsLeftEnd && uIso <= 0))
|
||||
{
|
||||
// Inflow (ambient enters pipe)
|
||||
airFracGhost = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Outflow (pipe exits to ambient)
|
||||
airFracGhost = airFracInt;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply ghost to pipe
|
||||
if (IsLeftEnd)
|
||||
Pipe.SetGhostLeft(rhoGhost, uGhost, pGhost);
|
||||
Pipe.SetGhostLeft(rhoGhost, uGhost, pGhost, airFracGhost);
|
||||
else
|
||||
Pipe.SetGhostRight(rhoGhost, uGhost, pGhost);
|
||||
Pipe.SetGhostRight(rhoGhost, uGhost, pGhost, airFracGhost);
|
||||
|
||||
// Mass flow out of the pipe (positive = leaving)
|
||||
double area = Pipe.Area;
|
||||
double mdot = rhoGhost * uGhost * area;
|
||||
if (IsLeftEnd) mdot = -mdot; // left end: positive u is into pipe, so out is -u
|
||||
if (IsLeftEnd) mdot = -mdot; // left end: positive u is into pipe, outward flow is -u
|
||||
LastMassFlowRate = mdot;
|
||||
LastFaceDensity = rhoGhost;
|
||||
LastFaceVelocity = uGhost;
|
||||
|
||||
@@ -46,16 +46,21 @@ namespace FluidSim.Core
|
||||
double volRho = VolumePort.Density;
|
||||
double volT = VolumePort.Temperature;
|
||||
double volH = VolumePort.SpecificEnthalpy;
|
||||
double volAF = VolumePort.AirFraction;
|
||||
|
||||
(double pipeRho, double pipeU, double pipeP) = IsPipeLeftEnd
|
||||
? Pipe.GetInteriorStateLeft()
|
||||
: Pipe.GetInteriorStateRight();
|
||||
|
||||
double pipeT = pipeP / Math.Max(pipeRho * 287.0, 1e-12);
|
||||
double pipeAF = IsPipeLeftEnd
|
||||
? Pipe.GetInteriorAirFractionLeft()
|
||||
: Pipe.GetInteriorAirFractionRight();
|
||||
|
||||
double gamma = 1.4;
|
||||
double R = 287.0;
|
||||
|
||||
// ---- Steady‑state nozzle solution (gives correct exit state) ----
|
||||
// ---- Steady‑state nozzle solution ----
|
||||
double mdotSS; // positive = volume → pipe
|
||||
double rhoFace0, uFace0, pFace0;
|
||||
if (volP >= pipeP)
|
||||
@@ -71,15 +76,6 @@ namespace FluidSim.Core
|
||||
mdotSS = -mdotUpToDown;
|
||||
}
|
||||
|
||||
// ====== Hard physical cap: max sonic flow × 1.1 ======
|
||||
double upRho = mdotSS >= 0 ? volRho : pipeRho;
|
||||
double upT = mdotSS >= 0 ? volT : pipeT;
|
||||
double upC = Math.Sqrt(gamma * R * upT);
|
||||
double maxFlow = upRho * upC * area * 1.1;
|
||||
if (Math.Abs(mdotSS) > maxFlow)
|
||||
mdotSS = Math.Sign(mdotSS) * maxFlow;
|
||||
// ====================================================
|
||||
|
||||
// ---- Dynamic update ----
|
||||
if (UseInertance)
|
||||
{
|
||||
@@ -102,21 +98,38 @@ namespace FluidSim.Core
|
||||
if (_mdot > maxOut) _mdot = maxOut;
|
||||
}
|
||||
|
||||
// ---- Ghost state ----
|
||||
// ---- Ghost state with air fraction ----
|
||||
double rhoFace = _mdot >= 0 ? volRho : pipeRho;
|
||||
double pFace = pFace0;
|
||||
double mdotMag = Math.Abs(_mdot);
|
||||
double uFace = mdotMag / (rhoFace * area);
|
||||
|
||||
// Determine air fraction for ghost and for volume port
|
||||
double airFracGhost; // air fraction of ghost cell (at pipe end)
|
||||
double airFracForVolume; // if flow reverses into volume, this is the air fraction entering volume
|
||||
|
||||
if (_mdot >= 0) // volume → pipe
|
||||
{
|
||||
airFracGhost = volAF;
|
||||
// Flow enters pipe; no need to set volume's air fraction (port already has its own)
|
||||
airFracForVolume = volAF; // unused
|
||||
}
|
||||
else // pipe → volume
|
||||
{
|
||||
airFracGhost = pipeAF;
|
||||
airFracForVolume = pipeAF;
|
||||
VolumePort.AirFraction = airFracForVolume;
|
||||
}
|
||||
|
||||
if (IsPipeLeftEnd)
|
||||
uFace = _mdot >= 0 ? uFace : -uFace;
|
||||
else
|
||||
uFace = _mdot >= 0 ? -uFace : uFace;
|
||||
|
||||
if (IsPipeLeftEnd)
|
||||
Pipe.SetGhostLeft(rhoFace, uFace, pFace);
|
||||
Pipe.SetGhostLeft(rhoFace, uFace, pFace, airFracGhost);
|
||||
else
|
||||
Pipe.SetGhostRight(rhoFace, uFace, pFace);
|
||||
Pipe.SetGhostRight(rhoFace, uFace, pFace, airFracGhost);
|
||||
|
||||
// Store results (positive = into volume)
|
||||
LastMassFlowRate = -_mdot;
|
||||
@@ -145,9 +158,9 @@ namespace FluidSim.Core
|
||||
: Pipe.GetInteriorStateRight();
|
||||
|
||||
if (IsPipeLeftEnd)
|
||||
Pipe.SetGhostLeft(rInt, -uInt, pInt);
|
||||
Pipe.SetGhostLeft(rInt, -uInt, pInt, IsPipeLeftEnd ? Pipe.GetInteriorAirFractionLeft() : Pipe.GetInteriorAirFractionRight());
|
||||
else
|
||||
Pipe.SetGhostRight(rInt, -uInt, pInt);
|
||||
Pipe.SetGhostRight(rInt, -uInt, pInt, IsPipeLeftEnd ? Pipe.GetInteriorAirFractionLeft() : Pipe.GetInteriorAirFractionRight());
|
||||
|
||||
LastMassFlowRate = 0.0;
|
||||
LastFaceDensity = rInt;
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace FluidSim.Core
|
||||
public float DryMix { get; set; } = 1.0f;
|
||||
public float EarlyMix { get; set; } = 0.5f;
|
||||
public float TailMix { get; set; } = 0.9f;
|
||||
public float Feedback { get; set; } = 0.75f; // safe range 0.7‑0.9
|
||||
public float Feedback { get; set; } = 0.55f; // safe range 0.7‑0.9
|
||||
public float DampingFreq { get; set; } = 6000f; // Hz
|
||||
|
||||
public OutdoorExhaustReverb(int sampleRate)
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace FluidSim.Core
|
||||
{
|
||||
private readonly List<IComponent> _components = new();
|
||||
private readonly List<OrificeLink> _orificeLinks = new();
|
||||
private readonly List<Junction> _junctions = new();
|
||||
private readonly List<OpenEndLink> _openEndLinks = new();
|
||||
|
||||
private double _dt;
|
||||
@@ -37,7 +36,6 @@ namespace FluidSim.Core
|
||||
|
||||
public void AddComponent(IComponent component) => _components.Add(component);
|
||||
public void AddOrificeLink(OrificeLink link) => _orificeLinks.Add(link);
|
||||
public void AddJunction(Junction junction) => _junctions.Add(junction);
|
||||
public void AddOpenEndLink(OpenEndLink link) => _openEndLinks.Add(link);
|
||||
|
||||
public void Step()
|
||||
@@ -76,11 +74,6 @@ namespace FluidSim.Core
|
||||
link.Resolve(dtSub);
|
||||
_timeOpenEnd += sw.Elapsed.TotalSeconds - t0;
|
||||
|
||||
t0 = sw.Elapsed.TotalSeconds;
|
||||
foreach (var junc in _junctions)
|
||||
junc.Resolve(dtSub);
|
||||
_timeJunction += sw.Elapsed.TotalSeconds - t0;
|
||||
|
||||
t0 = sw.Elapsed.TotalSeconds;
|
||||
foreach (var p in pipes)
|
||||
p.SimulateSingleStep(dtSub);
|
||||
|
||||
@@ -3,53 +3,142 @@ using FluidSim.Core;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Synthesises far‑field sound at a listener position from an open pipe end.
|
||||
/// Three source mechanisms are combined:
|
||||
/// 1. Monopole – time derivative of mass flow (dominant at low speed / high pulsation).
|
||||
/// 2. Dipole – time derivative of momentum flux (shear‑layer / vortex shedding).
|
||||
/// 3. Jet noise – Lighthill‑type turbulence mixing noise (scales with U^8).
|
||||
///
|
||||
/// References:
|
||||
/// • Lighthill, M.J. (1952) "On Sound Generated Aerodynamically".
|
||||
/// • Dowling, A.P. & Williams, J.E.F. (1983) "Sound and Sources of Sound".
|
||||
/// • 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>
|
||||
public class SoundProcessor
|
||||
{
|
||||
private readonly double dt;
|
||||
private readonly double scaleFactor; // 1 / (4π r)
|
||||
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 pipeArea; // cross‑sectional area of the pipe end (m²)
|
||||
|
||||
// ---------- monopole state ----------
|
||||
private double flowLP;
|
||||
private readonly double lpAlpha;
|
||||
private double prevMassFlowOut;
|
||||
private double smoothDMdt;
|
||||
private readonly double alpha;
|
||||
|
||||
// New: low‑pass the mass flow signal before derivative
|
||||
private double flowLP;
|
||||
private readonly double lpAlpha;
|
||||
// ---------- 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 SoundProcessor(int sampleRate, double listenerDistanceMeters = 1.0)
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="sampleRate">Audio sample rate (Hz).</param>
|
||||
/// <param name="listenerDistanceMeters">Distance from the pipe exit to the listener (m).</param>
|
||||
/// <param name="pipeDiameterMeters">Internal diameter of the pipe (m).</param>
|
||||
public SoundProcessor(int sampleRate,
|
||||
double listenerDistanceMeters = 1.0,
|
||||
double pipeDiameterMeters = 0.0217) // ~3.7 cm² area
|
||||
{
|
||||
dt = 1.0 / sampleRate;
|
||||
scaleFactor = 1.0 / (4.0 * Math.PI * listenerDistanceMeters);
|
||||
r = listenerDistanceMeters;
|
||||
pipeArea = Math.PI * 0.25 * pipeDiameterMeters * pipeDiameterMeters;
|
||||
|
||||
// Smoothing time constant for the derivative: 10 ms (much smoother)
|
||||
double tau = 0.005; // 10 ms
|
||||
// Ambient air properties
|
||||
c0 = 340.0;
|
||||
rho0 = 1.225;
|
||||
|
||||
// ---- Monopole smoothing ----
|
||||
double tau = 0.002; // 2 ms
|
||||
alpha = Math.Exp(-dt / tau);
|
||||
|
||||
// Low‑pass time constant for the mass flow: 5 ms (kneecap high‑freq directly)
|
||||
double tauLP = 0.005;
|
||||
double tauLP = 0.005; // 5 ms low‑pass on mass flow
|
||||
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>
|
||||
/// Process one sample. The OpenEndLink provides the instantaneous
|
||||
/// exit‑plane mass flow, density, velocity, and pressure.
|
||||
/// </summary>
|
||||
public float Process(OpenEndLink openEnd)
|
||||
{
|
||||
double flowOut = openEnd.LastMassFlowRate;
|
||||
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;
|
||||
|
||||
// Derivative of the smoothed mass flow
|
||||
double rawDerivative = (flowLP - prevMassFlowOut) / dt;
|
||||
prevMassFlowOut = flowLP;
|
||||
|
||||
// Smooth the derivative
|
||||
smoothDMdt = alpha * smoothDMdt + (1.0 - alpha) * rawDerivative;
|
||||
double pMono = smoothDMdt / (4.0 * Math.PI * r * c0);
|
||||
|
||||
// Far‑field monopole pressure
|
||||
double pressure = smoothDMdt * scaleFactor * Gain;
|
||||
// ============================================================
|
||||
// 2. DIPOLE – due to unsteady momentum flux at the exit plane
|
||||
// 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);
|
||||
|
||||
// Soft clip to ±1 (should rarely trigger now)
|
||||
return (float)pressure;
|
||||
// Dipole efficiency factor: ∝ (U / c0)³ (since Idipole ∝ U⁶, pdipole ∝ U³)
|
||||
double Mach = Math.Abs(uExit) / c0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user