Lots of improvements. Better UI, time scrolling, scenario system
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
Program.cs
153
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<float> simBuffer = new List<float>(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;
|
||||
}
|
||||
|
||||
lastAudioTime = currentTime;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
183
Scenarios/PipeResonatorScenario.cs
Normal file
183
Scenarios/PipeResonatorScenario.cs
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>Blue (low) → Green (ambient) → Red (high).</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Scenarios/Scenario.cs
Normal file
23
Scenarios/Scenario.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using SFML.Graphics;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
public abstract class Scenario
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialize the scenario with a given audio sample rate.
|
||||
/// </summary>
|
||||
public abstract void Initialize(int sampleRate);
|
||||
|
||||
/// <summary>
|
||||
/// Advance one simulation step and return an audio sample.
|
||||
/// The step size is 1 / sampleRate seconds.
|
||||
/// </summary>
|
||||
public abstract float Process();
|
||||
|
||||
/// <summary>
|
||||
/// Draw the current simulation state onto the given SFML render target.
|
||||
/// </summary>
|
||||
public abstract void Draw(RenderWindow target);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user