Load engines from TOML and improve scenario audio and carb tuning

Migrate 32 engine definitions into validated startup-loaded TOML files and separate configuration and snapshots from scenario assembly. Add camera-based stereo propagation from shared pipe geometry, preserve exhaust character, tune carburetor mixtures and idle, use ignition cuts for carb engines, and support returning to the scenario menu.

Validation: full Windows build and all 14 CTest checks passed.
This commit is contained in:
max
2026-10-24 03:24:35 +02:00
parent 5607dcf2b5
commit bf8621ad39
82 changed files with 24084 additions and 648 deletions

View File

@@ -1,4 +1,5 @@
#include "FluidSim/Scenario.hpp"
#include "FluidSim/ScenarioCatalog.hpp"
#include "FluidSim/TwoStrokeScenario.hpp"
#include <algorithm>
#include <cmath>
@@ -65,9 +66,13 @@ struct Metrics {
}
};
template <class Scenario>
void capture(const std::string &prefix, int frames, float pedal, OutdoorScene scene, TwoStrokeConfig config, PipeSolver solver, float fixed_rpm, float warmup) {
void capture(const std::string &prefix, int frames, float pedal, OutdoorScene scene, TwoStrokeConfig config, PipeSolver solver, float fixed_rpm, float warmup, const ScenarioExample* example=nullptr) {
std::unique_ptr<Scenario> s;
if constexpr (std::is_same_v<Scenario, TwoStrokeScenario>) s = std::make_unique<Scenario>(44100, config);
else if constexpr (std::is_same_v<Scenario, FourStrokeScenario>) {
if(example) s=std::make_unique<Scenario>(44100,*example);
else s=std::make_unique<Scenario>();
}
else s = std::make_unique<Scenario>();
s->set_pipe_solver(solver);
s->throttle = pedal;
@@ -101,6 +106,7 @@ void capture(const std::string &prefix, int frames, float pedal, OutdoorScene sc
} // namespace
int main(int argc, char **argv) {
try {
std::string example_id;
std::string scenario = "twin", prefix = "build/audio/twin", scene = "wall";
float seconds = 6, pedal = 1, fixed_rpm = 0, warmup = 0;
TwoStrokeConfig config;
@@ -110,7 +116,8 @@ int main(int argc, char **argv) {
if (i + 1 >= argc)
throw std::runtime_error("missing option value");
std::string value = argv[++i];
if (key == "--scenario")
if(key=="--example") example_id=value;
else if (key == "--scenario")
scenario = value;
else if (key == "--out")
prefix = value;
@@ -153,7 +160,14 @@ int main(int argc, char **argv) {
if (!std::filesystem::path(prefix).parent_path().empty())
std::filesystem::create_directories(std::filesystem::path(prefix).parent_path());
const int frames = int(std::lround(seconds * 44100));
if (scenario == "twin")
const auto examples=scenario_examples();
if(!example_id.empty()) {
const auto example=std::find_if(examples.begin(),examples.end(),[&](const auto& e){return e.id==example_id;});
if(example==examples.end()) throw std::runtime_error("unknown example: "+example_id);
if(example->kind==ScenarioKind::TwoStroke)
capture<TwoStrokeScenario>(prefix,frames,pedal,selected,example->two,solver,fixed_rpm,warmup);
else capture<FourStrokeScenario>(prefix,frames,pedal,selected,config,solver,fixed_rpm,warmup,&*example);
} else if (scenario == "twin")
capture<FourStrokeTwinScenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);
else if (scenario == "b205")
capture<SaabB205Scenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);

View File

@@ -0,0 +1,80 @@
#include "FluidSim/ScenarioCatalog.hpp"
#include "FluidSim/Scenario.hpp"
#include "FluidSim/TwoStrokeScenario.hpp"
#include <iostream>
#include <cmath>
#include <memory>
using namespace fluidsim::port;
int main(int argc,char** argv) {
const bool sweep=argc>1 && std::string(argv[1])=="--sweep";
const bool check=argc>1 && std::string(argv[1])=="--check";
bool failed=false;
int examples_checked=0;
std::cout<<"example,pedal,rpm,opening,carb_afr,charge_afr,free_idle_rpm,free_idle_afr,repairs\n";
for(auto e:scenario_examples()) {
if(e.kind!=ScenarioKind::TwoStroke && !e.engine.carbureted) continue;
if(sweep && argc>2 && e.id!=argv[2]) continue;
if(argc>1 && !check && !sweep && e.id!=argv[1]) continue;
++examples_checked;
const bool two=e.kind==ScenarioKind::TwoStroke;
if(!sweep && argc>2) {float k=std::stof(argv[2]);e.two.pilot_jet_scale*=k;e.engine.fuel.pilot_jet_diameter*=k;}
if(!sweep && argc>3) {float k=std::stof(argv[3]);e.two.main_jet_scale*=k;e.engine.fuel.main_jet_diameter*=k;}
if(!sweep && argc>4) {float k=std::stof(argv[4]);e.two.needle_jet_scale*=k;e.engine.fuel.needle_jet_diameter*=k;}
if(!sweep && argc>5) {float k=std::stof(argv[5]);e.two.idle_slide_opening=k;e.engine.ecu.idle_base_opening=k;}
if(!sweep && argc>6) {float k=std::stof(argv[6]);e.two.needle_low_scale*=k;e.engine.fuel.needle_low_scale*=k;}
if(!sweep && argc>7) {float k=std::stof(argv[7]);e.two.needle_mid_scale*=k;e.engine.fuel.needle_mid_scale*=k;}
for(int index=0;index<((sweep||check)?8:4);++index) {
const float pedal=(sweep||check)?std::array<float,8>{0,.05f,.1f,.15f,.25f,.5f,.75f,1}[index]:std::array<float,4>{0,.15f,.5f,1}[index];
const int point=pedal==0?0:pedal<.3f?1:pedal<.8f?2:3;
const float rpm=two?std::array<float,4>{3000,4000,6500,9000}[point]:
e.engine.cylinders==1?std::array<float,4>{1400,2500,4000,6000}[point]:std::array<float,4>{800,1800,3000,5000}[point];
std::unique_ptr<TwoStrokeScenario> t;
std::unique_ptr<FourStrokeScenario> f;
if(two) {t=std::make_unique<TwoStrokeScenario>(44100,e.two);t->set_controls(pedal,0,1);}
else {f=std::make_unique<FourStrokeScenario>(44100,e);f->set_controls(pedal,0,1);}
double afr=0,charge=0,opening=0;std::uint64_t repairs=0;
for(int i=0;i<55125;++i) {
if(two) {t->crankshaft.omega=rpm*2*pi/60;t->process();}
else {f->crankshaft.omega=rpm*2*pi/60;f->process();}
if(i>=44100 && i%10==0) {
afr+=(two?t->carburetor.indicated_afr():f->snapshot().target_afr)/1103.;
charge+=(two?t->crankcase.mass()*t->crankcase.air_fraction()/std::max(t->crankcase.fuel_mass(),1e-12f):f->intake_plenum.mass()*f->intake_plenum.air_fraction()/std::max(f->intake_plenum.fuel_mass(),1e-12f))/1103.;
opening+=(two?t->ecu.throttle_opening():f->ecu.throttle_opening())/1103.;
}
}
if(!std::isfinite(afr) || afr<11.5 || afr>(point==3?14.5:15.5)) {
std::cerr<<e.id<<": AFR outside calibration envelope at pedal "<<pedal<<": "<<afr<<'\n';failed=true;
}
float free_rpm=0,free_afr=0;
if(point==0) {
for(int i=0;i<132300;++i) {
if(two)t->process();else f->process();
if(i>=110250 && i%10==0) free_afr+=(two?t->carburetor.indicated_afr():f->snapshot().target_afr)/2205.;
}
free_rpm=(two?t->crankshaft.omega:f->crankshaft.omega)*60/(2*pi);
}
for(const auto& p:two?t->pipes:f->pipes)repairs+=p.positivity_repairs;
if(point==0) {
float idle_min=3000, idle_max=4000;
if(e.id=="fifty") {idle_min=3200;idle_max=4000;}
if(e.id=="fivehundred") {idle_min=2800;idle_max=3500;}
if(e.id=="single-450") {idle_min=1200;idle_max=1600;}
if(e.id=="big-carb") {idle_min=700;idle_max=900;}
if(!std::isfinite(free_rpm) || free_rpm<idle_min || free_rpm>idle_max ||
!std::isfinite(free_afr) || free_afr<11.5 || free_afr>15.5) {
std::cerr<<e.id<<": idle outside target RPM/AFR ("<<free_rpm<<", "<<free_afr<<")\n";failed=true;
}
}
if(repairs) {std::cerr<<e.id<<": pipe positivity repairs\n";failed=true;}
if(e.id=="yz-racing" && std::abs(opening-e.two.idle_slide_opening)>1e-6) {
if(point==0) {std::cerr<<"two-stroke idle stop did not reach its catalog setting\n";failed=true;}
}
if(e.id=="single-450" && (!f->cylinder1.transported_fuel || !e.engine.carbureted)) {
std::cerr<<"450 single must transport carburetor fuel\n";failed=true;
}
std::cout<<e.id<<','<<pedal<<','<<rpm<<','<<opening<<','<<afr<<','<<charge<<','<<free_rpm<<','<<free_afr<<','<<repairs<<std::endl;
}
}
if(!examples_checked) {std::cerr<<"No matching carbureted scenario\n";return 1;}
return (check||sweep) && failed ? 1 : 0;
}