elektron-lfo 1.0.11 → 1.0.13

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
@@ -283,15 +283,7 @@ function checkModeStop(config, state, previousPhase, currentPhase) {
283
283
  const isForward = config.speed >= 0;
284
284
  if (config.mode === "ONE") {
285
285
  if (state.cycleCount >= 1) {
286
- if (isForward) {
287
- if (startPhase === 0 || currentPhase >= startPhase) {
288
- return { shouldStop: true, cycleCompleted: true };
289
- }
290
- } else {
291
- if (startPhase === 0 || currentPhase <= startPhase) {
292
- return { shouldStop: true, cycleCompleted: true };
293
- }
294
- }
286
+ return { shouldStop: true, cycleCompleted: true };
295
287
  }
296
288
  } else if (config.mode === "HLF") {
297
289
  const halfPhase = (startPhase + 0.5) % 1;
@@ -346,14 +338,11 @@ function calculateFadeCycles(fadeValue) {
346
338
  if (fadeValue === 0)
347
339
  return 0;
348
340
  const absFade = Math.abs(fadeValue);
349
- if (absFade >= 48) {
350
- return Infinity;
351
- }
352
341
  if (absFade <= 16) {
353
- return Math.max(0.5, absFade / 6);
342
+ return Math.max(0.5, 0.1 * absFade + 0.6);
354
343
  }
355
- const baseAt16 = 16 / 6;
356
- return baseAt16 * Math.pow(2, (absFade - 16) / 5);
344
+ const baseAt16 = 2.2;
345
+ return baseAt16 * Math.pow(2, (absFade - 16) / 4.5);
357
346
  }
358
347
  function updateFade(config, state, cycleTimeMs, deltaMs) {
359
348
  if (config.fade === 0 || config.mode === "FRE") {
@@ -363,12 +352,6 @@ function updateFade(config, state, cycleTimeMs, deltaMs) {
363
352
  };
364
353
  }
365
354
  const fadeCycles = calculateFadeCycles(config.fade);
366
- if (!isFinite(fadeCycles)) {
367
- return {
368
- fadeProgress: 0,
369
- fadeMultiplier: 0
370
- };
371
- }
372
355
  const fadeDurationMs = fadeCycles * cycleTimeMs;
373
356
  if (fadeDurationMs === 0) {
374
357
  return {
@@ -387,10 +370,6 @@ function resetFade(config) {
387
370
  if (config.fade === 0) {
388
371
  return { fadeProgress: 1, fadeMultiplier: 1 };
389
372
  }
390
- const fadeCycles = calculateFadeCycles(config.fade);
391
- if (!isFinite(fadeCycles)) {
392
- return { fadeProgress: 0, fadeMultiplier: 0 };
393
- }
394
373
  if (config.fade < 0) {
395
374
  return { fadeProgress: 0, fadeMultiplier: 0 };
396
375
  } 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.13",
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 };
@@ -112,23 +112,17 @@ export function checkModeStop(
112
112
  const isForward = config.speed >= 0;
113
113
 
114
114
  if (config.mode === 'ONE') {
115
- // ONE mode: Stop after completing one full cycle back to startPhase
116
- // For non-zero startPhase, we need to:
117
- // 1. Wait for phase to wrap (cycleCount >= 1)
118
- // 2. Then continue until we reach/pass the startPhase again
115
+ // ONE mode: Stop immediately when phase completes one wrap (cycleCount >= 1)
116
+ // Based on Digitakt II hardware testing (January 2025):
117
+ // - Phase runs from startPhase until it wraps (crosses 1.0→0.0 or 0.0→1.0)
118
+ // - Stops immediately on wrap, does NOT continue back to startPhase
119
+ // - This means non-zero startPhase values result in partial amplitude coverage:
120
+ // - Phase=0: full amplitude range (0→1→0, complete waveform)
121
+ // - Phase=32: full amplitude range (0.25→1→0, starts at peak)
122
+ // - Phase=64: half amplitude range (0.5→1→0, starts at middle)
123
+ // - Phase=96: half amplitude range (0.75→1→0, starts at trough)
119
124
  if (state.cycleCount >= 1) {
120
- if (isForward) {
121
- // Forward: stop when current phase reaches or passes startPhase after wrapping
122
- // Handle the case where startPhase is 0 (stop immediately on wrap)
123
- if (startPhase === 0 || currentPhase >= startPhase) {
124
- return { shouldStop: true, cycleCompleted: true };
125
- }
126
- } else {
127
- // Backward: stop when current phase reaches or goes below startPhase after wrapping
128
- if (startPhase === 0 || currentPhase <= startPhase) {
129
- return { shouldStop: true, cycleCompleted: true };
130
- }
131
- }
125
+ return { shouldStop: true, cycleCompleted: true };
132
126
  }
133
127
  } else if (config.mode === 'HLF') {
134
128
  // HLF mode: Stop after half cycle (0.5 phase distance from start)
@@ -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) {
@@ -241,62 +241,40 @@ describe('checkModeStop - ONE mode', () => {
241
241
  expect(result.shouldStop).toBe(true);
242
242
  });
243
243
 
244
- test('with non-zero startPhase, does NOT stop immediately after wrap - must reach startPhase', () => {
245
- // Bug fix: With startPhase 53 (~0.414), the LFO should:
246
- // 1. Start at 0.414
247
- // 2. Run to 1.0, wrap to 0.0 (cycleCount = 1)
248
- // 3. Continue from 0.0 to 0.414 (startPhase)
249
- // 4. THEN stop
244
+ test('with non-zero startPhase, stops immediately on wrap (Digitakt II verified)', () => {
245
+ // Hardware verified (January 2025): ONE mode stops immediately when phase wraps,
246
+ // regardless of startPhase. It does NOT continue back to startPhase.
247
+ // This means non-zero startPhase results in partial amplitude coverage:
248
+ // - Phase=64 (0.5): only covers half the waveform amplitude
249
+ // - Phase=96 (0.75): only covers quarter of phase distance
250
250
  const config = createConfig({ mode: 'ONE', speed: 16, startPhase: 53 });
251
251
  const startPhaseNormalized = 53 / 128; // ~0.414
252
252
 
253
- // After wrap, phase is 0.1, which is BEFORE startPhase (0.414)
254
- // Should NOT stop yet
253
+ // After wrap, cycleCount = 1, should stop IMMEDIATELY
255
254
  const stateAfterWrap = createState({
256
255
  hasTriggered: true,
257
256
  startPhaseNormalized,
258
257
  cycleCount: 1,
259
258
  });
260
- const resultBeforeStart = checkModeStop(config, stateAfterWrap, 0.05, 0.1);
261
- expect(resultBeforeStart.shouldStop).toBe(false);
262
-
263
- // Phase reaches 0.3, still before startPhase - should NOT stop
264
- const resultStillBefore = checkModeStop(config, stateAfterWrap, 0.2, 0.3);
265
- expect(resultStillBefore.shouldStop).toBe(false);
266
-
267
- // Phase crosses startPhase (0.414) - NOW should stop
268
- const resultAtStart = checkModeStop(config, stateAfterWrap, 0.4, 0.45);
269
- expect(resultAtStart.shouldStop).toBe(true);
270
- expect(resultAtStart.cycleCompleted).toBe(true);
259
+ const resultAfterWrap = checkModeStop(config, stateAfterWrap, 0.05, 0.1);
260
+ expect(resultAfterWrap.shouldStop).toBe(true);
261
+ expect(resultAfterWrap.cycleCompleted).toBe(true);
271
262
  });
272
263
 
273
- test('with non-zero startPhase (backward), continues after wrap until reaching startPhase', () => {
274
- // Backward direction with startPhase 53 (~0.414):
275
- // 1. Start at 0.414
276
- // 2. Run backward to 0.0, wrap to 1.0 (cycleCount = 1)
277
- // 3. Continue from 1.0 down to 0.414
278
- // 4. THEN stop
264
+ test('with non-zero startPhase (backward), stops immediately on wrap (Digitakt II verified)', () => {
265
+ // Hardware verified: backward direction also stops immediately on wrap
279
266
  const config = createConfig({ mode: 'ONE', speed: -16, startPhase: 53 });
280
267
  const startPhaseNormalized = 53 / 128; // ~0.414
281
268
 
282
- // After wrap going backward, phase is 0.9, which is ABOVE startPhase (0.414)
283
- // Should NOT stop yet
269
+ // After wrap going backward, cycleCount = 1, should stop IMMEDIATELY
284
270
  const stateAfterWrap = createState({
285
271
  hasTriggered: true,
286
272
  startPhaseNormalized,
287
273
  cycleCount: 1,
288
274
  });
289
275
  const resultAfterWrap = checkModeStop(config, stateAfterWrap, 0.95, 0.9);
290
- expect(resultAfterWrap.shouldStop).toBe(false);
291
-
292
- // Phase reaches 0.5, still above startPhase - should NOT stop
293
- const resultStillAbove = checkModeStop(config, stateAfterWrap, 0.6, 0.5);
294
- expect(resultStillAbove.shouldStop).toBe(false);
295
-
296
- // Phase crosses startPhase (0.414) going down - NOW should stop
297
- const resultAtStart = checkModeStop(config, stateAfterWrap, 0.45, 0.4);
298
- expect(resultAtStart.shouldStop).toBe(true);
299
- expect(resultAtStart.cycleCompleted).toBe(true);
276
+ expect(resultAfterWrap.shouldStop).toBe(true);
277
+ expect(resultAfterWrap.cycleCompleted).toBe(true);
300
278
  });
301
279
  });
302
280