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

@@ -7,11 +7,17 @@ namespace fluidsim::port {
FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selected_preset, bool turbo_enabled)
: preset(selected_preset),
intake_plenum(selected_preset == FourStrokePreset::SaabB205 ? 3e-3f : 1e-3f, 101325, 300),
exhaust_collector(selected_preset == FourStrokePreset::SaabB205 ? .6e-3f : .15e-3f, 101325, 700),
intake_plenum(selected_preset == FourStrokePreset::AmericanV8 ? 4e-3f : selected_preset == FourStrokePreset::SaabB205 ? 3e-3f : 1e-3f, 101325, 300),
exhaust_collector(selected_preset == FourStrokePreset::AmericanV8 ? 1.5e-3f : selected_preset == FourStrokePreset::SaabB205 ? .6e-3f : .15e-3f, 101325, 700),
pipes(create_pipes(selected_preset, turbo_enabled)), dt_(1.f / std::max(sample_rate, 1)),
boundaries_(pipes), solver_(pipes, boundaries_), audio_(sample_rate), mechanical_sound_(sample_rate) {
boundaries_(pipes), solver_(pipes, boundaries_), audio_(sample_rate, [&] {
OutdoorAudioConfig config;
config.dual_exhaust = selected_preset == FourStrokePreset::AmericanV8;
return config;
}()), mechanical_sound_(sample_rate) {
const bool saab = preset == FourStrokePreset::SaabB205;
const bool v8 = preset == FourStrokePreset::AmericanV8;
if (v8 && turbo_enabled) throw std::invalid_argument("American V8 preset is naturally aspirated");
const int count = cylinder_count();
crankshaft.omega = (saab ? 1200 : 2000) * 2 * pi / 60;
crankshaft.inertia = saab ? .18f : .04f;
@@ -36,13 +42,54 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
ecu.overrun_cut_rpm = 2000;
ecu.overrun_resume_rpm = 1300;
}
if (v8) {
// Estimated mild 1980s 302-class pushrod small block and manual coupe.
crankshaft.omega = 1000 * 2*pi/60;
crankshaft.inertia = 1.1f;
crankshaft.friction_constant = 22;
crankshaft.friction_viscous = .025f;
vehicle.mass = 3000;
vehicle.primary_reduction = 1;
vehicle.final_drive_ratio = 6.16f / 1.7f; // 1.7x taller gearing in every gear.
vehicle.gear_ratios = {3.35f, 1.93f, 1.29f, 1.f, .68f};
vehicle.wheel_radius = .32f;
vehicle.clutch_capacity = 1100; // Hold V8 torque with the heavy flywheel/car.
vehicle.drag_coefficient = .4f;
vehicle.frontal_area = 2.f;
ecu.idle_rpm = 750;
ecu.throttle_time_constant = .065f;
ecu.throttle_rate = 2.3f;
ecu.throttle_closing_time_constant = .16f;
ecu.throttle_closing_rate = 1.4f;
ecu.idle_base_opening = .08f;
ecu.idle_spark_gain = .02f;
ecu.ignition_map = {{0, 10}, {750, 12}, {2500, 28}, {4000, 32}, {6000, 30}};
ecu.rev_limit_rpm = 7000;
ecu.rev_resume_rpm = 6750;
ecu.overrun_cut_rpm = 2000;
ecu.overrun_resume_rpm = 1100;
// One minimum substep; acoustic CFL still subdivides when needed.
solver_.minimum_substeps = 1;
}
vehicle.set_neutral();
vehicle.clutch_input = clutch;
const auto engines = cylinders();
const std::array<float, 4> phases{0, pi, 3*pi, 2*pi}; // I4 firing: 1-3-4-2
const std::array<float, 8> phases{0, pi, 3*pi, 2*pi}; // I4 firing: 1-3-4-2
const std::array<int, 8> firing_order{1, 5, 4, 2, 6, 3, 7, 8};
for (int i = 0; i < count; ++i) {
auto& c = *engines[i];
c.phase = phases[i];
if (v8) {
const auto position = std::find(firing_order.begin(), firing_order.end(), i+1) - firing_order.begin();
c.phase = std::fmod(4*pi - float(position)*pi*.5f, 4*pi);
c.bore = .1016f;
c.stroke = .0762f;
c.rod = .129f;
c.compression = 7.0f;
c.intake_open = 350; c.intake_close = 570;
c.exhaust_open = 140; c.exhaust_close = 370;
c.burn_duration = 65;
}
if (saab) {
c.bore = .090f;
c.stroke = .078f;
@@ -54,13 +101,17 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
c.intake_valve_lift = saab ? .0085f : .008f;
c.exhaust_valve_diameter = saab ? .028f : .026f;
c.exhaust_valve_lift = saab ? .008f : .0075f;
if (v8) {
c.intake_valve_diameter = .045f; c.exhaust_valve_diameter = .037f;
c.intake_valve_lift = .010f; c.exhaust_valve_lift = .010f;
}
c.rev_limit_enabled = true;
c.rev_limit_rpm = ecu.rev_limit_rpm;
c.rev_limit_hysteresis = 250;
c.rev_limit_cut_time = .020f;
c.ecu = &ecu;
c.initialize(crankshaft);
if (saab) {
if (saab || v8) {
// Initialize each phased cylinder at its actual piston position.
c.volume = c.compute_volume(c.phase);
c.air_mass = ambient_density * c.volume;
@@ -80,6 +131,7 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
solver_.add_component(intake_atmosphere);
solver_.add_component(intake_plenum);
solver_.add_component(exhaust_collector);
if (v8) solver_.add_component(second_bank_collector);
if (turbo_enabled) {
turbo = std::make_unique<Turbocharger>(saab ? TurboConfig::gt17() : TurboConfig{});
ecu.boost_control_enabled = saab;
@@ -93,7 +145,8 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
boundaries_.add_orifice(throttle_port, 0, false, throttle_area_, .7f);
for (int i = 0; i < count; ++i) {
auto& feed = intake_plenum.create_port();
auto& collector_in = exhaust_collector.create_port();
// 1--4 and 5--8 are separate physical banks for this firing order.
auto& collector_in = (v8 && i >= 4 ? second_bank_collector : exhaust_collector).create_port();
solver_.add_component(*engines[i]);
boundaries_.add_orifice(feed, 1+i, true, runner_area_);
boundaries_.add_orifice(engines[i]->intake, 1+i, false, intake_valve_areas_[i]);
@@ -104,11 +157,16 @@ FourStrokeScenario::FourStrokeScenario(int sample_rate, FourStrokePreset selecte
boundaries_.add_orifice(collector_out, 1+2*count, true, tail_area_);
const int exhaust_exit = turbo_enabled ? 3+2*count : 1+2*count;
boundaries_.add_open_end(exhaust_exit, false, 101325, pipes[exhaust_exit].area);
if (v8) {
auto& outlet = second_bank_collector.create_port();
boundaries_.add_orifice(outlet, 18, true, tail_area_);
boundaries_.add_open_end(18, false, 101325, pipes[18].area);
}
}
AudioFrame FourStrokeScenario::process() {
const std::lock_guard guard(mutex_);
step_motorcycle(crankshaft, dt_, preset == FourStrokePreset::SaabB205 ? 200.f : 30.f);
step_motorcycle(crankshaft, dt_, preset == FourStrokePreset::AmericanV8 ? 300.f : preset == FourStrokePreset::SaabB205 ? 200.f : 30.f);
ecu.step(dt_, throttle, crankshaft.omega * 60 / (2*pi));
if (turbo) {
ecu.control_boost(dt_, throttle, crankshaft.omega * 60/(2*pi), intake_plenum.pressure(), turbo->compressor_outlet.pressure());
@@ -122,12 +180,15 @@ AudioFrame FourStrokeScenario::process() {
intake_valve_areas_[i] = c.valve_area(degrees, true);
exhaust_valve_areas_[i] = c.valve_area(degrees, false);
}
const float angle = ecu.throttle_opening() * pi * .5f;
throttle_area_ = area_for_diameter(preset == FourStrokePreset::SaabB205 ? .052f : .026f) * (1-std::cos(angle));
const float opening = ecu.throttle_opening();
const float angle = opening * pi * .5f;
throttle_area_ = area_for_diameter(preset == FourStrokePreset::AmericanV8 ? .065f : preset == FourStrokePreset::SaabB205 ? .052f : .026f) * (1-std::cos(angle));
solver_.step(dt_);
intake_flow = intake_atmosphere.port.mass_flow;
exhaust_flow = boundaries_.open_end_mass_flow(0);
const auto rendered = audio_.process(intake_flow, exhaust_flow);
const float other_exhaust = preset == FourStrokePreset::AmericanV8 ? boundaries_.open_end_mass_flow(1) : 0.f;
const auto rendered = audio_.process(intake_flow, exhaust_flow, other_exhaust);
exhaust_flow += other_exhaust;
const float mechanical = mechanical_sound_enabled
? mechanical_sound_.process(crankshaft, cylinder1, load) * mechanical_mix : 0;
last_mix_l = rendered.left + mechanical;
@@ -222,7 +283,8 @@ ScenarioSnapshot FourStrokeScenario::snapshot() const {
result.mechanical_sound_enabled = mechanical_sound_enabled;
result.cylinder_count = cylinder_count();
result.rev_limit_rpm = ecu.rev_limit_rpm;
const std::array<const FourStrokeCylinder*, 4> engines{&cylinder1, &cylinder2, &cylinder3, &cylinder4};
const std::array<const FourStrokeCylinder*, 8> engines{&cylinder1, &cylinder2, &cylinder3, &cylinder4,
&cylinder5, &cylinder6, &cylinder7, &cylinder8};
for (int i = 0; i < cylinder_count(); ++i) {
result.cylinder_pressures[i] = engines[i]->pressure;
result.piston_fractions[i] = (engines[i]->volume-engines[i]->clearance_volume())/engines[i]->swept_volume();
@@ -239,6 +301,20 @@ float FourStrokeScenario::area_for_diameter(float diameter) {
std::vector<Pipe1D> FourStrokeScenario::create_pipes(FourStrokePreset preset, bool turbo_enabled) {
const bool saab = preset == FourStrokePreset::SaabB205;
if (preset == FourStrokePreset::AmericanV8) {
// One inlet/throttle -> shared plenum -> short individual runners.
// Independent exhaust banks. Cells stay long enough to avoid routine
// extra CFL substeps during high-vacuum coast-down.
std::vector<Pipe1D> result;
result.emplace_back(area_for_diameter(.070f), .18f, 3);
for (int i = 0; i < 8; ++i)
result.emplace_back(area_for_diameter(.038f), .12f, 2);
for (int i = 0; i < 8; ++i)
result.emplace_back(area_for_diameter(.040f), .60f + .06f*(i%4), 12);
result.emplace_back(area_for_diameter(.050f), 1.8f, 36);
result.emplace_back(area_for_diameter(.050f), 1.95f, 39);
return result;
}
const int count = saab ? 4 : 2;
std::vector<Pipe1D> result;
result.reserve(4+2*count);