elektron-lfo 1.0.3 → 1.0.6

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.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Command-line argument parsing for Elektron LFO CLI
3
+ */
4
+ import type { LFOConfig } from '../engine/types';
5
+ export interface CLIArgs extends LFOConfig {
6
+ bpm: number;
7
+ help: boolean;
8
+ }
9
+ export declare function printHelp(): void;
10
+ export declare function parseArgs(args: string[]): CLIArgs;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Terminal display for Elektron LFO CLI
3
+ *
4
+ * Renders the LFO state with ASCII waveform visualization
5
+ */
6
+ import type { LFOConfig, LFOState, TimingInfo } from '../engine/types';
7
+ /**
8
+ * Render the full display
9
+ */
10
+ export declare function render(config: LFOConfig, state: LFOState, timing: TimingInfo, bpm: number): string;
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Elektron LFO CLI - Main Entry Point
4
+ *
5
+ * Real-time terminal visualization of the Digitakt II LFO engine
6
+ */
7
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Keyboard input handling for Elektron LFO CLI
3
+ */
4
+ import type { Waveform, TriggerMode, Multiplier } from '../engine/types';
5
+ export type KeyAction = {
6
+ type: 'trigger';
7
+ } | {
8
+ type: 'quit';
9
+ } | {
10
+ type: 'bpm';
11
+ delta: number;
12
+ } | {
13
+ type: 'speed';
14
+ delta: number;
15
+ } | {
16
+ type: 'waveform';
17
+ waveform: Waveform;
18
+ } | {
19
+ type: 'mode';
20
+ mode: TriggerMode;
21
+ } | {
22
+ type: 'multiplier';
23
+ multiplier: Multiplier;
24
+ } | {
25
+ type: 'depth';
26
+ delta: number;
27
+ } | {
28
+ type: 'fade';
29
+ delta: number;
30
+ } | {
31
+ type: 'unknown';
32
+ };
33
+ /**
34
+ * Parse a key input into an action
35
+ */
36
+ export declare function parseKey(key: Buffer, currentWaveform: Waveform, currentMode: TriggerMode, currentMultiplier: Multiplier): KeyAction;
37
+ /**
38
+ * Set up raw mode input handling
39
+ */
40
+ export declare function setupKeyboardInput(onKey: (key: Buffer) => void, onExit: () => void): void;
41
+ /**
42
+ * Clean up keyboard input handling
43
+ */
44
+ export declare function cleanupKeyboardInput(): void;
@@ -73,4 +73,11 @@ export declare class LFO {
73
73
  * Stop the LFO
74
74
  */
75
75
  stop(): void;
76
+ /**
77
+ * Reset timing so the next update() call has deltaMs = 0
78
+ *
79
+ * Call this after trigger() when restarting from a paused state
80
+ * to prevent the LFO from jumping ahead by the paused duration.
81
+ */
82
+ resetTiming(): void;
76
83
  }
@@ -23,7 +23,7 @@ export declare function generateSine(phase: number): number;
23
23
  export declare function generateSquare(phase: number): number;
24
24
  /**
25
25
  * Sawtooth waveform - Bipolar
26
- * Linear rise from -1 to +1
26
+ * Linear fall from +1 to -1 (with positive depth)
27
27
  */
28
28
  export declare function generateSawtooth(phase: number): number;
29
29
  /**
@@ -33,7 +33,7 @@ export declare function generateSawtooth(phase: number): number;
33
33
  export declare function generateExponential(phase: number): number;
34
34
  /**
35
35
  * Ramp waveform - Unipolar (0 to +1)
36
- * Linear fall from +1 to 0
36
+ * Linear rise from 0 to +1
37
37
  */
38
38
  export declare function generateRamp(phase: number): number;
39
39
  /**
package/dist/index.js CHANGED
@@ -66,14 +66,15 @@ function generateSquare(phase) {
66
66
  return phase < 0.5 ? 1 : -1;
67
67
  }
68
68
  function generateSawtooth(phase) {
69
- return phase * 2 - 1;
69
+ return 1 - phase * 2;
70
70
  }
71
71
  function generateExponential(phase) {
72
- const k = 4;
73
- return (Math.exp(phase * k) - 1) / (Math.exp(k) - 1);
72
+ const k = 3;
73
+ const expK = Math.exp(-k);
74
+ return (Math.exp(-phase * k) - expK) / (1 - expK);
74
75
  }
75
76
  function generateRamp(phase) {
76
- return 1 - phase;
77
+ return phase;
77
78
  }
78
79
  function generateRandom(phase, state) {
79
80
  const stepsPerCycle = 16;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elektron-lfo",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "Elektron LFO engine simulator implementation with CLI visualization",
5
5
  "main": "dist/index.js",
6
6
  "module": "src/index.ts",
package/src/cli/index.ts CHANGED
File without changes
@@ -40,27 +40,32 @@ export function generateSquare(phase: number): number {
40
40
 
41
41
  /**
42
42
  * Sawtooth waveform - Bipolar
43
- * Linear rise from -1 to +1
43
+ * Linear fall from +1 to -1 (with positive depth)
44
44
  */
