#include "FluidSim/Constants.hpp" #include "FluidSim/OutdoorAudio.hpp" #include #include #include #include namespace fluidsim::port { namespace { Vec3 sub(Vec3 a, Vec3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; } float length(Vec3 a) { return std::sqrt(a.x * a.x + a.y * a.y + a.z * a.z); } float distance(Vec3 a, Vec3 b) { return length(sub(a, b)); } AudioFrame add(AudioFrame a, AudioFrame b) { return {a.left + b.left, a.right + b.right}; } } // namespace const char *scene_name(OutdoorScene scene) { switch (scene) { case OutdoorScene::Dry: return "Dry"; case OutdoorScene::Field: return "Open field"; case OutdoorScene::Wall: return "Nearby wall"; case OutdoorScene::Woodland: return "Woodland"; case OutdoorScene::Tunnel: return "Tunnel"; } return "Unknown"; } float OutdoorAudio::LowPass::process(float x) { const float y = b0 * x + z1; z1 = b1 * x - a1 * y + z2; z2 = b2 * x - a2 * y; if (std::abs(z1) < 1e-25f) z1 = 0; if (std::abs(z2) < 1e-25f) z2 = 0; return y; } OutdoorAudio::OutdoorAudio(int rate, OutdoorAudioConfig config) : rate_(rate), config_(config), late_reverb_(rate) { if (rate < 8000 || !std::isfinite(config.sound_speed) || config.sound_speed <= 0 || !std::isfinite(config.monitor_cutoff_hz) || config.monitor_cutoff_hz <= 0 || !std::isfinite(config.pressure_gain) || config.pressure_gain < 0) throw std::invalid_argument("invalid outdoor audio configuration"); const float norm = length(config_.listener_right); if (!std::isfinite(norm) || norm < 1e-6f) throw std::invalid_argument("invalid listener right vector"); config_.listener_right = {config_.listener_right.x / norm, config_.listener_right.y / norm, config_.listener_right.z / norm}; configure_emitter(emitters_[0], config_.intake_position); configure_emitter(emitters_[1], config_.exhaust_position); if (config_.dual_exhaust) configure_emitter(emitters_[2], config_.second_exhaust_position); late_reverb_.dry_mix = 0; late_reverb_.early_mix = 0; late_reverb_.tail_mix = 1; late_reverb_.set_decay_times(.75f, .45f, .22f); scene_alpha_ = 1 - std::exp(-1.0f / (rate_ * .02f)); } void OutdoorAudio::configure_emitter(Emitter &emitter, Vec3 source) { emitter.filtered.resize(std::bit_ceil(std::size_t(rate_))); emitter.raw.resize(emitter.filtered.size()); // One second, validated path range. const auto ear = [&](float sign) { return Vec3{config_.listener.x + sign * config_.listener_right.x * config_.ear_spacing * .5f, config_.listener.y + sign * config_.listener_right.y * config_.ear_spacing * .5f, config_.listener.z + sign * config_.listener_right.z * config_.ear_spacing * .5f}; }; const auto path = [&](Vec3 image, float coefficient, int group, float cutoff, float extra = 0, float source_distance = 1) { Path p; p.group = group; const float dl = std::max(distance(image, ear(-1)), .1f), dr = std::max(distance(image, ear(1)), .1f); const float sl = (dl + extra) * rate_ / config_.sound_speed, sr = (dr + extra) * rate_ / config_.sound_speed; if (!std::isfinite(sl) || !std::isfinite(sr) || sl >= rate_ - 2 || sr >= rate_ - 2) throw std::invalid_argument("outdoor propagation path exceeds one second"); p.delay_left = std::size_t(sl); p.delay_right = std::size_t(sr); p.fraction_left = sl - p.delay_left; p.fraction_right = sr - p.delay_right; // A scatterer adds path length before the listener leg. Use spreading // over the complete reflected path rather than multiplying two // inverse-distance losses, which made the woodland paths nearly // inaudible. const float path_left = source_distance > 1.0f ? dl + source_distance : dl; const float path_right = source_distance > 1.0f ? dr + source_distance : dr; p.gain_left = coefficient / path_left; p.gain_right = coefficient / path_right; p.alpha = cutoff > 0 ? 1 - std::exp(-2 * pi * cutoff / rate_) : 1; emitter.paths.push_back(p); }; path(source, 1, 0, 0); path({source.x, 2 * config_.ground_y - source.y, source.z}, config_.ground_reflection, 1, 1800); path({2 * config_.wall_x - source.x, source.y, source.z}, config_.wall_reflection, 2, 4500); // Single-bounce point scatterers: geometric paths, empirical scattering strengths. for (Vec3 tree : std::array{{{-4, 1, 3}, {3, 1, 5}, {5, 1, -2}, {-6, 2, -3}, {-2, 1, 7}, {6, 2, 6}}}) { const float first = std::max(distance(source, tree), .1f); path(tree, .35f, 3, 2400, first, first); } // Image sources for an approximate 6 m wide, 4 m high hard-surfaced // tunnel. Single and combined ceiling/side-wall bounces make the profile // distinct before the diffuse FDN tail builds up. constexpr float tunnel_width = 6.0f; constexpr float tunnel_height = 4.0f; const Vec3 ceiling{source.x, 2 * tunnel_height - source.y, source.z}; const Vec3 left_wall{source.x, source.y, -tunnel_width - source.z}; const Vec3 right_wall{source.x, source.y, tunnel_width - source.z}; path(ceiling, .82f, 4, 7200); path(left_wall, .82f, 4, 6200); path(right_wall, .82f, 4, 6200); path({ceiling.x, ceiling.y, left_wall.z}, .68f, 4, 5600); path({ceiling.x, ceiling.y, right_wall.z}, .68f, 4, 5600); path({source.x, source.y, -2 * tunnel_width + source.z}, .56f, 4, 4800); path({source.x, source.y, 2 * tunnel_width + source.z}, .56f, 4, 4800); const float cutoff = std::min(config_.monitor_cutoff_hz, rate_ * .45f); const float w = 2 * pi * cutoff / rate_, cs = std::cos(w), alpha = std::sin(w) / std::sqrt(2.0f), a0 = 1 + alpha; emitter.filter = {(1 - cs) * .5f / a0, (1 - cs) / a0, (1 - cs) * .5f / a0, -2 * cs / a0, (1 - alpha) / a0}; } AudioFrame OutdoorAudio::render(Emitter &e, float flow, AudioFrame &dry, AudioFrame &raw) { const float pressure = (flow - e.previous_flow) * rate_ * config_.pressure_gain / (4 * pi); e.previous_flow = flow; e.raw[e.position] = pressure; e.filtered[e.position] = monitor_filter_ ? e.filter.process(pressure) : pressure; const auto read = [&](const std::vector &data, std::size_t delay, float fraction) { const auto i = (e.position + data.size() - delay) & (data.size() - 1); const auto j = (i + data.size() - 1) & (data.size() - 1); return data[i] + (data[j] - data[i]) * fraction; }; AudioFrame result{}; for (auto &p : e.paths) { const float l = read(e.filtered, p.delay_left, p.fraction_left) * p.gain_left; const float r = read(e.filtered, p.delay_right, p.fraction_right) * p.gain_right; p.state_left += p.alpha * (l - p.state_left); p.state_right += p.alpha * (r - p.state_right); if (std::abs(p.state_left) < 1e-25f) p.state_left = 0; if (std::abs(p.state_right) < 1e-25f) p.state_right = 0; result.left += p.state_left * weights_[p.group]; result.right += p.state_right * weights_[p.group]; if (p.group == 0) { dry = add(dry, {l, r}); raw = add(raw, {read(e.raw, p.delay_left, p.fraction_left) * p.gain_left, read(e.raw, p.delay_right, p.fraction_right) * p.gain_right}); } } e.position = (e.position + 1) & (e.filtered.size() - 1); return result; } AudioFrame OutdoorAudio::process(float intake, float exhaust, float second_exhaust) { if (!std::isfinite(intake) || !std::isfinite(exhaust) || !std::isfinite(second_exhaust)) throw std::runtime_error("nonfinite acoustic source flow"); const OutdoorScene scene = requested_scene_.load(std::memory_order_relaxed); if (scene != active_scene_) { active_scene_ = scene; switch (scene) { case OutdoorScene::Dry: late_reverb_.set_decay_times(.20f, .12f, .06f); break; case OutdoorScene::Field: late_reverb_.set_decay_times(.28f, .17f, .08f); break; case OutdoorScene::Wall: late_reverb_.set_decay_times(.75f, .45f, .22f); break; case OutdoorScene::Woodland: late_reverb_.set_decay_times(.42f, .22f, .09f); break; case OutdoorScene::Tunnel: late_reverb_.set_decay_times(1.8f, 1.35f, .75f); break; } } const bool wet = scene != OutdoorScene::Dry; const std::array target{1, wet && scene != OutdoorScene::Tunnel ? 1.f : 0.f, scene == OutdoorScene::Wall ? 1.f : 0.f, scene == OutdoorScene::Woodland ? 1.f : 0.f, scene == OutdoorScene::Tunnel ? 1.f : 0.f}; for (std::size_t i = 0; i < weights_.size(); ++i) weights_[i] += (target[i] - weights_[i]) * scene_alpha_; // Keep the diffuse tail present between sharp events; the prior mix values // made it perceptible mainly after large transients. const float target_tail = scene == OutdoorScene::Wall ? .75f : scene == OutdoorScene::Woodland ? 1.0f : scene == OutdoorScene::Tunnel ? 1.15f : scene == OutdoorScene::Field ? .30f : 0.f; tail_mix_ += (target_tail - tail_mix_) * scene_alpha_; last_dry_ = {}; last_raw_ = {}; const auto a = render(emitters_[0], intake, last_dry_, last_raw_); const auto b = render(emitters_[1], exhaust, last_dry_, last_raw_); const auto c = config_.dual_exhaust ? render(emitters_[2], second_exhaust, last_dry_, last_raw_) : AudioFrame{}; const auto tail = late_reverb_.process_stereo((last_dry_.left + last_dry_.right) * .5f); return {a.left + b.left + c.left + tail.left * tail_mix_, a.right + b.right + c.right + tail.right * tail_mix_}; } } // namespace fluidsim::port