#include "FluidSim/Scenario.hpp" #include "FluidSim/TwoStrokeScenario.hpp" #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 * 4, 4); out_.write("WAVEfmt ", 8); integer(16, 4); integer(1, 2); integer(2, 2); integer(44100, 4); integer(44100 * 4, 4); integer(4, 2); integer(16, 2); out_.write("data", 4); integer(frames * 4, 4); } void sample(AudioFrame f) { for (float x : {f.left, f.right}) { const auto v = static_cast(std::lround(std::clamp(x, -1.f, 1.f) * 32767)); integer(static_cast(v), 2); } } }; 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) { std::unique_ptr s; if constexpr (std::is_same_v) s = std::make_unique(44100, config); else s = std::make_unique(); s->set_pipe_solver(solver); s->throttle = pedal; s->set_audio_scene(scene); 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; 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(); 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; } m.print("rendered"); d.print("dry filtered"); r.print("unfiltered spatial"); std::cout << "fuel-cut checkpoints=" << cuts << " final_rpm=" << s->snapshot().rpm << " scene=" << scene_name(scene) << '\n'; } } // namespace int main(int argc, char **argv) { try { std::string scenario = "twin", prefix = "build/audio/twin", scene = "wall"; float seconds = 6, pedal = 1, fixed_rpm = 0, warmup = 0; TwoStrokeConfig config; PipeSolver solver=PipeSolver::Rusanov; for (int i = 1; i < argc; i++) { std::string key = argv[i]; if (i + 1 >= argc) throw std::runtime_error("missing option value"); std::string value = argv[++i]; 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)); if (scenario == "twin") capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup); else if (scenario == "b205") capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup); else if (scenario == "two-stroke") capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup); else if (scenario == "v8") capture(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup); else throw std::runtime_error("choose twin, two-stroke, b205 or v8"); } catch (const std::exception &e) { std::cerr << e.what() << '\n'; return 1; } }