Add American V8 preset, carb fuel transport, and adaptive desktop audio

This commit is contained in:
2026-10-23 12:43:57 +02:00
parent 7a5c77c1c8
commit 85d4fb828a
36 changed files with 805 additions and 144 deletions

View File

@@ -31,6 +31,7 @@ private:
port.temperature = temperature;
port.enthalpy = specific_enthalpy();
port.air_fraction = 1.0f;
port.fuel_fraction = 0.0f;
}
};

View File

@@ -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_{};

View 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

View File

@@ -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

View File

@@ -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();

View File

@@ -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;

View File

@@ -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_; }

View File

@@ -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};

View File

@@ -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});
}
};

View File

@@ -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_{};

View File

@@ -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) {}

View File

@@ -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_; }

View File

@@ -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_{};

View File

@@ -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);
};