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 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 / 6);
350
+ return Math.max(0.5, 0.1 * absFade + 0.6);
354
351
  }
355
- const baseAt16 = 16 / 6;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elektron-lfo",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Elektron LFO engine simulator implementation with CLI visualization",
5
5
  "main": "dist/index.js",
6
6
  "module": "src/index.ts",
@@ -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
- * - |FADE| <= 16: Fast fade, roughly linear (~|FADE|/6 cycles)
52
- * - |FADE| = 4: ~1 cycle (nearly instant)
53
- * - |FADE| = 8: ~1.5 cycles
54
- * - |FADE| = 16: ~2.5 cycles
55
- * - |FADE| > 16: Exponential slowdown
56
- * - |FADE| = 32: ~25 cycles
57
- * - |FADE| >= 48: Effectively disabled (infinitely slow)
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| >= 48 is effectively disabled (infinitely slow fade)
67
- if (absFade >= 48) {
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
- // |FADE|=4 → ~0.7 cycles, |FADE|=16 ~2.7 cycles
74
- return Math.max(0.5, absFade / 6);
78
+ return Math.max(0.5, 0.1 * absFade + 0.6);
75
79
  }
76
80
 
77
- // Slow fade region: exponential growth
78
- // Base of ~2.7 cycles at |FADE|=16, doubling every 5 units
79
- const baseAt16 = 16 / 6; // ~2.67 cycles
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
- // Handle disabled fade (|FADE| >= 48)
111
- // Both fade-in and fade-out with high values result in no modulation
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 };
@@ -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
- // - |FADE| >= 48 is disabled (Infinity)
140
-
141
- // Fast fade region (|FADE| <= 16): ~|FADE|/6 cycles
142
- expect(calculateFadeCycles(4)).toBeCloseTo(0.67, 1); // ~0.67 cycles
143
- expect(calculateFadeCycles(-4)).toBeCloseTo(0.67, 1);
144
- expect(calculateFadeCycles(8)).toBeCloseTo(1.33, 1); // ~1.33 cycles
145
- expect(calculateFadeCycles(-8)).toBeCloseTo(1.33, 1);
146
- expect(calculateFadeCycles(16)).toBeCloseTo(2.67, 1); // ~2.67 cycles
147
- expect(calculateFadeCycles(-16)).toBeCloseTo(2.67, 1);
148
-
149
- // Slow fade region (16 < |FADE| < 48): exponential
150
- expect(calculateFadeCycles(32)).toBeCloseTo(24.5, 0); // ~24.5 cycles
151
- expect(calculateFadeCycles(-32)).toBeCloseTo(24.5, 0);
152
-
153
- // Disabled region (|FADE| >= 48)
154
- expect(calculateFadeCycles(48)).toBe(Infinity);
155
- expect(calculateFadeCycles(-48)).toBe(Infinity);
156
- expect(calculateFadeCycles(64)).toBe(Infinity);
157
- expect(calculateFadeCycles(-64)).toBe(Infinity);
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.67 cycles (based on new formula)
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.67 cycles * 1000ms = ~2670ms
187
- // After 1000ms (~37.5% of fade), progress should be ~0.375
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.375, 1);
190
- expect(result.fadeMultiplier).toBeCloseTo(0.375, 1);
214
+ expect(result.fadeProgress).toBeCloseTo(0.45, 1);
215
+ expect(result.fadeMultiplier).toBeCloseTo(0.45, 1);
191
216
  });
192
217
 
193
- test('returns 0 multiplier for disabled fade', () => {
194
- // fade=-64 is disabled (|FADE| >= 48)
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 = 2000;
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).toBe(0);
201
- expect(result.fadeMultiplier).toBe(0); // No modulation when disabled
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.67 cycles
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.67 cycles = ~5340ms at 2000ms/cycle
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('disabled fade returns no modulation', () => {
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, // Disabled (|FADE| >= 48)
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 disabled fade, output should stay at 0 (no modulation)
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
- expect(maxOutput).toBe(0);
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.67 cycles
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.67 cycles = ~5340ms at 2000ms/cycle
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) {