forked from polyend/FxPatchSDK
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPatchImpl.cpp
More file actions
272 lines (238 loc) · 7.68 KB
/
Copy pathPatchImpl.cpp
File metadata and controls
272 lines (238 loc) · 7.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
// GENERATED FROM effects/phase_90.cpp -- do not hand-edit.
//
// This is the default target for a bare `make` (the Makefile's
// PATCH_IMPL_SRC). It is a copy of a real effect with its include paths
// rewritten for this directory. To refresh it after phase_90.cpp changes,
// run this from the repository root:
//
// sed -e 's|"../source/Patch.h"|"Patch.h"|' -e 's|"../source/dsp/|"dsp/|' effects/phase_90.cpp > source/PatchImpl.cpp
//
// It had previously drifted into a stale hand-copy: older voice tuning, an
// inline triangle LFO instead of dsp::TriangleLfo, and no getParameterName,
// so the one artifact a default `make` produces shipped unnamed knobs.
//
// To build a different effect, prefer scripts/build_effects.sh, which
// generates into build/generated_effects/ without touching this file.
// Phase 90-inspired phaser for Polyend Endless
// Phase 90-inspired phaser for Polyend Endless
//
// Primary voice:
// Block-logo Phase 90 style with a more pronounced, chewy sweep driven by
// four all-pass stages plus the stronger feedback path.
//
// Alternate voice:
// Hold toggles a script-mod / vintage voice with the feedback removed for a
// smoother, softer sweep closer to the early script-logo units.
//
// Controls:
// Mid knob — Speed
// Expression — mirrors Speed in this fork because param 2 is hardwired
// Footswitch — Press: bypass, Hold: block / script toggle
// LED — Blue/DimBlue block, Beige/DimWhite script
//
// Notes:
// - Left knob is intentionally unused
// - Right knob is reserved for the expression lane; if turned without an
// expression pedal attached it will mirror Speed rather than expose a
// second public control
#include "Patch.h"
#include "dsp/clamp.h"
#include "dsp/lfo.h"
#include <cmath>
namespace {
using dsp::clamp01;
using dsp::clampUnit;
constexpr float kPi = 3.14159265359f;
constexpr float kTwoPi = 6.28318530718f;
constexpr float kFs = static_cast<float>(Patch::kSampleRate);
// Triangle LFO shape comes from source/dsp/lfo.h. phase_90 manages its own
// phase counter (lfoPhase_) outside this call, so the stateless value()
// helper is the right fit; the stateful TriangleLfo class is the choice for
// new effects.
inline float triangleLfo(float phase) { return dsp::TriangleLfo::value(phase); }
float mapSpeedHz(float value)
{
constexpr float kMinHz = 0.12f;
constexpr float kMaxHz = 7.20f;
const float ratio = kMaxHz / kMinHz;
return kMinHz * powf(ratio, clamp01(value));
}
float mapSweepHz(float lfo, float minHz, float maxHz)
{
const float norm = 0.5f * (lfo + 1.0f);
const float ratio = maxHz / minHz;
return minHz * powf(ratio, norm);
}
float allpassCoeff(float fc)
{
const float g = tanf(kPi * fc / kFs);
return (1.0f - g) / (1.0f + g);
}
struct VoiceParams
{
float feedback;
float sweepMinHz;
float sweepMaxHz;
float dryMix;
float wetMix;
};
VoiceParams getVoice(bool scriptMode)
{
// Script voice (no feedback, softer blend) intentionally keeps the gentler
// Phase 90 script-logo feel. The block voice got a deeper feedback path
// (0.36 → 0.58) and a more forward wet mix (0.62 → 0.72) — the previous
// numbers produced a sweep that was audibly present but never really
// "chewy" the way the real block-logo pedal is.
if (scriptMode) {
return {0.00f, 110.0f, 1450.0f, 0.78f, 0.62f};
}
return {0.58f, 120.0f, 1650.0f, 0.74f, 0.72f};
}
struct AllpassStage
{
float z1L = 0.0f;
float z1R = 0.0f;
float processLeft(float input, float a)
{
const float output = -a * input + z1L;
z1L = input + a * output;
return output;
}
float processRight(float input, float a)
{
const float output = -a * input + z1R;
z1R = input + a * output;
return output;
}
void clear()
{
z1L = 0.0f;
z1R = 0.0f;
}
};
}
class Phase90Patch final : public Patch
{
public:
void init() override
{
speed_ = 0.34f;
bypassed_ = false;
script_ = false;
lfoPhase_ = 0.0f;
feedbackL_ = 0.0f;
feedbackR_ = 0.0f;
clearState();
}
void setWorkingBuffer(std::span<float, kWorkingBufferSize> /* buf */) override
{
// No working buffer required.
}
void processAudio(std::span<float> left, std::span<float> right) override
{
if (bypassed_) {
return;
}
const VoiceParams voice = getVoice(script_);
const float speedHz = mapSpeedHz(speed_);
const float phaseInc = speedHz / kFs;
for (size_t i = 0; i < left.size(); ++i)
{
const float lfo = triangleLfo(lfoPhase_);
const float sweepHz = mapSweepHz(lfo, voice.sweepMinHz, voice.sweepMaxHz);
const float a = allpassCoeff(sweepHz);
const float dryL = left[i];
const float dryR = right[i];
float wetL = dryL + feedbackL_ * voice.feedback;
float wetR = dryR + feedbackR_ * voice.feedback;
wetL = stages_[0].processLeft(wetL, a);
wetL = stages_[1].processLeft(wetL, a);
wetL = stages_[2].processLeft(wetL, a);
wetL = stages_[3].processLeft(wetL, a);
wetR = stages_[0].processRight(wetR, a);
wetR = stages_[1].processRight(wetR, a);
wetR = stages_[2].processRight(wetR, a);
wetR = stages_[3].processRight(wetR, a);
feedbackL_ = wetL;
feedbackR_ = wetR;
left[i] = clampUnit(dryL * voice.dryMix + wetL * voice.wetMix);
right[i] = clampUnit(dryR * voice.dryMix + wetR * voice.wetMix);
lfoPhase_ += phaseInc;
if (lfoPhase_ >= 1.0f) {
lfoPhase_ -= 1.0f;
}
}
}
ParameterMetadata getParameterMetadata(int idx) override
{
switch (idx) {
case 0: return {0.0f, 1.0f, 0.0f}; // intentionally unused
case 1: return {0.0f, 1.0f, 0.34f}; // Speed
case 2: return {0.0f, 1.0f, 0.34f}; // Expression mirrors Speed
default: return {0.0f, 1.0f, 0.5f};
}
}
const char* getParameterName(int idx) override
{
switch (idx) {
case 0: return nullptr; // intentionally unused
case 1: return "Speed";
case 2: return "Speed";
default: return nullptr;
}
}
void setParamValue(int idx, float value) override
{
switch (idx) {
case 1:
case 2:
speed_ = value;
break;
default:
break;
}
}
void handleAction(int actionIdx) override
{
if (actionIdx == static_cast<int>(endless::ActionId::kLeftFootSwitchPress))
{
bypassed_ = !bypassed_;
if (!bypassed_) {
clearState();
}
}
else if (actionIdx == static_cast<int>(endless::ActionId::kLeftFootSwitchHold))
{
script_ = !script_;
clearState();
}
}
Color getStateLedColor() override
{
if (script_) {
return bypassed_ ? Color::kDimWhite : Color::kBeige;
}
return bypassed_ ? Color::kDimBlue : Color::kBlue;
}
private:
void clearState()
{
for (auto& stage : stages_) {
stage.clear();
}
feedbackL_ = 0.0f;
feedbackR_ = 0.0f;
}
float speed_ = 0.34f;
bool bypassed_ = false;
bool script_ = false;
float lfoPhase_ = 0.0f;
float feedbackL_ = 0.0f;
float feedbackR_ = 0.0f;
AllpassStage stages_[4];
};
Patch* Patch::getInstance()
{
static Phase90Patch instance;
return &instance;
}