Add American V8 preset, carb fuel transport, and adaptive desktop audio
This commit is contained in:
@@ -31,6 +31,7 @@ private:
|
||||
port.temperature = temperature;
|
||||
port.enthalpy = specific_enthalpy();
|
||||
port.air_fraction = 1.0f;
|
||||
port.fuel_fraction = 0.0f;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ public:
|
||||
void set_speed(double speed) { speed_.store(speed, std::memory_order_relaxed); }
|
||||
std::uint64_t underrun_blocks() const { return underrun_blocks_.load(std::memory_order_relaxed); }
|
||||
double speed() const { return speed_.load(std::memory_order_relaxed); }
|
||||
void set_auto_gain(bool enabled) { auto_gain_enabled_.store(enabled, std::memory_order_relaxed); }
|
||||
bool auto_gain() const { return auto_gain_enabled_.load(std::memory_order_relaxed); }
|
||||
|
||||
private:
|
||||
static constexpr std::size_t block_size = 512;
|
||||
@@ -22,6 +24,9 @@ private:
|
||||
std::array<std::int16_t, block_size * 2> output_{};
|
||||
std::atomic<std::uint64_t> underrun_blocks_{};
|
||||
std::atomic<double> speed_{1.0};
|
||||
std::atomic<bool> auto_gain_enabled_{true};
|
||||
float rms_power_{}, peak_envelope_{}, automatic_gain_{1.f};
|
||||
float level_alpha_{}, peak_decay_{}, gain_rise_alpha_{}, gain_fall_alpha_{};
|
||||
float previous_input_left_{}, previous_output_left_{};
|
||||
float previous_input_right_{}, previous_output_right_{};
|
||||
|
||||
|
||||
46
include/FluidSim/Carburetor.hpp
Normal file
46
include/FluidSim/Carburetor.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
// Lumped slide-carburetor model for the crankcase-scavenged two-stroke. The
|
||||
// jets use liquid-orifice flow from a float bowl into the measured venturi
|
||||
// depression. Delivered fuel is injected into the downstream fluid volume and
|
||||
// is then transported as a conserved species; this class never doses a cylinder.
|
||||
class Carburetor {
|
||||
public:
|
||||
float fuel_density{740.0f};
|
||||
float pilot_jet_diameter{0.00040f}, main_jet_diameter{0.00150f};
|
||||
float needle_jet_diameter{0.00190f};
|
||||
float pilot_discharge_coefficient{0.62f}, main_discharge_coefficient{0.72f};
|
||||
float bowl_head_pressure{};
|
||||
float film_fraction{0.28f}, film_time_constant{0.11f};
|
||||
float bowl_capacity{0.008f}, bowl_refill_rate{0.0025f}; // kg, kg/s
|
||||
float overrun_rpm{4200.0f}, overrun_exit_rpm{3300.0f};
|
||||
|
||||
explicit Carburetor(float throat_diameter = 0.0381f);
|
||||
|
||||
// air_mass_flow is positive from atmosphere toward the engine.
|
||||
void step(float dt, float pedal, float slide, float rpm, float air_mass_flow,
|
||||
float air_density, float upstream_pressure, float downstream_pressure);
|
||||
|
||||
float indicated_afr() const { return indicated_afr_; }
|
||||
float fuel_flow() const { return delivered_fuel_flow_; }
|
||||
float film_mass() const { return film_mass_; }
|
||||
float bowl_level() const { return bowl_mass_ / bowl_capacity; }
|
||||
bool overrun_active() const { return overrun_; }
|
||||
bool fault() const { return fault_; }
|
||||
float venturi_depression() const { return venturi_depression_; }
|
||||
|
||||
private:
|
||||
float bowl_mass_{}, film_mass_{};
|
||||
float throat_area_{}, venturi_depression_{};
|
||||
float filtered_air_flow_{}, filtered_fuel_flow_{}, delivered_fuel_flow_{};
|
||||
float indicated_afr_{13.5f};
|
||||
float previous_pedal_{}, lift_enrichment_{};
|
||||
bool initialized_{}, overrun_{}, fault_{};
|
||||
};
|
||||
|
||||
} // namespace fluidsim::port
|
||||
@@ -12,9 +12,11 @@ struct Port {
|
||||
float temperature{300.0f};
|
||||
float gamma_value{1.4f}, gas_constant_value{287.0f};
|
||||
float air_fraction{1.0f};
|
||||
float fuel_fraction{};
|
||||
float* owner_energy{};
|
||||
float* owner_air_mass{};
|
||||
float* owner_exhaust_mass{};
|
||||
float* owner_fuel_mass{};
|
||||
};
|
||||
|
||||
// Small equivalent of the C# IComponent lifecycle. Components update their
|
||||
|
||||
@@ -20,6 +20,13 @@ public:
|
||||
float geometry_work() const { return geometry_work_; } // J delivered by gas during the latest geometry step.
|
||||
float mass() const { return mass_; }
|
||||
float air_fraction() const { return air_fraction_; }
|
||||
float fuel_fraction() const { return fuel_fraction_; }
|
||||
float fuel_mass() const { return mass_ * fuel_fraction_; }
|
||||
void initialize_mixture(float afr) {
|
||||
fuel_fraction_ = 1.0f / (1.0f + afr);
|
||||
air_fraction_ = 1.0f - fuel_fraction_;
|
||||
sync_ports();
|
||||
}
|
||||
|
||||
std::vector<Port*> ports() override { return {&intake_port, &transfer_port}; }
|
||||
void pre_step(float dt);
|
||||
@@ -30,7 +37,7 @@ private:
|
||||
float crank_radius_{}, connecting_rod_length_{}, piston_area_{};
|
||||
float clearance_volume_{}, obliquity_{};
|
||||
float geometry_work_{};
|
||||
float mass_{}, energy_{}, air_fraction_{1.0f};
|
||||
float mass_{}, energy_{}, air_fraction_{1.0f}, fuel_fraction_{};
|
||||
float pressure_{101325.0f}, temperature_{300.0f}, volume_{}, previous_volume_{};
|
||||
|
||||
void sync_ports();
|
||||
|
||||
@@ -14,7 +14,8 @@ public:
|
||||
two_ = std::make_unique<TwoStrokeScenario>(44100, config);
|
||||
else
|
||||
twin_ = std::make_unique<FourStrokeScenario>(44100, kind == ScenarioKind::SaabB205 ?
|
||||
FourStrokePreset::SaabB205 : FourStrokePreset::Twin, kind == ScenarioKind::TurboTwin || (kind == ScenarioKind::SaabB205 && saab_turbo));
|
||||
FourStrokePreset::SaabB205 : kind == ScenarioKind::AmericanV8 ? FourStrokePreset::AmericanV8 : FourStrokePreset::Twin,
|
||||
kind == ScenarioKind::TurboTwin || (kind == ScenarioKind::SaabB205 && saab_turbo));
|
||||
}
|
||||
AudioFrame process() {
|
||||
if (twin_)
|
||||
@@ -60,7 +61,7 @@ public:
|
||||
s.exhaust_pressure = two_->pipes[3].state(0).pressure;
|
||||
s.throttle = source.throttle;
|
||||
s.pedal = source.pedal;
|
||||
s.target_afr = two_->ecu.target_afr();
|
||||
s.target_afr = two_->carburetor.indicated_afr();
|
||||
s.valve1_intake_area = source.transfer_area;
|
||||
s.valve1_exhaust_area = source.exhaust_area;
|
||||
s.reed_open = source.reed_open;
|
||||
|
||||
@@ -12,9 +12,12 @@ public:
|
||||
float idle_base_opening{0.08f}, idle_max_opening{0.25f};
|
||||
float idle_kp{0.0001f}, idle_ki{0.00004f};
|
||||
float throttle_time_constant{0.08f}, throttle_rate{2.0f}; // seconds, full travel/second
|
||||
// Zero uses the opening calibration, preserving symmetric response.
|
||||
float throttle_closing_time_constant{}, throttle_closing_rate{};
|
||||
float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f};
|
||||
bool rev_limit_enabled{true};
|
||||
float overrun_cut_rpm{3500.0f}, overrun_resume_rpm{2400.0f};
|
||||
bool overrun_fuel_cut_enabled{true};
|
||||
float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC
|
||||
float stoichiometric_afr{14.7f}, power_afr{12.8f};
|
||||
bool boost_control_enabled{false};
|
||||
@@ -30,7 +33,7 @@ public:
|
||||
float throttle_target() const { return target_; }
|
||||
float target_afr() const { return afr_; }
|
||||
float spark_advance() const { return spark_; }
|
||||
bool fuel_cut() const { return limiter_ || overrun_ || fault_ || boost_cut_; }
|
||||
bool fuel_cut() const { return limiter_ || (overrun_ && overrun_fuel_cut_enabled) || fault_ || boost_cut_; }
|
||||
bool rev_limited() const { return limiter_; }
|
||||
bool idle_active() const { return idle_; }
|
||||
bool sensor_fault() const { return fault_; }
|
||||
|
||||
@@ -31,6 +31,7 @@ private:
|
||||
class EngineCylinder : public Component {
|
||||
public:
|
||||
Ecu* ecu{}; // Optional ideal electronic fuel/ignition controller.
|
||||
bool transported_fuel{}; // Upstream carb meters the fuel species.
|
||||
float bore{0.075f}, stroke{0.068f}, rod{0.112f}, compression{11.5f};
|
||||
float intake_valve_diameter{0.030f}, exhaust_valve_diameter{0.028f};
|
||||
int intake_valve_count{1}, exhaust_valve_count{1};
|
||||
@@ -60,6 +61,7 @@ public:
|
||||
|
||||
protected:
|
||||
float energy_factor_{1.0f};
|
||||
float active_fuel_mass_{};
|
||||
float rev_limit_timer_{};
|
||||
std::mt19937 random_{0xF17D51u};
|
||||
std::uniform_real_distribution<float> unit_{0.0f, 1.0f};
|
||||
|
||||
@@ -37,6 +37,7 @@ public:
|
||||
p.owner_energy = &energy_;
|
||||
p.owner_air_mass = &air_mass_;
|
||||
p.owner_exhaust_mass = &exhaust_mass_;
|
||||
p.owner_fuel_mass = &fuel_mass_;
|
||||
sync_port(p);
|
||||
return p;
|
||||
}
|
||||
@@ -44,19 +45,30 @@ public:
|
||||
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 mass() const { return air_mass_ + exhaust_mass_ + fuel_mass_; }
|
||||
float internal_energy() const { return energy_; }
|
||||
float air_fraction() const { return air_mass_ / std::max(mass(), 1e-12f); }
|
||||
float fuel_fraction() const { return fuel_mass_ / std::max(mass(), 1e-12f); }
|
||||
float fuel_mass() const { return fuel_mass_; }
|
||||
void add_fuel_mass(float value, float temperature = 300.0f) {
|
||||
if (!std::isfinite(value) || value < 0.0f || !std::isfinite(temperature) || temperature <= 0.0f)
|
||||
throw std::invalid_argument("invalid fuel addition");
|
||||
fuel_mass_ += value;
|
||||
energy_ += value * gas_constant / (gamma - 1.0f) * temperature;
|
||||
sync_ports();
|
||||
}
|
||||
void set_pressure(float value, float temp = -1.0f) {
|
||||
if (!std::isfinite(value) || value <= 0 || !std::isfinite(temp))
|
||||
throw std::invalid_argument("invalid volume pressure/temperature");
|
||||
const float target_t = temp > 0.0f ? temp : temperature();
|
||||
const float total = value * volume_ / (gas_constant * target_t);
|
||||
const float fraction = air_fraction();
|
||||
const float fuel = fuel_fraction();
|
||||
air_mass_ = total * fraction;
|
||||
exhaust_mass_ = total * (1.0f - fraction);
|
||||
fuel_mass_ = total * fuel;
|
||||
exhaust_mass_ = total * std::max(1.0f - fraction - fuel, 0.0f);
|
||||
energy_ = value * volume_ / (gamma - 1.0f);
|
||||
air_roundoff_ = exhaust_roundoff_ = energy_roundoff_ = 0;
|
||||
air_roundoff_ = exhaust_roundoff_ = fuel_roundoff_ = energy_roundoff_ = 0;
|
||||
sync_ports();
|
||||
}
|
||||
void update(float dt) {
|
||||
@@ -64,16 +76,20 @@ public:
|
||||
throw std::invalid_argument("invalid volume timestep");
|
||||
if (dt == 0)
|
||||
return;
|
||||
double d_air = 0, d_exhaust = 0, d_energy = 0;
|
||||
double d_air = 0, d_exhaust = 0, d_fuel = 0, d_energy = 0;
|
||||
const float fraction = air_fraction();
|
||||
const float fuel = fuel_fraction();
|
||||
for (const Port &p : ports_) {
|
||||
const float flow_fraction = p.mass_flow >= 0.0f ? p.air_fraction : fraction;
|
||||
const float flow_fuel = p.mass_flow >= 0.0f ? p.fuel_fraction : fuel;
|
||||
d_air += double(p.mass_flow) * flow_fraction;
|
||||
d_exhaust += double(p.mass_flow) * (1.0f - flow_fraction);
|
||||
d_fuel += double(p.mass_flow) * flow_fuel;
|
||||
d_exhaust += double(p.mass_flow) * std::max(1.0f - flow_fraction - flow_fuel, 0.0f);
|
||||
d_energy += double(p.mass_flow) * p.enthalpy;
|
||||
}
|
||||
accumulate(air_mass_, air_roundoff_, d_air * dt);
|
||||
accumulate(exhaust_mass_, exhaust_roundoff_, d_exhaust * dt);
|
||||
accumulate(fuel_mass_, fuel_roundoff_, d_fuel * dt);
|
||||
accumulate(energy_, energy_roundoff_, d_energy * dt);
|
||||
const float new_volume = volume_ + volume_change_rate * dt;
|
||||
if (!(new_volume > 0) || !std::isfinite(new_volume))
|
||||
@@ -93,10 +109,11 @@ public:
|
||||
if (!(mass() > 0) || !(energy_ > 0) || !std::isfinite(mass()) || !std::isfinite(energy_))
|
||||
throw std::runtime_error(
|
||||
"nonphysical volume state; reduce timestep or check external port fluxes");
|
||||
if (air_mass_ < -1e-12f || exhaust_mass_ < -1e-12f)
|
||||
if (air_mass_ < -1e-12f || exhaust_mass_ < -1e-12f || fuel_mass_ < -1e-12f)
|
||||
throw std::runtime_error("negative volume species mass");
|
||||
air_mass_ = std::max(air_mass_, 0.0f);
|
||||
exhaust_mass_ = std::max(exhaust_mass_, 0.0f);
|
||||
fuel_mass_ = std::max(fuel_mass_, 0.0f);
|
||||
sync_ports();
|
||||
}
|
||||
void update_state(float dt) override { update(dt); }
|
||||
@@ -110,9 +127,9 @@ public:
|
||||
const std::deque<Port> &ports() const { return ports_; }
|
||||
|
||||
private:
|
||||
float volume_, air_mass_{}, exhaust_mass_{}, energy_;
|
||||
float volume_, air_mass_{}, exhaust_mass_{}, fuel_mass_{}, energy_;
|
||||
// Preserve sub-ulp transfers without changing the float port/budget ABI.
|
||||
double air_roundoff_{}, exhaust_roundoff_{}, energy_roundoff_{};
|
||||
double air_roundoff_{}, exhaust_roundoff_{}, fuel_roundoff_{}, energy_roundoff_{};
|
||||
static void accumulate(float& value, double& roundoff, double increment) {
|
||||
const double precise = double(value)+roundoff+increment;
|
||||
value = float(precise);
|
||||
@@ -127,6 +144,7 @@ private:
|
||||
p.temperature = temperature();
|
||||
p.enthalpy = gamma / (gamma - 1.0f) * p.pressure / std::max(p.density, 1e-12f);
|
||||
p.air_fraction = air_fraction();
|
||||
p.fuel_fraction = fuel_fraction();
|
||||
}
|
||||
void sync_ports() {
|
||||
for (Port &p : ports_)
|
||||
@@ -138,16 +156,18 @@ struct GasCell {
|
||||
float rho{1.225f}, momentum{};
|
||||
float energy{1.225f * (101325.0f / (pipe_gm1 * 1.225f))};
|
||||
float air{1.0f};
|
||||
float fuel{};
|
||||
};
|
||||
struct GasState {
|
||||
float rho{1.225f}, velocity{}, pressure{101325.0f}, air{1.0f};
|
||||
float fuel{};
|
||||
};
|
||||
|
||||
class Pipe1D {
|
||||
public:
|
||||
// Integrated face fluxes: kg/s, N, W, kg/s of fresh air, positive along +x.
|
||||
// Integrated face fluxes: kg/s, N, W, kg/s of fresh air and fuel, positive along +x.
|
||||
struct Flux {
|
||||
float m{}, p{}, e{}, y{};
|
||||
float m{}, p{}, e{}, y{}, fuel{};
|
||||
};
|
||||
float area{}, dx{};
|
||||
float damping_multiplier{0.0f}, energy_relaxation_rate{0.0f}, ambient_pressure{101325.0f};
|
||||
@@ -221,7 +241,9 @@ public:
|
||||
}
|
||||
static GasState primitive(const GasCell &c) {
|
||||
const float u = c.momentum / std::max(c.rho, 1e-12f);
|
||||
return {c.rho, u, pipe_gm1 * (c.energy - .5f * c.momentum * u), std::clamp(c.air, 0.0f, 1.0f)};
|
||||
const float air = std::clamp(c.air, 0.0f, 1.0f);
|
||||
return {c.rho, u, pipe_gm1 * (c.energy - .5f * c.momentum * u), air,
|
||||
std::clamp(c.fuel, 0.0f, 1.0f - air)};
|
||||
}
|
||||
static float wave(const GasState &s) {
|
||||
return std::abs(s.velocity) + std::sqrt(gamma * s.pressure / s.rho);
|
||||
@@ -253,7 +275,9 @@ public:
|
||||
float mom = cells[i].momentum - factor * (fr.p - fl.p - states_[i].pressure * (ar - al));
|
||||
float en = cells[i].energy - factor * (fr.e - fl.e);
|
||||
const float air = cells[i].rho * cells[i].air - factor * (fr.y - fl.y);
|
||||
if (!std::isfinite(rho) || !std::isfinite(mom) || !std::isfinite(en) || !std::isfinite(air))
|
||||
const float fuel = cells[i].rho * cells[i].fuel - factor * (fr.fuel - fl.fuel);
|
||||
if (!std::isfinite(rho) || !std::isfinite(mom) || !std::isfinite(en) ||
|
||||
!std::isfinite(air) || !std::isfinite(fuel))
|
||||
throw std::runtime_error("nonfinite finite-volume update");
|
||||
if (rho < 1e-12f) {
|
||||
rho = 1e-12f;
|
||||
@@ -273,14 +297,18 @@ public:
|
||||
// than 1.18e-38 of the gas mass. Avoid repeatedly transporting
|
||||
// subnormal concentrations; total gas mass and energy are unchanged.
|
||||
const float fraction = std::clamp(air / rho, 0.0f, 1.0f);
|
||||
const float fuel_fraction = std::clamp(fuel / rho, 0.0f, 1.0f - fraction);
|
||||
next_cells_[i] = {rho, mom, en,
|
||||
fraction < std::numeric_limits<float>::min() ? 0.0f : fraction};
|
||||
fraction < std::numeric_limits<float>::min() ? 0.0f : fraction,
|
||||
fuel_fraction < std::numeric_limits<float>::min() ? 0.0f : fuel_fraction};
|
||||
}
|
||||
cells.swap(next_cells_);
|
||||
prepared_ = false;
|
||||
left_override_ = right_override_ = false;
|
||||
}
|
||||
static Flux scaled(Flux f, float a) { return {f.m * a, f.p * a, f.e * a, f.y * a}; }
|
||||
static Flux scaled(Flux f, float a) {
|
||||
return {f.m * a, f.p * a, f.e * a, f.y * a, f.fuel * a};
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<GasCell> next_cells_;
|
||||
@@ -301,7 +329,8 @@ private:
|
||||
return {.5f * (ma + mb - c * (b.rho - a.rho)),
|
||||
.5f * (ma * a.velocity + a.pressure + mb * b.velocity + b.pressure - c * (mb - ma)),
|
||||
.5f * ((ea + a.pressure) * a.velocity + (eb + b.pressure) * b.velocity - c * (eb - ea)),
|
||||
.5f * (ma * a.air + mb * b.air - c * (b.rho * b.air - a.rho * a.air))};
|
||||
.5f * (ma * a.air + mb * b.air - c * (b.rho * b.air - a.rho * a.air)),
|
||||
.5f * (ma * a.fuel + mb * b.fuel - c * (b.rho * b.fuel - a.rho * a.fuel))};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -329,7 +358,8 @@ public:
|
||||
[&](const Budget &b) { return b.air == port.owner_air_mass; });
|
||||
if (it == budgets_.end()) {
|
||||
o.budget = budgets_.size();
|
||||
budgets_.push_back({port.owner_air_mass, port.owner_exhaust_mass, port.owner_energy});
|
||||
budgets_.push_back({port.owner_air_mass, port.owner_exhaust_mass,
|
||||
port.owner_fuel_mass, port.owner_energy});
|
||||
} else
|
||||
o.budget = static_cast<std::size_t>(it - budgets_.begin());
|
||||
}
|
||||
@@ -385,7 +415,7 @@ public:
|
||||
if (!std::isfinite(dt) || dt <= 0)
|
||||
throw std::invalid_argument("boundary timestep must be positive");
|
||||
for (auto &b : budgets_) {
|
||||
b.mass = .25f * (*b.air + (b.exhaust ? *b.exhaust : 0));
|
||||
b.mass = .25f * (*b.air + (b.exhaust ? *b.exhaust : 0) + (b.fuel ? *b.fuel : 0));
|
||||
b.heat = b.energy ? .25f * (*b.energy) : std::numeric_limits<float>::max();
|
||||
}
|
||||
for (auto &o : orifices_)
|
||||
@@ -398,7 +428,7 @@ public:
|
||||
|
||||
private:
|
||||
struct Budget {
|
||||
float *air{}, *exhaust{}, *energy{};
|
||||
float *air{}, *exhaust{}, *fuel{}, *energy{};
|
||||
float mass{}, heat{};
|
||||
};
|
||||
struct Orifice {
|
||||
@@ -587,15 +617,18 @@ private:
|
||||
o.length > 0 ? .5f * (v.pressure + s.pressure) : static_cast<float>(duct_pressure);
|
||||
const float direction = o.left ? 1.f : -1.f;
|
||||
const float species = from ? v.air_fraction : s.air;
|
||||
const float fuel = from ? v.fuel_fraction : s.fuel;
|
||||
const float signed_mass = direction * mdot;
|
||||
p.set_boundary_flux(o.left, {signed_mass,
|
||||
mdot * velocity + face_pressure * area + s.pressure * (face_area - area),
|
||||
signed_mass * h, signed_mass * species});
|
||||
ghost(p, o.left, {density, direction * velocity, face_pressure, species});
|
||||
signed_mass * h, signed_mass * species, signed_mass * fuel});
|
||||
ghost(p, o.left, {density, direction * velocity, face_pressure, species, fuel});
|
||||
v.mass_flow = -mdot;
|
||||
v.enthalpy = h;
|
||||
if (!from)
|
||||
v.air_fraction = s.air;
|
||||
if (!from)
|
||||
v.fuel_fraction = s.fuel;
|
||||
}
|
||||
void resolve_open(Open &o) {
|
||||
auto &p = pipes_[o.pipe];
|
||||
@@ -611,7 +644,9 @@ private:
|
||||
const float u =
|
||||
o.left ? s.velocity - 2 * c / gm1 + 2 * cb / gm1 : s.velocity + 2 * c / gm1 - 2 * cb / gm1;
|
||||
const bool out = o.left ? s.velocity <= -c : s.velocity >= c;
|
||||
GasState g = out ? s : GasState{rho, u, o.pressure, (o.left ? u >= 0 : u <= 0) ? 1.f : s.air};
|
||||
const bool ambient_inflow = o.left ? u >= 0 : u <= 0;
|
||||
GasState g = out ? s : GasState{rho, u, o.pressure, ambient_inflow ? 1.f : s.air,
|
||||
ambient_inflow ? 0.f : s.fuel};
|
||||
ghost(p, o.left, g);
|
||||
const float a = std::min(o.area, p.cell_area(i));
|
||||
auto f = Pipe1D::scaled(o.left ? Pipe1D::riemann(g, s) : Pipe1D::riemann(s, g), a);
|
||||
@@ -631,9 +666,11 @@ private:
|
||||
const float area = std::min(a.cell_area(ia), b.cell_area(ib));
|
||||
auto f = Pipe1D::scaled(Pipe1D::riemann(sa, sb), area);
|
||||
a.set_boundary_flux(o.left_a,
|
||||
{da * f.m, f.p + sa.pressure * (a.cell_area(ia) - area), da * f.e, da * f.y});
|
||||
{da * f.m, f.p + sa.pressure * (a.cell_area(ia) - area), da * f.e,
|
||||
da * f.y, da * f.fuel});
|
||||
b.set_boundary_flux(o.left_b,
|
||||
{db * f.m, f.p + sb.pressure * (b.cell_area(ib) - area), db * f.e, db * f.y});
|
||||
{db * f.m, f.p + sb.pressure * (b.cell_area(ib) - area), db * f.e,
|
||||
db * f.y, db * f.fuel});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ struct OutdoorAudioConfig {
|
||||
Vec3 listener_right{1.0f, 0.0f, 0.0f};
|
||||
Vec3 intake_position{-0.2f, 0.7f, 0.0f};
|
||||
Vec3 exhaust_position{0.55f, 0.5f, 0.0f};
|
||||
Vec3 second_exhaust_position{-0.55f, 0.5f, 0.0f};
|
||||
bool dual_exhaust{};
|
||||
float ear_spacing{0.18f}, sound_speed{343.0f};
|
||||
float ground_y{0.0f}, wall_x{-1.5f};
|
||||
float ground_reflection{0.35f}, wall_reflection{0.65f};
|
||||
@@ -26,7 +28,7 @@ struct OutdoorAudioConfig {
|
||||
class OutdoorAudio {
|
||||
public:
|
||||
explicit OutdoorAudio(int sample_rate = 44100, OutdoorAudioConfig config = {});
|
||||
AudioFrame process(float intake_mass_flow, float exhaust_mass_flow);
|
||||
AudioFrame process(float intake_mass_flow, float exhaust_mass_flow, float second_exhaust_mass_flow = 0);
|
||||
void set_scene(OutdoorScene scene) { scene_ = scene; }
|
||||
OutdoorScene scene() const { return scene_; }
|
||||
void set_monitor_filter(bool enabled) { monitor_filter_ = enabled; } // Set before playback/capture.
|
||||
@@ -60,7 +62,7 @@ private:
|
||||
OutdoorAudioConfig config_;
|
||||
OutdoorScene scene_{OutdoorScene::Wall};
|
||||
bool monitor_filter_{true};
|
||||
std::array<Emitter, 2> emitters_;
|
||||
std::array<Emitter, 3> emitters_;
|
||||
std::array<TailLine, 4> tail_;
|
||||
std::array<float, 4> weights_{1, 0, 0, 0};
|
||||
float scene_alpha_{}, tail_alpha_{}, tail_mix_{};
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
enum class ScenarioKind { Twin, TwoStroke, SaabB205, TurboTwin };
|
||||
enum class FourStrokePreset { Twin, SaabB205 };
|
||||
enum class ScenarioKind { Twin, TwoStroke, SaabB205, TurboTwin, AmericanV8 };
|
||||
enum class FourStrokePreset { Twin, SaabB205, AmericanV8 };
|
||||
|
||||
struct ScenarioSnapshot {
|
||||
bool turbo_enabled{};
|
||||
@@ -28,7 +28,7 @@ struct ScenarioSnapshot {
|
||||
TurboSnapshot turbo{};
|
||||
int cylinder_count{2};
|
||||
float rev_limit_rpm{10000};
|
||||
std::array<float, 4> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
|
||||
std::array<float, 8> cylinder_pressures{}, piston_fractions{}, intake_areas{}, exhaust_areas{};
|
||||
std::uint64_t solver_substeps{}, boundary_iterations{};
|
||||
float rpm{}, speed_kmh{}, kinetic_energy{}, average_power{}, average_torque{};
|
||||
float cylinder_pressure1{}, cylinder_pressure2{};
|
||||
@@ -43,8 +43,8 @@ struct ScenarioSnapshot {
|
||||
OutdoorScene audio_scene{OutdoorScene::Wall};
|
||||
int gear{};
|
||||
bool rev_limit1{}, rev_limit2{}, mechanical_sound_enabled{};
|
||||
std::array<std::vector<GasState>, 12> pipe_states;
|
||||
std::array<std::vector<float>, 12> pipe_areas, pipe_lengths;
|
||||
std::array<std::vector<GasState>, 22> pipe_states;
|
||||
std::array<std::vector<float>, 22> pipe_areas, pipe_lengths;
|
||||
void capture_pipes(const std::vector<Pipe1D>& pipes) {
|
||||
for (std::size_t k = 0; k < pipes.size(); ++k) {
|
||||
pipe_areas[k] = pipes[k].cell_areas;
|
||||
@@ -57,14 +57,15 @@ struct ScenarioSnapshot {
|
||||
};
|
||||
|
||||
// Shared four-stroke topology: independent runners/headers and shared plenums.
|
||||
// Presets retain the original 600 cc twin and add a B205-sized inline-four.
|
||||
// Presets include a 600 cc twin, B205-sized inline-four and American 5.0 V8.
|
||||
class FourStrokeScenario : public MotorcycleTest {
|
||||
public:
|
||||
explicit FourStrokeScenario(int sample_rate = 44100, FourStrokePreset preset = FourStrokePreset::Twin,
|
||||
bool turbo_enabled = false);
|
||||
const FourStrokePreset preset;
|
||||
int cylinder_count() const { return preset == FourStrokePreset::SaabB205 ? 4 : 2; }
|
||||
std::array<FourStrokeCylinder*, 4> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4}; }
|
||||
int cylinder_count() const { return preset == FourStrokePreset::AmericanV8 ? 8 : preset == FourStrokePreset::SaabB205 ? 4 : 2; }
|
||||
std::array<FourStrokeCylinder*, 8> cylinders() { return {&cylinder1, &cylinder2, &cylinder3, &cylinder4,
|
||||
&cylinder5, &cylinder6, &cylinder7, &cylinder8}; }
|
||||
AudioFrame process();
|
||||
void set_controls(float throttle_value, float load_value, float clutch_value);
|
||||
void toggle_mechanical_sound();
|
||||
@@ -85,9 +86,11 @@ public:
|
||||
std::unique_ptr<Turbocharger> turbo;
|
||||
Crankshaft crankshaft{};
|
||||
FourStrokeCylinder cylinder1{}, cylinder2{}, cylinder3{}, cylinder4{};
|
||||
FourStrokeCylinder cylinder5{}, cylinder6{}, cylinder7{}, cylinder8{};
|
||||
Atmosphere intake_atmosphere{};
|
||||
Volume0D intake_plenum{1.0e-3f, 101325.0f, 300.0f};
|
||||
Volume0D exhaust_collector{0.15e-3f, 101325.0f, 700.0f};
|
||||
Volume0D second_bank_collector{1.5e-3f, 101325.0f, 700.0f};
|
||||
std::vector<Pipe1D> pipes;
|
||||
float intake_flow{}, exhaust_flow{};
|
||||
float last_mix_l{}, last_mix_r{};
|
||||
@@ -95,7 +98,7 @@ public:
|
||||
|
||||
private:
|
||||
float intake_boundary_area_{}, throttle_area_{}, runner_area_{}, header_area_{}, tail_area_{};
|
||||
std::array<float, 4> intake_valve_areas_{}, exhaust_valve_areas_{};
|
||||
std::array<float, 8> intake_valve_areas_{}, exhaust_valve_areas_{};
|
||||
float dt_{};
|
||||
BoundarySystem boundaries_;
|
||||
Solver solver_;
|
||||
@@ -108,6 +111,11 @@ private:
|
||||
};
|
||||
|
||||
using FourStrokeTwinScenario = FourStrokeScenario;
|
||||
class AmericanV8Scenario : public FourStrokeScenario {
|
||||
public:
|
||||
explicit AmericanV8Scenario(int sample_rate = 44100)
|
||||
: FourStrokeScenario(sample_rate, FourStrokePreset::AmericanV8) {}
|
||||
};
|
||||
class TurboTwinScenario : public FourStrokeScenario {
|
||||
public:
|
||||
explicit TurboTwinScenario(int sample_rate = 44100) : FourStrokeScenario(sample_rate, FourStrokePreset::Twin, true) {}
|
||||
|
||||
@@ -21,6 +21,8 @@ public:
|
||||
double speed() const;
|
||||
void set_volume(float volume);
|
||||
float volume() const;
|
||||
void set_auto_gain(bool enabled) { stream_.set_auto_gain(enabled); }
|
||||
bool auto_gain() const { return stream_.auto_gain(); }
|
||||
std::uint64_t underrun_blocks() const { return stream_.underrun_blocks(); }
|
||||
bool is_playing() const { return is_playing_; }
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
namespace fluidsim::port {
|
||||
|
||||
class Carburetor;
|
||||
|
||||
// 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.
|
||||
@@ -19,6 +21,7 @@ public:
|
||||
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; }
|
||||
void set_carburetor(Carburetor& carburetor) { carburetor_ = &carburetor; }
|
||||
float transfer_width{.050f}, exhaust_width{.036f};
|
||||
float minimum_burn_seconds{.0008f};
|
||||
std::uint64_t combustion_count() const { return combustion_count_; }
|
||||
@@ -29,6 +32,7 @@ public:
|
||||
private:
|
||||
float transfer_duration_{}, exhaust_duration_{};
|
||||
Crankcase* crankcase_{};
|
||||
Carburetor* carburetor_{};
|
||||
|
||||
float active_burn_duration_{60};
|
||||
float current_rpm_{};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "FluidSim/AudioProcessing.hpp"
|
||||
#include "FluidSim/Crankcase.hpp"
|
||||
#include "FluidSim/Carburetor.hpp"
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include "FluidSim/OutdoorAudio.hpp"
|
||||
#include "FluidSim/Atmosphere.hpp"
|
||||
@@ -44,6 +45,7 @@ public:
|
||||
float throttle{};
|
||||
const TwoStrokeConfig configuration;
|
||||
Ecu ecu{};
|
||||
Carburetor carburetor;
|
||||
ReedValve reed{};
|
||||
Atmosphere intake_atmosphere{};
|
||||
float intake_flow{}, exhaust_flow{};
|
||||
@@ -65,6 +67,7 @@ private:
|
||||
OutdoorAudio audio_;
|
||||
|
||||
static float area_for_diameter(float diameter);
|
||||
static float slide_area(float diameter, float opening);
|
||||
static std::vector<Pipe1D> create_pipes(const TwoStrokeConfig& config);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user