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 +4 -25
- package/package.json +1 -1
- package/src/engine/fade.ts +29 -36
- package/src/engine/triggers.ts +10 -16
- package/tests/depth-fade.test.ts +69 -39
- package/tests/triggers.test.ts +15 -37
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
|
-
|
|
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
|
|
342
|
+
return Math.max(0.5, 0.1 * absFade + 0.6);
|
|
354
343
|
}
|
|
355
|
-
const baseAt16 =
|
|
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
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/src/engine/triggers.ts
CHANGED
|
@@ -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
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
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
|
-
|
|
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)
|
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) {
|
package/tests/triggers.test.ts
CHANGED
|
@@ -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,
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
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,
|
|
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
|
|
261
|
-
expect(
|
|
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),
|
|
274
|
-
//
|
|
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,
|
|
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(
|
|
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
|
|