1+ #include " StringProcessor.h"
2+ #include " c74_min.h"
3+ #include < atomic>
4+ #include < memory>
5+ using namespace c74 ::min;
6+
7+ // clang-format off
8+
9+ class CubicStringMidi
10+ : public object<CubicStringMidi>,
11+ public sample_operator<4 , 3 >
12+ {
13+ private:
14+ std::shared_ptr<StringProcessor<double >> processor, newProcessor;
15+ float sr{0 };
16+ float pbend{0 }, posex{0.9 }, poslistL{0.3 }, poslistR{0.3 }, t60_0mod{4 };
17+ std::atomic<bool > reinitFlag{true };
18+
19+ // note trigger state
20+ std::atomic<bool > swapProc{false };
21+ std::atomic<double > velocity{0.0 };
22+
23+ // Excitation signal
24+ int excitationType{0 }; // 0 for "struck" 1 for "pluck"
25+ float elapsedTimeImpulse{1 };
26+ float inputForce{0 };
27+
28+ public:
29+ MIN_DESCRIPTION{" String model with cubic nonlinearity, midi activated through `note' message" };
30+ MIN_TAGS{" audio" };
31+ MIN_AUTHOR{" Thomas Risse" };
32+
33+ // no sample input inlet: note event is handled through message<>
34+ inlet<> posexInlet{this , " (signal) excitation position" };
35+ inlet<> poslistLInlet{this , " (signal) left listening position" };
36+ inlet<> poslistRInlet{this , " (signal) right listening position" };
37+ inlet<> t60_0Inlet{this , " (signal) decay time at 0 Hz" };
38+ outlet<> outputL{this , " (signal) left output" , " signal" };
39+ outlet<> outputR{this , " (signal) right output" , " signal" };
40+ outlet<> outputEps{this , " (signal) epsilon" , " signal" };
41+ outlet<> outputData{this , " (list) Data" };
42+
43+ attribute<number, threadsafe::no, limit::clamp> lambda0{
44+ this ,
45+ " regularisation parameter" ,
46+ 100 ,
47+ range{0 , 1000000 },
48+ setter{
49+ MIN_FUNCTION{
50+ if (reinitFlag.exchange (false ))
51+ newProcessor = std::make_shared<StringProcessor<double >>(*processor);
52+ newProcessor->lambda0 = args[0 ];
53+ return args;
54+ }
55+ }
56+ };
57+
58+ attribute<number, threadsafe::no, limit::clamp> alpha{
59+ this ,
60+ " stability condition setting" ,
61+ 0.9 ,
62+ range{0.1 , 1 },
63+ setter{
64+ MIN_FUNCTION{
65+ if (reinitFlag.exchange (false ))
66+ newProcessor = std::make_shared<StringProcessor<double >>(*processor);
67+ newProcessor->alpha = args[0 ];
68+ return args;
69+ }
70+ }
71+ };
72+
73+ attribute<number, threadsafe::no, limit::clamp> f0{
74+ this ,
75+ " fundamental frequency" ,
76+ 200 ,
77+ range{1 , 10000 },
78+ setter{
79+ MIN_FUNCTION{
80+ if (reinitFlag.exchange (false ))
81+ newProcessor = std::make_shared<StringProcessor<double >>(*processor);
82+ newProcessor->f0 = args[0 ];
83+ return args;
84+ }
85+ }
86+ };
87+
88+ attribute<number, threadsafe::no, limit::clamp> beta{
89+ this ,
90+ " beta" ,
91+ 1e-4 ,
92+ range{1e-12 , 1 },
93+ setter{
94+ MIN_FUNCTION{
95+ if (reinitFlag.exchange (false ))
96+ newProcessor = std::make_shared<StringProcessor<double >>(*processor);
97+ newProcessor->beta = args[0 ];
98+ return args;
99+ }
100+ }
101+ };
102+
103+ attribute<number, threadsafe::no, limit::clamp> impulseWidth{
104+ this ,
105+ " impulse width (s)" ,
106+ 1e-3 ,
107+ range{1e-5 , 1e-1 },
108+ setter{
109+ MIN_FUNCTION{
110+ return args;
111+ }
112+ }
113+ };
114+
115+ attribute<number, threadsafe::no, limit::clamp> t60_0{
116+ this ,
117+ " first decay time" ,
118+ 4 ,
119+ range{1e-4 , 100 },
120+ setter{
121+ MIN_FUNCTION{
122+ if (reinitFlag.exchange (false ))
123+ newProcessor = std::make_shared<StringProcessor<double >>(*processor);
124+ newProcessor->t60_0 = args[0 ];
125+ newProcessor->t60_1 = newProcessor->t60_0 * brightness;
126+ return args;
127+ }
128+ }
129+ };
130+
131+ attribute<number, threadsafe::no, limit::clamp> fd0{
132+ this ,
133+ " first decay frequency" ,
134+ 100 ,
135+ range{0 , 1000 },
136+ setter{
137+ MIN_FUNCTION{
138+ if (reinitFlag.exchange (false ))
139+ newProcessor = std::make_shared<StringProcessor<double >>(*processor);
140+ newProcessor->fd0 = args[0 ];
141+ return args;
142+ }
143+ }
144+ };
145+
146+ attribute<number, threadsafe::no, limit::clamp> brightness{
147+ this ,
148+ " brightness" ,
149+ 0.8 ,
150+ range{1e-2 , 1 },
151+ setter{
152+ MIN_FUNCTION{
153+ if (reinitFlag.exchange (false ))
154+ newProcessor = std::make_shared<StringProcessor<double >>(*processor);
155+ newProcessor->t60_1 = newProcessor->t60_0 * brightness;
156+ return args;
157+ }
158+ }
159+ };
160+
161+ attribute<number, threadsafe::no, limit::clamp> fd1{
162+ this ,
163+ " brightness freqeuncy" ,
164+ 1000 ,
165+ range{100 , 10000 },
166+ setter{
167+ MIN_FUNCTION{
168+ if (reinitFlag.exchange (false ))
169+ newProcessor = std::make_shared<StringProcessor<double >>(*processor);
170+ newProcessor->fd1 = args[0 ];
171+ return args;
172+ }
173+ }
174+ };
175+
176+ attribute<int , threadsafe::no, limit::clamp> nl_mode{
177+ this ,
178+ " nonlinear mode" ,
179+ 2 ,
180+ range{0 , 4 },
181+ setter{
182+ MIN_FUNCTION{
183+ if (reinitFlag.exchange (false ))
184+ newProcessor = std::make_shared<StringProcessor<double >>(*processor);
185+ newProcessor->nonlinear_mode = args[0 ];
186+ return args;
187+ }
188+ }
189+ };
190+
191+ message<> note{
192+ this ,
193+ " note" ,
194+ " Trigger a note: arg0 frequency [Hz], arg1 velocity [0..1]" ,
195+ MIN_FUNCTION{
196+ if (args.size () >= 2 && sr > 0 ){
197+ double velocity = std::max (0.0 , std::min (1.0 , (double )args[1 ]));
198+ this ->velocity .store (velocity);
199+
200+ if (reinitFlag.exchange (false ))
201+ newProcessor = std::make_shared<StringProcessor<double >>(*processor);
202+
203+ newProcessor->f0 = args[0 ];
204+ swapProc.store (newProcessor->reinitDsp (sr));
205+ }
206+ return args;
207+ }
208+ };
209+
210+ message<> dspsetup{
211+ this ,
212+ " dspsetup" ,
213+ MIN_FUNCTION{
214+ sr = args[0 ];
215+ processor->reinitDsp (sr);
216+ return {};
217+ }
218+ };
219+
220+ message<> outstate{
221+ this ,
222+ " state" ,
223+ MIN_FUNCTION{
224+ std::vector<float > state = processor->getState ();
225+ atoms state_atoms;
226+ state_atoms.reserve (state.size ());
227+ for (auto v : state)
228+ state_atoms.push_back (atom (v));
229+ outputData.send (state_atoms);
230+ return {};
231+ }
232+ };
233+
234+ message<> number{
235+ this ,
236+ " number" ,
237+ MIN_FUNCTION{
238+ if (inlet == 1 )
239+ pbend = args[0 ];
240+ else if (inlet == 2 )
241+ posex = args[0 ];
242+ else if (inlet == 3 )
243+ poslistL = args[0 ];
244+ else if (inlet == 4 )
245+ poslistR = args[0 ];
246+ else if (inlet == 5 )
247+ t60_0mod = args[0 ];
248+ return {};
249+ }
250+ };
251+
252+ CubicStringMidi (const atom &args = {})
253+ {
254+ processor = std::make_shared<StringProcessor<double >>(44100 );
255+ }
256+
257+ samples<3 >
258+ operator ()(sample posex,
259+ sample poslistL,
260+ sample poslistR,
261+ sample t60_0)
262+ {
263+ if (swapProc.exchange (false ))
264+ {
265+ reinitFlag.store (true );
266+ processor = newProcessor;
267+ elapsedTimeImpulse = 0 ;
268+ }
269+
270+ // Compute input
271+ if (excitationType == 0 )
272+ {
273+ inputForce = velocity.load () * sin (M_PI * elapsedTimeImpulse / impulseWidth) * float (elapsedTimeImpulse < impulseWidth);
274+ }
275+ else
276+ {
277+ inputForce = velocity.load () / 2 *
278+ ((1 - cos (M_PI * elapsedTimeImpulse / impulseWidth)) * (float (elapsedTimeImpulse < impulseWidth)) +
279+ (1 + cos (M_PI * (elapsedTimeImpulse - impulseWidth) * 10 / impulseWidth)) * float ((elapsedTimeImpulse > impulseWidth) and (elapsedTimeImpulse < impulseWidth * 1.1 )));
280+ }
281+ elapsedTimeImpulse += float (1 ) / sr;
282+
283+ if (posexInlet.has_signal_connection ())
284+ this ->posex = posex;
285+ if (poslistLInlet.has_signal_connection ())
286+ this ->poslistL = poslistL;
287+ if (poslistRInlet.has_signal_connection ())
288+ this ->poslistR = poslistR;
289+ if (t60_0Inlet.has_signal_connection ())
290+ this ->t60_0mod = t60_0;
291+
292+ auto [outL, outR, epsilon] = processor->process (double (inputForce),
293+ double (0.0 ), // no pbend dynamic in note API
294+ double (this ->posex ),
295+ double (this ->poslistL ),
296+ double (this ->poslistR ),
297+ double (this ->t60_0mod ));
298+
299+ return {{outL, outR, epsilon}};
300+ };
301+ };
302+
303+ MIN_EXTERNAL (CubicStringMidi);
304+
305+ // clang-format off
0 commit comments