45
45
  export function generateSawtooth(phase: number): number {
46
- return phase * 2 - 1;
46
+ return 1 - phase * 2;
47
47
  }
48
48
 
49
49
  /**
50
50
  * Exponential waveform - Unipolar (0 to +1)
51
- * Accelerating curve from 0 to 1
51
+ * Decaying curve from 1 to 0 (matches Digitakt II behavior)
52
+ * Fast initial decay, slowing toward the end (true exponential decay shape)
52
53
  */
53
54
  export function generateExponential(phase: number): number {
54
- const k = 4; // Steepness factor
55
- return (Math.exp(phase * k) - 1) / (Math.exp(k) - 1);
55
+ const k = 3; // Decay rate - controls steepness of decay
56
+ // True exponential decay: starts at 1, decays rapidly then slows
57
+ // Formula: (exp(-phase * k) - exp(-k)) / (1 - exp(-k))
58
+ // This normalizes to exactly [1, 0] range
59
+ const expK = Math.exp(-k);
60
+ return (Math.exp(-phase * k) - expK) / (1 - expK);
56
61
  }
57
62
 
58
63
  /**
59
64
  * Ramp waveform - Unipolar (0 to +1)
60
- * Linear fall from +1 to 0
65
+ * Linear rise from 0 to +1
61
66
  */
62
67
  export function generateRamp(phase: number): number {
63
- return 1 - phase;
68
+ return phase;
64
69
  }
65
70
 
