DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
ModulationRouter.h
Go to the documentation of this file.
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
56template <FloatType T, int MaxRoutes = 16>
58{
59public:
73 int addRoute(std::function<T()> source, std::function<void(T)> target,
74 T depth = T(1), T base = T(0), T smoothMs = T(20))
75 {
76 // Empty callables would raise std::bad_function_call inside the
77 // noexcept audio-thread update() -- reject them at the door.
78 if (numRoutes_ >= MaxRoutes || !source || !target) return -1;
79 auto& r = routes_[static_cast<size_t>(numRoutes_)];
80 r.source = std::move(source);
81 r.target = std::move(target);
82 r.depth.store(depth, std::memory_order_relaxed);
83 r.base = base;
84 r.smoothMs = std::max(smoothMs, T(0));
85 r.state = T(0);
86 r.primed = false;
87 return numRoutes_++;
88 }
89
91 void setDepth(int route, T depth) noexcept
92 {
93 if (route >= 0 && route < numRoutes_)
94 routes_[static_cast<size_t>(route)].depth.store(depth, std::memory_order_relaxed);
95 }
96
98 void clear() noexcept
99 {
100 for (int i = 0; i < numRoutes_; ++i)
101 {
102 routes_[static_cast<size_t>(i)].source = nullptr;
103 routes_[static_cast<size_t>(i)].target = nullptr;
104 }
105 numRoutes_ = 0;
106 }
107
109 [[nodiscard]] int getNumRoutes() const noexcept { return numRoutes_; }
110
116 void update(int numSamples, double sampleRate) noexcept
117 {
118 const double dt = static_cast<double>(numSamples) / std::max(sampleRate, 1.0);
119 for (int i = 0; i < numRoutes_; ++i)
120 {
121 auto& r = routes_[static_cast<size_t>(i)];
122 const T depth = r.depth.load(std::memory_order_relaxed);
123 const T value = r.base + r.source() * depth;
124 // Defensive: a transiently non-finite source would otherwise poison
125 // r.state permanently (NaN is sticky through the one-pole). Hold the
126 // last good value instead, matching DryWetMixer's isnan guard.
127 if (!std::isfinite(value))
128 {
129 r.target(r.state);
130 continue;
131 }
132 if (!r.primed || r.smoothMs <= T(0))
133 {
134 r.state = value; // no sweep-in from zero on the first hit
135 r.primed = true;
136 }
137 else
138 {
139 const double a = std::exp(-dt * 1000.0 / static_cast<double>(r.smoothMs));
140 r.state = static_cast<T>(a * static_cast<double>(r.state)
141 + (1.0 - a) * static_cast<double>(value));
142 }
143 r.target(r.state);
144 }
145 }
146
147private:
148 struct Route
149 {
150 std::function<T()> source;
151 std::function<void(T)> target;
152 std::atomic<T> depth { T(1) };
153 T base = T(0);
154 T smoothMs = T(20);
155 T state = T(0);
156 bool primed = false;
157 };
158
159 std::array<Route, static_cast<size_t>(MaxRoutes)> routes_ {};
160 int numRoutes_ = 0;
161};
162
163} // namespace dspark
Core mathematical utilities for digital signal processing.
Fixed-capacity source-to-target router with per-route depth/smoothing.
void setDepth(int route, T depth) noexcept
Changes a route's depth. Thread-safe (atomic, relaxed).
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.