66#ifndef SBL_DSP_PM_STRING_LOOP_HPP_
67#define SBL_DSP_PM_STRING_LOOP_HPP_
105 void init(
float* a_buffer, uint32_t a_max,
106 float* b_buffer, uint32_t b_max) {
107 end_a_.
init(a_buffer, a_max);
108 end_b_.
init(b_buffer, b_max);
110 limit_a_ =
static_cast<float>(a_max > 1 ? a_max - 1 : 1);
111 limit_b_ =
static_cast<float>(b_max > 1 ? b_max - 1 : 1);
112 loss_.
set_range(DAMPING_MIN_HZ, DAMPING_MAX_HZ);
126 beta_ =
math::clamp(beta, JUNCTION_EDGE, 1.0f - JUNCTION_EDGE);
162 template<
typename Excite,
typename EndB>
163 float tick(Excite&& excite, EndB&& end_b) {
164 static_assert(is_wave_callable_v<EndB>,
"the end takes a wave and returns one: float(float)");
165 const float to_b = end_b_.
read(d_b_);
166 const float to_a = end_a_.
read(d_a_);
170 const float from_a = -to_a;
172 const float from_b = gain_ * end_b(arriving);
174 const float v_free = from_a + from_b;
175 const float injection = inject(
static_cast<Excite&&
>(excite), v_free);
177 end_a_.
write(from_b + injection);
178 end_b_.
write(from_a + injection);
179 x_ = leak_ * x_ + (v_free + injection);
184 template<
typename Excite>
195 static float rigid_end(
float arriving) {
return -arriving; }
220 const uint32_t n_a =
static_cast<uint32_t
>(d_a_);
221 const uint32_t n_b =
static_cast<uint32_t
>(d_b_);
222 for (uint32_t i = 1; i <= n_a; ++i) {
const float v = end_a_.
read(
static_cast<float>(i)); e += v * v; }
223 for (uint32_t i = 1; i <= n_b; ++i) {
const float v = end_b_.
read(
static_cast<float>(i)); e += v * v; }
245 bool external_end_b =
false)
const {
252 const uint8_t seg_a = g.
add_node(Kind::DelayLine,
"segment a", p.
group, d_a_);
253 const uint8_t wall_a = g.
add_node(Kind::Reflection,
"end a", p.
group, -1.0f);
254 const uint8_t seg_b = g.
add_node(Kind::DelayLine,
"segment b", p.
group, d_b_);
255 const uint8_t allpass = g.
add_node(Kind::Allpass,
"tuning", p.
group,
257 const uint8_t loss = g.
add_node(Kind::Loss,
"loss", p.
group, loss_cutoff_hz());
260 g.
add_edge(seg_a, wall_a, Wave::Velocity);
263 g.
add_edge(seg_b, allpass, Wave::Velocity);
264 g.
add_edge(allpass, loss, Wave::Velocity);
266 if (external_end_b) {
267 p.
add(
"to end b", loss);
270 const uint8_t wall = g.
add_node(Kind::Reflection,
"end b", p.
group, -1.0f);
271 g.
add_edge(loss, wall, Wave::Velocity);
284 static constexpr float DAMPING_MIN_HZ = 500.0f;
285 static constexpr float DAMPING_MAX_HZ = 16000.0f;
286 static constexpr float DEFAULT_DAMPING_SU = 0.8f;
287 static constexpr float DEFAULT_PERIOD = 100.0f;
288 static constexpr float DEFAULT_JUNCTION = 0.3f;
289 static constexpr float DEFAULT_LOOP_GAIN = 0.995f;
290 static constexpr float JUNCTION_EDGE = 0.02f;
291 static constexpr float ALLPASS_NOMINAL = 1.0f;
292 static constexpr float MIN_SEGMENT = 1.0f;
293 static constexpr float HALF_SAMPLE = 0.5f;
294 static constexpr float TWO = 2.0f;
297 float loss_cutoff_hz()
const {
299 if (c <= 0.0f || c >= 1.0f)
return 0.0f;
305 template<
typename Excite>
306 float inject(Excite&& excite,
float v_free) {
307 if constexpr (is_exciter_v<std::decay_t<Excite>>) {
308 return excite.excite(JunctionState{v_free, x_, impedance_}) / (TWO * impedance_);
310 static_assert(is_wave_callable_v<Excite>,
"excite takes the junction state or the velocity");
311 return excite(v_free);
315 static float round_to_sample(
float x) {
316 return static_cast<float>(
static_cast<int32_t
>(x + HALF_SAMPLE));
319 static float clamp_segment(
float d,
float limit) {
320 if (d < MIN_SEGMENT)
return MIN_SEGMENT;
321 if (d > limit)
return limit;
335 const float total = period_ - ALLPASS_NOMINAL;
337 float a = clamp_segment(round_to_sample(total * beta_), limit_a_);
338 float b = clamp_segment(round_to_sample(total - a), limit_b_);
342 float residual = total - a - b;
343 if (residual > HALF_SAMPLE) {
344 if (b < limit_b_) { b += 1.0f; residual -= 1.0f; }
345 else if (a < limit_a_) { a += 1.0f; residual -= 1.0f; }
346 }
else if (residual < -HALF_SAMPLE) {
347 if (b > MIN_SEGMENT) { b -= 1.0f; residual += 1.0f; }
348 else if (a > MIN_SEGMENT) { a -= 1.0f; residual += 1.0f; }
353 allpass_.
set_delay(ALLPASS_NOMINAL + residual);
361 float period_ = DEFAULT_PERIOD;
362 float beta_ = DEFAULT_JUNCTION;
363 float gain_ = DEFAULT_LOOP_GAIN;
364 float limit_a_ = 1.0f;
365 float limit_b_ = 1.0f;
366 float d_a_ = MIN_SEGMENT;
367 float d_b_ = MIN_SEGMENT;
Clamping (Cross-cutting — Math)
uint8_t add_group(const char *name, uint8_t parent=NO_GROUP)
Open a group (a component); returns its id. Nodes added with it belong to it.
void add_edge(uint8_t from, uint8_t to, Wave wave)
uint8_t add_node(Kind kind, const char *name, uint8_t group, float value=0.0f, float value2=0.0f)
void set_damping_su(float su)
Loss filter cutoff: how fast the high partials give up. Signal units.
float tick()
Advance one sample with nothing injected: the string rings on alone.
float delay_b_samples() const
float tick(Excite &&excite, EndB &&end_b)
Advance one sample.
static constexpr float DEFAULT_IMPEDANCE
The loop's wave impedance unless told otherwise: the unit the laws and loads are written in.
JunctionState junction_state(float v_free) const
What the exciter sees this sample, for a scope.
void set_junction(float beta)
Excitation point along the string, 0 at end a, 1 at end b (a string: nut and bridge).
void init(float *a_buffer, uint32_t a_max, float *b_buffer, uint32_t b_max)
Install the two segments' buffers.
float tick(Excite &&excite)
Advance one sample against a rigid end b: reflection −1.
float energy() const
Energy in the two segments, in wave units: the sum of squares of every sample in flight (physical-mod...
static float rigid_end(float arriving)
An end with nothing on it: a wall.
void set_loop_gain(float gain)
Round-trip gain, the passivity margin.
void set_period_samples(float period)
Loop period in samples — the pitch. Sample rate over frequency.
float allpass_delay_samples() const
The allpass's share of the period: its nominal sample plus the rounding residual.
diagram::Ports describe(diagram::Graph &g, uint8_t parent=diagram::NO_GROUP, bool external_end_b=false) const
The loop's wiring as data (AP-037). Never called from audio code.
float delay_a_samples() const
Segment lengths actually read — the split the junction makes, clamped to the buffers.
static constexpr float DISPLACEMENT_LEAK_HZ
The junction displacement forgets DC below this: a leaky integrator.
void set_impedance(float z)
The wave impedance Z the junction reports and converts forces with.
float displacement() const
The junction's displacement right now (wave units × samples).
float period_samples() const
static constexpr float MIN_PERIOD
Shortest loop: eight samples, 6 kHz at 48 kHz. Pitch requests above it play this.
float read(float delay_samples) const
Read at fractional delay with linear interpolation.
void write(float sample)
Write a sample to the delay line.
void init(float *buffer, uint32_t max_delay)
Initialize after default construction.
void reset()
Zero all samples in the buffer and reset write position.
void reset()
Reset filter state to zero.
void set_range(float min_hz, float max_hz)
Set frequency range for _su mapping (configuration, not a signal)
float process(float x)
Process a single sample.
float coefficient() const
Current coefficient.
void set_cutoff_su(float su)
Set cutoff frequency via signal unit.
Order-N Thiran allpass: fractional delay by phase, not interpolation.
float process(float x)
Process one sample.
void set_delay(float delay_samples)
Set the total phase delay in samples.
float group_delay_samples() const
Phase delay the filter is tuned to, in samples (flat at DC by design).
The numbers every layer reaches for.
What a load, an exciter, a termination and a friction law provide (Physical modeling — cross-cutting)
Circular buffer delay line.
Fixed-point constants and audio sample types.
A model's wiring, as data (AP-037)
Wave
What travels along an edge.
Kind
What a node is, in the paper's vocabulary.
constexpr uint8_t NO_GROUP
constexpr float clamp01(float x)
x held to [0, 1]; NaN → 0.
constexpr float clamp(float x, float lo, float hi)
x held to [lo, hi]; NaN → lo.
Physical modeling: laws, bows, junctions, loads, resonators, strings (docs/conventions/physical-model...
Single-pole IIR filters (LP and HP)
The ports a component exposes after describing itself, so an owner can wire them.
void add(const char *name, uint8_t node)
Add a named port; a ninth is refused and find() answers NO_NODE for it.
uint8_t in
where a wave enters (the junction, the load)
uint8_t out
where a wave leaves (the pickup, the reflection)
What the guide reports at an excitation point, each sample.
Maximally-flat group-delay allpass.