66
71
  /**
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Expectation-based tests for EXP and RMP waveforms.
3
+ *
4
+ * Based on Digitakt II hardware verification, these waveforms should behave as:
5
+ *
6
+ * EXP (Exponential):
7
+ * - DECAY curve: starts at 1 (peak), decays toward 0 (center)
8
+ * - Unipolar output range: 0 to 1
9
+ * - With positive depth: CC goes from (64+depth) down to 64
10
+ * - Shape: Fast initial decay, slowing toward the end
11
+ *
12
+ * RMP (Ramp):
13
+ * - RISE curve: starts at 0 (center), rises toward 1 (peak)
14
+ * - Unipolar output range: 0 to 1
15
+ * - With positive depth: CC goes from 64 up to (64+depth)
16
+ * - Shape: Linear rise
17
+ */
18
+
19
+ import { describe, test, expect } from 'bun:test';
20
+ import { generateExponential, generateRamp } from '../src/engine/waveforms';
21
+ import { LFO } from '../src/engine/lfo';
22
+
23
+ // Helper to convert LFO output to CC value
24
+ const outputToCC = (output: number): number => {
25
+ return Math.max(0, Math.min(127, Math.round(64 + output * 63)));
26
+ };
27
+
28
+ describe('EXP Waveform - Expected Behavior', () => {
29
+ describe('raw waveform generator', () => {
30
+ test('should return 1.0 at phase 0 (starts at peak)', () => {
31
+ expect(generateExponential(0)).toBeCloseTo(1.0, 2);
32
+ });
33
+
34
+ test('should return value < 0.5 at phase 0.5 (decaying)', () => {
35
+ const value = generateExponential(0.5);
36
+ expect(value).toBeLessThan(0.5);
37
+ expect(value).toBeGreaterThan(0);
38
+ });
39
+
40
+ test('should return value near 0 at phase 1 (decayed to center)', () => {
41
+ expect(generateExponential(1)).toBeLessThan(0.1);
42
+ });
43
+
44
+ test('should monotonically decrease from phase 0 to 1', () => {
45
+ const phases = [0, 0.2, 0.4, 0.6, 0.8, 1.0];
46
+ const values = phases.map(p => generateExponential(p));
47
+
48
+ for (let i = 1; i < values.length; i++) {
49
+ expect(values[i]).toBeLessThan(values[i - 1]);
50
+ }
51
+ });
52
+
53
+ test('should be unipolar (output range 0 to 1)', () => {
54
+ for (let i = 0; i <= 100; i++) {
55
+ const value = generateExponential(i / 100);
56
+ expect(value).toBeGreaterThanOrEqual(0);
57
+ expect(value).toBeLessThanOrEqual(1);
58
+ }
59
+ });
60
+ });
61
+
62
+ describe('full LFO with depth=40', () => {
63
+ const BPM = 120;
64
+ const DEPTH = 40;
65
+
66
+ test('should produce CC near 104 at phase 0 (start at peak)', () => {
67
+ const lfo = new LFO({
68
+ waveform: 'EXP',
69
+ speed: 16,
70
+ multiplier: 4,
71
+ depth: DEPTH,
72
+ startPhase: 0,
73
+ mode: 'TRG',
74
+ }, BPM);
75
+
76
+ lfo.trigger();
77
+ const state = lfo.update(1);
78
+ const cc = outputToCC(state.output);
79
+
80
+ // At phase 0, EXP should be at peak: CC = 64 + 40 = 104
81
+ expect(cc).toBeGreaterThan(100);
82
+ expect(cc).toBeLessThanOrEqual(104);
83
+ });
84
+
85
+ test('should produce CC near 64 at phase 1 (end at center)', () => {
86
+ const lfo = new LFO({
87
+ waveform: 'EXP',
88
+ speed: 16,
89
+ multiplier: 4,
90
+ depth: DEPTH,
91
+ startPhase: 0,
92
+ mode: 'TRG',
93
+ }, BPM);
94
+
95
+ lfo.trigger();
96
+ // First update to set initial time (deltaMs=0 on first call)
97
+ lfo.update(1);
98
+ // Update to near end of cycle
99
+ const cycleMs = 4000;
100
+ const state = lfo.update(1 + cycleMs * 0.99);
101
+ const cc = outputToCC(state.output);
102
+
103
+ // At phase ~1, EXP should be near center: CC ≈ 64
104
+ expect(cc).toBeGreaterThanOrEqual(64);
105
+ expect(cc).toBeLessThan(75);
106
+ });
107
+
108
+ test('CC should decrease over time (decay behavior)', () => {
109
+ const lfo = new LFO({
110
+ waveform: 'EXP',
111
+ speed: 16,
112
+ multiplier: 4,
113
+ depth: DEPTH,
114
+ startPhase: 0,
115
+ mode: 'TRG',
116
+ }, BPM);
117
+
118
+ lfo.trigger();
119
+ const cycleMs = 4000;
120
+
121
+ const ccStart = outputToCC(lfo.update(1).output);
122
+ const ccMid = outputToCC(lfo.update(1 + cycleMs * 0.5).output);
123
+ const ccEnd = outputToCC(lfo.update(1 + cycleMs * 0.99).output);
124
+
125
+ expect(ccStart).toBeGreaterThan(ccMid);
126
+ expect(ccMid).toBeGreaterThan(ccEnd);
127
+ });
128
+ });
129
+ });
130
+
131
+ describe('RMP Waveform - Expected Behavior', () => {
132
+ describe('raw waveform generator', () => {
133
+ test('should return 0 at phase 0 (starts at center)', () => {
134
+ expect(generateRamp(0)).toBe(0);
135
+ });
136
+
137
+ test('should return 0.5 at phase 0.5 (linear rise)', () => {
138
+ expect(generateRamp(0.5)).toBe(0.5);
139
+ });
140
+
141
+ test('should return 1.0 at phase 1 (ends at peak)', () => {
142
+ expect(generateRamp(1)).toBe(1);
143
+ });
144
+
145
+ test('should rise linearly from phase 0 to 1', () => {
146
+ for (let i = 0; i <= 100; i++) {
147
+ const phase = i / 100;
148
+ expect(generateRamp(phase)).toBeCloseTo(phase, 5);
149
+ }
150
+ });
151
+
152
+ test('should be unipolar (output range 0 to 1)', () => {
153
+ for (let i = 0; i <= 100; i++) {
154
+ const value = generateRamp(i / 100);
155
+ expect(value).toBeGreaterThanOrEqual(0);
156
+ expect(value).toBeLessThanOrEqual(1);
157
+ }
158
+ });
159
+ });
160
+
161
+ describe('full LFO with depth=40', () => {
162
+ const BPM = 120;
163
+ const DEPTH = 40;
164
+
165
+ test('should produce CC near 64 at phase 0 (start at center)', () => {
166
+ const lfo = new LFO({
167
+ waveform: 'RMP',
168
+ speed: 16,
169
+ multiplier: 4,
170
+ depth: DEPTH,
171
+ startPhase: 0,
172
+ mode: 'TRG',
173
+ }, BPM);
174
+
175
+ lfo.trigger();
176
+ const state = lfo.update(1);
177
+ const cc = outputToCC(state.output);
178
+
179
+ // At phase 0, RMP should be at center: CC = 64
180
+ expect(cc).toBe(64);
181
+ });
182
+
183
+ test('should produce CC near 104 at phase 1 (end at peak)', () => {
184
+ const lfo = new LFO({
185
+ waveform: 'RMP',
186
+ speed: 16,
187
+ multiplier: 4,
188
+ depth: DEPTH,
189
+ startPhase: 0,
190
+ mode: 'TRG',
191
+ }, BPM);
192
+
193
+ lfo.trigger();
194
+ // First update to set initial time (deltaMs=0 on first call)
195
+ lfo.update(1);
196
+ const cycleMs = 4000;
197
+ const state = lfo.update(1 + cycleMs * 0.99);
198
+ const cc = outputToCC(state.output);
199
+
200
+ // At phase ~1, RMP should be at peak: CC = 64 + 40 = 104
201
+ expect(cc).toBeGreaterThan(100);
202
+ expect(cc).toBeLessThanOrEqual(104);
203
+ });
204
+
205
+ test('CC should increase over time (rise behavior)', () => {
206
+ const lfo = new LFO({
207
+ waveform: 'RMP',
208
+ speed: 16,
209
+ multiplier: 4,
210
+ depth: DEPTH,
211
+ startPhase: 0,
212
+ mode: 'TRG',
213
+ }, BPM);
214
+
215
+ lfo.trigger();
216
+ const cycleMs = 4000;
217
+
218
+ const ccStart = outputToCC(lfo.update(1).output);
219
+ const ccMid = outputToCC(lfo.update(1 + cycleMs * 0.5).output);
220
+ const ccEnd = outputToCC(lfo.update(1 + cycleMs * 0.99).output);
221
+
222
+ expect(ccStart).toBeLessThan(ccMid);
223
+ expect(ccMid).toBeLessThan(ccEnd);
224
+ });
225
+
226
+ test('should produce CC range [64-104] over full cycle', () => {
227
+ const lfo = new LFO({
228
+ waveform: 'RMP',
229
+ speed: 16,
230
+ multiplier: 4,
231
+ depth: DEPTH,
232
+ startPhase: 0,
233
+ mode: 'TRG',
234
+ }, BPM);
235
+
236
+ lfo.trigger();
237
+ const cycleMs = 4000;
238
+ let minCC = 127, maxCC = 0;
239
+
240
+ for (let i = 0; i <= 100; i++) {
241
+ const state = lfo.update(1 + (i / 100) * cycleMs);
242
+ const cc = outputToCC(state.output);
243
+ minCC = Math.min(minCC, cc);
244
+ maxCC = Math.max(maxCC, cc);
245
+ }
246
+
247
+ expect(minCC).toBe(64);
248
+ expect(maxCC).toBeGreaterThanOrEqual(103);
249
+ expect(maxCC).toBeLessThanOrEqual(104);
250
+ });
251
+ });
252
+ });
253
+
254
+ describe('Depth Scaling Comparison', () => {
255
+ const BPM = 120;
256
+ const DEPTH = 40;
257
+
258
+ test('TRI (bipolar) should swing ±40 from center [24-104]', () => {
259
+ const lfo = new LFO({
260
+ waveform: 'TRI',
261
+ speed: 16,
262
+ multiplier: 4,
263
+ depth: DEPTH,
264
+ startPhase: 0,
265
+ mode: 'TRG',
266
+ }, BPM);
267
+
268
+ lfo.trigger();
269
+ const cycleMs = 4000;
270
+ let minCC = 127, maxCC = 0;
271
+
272
+ for (let i = 0; i <= 100; i++) {
273
+ const state = lfo.update(1 + (i / 100) * cycleMs);
274
+ const cc = outputToCC(state.output);
275
+ minCC = Math.min(minCC, cc);
276
+ maxCC = Math.max(maxCC, cc);
277
+ }
278
+
279
+ // Bipolar: 64 - 40 = 24, 64 + 40 = 104
280
+ expect(minCC).toBe(24);
281
+ expect(maxCC).toBe(104);
282
+ });
283
+
284
+ test('RMP (unipolar) should swing +40 from center [64-104]', () => {
285
+ const lfo = new LFO({
286
+ waveform: 'RMP',
287
+ speed: 16,
288
+ multiplier: 4,
289
+ depth: DEPTH,
290
+ startPhase: 0,
291
+ mode: 'TRG',
292
+ }, BPM);
293
+
294
+ lfo.trigger();
295
+ const cycleMs = 4000;
296
+ let minCC = 127, maxCC = 0;
297
+
298
+ for (let i = 0; i <= 100; i++) {
299
+ const state = lfo.update(1 + (i / 100) * cycleMs);
300
+ const cc = outputToCC(state.output);
301
+ minCC = Math.min(minCC, cc);
302
+ maxCC = Math.max(maxCC, cc);
303
+ }
304
+
305
+ // Unipolar rising: 64 to 64 + 40 = 104
306
+ expect(minCC).toBe(64);
307
+ expect(maxCC).toBeGreaterThanOrEqual(103);
308
+ });
309
+
310
+ test('EXP (unipolar) should swing +40 from center [64-104]', () => {
311
+ const lfo = new LFO({
312
+ waveform: 'EXP',
313
+ speed: 16,
314
+ multiplier: 4,
315
+ depth: DEPTH,
316
+ startPhase: 0,
317
+ mode: 'TRG',
318
+ }, BPM);
319
+
320
+ lfo.trigger();
321
+ const cycleMs = 4000;
322
+ let minCC = 127, maxCC = 0;
323
+
324
+ for (let i = 0; i <= 100; i++) {
325
+ const state = lfo.update(1 + (i / 100) * cycleMs);
326
+ const cc = outputToCC(state.output);
327
+ minCC = Math.min(minCC, cc);
328
+ maxCC = Math.max(maxCC, cc);
329
+ }
330
+
331
+ // Unipolar decay: starts at 64 + 40 = 104, decays to 64
332
+ expect(minCC).toBeGreaterThanOrEqual(64);
333
+ expect(minCC).toBeLessThan(70);
334
+ expect(maxCC).toBeGreaterThanOrEqual(103);
335
+ });
336
+ });
337
+
338
+ describe('Negative Depth', () => {
339
+ const BPM = 120;
340
+ const DEPTH = -40;
341
+
342
+ test('EXP with negative depth should produce CC below center', () => {
343
+ const lfo = new LFO({
344
+ waveform: 'EXP',
345
+ speed: 16,
346
+ multiplier: 4,
347
+ depth: DEPTH,
348
+ startPhase: 0,
349
+ mode: 'TRG',
350
+ }, BPM);
351
+
352
+ lfo.trigger();
353
+ const state = lfo.update(1);
354
+ const cc = outputToCC(state.output);
355
+
356
+ // With negative depth, peak should be BELOW center: 64 - 40 = 24
357
+ expect(cc).toBeLessThan(30);
358
+ expect(cc).toBeGreaterThanOrEqual(24);
359
+ });
360
+
361
+ test('RMP with negative depth should rise toward center', () => {
362
+ const lfo = new LFO({
363
+ waveform: 'RMP',
364
+ speed: 16,
365
+ multiplier: 4,
366
+ depth: DEPTH,
367
+ startPhase: 0,
368
+ mode: 'TRG',
369
+ }, BPM);
370
+
371
+ lfo.trigger();
372
+ const state = lfo.update(1);
373
+ const cc = outputToCC(state.output);
374
+
375
+ // At phase 0, RMP should be at center
376
+ expect(cc).toBe(64);
377
+
378
+ // At end of cycle, should be at negative peak
379
+ const cycleMs = 4000;
380
+ const stateEnd = lfo.update(1 + cycleMs * 0.99);
381
+ const ccEnd = outputToCC(stateEnd.output);
382
+
383
+ // 64 - 40 = 24
384
+ expect(ccEnd).toBeLessThan(30);
385
+ expect(ccEnd).toBeGreaterThanOrEqual(24);
386
+ });
387
+ });
@@ -123,32 +123,33 @@ describe('Square Waveform', () => {
123
123
  });
