Port FluidSim core and scenarios to C++20
This commit is contained in:
35
include/FluidSim/Atmosphere.hpp
Normal file
35
include/FluidSim/Atmosphere.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Engine.hpp"
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Fixed thermodynamic boundary matching the C# Atmosphere component.
|
||||
class Atmosphere final : public Component {
|
||||
public:
|
||||
float pressure{101325.0f};
|
||||
float temperature{300.0f};
|
||||
float gas_constant_value{gas_constant};
|
||||
float gamma_value{gamma};
|
||||
Port port{};
|
||||
|
||||
Atmosphere() { update_port(); }
|
||||
|
||||
float density() const { return pressure / (gas_constant_value * temperature); }
|
||||
float specific_enthalpy() const {
|
||||
return gamma_value / (gamma_value - 1.0f) * pressure / density();
|
||||
}
|
||||
std::vector<Port*> ports() override { return {&port}; }
|
||||
void update_state(float /*dt*/) override { update_port(); }
|
||||
|
||||
private:
|
||||
void update_port() {
|
||||
port.pressure = pressure;
|
||||
port.density = density();
|
||||
port.temperature = temperature;
|
||||
port.enthalpy = specific_enthalpy();
|
||||
port.air_fraction = 1.0f;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
30
include/FluidSim/AudioOutputStream.hpp
Normal file
30
include/FluidSim/AudioOutputStream.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/SimulationRingBuffer.hpp"
|
||||
#include <SFML/Audio/SoundStream.hpp>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class AudioOutputStream final : public sf::SoundStream {
|
||||
public:
|
||||
explicit AudioOutputStream(SimulationRingBuffer& ring, unsigned int sample_rate = 44100);
|
||||
void set_speed(double speed) { speed_.store(speed, std::memory_order_relaxed); }
|
||||
double speed() const { return speed_.load(std::memory_order_relaxed); }
|
||||
|
||||
private:
|
||||
static constexpr std::size_t block_size = 512;
|
||||
SimulationRingBuffer& ring_;
|
||||
std::array<float, block_size> temp_left_{}, temp_right_{};
|
||||
std::array<std::int16_t, block_size * 2> output_{};
|
||||
std::atomic<double> speed_{1.0};
|
||||
float previous_input_left_{}, previous_output_left_{};
|
||||
float previous_input_right_{}, previous_output_right_{};
|
||||
|
||||
bool onGetData(Chunk& data) override;
|
||||
void onSeek(sf::Time time_offset) override;
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
50
include/FluidSim/AudioProcessing.hpp
Normal file
50
include/FluidSim/AudioProcessing.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
struct AudioFrame { float left{}, right{}; };
|
||||
struct Vec3 { float x{}, y{}, z{}; };
|
||||
|
||||
// Flow-derivative sound source used by the C# two-stroke and pulse scenarios.
|
||||
class SoundProcessor {
|
||||
public:
|
||||
float gain{1.0f};
|
||||
explicit SoundProcessor(int sample_rate = 44100, float listener_distance = 1.0f);
|
||||
float process(float mass_flow_out);
|
||||
private:
|
||||
float dt_{}, scale_factor_{}, lowpass_alpha_{}, derivative_alpha_{};
|
||||
float flow_lowpass_{}, previous_flow_{}, smooth_derivative_{};
|
||||
};
|
||||
|
||||
class RadiationSource {
|
||||
public:
|
||||
float flow_tau{0.005f}, smooth_tau{0.020f}, gain{15.0f};
|
||||
Vec3 pipe_end{}, pipe_axis{1.0f, 0.0f, 0.0f};
|
||||
Vec3 listener{0.0f, 0.0f, 1.5f}, listener_right{1.0f, 0.0f, 0.0f};
|
||||
float ear_spacing{0.18f}, head_shadow_cutoff{4000.0f}, speed_of_sound{343.0f};
|
||||
float directivity_balance{0.30f}, air_absorption_exponent{0.002f}, min_air_cutoff{1500.0f};
|
||||
|
||||
explicit RadiationSource(int sample_rate = 44100);
|
||||
AudioFrame process(float mass_flow);
|
||||
float last_mono_pressure() const { return last_mono_pressure_; }
|
||||
|
||||
private:
|
||||
class DelayLine;
|
||||
class OnePole;
|
||||
int sample_rate_{};
|
||||
float dt_{}, flow_lp_{}, previous_flow_lp_{}, smooth_derivative_{};
|
||||
float last_mono_pressure_{};
|
||||
std::vector<float> left_delay_, right_delay_;
|
||||
std::size_t write_position_{}, left_offset_{}, right_offset_{};
|
||||
float air_filter_{}, shadow_left_{}, shadow_right_{};
|
||||
|
||||
static float dot(Vec3 a, Vec3 b);
|
||||
static float length(Vec3 value);
|
||||
static Vec3 subtract(Vec3 a, Vec3 b);
|
||||
float one_pole(float input, float cutoff, float& state) const;
|
||||
float delayed(float input, std::vector<float>& buffer, std::size_t offset);
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
27
include/FluidSim/Component.hpp
Normal file
27
include/FluidSim/Component.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
struct Port {
|
||||
float mass_flow{}; // kg/s, positive into the owning component
|
||||
float enthalpy{}; // J/kg
|
||||
float pressure{101325.0f};
|
||||
float density{1.1768f};
|
||||
float temperature{300.0f};
|
||||
float air_fraction{1.0f};
|
||||
float* owner_air_mass{};
|
||||
float* owner_exhaust_mass{};
|
||||
};
|
||||
|
||||
// Small equivalent of the C# IComponent lifecycle. Components update their
|
||||
// conserved state after the coupled 1-D pipe solve has accumulated port flow.
|
||||
class Component {
|
||||
public:
|
||||
virtual ~Component() = default;
|
||||
virtual std::vector<Port*> ports() = 0;
|
||||
virtual void update_state(float dt) = 0;
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
16
include/FluidSim/Constants.hpp
Normal file
16
include/FluidSim/Constants.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
inline constexpr float pi = 3.14159265358979323846f;
|
||||
inline constexpr float gamma = 1.4f;
|
||||
// PipeSystem.cs stores these as independent float constants rather than
|
||||
// deriving them from Gamma at runtime; preserve that rounding in the port.
|
||||
inline constexpr float pipe_gm1 = 0.4f;
|
||||
inline constexpr float pipe_gm1_inverse = 1.0f / pipe_gm1;
|
||||
inline constexpr float gas_constant = 287.0f;
|
||||
inline constexpr float ambient_pressure = 101325.0f;
|
||||
inline constexpr float ambient_temperature = 300.0f;
|
||||
inline constexpr float ambient_density = ambient_pressure / (gas_constant * ambient_temperature);
|
||||
|
||||
} // namespace fluidsim::port
|
||||
37
include/FluidSim/Crankcase.hpp
Normal file
37
include/FluidSim/Crankcase.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Component.hpp"
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class Crankcase final : public Component {
|
||||
public:
|
||||
Crankcase(Crankshaft& crankshaft, float crank_radius, float connecting_rod_length,
|
||||
float bore, float clearance_volume, float initial_pressure = 101325.0f,
|
||||
float initial_temperature = 300.0f);
|
||||
|
||||
Port intake_port{}, transfer_port{};
|
||||
float pressure() const { return pressure_; }
|
||||
float temperature() const { return temperature_; }
|
||||
float density() const { return mass_ / (volume_ > 1e-12f ? volume_ : 1e-12f); }
|
||||
float volume() const { return volume_; }
|
||||
float mass() const { return mass_; }
|
||||
float air_fraction() const { return air_fraction_; }
|
||||
|
||||
std::vector<Port*> ports() override { return {&intake_port, &transfer_port}; }
|
||||
void pre_step(float dt);
|
||||
void update_state(float dt) override;
|
||||
|
||||
private:
|
||||
Crankshaft& crankshaft_;
|
||||
float crank_radius_{}, connecting_rod_length_{}, piston_area_{};
|
||||
float clearance_volume_{}, obliquity_{};
|
||||
float mass_{}, energy_{}, air_fraction_{1.0f};
|
||||
float pressure_{101325.0f}, temperature_{300.0f}, volume_{}, previous_volume_{};
|
||||
|
||||
void sync_ports();
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
64
include/FluidSim/DynoCurve.hpp
Normal file
64
include/FluidSim/DynoCurve.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
struct DynoPoint {
|
||||
float power_kw{};
|
||||
float torque_nm{};
|
||||
};
|
||||
|
||||
// Shared rolling peak curve from the C# Scenario base class. Each bin stores
|
||||
// the greatest observed power and torque independently, with missing bins
|
||||
// interpolated when the engine crosses an RPM range too quickly to sample it.
|
||||
class DynoCurve {
|
||||
public:
|
||||
static constexpr float rpm_bin_size = 50.0f;
|
||||
|
||||
void reset() {
|
||||
bins_.clear();
|
||||
last_bin_ = invalid_bin;
|
||||
}
|
||||
|
||||
void update(float rpm, float power_kw, float torque_nm) {
|
||||
if (!std::isfinite(rpm) || rpm <= 0.0f || !std::isfinite(power_kw) || !std::isfinite(torque_nm))
|
||||
return;
|
||||
const auto bin = static_cast<std::size_t>(rpm / rpm_bin_size);
|
||||
if (bin >= bins_.max_size()) return;
|
||||
while (bins_.size() <= bin) bins_.push_back({});
|
||||
|
||||
if (last_bin_ != invalid_bin && bin > last_bin_ + 1) {
|
||||
const float last_power = bins_[last_bin_].power_kw > 0.0f ? bins_[last_bin_].power_kw : 0.0f;
|
||||
const float last_torque = bins_[last_bin_].torque_nm > 0.0f ? bins_[last_bin_].torque_nm : 0.0f;
|
||||
for (std::size_t missing = last_bin_ + 1; missing < bin; ++missing) {
|
||||
const float t = static_cast<float>(missing - last_bin_) /
|
||||
static_cast<float>(bin - last_bin_);
|
||||
const float interpolated_power = last_power + (power_kw - last_power) * t;
|
||||
const float interpolated_torque = last_torque + (torque_nm - last_torque) * t;
|
||||
auto& point = bins_[missing];
|
||||
if (interpolated_power > point.power_kw || point.power_kw <= 0.0f)
|
||||
point.power_kw = interpolated_power;
|
||||
if (interpolated_torque > point.torque_nm || point.torque_nm <= 0.0f)
|
||||
point.torque_nm = interpolated_torque;
|
||||
}
|
||||
}
|
||||
|
||||
auto& current = bins_[bin];
|
||||
if (power_kw > current.power_kw || current.power_kw <= 0.0f) current.power_kw = power_kw;
|
||||
if (torque_nm > current.torque_nm || current.torque_nm <= 0.0f) current.torque_nm = torque_nm;
|
||||
last_bin_ = bin;
|
||||
}
|
||||
|
||||
const std::vector<DynoPoint>& bins() const { return bins_; }
|
||||
|
||||
private:
|
||||
static constexpr std::size_t invalid_bin = static_cast<std::size_t>(-1);
|
||||
std::vector<DynoPoint> bins_;
|
||||
std::size_t last_bin_{invalid_bin};
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
75
include/FluidSim/Engine.hpp
Normal file
75
include/FluidSim/Engine.hpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Component.hpp"
|
||||
#include "FluidSim/Constants.hpp"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <random>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class Crankshaft {
|
||||
public:
|
||||
float omega{400.0f * 2.0f * pi / 60.0f};
|
||||
float angle{}, previous_angle{}, inertia{0.2f};
|
||||
float friction_constant{}, friction_viscous{};
|
||||
float load_torque{}, drivetrain_torque{};
|
||||
float average_power{}, average_torque{};
|
||||
|
||||
void add_torque(float torque);
|
||||
void step(float dt);
|
||||
private:
|
||||
float external_torque_{};
|
||||
std::array<float, 16384> power_buffer_{}, torque_buffer_{};
|
||||
std::size_t power_index_{}, power_count_{}, torque_index_{}, torque_count_{};
|
||||
float power_sum_{}, torque_sum_{};
|
||||
};
|
||||
|
||||
class EngineCylinder : public Component {
|
||||
public:
|
||||
float bore{0.075f}, stroke{0.068f}, rod{0.112f}, compression{11.5f};
|
||||
float intake_valve_diameter{0.030f}, exhaust_valve_diameter{0.028f};
|
||||
float intake_valve_lift{0.005f}, exhaust_valve_lift{0.005f};
|
||||
float phase{};
|
||||
float spark_advance{20.0f}, wiebe_a{5.0f}, wiebe_m{2.0f}, burn_duration{60.0f}, burn_start{5.0f};
|
||||
float afr{14.7f}, fuel_lhv{44.0e6f}, energy_variation{0.05f}, misfire_probability{};
|
||||
float wall_area{0.02f}, heat_transfer{100.0f}, ambient_temperature{300.0f};
|
||||
bool rev_limit_enabled{};
|
||||
float rev_limit_rpm{7500.0f}, rev_limit_cut_time{0.05f}, rev_limit_hysteresis{200.0f};
|
||||
float air_mass{}, exhaust_mass{}, energy{}, volume{}, pressure{101325.0f};
|
||||
float fuel_mass{}, burn_fraction{};
|
||||
bool combustion_active{}, fuel_injected{}, rev_limit_active{};
|
||||
Port intake{}, exhaust{};
|
||||
|
||||
std::vector<Port*> ports() override { return {&intake, &exhaust}; }
|
||||
float swept_volume() const;
|
||||
float clearance_volume() const;
|
||||
float total_mass() const;
|
||||
float temperature() const;
|
||||
float air_fraction() const;
|
||||
float compute_volume(float theta) const;
|
||||
void initialize(const Crankshaft& crank);
|
||||
virtual float valve_area(float cycle_degrees, bool inlet) const = 0;
|
||||
virtual void pre_step(Crankshaft& crank, float dt, float throttle) = 0;
|
||||
void update_state(float dt) override;
|
||||
|
||||
protected:
|
||||
float energy_factor_{1.0f};
|
||||
float rev_limit_timer_{};
|
||||
std::mt19937 random_{0xF17D51u};
|
||||
std::uniform_real_distribution<float> unit_{0.0f, 1.0f};
|
||||
|
||||
float random_unit();
|
||||
static float wrap_degrees(float value, float cycle_degrees = 720.0f);
|
||||
static bool crossed_window_close(float previous, float current, float open, float close);
|
||||
void update_rev_limiter(float rpm, float dt);
|
||||
void sync_ports();
|
||||
};
|
||||
|
||||
class FourStrokeCylinder final : public EngineCylinder {
|
||||
public:
|
||||
float intake_open{340.0f}, intake_close{590.0f}, exhaust_open{130.0f}, exhaust_close{390.0f};
|
||||
float valve_area(float cycle_degrees, bool inlet) const override;
|
||||
void pre_step(Crankshaft& crank, float dt, float throttle) override;
|
||||
};
|
||||
} // namespace fluidsim::port
|
||||
387
include/FluidSim/Fluid.hpp
Normal file
387
include/FluidSim/Fluid.hpp
Normal file
@@ -0,0 +1,387 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class Volume0D : public Component {
|
||||
public:
|
||||
explicit Volume0D(float volume_m3, float pressure = 101325.0f, float temperature = 300.0f,
|
||||
float gas_constant_value = 287.0f, float gamma_value = 1.4f)
|
||||
: gamma(gamma_value), gas_constant(gas_constant_value), volume_(volume_m3),
|
||||
air_mass_(pressure * volume_m3 / (gas_constant_value * temperature)),
|
||||
energy_(pressure * volume_m3 / (gamma_value - 1.0f)) {}
|
||||
|
||||
float gamma{1.4f};
|
||||
float gas_constant{287.0f};
|
||||
float ambient_pressure{101325.0f};
|
||||
float ambient_temperature{300.0f};
|
||||
float energy_relaxation_rate{10.0f};
|
||||
float volume_change_rate{};
|
||||
|
||||
Port& create_port() {
|
||||
ports_.emplace_back();
|
||||
auto& p = ports_.back();
|
||||
p.owner_air_mass = &air_mass_;
|
||||
p.owner_exhaust_mass = &exhaust_mass_;
|
||||
sync_port(p);
|
||||
return p;
|
||||
}
|
||||
float pressure() const { return (gamma - 1.0f) * energy_ / std::max(volume_, 1e-12f); }
|
||||
float density() const { return mass() / std::max(volume_, 1e-12f); }
|
||||
float temperature() const { return pressure() / std::max(density() * gas_constant, 1e-9f); }
|
||||
float volume() const { return volume_; }
|
||||
float mass() const { return air_mass_ + exhaust_mass_; }
|
||||
float air_fraction() const { return air_mass_ / std::max(mass(), 1e-12f); }
|
||||
void set_pressure(float value, float temp = -1.0f) {
|
||||
const float target_t = temp > 0.0f ? temp : temperature();
|
||||
const float total = value * volume_ / (gas_constant * target_t);
|
||||
const float fraction = air_fraction();
|
||||
air_mass_ = total * fraction;
|
||||
exhaust_mass_ = total * (1.0f - fraction);
|
||||
energy_ = value * volume_ / (gamma - 1.0f);
|
||||
sync_ports();
|
||||
}
|
||||
void update(float dt) {
|
||||
if (dt <= 0.0f) return;
|
||||
float d_air = 0.0f, d_exhaust = 0.0f, d_energy = 0.0f;
|
||||
const float fraction = air_fraction();
|
||||
for (const Port& p : ports_) {
|
||||
const float flow_fraction = p.mass_flow >= 0.0f ? p.air_fraction : fraction;
|
||||
d_air += p.mass_flow * flow_fraction;
|
||||
d_exhaust += p.mass_flow * (1.0f - flow_fraction);
|
||||
d_energy += p.mass_flow * p.enthalpy;
|
||||
}
|
||||
air_mass_ += d_air * dt;
|
||||
exhaust_mass_ += d_exhaust * dt;
|
||||
energy_ += (d_energy - pressure() * volume_change_rate) * dt;
|
||||
if (energy_relaxation_rate > 0.0f && mass() > 1e-12f) {
|
||||
const float target = mass() * gas_constant * ambient_temperature / (gamma - 1.0f);
|
||||
energy_ = target + (energy_ - target) * std::exp(-energy_relaxation_rate * dt);
|
||||
}
|
||||
if (mass() < 1e-9f || energy_ < 0.0f) {
|
||||
air_mass_ = 1e-9f;
|
||||
exhaust_mass_ = 0.0f;
|
||||
energy_ = ambient_pressure * volume_ / (gamma - 1.0f);
|
||||
}
|
||||
air_mass_ = std::max(air_mass_, 0.0f);
|
||||
exhaust_mass_ = std::max(exhaust_mass_, 0.0f);
|
||||
sync_ports();
|
||||
}
|
||||
void update_state(float dt) override { update(dt); }
|
||||
std::vector<Port*> ports() override {
|
||||
std::vector<Port*> result;
|
||||
result.reserve(ports_.size());
|
||||
for (Port& port : ports_) result.push_back(&port);
|
||||
return result;
|
||||
}
|
||||
const std::deque<Port>& ports() const { return ports_; }
|
||||
private:
|
||||
float volume_, air_mass_{}, exhaust_mass_{}, energy_;
|
||||
std::deque<Port> ports_;
|
||||
void sync_port(Port& p) const {
|
||||
p.pressure = pressure();
|
||||
p.density = density();
|
||||
p.temperature = temperature();
|
||||
p.enthalpy = gamma / (gamma - 1.0f) * p.pressure / std::max(p.density, 1e-12f);
|
||||
p.air_fraction = air_fraction();
|
||||
}
|
||||
void sync_ports() { for (Port& p : ports_) sync_port(p); }
|
||||
};
|
||||
|
||||
struct GasCell {
|
||||
float rho{1.225f}, momentum{};
|
||||
float energy{1.225f * (101325.0f / (pipe_gm1 * 1.225f))};
|
||||
float air{1.0f};
|
||||
};
|
||||
struct GasState { float rho{1.225f}, velocity{}, pressure{101325.0f}, air{1.0f}; };
|
||||
|
||||
class Pipe1D {
|
||||
public:
|
||||
float area{}, dx{};
|
||||
float damping_multiplier{1.0f}, energy_relaxation_rate{0.5f}, ambient_pressure{101325.0f};
|
||||
std::vector<float> cell_areas, cell_lengths;
|
||||
std::vector<GasCell> cells;
|
||||
GasState left_ghost{}, right_ghost{};
|
||||
Pipe1D(float area_m2, float length_m, int cell_count)
|
||||
: area(area_m2), dx(length_m / static_cast<float>(cell_count)),
|
||||
cell_areas(static_cast<std::size_t>(cell_count), area_m2),
|
||||
cell_lengths(static_cast<std::size_t>(cell_count), dx),
|
||||
cells(static_cast<std::size_t>(cell_count)), next_cells_(static_cast<std::size_t>(cell_count)) {}
|
||||
Pipe1D(std::vector<float> areas_m2, std::vector<float> lengths_m)
|
||||
: area(areas_m2.empty() ? 0.0f : areas_m2.front()),
|
||||
dx(lengths_m.empty() ? 0.0f : lengths_m.front()),
|
||||
cell_areas(std::move(areas_m2)), cell_lengths(std::move(lengths_m)),
|
||||
cells(cell_areas.size()), next_cells_(cell_areas.size()) {
|
||||
if (cell_areas.empty() || cell_lengths.size() != cell_areas.size())
|
||||
throw std::invalid_argument("pipe area and length profiles must be non-empty and have equal length");
|
||||
for (std::size_t i = 0; i < cell_areas.size(); ++i) {
|
||||
if (cell_areas[i] <= 0.0f || cell_lengths[i] <= 0.0f)
|
||||
throw std::invalid_argument("pipe cell area and length must be positive");
|
||||
}
|
||||
}
|
||||
GasState state(int idx) const {
|
||||
const auto& c = cells.at(static_cast<std::size_t>(idx));
|
||||
const float rho = c.rho;
|
||||
const float u = c.momentum / std::max(rho, 1e-12f);
|
||||
const float p = pipe_gm1 * (c.energy - 0.5f * c.momentum * u);
|
||||
return {rho, u, p, std::clamp(c.air, 0.0f, 1.0f)};
|
||||
}
|
||||
float cell_mass(int idx) const {
|
||||
const auto cell = static_cast<std::size_t>(idx);
|
||||
return cells.at(cell).rho * cell_areas.at(cell) * cell_lengths.at(cell);
|
||||
}
|
||||
float cell_area(int idx) const { return cell_areas.at(static_cast<std::size_t>(idx)); }
|
||||
float cell_length(int idx) const { return cell_lengths.at(static_cast<std::size_t>(idx)); }
|
||||
float minimum_cell_length() const {
|
||||
return *std::min_element(cell_lengths.begin(), cell_lengths.end());
|
||||
}
|
||||
int required_substeps(float dt, float target_cfl = 0.8f) const {
|
||||
float maximum_wave_speed = 0.0f;
|
||||
for (int i = 0; i < static_cast<int>(cells.size()); ++i) {
|
||||
const GasState s = state(i);
|
||||
maximum_wave_speed = std::max(maximum_wave_speed,
|
||||
std::abs(s.velocity) + std::sqrt(gamma * s.pressure / std::max(s.rho, 1e-12f)));
|
||||
}
|
||||
return std::max(1, static_cast<int>(std::ceil(dt * maximum_wave_speed /
|
||||
(target_cfl * std::max(minimum_cell_length(), 1e-12f)))));
|
||||
}
|
||||
void advance(float dt) {
|
||||
const int n = static_cast<int>(cells.size());
|
||||
if (n == 0 || dt <= 0.0f) return;
|
||||
const float ambient_energy = ambient_pressure * pipe_gm1_inverse;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const GasState l = i == 0 ? left_ghost : flux_state(i - 1), r = flux_state(i);
|
||||
const GasState a = flux_state(i), b = i == n - 1 ? right_ghost : flux_state(i + 1);
|
||||
const Flux fl = flux(l, r), fr = flux(a, b);
|
||||
const float factor = dt / cell_lengths[static_cast<std::size_t>(i)];
|
||||
float rho = cells[i].rho - factor * (fr.m - fl.m);
|
||||
float mom = cells[i].momentum - factor * (fr.p - fl.p);
|
||||
float en = cells[i].energy - factor * (fr.e - fl.e);
|
||||
const float rho_air = cells[i].rho * cells[i].air - factor * (fr.y - fl.y);
|
||||
if (!std::isfinite(rho) || !std::isfinite(mom) || !std::isfinite(en) || !std::isfinite(rho_air)) {
|
||||
next_cells_[i] = cells[i];
|
||||
continue;
|
||||
}
|
||||
rho = std::max(rho, 1e-12f);
|
||||
const float damping = std::exp(-0.1f * damping_multiplier * dt /
|
||||
std::max(cells[i].rho, 1e-12f));
|
||||
mom *= damping;
|
||||
if (energy_relaxation_rate > 0.0f) {
|
||||
const float relax = std::exp(-energy_relaxation_rate * dt);
|
||||
en = ambient_energy + (en - ambient_energy) * relax;
|
||||
}
|
||||
en = std::max(en, 100.0f * pipe_gm1_inverse + 0.5f * mom * mom / rho);
|
||||
next_cells_[i] = {rho, mom, en, std::clamp(rho_air / rho, 0.0f, 1.0f)};
|
||||
}
|
||||
cells.swap(next_cells_);
|
||||
}
|
||||
private:
|
||||
struct Flux { float m{}, p{}, e{}, y{}; };
|
||||
std::vector<GasCell> next_cells_;
|
||||
GasState flux_state(int idx) const {
|
||||
const auto& c = cells[static_cast<std::size_t>(idx)];
|
||||
const float rho = c.rho;
|
||||
const float inverse_rho = 1.0f / std::max(rho, 1e-12f);
|
||||
const float u = c.momentum * inverse_rho;
|
||||
const float p = pipe_gm1 * (c.energy - 0.5f * c.momentum * u);
|
||||
return {rho, u, p, std::clamp(c.air, 0.0f, 1.0f)};
|
||||
}
|
||||
static Flux flux(const GasState& a, const GasState& b) {
|
||||
const float ua = a.velocity, ub = b.velocity;
|
||||
const float inv_rho_a = 1.0f / std::max(a.rho, 1e-12f);
|
||||
const float inv_rho_b = 1.0f / std::max(b.rho, 1e-12f);
|
||||
const float ea = a.pressure * pipe_gm1_inverse * inv_rho_a + 0.5f * ua * ua;
|
||||
const float eb = b.pressure * pipe_gm1_inverse * inv_rho_b + 0.5f * ub * ub;
|
||||
const float c = std::max(std::abs(ua) + std::sqrt(gamma * a.pressure * inv_rho_a),
|
||||
std::abs(ub) + std::sqrt(gamma * b.pressure * inv_rho_b));
|
||||
return {
|
||||
0.5f * (a.rho * ua + b.rho * ub) - 0.5f * c * (b.rho - a.rho),
|
||||
0.5f * (a.rho * ua * ua + a.pressure + b.rho * ub * ub + b.pressure) - 0.5f * c * (b.rho * ub - a.rho * ua),
|
||||
0.5f * ((a.rho * ea + a.pressure) * ua + (b.rho * eb + b.pressure) * ub) - 0.5f * c * (b.rho * eb - a.rho * ea),
|
||||
0.5f * (a.rho * ua * a.air + b.rho * ub * b.air) - 0.5f * c * (b.rho * b.air - a.rho * a.air)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
class BoundarySystem {
|
||||
public:
|
||||
explicit BoundarySystem(std::vector<Pipe1D>& pipes) : pipes_(pipes) {}
|
||||
void add_orifice(Port& port, int pipe, bool left, float& area, float cd = 1.0f) {
|
||||
Orifice desc;
|
||||
desc.port = &port;
|
||||
desc.pipe = pipe;
|
||||
desc.left = left;
|
||||
desc.area = &area;
|
||||
desc.cd = cd;
|
||||
orifices_.push_back(desc);
|
||||
}
|
||||
void add_orifice_with_inertance(Port& port, int pipe, bool left, float& area,
|
||||
float cd, float effective_length, float loss_coefficient = 0.0f) {
|
||||
add_orifice(port, pipe, left, area, cd);
|
||||
auto& desc = orifices_.back();
|
||||
desc.use_inertance = true;
|
||||
desc.effective_length = effective_length;
|
||||
desc.loss_coefficient = loss_coefficient;
|
||||
}
|
||||
void add_closed_end(int pipe, bool left) {
|
||||
Orifice desc;
|
||||
desc.pipe = pipe;
|
||||
desc.left = left;
|
||||
desc.closed_end = true;
|
||||
orifices_.push_back(desc);
|
||||
}
|
||||
void add_open_end(int pipe, bool left, float pressure, float area) { opens_.push_back({pipe, left, pressure, area}); }
|
||||
float open_end_mass_flow(std::size_t index) const { return opens_.at(index).last_mass_flow; }
|
||||
float open_end_pressure(std::size_t index) const { return opens_.at(index).last_face_pressure; }
|
||||
void resolve(float dt) {
|
||||
for (auto& o : orifices_) resolve_orifice(o, dt);
|
||||
for (auto& o : opens_) resolve_open_end(o, dt);
|
||||
}
|
||||
private:
|
||||
struct Orifice {
|
||||
Port* port{};
|
||||
int pipe{};
|
||||
bool left{};
|
||||
float* area{};
|
||||
float cd{1.0f};
|
||||
bool closed_end{}, use_inertance{};
|
||||
float effective_length{}, loss_coefficient{}, current_mass_flow{};
|
||||
};
|
||||
struct Open { int pipe; bool left; float pressure, area; float last_mass_flow{}, last_face_pressure{}; };
|
||||
struct OrificeSolution { float mass_flow{}, rho{}, velocity{}, pressure{}; };
|
||||
std::vector<Pipe1D>& pipes_;
|
||||
std::vector<Orifice> orifices_;
|
||||
std::vector<Open> opens_;
|
||||
|
||||
static OrificeSolution isentropic(float pup, float rho_up, float t_up, float pdown, float area, float cd) {
|
||||
OrificeSolution out{0.0f, rho_up, 0.0f, pup};
|
||||
if (area <= 0.0f || pup <= 0.0f || rho_up <= 0.0f || t_up <= 0.0f) return out;
|
||||
float ratio = std::clamp(pdown / pup, 1e-6f, 1.0f);
|
||||
const float critical = std::pow(2.0f / (gamma + 1.0f), gamma / (gamma - 1.0f));
|
||||
ratio = std::max(ratio, critical);
|
||||
const float mach = std::sqrt(std::max(0.0f, 2.0f / (gamma - 1.0f) *
|
||||
(std::pow(ratio, -(gamma - 1.0f) / gamma) - 1.0f)));
|
||||
const float a_up = std::sqrt(gamma * gas_constant * t_up);
|
||||
out.velocity = mach * a_up;
|
||||
out.rho = rho_up * std::pow(ratio, 1.0f / gamma);
|
||||
out.pressure = pup * ratio;
|
||||
out.mass_flow = out.rho * out.velocity * area * cd;
|
||||
return out;
|
||||
}
|
||||
static float available_mass(const Port& port) {
|
||||
return (port.owner_air_mass ? *port.owner_air_mass : 0.0f) +
|
||||
(port.owner_exhaust_mass ? *port.owner_exhaust_mass : 0.0f);
|
||||
}
|
||||
void resolve_orifice(Orifice& o, float dt) {
|
||||
Pipe1D& pipe = pipes_.at(static_cast<std::size_t>(o.pipe));
|
||||
const int idx = o.left ? 0 : static_cast<int>(pipe.cells.size()) - 1;
|
||||
const GasState interior = pipe.state(idx);
|
||||
const float area = o.area ? std::max(*o.area, 0.0f) : 0.0f;
|
||||
if (o.closed_end || !o.port || area <= 1e-12f) {
|
||||
GasState wall = interior;
|
||||
wall.velocity = -interior.velocity;
|
||||
if (o.left) pipe.left_ghost = wall; else pipe.right_ghost = wall;
|
||||
if (o.port) o.port->mass_flow = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
const float dp = o.port->pressure - interior.pressure;
|
||||
OrificeSolution solution{0.0f, interior.rho, 0.0f, interior.pressure};
|
||||
// Preserve the C# tie-break: at equal pressure it treats the volume as
|
||||
// upstream, retaining the volume-side density in the ghost state. That
|
||||
// matters when equal-pressure volume and pipe states have different
|
||||
// temperatures/densities, as on the initialized intake plenum.
|
||||
if (o.port->pressure >= interior.pressure) {
|
||||
solution = isentropic(o.port->pressure, o.port->density, o.port->temperature, interior.pressure, area, o.cd);
|
||||
} else {
|
||||
solution = isentropic(interior.pressure, interior.rho,
|
||||
interior.pressure / std::max(interior.rho * gas_constant, 1e-12f), o.port->pressure, area, o.cd);
|
||||
solution.mass_flow = -solution.mass_flow;
|
||||
}
|
||||
float mdot = solution.mass_flow; // positive means port component -> pipe
|
||||
const float adjacent_mass = pipe.cell_mass(idx);
|
||||
if (o.use_inertance) {
|
||||
const float rho_up = o.current_mass_flow >= 0.0f ? o.port->density : interior.rho;
|
||||
const float inertance = rho_up * o.effective_length / area;
|
||||
const float derivative = (dp - o.loss_coefficient * o.current_mass_flow) /
|
||||
std::max(inertance, 1e-12f);
|
||||
mdot = o.current_mass_flow + derivative * dt;
|
||||
}
|
||||
if (mdot > 0.0f && (o.port->owner_air_mass || o.port->owner_exhaust_mass))
|
||||
mdot = std::min(mdot, available_mass(*o.port) / std::max(dt, 1e-12f));
|
||||
if (mdot < 0.0f) mdot = std::max(mdot, -adjacent_mass / std::max(dt, 1e-12f));
|
||||
|
||||
if (o.use_inertance) {
|
||||
o.current_mass_flow = mdot;
|
||||
const bool leaves_port = mdot >= 0.0f;
|
||||
const float rho_up = leaves_port ? o.port->density : interior.rho;
|
||||
const float temperature_up = leaves_port ? o.port->temperature :
|
||||
interior.pressure / std::max(interior.rho * gas_constant, 1e-12f);
|
||||
const float max_velocity = 0.9f * std::sqrt(gamma * gas_constant * temperature_up);
|
||||
const float max_flow = rho_up * max_velocity * area;
|
||||
mdot = std::clamp(mdot, -max_flow, max_flow);
|
||||
o.current_mass_flow = mdot;
|
||||
if (mdot >= 0.0f) {
|
||||
solution = isentropic(o.port->pressure, o.port->density, o.port->temperature,
|
||||
interior.pressure, area, o.cd);
|
||||
} else {
|
||||
solution = isentropic(interior.pressure, interior.rho, temperature_up,
|
||||
o.port->pressure, area, o.cd);
|
||||
solution.mass_flow = -solution.mass_flow;
|
||||
}
|
||||
solution.mass_flow = mdot;
|
||||
solution.rho = rho_up;
|
||||
}
|
||||
|
||||
const bool from_port = mdot >= 0.0f;
|
||||
const float rho_face = std::max(solution.rho, 1e-9f);
|
||||
const float u = std::abs(mdot) / std::max(rho_face * area, 1e-12f);
|
||||
GasState ghost{rho_face, 0.0f, std::max(solution.pressure, 1.0f), from_port ? o.port->air_fraction : interior.air};
|
||||
ghost.velocity = ((from_port == o.left) ? u : -u);
|
||||
if (o.left) pipe.left_ghost = ghost; else pipe.right_ghost = ghost;
|
||||
o.port->mass_flow = -mdot;
|
||||
if (!from_port) o.port->air_fraction = interior.air;
|
||||
o.port->enthalpy = from_port
|
||||
? o.port->enthalpy
|
||||
: gamma / (gamma - 1.0f) * interior.pressure / std::max(interior.rho, 1e-12f);
|
||||
(void)dp;
|
||||
}
|
||||
void resolve_open_end(Open& o, float dt) {
|
||||
Pipe1D& pipe = pipes_.at(static_cast<std::size_t>(o.pipe));
|
||||
const int idx = o.left ? 0 : static_cast<int>(pipe.cells.size()) - 1;
|
||||
const GasState interior = pipe.state(idx);
|
||||
const float c = std::sqrt(gamma * interior.pressure / std::max(interior.rho, 1e-12f));
|
||||
const float gm1 = gamma - 1.0f;
|
||||
const float j_plus = interior.velocity + 2.0f * c / gm1;
|
||||
const float j_minus = interior.velocity - 2.0f * c / gm1;
|
||||
const float entropy = interior.pressure / std::pow(std::max(interior.rho, 1e-12f), gamma);
|
||||
const float rho_iso = std::pow(std::max(o.pressure / entropy, 1e-12f), 1.0f / gamma);
|
||||
const float c_iso = std::sqrt(gamma * o.pressure / std::max(rho_iso, 1e-12f));
|
||||
const float u_iso = o.left ? j_minus + 2.0f * c_iso / gm1 : j_plus - 2.0f * c_iso / gm1;
|
||||
const bool supersonic = o.left ? (interior.velocity <= -c || u_iso <= -c_iso)
|
||||
: (interior.velocity >= c || u_iso >= c_iso);
|
||||
GasState ghost = supersonic ? interior : GasState{rho_iso, u_iso, o.pressure,
|
||||
((o.left ? u_iso >= 0.0f : u_iso <= 0.0f) ? 1.0f : interior.air)};
|
||||
|
||||
const float cell_mass = pipe.cell_mass(idx);
|
||||
float mdot_out = ghost.rho * ghost.velocity * o.area * (o.left ? -1.0f : 1.0f);
|
||||
if (mdot_out > 0.0f) mdot_out = std::min(mdot_out, cell_mass / std::max(dt, 1e-12f));
|
||||
else mdot_out = std::max(mdot_out, -10.0f * cell_mass / std::max(dt, 1e-12f));
|
||||
const float speed = std::abs(mdot_out) / std::max(ghost.rho * o.area, 1e-12f);
|
||||
ghost.velocity = o.left ? (mdot_out >= 0.0f ? -speed : speed)
|
||||
: (mdot_out >= 0.0f ? speed : -speed);
|
||||
if (o.left) pipe.left_ghost = ghost; else pipe.right_ghost = ghost;
|
||||
o.last_mass_flow = mdot_out;
|
||||
o.last_face_pressure = ghost.pressure;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
28
include/FluidSim/HelmholtzScenario.hpp
Normal file
28
include/FluidSim/HelmholtzScenario.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim\AudioProcessing.hpp"
|
||||
#include "FluidSim\Fluid.hpp"
|
||||
#include "FluidSim\Solver.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class HelmholtzScenario {
|
||||
public:
|
||||
explicit HelmholtzScenario(int sample_rate = 44100);
|
||||
float process();
|
||||
float cavity_pressure() const { return cavity_.pressure(); }
|
||||
float open_end_flow() const { return boundaries_.open_end_mass_flow(0); }
|
||||
const Pipe1D& neck() const { return pipes_.front(); }
|
||||
|
||||
private:
|
||||
Volume0D cavity_{1e-3f, 1.1f * 101325.0f, 300.0f};
|
||||
Port* cavity_port_{};
|
||||
std::vector<Pipe1D> pipes_;
|
||||
float dt_{}, neck_area_{}, cavity_orifice_area_{};
|
||||
BoundarySystem boundaries_;
|
||||
Solver solver_;
|
||||
SoundProcessor sound_;
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
49
include/FluidSim/MechanicalSoundProcessor.hpp
Normal file
49
include/FluidSim/MechanicalSoundProcessor.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include <random>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Synthetic valve, piston, combustion, chain, and gear sounds driven by live
|
||||
// engine state; direct port of Core/MechanicalSoundProcessor.cs.
|
||||
class MechanicalSoundProcessor {
|
||||
public:
|
||||
float master_gain{0.6f}, valve_tick_gain{0.9f}, piston_slap_gain{1.2f};
|
||||
float combustion_gain{0.5f}, gear_whine_gain{0.12f}, cam_chain_gain{0.25f};
|
||||
|
||||
explicit MechanicalSoundProcessor(int sample_rate = 44100);
|
||||
float process(const Crankshaft& crank, const EngineCylinder& cylinder, float load);
|
||||
|
||||
private:
|
||||
class Resonator {
|
||||
public:
|
||||
Resonator(float frequency_hz, float q, float sample_rate);
|
||||
float process(float input);
|
||||
private:
|
||||
float b1_{}, b2_{}, gain_{}, y1_{}, y2_{};
|
||||
};
|
||||
class OnePoleLowPass {
|
||||
public:
|
||||
OnePoleLowPass(float cutoff_hz, float sample_rate);
|
||||
float process(float input);
|
||||
private:
|
||||
float alpha_{}, state_{};
|
||||
};
|
||||
|
||||
float dt_{};
|
||||
Resonator intake_tick_, exhaust_tick_;
|
||||
OnePoleLowPass slap_filter_, combustion_filter_, chain_filter_;
|
||||
float slap_envelope_{}, combustion_envelope_{}, chain_envelope_{};
|
||||
float slap_decay_{}, combustion_decay_{}, chain_decay_{};
|
||||
float gear_phase1_{}, gear_phase2_{}, gear_phase3_{};
|
||||
float previous_intake_area_{}, previous_exhaust_area_{}, previous_pressure_{};
|
||||
float previous_cycle_position_{-1.0f}, previous_crank_fraction_{-1.0f};
|
||||
std::mt19937 random_{0x5EEDu};
|
||||
std::uniform_real_distribution<float> unit_{-1.0f, 1.0f};
|
||||
|
||||
float random_sample();
|
||||
static bool crossed_fraction(float previous, float current, float target);
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
55
include/FluidSim/OutdoorExhaustReverb.hpp
Normal file
55
include/FluidSim/OutdoorExhaustReverb.hpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Eight-line frequency-dependent feedback delay network with early reflections
|
||||
// and transient bloom, ported from Core/OutdoorExhaustReverb.cs.
|
||||
class OutdoorExhaustReverb {
|
||||
public:
|
||||
float dry_mix{1.00f}, early_mix{0.10f}, tail_mix{0.28f};
|
||||
float pre_delay_ms{5.0f}, ground_delay_ms{9.0f}, wall_delay_ms{45.0f};
|
||||
float low_decay_seconds{1.50f}, mid_decay_seconds{0.70f}, high_decay_seconds{0.20f};
|
||||
float band_low_hz{300.0f}, band_high_hz{3000.0f};
|
||||
float transient_feedback_boost{0.35f}, transient_hf_boost{3.0f};
|
||||
float transient_decay_ms{110.0f}, saturation_drive{1.2f};
|
||||
|
||||
explicit OutdoorExhaustReverb(int sample_rate = 44100);
|
||||
AudioFrame process_stereo(float input);
|
||||
float process(float input);
|
||||
|
||||
private:
|
||||
class DelayLine {
|
||||
public:
|
||||
explicit DelayLine(std::size_t size = 1);
|
||||
float read() const;
|
||||
float read_write(float value);
|
||||
void write(float value);
|
||||
private:
|
||||
std::vector<float> buffer_;
|
||||
std::size_t position_{};
|
||||
};
|
||||
class OnePoleLowPass {
|
||||
public:
|
||||
OnePoleLowPass(float cutoff_hz, int sample_rate);
|
||||
float process(float value);
|
||||
private:
|
||||
float alpha_{}, state_{};
|
||||
};
|
||||
|
||||
int sample_rate_{};
|
||||
float dt_{}, gain_low_{}, gain_mid_{}, gain_high_{};
|
||||
std::vector<DelayLine> fdn_;
|
||||
std::vector<OnePoleLowPass> low_filters_, mid_filters_;
|
||||
std::vector<float> read_buffer_, mix_buffer_;
|
||||
DelayLine pre_delay_, ground_left_, ground_right_, wall_left_, wall_right_;
|
||||
OnePoleLowPass ground_filter_left_, ground_filter_right_, wall_filter_left_, wall_filter_right_;
|
||||
float fast_envelope_{}, slow_envelope_{}, bloom_{};
|
||||
float fast_attack_{}, fast_release_{}, slow_attack_{}, slow_release_{}, bloom_decay_{};
|
||||
|
||||
static std::size_t delay_samples(int sample_rate, float milliseconds);
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
24
include/FluidSim/PulseTestScenario.hpp
Normal file
24
include/FluidSim/PulseTestScenario.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class PulseTestScenario {
|
||||
public:
|
||||
explicit PulseTestScenario(int sample_rate = 44100);
|
||||
float process();
|
||||
const Pipe1D& pipe() const { return pipes_.front(); }
|
||||
|
||||
private:
|
||||
std::vector<Pipe1D> pipes_;
|
||||
float dt_{}, pipe_area_{};
|
||||
BoundarySystem boundaries_;
|
||||
Solver solver_;
|
||||
SoundProcessor sound_;
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
82
include/FluidSim/Scenario.hpp
Normal file
82
include/FluidSim/Scenario.hpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include "FluidSim/Engine.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/MechanicalSoundProcessor.hpp"
|
||||
#include "FluidSim/OutdoorExhaustReverb.hpp"
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include "FluidSim/Vehicle.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
struct ScenarioSnapshot {
|
||||
float rpm{}, speed_kmh{}, kinetic_energy{}, average_power{}, average_torque{};
|
||||
float cylinder_pressure1{}, cylinder_pressure2{};
|
||||
float cylinder_temperature1{}, cylinder_temperature2{};
|
||||
float cylinder_piston_fraction1{}, cylinder_piston_fraction2{};
|
||||
float valve1_intake_area{}, valve1_exhaust_area{}, valve2_intake_area{}, valve2_exhaust_area{};
|
||||
float intake_pressure{}, exhaust_pressure{};
|
||||
float throttle{}, clutch{}, intake_flow{}, exhaust_flow{};
|
||||
int gear{};
|
||||
bool rev_limit1{}, rev_limit2{}, mechanical_sound_enabled{};
|
||||
std::array<std::vector<GasState>, 6> pipe_states;
|
||||
};
|
||||
|
||||
// Active C# SingleCylScenario topology: a 600 cc parallel twin, independent
|
||||
// intake runners, separate short exhaust headers, shared collector and tailpipe.
|
||||
class FourStrokeTwinScenario {
|
||||
public:
|
||||
explicit FourStrokeTwinScenario(int sample_rate = 44100);
|
||||
AudioFrame process();
|
||||
void set_controls(float throttle_value, float load_value, float clutch_value);
|
||||
void toggle_mechanical_sound();
|
||||
void shift_up();
|
||||
void shift_down();
|
||||
void set_neutral();
|
||||
void set_first_gear();
|
||||
ScenarioSnapshot snapshot() const;
|
||||
|
||||
float throttle{};
|
||||
float load{};
|
||||
float clutch{1.0f};
|
||||
bool mechanical_sound_enabled{};
|
||||
float mechanical_mix{0.02f}, reverb_send_gain{1.0f};
|
||||
float exhaust_send{0.5f}, intake_send{0.5f}, mechanical_send{};
|
||||
Crankshaft crankshaft{};
|
||||
FourStrokeCylinder cylinder1{}, cylinder2{};
|
||||
Vehicle vehicle{};
|
||||
Volume0D intake_plenum{1.0e-3f, 101325.0f, 300.0f};
|
||||
Volume0D exhaust_collector{0.15e-3f, 101325.0f, 700.0f};
|
||||
std::vector<Pipe1D> pipes;
|
||||
float intake_flow{}, exhaust_flow{};
|
||||
float last_mix_l{}, last_mix_r{};
|
||||
std::uint64_t sample_count{};
|
||||
|
||||
private:
|
||||
Port* throttle_port_{};
|
||||
Port* runner_feed1_{};
|
||||
Port* runner_feed2_{};
|
||||
Port* collector_in1_{};
|
||||
Port* collector_in2_{};
|
||||
Port* collector_out_{};
|
||||
float throttle_area_{}, runner_area_{}, valve1_in_{}, valve2_in_{};
|
||||
float valve1_out_{}, valve2_out_{}, header_area_{}, tail_area_{};
|
||||
float dt_{};
|
||||
BoundarySystem boundaries_;
|
||||
Solver solver_;
|
||||
RadiationSource intake_radiation_;
|
||||
RadiationSource exhaust_radiation_;
|
||||
OutdoorExhaustReverb reverb_;
|
||||
MechanicalSoundProcessor mechanical_sound_;
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
static float area_for_diameter(float diameter);
|
||||
static std::vector<Pipe1D> create_pipes();
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
35
include/FluidSim/SimulationRingBuffer.hpp
Normal file
35
include/FluidSim/SimulationRingBuffer.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <stop_token>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class SimulationRingBuffer {
|
||||
public:
|
||||
explicit SimulationRingBuffer(std::size_t capacity);
|
||||
std::size_t available_samples() const;
|
||||
std::size_t free_space() const;
|
||||
bool write(float left, float right, std::stop_token stop = {});
|
||||
bool wait_for_space(std::size_t target_fill, std::stop_token stop);
|
||||
void reset_consumer();
|
||||
std::size_t read_interpolated(std::span<float> left, std::span<float> right, double speed);
|
||||
|
||||
private:
|
||||
std::vector<float> left_, right_;
|
||||
std::size_t mask_{};
|
||||
std::atomic<std::uint64_t> write_head_{};
|
||||
std::atomic<std::uint64_t> read_head_{};
|
||||
double read_position_{};
|
||||
bool consumer_initialized_{};
|
||||
mutable std::mutex wait_mutex_;
|
||||
std::condition_variable space_available_;
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
24
include/FluidSim/Solver.hpp
Normal file
24
include/FluidSim/Solver.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Component.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class Solver {
|
||||
public:
|
||||
Solver(std::vector<Pipe1D>& pipes, BoundarySystem& boundaries);
|
||||
int minimum_substeps{4};
|
||||
void add_component(Component& component);
|
||||
void step(float dt);
|
||||
std::uint64_t step_count() const { return step_count_; }
|
||||
private:
|
||||
std::vector<Pipe1D>& pipes_;
|
||||
BoundarySystem& boundaries_;
|
||||
std::vector<Component*> components_;
|
||||
std::uint64_t step_count_{};
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
31
include/FluidSim/SoundEngine.hpp
Normal file
31
include/FluidSim/SoundEngine.hpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/AudioOutputStream.hpp"
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Lifecycle wrapper matching the C# Audio.SoundEngine. The output stream owns
|
||||
// the device callback, while this class gives the application one explicit
|
||||
// start/stop and playback-control surface.
|
||||
class SoundEngine {
|
||||
public:
|
||||
explicit SoundEngine(SimulationRingBuffer& source_buffer);
|
||||
~SoundEngine();
|
||||
|
||||
SoundEngine(const SoundEngine&) = delete;
|
||||
SoundEngine& operator=(const SoundEngine&) = delete;
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
void set_speed(double speed);
|
||||
double speed() const;
|
||||
void set_volume(float volume);
|
||||
float volume() const;
|
||||
bool is_playing() const { return is_playing_; }
|
||||
|
||||
private:
|
||||
AudioOutputStream stream_;
|
||||
bool is_playing_{};
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
24
include/FluidSim/ThreadLoadTracker.hpp
Normal file
24
include/FluidSim/ThreadLoadTracker.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class ThreadLoadTracker {
|
||||
public:
|
||||
void record(double busy_milliseconds, double budget_milliseconds) {
|
||||
if (budget_milliseconds <= 0.0) return;
|
||||
constexpr double alpha = 0.05;
|
||||
const double instant = busy_milliseconds / budget_milliseconds * 100.0;
|
||||
const double previous = load_percent_.load(std::memory_order_relaxed);
|
||||
load_percent_.store(previous + alpha * (instant - previous), std::memory_order_relaxed);
|
||||
last_busy_ms_.store(busy_milliseconds, std::memory_order_relaxed);
|
||||
}
|
||||
double load_percent() const { return load_percent_.load(std::memory_order_relaxed); }
|
||||
double last_step_microseconds() const { return last_busy_ms_.load(std::memory_order_relaxed) * 1000.0; }
|
||||
private:
|
||||
std::atomic<double> load_percent_{};
|
||||
std::atomic<double> last_busy_ms_{};
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
33
include/FluidSim/TwoStrokeCylinder.hpp
Normal file
33
include/FluidSim/TwoStrokeCylinder.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Crankcase.hpp"
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Two-stroke variant of the reciprocating cylinder model. The shared four-
|
||||
// stroke implementation supplies geometry, state balance, and combustion
|
||||
// parameters; cycle events and pumping torque are specialized here.
|
||||
class TwoStrokeCylinder final : public EngineCylinder {
|
||||
public:
|
||||
TwoStrokeCylinder(float bore, float stroke, float connecting_rod_length,
|
||||
float compression_ratio, float transfer_duration_degrees,
|
||||
float exhaust_duration_degrees);
|
||||
|
||||
float transfer_open() const { return 180.0f - transfer_duration_ * 0.5f; }
|
||||
float transfer_close() const { return 180.0f + transfer_duration_ * 0.5f; }
|
||||
float exhaust_open() const { return 180.0f - exhaust_duration_ * 0.5f; }
|
||||
float exhaust_close() const { return 180.0f + exhaust_duration_ * 0.5f; }
|
||||
void set_crankcase(Crankcase& crankcase) { crankcase_ = &crankcase; }
|
||||
|
||||
float valve_area(float cycle_degrees, bool inlet) const override;
|
||||
void pre_step(Crankshaft& crank, float dt, float throttle) override;
|
||||
|
||||
private:
|
||||
float transfer_duration_{}, exhaust_duration_{};
|
||||
Crankcase* crankcase_{};
|
||||
|
||||
static float lift(float angle, float opens, float closes, float peak_lift);
|
||||
void handle_cycle_events(float previous_degrees, float current_degrees);
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
53
include/FluidSim/TwoStrokeScenario.hpp
Normal file
53
include/FluidSim/TwoStrokeScenario.hpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include "FluidSim/Crankcase.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/OutdoorExhaustReverb.hpp"
|
||||
#include "FluidSim/Solver.hpp"
|
||||
#include "FluidSim/TwoStrokeCylinder.hpp"
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
struct TwoStrokeSnapshot {
|
||||
float rpm{}, cylinder_pressure{}, crankcase_pressure{}, intake_pressure{};
|
||||
float throttle{}, transfer_area{}, exhaust_area{};
|
||||
bool reed_open{};
|
||||
};
|
||||
|
||||
// Port of the C# 125 cc crankcase-scavenged two-stroke scenario. It retains
|
||||
// the plenum / reed / crankcase / transfer / cylinder / exhaust topology.
|
||||
class TwoStrokeScenario {
|
||||
public:
|
||||
explicit TwoStrokeScenario(int sample_rate = 44100);
|
||||
AudioFrame process();
|
||||
void set_throttle(float value);
|
||||
TwoStrokeSnapshot snapshot() const;
|
||||
|
||||
float throttle{};
|
||||
Crankshaft crankshaft{};
|
||||
TwoStrokeCylinder cylinder;
|
||||
Crankcase crankcase;
|
||||
Volume0D intake_plenum{0.5e-3f, 101325.0f, 300.0f};
|
||||
std::vector<Pipe1D> pipes;
|
||||
|
||||
private:
|
||||
float dt_{}, max_throttle_area_{};
|
||||
float reed_in_area_{}, reed_out_area_{}, transfer_pipe_area_{}, transfer_valve_area_{}, exhaust_valve_area_{};
|
||||
bool reed_open_{};
|
||||
std::uint64_t step_count_{};
|
||||
Port* plenum_inlet_{};
|
||||
Port* plenum_outlet_{};
|
||||
BoundarySystem boundaries_;
|
||||
Solver solver_;
|
||||
SoundProcessor intake_sound_;
|
||||
SoundProcessor exhaust_sound_;
|
||||
OutdoorExhaustReverb reverb_;
|
||||
|
||||
static float area_for_diameter(float diameter);
|
||||
static std::vector<Pipe1D> create_pipes();
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
26
include/FluidSim/Units.hpp
Normal file
26
include/FluidSim/Units.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "FluidSim/Constants.hpp"
|
||||
|
||||
namespace fluidsim::port::units {
|
||||
|
||||
inline constexpr double mm = 1e-3;
|
||||
inline constexpr double cm = 1e-2;
|
||||
inline constexpr double inch = 0.0254;
|
||||
inline constexpr double mm2 = 1e-6;
|
||||
inline constexpr double cm2 = 1e-4;
|
||||
inline constexpr double milliliter = 1e-6;
|
||||
inline constexpr double liter = 1e-3;
|
||||
inline constexpr double pascal = 1.0;
|
||||
inline constexpr double kilopascal = 1e3;
|
||||
inline constexpr double bar = 1e5;
|
||||
inline constexpr double atmosphere = 101325.0;
|
||||
inline constexpr double psi = 6894.76;
|
||||
inline constexpr double gram = 1e-3;
|
||||
inline constexpr double pound = 0.453592;
|
||||
|
||||
inline constexpr double celsius(double value) { return value + 273.15; }
|
||||
inline constexpr double area_from_radius(double radius) { return static_cast<double>(pi) * radius * radius; }
|
||||
inline constexpr double area_from_diameter(double diameter) { return static_cast<double>(pi) * 0.25 * diameter * diameter; }
|
||||
|
||||
} // namespace fluidsim::port::units
|
||||
32
include/FluidSim/Vehicle.hpp
Normal file
32
include/FluidSim/Vehicle.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Longitudinal motorcycle model and the friction clutch coupling it to the crank.
|
||||
class Vehicle {
|
||||
public:
|
||||
int current_gear{};
|
||||
std::array<float, 6> gear_ratios{4.1f, 2.7f, 2.0f, 1.55f, 1.25f, 0.98f};
|
||||
float final_drive_ratio{2.7f}, primary_reduction{1.8f};
|
||||
float clutch_input{1.0f}, clutch_capacity{80.0f}, clutch_engagement_rate{8.0f};
|
||||
float mass{160.0f}, wheel_radius{0.32f}, rotational_inertia{1.2f};
|
||||
float rolling_resistance{0.015f}, drag_coefficient{0.7f}, frontal_area{0.65f};
|
||||
float air_density{1.225f}, gravity{9.80665f};
|
||||
float speed{}, engagement{};
|
||||
|
||||
float total_gear_ratio() const;
|
||||
float kinetic_energy() const;
|
||||
float update(float engine_omega, float engine_inertia, float dt);
|
||||
void shift_up();
|
||||
void shift_down();
|
||||
void set_neutral();
|
||||
void set_first_gear();
|
||||
|
||||
private:
|
||||
float shift_cut_timer_{};
|
||||
void apply_road_resistance(float dt);
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
Reference in New Issue
Block a user