General testing
This commit is contained in:
46
Core/NozzleFlow.cs
Normal file
46
Core/NozzleFlow.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using FluidSim.Components;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
public static class NozzleFlow
|
||||
{
|
||||
public static void Compute(Volume0D vol, double area, double downstreamPressure,
|
||||
out double massFlow, out double rhoFace, out double uFace, out double pFace,
|
||||
double gamma = 1.4)
|
||||
{
|
||||
// Default fallback (no flow)
|
||||
massFlow = 0.0;
|
||||
rhoFace = 0.0;
|
||||
uFace = 0.0;
|
||||
pFace = 0.0;
|
||||
|
||||
if (vol == null || vol.Mass <= 0 || vol.Volume <= 0)
|
||||
return;
|
||||
|
||||
double p0 = vol.Pressure;
|
||||
double T0 = vol.Temperature;
|
||||
double R = vol.GasConstant;
|
||||
double rho0 = vol.Density;
|
||||
|
||||
if (double.IsNaN(p0) || double.IsNaN(T0) || double.IsNaN(rho0) ||
|
||||
p0 <= 0 || T0 <= 0 || rho0 <= 0)
|
||||
return;
|
||||
|
||||
double pr = downstreamPressure / p0;
|
||||
double choked = Math.Pow(2.0 / (gamma + 1.0), gamma / (gamma - 1.0));
|
||||
if (pr < choked) pr = choked;
|
||||
|
||||
double M = Math.Sqrt((2.0 / (gamma - 1.0)) * (Math.Pow(pr, -(gamma - 1.0) / gamma) - 1.0));
|
||||
if (double.IsNaN(M)) return;
|
||||
|
||||
uFace = M * Math.Sqrt(gamma * R * T0);
|
||||
rhoFace = rho0 * Math.Pow(pr, 1.0 / gamma);
|
||||
pFace = p0 * pr;
|
||||
|
||||
massFlow = rhoFace * uFace * area;
|
||||
if (double.IsNaN(massFlow) || double.IsInfinity(massFlow))
|
||||
massFlow = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Core/PipeVolumeConnection.cs
Normal file
20
Core/PipeVolumeConnection.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using FluidSim.Components;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
public class PipeVolumeConnection
|
||||
{
|
||||
public Volume0D Volume { get; }
|
||||
public Pipe1D Pipe { get; }
|
||||
public bool IsPipeLeftEnd { get; }
|
||||
public double OrificeArea { get; set; }
|
||||
|
||||
public PipeVolumeConnection(Volume0D vol, Pipe1D pipe, bool isPipeLeftEnd, double orificeArea)
|
||||
{
|
||||
Volume = vol;
|
||||
Pipe = pipe;
|
||||
IsPipeLeftEnd = isPipeLeftEnd;
|
||||
OrificeArea = orificeArea;
|
||||
}
|
||||
}
|
||||
}
|
||||
157
Core/Solver.cs
157
Core/Solver.cs
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluidSim.Components;
|
||||
using FluidSim.Interfaces;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
@@ -9,162 +8,80 @@ namespace FluidSim.Core
|
||||
{
|
||||
private readonly List<Volume0D> _volumes = new();
|
||||
private readonly List<Pipe1D> _pipes = new();
|
||||
private readonly List<Connection> _connections = new();
|
||||
private readonly List<PipeVolumeConnection> _connections = new();
|
||||
|
||||
private double _dt;
|
||||
private double _ambientPressure = 101325.0;
|
||||
|
||||
public void SetAmbientPressure(double p) => _ambientPressure = p;
|
||||
public void AddVolume(Volume0D v) => _volumes.Add(v);
|
||||
public void AddPipe(Pipe1D p) => _pipes.Add(p);
|
||||
public void AddConnection(Connection c) => _connections.Add(c);
|
||||
public void AddConnection(PipeVolumeConnection c) => _connections.Add(c);
|
||||
public void SetTimeStep(double dt) => _dt = dt;
|
||||
|
||||
/// <summary>
|
||||
/// Set boundary type for a pipe end. isA = true for port A (left), false for port B (right).
|
||||
/// </summary>
|
||||
public void SetPipeBoundary(Pipe1D pipe, bool isA, BoundaryType type, double ambientPressure = 101325.0)
|
||||
{
|
||||
if (isA)
|
||||
{
|
||||
pipe.SetABoundaryType(type);
|
||||
if (type == BoundaryType.OpenEnd)
|
||||
pipe.SetAAmbientPressure(ambientPressure);
|
||||
if (type == BoundaryType.OpenEnd) pipe.SetAAmbientPressure(ambientPressure);
|
||||
}
|
||||
else
|
||||
{
|
||||
pipe.SetBBoundaryType(type);
|
||||
if (type == BoundaryType.OpenEnd)
|
||||
pipe.SetBAmbientPressure(ambientPressure);
|
||||
if (type == BoundaryType.OpenEnd) pipe.SetBAmbientPressure(ambientPressure);
|
||||
}
|
||||
}
|
||||
|
||||
public float Step()
|
||||
{
|
||||
// 1. Volumes publish state
|
||||
foreach (var v in _volumes)
|
||||
v.PushStateToPort();
|
||||
|
||||
// 2. Set volume BCs for volume‑coupled ends
|
||||
// 1. Compute nozzle flows and update volumes (once per audio sample)
|
||||
foreach (var conn in _connections)
|
||||
{
|
||||
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB))
|
||||
{
|
||||
var pipe = GetPipe(conn.PortA);
|
||||
bool isA = pipe.PortA == conn.PortA;
|
||||
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
|
||||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
|
||||
SetVolumeBC(conn.PortA, conn.PortB);
|
||||
}
|
||||
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB))
|
||||
{
|
||||
var pipe = GetPipe(conn.PortB);
|
||||
bool isA = pipe.PortB == conn.PortB;
|
||||
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
|
||||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
|
||||
SetVolumeBC(conn.PortB, conn.PortA);
|
||||
}
|
||||
double downstreamPressure = conn.IsPipeLeftEnd
|
||||
? conn.Pipe.GetCellPressure(0)
|
||||
: conn.Pipe.GetCellPressure(conn.Pipe.GetCellCount() - 1);
|
||||
|
||||
NozzleFlow.Compute(conn.Volume, conn.OrificeArea, downstreamPressure,
|
||||
out double mdot, out double rhoFace, out double uFace, out double pFace,
|
||||
gamma: conn.Volume.Gamma);
|
||||
|
||||
// Limit mass flow to available mass
|
||||
double maxMdot = conn.Volume.Mass / _dt;
|
||||
if (mdot > maxMdot) mdot = maxMdot;
|
||||
if (mdot < -maxMdot) mdot = -maxMdot;
|
||||
|
||||
conn.Volume.MassFlowRateIn = -mdot;
|
||||
conn.Volume.SpecificEnthalpyIn = (conn.Volume.Gamma / (conn.Volume.Gamma - 1.0)) *
|
||||
(conn.Volume.Pressure / Math.Max(conn.Volume.Density, 1e-12));
|
||||
conn.Volume.Integrate(_dt);
|
||||
|
||||
if (conn.IsPipeLeftEnd)
|
||||
conn.Pipe.SetGhostLeft(rhoFace, uFace, pFace);
|
||||
else
|
||||
conn.Pipe.SetGhostRight(rhoFace, uFace, pFace);
|
||||
}
|
||||
|
||||
// 3. Sub‑steps
|
||||
// 2. Determine required sub‑steps
|
||||
int nSub = 1;
|
||||
foreach (var p in _pipes)
|
||||
nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt));
|
||||
double dtSub = _dt / nSub;
|
||||
|
||||
// 3. Sub‑step loop for pipes
|
||||
for (int sub = 0; sub < nSub; sub++)
|
||||
{
|
||||
foreach (var p in _pipes)
|
||||
p.SimulateSingleStep(dtSub);
|
||||
|
||||
foreach (var conn in _connections)
|
||||
{
|
||||
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB))
|
||||
{
|
||||
var pipe = GetPipe(conn.PortA);
|
||||
bool isA = pipe.PortA == conn.PortA;
|
||||
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
|
||||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
|
||||
TransferAndIntegrate(conn.PortA, conn.PortB, dtSub);
|
||||
}
|
||||
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB))
|
||||
{
|
||||
var pipe = GetPipe(conn.PortB);
|
||||
bool isA = pipe.PortB == conn.PortB;
|
||||
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
|
||||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
|
||||
TransferAndIntegrate(conn.PortB, conn.PortA, dtSub);
|
||||
}
|
||||
}
|
||||
|
||||
if (sub < nSub - 1)
|
||||
{
|
||||
foreach (var v in _volumes)
|
||||
v.PushStateToPort();
|
||||
|
||||
foreach (var conn in _connections)
|
||||
{
|
||||
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB))
|
||||
{
|
||||
var pipe = GetPipe(conn.PortA);
|
||||
bool isA = pipe.PortA == conn.PortA;
|
||||
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
|
||||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
|
||||
SetVolumeBC(conn.PortA, conn.PortB);
|
||||
}
|
||||
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB))
|
||||
{
|
||||
var pipe = GetPipe(conn.PortB);
|
||||
bool isA = pipe.PortB == conn.PortB;
|
||||
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
|
||||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
|
||||
SetVolumeBC(conn.PortB, conn.PortA);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Audio samples (none for now, but placeholder)
|
||||
var audioSamples = new List<float>();
|
||||
foreach (var conn in _connections)
|
||||
{
|
||||
if (conn is SoundConnection sc)
|
||||
audioSamples.Add(sc.GetAudioSample());
|
||||
}
|
||||
|
||||
// 6. Clear BC flags
|
||||
// 4. Clear ghost flags
|
||||
foreach (var p in _pipes)
|
||||
p.ClearBC();
|
||||
p.ClearGhostFlag();
|
||||
|
||||
return SoundProcessor.MixAndClip(audioSamples.ToArray());
|
||||
}
|
||||
// 5. Return raw mass flow from the first pipe’s open end (assumed exhaust tailpipe)
|
||||
if (_pipes.Count > 0)
|
||||
return (float)_pipes[0].GetOpenEndMassFlow();
|
||||
|
||||
private bool IsVolumePort(Port p) => _volumes.Exists(v => v.Port == p);
|
||||
private bool IsPipePort(Port p) => _pipes.Exists(pp => pp.PortA == p || pp.PortB == p);
|
||||
private Pipe1D GetPipe(Port p) => _pipes.Find(pp => pp.PortA == p || pp.PortB == p);
|
||||
private Volume0D GetVolume(Port p) => _volumes.Find(v => v.Port == p);
|
||||
|
||||
private void SetVolumeBC(Port pipePort, Port volPort)
|
||||
{
|
||||
var pipe = GetPipe(pipePort);
|
||||
if (pipe == null) return;
|
||||
bool isA = pipe.PortA == pipePort;
|
||||
if (isA)
|
||||
pipe.SetAVolumeState(volPort.Density, volPort.Pressure);
|
||||
else
|
||||
pipe.SetBVolumeState(volPort.Density, volPort.Pressure);
|
||||
}
|
||||
|
||||
private void TransferAndIntegrate(Port pipePort, Port volPort, double dtSub)
|
||||
{
|
||||
double mdot = pipePort.MassFlowRate;
|
||||
volPort.MassFlowRate = -mdot;
|
||||
|
||||
if (mdot < 0) // pipe → volume
|
||||
{
|
||||
volPort.SpecificEnthalpy = pipePort.SpecificEnthalpy;
|
||||
}
|
||||
// else volume’s own enthalpy (from PushStateToPort) is used
|
||||
|
||||
GetVolume(volPort)?.Integrate(dtSub);
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,155 @@
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Mixes multiple audio samples and applies a soft‑clipping tanh.
|
||||
/// </summary>
|
||||
public static class SoundProcessor
|
||||
{
|
||||
/// <summary>Overall gain applied after mixing (before tanh).</summary>
|
||||
public static float MasterGain { get; set; } = 0.01f;
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Mixes an array of raw audio samples and returns a single sample in [‑1, 1].
|
||||
/// </summary>
|
||||
public static float MixAndClip(params float[] samples)
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
public class SoundProcessor
|
||||
{
|
||||
// Monopole state
|
||||
private double lastMassFlow = 0.0;
|
||||
private double dt;
|
||||
|
||||
// Resonant band‑pass filter (second‑order)
|
||||
private double b0, b1, b2, a1, a2;
|
||||
private double x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
||||
private double pipeLength;
|
||||
|
||||
// Reverb (outdoor)
|
||||
private float[] delayLine;
|
||||
private int writeIndex;
|
||||
private float feedback = 0.50f;
|
||||
private float lowpassCoeff = 0.70f;
|
||||
private float lastFeedbackSample = 0f;
|
||||
|
||||
// Turbulence (pink noise scaled by U³)
|
||||
private PinkNoiseGenerator pinkNoise;
|
||||
private float turbulenceGain = 0.05f;
|
||||
private double pipeArea;
|
||||
private double ambientPressure = 101325.0;
|
||||
|
||||
// Gains
|
||||
private float masterGain = 0.0005f;
|
||||
private float pressureGain = 0.12f;
|
||||
|
||||
public SoundProcessor(int sampleRate, double pipeLengthMeters, double pipeDiameterMeters = 0.04, float reverbTimeMs = 200.0f)
|
||||
{
|
||||
dt = 1.0 / sampleRate;
|
||||
pipeLength = pipeLengthMeters;
|
||||
pipeArea = Math.PI * Math.Pow(pipeDiameterMeters / 2.0, 2.0);
|
||||
|
||||
// Design resonant filter at pipe fundamental frequency
|
||||
double c = 340.0;
|
||||
double f0 = c / (4.0 * pipeLength);
|
||||
double Q = 15.0;
|
||||
double omega = 2.0 * Math.PI * f0;
|
||||
double alpha = Math.Sin(omega * dt) / (2.0 * Q);
|
||||
double norm = 1.0 / (1.0 + alpha);
|
||||
b0 = alpha * norm;
|
||||
b1 = 0.0;
|
||||
b2 = -alpha * norm;
|
||||
a1 = -2.0 * Math.Cos(omega * dt) * norm;
|
||||
a2 = (1.0 - alpha) * norm;
|
||||
|
||||
// Reverb delay line
|
||||
int delaySamples = (int)(sampleRate * reverbTimeMs / 1000.0);
|
||||
delayLine = new float[delaySamples];
|
||||
writeIndex = 0;
|
||||
|
||||
pinkNoise = new PinkNoiseGenerator();
|
||||
}
|
||||
|
||||
public float MasterGain
|
||||
{
|
||||
get => masterGain;
|
||||
set => masterGain = value;
|
||||
}
|
||||
|
||||
public float PressureGain
|
||||
{
|
||||
get => pressureGain;
|
||||
set => pressureGain = value;
|
||||
}
|
||||
|
||||
public float TurbulenceGain
|
||||
{
|
||||
get => turbulenceGain;
|
||||
set => turbulenceGain = value;
|
||||
}
|
||||
|
||||
public void SetAmbientPressure(double p) => ambientPressure = p;
|
||||
public void SetPipeDiameter(double diameterMeters) => pipeArea = Math.PI * Math.Pow(diameterMeters / 2.0, 2.0);
|
||||
|
||||
public float Process(float massFlow, float pipeEndPressure)
|
||||
{
|
||||
// 1. Monopole source: d(mdot)/dt
|
||||
double derivative = (massFlow - lastMassFlow) / dt;
|
||||
lastMassFlow = massFlow;
|
||||
float monopole = (float)(derivative * masterGain);
|
||||
|
||||
// 2. Pressure difference (low‑frequency component)
|
||||
float pressureDiff = (float)((pipeEndPressure - ambientPressure) / ambientPressure) * pressureGain;
|
||||
float mixed = monopole + pressureDiff;
|
||||
// DO NOT clamp here – let the filter and final clamp handle dynamics
|
||||
|
||||
// 3. Resonant band‑pass filter
|
||||
double y = b0 * mixed + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
|
||||
x2 = x1; x1 = mixed;
|
||||
y2 = y1; y1 = y;
|
||||
float resonant = (float)Math.Clamp(y, -1f, 1f);
|
||||
|
||||
// 4. Turbulence noise: amplitude ∝ U³ (empirical for low speeds)
|
||||
double velocity = massFlow / (pipeArea * 1.225);
|
||||
double Uref = 100.0;
|
||||
double turbulenceAmp = Math.Pow(Math.Abs(velocity) / Uref, 3.0);
|
||||
float pink = pinkNoise.Next() * turbulenceGain * (float)turbulenceAmp;
|
||||
|
||||
resonant += pink;
|
||||
resonant = Math.Clamp(resonant, -1f, 1f);
|
||||
|
||||
// 5. Outdoor reverb
|
||||
float delayed = delayLine[writeIndex];
|
||||
float filteredDelay = delayed * lowpassCoeff + lastFeedbackSample * (1f - lowpassCoeff);
|
||||
lastFeedbackSample = filteredDelay;
|
||||
float wet = delayed + filteredDelay * feedback;
|
||||
delayLine[writeIndex] = resonant + filteredDelay * feedback;
|
||||
writeIndex = (writeIndex + 1) % delayLine.Length;
|
||||
|
||||
// 6. Dry/wet mix
|
||||
float output = resonant * 0.7f + wet * 0.3f;
|
||||
output = Math.Clamp(output, -1f, 1f);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
internal class PinkNoiseGenerator
|
||||
{
|
||||
private readonly Random random = new Random();
|
||||
private readonly float[] whiteNoise = new float[7];
|
||||
private int currentIndex = 0;
|
||||
|
||||
public PinkNoiseGenerator()
|
||||
{
|
||||
for (int i = 0; i < 7; i++)
|
||||
whiteNoise[i] = (float)(random.NextDouble() * 2.0 - 1.0);
|
||||
}
|
||||
|
||||
public float Next()
|
||||
{
|
||||
whiteNoise[0] = (float)(random.NextDouble() * 2.0 - 1.0);
|
||||
currentIndex = (currentIndex + 1) & 0x7F;
|
||||
int updateMask = 0;
|
||||
int temp = currentIndex;
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
if ((temp & 1) == 0)
|
||||
updateMask |= (1 << i);
|
||||
temp >>= 1;
|
||||
}
|
||||
for (int i = 1; i < 7; i++)
|
||||
if ((updateMask & (1 << i)) != 0)
|
||||
whiteNoise[i] = (float)(random.NextDouble() * 2.0 - 1.0);
|
||||
float sum = 0f;
|
||||
foreach (float s in samples)
|
||||
sum += s;
|
||||
sum *= MasterGain;
|
||||
return sum;
|
||||
for (int i = 0; i < 7; i++) sum += whiteNoise[i];
|
||||
return sum / 3.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user