Files
FluidSimCpp/tools/audio_capture.cpp
max bf8621ad39 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.
2026-10-24 03:24:35 +02:00

185 lines
7.7 KiB
C++

#include "FluidSim/Scenario.hpp"
#include "FluidSim/ScenarioCatalog.hpp"
#include "FluidSim/TwoStrokeScenario.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
#include <type_traits>
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::int16_t>(std::lround(std::clamp(x, -1.f, 1.f) * 32767));
integer(static_cast<std::uint16_t>(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 <class Scenario>
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;
s->set_audio_scene(scene);
for(int i=0; i<int(std::lround(warmup*44100)); ++i) {
if(fixed_rpm>0) 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 example_id;
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=="--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));
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);
else if (scenario == "two-stroke")
capture<TwoStrokeScenario>(prefix, frames, pedal, selected, config, solver, fixed_rpm, warmup);
else if (scenario == "v8")
capture<AmericanV8Scenario>(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;
}
}