using System; using FluidSim.Components; using FluidSim.Utils; using SFML.Graphics; using SFML.System; namespace FluidSim.Core { public class HelmholtzResonatorScenario : Scenario { private Solver solver; private Volume0D cavity; private Pipe1D neck; private PipeVolumeConnection coupling; private int stepCount; private double time; private double dt; private double ambientPressure = 1.0 * Units.atm; public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; // 1‑litre cavity, 10% over‑pressure double cavityVolume = 1e-3; double initialCavityPressure = 1.1 * ambientPressure; cavity = new Volume0D(cavityVolume, initialCavityPressure, 300.0, sampleRate) { Gamma = 1.4, GasConstant = 287.0 }; // Neck: length 10 cm, radius 1 cm double neckLength = 0.1; double neckRadius = 0.01; double neckArea = Math.PI * neckRadius * neckRadius; neck = new Pipe1D(neckLength, neckArea, sampleRate, forcedCellCount: 40); neck.SetUniformState(1.225, 0.0, ambientPressure); // Create the coupling between cavity and left end of the neck (PortA) coupling = new PipeVolumeConnection(cavity, neck, isPipeLeftEnd: true, orificeArea: neckArea); solver = new Solver(); solver.SetTimeStep(dt); solver.AddVolume(cavity); solver.AddPipe(neck); solver.AddConnection(coupling); // Left boundary (PortA) is volume‑coupled via ghost cell, right boundary (PortB) is open end solver.SetPipeBoundary(neck, isA: true, BoundaryType.GhostCell); solver.SetPipeBoundary(neck, isA: false, BoundaryType.OpenEnd, ambientPressure); } public override float Process() { float sample = solver.Step(); time += dt; stepCount++; double pOpen = neck.GetCellPressure(neck.GetCellCount() - 1); float audio = (float)((pOpen - ambientPressure) / ambientPressure); if (stepCount % 20 == 0) { double pCav = cavity.Pressure; // Mass flow rate is not directly available – we can compute from pressure difference or skip Console.WriteLine( $"t={time * 1e3:F2} ms step={stepCount} " + $"P_cav={pCav:F1} Pa, P_open={pOpen:F1} Pa, " + $"audio={audio:F4}"); } return audio; } public override void Draw(RenderWindow target) { float winW = target.GetView().Size.X; float winH = target.GetView().Size.Y; float centerY = winH / 2f; // Cavity rectangle float cavityWidth = 120f; float cavityHeight = 180f; var cavityRect = new RectangleShape(new Vector2f(cavityWidth, cavityHeight)); cavityRect.Position = new Vector2f(40f, centerY - cavityHeight / 2f); cavityRect.FillColor = PressureColor(cavity.Pressure); target.Draw(cavityRect); // Neck drawn as tapered pipe int n = neck.GetCellCount(); float neckStartX = 40f + cavityWidth + 10f; float neckEndX = winW - 60f; float neckLenPx = neckEndX - neckStartX; float dx = neckLenPx / (n - 1); float baseRadius = 20f; var vertices = new Vertex[n * 2]; for (int i = 0; i < n; i++) { float x = neckStartX + i * dx; double p = neck.GetCellPressure(i); float r = baseRadius * (float)(0.5 + 0.5 * Math.Tanh((p - ambientPressure) / (ambientPressure * 0.2))); if (r < 4f) r = 4f; Color col = PressureColor(p); 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); // Open end indicator var arrow = new CircleShape(8f); arrow.Position = new Vector2f(neckEndX - 4f, centerY - 4f); arrow.FillColor = Color.White; target.Draw(arrow); } private Color PressureColor(double pressure) { double range = ambientPressure * 0.1; double t = Math.Clamp((pressure - ambientPressure) / range, -1.0, 1.0); byte r = (byte)(t > 0 ? 255 * t : 0); byte b = (byte)(t < 0 ? -255 * t : 0); byte g = (byte)(255 * (1 - Math.Abs(t))); return new Color(r, g, b); } } }