diff --git a/Core/Simulation.cs b/Core/Simulation.cs deleted file mode 100644 index 9188aed..0000000 --- a/Core/Simulation.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using FluidSim.Components; -using FluidSim.Interfaces; -using FluidSim.Utils; - -namespace FluidSim.Core -{ - public static class Simulation - { - private static Solver solver; - private static Pipe1D pipe; - private static int stepCount; - private static double time; - private static double dt; - private static float sample; - private static double ambientPressure = 1.0 * Units.atm; - - private static bool enableLogging = false; - - public static void Initialize(int sampleRate) - { - dt = 1.0 / sampleRate; - - double length = 2; - double radius = 20 * Units.mm; - double area = Units.AreaFromDiameter(radius); - - pipe = new Pipe1D(length, area, sampleRate, forcedCellCount: 80); - pipe.SetUniformState(1.225, 0.0, ambientPressure); - - solver = new Solver(); - solver.SetTimeStep(dt); - solver.AddPipe(pipe); - solver.SetPipeBoundary(pipe, isLeft: true, BoundaryType.OpenEnd, ambientPressure); - solver.SetPipeBoundary(pipe, isLeft: false, BoundaryType.ClosedEnd); - - // Excite the pipe with an initial pressure pulse near the open end - int pulseCells = 5; - double pulsePressure = 4 * ambientPressure; - for (int i = 0; i < pulseCells; i++) - pipe.SetCellState(i, 1.225, 0.0, pulsePressure); - } - - public static float Process() - { - sample = solver.Step(); - time += dt; - stepCount++; - - // Override the audio sample with mid-pipe pressure deviation - double pMid = pipe.GetPressureAtFraction(0.5); - sample = (float)((pMid - ambientPressure) / ambientPressure); - - Log(); - return sample; - } - - public static void Log() - { - if (!enableLogging) return; - if (stepCount % 10 == 0 && stepCount < 1000) - { - double pMid = pipe.GetPressureAtFraction(0.5); - double pOpen = pipe.GetCellPressure(0); - double pClosed = pipe.GetCellPressure(pipe.GetCellCount() - 1); - Console.WriteLine( - $"t = {time * 1e3:F3} ms Step {stepCount:D4}: " + - $"Sample: = {sample:F3}, " + - $"P_mid = {pMid:F2} Pa ({pMid / ambientPressure:F4} atm), " + - $"P_open = {pOpen:F2} Pa, P_closed = {pClosed:F2} Pa"); - } - } - } -} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 938ced0..3e7f273 100644 --- a/Program.cs +++ b/Program.cs @@ -9,61 +9,172 @@ namespace FluidSim; public class Program { private const int SampleRate = 44100; + private const double DrawFrequency = 60.0; + private static Scenario scenario; + + // Speed control + private static double desiredSpeed = 1.0; + private static double currentSpeed = desiredSpeed; + private const double MinSpeed = 0.0001; + private const double MaxSpeed = 1.0; + private const double ScrollFactor = 1.1; + + // Space‑toggle state + private static double lastDesiredSpeed = 0.1; // remembers the last non‑1.0 scroll speed + private static bool isRealTime = true; // true when desiredSpeed == 1.0 + private static volatile bool running = true; public static void Main() { var mode = new VideoMode(new Vector2u(1280, 720)); - var window = new RenderWindow(mode, "Fluid Simulation"); + var window = new RenderWindow(mode, "Pipe Resonator"); window.SetVerticalSyncEnabled(true); window.Closed += (_, _) => { running = false; window.Close(); }; + window.MouseWheelScrolled += OnMouseWheel; + window.KeyPressed += OnKeyPressed; - var soundEngine = new SoundEngine(bufferCapacity: 2048); + var soundEngine = new SoundEngine(bufferCapacity: 16384); soundEngine.Volume = 70; soundEngine.Start(); - double lastAudioTime = 0.0; + scenario = new PipeResonatorScenario(); + scenario.Initialize(SampleRate); + var stopwatch = Stopwatch.StartNew(); + double lastDrawTime = 0.0; + double drawInterval = 1.0 / DrawFrequency; + double lastSpeedUpdateTime = stopwatch.Elapsed.TotalSeconds; - int warmupSamples = SampleRate / 2; - float[] warmup = new float[warmupSamples]; - for (int i = 0; i < warmupSamples; i++) - warmup[i] = 0; + // Resampling buffer + List simBuffer = new List(4096); + double readIndex = 0.0; - soundEngine.WriteSamples(warmup, warmupSamples); - lastAudioTime = stopwatch.Elapsed.TotalSeconds; + for (int i = 0; i < 4; i++) + simBuffer.Add(scenario.Process()); - const int chunkSize = 2048; - float[] buffer = new float[chunkSize]; + long totalSimSteps = simBuffer.Count; + long totalOutputSamples = 0; - Simulation.Initialize(SampleRate); + double lastRealTime = stopwatch.Elapsed.TotalSeconds; + const int outputChunk = 256; + float[] outputBuf = new float[outputChunk]; while (window.IsOpen) { window.DispatchEvents(); - double currentTime = stopwatch.Elapsed.TotalSeconds; - double elapsed = currentTime - lastAudioTime; - int samplesNeeded = (int)(elapsed * SampleRate); + double currentRealTime = stopwatch.Elapsed.TotalSeconds; + double dtSpeed = currentRealTime - lastSpeedUpdateTime; + lastSpeedUpdateTime = currentRealTime; - while (samplesNeeded > 0 && running) + // Smoothly transition currentSpeed → desiredSpeed + // When toggling, desiredSpeed jumps, but currentSpeed follows with a smooth lerp + double smoothingRate = 8.0; // higher = faster catch‑up + currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-smoothingRate * dtSpeed)); + + // ---------- Generate audio ---------- + double targetAudioClock = currentRealTime + 0.05; + + while (totalOutputSamples < targetAudioClock * SampleRate && running) { - int toGenerate = Math.Min(samplesNeeded, chunkSize); + int toGenerate = (int)Math.Min( + (long)outputChunk, + (long)(targetAudioClock * SampleRate) - totalOutputSamples + ); + if (toGenerate <= 0) break; + + double maxIndex = readIndex + (toGenerate - 1) * currentSpeed + 2; + int requiredSimIndex = (int)Math.Ceiling(maxIndex); + while (simBuffer.Count - 1 < requiredSimIndex) + { + simBuffer.Add(scenario.Process()); + totalSimSteps++; + } + for (int i = 0; i < toGenerate; i++) { - buffer[i] = Simulation.Process(); + int i0 = (int)readIndex; + int i1 = i0 + 1; + double frac = readIndex - i0; + + float y0 = simBuffer[Math.Clamp(i0, 0, simBuffer.Count - 1)]; + float y1 = simBuffer[Math.Clamp(i1, 0, simBuffer.Count - 1)]; + outputBuf[i] = (float)(y0 + (y1 - y0) * frac); + + readIndex += currentSpeed; + + while (readIndex >= 1.0 && simBuffer.Count > 2) + { + simBuffer.RemoveAt(0); + readIndex -= 1.0; + } } - soundEngine.WriteSamples(buffer, toGenerate); - samplesNeeded -= toGenerate; + + int accepted = soundEngine.WriteSamples(outputBuf, toGenerate); + totalOutputSamples += accepted; + + if (accepted < toGenerate) + break; } - lastAudioTime = currentTime; + // ---------- Drawing & title ---------- + if (currentRealTime - lastDrawTime >= drawInterval) + { + double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate); + double simTime = totalSimSteps / (double)SampleRate; + string toggleHint = isRealTime ? "[Space] slow mo" : "[Space] real time"; + window.SetTitle( + $"{toggleHint} Sim: {simTime:F2}s | " + + $"Speed: {currentSpeed:F4}x → {desiredSpeed:F4}x | " + + $"Actual: {actualSpeed:F2}x" + ); - window.Clear(Color.Black); - window.Display(); + window.Clear(Color.Black); + scenario.Draw(window); + window.Display(); + lastDrawTime = currentRealTime; + } } soundEngine.Dispose(); window.Dispose(); } + + private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e) + { + bool wasRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6; + + if (e.Delta > 0) + desiredSpeed *= ScrollFactor; + else if (e.Delta < 0) + desiredSpeed /= ScrollFactor; + + desiredSpeed = Math.Clamp(desiredSpeed, MinSpeed, MaxSpeed); + + // Update the remembered slow-mo speed (unless we are exactly at 1.0) + if (!wasRealTime || Math.Abs(desiredSpeed - 1.0) > 1e-6) + lastDesiredSpeed = desiredSpeed; + + // Update isRealTime flag + isRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6; + } + + private static void OnKeyPressed(object? sender, KeyEventArgs e) + { + if (e.Code == Keyboard.Key.Space) + { + if (isRealTime) + { + // Switch to the remembered slow speed + desiredSpeed = lastDesiredSpeed; + } + else + { + // Switch back to real time + desiredSpeed = 1.0; + } + isRealTime = !isRealTime; + } + } } \ No newline at end of file diff --git a/Scenarios/PipeResonatorScenario.cs b/Scenarios/PipeResonatorScenario.cs new file mode 100644 index 0000000..5496c14 --- /dev/null +++ b/Scenarios/PipeResonatorScenario.cs @@ -0,0 +1,183 @@ +using FluidSim.Components; +using FluidSim.Interfaces; +using FluidSim.Utils; +using SFML.Graphics; +using SFML.System; +using System; + +namespace FluidSim.Core +{ + public class PipeResonatorScenario : Scenario + { + private Solver solver; + private Pipe1D pipe; + private int stepCount; + private double time; + private double dt; + private double ambientPressure = 1.0 * Units.atm; + private bool enableLogging = true; + + public override void Initialize(int sampleRate) + { + dt = 1.0 / sampleRate; + + double length = 0.5; + double radius = 50 * Units.mm; + double area = Units.AreaFromDiameter(radius); + + pipe = new Pipe1D(length, area, sampleRate, forcedCellCount: 80); + pipe.SetUniformState(1.225, 0.0, ambientPressure); + + solver = new Solver(); + solver.SetTimeStep(dt); + solver.AddPipe(pipe); + solver.SetPipeBoundary(pipe, isLeft: true, BoundaryType.OpenEnd, ambientPressure); + solver.SetPipeBoundary(pipe, isLeft: false, BoundaryType.ClosedEnd); + + // Initial pressure pulse + int pulseCells = 5; + double pulsePressure = 2 * ambientPressure; + for (int i = 0; i < pulseCells; i++) + pipe.SetCellState(i, 1.225, 0.0, pulsePressure); + } + + public override float Process() + { + float sample = solver.Step(); + time += dt; + stepCount++; + + double pMid = pipe.GetPressureAtFraction(0.5); + sample = (float)((pMid - ambientPressure) / ambientPressure); + + Log(sample); + return sample; + } + + private void Log(float sample) + { + if (!enableLogging) return; + if (stepCount % 10 == 0 && stepCount < 1000) + { + double pMid = pipe.GetPressureAtFraction(0.5); + double pOpen = pipe.GetCellPressure(0); + double pClosed = pipe.GetCellPressure(pipe.GetCellCount() - 1); + Console.WriteLine( + $"t = {time * 1e3:F3} ms Step {stepCount:D4}: " + + $"sample = {sample:F3}, " + + $"P_mid = {pMid:F2} Pa ({pMid / ambientPressure:F4} atm), " + + $"P_open = {pOpen:F2} Pa, P_closed = {pClosed:F2} Pa"); + } + } + + public override void Draw(RenderWindow target) + { + float winWidth = target.GetView().Size.X; + float winHeight = target.GetView().Size.Y; + + float pipeCenterY = winHeight / 2f; + float margin = 60f; + float pipeStartX = margin; + float pipeEndX = winWidth - margin; + float pipeLengthPx = pipeEndX - pipeStartX; + int n = pipe.GetCellCount(); + float dx = pipeLengthPx / (n - 1); // spacing between cell centres + + float baseRadius = 25f; + float rangeFactor = 1f; + float scaleFactor = 5f; + + // ----- smoothstep helper ----- + static float SmoothStep(float edge0, float edge1, float x) + { + float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f); + return t * t * (3f - 2f * t); + } + + // ----- Pre‑compute cell positions and radii ----- + var centers = new float[n]; + var radii = new float[n]; + for (int i = 0; i < n; i++) + { + double p = pipe.GetCellPressure(i); + float deviation = (float)Math.Tanh((p - ambientPressure) / ambientPressure / rangeFactor); + radii[i] = baseRadius * (1f + deviation * scaleFactor); + if (radii[i] < 2f) radii[i] = 2f; + centers[i] = pipeStartX + i * dx; + } + + // ----- Build triangle‑strip vertices ----- + int segmentsPerCell = 8; // smoothness + int totalPoints = n + (n - 1) * segmentsPerCell; + Vertex[] stripVertices = new Vertex[totalPoints * 2]; // top + bottom for each point + int idx = 0; + + for (int i = 0; i < n; i++) + { + // ---- Cell centre ---- + float x = centers[i]; + float r = radii[i]; + double p = pipe.GetCellPressure(i); + Color col = PressureColor(p); + + stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY - r), col); + stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY + r), col); + + // ---- Intermediate segments after this cell (if not last) ---- + if (i < n - 1) + { + for (int s = 1; s <= segmentsPerCell; s++) + { + float t = s / (float)segmentsPerCell; + float st = SmoothStep(0f, 1f, t); + float xi = centers[i] + (centers[i + 1] - centers[i]) * t; + float ri = radii[i] + (radii[i + 1] - radii[i]) * st; + double pi = pipe.GetCellPressure(i) * (1 - t) + pipe.GetCellPressure(i + 1) * t; + Color coli = PressureColor(pi); + + stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli); + stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY + ri), coli); + } + } + } + + // Draw the pipe as a triangle strip + var pipeMesh = new VertexArray(PrimitiveType.TriangleStrip, (uint)stripVertices.Length); + for (int i = 0; i < stripVertices.Length; i++) + pipeMesh[(uint)i] = stripVertices[i]; + target.Draw(pipeMesh); + + // ----- Closed end indicator (right) ----- + float wallThickness = 8f; + var wall = new RectangleShape(new Vector2f(wallThickness, winHeight * 0.6f)); + wall.Position = new Vector2f(pipeEndX, pipeCenterY - winHeight * 0.6f / 2f); + wall.FillColor = new Color(180, 180, 180); + target.Draw(wall); + } + + /// Blue (low) → Green (ambient) → Red (high). + private Color PressureColor(double pressure) + { + double range = ambientPressure * 0.05; // ±5% gives full colour swing + double t = (pressure - ambientPressure) / range; + t = Math.Clamp(t, -1.0, 1.0); + + byte r, g, b; + if (t < 0) + { + double factor = -t; + r = 0; + g = (byte)(255 * (1 - factor)); + b = (byte)(255 * factor); + } + else + { + double factor = t; + r = (byte)(255 * factor); + g = (byte)(255 * (1 - factor)); + b = 0; + } + return new Color(r, g, b); + } + } +} \ No newline at end of file diff --git a/Scenarios/Scenario.cs b/Scenarios/Scenario.cs new file mode 100644 index 0000000..52fba3f --- /dev/null +++ b/Scenarios/Scenario.cs @@ -0,0 +1,23 @@ +using SFML.Graphics; + +namespace FluidSim.Core +{ + public abstract class Scenario + { + /// + /// Initialize the scenario with a given audio sample rate. + /// + public abstract void Initialize(int sampleRate); + + /// + /// Advance one simulation step and return an audio sample. + /// The step size is 1 / sampleRate seconds. + /// + public abstract float Process(); + + /// + /// Draw the current simulation state onto the given SFML render target. + /// + public abstract void Draw(RenderWindow target); + } +} \ No newline at end of file