Add American V8 preset, carb fuel transport, and adaptive desktop audio
This commit is contained in:
@@ -7,6 +7,11 @@ namespace fluidsim::port {
|
||||
|
||||
AudioOutputStream::AudioOutputStream(SimulationRingBuffer& ring, unsigned int sample_rate)
|
||||
: ring_(ring) {
|
||||
const float rate = static_cast<float>(sample_rate);
|
||||
level_alpha_ = -std::expm1(-1.f / (rate * .15f));
|
||||
peak_decay_ = std::exp(-1.f / (rate * .15f));
|
||||
gain_rise_alpha_ = -std::expm1(-1.f / (rate * 1.5f));
|
||||
gain_fall_alpha_ = -std::expm1(-1.f / (rate * .03f));
|
||||
initialize(2, sample_rate, {sf::SoundChannel::FrontLeft, sf::SoundChannel::FrontRight});
|
||||
}
|
||||
|
||||
@@ -14,18 +19,38 @@ bool AudioOutputStream::onGetData(Chunk& data) {
|
||||
const std::size_t read = ring_.read_interpolated(temp_left_, temp_right_, speed());
|
||||
if (read < block_size) underrun_blocks_.fetch_add(1, std::memory_order_relaxed);
|
||||
constexpr float dc_coefficient = 0.9995f;
|
||||
constexpr float playback_gain = 4.0f; // +12 dB desktop playback boost
|
||||
const bool automatic = auto_gain();
|
||||
// Keep callback work bounded: no allocation, waits or simulation here.
|
||||
for (std::size_t i = 0; i < block_size; ++i) {
|
||||
float left{}, right{};
|
||||
if (i < read) {
|
||||
const float input_left = std::isfinite(temp_left_[i]) ? temp_left_[i] : 0.0f;
|
||||
const float input_right = std::isfinite(temp_right_[i]) ? temp_right_[i] : 0.0f;
|
||||
const float input_left = std::isfinite(temp_left_[i]) ? temp_left_[i] * playback_gain : 0.0f;
|
||||
const float input_right = std::isfinite(temp_right_[i]) ? temp_right_[i] * playback_gain : 0.0f;
|
||||
left = input_left - previous_input_left_ + dc_coefficient * previous_output_left_;
|
||||
right = input_right - previous_input_right_ + dc_coefficient * previous_output_right_;
|
||||
previous_input_left_ = input_left;
|
||||
previous_input_right_ = input_right;
|
||||
previous_output_left_ = left;
|
||||
previous_output_right_ = right;
|
||||
// Stereo-linked level detection after DC removal. Freeze on
|
||||
// underrun; silence returns to unity rather than chasing noise.
|
||||
const float peak = std::max(std::abs(left), std::abs(right));
|
||||
rms_power_ += (.5f * (left*left + right*right) - rms_power_) * level_alpha_;
|
||||
peak_envelope_ = std::max(peak, peak_envelope_ * peak_decay_);
|
||||
float target = 1.f;
|
||||
if (automatic && rms_power_ > 1e-8f) {
|
||||
target = std::clamp(.08f / std::sqrt(rms_power_), .125f, 12.f);
|
||||
target = std::min(target, .85f / std::max(peak_envelope_, 1e-6f));
|
||||
}
|
||||
const float alpha = target < automatic_gain_ ? gain_fall_alpha_ : gain_rise_alpha_;
|
||||
automatic_gain_ += (target - automatic_gain_) * alpha;
|
||||
// Catch a sudden rev while the slow idle boost is still elevated.
|
||||
// One linked multiplier preserves the stereo image.
|
||||
const float gain = (automatic || automatic_gain_ > 1.001f) ?
|
||||
std::min(automatic_gain_, .98f / std::max(peak, 1e-6f)) : automatic_gain_;
|
||||
left *= gain;
|
||||
right *= gain;
|
||||
if (starving_) {
|
||||
transition_left_ = last_left_ - left;
|
||||
transition_right_ = last_right_ - right;
|
||||
|
||||
102
src/carburetor.cpp
Normal file
102
src/carburetor.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#include "FluidSim/Carburetor.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace fluidsim::port {
|
||||
namespace {
|
||||
constexpr float pi = 3.14159265358979323846f;
|
||||
float circle_area(float diameter) { return .25f * pi * diameter * diameter; }
|
||||
float smoothstep(float value) {
|
||||
value = std::clamp(value, 0.0f, 1.0f);
|
||||
return value * value * (3.0f - 2.0f * value);
|
||||
}
|
||||
}
|
||||
|
||||
Carburetor::Carburetor(float throat_diameter)
|
||||
: bowl_mass_(bowl_capacity), throat_area_(circle_area(std::max(throat_diameter, 1e-4f))) {}
|
||||
|
||||
void Carburetor::step(float dt, float pedal, float slide, float rpm, float air_mass_flow,
|
||||
float air_density, float upstream_pressure, float downstream_pressure) {
|
||||
fault_ = !std::isfinite(dt) || dt <= 0.0f || !std::isfinite(pedal) ||
|
||||
!std::isfinite(slide) || !std::isfinite(rpm) || rpm < 0.0f ||
|
||||
!std::isfinite(air_mass_flow) || !std::isfinite(air_density) || air_density <= 0.0f ||
|
||||
!std::isfinite(upstream_pressure) ||
|
||||
!std::isfinite(downstream_pressure) || upstream_pressure <= 0.0f ||
|
||||
downstream_pressure <= 0.0f;
|
||||
if (fault_) {
|
||||
delivered_fuel_flow_ = 0.0f;
|
||||
overrun_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
pedal = std::clamp(pedal, 0.0f, 1.0f);
|
||||
slide = std::clamp(slide, 0.0f, 1.0f);
|
||||
air_mass_flow = std::max(air_mass_flow, 0.0f);
|
||||
if (!initialized_) {
|
||||
previous_pedal_ = pedal;
|
||||
initialized_ = true;
|
||||
}
|
||||
const bool throttle_lift = previous_pedal_ > 0.08f && pedal <= 0.005f && rpm > overrun_rpm;
|
||||
if (throttle_lift)
|
||||
lift_enrichment_ = 1.0f;
|
||||
previous_pedal_ = pedal;
|
||||
lift_enrichment_ *= std::exp(-dt / 0.32f);
|
||||
if (pedal > 0.005f || rpm < overrun_exit_rpm)
|
||||
overrun_ = false;
|
||||
else if (throttle_lift)
|
||||
overrun_ = true;
|
||||
|
||||
const float air_alpha = -std::expm1(-dt / 0.012f);
|
||||
filtered_air_flow_ += (air_mass_flow - filtered_air_flow_) * air_alpha;
|
||||
|
||||
// The open slide controls the effective venturi area. Its local dynamic
|
||||
// pressure and the measured static-pressure loss provide the jet signal.
|
||||
const float venturi_area = throat_area_ * 0.55f * (0.10f + 0.90f * slide);
|
||||
const float velocity = filtered_air_flow_ / std::max(air_density * venturi_area, 1e-9f);
|
||||
const float dynamic_pressure = 0.5f * air_density * velocity * velocity;
|
||||
const float static_drop = std::max(upstream_pressure - downstream_pressure, 0.0f);
|
||||
venturi_depression_ = std::clamp(dynamic_pressure + 0.18f * static_drop, 0.0f, 60000.0f);
|
||||
|
||||
const float pilot_area = circle_area(pilot_jet_diameter);
|
||||
const float main_area = circle_area(main_jet_diameter);
|
||||
const float needle_jet_area = circle_area(needle_jet_diameter);
|
||||
const float needle_lift = smoothstep((slide - 0.04f) / 0.86f);
|
||||
const float needle_area = needle_jet_area * needle_lift;
|
||||
const float main_path_area = needle_area > 1e-12f ?
|
||||
1.0f / std::sqrt(1.0f / (main_area * main_area) + 1.0f / (needle_area * needle_area)) : 0.0f;
|
||||
const float pilot_depression = bowl_head_pressure + (0.62f * static_drop +
|
||||
0.25f * dynamic_pressure) * (1.0f + 0.25f * lift_enrichment_);
|
||||
const float main_depression = bowl_head_pressure + venturi_depression_;
|
||||
const float pilot_flow = pilot_discharge_coefficient * pilot_area *
|
||||
std::sqrt(2.0f * fuel_density * std::max(pilot_depression, 0.0f));
|
||||
const float main_flow = main_discharge_coefficient * main_path_area *
|
||||
std::sqrt(2.0f * fuel_density * std::max(main_depression, 0.0f));
|
||||
float demanded_fuel_flow = pilot_flow + main_flow;
|
||||
|
||||
const float deposit_fraction = std::clamp(film_fraction * (0.55f + 0.45f * slide), 0.0f, 0.8f);
|
||||
const float deposited = demanded_fuel_flow * deposit_fraction;
|
||||
const float evaporation_time = film_time_constant * (overrun_ ? 0.58f : 1.0f);
|
||||
const float evaporated = std::min(film_mass_ / dt,
|
||||
film_mass_ / std::max(evaporation_time, 1e-4f));
|
||||
float jet_flow = demanded_fuel_flow;
|
||||
const float available_flow = bowl_mass_ / dt;
|
||||
if (jet_flow > available_flow) {
|
||||
const float scale = available_flow / std::max(jet_flow, 1e-12f);
|
||||
jet_flow *= scale;
|
||||
}
|
||||
bowl_mass_ -= jet_flow * dt;
|
||||
bowl_mass_ += std::min(bowl_refill_rate * dt, bowl_capacity - bowl_mass_);
|
||||
bowl_mass_ = std::clamp(bowl_mass_, 0.0f, bowl_capacity);
|
||||
|
||||
// Deposition cannot exceed the fuel that actually left the bowl.
|
||||
const float actual_deposit = std::min(deposited, jet_flow);
|
||||
film_mass_ = std::max(film_mass_ + (actual_deposit - evaporated) * dt, 0.0f);
|
||||
delivered_fuel_flow_ = std::max(jet_flow - actual_deposit + evaporated, 0.0f);
|
||||
const float fuel_alpha = -std::expm1(-dt / 0.025f);
|
||||
filtered_fuel_flow_ += (delivered_fuel_flow_ - filtered_fuel_flow_) * fuel_alpha;
|
||||
if (filtered_air_flow_ > 1e-7f && filtered_fuel_flow_ > 1e-9f)
|
||||
indicated_afr_ = std::clamp(filtered_air_flow_ / filtered_fuel_flow_, 4.0f, 40.0f);
|
||||
}
|
||||
|
||||
} // namespace fluidsim::port
|
||||
@@ -51,18 +51,25 @@ void Crankcase::pre_step(float /*dt*/) {
|
||||
}
|
||||
|
||||
void Crankcase::update_state(float dt) {
|
||||
float delta_mass = 0.0f, delta_air_mass = 0.0f, delta_energy = 0.0f;
|
||||
float delta_mass = 0.0f, delta_air_mass = 0.0f, delta_fuel_mass = 0.0f, delta_energy = 0.0f;
|
||||
for (Port* port : {&intake_port, &transfer_port}) {
|
||||
const float flow = port->mass_flow;
|
||||
const float fraction = flow >= 0.0f ? port->air_fraction : air_fraction_;
|
||||
const float fuel = flow >= 0.0f ? port->fuel_fraction : fuel_fraction_;
|
||||
delta_mass += flow * dt;
|
||||
delta_air_mass += flow * fraction * dt;
|
||||
delta_fuel_mass += flow * fuel * dt;
|
||||
delta_energy += flow * port->enthalpy * dt;
|
||||
}
|
||||
const float old_air_mass = mass_ * air_fraction_;
|
||||
const float old_fuel_mass = mass_ * fuel_fraction_;
|
||||
mass_ += delta_mass;
|
||||
energy_ += delta_energy;
|
||||
if (mass_ > 1e-12f) air_fraction_ = std::clamp((old_air_mass + delta_air_mass) / mass_, 0.0f, 1.0f);
|
||||
if (mass_ > 1e-12f) {
|
||||
air_fraction_ = std::clamp((old_air_mass + delta_air_mass) / mass_, 0.0f, 1.0f);
|
||||
fuel_fraction_ = std::clamp((old_fuel_mass + delta_fuel_mass) / mass_,
|
||||
0.0f, 1.0f - air_fraction_);
|
||||
}
|
||||
|
||||
if (!(mass_ > 0.0f) || !(energy_ > 0.0f) || !std::isfinite(mass_) || !std::isfinite(energy_))
|
||||
throw std::runtime_error("nonphysical crankcase state");
|
||||
@@ -80,6 +87,7 @@ void Crankcase::sync_ports() {
|
||||
port->temperature = temperature_;
|
||||
port->enthalpy = enthalpy;
|
||||
port->air_fraction = air_fraction_;
|
||||
port->fuel_fraction = fuel_fraction_;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,14 +57,15 @@ void draw_label(sf::RenderWindow& window, const sf::Font* font, const std::strin
|
||||
}
|
||||
|
||||
std::optional<ScenarioKind> choose_scenario(sf::RenderWindow& window, const sf::Font* font) {
|
||||
const std::array<sf::FloatRect, 4> cards{{{{32, 110}, {656, 84}}, {{32, 206}, {656, 84}}, {{32, 302}, {656, 84}}, {{32, 398}, {656, 84}}}};
|
||||
const sf::FloatRect start_button{{32, 506}, {656, 52}};
|
||||
const std::array<const char*, 4> titles{"600 cc parallel twin", "YZ125 two-stroke", "Saab B205 / 9-5 wagon", "600 cc twin + VZ21-size turbo"};
|
||||
const std::array<const char*, 4> descriptions{
|
||||
const std::array<sf::FloatRect, 5> cards{{{{32, 110}, {656, 72}}, {{32, 190}, {656, 72}}, {{32, 270}, {656, 72}}, {{32, 350}, {656, 72}}, {{32, 430}, {656, 72}}}};
|
||||
const sf::FloatRect start_button{{32, 522}, {656, 52}};
|
||||
const std::array<const char*, 5> titles{"600 cc parallel twin", "YZ125 two-stroke", "Saab B205 / 9-5 wagon", "600 cc twin + VZ21-size turbo", "1980s American 5.0 V8"};
|
||||
const std::array<const char*, 5> descriptions{
|
||||
"Four-stroke twin, motorcycle test and dyno",
|
||||
"125 cc single, expansion chamber, motorcycle test and dyno",
|
||||
"2.0 litre inline-four, GT17 turbo, ECU boost control and five-speed wagon",
|
||||
"Coupled turbine/compressor, shaft inertia and pressure diagnostics"};
|
||||
"Coupled turbine/compressor, shaft inertia and pressure diagnostics",
|
||||
"EFI cross-plane V8, heavy flywheel, independent exhaust banks"};
|
||||
int selected = 0;
|
||||
while (window.isOpen()) {
|
||||
while (const auto event = window.pollEvent()) {
|
||||
@@ -72,26 +73,27 @@ std::optional<ScenarioKind> choose_scenario(sf::RenderWindow& window, const sf::
|
||||
if (const auto* key = event->getIf<sf::Event::KeyPressed>()) {
|
||||
switch (key->code) {
|
||||
case sf::Keyboard::Key::Escape: return std::nullopt;
|
||||
case sf::Keyboard::Key::Up: selected = (selected + 3) % 4; break;
|
||||
case sf::Keyboard::Key::Up: selected = (selected + 4) % 5; break;
|
||||
case sf::Keyboard::Key::Down:
|
||||
case sf::Keyboard::Key::Tab: selected = (selected + 1) % 4; break;
|
||||
case sf::Keyboard::Key::Tab: selected = (selected + 1) % 5; break;
|
||||
case sf::Keyboard::Key::Num1: selected = 0; break;
|
||||
case sf::Keyboard::Key::Num2: selected = 1; break;
|
||||
case sf::Keyboard::Key::Num3: selected = 2; break;
|
||||
case sf::Keyboard::Key::Num4: selected = 3; break;
|
||||
case sf::Keyboard::Key::Num5: selected = 4; break;
|
||||
case sf::Keyboard::Key::Enter: return static_cast<ScenarioKind>(selected);
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
if (const auto* move = event->getIf<sf::Event::MouseMoved>()) {
|
||||
const auto point = window.mapPixelToCoords(move->position);
|
||||
for (int i = 0; i < 4; ++i)
|
||||
for (int i = 0; i < 5; ++i)
|
||||
if (cards[i].contains(point)) selected = i;
|
||||
}
|
||||
if (const auto* click = event->getIf<sf::Event::MouseButtonReleased>();
|
||||
click && click->button == sf::Mouse::Button::Left) {
|
||||
const auto point = window.mapPixelToCoords(click->position);
|
||||
for (int i = 0; i < 4; ++i)
|
||||
for (int i = 0; i < 5; ++i)
|
||||
if (cards[i].contains(point)) return static_cast<ScenarioKind>(i);
|
||||
if (start_button.contains(point)) return static_cast<ScenarioKind>(selected);
|
||||
}
|
||||
@@ -99,7 +101,7 @@ std::optional<ScenarioKind> choose_scenario(sf::RenderWindow& window, const sf::
|
||||
window.clear({9, 12, 17});
|
||||
draw_label(window, font, "FluidSim", {32, 24}, sf::Color::White, 28);
|
||||
draw_label(window, font, "Choose a scenario to start", {32, 66}, {180, 190, 205}, 16);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
sf::RectangleShape card(cards[i].size);
|
||||
card.setPosition(cards[i].position);
|
||||
card.setFillColor(i == selected ? sf::Color(24, 46, 60) : sf::Color(18, 24, 32));
|
||||
@@ -115,7 +117,7 @@ std::optional<ScenarioKind> choose_scenario(sf::RenderWindow& window, const sf::
|
||||
start.setPosition(start_button.position);
|
||||
start.setFillColor({35, 105, 115});
|
||||
window.draw(start);
|
||||
draw_label(window, font, "Start selected scenario [Enter]", {52, 519}, sf::Color::White, 17);
|
||||
draw_label(window, font, "Start selected scenario [Enter]", {52, 537}, sf::Color::White, 17);
|
||||
draw_label(window, font, "Click a scenario or use Up/Down. Esc: quit", {32, 574}, {150, 165, 180}, 12);
|
||||
window.display();
|
||||
}
|
||||
@@ -296,13 +298,13 @@ void draw_dyno_curve(sf::RenderWindow& window, const sf::Font* font, const DynoC
|
||||
|
||||
void draw_dashboard(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state,
|
||||
float throttle_target, float brake, double playback_speed, std::size_t buffer_fill,
|
||||
std::size_t buffer_capacity, bool time_warp, std::uint64_t underrun_blocks) {
|
||||
std::size_t buffer_capacity, bool time_warp, std::uint64_t underrun_blocks, bool auto_gain) {
|
||||
std::ostringstream status;
|
||||
status << (time_warp ? "[Space] normal speed" : "[Space] real time")
|
||||
<< " Sim " << std::fixed << std::setprecision(3) << playback_speed << "x"
|
||||
<< " Audio buffer " << buffer_fill << "/" << buffer_capacity
|
||||
<< " Underruns " << underrun_blocks
|
||||
<< (state.cylinder_count == 4 ? "\nWagon " : "\nBike ") << std::setprecision(1) << state.speed_kmh << " km/h Gear "
|
||||
<< (state.cylinder_count == 8 ? "\nCoupe " : state.cylinder_count == 4 ? "\nWagon " : "\nBike ") << std::setprecision(1) << state.speed_kmh << " km/h Gear "
|
||||
<< (state.gear == 0 ? "N" : std::to_string(state.gear))
|
||||
<< " Throttle " << state.throttle * 100.0f << "% / target " << throttle_target * 100.0f << "% [W, up/down]"
|
||||
<< " Clutch " << state.clutch * 100.0f << "% [C] Engine load " << brake * 100.0f << "% [left/right]"
|
||||
@@ -310,6 +312,7 @@ void draw_dashboard(sf::RenderWindow& window, const sf::Font* font, const Scenar
|
||||
<< "\nScene [R]: " << scene_name(state.audio_scene) << " " << (state.fuel_cut ? "FUEL CUT" : (state.idle_control ? "IDLE CONTROL" : "RUNNING"));
|
||||
if (!state.two_stroke)
|
||||
status << " Mechanical [M]: " << (state.mechanical_sound_enabled ? "ON" : "OFF");
|
||||
status << " Auto gain [G]: " << (auto_gain ? "ON" : "OFF");
|
||||
if (font) {
|
||||
sf::RectangleShape panel({1260.0f, 92.0f});
|
||||
panel.setPosition({10.0f, 10.0f});
|
||||
@@ -350,7 +353,7 @@ void draw_two_stroke_scene(sf::RenderWindow& window,const sf::Font* font,const S
|
||||
draw_label(window,font,"CRANKCASE",{455,385},sf::Color::White,14);
|
||||
draw_pipe(window,s,0,460,30,170);
|
||||
draw_volume(window,s.intake_pressure,{{180,425},{95,70}});
|
||||
draw_label(window,font,"INTAKE",{180,400},sf::Color::White,14);
|
||||
draw_label(window,font,"CARB / INTAKE",{180,400},sf::Color::White,14);
|
||||
draw_pipe(window,s,1,460,285,445);
|
||||
draw_label(window,font,s.reed_open?"REED OPEN":"REED CLOSED",{290,485},s.reed_open?sf::Color::Green:sf::Color::Red,12);
|
||||
draw_pipe(window,s,2,350,315,450);
|
||||
@@ -365,7 +368,8 @@ void draw_two_stroke_scene(sf::RenderWindow& window,const sf::Font* font,const S
|
||||
draw_label(window,font,s.racing_exhaust ? "HEADER / DIFFUSERS / BELLY / BAFFLE / STINGER" : "STRAIGHT EXHAUST",{670,205},sf::Color::White,13);
|
||||
std::ostringstream pressures;
|
||||
pressures<<std::fixed<<std::setprecision(1)<<"Cylinder "<<s.cylinder_pressure1/1000<<" kPa Crankcase "
|
||||
<<s.crankcase_pressure/1000<<" kPa Intake "<<s.intake_pressure/1000<<" kPa";
|
||||
<<s.crankcase_pressure/1000<<" kPa Intake "<<s.intake_pressure/1000<<" kPa Carb AFR "
|
||||
<<s.target_afr<<":1";
|
||||
draw_label(window,font,pressures.str(),{22,580},sf::Color::White,17);
|
||||
draw_label(window,font,"54 x 54.5 mm | 8.2:1 trapped compression | estimated pipe/ports/CDI | fixed-open power valve",
|
||||
{22,625},{180,190,200},11);
|
||||
@@ -459,14 +463,38 @@ void draw_b205_scene(sf::RenderWindow& window, const sf::Font* font, const Scena
|
||||
|
||||
void draw_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state,
|
||||
float throttle_target, float brake, double playback_speed, std::size_t buffer_fill,
|
||||
std::size_t buffer_capacity, bool time_warp, DynoCurve& dyno_curve, std::uint64_t underrun_blocks) {
|
||||
std::size_t buffer_capacity, bool time_warp, DynoCurve& dyno_curve, std::uint64_t underrun_blocks, bool auto_gain) {
|
||||
const float power_kw = state.average_power / 1000.0f;
|
||||
dyno_curve.update(state.rpm, power_kw, state.average_torque);
|
||||
if (state.two_stroke) draw_two_stroke_scene(window, font, state);
|
||||
if (state.cylinder_count == 8) {
|
||||
window.clear({9,12,17});
|
||||
draw_label(window, font, "American 5.0 V8 | EFI / ECU | 7000 RPM | heavy flywheel", {25,185}, sf::Color::White, 16);
|
||||
draw_pipe(window, state, 0, 205, 25, 115);
|
||||
draw_volume(window, state.intake_pressure, {{120,200},{60,25}});
|
||||
draw_label(window, font, "ONE THROTTLE / SHARED PLENUM", {850,225}, sf::Color::White, 12);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const float x = 70 + (i%4)*185;
|
||||
const float top = i < 4 ? 230.f : 455.f;
|
||||
const std::string name = "CYL " + std::to_string(i+1);
|
||||
draw_cylinder(window, font, state.cylinder_pressures[i], state.piston_fractions[i], top,
|
||||
name.c_str(), state.intake_areas[i]>1e-7f, state.exhaust_areas[i]>1e-7f, x);
|
||||
draw_pipe(window, state, 9+i, top+150, x-10, x+90);
|
||||
draw_pipe(window, state, 1+i, top+125, x-10, x+90);
|
||||
draw_graph_line(window, {x+90,top+150}, {830,top+150}, {200,120,80});
|
||||
}
|
||||
draw_graph_line(window, {830,380}, {850,300}, {200,120,80});
|
||||
draw_graph_line(window, {830,605}, {850,375}, {200,120,80});
|
||||
draw_pipe(window, state, 17, 300, 850, 1250);
|
||||
draw_pipe(window, state, 18, 375, 850, 1250);
|
||||
draw_label(window, font, "SEPARATE BANK COLLECTORS / DUAL TAILS", {850,410}, sf::Color::White, 11);
|
||||
draw_label(window, font, "EFI AFR " + std::to_string(state.target_afr), {850,430}, sf::Color::White, 13);
|
||||
draw_label(window, font, "302-class | shared 4 L intake, 12 cm runners | 7:1 compression | 750 rpm idle", {25,690}, {180,190,205}, 12);
|
||||
}
|
||||
else if (state.two_stroke) draw_two_stroke_scene(window, font, state);
|
||||
else if (state.cylinder_count == 4) draw_b205_scene(window, font, state);
|
||||
else draw_twin_scene(window, font, state);
|
||||
draw_dashboard(window, font, state, throttle_target, brake, playback_speed, buffer_fill,
|
||||
buffer_capacity, time_warp, underrun_blocks);
|
||||
buffer_capacity, time_warp, underrun_blocks, auto_gain);
|
||||
draw_dyno_curve(window, font, dyno_curve, 860, 455, 400, 250, state.rpm, power_kw);
|
||||
window.display();
|
||||
}
|
||||
@@ -486,8 +514,8 @@ int main(int argc, char** argv) {
|
||||
if((option=="--scenario"||option=="--scene"||option=="--exhaust"||option=="--turbo")&&i+1<argc){
|
||||
const std::string value=argv[++i];
|
||||
if(option=="--scenario"){
|
||||
if(value!="twin"&&value!="two-stroke"&&value!="b205"){std::cerr<<"Choose twin, two-stroke or b205\n";return 2;}
|
||||
selected_kind=value=="b205" ? ScenarioKind::SaabB205 :
|
||||
if(value!="twin"&&value!="two-stroke"&&value!="b205"&&value!="v8"){std::cerr<<"Choose twin, two-stroke, b205 or v8\n";return 2;}
|
||||
selected_kind=value=="v8" ? ScenarioKind::AmericanV8 : value=="b205" ? ScenarioKind::SaabB205 :
|
||||
(value=="two-stroke" ? ScenarioKind::TwoStroke : ScenarioKind::Twin);
|
||||
scenario_from_args=true;
|
||||
}else if(option=="--turbo"){
|
||||
@@ -505,7 +533,7 @@ int main(int argc, char** argv) {
|
||||
else if(value=="woodland")selected_scene=OutdoorScene::Woodland;
|
||||
else {std::cerr<<"Choose dry, field, wall or woodland\n";return 2;}
|
||||
}
|
||||
}else{std::cerr<<"Usage: fluidsim_desktop [--scenario twin|two-stroke|b205] [--scene dry|field|wall|woodland] [--exhaust racing|straight] [--turbo none|vz21|gt17]\n";return 2;}
|
||||
}else{std::cerr<<"Usage: fluidsim_desktop [--scenario twin|two-stroke|b205|v8] [--scene dry|field|wall|woodland] [--exhaust racing|straight] [--turbo none|vz21|gt17]\n";return 2;}
|
||||
}
|
||||
const sf::Vector2u initial_size = scenario_from_args ? sf::Vector2u{window_width, window_height}
|
||||
: sf::Vector2u{720, 606};
|
||||
@@ -542,7 +570,7 @@ int main(int argc, char** argv) {
|
||||
std::cerr<<"Use gt17 with b205 or vz21 with twin\n";return 2;
|
||||
}
|
||||
}
|
||||
window.setTitle(selected_kind == ScenarioKind::TurboTwin ? "FluidSim - twin + VZ21 turbo" : selected_kind == ScenarioKind::SaabB205 ? "FluidSim - Saab B205 / 9-5 wagon" :
|
||||
window.setTitle(selected_kind == ScenarioKind::AmericanV8 ? "FluidSim - 1980s American 5.0 V8" : selected_kind == ScenarioKind::TurboTwin ? "FluidSim - twin + VZ21 turbo" : selected_kind == ScenarioKind::SaabB205 ? "FluidSim - Saab B205 / 9-5 wagon" :
|
||||
(selected_kind == ScenarioKind::TwoStroke ? "FluidSim - YZ125 two-stroke" : "FluidSim - parallel twin"));
|
||||
DesktopScenario scenario(selected_kind, two_stroke_config, turbo_name!="none");
|
||||
scenario.set_audio_scene(selected_scene);
|
||||
@@ -600,6 +628,7 @@ int main(int argc, char** argv) {
|
||||
scenario.set_audio_scene(selected_scene);
|
||||
break;
|
||||
case sf::Keyboard::Key::M: scenario.toggle_mechanical_sound(); break;
|
||||
case sf::Keyboard::Key::G: audio.set_auto_gain(!audio.auto_gain()); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -629,7 +658,7 @@ int main(int argc, char** argv) {
|
||||
scenario.set_controls(throttle_current, brake_current, clutch_current);
|
||||
|
||||
draw_scene(window, font_ptr, scenario.snapshot(), throttle_target, brake_current,
|
||||
current_speed, ring.available_samples(), ring_capacity, time_warp, dyno_curve, audio.underrun_blocks());
|
||||
current_speed, ring.available_samples(), ring_capacity, time_warp, dyno_curve, audio.underrun_blocks(), audio.auto_gain());
|
||||
}
|
||||
|
||||
simulation.request_stop();
|
||||
|
||||
@@ -51,8 +51,12 @@ void Ecu::step(float dt, float pedal, float rpm) {
|
||||
const float idle_command = idle_ ? std::clamp(idle_base_opening + idle_kp * error + integral_,
|
||||
0.0f, idle_max_opening) : 0.0f;
|
||||
target_ = std::max(pedal, idle_command);
|
||||
const float change = (target_ - opening_) * (-std::expm1(-dt / std::max(throttle_time_constant, 1e-4f)));
|
||||
const float travel = std::max(throttle_rate, 0.0f) * dt;
|
||||
const bool closing = target_ < opening_;
|
||||
const float response = closing && throttle_closing_time_constant > 0 ?
|
||||
throttle_closing_time_constant : throttle_time_constant;
|
||||
const float rate = closing && throttle_closing_rate > 0 ? throttle_closing_rate : throttle_rate;
|
||||
const float change = (target_ - opening_) * (-std::expm1(-dt / std::max(response, 1e-4f)));
|
||||
const float travel = std::max(rate, 0.0f) * dt;
|
||||
opening_ = std::clamp(opening_ + std::clamp(change, -travel, travel), 0.0f, 1.0f);
|
||||
const float enrichment = std::clamp((opening_ - 0.7f) / 0.3f, 0.0f, 1.0f);
|
||||
afr_ = stoichiometric_afr + (power_afr - stoichiometric_afr) * enrichment;
|
||||
|
||||
@@ -50,7 +50,7 @@ float EngineCylinder::clearance_volume() const {
|
||||
}
|
||||
|
||||
float EngineCylinder::total_mass() const {
|
||||
return air_mass + exhaust_mass;
|
||||
return air_mass + exhaust_mass + fuel_mass;
|
||||
}
|
||||
|
||||
float EngineCylinder::temperature() const {
|
||||
@@ -79,11 +79,13 @@ void EngineCylinder::initialize(const Crankshaft& /*crank*/) {
|
||||
volume = clearance_volume();
|
||||
air_mass = 1.225f * volume;
|
||||
exhaust_mass = 0.0f;
|
||||
fuel_mass = 0.0f;
|
||||
energy = 101325.0f * volume / (gamma - 1.0f);
|
||||
pressure = 101325.0f;
|
||||
intake.owner_energy = exhaust.owner_energy = &energy;
|
||||
intake.owner_air_mass = exhaust.owner_air_mass = &air_mass;
|
||||
intake.owner_exhaust_mass = exhaust.owner_exhaust_mass = &exhaust_mass;
|
||||
intake.owner_fuel_mass = exhaust.owner_fuel_mass = &fuel_mass;
|
||||
sync_ports();
|
||||
}
|
||||
|
||||
@@ -140,8 +142,10 @@ void FourStrokeCylinder::pre_step(Crankshaft& crank, float dt, float /*throttle*
|
||||
const float previous_degrees = wrap_degrees((crank.previous_angle + phase) * 180.0f / pi);
|
||||
const float current_degrees = wrap_degrees(theta * 180.0f / pi);
|
||||
if (crossed_window_close(previous_degrees, current_degrees, intake_open, intake_close)) {
|
||||
fuel_mass = ecu ? ecu->fuel_for_air(air_mass) : air_mass / std::max(afr, 1e-6f);
|
||||
fuel_injected = fuel_mass > 0.0f;
|
||||
if (!transported_fuel)
|
||||
fuel_mass = ecu ? ecu->fuel_for_air(air_mass) : air_mass / std::max(afr, 1e-6f);
|
||||
const float charge_afr = air_mass / std::max(fuel_mass, 1e-12f);
|
||||
fuel_injected = fuel_mass > 0.0f && (!transported_fuel || (charge_afr >= 7 && charge_afr <= 24));
|
||||
}
|
||||
|
||||
const float spark_angle = wrap_degrees(720.0f - spark_advance);
|
||||
@@ -149,6 +153,7 @@ void FourStrokeCylinder::pre_step(Crankshaft& crank, float dt, float /*throttle*
|
||||
if (crossed_spark && fuel_injected && !combustion_active && !(ecu ? ecu->fuel_cut() : rev_limit_active) && random_unit() >= misfire_probability) {
|
||||
combustion_active = true;
|
||||
burn_fraction = 0.0f;
|
||||
active_fuel_mass_ = transported_fuel ? std::min(fuel_mass, air_mass / 14.7f) : fuel_mass;
|
||||
energy_factor_ = 1.0f + energy_variation * (2.0f * random_unit() - 1.0f);
|
||||
}
|
||||
|
||||
@@ -161,14 +166,23 @@ void FourStrokeCylinder::pre_step(Crankshaft& crank, float dt, float /*throttle*
|
||||
if (fraction >= 1.0f || angle_since_spark > burn_duration + burn_start + spark_advance) {
|
||||
fraction = 1.0f;
|
||||
combustion_active = false;
|
||||
exhaust_mass += air_mass;
|
||||
air_mass = 0.0f;
|
||||
if (!transported_fuel) {
|
||||
exhaust_mass += air_mass;
|
||||
air_mass = 0.0f;
|
||||
}
|
||||
}
|
||||
fuel_injected = false;
|
||||
const float increment = fraction - burn_fraction;
|
||||
if (increment > 0.0f) {
|
||||
energy += fuel_mass * fuel_lhv * energy_factor_ * increment;
|
||||
exhaust_mass += fuel_mass * increment;
|
||||
const float burned_fuel = std::min(active_fuel_mass_ * increment, fuel_mass);
|
||||
energy += burned_fuel * fuel_lhv * energy_factor_;
|
||||
fuel_mass -= burned_fuel;
|
||||
exhaust_mass += burned_fuel;
|
||||
if (transported_fuel) {
|
||||
const float burned_air = std::min(air_mass, burned_fuel * 14.7f);
|
||||
air_mass -= burned_air;
|
||||
exhaust_mass += burned_air;
|
||||
}
|
||||
burn_fraction = fraction;
|
||||
}
|
||||
}
|
||||
@@ -184,16 +198,20 @@ void FourStrokeCylinder::pre_step(Crankshaft& crank, float dt, float /*throttle*
|
||||
}
|
||||
|
||||
void EngineCylinder::update_state(float dt) {
|
||||
float delta_air = 0.0f, delta_exhaust = 0.0f, delta_energy = 0.0f;
|
||||
float delta_air = 0.0f, delta_exhaust = 0.0f, delta_fuel = 0.0f, delta_energy = 0.0f;
|
||||
for (Port* port : {&intake, &exhaust}) {
|
||||
const float mass_flow = port->mass_flow;
|
||||
const float fraction = mass_flow >= 0.0f ? port->air_fraction : air_fraction();
|
||||
const float fuel_fraction = mass_flow >= 0.0f ? port->fuel_fraction :
|
||||
fuel_mass / std::max(total_mass(), 1e-12f);
|
||||
delta_air += mass_flow * fraction * dt;
|
||||
delta_exhaust += mass_flow * (1.0f - fraction) * dt;
|
||||
delta_fuel += mass_flow * fuel_fraction * dt;
|
||||
delta_exhaust += mass_flow * std::max(1.0f - fraction - fuel_fraction, 0.0f) * dt;
|
||||
delta_energy += mass_flow * port->enthalpy * dt;
|
||||
}
|
||||
air_mass += delta_air;
|
||||
exhaust_mass += delta_exhaust;
|
||||
fuel_mass += delta_fuel;
|
||||
energy += delta_energy;
|
||||
|
||||
const float current_volume = std::max(volume, 1e-12f);
|
||||
@@ -211,12 +229,14 @@ void EngineCylinder::update_state(float dt) {
|
||||
if (total_mass() < 1e-9f) {
|
||||
air_mass = 1e-9f;
|
||||
exhaust_mass = 0.0f;
|
||||
fuel_mass = 0.0f;
|
||||
energy = 101325.0f * current_volume / (gamma - 1.0f);
|
||||
} else if (energy < 0.0f) {
|
||||
energy = 101325.0f * current_volume / (gamma - 1.0f);
|
||||
}
|
||||
air_mass = std::max(air_mass, 0.0f);
|
||||
exhaust_mass = std::max(exhaust_mass, 0.0f);
|
||||
fuel_mass = std::max(fuel_mass, 0.0f);
|
||||
pressure = (gamma - 1.0f) * energy / current_volume;
|
||||
sync_ports();
|
||||
}
|
||||
@@ -257,12 +277,14 @@ void EngineCylinder::sync_ports() {
|
||||
pressure = (gamma - 1.0f) * energy / std::max(volume, 1e-12f);
|
||||
const float temp = pressure / std::max(density * gas_constant, 1e-12f);
|
||||
const float fraction = air_fraction();
|
||||
const float fuel_fraction = fuel_mass / std::max(total_mass(), 1e-12f);
|
||||
const float enthalpy = gamma / (gamma - 1.0f) * pressure / std::max(density, 1e-12f);
|
||||
for (Port* port : {&intake, &exhaust}) {
|
||||
port->pressure = pressure;
|
||||
port->density = density;
|
||||
port->temperature = temp;
|
||||
port->air_fraction = fraction;
|
||||
port->fuel_fraction = fuel_fraction;
|
||||
port->enthalpy = enthalpy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,10 @@ int main(int argc, char** argv) {
|
||||
constexpr int sample_rate = 44100;
|
||||
const auto total_samples = static_cast<std::uint64_t>(duration_seconds * sample_rate);
|
||||
const auto report_interval = static_cast<std::uint64_t>(sample_rate / 10);
|
||||
if (scenario_name == "twin" || scenario_name == "b205") {
|
||||
FourStrokeScenario scenario(sample_rate, scenario_name == "b205" ? FourStrokePreset::SaabB205 : FourStrokePreset::Twin, turbo_enabled);
|
||||
if (scenario_name == "twin" || scenario_name == "b205" || scenario_name == "v8") {
|
||||
FourStrokeScenario scenario(sample_rate, scenario_name == "v8" ? FourStrokePreset::AmericanV8 : scenario_name == "b205" ? FourStrokePreset::SaabB205 : FourStrokePreset::Twin, turbo_enabled);
|
||||
scenario.throttle = throttle;
|
||||
std::cout << (scenario_name == "b205" ? "FluidSimCpp | Saab B205 / 9-5 wagon | throttle " : "FluidSimCpp | 600 cc parallel twin | throttle ") << std::fixed << std::setprecision(0)
|
||||
std::cout << (scenario_name == "v8" ? "FluidSimCpp | American 5.0 V8 | throttle " : scenario_name == "b205" ? "FluidSimCpp | Saab B205 / 9-5 wagon | throttle " : "FluidSimCpp | 600 cc parallel twin | throttle ") << std::fixed << std::setprecision(0)
|
||||
<< throttle * 100.0f << "%\n";
|
||||
for (std::uint64_t i = 0; i < total_samples; ++i) {
|
||||
scenario.process();
|
||||
@@ -116,7 +116,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Unknown scenario '" << scenario_name
|
||||
<< "'. Choose twin, two-stroke, b205, helmholtz, or pulse.\n";
|
||||
<< "'. Choose twin, two-stroke, b205, v8, helmholtz, or pulse.\n";
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -47,6 +47,7 @@ OutdoorAudio::OutdoorAudio(int rate, OutdoorAudioConfig config) : rate_(rate), c
|
||||
config_.listener_right.z / norm};
|
||||
configure_emitter(emitters_[0], config_.intake_position);
|
||||
configure_emitter(emitters_[1], config_.exhaust_position);
|
||||
if (config_.dual_exhaust) configure_emitter(emitters_[2], config_.second_exhaust_position);
|
||||
constexpr std::array<int, 4> lengths{1499, 2111, 2633, 3251};
|
||||
for (std::size_t i = 0; i < tail_.size(); ++i) {
|
||||
auto &line = tail_[i];
|
||||
@@ -148,8 +149,8 @@ AudioFrame OutdoorAudio::diffuse(float input) {
|
||||
}
|
||||
return {(values[0] + values[2]) * .35f, (values[1] + values[3]) * .35f};
|
||||
}
|
||||
AudioFrame OutdoorAudio::process(float intake, float exhaust) {
|
||||
if (!std::isfinite(intake) || !std::isfinite(exhaust))
|
||||
AudioFrame OutdoorAudio::process(float intake, float exhaust, float second_exhaust) {
|
||||
if (!std::isfinite(intake) || !std::isfinite(exhaust) || !std::isfinite(second_exhaust))
|
||||
throw std::runtime_error("nonfinite acoustic source flow");
|
||||
const bool wet = scene_ != OutdoorScene::Dry;
|
||||
const std::array<float, 4> target{1, wet ? 1.f : 0.f, scene_ == OutdoorScene::Wall ? 1.f : 0.f,
|
||||
@@ -165,7 +166,8 @@ AudioFrame OutdoorAudio::process(float intake, float exhaust) {
|
||||
last_raw_ = {};
|
||||
const auto a = render(emitters_[0], intake, last_dry_, last_raw_);
|
||||
const auto b = render(emitters_[1], exhaust, last_dry_, last_raw_);
|
||||
const auto c = config_.dual_exhaust ? render(emitters_[2], second_exhaust, last_dry_, last_raw_) : AudioFrame{};
|
||||
const auto tail = diffuse((last_dry_.left + last_dry_.right) * .5f);
|
||||
return {a.left + b.left + tail.left * tail_mix_, a.right + b.right + tail.right * tail_mix_};
|
||||
return {a.left + b.left + c.left + tail.left * tail_mix_, a.right + b.right + c.right + tail.right * tail_mix_};
|
||||
}
|
||||
} // namespace fluidsim::port
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -106,6 +106,7 @@ void Turbocharger::set_transfer(Volume0D& in, Volume0D& out, Port& ip, Port& op,
|
||||
ip.mass_flow = float(-flow);
|
||||
op.mass_flow = float(flow);
|
||||
ip.air_fraction = op.air_fraction = donor.air_fraction();
|
||||
ip.fuel_fraction = op.fuel_fraction = donor.fuel_fraction();
|
||||
ip.enthalpy = op.enthalpy = float(h);
|
||||
// Positive work means energy delivered to the shaft. Only the receiving
|
||||
// fluid enthalpy changes; the donor loses precisely its stagnation enthalpy.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "FluidSim/Carburetor.hpp"
|
||||
#include "FluidSim/Ecu.hpp"
|
||||
#include "FluidSim/TwoStrokeCylinder.hpp"
|
||||
|
||||
@@ -70,16 +71,22 @@ void TwoStrokeCylinder::pre_step(Crankshaft &crank, float dt, float /*throttle*/
|
||||
void TwoStrokeCylinder::handle_cycle_events(float previous_degrees, float current_degrees) {
|
||||
const float trapped_angle = std::max(transfer_close(), exhaust_close());
|
||||
if (previous_degrees < trapped_angle && current_degrees >= trapped_angle) {
|
||||
fuel_mass = ecu ? ecu->fuel_for_air(air_mass) : air_mass / std::max(afr, 1e-6f);
|
||||
fuel_injected = fuel_mass > 0;
|
||||
if (!carburetor_ && fuel_mass <= 0.0f)
|
||||
fuel_mass = ecu ? ecu->fuel_for_air(air_mass) : air_mass / std::max(afr, 1e-6f);
|
||||
const float charge_afr = air_mass / std::max(fuel_mass, 1e-12f);
|
||||
fuel_injected = fuel_mass > 0.0f && charge_afr >= 7.0f && charge_afr <= 24.0f;
|
||||
}
|
||||
|
||||
const float spark_angle = wrap_degrees(360.0f - spark_advance, 360.0f);
|
||||
const bool crossed_spark = crossed_angle(previous_degrees,current_degrees,spark_angle);
|
||||
if (crossed_spark && fuel_injected && !combustion_active && !(ecu ? ecu->fuel_cut() : rev_limit_active) &&
|
||||
if (crossed_spark && fuel_injected && !combustion_active &&
|
||||
!(ecu ? ecu->fuel_cut() : rev_limit_active) &&
|
||||
random_unit() >= misfire_probability) {
|
||||
combustion_active = true;
|
||||
burn_fraction = 0.0f;
|
||||
// Fuel is a transported species. Only the portion with enough oxygen
|
||||
// can release heat; rich excess remains unburned and can reach the pipe.
|
||||
active_fuel_mass_ = std::min(fuel_mass, air_mass / 14.7f);
|
||||
++combustion_count_;
|
||||
active_spark_angle_ = spark_angle;
|
||||
// Freeze the duration and timing for this charge. Changing the ECU map
|
||||
@@ -99,14 +106,16 @@ void TwoStrokeCylinder::handle_cycle_events(float previous_degrees, float curren
|
||||
if (fraction >= 1.0f || angle_since_spark > active_burn_duration_ + burn_start) {
|
||||
fraction = 1.0f;
|
||||
combustion_active = false;
|
||||
exhaust_mass += air_mass;
|
||||
air_mass = 0.0f;
|
||||
}
|
||||
fuel_injected = false;
|
||||
const float increment = fraction - burn_fraction;
|
||||
if (increment > 0.0f) {
|
||||
energy += fuel_mass * fuel_lhv * energy_factor_ * increment;
|
||||
exhaust_mass += fuel_mass * increment;
|
||||
const float burned_fuel = std::min(active_fuel_mass_ * increment, fuel_mass);
|
||||
energy += burned_fuel * fuel_lhv * energy_factor_;
|
||||
fuel_mass -= burned_fuel;
|
||||
const float burned_air = std::min(air_mass, burned_fuel * 14.7f);
|
||||
air_mass -= burned_air;
|
||||
exhaust_mass += burned_fuel + burned_air;
|
||||
burn_fraction = fraction;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace fluidsim::port {
|
||||
|
||||
TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
|
||||
: configuration(config),
|
||||
carburetor(config.carburetor_diameter),
|
||||
cylinder(config.bore, config.stroke, config.rod, config.geometric_compression(),
|
||||
config.transfer_duration, config.exhaust_duration),
|
||||
crankcase(crankshaft, config.stroke * .5f, config.rod, config.bore, 150e-6f), pipes(create_pipes(config)),
|
||||
@@ -43,9 +44,22 @@ TwoStrokeScenario::TwoStrokeScenario(int sample_rate, TwoStrokeConfig config)
|
||||
ecu.idle_spark_gain=.04f;
|
||||
ecu.overrun_cut_rpm = 5000;
|
||||
ecu.overrun_resume_rpm = 3500;
|
||||
ecu.overrun_fuel_cut_enabled = false;
|
||||
cylinder.ecu = &ecu;
|
||||
cylinder.initialize(crankshaft);
|
||||
cylinder.set_crankcase(crankcase);
|
||||
cylinder.set_carburetor(carburetor);
|
||||
// This scenario starts as an already-running warm engine at 3000 RPM.
|
||||
// Seed its upstream charge accordingly; a dry system needs a starter.
|
||||
crankcase.initialize_mixture(14.7f);
|
||||
cylinder.fuel_mass = cylinder.air_mass / 15.7f;
|
||||
cylinder.air_mass -= cylinder.fuel_mass;
|
||||
intake_plenum.add_fuel_mass(intake_plenum.mass() / 14.7f);
|
||||
for (int index : {1, 2})
|
||||
for (auto& cell : pipes[index].cells) {
|
||||
cell.fuel = 1.0f / 15.7f;
|
||||
cell.air = 1.0f - cell.fuel;
|
||||
}
|
||||
|
||||
for (auto &pipe : pipes) {
|
||||
pipe.damping_multiplier = 0.8f;
|
||||
@@ -77,8 +91,12 @@ AudioFrame TwoStrokeScenario::process() {
|
||||
reed_out_area_ = std::min(reed.step(dt_, upstream - crankcase.pressure()), reed_in_area_);
|
||||
reed_open_ = reed.lift() > 1e-6f;
|
||||
ecu.step(dt_, throttle, crankshaft.omega * 60 / (2 * pi));
|
||||
const float angle = ecu.throttle_opening() * pi * .5f;
|
||||
max_throttle_area_ = area_for_diameter(configuration.carburetor_diameter) * (1 - std::cos(angle));
|
||||
const auto carb_inlet = pipes[0].state(static_cast<int>(pipes[0].cells.size()) - 1);
|
||||
carburetor.step(dt_, throttle, ecu.throttle_opening(), crankshaft.omega * 60 / (2 * pi),
|
||||
std::max(plenum_inlet_->mass_flow, 0.0f), carb_inlet.rho,
|
||||
carb_inlet.pressure, intake_plenum.pressure());
|
||||
intake_plenum.add_fuel_mass(carburetor.fuel_flow() * dt_);
|
||||
max_throttle_area_ = slide_area(configuration.carburetor_diameter, ecu.throttle_opening());
|
||||
step_motorcycle(crankshaft, dt_, 10.0f);
|
||||
crankcase.pre_step(dt_);
|
||||
cylinder.pre_step(crankshaft, dt_, ecu.throttle_opening());
|
||||
@@ -116,6 +134,16 @@ TwoStrokeSnapshot TwoStrokeScenario::snapshot() const {
|
||||
|
||||
float TwoStrokeScenario::area_for_diameter(float diameter) { return pi * diameter * diameter * 0.25f; }
|
||||
|
||||
float TwoStrokeScenario::slide_area(float diameter, float opening) {
|
||||
const float radius = diameter * 0.5f;
|
||||
const float height = std::clamp(opening, 0.0f, 1.0f) * diameter;
|
||||
const float offset = radius - height;
|
||||
const float root = std::sqrt(std::max(radius * radius - offset * offset, 0.0f));
|
||||
// Cancellation near the seat can produce a tiny negative area in float.
|
||||
return std::clamp(radius * radius * std::acos(std::clamp(offset / radius, -1.0f, 1.0f)) - offset * root,
|
||||
0.0f, area_for_diameter(diameter));
|
||||
}
|
||||
|
||||
std::vector<Pipe1D> TwoStrokeScenario::create_pipes(const TwoStrokeConfig& config) {
|
||||
std::vector<Pipe1D> result;
|
||||
result.reserve(4);
|
||||
|
||||
Reference in New Issue
Block a user