Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
table_friction.hpp
Go to the documentation of this file.
1// sbl/dsp/pm/table_friction.hpp — A bow: friction solved against the string (Physical modeling — primitive)
2//
3// Friction at a bow depends on the relative velocity between bow and string,
4// but the string's velocity depends on the force applied. The two are one
5// equation:
6//
7// dv = dv_free - F / (2Z) dv_free = v_bow - v_incoming
8// F = f(dv)
9//
10// f(dv) is the friction characteristic; the string constrains the operating
11// point to a line of slope -2Z through (dv_free, 0) — the load line. The
12// answer is where they cross. McIntyre, Schumacher and Woodhouse (1983)
13// showed the crossing is a function of dv_free alone, so lute solves it
14// offline and this reads the answer: one lookup, no iteration. STK's bow
15// table (Smith, Cook) and Elements' Resonator::BowTable (Emilie Gillet, MIT)
16// are the same object, with the impedance folded into constants.
17//
18// Units are the waveguide's wave variables, normalised so 2Z = 1. Holding
19// the string stuck then costs exactly F = dv_free, which makes the stick
20// test one comparison.
21//
22// This is the "friction curve model": force a function of sliding speed
23// alone. Woodhouse & Galluzzo (Acta Acustica 90, 2004, §3.5) call it the
24// one part of the bowed-string model that measurements contradict — real
25// rosin traces a hysteresis loop in the force–velocity plane, and the
26// thermal plastic model is the candidate replacement. It is the right first
27// layer (every classic result rests on it) and a known simplification, not a
28// finished bow. RPT-029: provide both, architect around neither.
29//
30// ## Stick and slip
31//
32// Stuck, the string travels with the bow and the contact carries whatever
33// force that takes — until it exceeds what static friction can supply, and
34// the string releases. Sliding, the force follows the solved branch until
35// the relative velocity collapses and the bow recaptures the string. The
36// release and capture points differ: that hysteresis is stick-slip, and it
37// is what gives a bowed attack its bite. One bool of state carries it.
38//
39// ## Energy
40//
41// A bow is a source: it does work on the string, so this has no passivity
42// proof and is one of the components RPT-029 Part 4 rule 6 names — the
43// SoftLimiter on the injection and the NaN guard downstream are
44// load-bearing. Force never exceeds dv_free (the table is generated with
45// that property asserted), so the bow cannot drive the string faster than
46// itself.
47//
48// A law, not an exciter: a Bow<TableFriction> owns the speed, the weight and
49// the junction; this owns the friction. In wave units: the force returned is
50// F / 2Z, what a string of impedance Z takes from it.
51//
52// Usage:
53// sbl::dsp::pm::TableFriction law;
54// law.set_bow(lut::bow_stribeck_128, lut::bow_stribeck_limits, 8, 128, 129);
55// law.set_force_su(pressure);
56// float injection = law.process(v_bow - v_incoming);
57
58#ifndef SBL_DSP_PM_TABLE_FRICTION_HPP_
59#define SBL_DSP_PM_TABLE_FRICTION_HPP_
60
61#include <cmath>
62#include <cstdint>
63
66
67namespace sbl::dsp::pm {
68
69/**
70 * @brief One bow: a lute-solved family and the shape of its arrays
71 *
72 * The generated headers give a family and its limits; this carries them
73 * together with the geometry needed to read them, so a bow can be passed
74 * around as one value and a set of bows is an array.
75 */
76struct BowTable {
77 const float* family = nullptr; ///< force_slices rows, light bow first
78 const float* limits = nullptr; ///< static-friction limit per slice
79 uint8_t slices = 0;
80 uint16_t size = 0; ///< samples per row
81 uint16_t stride = 0; ///< row pitch (size + guard points)
82 float dv_max = 1.0f; ///< relative velocity at the last sample
83};
84
86public:
87 /// @note All public methods are ISR-safe — bounded computation, no I/O.
88
89 /**
90 * @brief Install a bow: a lute-solved family and its per-slice limits
91 *
92 * @param family force_slices rows of `stride` floats, light bow first
93 * @param limits one static-friction limit per slice
94 * @param slices number of force slices
95 * @param size samples per row spanning dv_free in [0, dv_max]
96 * @param stride row pitch (size + guard points)
97 * @param dv_max relative velocity the last sample corresponds to
98 */
99 void set_bow(const float* family, const float* limits, uint8_t slices,
100 uint16_t size, uint16_t stride, float dv_max = 1.0f) {
101 family_ = family;
102 limits_ = limits;
103 slices_ = slices;
104 size_ = size;
105 stride_ = stride;
106 dv_max_ = dv_max > 0.0f ? dv_max : 1.0f;
107 inv_dv_max_ = 1.0f / dv_max_; // the per-sample lookup multiplies, never divides
108 set_force_su(force_su_);
109 }
110
111 /// Install a bow described in one value.
112 void set_bow(const BowTable& bow) {
113 set_bow(bow.family, bow.limits, bow.slices, bow.size, bow.stride, bow.dv_max);
114 }
115
116 /// Bow force across the family [0, 1]; crossfades adjacent slices.
117 void set_force_su(float su) {
118 force_su_ = su < 0.0f ? 0.0f : (su > 1.0f ? 1.0f : su);
119 if (family_ == nullptr || slices_ == 0) return;
120
121 const float pos = force_su_ * static_cast<float>(slices_ - 1);
122 slice_ = static_cast<uint8_t>(pos);
123 if (slice_ >= slices_ - 1 && slices_ > 1) slice_ = static_cast<uint8_t>(slices_ - 2);
124 blend_ = slices_ > 1 ? pos - static_cast<float>(slice_) : 0.0f;
125
126 limit_ = slices_ > 1
127 ? limits_[slice_] + blend_ * (limits_[slice_ + 1] - limits_[slice_])
128 : limits_[0];
129 }
130
131 /**
132 * @brief One sample: relative velocity in, injection force out
133 *
134 * @param dv_free v_bow - v_incoming, in wave variables
135 */
136 float process(float dv_free) {
137 if (family_ == nullptr) return 0.0f;
138
139 const float a = std::fabs(dv_free);
140
141 if (stuck_) {
142 if (a <= limit_) return dv_free; // travelling with the bow
143 stuck_ = false; // static friction gave way
144 }
145
146 const float f = lookup(a);
147 if (a - f <= CAPTURE_VELOCITY) stuck_ = true; // the bow caught it again
148 return dv_free < 0.0f ? -f : f;
149 }
150
151 /// True while the string travels with the bow.
152 bool sticking() const { return stuck_; }
153
154 /// Static-friction limit at the current bow force — where the string releases.
155 float limit() const { return limit_; }
156
157 void reset() { stuck_ = true; }
158
159 /// One Junction node: the friction nonlinearity. Never called from audio code (AP-037).
160 diagram::Ports describe(diagram::Graph& g, uint8_t group, const char* name = "bow friction") const {
162 p.in = p.out = g.add_node(diagram::Kind::Junction, name, group);
163 p.group = group;
164 return p;
165 }
166
167private:
168 /// Relative velocity below which the bow is considered to have recaptured.
169 static constexpr float CAPTURE_VELOCITY = 1e-4f;
170
171 /// Solved force at |dv_free|, interpolated within and across slices.
172 float lookup(float a) const {
173 const float last = static_cast<float>(size_ - 1);
174 float x = a * inv_dv_max_ * last;
175 // NaN fails every comparison, so test the positive case: a NaN
176 // velocity reads the table's origin rather than casting NaN, which
177 // is undefined.
178 x = (x > 0.0f) ? (x < last ? x : last) : 0.0f;
179
180 const uint16_t i = static_cast<uint16_t>(x);
181 const float frac = x - static_cast<float>(i);
182 const uint16_t j = (i + 1 < size_) ? static_cast<uint16_t>(i + 1) : i;
183
184 const float* row0 = family_ + static_cast<uint32_t>(slice_) * stride_;
185 const float v0 = row0[i] + frac * (row0[j] - row0[i]);
186 if (slices_ < 2) return v0;
187
188 const float* row1 = row0 + stride_;
189 const float v1 = row1[i] + frac * (row1[j] - row1[i]);
190 return v0 + blend_ * (v1 - v0);
191 }
192
193 const float* family_ = nullptr;
194 const float* limits_ = nullptr;
195 uint8_t slices_ = 0;
196 uint16_t size_ = 0;
197 uint16_t stride_ = 0;
198 float dv_max_ = 1.0f;
199 float inv_dv_max_ = 1.0f;
200
201 float force_su_ = 1.0f;
202 uint8_t slice_ = 0;
203 float blend_ = 0.0f;
204 float limit_ = 0.0f;
205 bool stuck_ = true;
206};
207
208static_assert(is_friction_law_v<TableFriction>, "TableFriction is a friction law (physical-modeling.md §4.5)");
209
210} // namespace sbl::dsp::pm
211
212#endif // SBL_DSP_PM_TABLE_FRICTION_HPP_
uint8_t add_node(Kind kind, const char *name, uint8_t group, float value=0.0f, float value2=0.0f)
Definition graph.hpp:81
float process(float dv_free)
One sample: relative velocity in, injection force out.
float limit() const
Static-friction limit at the current bow force — where the string releases.
void set_bow(const BowTable &bow)
Install a bow described in one value.
void set_bow(const float *family, const float *limits, uint8_t slices, uint16_t size, uint16_t stride, float dv_max=1.0f)
Install a bow: a lute-solved family and its per-slice limits.
bool sticking() const
True while the string travels with the bow.
void set_force_su(float su)
Bow force across the family [0, 1]; crossfades adjacent slices.
diagram::Ports describe(diagram::Graph &g, uint8_t group, const char *name="bow friction") const
One Junction node: the friction nonlinearity. Never called from audio code (AP-037).
What a load, an exciter, a termination and a friction law provide (Physical modeling — cross-cutting)
A model's wiring, as data (AP-037)
@ Junction
where waves scatter or a source meets the string
Physical modeling: laws, bows, junctions, loads, resonators, strings (docs/conventions/physical-model...
Definition bow.hpp:31
The ports a component exposes after describing itself, so an owner can wire them.
Definition graph.hpp:130
uint8_t in
where a wave enters (the junction, the load)
Definition graph.hpp:133
uint8_t out
where a wave leaves (the pickup, the reflection)
Definition graph.hpp:134
One bow: a lute-solved family and the shape of its arrays.
const float * limits
static-friction limit per slice
float dv_max
relative velocity at the last sample
uint16_t size
samples per row
const float * family
force_slices rows, light bow first
uint16_t stride
row pitch (size + guard points)