Skip to content

Commit 46ccde9

Browse files
committed
cubicstringmidi for easier midi note handling
1 parent 6bc72c9 commit 46ccde9

5 files changed

Lines changed: 391 additions & 9 deletions

File tree

src/maxmsp/1dSAV/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ if (APPLE)
2424
endif()
2525
endif()
2626

27+
# Export compile config
28+
add_definitions(-DCMAKE_EXPORT_COMPILE_COMMANDS=ON)
29+
2730

2831
# Misc setup and subroutines
2932
include(${CMAKE_CURRENT_SOURCE_DIR}/source/min-api/script/min-package.cmake)
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
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
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
cmake_minimum_required(VERSION 3.10)
2+
3+
set(C74_MIN_API_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../min-api)
4+
include(${C74_MIN_API_DIR}/script/min-pretarget.cmake)
5+
6+
set(AUDIO_PROCESSORS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../processor)
7+
8+
#############################################################
9+
# MAX EXTERNAL
10+
#############################################################
11+
12+
13+
include_directories(
14+
"${C74_INCLUDES}"
15+
${CMAKE_CURRENT_SOURCE_DIR}
16+
${AUDIO_PROCESSORS_DIR}
17+
)
18+
19+
20+
set( SOURCE_FILES
21+
${PROJECT_NAME}.cpp
22+
${AUDIO_PROCESSORS_DIR}/StringProcessor.cpp
23+
)
24+
25+
26+
find_package (Eigen3 REQUIRED NO_MODULE)
27+
28+
add_library(
29+
${PROJECT_NAME}
30+
MODULE
31+
${SOURCE_FILES}
32+
)
33+
34+
if(APPLE)
35+
target_link_libraries(
36+
${PROJECT_NAME} PRIVATE
37+
"-framework Accelerate"
38+
)
39+
endif()
40+
41+
target_link_libraries(
42+
${PROJECT_NAME} PUBLIC
43+
Eigen3::Eigen
44+
)
45+
46+
47+
include(${C74_MIN_API_DIR}/script/min-posttarget.cmake)
48+
49+
50+
# Optimisation flags
51+
if(NOT MSVC)
52+
target_compile_options(${PROJECT_NAME} PRIVATE
53+
-O3
54+
-march=native
55+
-ffast-math
56+
-funroll-loops
57+
-fno-omit-frame-pointer
58+
)
59+
target_compile_definitions(${PROJECT_NAME} PRIVATE
60+
EIGEN_NO_DEBUG
61+
EIGEN_DONT_PARALLELIZE
62+
)
63+
endif()

0 commit comments

Comments
 (0)