using SFML.Graphics; using SFML.Window; using SFML.System; using System.Diagnostics; using FluidSim.Core; 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, "Pipe Resonator"); window.SetVerticalSyncEnabled(true); window.Closed += (_, _) => { running = false; window.Close(); }; window.MouseWheelScrolled += OnMouseWheel; window.KeyPressed += OnKeyPressed; var soundEngine = new SoundEngine(bufferCapacity: 16384); soundEngine.Volume = 70; soundEngine.Start(); scenario = new PipeResonatorScenario(); scenario.Initialize(SampleRate); var stopwatch = Stopwatch.StartNew(); double lastDrawTime = 0.0; double drawInterval = 1.0 / DrawFrequency; double lastSpeedUpdateTime = stopwatch.Elapsed.TotalSeconds; // Resampling buffer List simBuffer = new List(4096); double readIndex = 0.0; for (int i = 0; i < 4; i++) simBuffer.Add(scenario.Process()); long totalSimSteps = simBuffer.Count; long totalOutputSamples = 0; double lastRealTime = stopwatch.Elapsed.TotalSeconds; const int outputChunk = 256; float[] outputBuf = new float[outputChunk]; while (window.IsOpen) { window.DispatchEvents(); double currentRealTime = stopwatch.Elapsed.TotalSeconds; double dtSpeed = currentRealTime - lastSpeedUpdateTime; lastSpeedUpdateTime = currentRealTime; // 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 = (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++) { 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; } } int accepted = soundEngine.WriteSamples(outputBuf, toGenerate); totalOutputSamples += accepted; if (accepted < toGenerate) break; } // ---------- 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); 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; } } }