Add turbocharged twin scenario and cross-platform test harness
This commit is contained in:
114
tests/outdoor_audio_tests.cpp
Normal file
114
tests/outdoor_audio_tests.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#include "FluidSim/OutdoorAudio.hpp"
|
||||
#include "FluidSim/ReedValve.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
void check(bool c, const char *m) {
|
||||
if (!c)
|
||||
throw std::runtime_error(m);
|
||||
}
|
||||
OutdoorAudioConfig config() {
|
||||
OutdoorAudioConfig c;
|
||||
c.exhaust_position = {0, 1, 0};
|
||||
c.listener = {0, 1, 3};
|
||||
c.ear_spacing = 0;
|
||||
c.wall_x = -2;
|
||||
return c;
|
||||
}
|
||||
} // namespace
|
||||
int main() {
|
||||
// Geometric reflections must not arrive before the direct path or before
|
||||
// the ground image's path length, even with the diffuse network enabled.
|
||||
OutdoorAudio dry(44100, config()), wall(44100, config());
|
||||
dry.set_scene(OutdoorScene::Dry);
|
||||
dry.set_monitor_filter(false);
|
||||
wall.set_monitor_filter(false);
|
||||
for (int i = 0; i < 4410; i++) {
|
||||
dry.process(0, 0);
|
||||
wall.process(0, 0);
|
||||
}
|
||||
int first = -1, first_reflection = -1;
|
||||
double tail_energy = 0, late_energy = 0;
|
||||
float peak = 0;
|
||||
for (int i = 0; i < 88200; i++) {
|
||||
const auto d = dry.process(0, .001f), w = wall.process(0, .001f);
|
||||
if (first < 0 && std::abs(d.left) > 1e-9)
|
||||
first = i;
|
||||
if (first_reflection < 0 && std::abs(w.left - d.left) > 1e-9)
|
||||
first_reflection = i;
|
||||
peak = std::max(peak, std::abs(w.left));
|
||||
if (i > 2000 && i < 10000)
|
||||
tail_energy += w.left * w.left + w.right * w.right;
|
||||
if (i > 60000)
|
||||
late_energy += w.left * w.left + w.right * w.right;
|
||||
check(std::isfinite(w.left) && std::isfinite(w.right), "unstable outdoor tail");
|
||||
}
|
||||
check(first == 385, "direct fractional propagation delay is wrong");
|
||||
check(first_reflection >= 463 && first_reflection <= 465, "ground image reflection time is wrong");
|
||||
check(tail_energy > 0 && late_energy < tail_energy * 1e-6, "outdoor tail does not decay");
|
||||
check(peak < 1, "impulse reflection exceeds device range");
|
||||
|
||||
OutdoorAudio filtered(44100, config()), unfiltered(44100, config()), doubled(44100, config());
|
||||
for (auto *p : {&filtered, &unfiltered, &doubled})
|
||||
p->set_scene(OutdoorScene::Dry);
|
||||
unfiltered.set_monitor_filter(false);
|
||||
float prevf = 0, prevu = 0, maxf = 0, maxu = 0;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
const float q = i == 0 ? .001f : 0;
|
||||
auto f = filtered.process(0, q), u = unfiltered.process(0, q), twice = doubled.process(0, 2 * q);
|
||||
maxf = std::max(maxf, std::abs(f.left - prevf));
|
||||
maxu = std::max(maxu, std::abs(u.left - prevu));
|
||||
prevf = f.left;
|
||||
prevu = u.left;
|
||||
check(std::abs(twice.left - 2 * f.left) < 1e-7, "monitoring path is nonlinear");
|
||||
}
|
||||
check(maxf < .6f * maxu, "monitor bandwidth fails to reduce adjacent-sample impulse steps");
|
||||
|
||||
auto near_config = config(), far_config = config();
|
||||
near_config.listener.z = 1;
|
||||
far_config.listener.z = 2;
|
||||
OutdoorAudio near(44100, near_config), far(44100, far_config);
|
||||
near.set_scene(OutdoorScene::Dry);
|
||||
far.set_scene(OutdoorScene::Dry);
|
||||
near.set_monitor_filter(false);
|
||||
far.set_monitor_filter(false);
|
||||
double sum_near = 0, sum_far = 0;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
sum_near += near.process(0, .001f).left;
|
||||
sum_far += far.process(0, .001f).left;
|
||||
}
|
||||
check(std::abs(sum_near - 2 * sum_far) < 1e-7, "direct spreading does not follow inverse distance");
|
||||
|
||||
auto stereo_config = config();
|
||||
stereo_config.ear_spacing = .18f;
|
||||
stereo_config.exhaust_position.x = 1;
|
||||
OutdoorAudio stereo(44100, stereo_config);
|
||||
stereo.set_scene(OutdoorScene::Dry);
|
||||
stereo.set_monitor_filter(false);
|
||||
int left = -1, right = -1;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
auto a = stereo.process(0, .001f);
|
||||
if (left < 0 && std::abs(a.left) > 1e-9)
|
||||
left = i;
|
||||
if (right < 0 && std::abs(a.right) > 1e-9)
|
||||
right = i;
|
||||
}
|
||||
check(right < left, "source on right does not reach right ear first");
|
||||
|
||||
ReedValve reed;
|
||||
for (int i = 0; i < 10000; i++)
|
||||
reed.step(1.f / 44100, 100);
|
||||
const double inertia = reed.width * std::pow(reed.thickness, 3) / 12;
|
||||
const double expected = 100 * reed.width * std::pow(reed.length, 4) / (8 * reed.youngs_modulus * inertia);
|
||||
check(std::abs(reed.lift() - expected) < expected * .001,
|
||||
"reed static compliance differs from cantilever equation");
|
||||
for (int i = 0; i < 1000; i++)
|
||||
reed.step(1.f / 44100, -1000);
|
||||
check(reed.lift() == 0, "reed does not seat under reverse pressure");
|
||||
std::cout << "Outdoor audio tests passed: direct=" << first << " samples, ground=" << first_reflection
|
||||
<< " samples, filtered step ratio=" << maxf / maxu << ", stereo lead=" << left - right
|
||||
<< " samples\n";
|
||||
}
|
||||
Reference in New Issue
Block a user