#include "FluidSim/Scenario.hpp" #include "FluidSim/ScenarioCatalog.hpp" #include "FluidSim/TwoStrokeScenario.hpp" #include #include #include #include #include #include #include #include #include #include using namespace fluidsim::port; namespace { class Wav { std::ofstream out_; void integer(std::uint32_t n, int bytes) { for (int i = 0; i < bytes; i++) out_.put(char(n >> (8 * i))); } public: Wav(const std::string &path, int frames) : out_(path, std::ios::binary) { if (!out_) throw std::runtime_error("cannot open WAV: " + path); out_.write("RIFF", 4); integer(36 + frames * 8, 4); out_.write("WAVEfmt ", 8); integer(16, 4); integer(3, 2); // IEEE float preserves quiet diagnostic sources. integer(2, 2); integer(44100, 4); integer(44100 * 8, 4); integer(8, 2); integer(32, 2); out_.write("data", 4); integer(frames * 8, 4); } void sample(AudioFrame f) { for (float x : {f.left, f.right}) { integer(std::bit_cast(x), 4); } } }; struct Metrics { double sum{}, peak{}, delta{}; std::uint64_t samples{}, clips{}; AudioFrame previous{}; void add(AudioFrame f) { for (float x : {f.left, f.right}) { if (!std::isfinite(x)) throw std::runtime_error("nonfinite capture"); sum += x * x; peak = std::max(peak, double(std::abs(x))); clips += std::abs(x) > 1; ++samples; } delta = std::max( {delta, double(std::abs(f.left - previous.left)), double(std::abs(f.right - previous.right))}); previous = f; } void print(const char *name) const { std::cout << name << " peak=" << peak << " rms=" << std::sqrt(sum / samples) << " max_sample_delta=" << delta << " clipping_samples=" << clips << '\n'; } }; template 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, const std::string& solo="all") { std::unique_ptr s; if constexpr (std::is_same_v) s = std::make_unique(44100, config); else if constexpr (std::is_same_v) { if(example) s=std::make_unique(44100,*example); else s=std::make_unique(); } else s = std::make_unique(); s->set_pipe_solver(solver); s->throttle = pedal; s->set_audio_scene(scene); bool valid_solo=solo=="all"; for(std::size_t i=0;iset_audio_source_enabled(i,solo=="all" || selected); } if(!valid_solo) throw std::runtime_error("choose --solo all, intake, exhaust, exhaust-2, or valve-contact"); for(int i=0; i0) s->crankshaft.omega=fixed_rpm*2*pi/60; s->process(); } Wav rendered(prefix + "-rendered.wav", frames), dry(prefix + "-dry.wav", frames), raw(prefix + "-unfiltered.wav", frames); Metrics m, d, r, tail_metrics; std::array,4> stems; std::array source_metrics; for(std::size_t i=0;i(prefix+"-"+OutdoorAudio::source_names[i]+".wav",frames); Wav tail(prefix+"-tail.wav",frames); int cuts = 0; for (int i = 0; i < frames; i++) { if(fixed_rpm>0) s->crankshaft.omega=fixed_rpm*2*pi/60; auto f = s->process(), df = s->dry_audio(), rf = s->unfiltered_audio(); for(std::size_t j=0;jsource_audio()[j].rendered; stems[j]->sample(stem); source_metrics[j].add(stem); } tail.sample(s->tail_audio()); tail_metrics.add(s->tail_audio()); m.add(f); d.add(df); r.add(rf); rendered.sample(f); dry.sample(df); raw.sample(rf); if (i % 4410 == 0) cuts += s->snapshot().fuel_cut; } for(std::size_t j=0;jpipes) { repairs+=pipe.positivity_repairs; fallbacks+=pipe.hllc_fallbacks; } if constexpr (std::is_same_v || std::is_base_of_v) { const auto diagnostics=s->snapshot(); std::cout<<"junction_model="<junction_config.model<<" junction_fallbacks="<= argc) throw std::runtime_error("missing option value"); std::string value = argv[++i]; if(key=="--junctions") { if(value!="volume" && value!="characteristic" && value!="legacy") throw std::runtime_error("choose volume, characteristic, or legacy junctions"); junction_model=value; } else if(key=="--junction-loss") { junction_loss=std::stof(value); if(!std::isfinite(junction_loss) || junction_loss<0 || junction_loss>10) throw std::runtime_error("junction loss must be 0..10"); } else if(key=="--solo") solo=value; else if(key=="--example") example_id=value; else if (key == "--scenario") scenario = value; else if (key == "--out") prefix = value; else if (key == "--seconds") seconds = std::stof(value); else if (key == "--throttle") pedal = std::stof(value); else if (key == "--warmup") warmup=std::stof(value); else if (key == "--rpm") fixed_rpm=std::stof(value); else if (key == "--solver") solver=parse_pipe_solver(value); else if (key == "--exhaust") { if (value == "racing") config.exhaust = TwoStrokeExhaust::Racing; else if (value == "straight") config.exhaust = TwoStrokeExhaust::Straight; else throw std::runtime_error("choose racing or straight exhaust"); } else if (key == "--scene") scene = value; else throw std::runtime_error("unknown capture option"); } if (!std::isfinite(seconds) || seconds <= 0 || seconds > 600 || !std::isfinite(pedal) || pedal < 0 || pedal > 1) throw std::runtime_error("invalid capture duration or throttle"); if(!std::isfinite(fixed_rpm) || fixed_rpm<0 || fixed_rpm>30000) throw std::runtime_error("invalid capture RPM"); if(!std::isfinite(warmup) || warmup<0 || warmup>600) throw std::runtime_error("invalid warmup"); OutdoorScene selected; if (scene == "dry") selected = OutdoorScene::Dry; else if (scene == "field") selected = OutdoorScene::Field; else if (scene == "wall") selected = OutdoorScene::Wall; else if (scene == "woodland") selected = OutdoorScene::Woodland; else if (scene == "tunnel") selected = OutdoorScene::Tunnel; else throw std::runtime_error("invalid scene"); 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)); auto examples=scenario_examples(); if(!junction_model.empty() || junction_loss>=0) { if(example_id.empty()) example_id=scenario=="v8"?"v8-h":scenario=="b205"?"saab-straight":scenario=="twin"?"twin":"yz-racing"; for(auto& example:examples) { if(!junction_model.empty()) example.junctions.model=junction_model; if(junction_loss>=0) example.junctions.turning_loss=junction_loss; } } 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 && (!junction_model.empty() || junction_loss>=0)) throw std::runtime_error("two-stroke exhaust has no configurable pipe junctions"); if(example->kind==ScenarioKind::TwoStroke) capture(prefix,frames,pedal,selected,example->two,solver,fixed_rpm,warmup,nullptr,solo); else capture(prefix,frames,pedal,selected,config,solver,fixed_rpm,warmup,&*example,solo); } else if (scenario == "twin") capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup,nullptr,solo); else if (scenario == "b205") capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup,nullptr,solo); else if (scenario == "two-stroke") capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup,nullptr,solo); else if (scenario == "v8") capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup,nullptr,solo); else throw std::runtime_error("choose twin, two-stroke, b205 or v8"); } catch (const std::exception &e) { std::cerr << e.what() << '\n'; return 1; } }