124
124
 
125
125
  describe('Sawtooth Waveform', () => {
126
- test('starts at -1 at phase 0', () => {
127
- expect(generateSawtooth(0)).toBe(-1);
126
+ test('starts at +1 at phase 0', () => {
127
+ expect(generateSawtooth(0)).toBe(1);
128
128
  });
129
129
 
130
130
  test('is 0 at phase 0.5', () => {
131
131
  expect(generateSawtooth(0.5)).toBe(0);
132
132
  });
133
133
 
134
- test('approaches +1 at phase ~1', () => {
135
- expect(generateSawtooth(1)).toBe(1);
136
- expect(generateSawtooth(0.999)).toBeCloseTo(1, 2);
134
+ test('approaches -1 at phase ~1', () => {
135
+ expect(generateSawtooth(1)).toBe(-1);
136
+ expect(generateSawtooth(0.999)).toBeCloseTo(-1, 2);
137
137
  });
138
138
 
139
- test('is bipolar and linear', () => {
140
- expect(generateSawtooth(0.25)).toBeCloseTo(-0.5, 5);
141
- expect(generateSawtooth(0.75)).toBeCloseTo(0.5, 5);
139
+ test('is bipolar and linear (falling)', () => {
140
+ expect(generateSawtooth(0.25)).toBeCloseTo(0.5, 5);
141
+ expect(generateSawtooth(0.75)).toBeCloseTo(-0.5, 5);
142
142
  });
143
143
  });
144
144
 
145
145
  describe('Exponential Waveform', () => {
146
- test('starts at 0 at phase 0', () => {
147
- expect(generateExponential(0)).toBeCloseTo(0, 5);
146
+ test('starts at 1 at phase 0 (peak, matching Digitakt II)', () => {
147
+ expect(generateExponential(0)).toBeCloseTo(1, 5);
148
148
  });
149
149
 
150
- test('ends at 1 at phase 1', () => {
151
- expect(generateExponential(1)).toBeCloseTo(1, 5);
150
+ test('decays toward 0 at phase 1', () => {
151
+ // Normalized exponential decay reaches exactly 0 at phase 1
152
+ expect(generateExponential(1)).toBeCloseTo(0, 5);
152
153
  });
153
154
 
154
155
  test('is unipolar (0 to +1)', () => {
@@ -157,20 +158,21 @@ describe('Exponential Waveform', () => {
157
158
  expect(Math.max(...samples)).toBeLessThanOrEqual(1);
158
159
  });
159
160
 
160
- test('has accelerating (exponential) curve shape', () => {
161
- // At phase 0.5, should be less than 0.5 (curve bends up)
161
+ test('has decaying (exponential decay) curve shape', () => {
162
+ // At phase 0.5, should be greater than 0.5 (decay curve starts high)
162
163
  const midValue = generateExponential(0.5);
164
+ expect(midValue).toBeGreaterThan(0.1);
163
165
  expect(midValue).toBeLessThan(0.5);
164
166
  });
165
167
  });
166
168
 
167
169
  describe('Ramp Waveform', () => {
168
- test('starts at 1 at phase 0', () => {
169
- expect(generateRamp(0)).toBe(1);
170
+ test('starts at 0 at phase 0', () => {
171
+ expect(generateRamp(0)).toBe(0);
170
172
  });
171
173
 
172
- test('ends at 0 at phase 1', () => {
173
- expect(generateRamp(1)).toBe(0);
174
+ test('ends at 1 at phase 1', () => {
175
+ expect(generateRamp(1)).toBe(1);
174
176
  });
175
177
 
176
178
  test('is 0.5 at phase 0.5', () => {
@@ -183,9 +185,9 @@ describe('Ramp Waveform', () => {
183
185
  expect(Math.max(...samples)).toBeLessThanOrEqual(1);
184
186
  });
185
187
 
186
- test('is linear', () => {
187
- expect(generateRamp(0.25)).toBe(0.75);
188
- expect(generateRamp(0.75)).toBe(0.25);
188
+ test('is linear (rising)', () => {
189
+ expect(generateRamp(0.25)).toBe(0.25);
190
+ expect(generateRamp(0.75)).toBe(0.75);
189
191
  });
190
192
  });
191
193
 
@@ -237,8 +239,8 @@ describe('generateWaveform', () => {
237
239
  expect(generateWaveform('SIN', 0.25, state).value).toBeCloseTo(1, 5);
238
240
  expect(generateWaveform('SQR', 0.25, state).value).toBe(1);
239
241
  expect(generateWaveform('SAW', 0.5, state).value).toBe(0);
240
- expect(generateWaveform('EXP', 1, state).value).toBeCloseTo(1, 5);
241
- expect(generateWaveform('RMP', 0, state).value).toBe(1);
242
+ expect(generateWaveform('EXP', 0, state).value).toBeCloseTo(1, 5); // EXP starts at peak
243
+ expect(generateWaveform('RMP', 0, state).value).toBe(0);
242
244
  });
243
245
  });
244
246