DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
ModulationRouter.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
33#include "DspMath.h"
34
35#include <algorithm>
36#include <array>
37#include <atomic>
38#include <cmath>
39#include <functional>
40
41namespace dspark {
42
60template <FloatType T, int MaxRoutes = 16>
62{
63public:
77 int addRoute(std::function<T()> source, std::function<void(T)> target,
78 T depth = T(1), T base = T(0), T smoothMs = T(20))
79 {
80 // Empty callables would raise std::bad_function_call inside the
81 // noexcept audio-thread update() -- reject them at the door.
82 if (numRoutes_ >= MaxRoutes || !source || !target) return -1;
83 auto& r = routes_[static_cast<size_t>(numRoutes_)];
84 r.source = std::move(source);
85 r.target = std::move(target);
86 r.depth.store(depth, std::memory_order_relaxed);
87 r.base = base;
88 r.smoothMs = std::max(smoothMs, T(0));
89 r.state = T(0);
90 r.primed = false;
91 return numRoutes_++;
92 }
93
96 void setDepth(int route, T depth) noexcept
97 {
98 if (route >= 0 && route < numRoutes_)
99 routes_[static_cast<size_t>(route)].depth.store(depth, std::memory_order_relaxed);
100 }
101
103 void clear() noexcept
104 {
105 for (int i = 0; i < numRoutes_; ++i)
106 {
107 routes_[static_cast<size_t>(i)].source = nullptr;
108 routes_[static_cast<size_t>(i)].target = nullptr;
109 }
110 numRoutes_ = 0;
111 }
112
114 [[nodiscard]] int getNumRoutes() const noexcept { return numRoutes_; }
115
121 void update(int numSamples, double sampleRate) noexcept
122 {
123 const double dt = static_cast<double>(numSamples) / std::max(sampleRate, 1.0);
124 for (int i = 0; i < numRoutes_; ++i)
125 {
126 auto& r = routes_[static_cast<size_t>(i)];
127 const T depth = r.depth.load(std::memory_order_relaxed);
128 const T value = r.base + r.source() * depth;
129 // Defensive: a transiently non-finite source would otherwise poison
130 // r.state permanently (NaN is sticky through the one-pole). Hold the
131 // last good value instead, matching DryWetMixer's isnan guard.
132 if (!std::isfinite(value))
133 {
134 r.target(r.state);
135 continue;
136 }
137 if (!r.primed || r.smoothMs <= T(0))
138 {
139 r.state = value; // no sweep-in from zero on the first hit
140 r.primed = true;
141 }
142 else
143 {
144 const double a = std::exp(-dt * 1000.0 / static_cast<double>(r.smoothMs));
145 r.state = static_cast<T>(a * static_cast<double>(r.state)
146 + (1.0 - a) * static_cast<double>(value));
147 }
148 r.target(r.state);
149 }
150 }
151
152private:
153 struct Route
154 {
155 std::function<T()> source;
156 std::function<void(T)> target;
157 std::atomic<T> depth { T(1) };
158 T base = T(0);
159 T smoothMs = T(20);
160 T state = T(0);
161 bool primed = false;
162 };
163
164 std::array<Route, static_cast<size_t>(MaxRoutes)> routes_ {};
165 int numRoutes_ = 0;
166};
167
168} // namespace dspark
Fixed-capacity source-to-target router with per-route depth/smoothing.
void setDepth(int route, T depth) noexcept
Changes a route's depth. Control thread (relaxed atomic word, adopted by the next update()).
void update(int numSamples, double sampleRate) noexcept
Evaluates every route once. Call per audio block.
int addRoute(std::function< T()> source, std::function< void(T)> target, T depth=T(1), T base=T(0), T smoothMs=T(20))
Adds a route (call from setup/UI threads, not the audio callback).
void clear() noexcept
Removes all routes and releases their captures (setup threads only).
int getNumRoutes() const noexcept
Number of configured routes.
Main namespace for the DSPark framework.