elektron-lfo 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3 -16
- package/package.json +1 -1
- package/src/engine/fade.ts +29 -36
- package/tests/depth-fade.test.ts +69 -39
package/dist/index.js
CHANGED
|
@@ -346,14 +346,11 @@ function calculateFadeCycles(fadeValue) {
|
|
|
346
346
|
if (fadeValue === 0)
|
|
347
347
|
return 0;
|
|
348
348
|
const absFade = Math.abs(fadeValue);
|
|
349
|
-
if (absFade >= 48) {
|
|
350
|
-
return Infinity;
|
|
351
|
-
}
|
|
352
349
|
if (absFade <= 16) {
|
|
353
|
-
return Math.max(0.5, absFade
|
|
350
|
+
return Math.max(0.5, 0.1 * absFade + 0.6);
|
|
354
351
|
}
|
|
355
|
-
const baseAt16 =
|
|
356
|
-
return baseAt16 * Math.pow(2, (absFade - 16) / 5);
|
|
352
|
+
const baseAt16 = 2.2;
|
|
353
|
+
return baseAt16 * Math.pow(2, (absFade - 16) / 4.5);
|
|
357
354
|
}
|
|
358
355
|
function updateFade(config, state, cycleTimeMs, deltaMs) {
|
|
359
356
|
if (config.fade === 0 || config.mode === "FRE") {
|
|
@@ -363,12 +360,6 @@ function updateFade(config, state, cycleTimeMs, deltaMs) {
|
|
|
363
360
|
};
|
|
364
361
|
}
|
|
365
362
|
const fadeCycles = calculateFadeCycles(config.fade);
|
|
366
|
-
if (!isFinite(fadeCycles)) {
|
|
367
|
-
return {
|
|
368
|
-
fadeProgress: 0,
|
|
369
|
-
fadeMultiplier: 0
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
363
|
const fadeDurationMs = fadeCycles * cycleTimeMs;
|
|
373
364
|
if (fadeDurationMs === 0) {
|
|
374
365
|
return {
|
|
@@ -387,10 +378,6 @@ function resetFade(config) {
|
|
|
387
378
|
if (config.fade === 0) {
|
|
388
379
|
return { fadeProgress: 1, fadeMultiplier: 1 };
|
|
389
380
|
}
|
|
390
|
-
const fadeCycles = calculateFadeCycles(config.fade);
|
|
391
|
-
if (!isFinite(fadeCycles)) {
|
|
392
|
-
return { fadeProgress: 0, fadeMultiplier: 0 };
|
|
393
|
-
}
|
|
394
381
|
if (config.fade < 0) {
|
|
395
382
|
return { fadeProgress: 0, fadeMultiplier: 0 };
|
|
396
383
|
} else {
|
package/package.json
CHANGED
package/src/engine/fade.ts
CHANGED
|
@@ -47,14 +47,23 @@ export function calculateFadeMultiplier(
|
|
|
47
47
|
/**
|
|
48
48
|
* Calculate fade cycles - how many LFO cycles for complete fade
|
|
49
49
|
*
|
|
50
|
-
* Based on empirical testing against Digitakt II hardware:
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* - |FADE
|
|
56
|
-
*
|
|
57
|
-
*
|
|
50
|
+
* Based on empirical testing against Digitakt II hardware (January 2025):
|
|
51
|
+
*
|
|
52
|
+
* Key observations:
|
|
53
|
+
* - |FADE| <= 16: Linear region, ~1 cycle at FADE=4, ~2.2 cycles at FADE=16
|
|
54
|
+
* - |FADE| > 16: Exponential slowdown, doubling every ~4.7 units
|
|
55
|
+
* - NO "disabled" threshold - even |FADE|=63 continues fading, just very slowly
|
|
56
|
+
*
|
|
57
|
+
* Measured values:
|
|
58
|
+
* |FADE| = 4: ~1 cycle
|
|
59
|
+
* |FADE| = 8: ~1.6 cycles
|
|
60
|
+
* |FADE| = 16: ~2.2 cycles
|
|
61
|
+
* |FADE| = 24: ~7 cycles
|
|
62
|
+
* |FADE| = 32: ~26 cycles
|
|
63
|
+
* |FADE| = 40: ~90 cycles
|
|
64
|
+
* |FADE| = 48: ~320 cycles
|
|
65
|
+
* |FADE| = 56: ~1100 cycles
|
|
66
|
+
* |FADE| = 63: ~3300 cycles
|
|
58
67
|
*
|
|
59
68
|
* IMPORTANT: Higher |FADE| = SLOWER fade (opposite of what you might expect)
|
|
60
69
|
*/
|
|
@@ -63,22 +72,16 @@ export function calculateFadeCycles(fadeValue: number): number {
|
|
|
63
72
|
|
|
64
73
|
const absFade = Math.abs(fadeValue);
|
|
65
74
|
|
|
66
|
-
// |FADE|
|
|
67
|
-
|
|
68
|
-
return Infinity;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Fast fade region: roughly linear
|
|
75
|
+
// Linear region (|FADE| <= 16): ~1 cycle at FADE=4, ~2.2 cycles at FADE=16
|
|
76
|
+
// Formula: 0.1 * |FADE| + 0.6, with minimum of 0.5
|
|
72
77
|
if (absFade <= 16) {
|
|
73
|
-
|
|
74
|
-
return Math.max(0.5, absFade / 6);
|
|
78
|
+
return Math.max(0.5, 0.1 * absFade + 0.6);
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
const baseAt16 =
|
|
80
|
-
return baseAt16 * Math.pow(2, (absFade - 16) / 5);
|
|
81
|
-
// |FADE|=32: 2.67 * 2^3.2 ≈ 24.5 cycles (matches hardware)
|
|
81
|
+
// Exponential region (|FADE| > 16): starts at 2.2 cycles at FADE=16
|
|
82
|
+
// Doubles every ~4.5 units of |FADE|
|
|
83
|
+
const baseAt16 = 2.2;
|
|
84
|
+
return baseAt16 * Math.pow(2, (absFade - 16) / 4.5);
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/**
|
|
@@ -107,14 +110,8 @@ export function updateFade(
|
|
|
107
110
|
// Calculate how many cycles the fade takes
|
|
108
111
|
const fadeCycles = calculateFadeCycles(config.fade);
|
|
109
112
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
if (!isFinite(fadeCycles)) {
|
|
113
|
-
return {
|
|
114
|
-
fadeProgress: 0,
|
|
115
|
-
fadeMultiplier: 0, // No modulation when fade is disabled
|
|
116
|
-
};
|
|
117
|
-
}
|
|
113
|
+
// Note: There is no "disabled" threshold - even extreme fade values
|
|
114
|
+
// just result in very slow fades (thousands of cycles for |FADE|=63)
|
|
118
115
|
|
|
119
116
|
const fadeDurationMs = fadeCycles * cycleTimeMs;
|
|
120
117
|
|
|
@@ -137,19 +134,15 @@ export function updateFade(
|
|
|
137
134
|
|
|
138
135
|
/**
|
|
139
136
|
* Reset fade state (called on trigger for modes that reset fade)
|
|
137
|
+
*
|
|
138
|
+
* Note: There is no "disabled" threshold - even extreme fade values
|
|
139
|
+
* will eventually complete, just over thousands of cycles.
|
|
140
140
|
*/
|
|
141
141
|
export function resetFade(config: LFOConfig): { fadeProgress: number; fadeMultiplier: number } {
|
|
142
142
|
if (config.fade === 0) {
|
|
143
143
|
return { fadeProgress: 1, fadeMultiplier: 1 };
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
// Check if fade is disabled (|FADE| >= 48)
|
|
147
|
-
const fadeCycles = calculateFadeCycles(config.fade);
|
|
148
|
-
if (!isFinite(fadeCycles)) {
|
|
149
|
-
// Disabled fade = no modulation
|
|
150
|
-
return { fadeProgress: 0, fadeMultiplier: 0 };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
146
|
if (config.fade < 0) {
|
|
154
147
|
// Fade IN: start at 0
|
|
155
148
|
return { fadeProgress: 0, fadeMultiplier: 0 };
|
package/tests/depth-fade.test.ts
CHANGED
|
@@ -133,28 +133,53 @@ describe('calculateFadeCycles', () => {
|
|
|
133
133
|
expect(calculateFadeCycles(0)).toBe(0);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
test('calculates cycles based on fade value (empirical formula)', () => {
|
|
137
|
-
// Based on Digitakt II hardware testing:
|
|
136
|
+
test('calculates cycles based on fade value (empirical formula from Digitakt II)', () => {
|
|
137
|
+
// Based on Digitakt II hardware testing (January 2025):
|
|
138
138
|
// - Higher |FADE| = SLOWER fade (more cycles)
|
|
139
|
-
// -
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
expect(calculateFadeCycles(4)).toBeCloseTo(0
|
|
143
|
-
expect(calculateFadeCycles(-4)).toBeCloseTo(0
|
|
144
|
-
expect(calculateFadeCycles(8)).toBeCloseTo(1.
|
|
145
|
-
expect(calculateFadeCycles(-8)).toBeCloseTo(1.
|
|
146
|
-
expect(calculateFadeCycles(16)).toBeCloseTo(2.
|
|
147
|
-
expect(calculateFadeCycles(-16)).toBeCloseTo(2.
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
expect(calculateFadeCycles(
|
|
151
|
-
expect(calculateFadeCycles(-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
139
|
+
// - NO disabled threshold - even extreme values fade, just slowly
|
|
140
|
+
|
|
141
|
+
// Linear region (|FADE| <= 16): 0.1 * |FADE| + 0.6
|
|
142
|
+
expect(calculateFadeCycles(4)).toBeCloseTo(1.0, 1); // ~1 cycle
|
|
143
|
+
expect(calculateFadeCycles(-4)).toBeCloseTo(1.0, 1);
|
|
144
|
+
expect(calculateFadeCycles(8)).toBeCloseTo(1.4, 1); // ~1.4 cycles
|
|
145
|
+
expect(calculateFadeCycles(-8)).toBeCloseTo(1.4, 1);
|
|
146
|
+
expect(calculateFadeCycles(16)).toBeCloseTo(2.2, 1); // ~2.2 cycles
|
|
147
|
+
expect(calculateFadeCycles(-16)).toBeCloseTo(2.2, 1);
|
|
148
|
+
|
|
149
|
+
// Exponential region (|FADE| > 16): 2.2 * 2^((|FADE| - 16) / 4.5)
|
|
150
|
+
expect(calculateFadeCycles(24)).toBeCloseTo(7.5, 0); // ~7-8 cycles
|
|
151
|
+
expect(calculateFadeCycles(-24)).toBeCloseTo(7.5, 0);
|
|
152
|
+
expect(calculateFadeCycles(32)).toBeCloseTo(26, 0); // ~26 cycles
|
|
153
|
+
expect(calculateFadeCycles(-32)).toBeCloseTo(26, 0);
|
|
154
|
+
|
|
155
|
+
// Extreme values - NOT disabled, just very slow
|
|
156
|
+
// Using loose precision since exact values are less critical at extremes
|
|
157
|
+
const fade40 = calculateFadeCycles(40);
|
|
158
|
+
const fade48 = calculateFadeCycles(48);
|
|
159
|
+
const fade56 = calculateFadeCycles(56);
|
|
160
|
+
const fade63 = calculateFadeCycles(63);
|
|
161
|
+
|
|
162
|
+
expect(fade40).toBeGreaterThan(70);
|
|
163
|
+
expect(fade40).toBeLessThan(120); // ~90 cycles
|
|
164
|
+
expect(fade48).toBeGreaterThan(250);
|
|
165
|
+
expect(fade48).toBeLessThan(400); // ~300 cycles
|
|
166
|
+
expect(fade56).toBeGreaterThan(800);
|
|
167
|
+
expect(fade56).toBeLessThan(1400); // ~1000 cycles
|
|
168
|
+
expect(fade63).toBeGreaterThan(2500);
|
|
169
|
+
expect(fade63).toBeLessThan(4000); // ~3000 cycles
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('is symmetric for positive and negative fade values', () => {
|
|
173
|
+
expect(calculateFadeCycles(-16)).toBe(calculateFadeCycles(16));
|
|
174
|
+
expect(calculateFadeCycles(-32)).toBe(calculateFadeCycles(32));
|
|
175
|
+
expect(calculateFadeCycles(-48)).toBe(calculateFadeCycles(48));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('extreme fade values return finite (large) cycle counts', () => {
|
|
179
|
+
// Verify no Infinity values - Digitakt continues fading even at extremes
|
|
180
|
+
expect(isFinite(calculateFadeCycles(48))).toBe(true);
|
|
181
|
+
expect(isFinite(calculateFadeCycles(63))).toBe(true);
|
|
182
|
+
expect(isFinite(calculateFadeCycles(-64))).toBe(true);
|
|
158
183
|
});
|
|
159
184
|
});
|
|
160
185
|
|
|
@@ -178,27 +203,29 @@ describe('updateFade', () => {
|
|
|
178
203
|
});
|
|
179
204
|
|
|
180
205
|
test('progresses fade over time', () => {
|
|
181
|
-
// fade=-16 takes ~2.
|
|
206
|
+
// fade=-16 takes ~2.2 cycles (based on new empirical formula)
|
|
182
207
|
const config = createConfig({ fade: -16, mode: 'TRG' });
|
|
183
208
|
const state = createState({ fadeProgress: 0 });
|
|
184
209
|
const cycleTimeMs = 1000;
|
|
185
210
|
|
|
186
|
-
// Fade duration = ~2.
|
|
187
|
-
// After 1000ms (~
|
|
211
|
+
// Fade duration = ~2.2 cycles * 1000ms = ~2200ms
|
|
212
|
+
// After 1000ms (~45% of fade), progress should be ~0.45
|
|
188
213
|
const result = updateFade(config, state, cycleTimeMs, 1000);
|
|
189
|
-
expect(result.fadeProgress).toBeCloseTo(0.
|
|
190
|
-
expect(result.fadeMultiplier).toBeCloseTo(0.
|
|
214
|
+
expect(result.fadeProgress).toBeCloseTo(0.45, 1);
|
|
215
|
+
expect(result.fadeMultiplier).toBeCloseTo(0.45, 1);
|
|
191
216
|
});
|
|
192
217
|
|
|
193
|
-
test('
|
|
194
|
-
// fade=-64
|
|
218
|
+
test('extreme fade values still progress, just very slowly', () => {
|
|
219
|
+
// fade=-64 takes ~3500+ cycles - NOT disabled, just very slow
|
|
195
220
|
const config = createConfig({ fade: -64, mode: 'TRG' });
|
|
196
221
|
const state = createState({ fadeProgress: 0 });
|
|
197
|
-
const cycleTimeMs =
|
|
222
|
+
const cycleTimeMs = 1000;
|
|
198
223
|
|
|
224
|
+
// After 1000ms (1 cycle), progress should be 1/3500 = ~0.0003
|
|
199
225
|
const result = updateFade(config, state, cycleTimeMs, 1000);
|
|
200
|
-
expect(result.fadeProgress).
|
|
201
|
-
expect(result.
|
|
226
|
+
expect(result.fadeProgress).toBeGreaterThan(0);
|
|
227
|
+
expect(result.fadeProgress).toBeLessThan(0.01);
|
|
228
|
+
expect(result.fadeMultiplier).toBeGreaterThan(0);
|
|
202
229
|
});
|
|
203
230
|
});
|
|
204
231
|
|
|
@@ -252,7 +279,7 @@ describe('Fade with LFO integration', () => {
|
|
|
252
279
|
const lfo = new LFO({
|
|
253
280
|
waveform: 'SIN',
|
|
254
281
|
depth: 63,
|
|
255
|
-
fade: -16, // Fade in over ~2.
|
|
282
|
+
fade: -16, // Fade in over ~2.2 cycles
|
|
256
283
|
mode: 'TRG',
|
|
257
284
|
}, 120);
|
|
258
285
|
|
|
@@ -262,7 +289,7 @@ describe('Fade with LFO integration', () => {
|
|
|
262
289
|
const state1 = lfo.update(100);
|
|
263
290
|
expect(Math.abs(state1.output)).toBeLessThan(0.2); // Start near 0
|
|
264
291
|
|
|
265
|
-
// Fade takes ~2.
|
|
292
|
+
// Fade takes ~2.2 cycles = ~4400ms at 2000ms/cycle
|
|
266
293
|
// After more time, output should increase significantly
|
|
267
294
|
let maxOutput = 0;
|
|
268
295
|
for (let t = 100; t < 6000; t += 100) {
|
|
@@ -272,31 +299,34 @@ describe('Fade with LFO integration', () => {
|
|
|
272
299
|
expect(maxOutput).toBeGreaterThan(0.5);
|
|
273
300
|
});
|
|
274
301
|
|
|
275
|
-
test('
|
|
302
|
+
test('extreme fade values progress very slowly but are not disabled', () => {
|
|
276
303
|
const lfo = new LFO({
|
|
277
304
|
waveform: 'SIN',
|
|
278
305
|
depth: 63,
|
|
279
|
-
fade: -64, //
|
|
306
|
+
fade: -64, // Very slow fade (~3500 cycles), NOT disabled
|
|
280
307
|
mode: 'TRG',
|
|
281
308
|
}, 120);
|
|
282
309
|
|
|
283
310
|
lfo.trigger();
|
|
284
311
|
lfo.update(0);
|
|
285
312
|
|
|
286
|
-
// With
|
|
313
|
+
// With extreme fade, output starts near 0 and increases VERY slowly
|
|
314
|
+
// After 10 seconds (~5 cycles), fade is only 5/3500 = 0.14% complete
|
|
287
315
|
let maxOutput = 0;
|
|
288
316
|
for (let t = 100; t < 10000; t += 100) {
|
|
289
317
|
const state = lfo.update(t);
|
|
290
318
|
maxOutput = Math.max(maxOutput, Math.abs(state.output));
|
|
291
319
|
}
|
|
292
|
-
|
|
320
|
+
// Should have some tiny output (not zero) since fade is progressing
|
|
321
|
+
expect(maxOutput).toBeGreaterThan(0);
|
|
322
|
+
expect(maxOutput).toBeLessThan(0.05); // But still very small
|
|
293
323
|
});
|
|
294
324
|
|
|
295
325
|
test('fade out starts at full output and decreases', () => {
|
|
296
326
|
const lfo = new LFO({
|
|
297
327
|
waveform: 'SIN',
|
|
298
328
|
depth: 63,
|
|
299
|
-
fade: 16, // Fade out over ~2.
|
|
329
|
+
fade: 16, // Fade out over ~2.2 cycles
|
|
300
330
|
mode: 'TRG',
|
|
301
331
|
startPhase: 32, // Start at peak
|
|
302
332
|
}, 120);
|
|
@@ -308,7 +338,7 @@ describe('Fade with LFO integration', () => {
|
|
|
308
338
|
// Should start near full output (at SIN peak)
|
|
309
339
|
expect(Math.abs(state1.output)).toBeGreaterThan(0.8);
|
|
310
340
|
|
|
311
|
-
// Fade takes ~2.
|
|
341
|
+
// Fade takes ~2.2 cycles = ~4400ms at 2000ms/cycle
|
|
312
342
|
// After fade completes, output should be near 0
|
|
313
343
|
let lastOutput = 0;
|
|
314
344
|
for (let t = 10; t < 6000; t += 100) {
|