elektron-lfo 1.0.0
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/bun.lock +23 -0
- package/bunfig.toml +3 -0
- package/docs/PLAN.md +446 -0
- package/docs/TEST_QUESTIONS.md +550 -0
- package/package.json +26 -0
- package/src/cli/args.ts +182 -0
- package/src/cli/display.ts +237 -0
- package/src/cli/index.ts +129 -0
- package/src/cli/keyboard.ts +143 -0
- package/src/engine/fade.ts +137 -0
- package/src/engine/index.ts +72 -0
- package/src/engine/lfo.ts +269 -0
- package/src/engine/timing.ts +157 -0
- package/src/engine/triggers.ts +179 -0
- package/src/engine/types.ts +126 -0
- package/src/engine/waveforms.ts +152 -0
- package/src/index.ts +31 -0
- package/tests/depth-fade.test.ts +306 -0
- package/tests/phase.test.ts +219 -0
- package/tests/presets.test.ts +344 -0
- package/tests/timing.test.ts +232 -0
- package/tests/triggers.test.ts +345 -0
- package/tests/waveforms.test.ts +273 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trigger mode handling for Elektron Digitakt II LFO
|
|
3
|
+
*
|
|
4
|
+
* Trigger Modes:
|
|
5
|
+
* - FRE (Free): LFO runs continuously, triggers ignored
|
|
6
|
+
* - TRG (Trigger): Restarts LFO phase and fade on trigger
|
|
7
|
+
* - HLD (Hold): Captures and holds output on trigger, LFO continues in background
|
|
8
|
+
* - ONE (One-shot): Runs one complete cycle then stops, can be retriggered
|
|
9
|
+
* - HLF (Half): Runs half cycle then stops, can be retriggered
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { LFOConfig, LFOState, TriggerMode } from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle a trigger event based on the current mode
|
|
16
|
+
* Returns updated state after trigger processing
|
|
17
|
+
*/
|
|
18
|
+
export function handleTrigger(
|
|
19
|
+
config: LFOConfig,
|
|
20
|
+
state: LFOState,
|
|
21
|
+
currentRawOutput: number
|
|
22
|
+
): LFOState {
|
|
23
|
+
const newState = { ...state };
|
|
24
|
+
newState.triggerCount++;
|
|
25
|
+
|
|
26
|
+
switch (config.mode) {
|
|
27
|
+
case 'FRE':
|
|
28
|
+
// Free running - triggers are ignored
|
|
29
|
+
// LFO continues without interruption
|
|
30
|
+
break;
|
|
31
|
+
|
|
32
|
+
case 'TRG':
|
|
33
|
+
// Trigger mode - reset phase and fade
|
|
34
|
+
newState.phase = newState.startPhaseNormalized;
|
|
35
|
+
newState.previousPhase = newState.startPhaseNormalized;
|
|
36
|
+
newState.fadeProgress = 0;
|
|
37
|
+
newState.fadeMultiplier = config.fade < 0 ? 0 : 1;
|
|
38
|
+
newState.cycleCount = 0;
|
|
39
|
+
// Generate new random value on trigger for RND waveform
|
|
40
|
+
if (config.waveform === 'RND') {
|
|
41
|
+
newState.randomValue = Math.random() * 2 - 1;
|
|
42
|
+
newState.randomStep = Math.floor(newState.phase * 16);
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case 'HLD':
|
|
47
|
+
// Hold mode - capture current output, LFO continues in background
|
|
48
|
+
newState.heldOutput = currentRawOutput;
|
|
49
|
+
// Note: Phase continues running, only output is held
|
|
50
|
+
// Fade resets on trigger
|
|
51
|
+
newState.fadeProgress = 0;
|
|
52
|
+
newState.fadeMultiplier = config.fade < 0 ? 0 : 1;
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
case 'ONE':
|
|
56
|
+
// One-shot mode - reset and run one complete cycle
|
|
57
|
+
newState.phase = newState.startPhaseNormalized;
|
|
58
|
+
newState.previousPhase = newState.startPhaseNormalized;
|
|
59
|
+
newState.isRunning = true;
|
|
60
|
+
newState.hasTriggered = true;
|
|
61
|
+
newState.fadeProgress = 0;
|
|
62
|
+
newState.fadeMultiplier = config.fade < 0 ? 0 : 1;
|
|
63
|
+
newState.cycleCount = 0;
|
|
64
|
+
if (config.waveform === 'RND') {
|
|
65
|
+
newState.randomValue = Math.random() * 2 - 1;
|
|
66
|
+
newState.randomStep = Math.floor(newState.phase * 16);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
case 'HLF':
|
|
71
|
+
// Half mode - reset and run half cycle
|
|
72
|
+
newState.phase = newState.startPhaseNormalized;
|
|
73
|
+
newState.previousPhase = newState.startPhaseNormalized;
|
|
74
|
+
newState.isRunning = true;
|
|
75
|
+
newState.hasTriggered = true;
|
|
76
|
+
newState.fadeProgress = 0;
|
|
77
|
+
newState.fadeMultiplier = config.fade < 0 ? 0 : 1;
|
|
78
|
+
newState.cycleCount = 0;
|
|
79
|
+
if (config.waveform === 'RND') {
|
|
80
|
+
newState.randomValue = Math.random() * 2 - 1;
|
|
81
|
+
newState.randomStep = Math.floor(newState.phase * 16);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return newState;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if the LFO should stop based on mode and phase
|
|
91
|
+
*
|
|
92
|
+
* For ONE mode: Stop when phase wraps and returns to start phase (one complete cycle)
|
|
93
|
+
* For HLF mode: Stop when phase reaches 0.5 past start phase (half cycle)
|
|
94
|
+
*/
|
|
95
|
+
export function checkModeStop(
|
|
96
|
+
config: LFOConfig,
|
|
97
|
+
state: LFOState,
|
|
98
|
+
previousPhase: number,
|
|
99
|
+
currentPhase: number
|
|
100
|
+
): { shouldStop: boolean; cycleCompleted: boolean } {
|
|
101
|
+
// Only ONE and HLF modes can stop
|
|
102
|
+
if (config.mode !== 'ONE' && config.mode !== 'HLF') {
|
|
103
|
+
return { shouldStop: false, cycleCompleted: false };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Need to have been triggered to run
|
|
107
|
+
if (!state.hasTriggered) {
|
|
108
|
+
return { shouldStop: true, cycleCompleted: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const startPhase = state.startPhaseNormalized;
|
|
112
|
+
const isForward = config.speed >= 0;
|
|
113
|
+
|
|
114
|
+
if (config.mode === 'ONE') {
|
|
115
|
+
// ONE mode: Stop after completing one full cycle
|
|
116
|
+
// We detect this by checking if cycleCount has incremented (phase wrapped)
|
|
117
|
+
// and we've returned to or passed the start phase
|
|
118
|
+
if (state.cycleCount >= 1) {
|
|
119
|
+
return { shouldStop: true, cycleCompleted: true };
|
|
120
|
+
}
|
|
121
|
+
} else if (config.mode === 'HLF') {
|
|
122
|
+
// HLF mode: Stop after half cycle (0.5 phase distance from start)
|
|
123
|
+
const halfPhase = (startPhase + 0.5) % 1;
|
|
124
|
+
|
|
125
|
+
if (isForward) {
|
|
126
|
+
// Check if we crossed the half-point
|
|
127
|
+
if (startPhase < 0.5) {
|
|
128
|
+
// Half point is greater than start (no wrap needed)
|
|
129
|
+
if (previousPhase < halfPhase && currentPhase >= halfPhase) {
|
|
130
|
+
return { shouldStop: true, cycleCompleted: true };
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// Half point wraps around through 0 (halfPhase < startPhase)
|
|
134
|
+
// We need to cross 1->0 boundary first, then reach halfPhase
|
|
135
|
+
if (state.cycleCount >= 1 || (previousPhase < halfPhase && currentPhase >= halfPhase)) {
|
|
136
|
+
return { shouldStop: true, cycleCompleted: true };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// Backward direction: halfPhase is 0.5 BEHIND start
|
|
141
|
+
const halfPhaseBackward = (startPhase - 0.5 + 1) % 1;
|
|
142
|
+
if (startPhase >= 0.5) {
|
|
143
|
+
// Half point is less than start (no wrap needed)
|
|
144
|
+
if (previousPhase > halfPhaseBackward && currentPhase <= halfPhaseBackward) {
|
|
145
|
+
return { shouldStop: true, cycleCompleted: true };
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// Half point wraps around through 1
|
|
149
|
+
if (state.cycleCount >= 1 || (previousPhase > halfPhaseBackward && currentPhase <= halfPhaseBackward)) {
|
|
150
|
+
return { shouldStop: true, cycleCompleted: true };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { shouldStop: false, cycleCompleted: false };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a trigger mode requires a trigger to start
|
|
161
|
+
*/
|
|
162
|
+
export function requiresTriggerToStart(mode: TriggerMode): boolean {
|
|
163
|
+
return mode === 'ONE' || mode === 'HLF';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if a trigger mode resets phase on trigger
|
|
168
|
+
*/
|
|
169
|
+
export function resetsPhaseOnTrigger(mode: TriggerMode): boolean {
|
|
170
|
+
return mode === 'TRG' || mode === 'ONE' || mode === 'HLF';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if a trigger mode resets fade on trigger
|
|
175
|
+
*/
|
|
176
|
+
export function resetsFadeOnTrigger(mode: TriggerMode): boolean {
|
|
177
|
+
// FRE mode doesn't reset fade (fade doesn't work in FRE mode per spec)
|
|
178
|
+
return mode !== 'FRE';
|
|
179
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elektron Digitakt II LFO Engine Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Available LFO waveform types */
|
|
6
|
+
export type Waveform = 'TRI' | 'SIN' | 'SQR' | 'SAW' | 'EXP' | 'RMP' | 'RND';
|
|
7
|
+
|
|
8
|
+
/** LFO trigger/behavior modes */
|
|
9
|
+
export type TriggerMode = 'FRE' | 'TRG' | 'HLD' | 'ONE' | 'HLF';
|
|
10
|
+
|
|
11
|
+
/** Available tempo multiplier values */
|
|
12
|
+
export type Multiplier = 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048;
|
|
13
|
+
|
|
14
|
+
/** LFO configuration parameters */
|
|
15
|
+
export interface LFOConfig {
|
|
16
|
+
/** Waveform type */
|
|
17
|
+
waveform: Waveform;
|
|
18
|
+
/** Speed parameter: -64.00 to +63.00 */
|
|
19
|
+
speed: number;
|
|
20
|
+
/** Tempo multiplier */
|
|
21
|
+
multiplier: Multiplier;
|
|
22
|
+
/** Use fixed 120 BPM instead of project BPM */
|
|
23
|
+
useFixedBPM: boolean;
|
|
24
|
+
/** Start phase: 0 to 127 (maps to 0-360 degrees) */
|
|
25
|
+
startPhase: number;
|
|
26
|
+
/** Trigger/behavior mode */
|
|
27
|
+
mode: TriggerMode;
|
|
28
|
+
/** Modulation depth: -64.00 to +63.00 */
|
|
29
|
+
depth: number;
|
|
30
|
+
/** Fade amount: -64 to +63 (negative = fade in, positive = fade out) */
|
|
31
|
+
fade: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** LFO runtime state */
|
|
35
|
+
export interface LFOState {
|
|
36
|
+
/** Current phase position: 0.0 to 1.0 */
|
|
37
|
+
phase: number;
|
|
38
|
+
/** Final output value after depth and fade processing */
|
|
39
|
+
output: number;
|
|
40
|
+
/** Raw waveform output before depth/fade processing */
|
|
41
|
+
rawOutput: number;
|
|
42
|
+
/** Whether the LFO is currently running */
|
|
43
|
+
isRunning: boolean;
|
|
44
|
+
/** Current fade envelope multiplier: 0.0 to 1.0 */
|
|
45
|
+
fadeMultiplier: number;
|
|
46
|
+
/** Fade envelope progress: 0.0 to 1.0 */
|
|
47
|
+
fadeProgress: number;
|
|
48
|
+
/** Current random value for RND waveform */
|
|
49
|
+
randomValue: number;
|
|
50
|
+
/** Previous phase value for detecting step changes */
|
|
51
|
+
previousPhase: number;
|
|
52
|
+
/** Held output value for HLD mode */
|
|
53
|
+
heldOutput: number;
|
|
54
|
+
/** Normalized start phase: 0.0 to 1.0 */
|
|
55
|
+
startPhaseNormalized: number;
|
|
56
|
+
/** Number of complete cycles */
|
|
57
|
+
cycleCount: number;
|
|
58
|
+
/** Number of triggers received */
|
|
59
|
+
triggerCount: number;
|
|
60
|
+
/** Whether the LFO has been triggered at least once (for ONE/HLF modes) */
|
|
61
|
+
hasTriggered: boolean;
|
|
62
|
+
/** Current random step (0-15 for 16 steps per cycle) */
|
|
63
|
+
randomStep: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Timing information for an LFO configuration */
|
|
67
|
+
export interface TimingInfo {
|
|
68
|
+
/** Duration of one complete cycle in milliseconds */
|
|
69
|
+
cycleTimeMs: number;
|
|
70
|
+
/** Musical note value representation (e.g., "1/4", "1 bar", "4 bars") */
|
|
71
|
+
noteValue: string;
|
|
72
|
+
/** LFO frequency in Hz */
|
|
73
|
+
frequencyHz: number;
|
|
74
|
+
/** Number of LFO cycles per bar */
|
|
75
|
+
cyclesPerBar: number;
|
|
76
|
+
/** Product of |SPD| × MULT */
|
|
77
|
+
product: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Default LFO configuration */
|
|
81
|
+
export const DEFAULT_CONFIG: LFOConfig = {
|
|
82
|
+
waveform: 'TRI',
|
|
83
|
+
speed: 16,
|
|
84
|
+
multiplier: 8,
|
|
85
|
+
useFixedBPM: false,
|
|
86
|
+
startPhase: 0,
|
|
87
|
+
mode: 'FRE',
|
|
88
|
+
depth: 63,
|
|
89
|
+
fade: 0,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Create initial LFO state */
|
|
93
|
+
export function createInitialState(config: LFOConfig): LFOState {
|
|
94
|
+
const startPhaseNormalized = config.startPhase / 128;
|
|
95
|
+
return {
|
|
96
|
+
phase: startPhaseNormalized,
|
|
97
|
+
output: 0,
|
|
98
|
+
rawOutput: 0,
|
|
99
|
+
isRunning: true,
|
|
100
|
+
fadeMultiplier: config.fade < 0 ? 0 : 1, // Fade in starts at 0, fade out starts at 1
|
|
101
|
+
fadeProgress: 0,
|
|
102
|
+
randomValue: Math.random() * 2 - 1, // Initial random value: -1 to +1
|
|
103
|
+
previousPhase: startPhaseNormalized,
|
|
104
|
+
heldOutput: 0,
|
|
105
|
+
startPhaseNormalized,
|
|
106
|
+
cycleCount: 0,
|
|
107
|
+
triggerCount: 0,
|
|
108
|
+
hasTriggered: false,
|
|
109
|
+
randomStep: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Clamp a value to a range */
|
|
114
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
115
|
+
return Math.max(min, Math.min(max, value));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Valid multiplier values for validation */
|
|
119
|
+
export const VALID_MULTIPLIERS: readonly Multiplier[] = [
|
|
120
|
+
1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048,
|
|
121
|
+
] as const;
|
|
122
|
+
|
|
123
|
+
/** Check if a number is a valid multiplier */
|
|
124
|
+
export function isValidMultiplier(value: number): value is Multiplier {
|
|
125
|
+
return VALID_MULTIPLIERS.includes(value as Multiplier);
|
|
126
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waveform generators for Elektron Digitakt II LFO
|
|
3
|
+
*
|
|
4
|
+
* Waveform types:
|
|
5
|
+
* - Bipolar (-1 to +1): TRI, SIN, SQR, SAW, RND
|
|
6
|
+
* - Unipolar (0 to +1): EXP, RMP
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Waveform, LFOState } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Triangle waveform - Bipolar
|
|
13
|
+
* Starts at 0, peaks at +1 at phase 0.25, troughs at -1 at phase 0.75
|
|
14
|
+
*/
|
|
15
|
+
export function generateTriangle(phase: number): number {
|
|
16
|
+
if (phase < 0.25) {
|
|
17
|
+
return phase * 4; // 0 to +1
|
|
18
|
+
}
|
|
19
|
+
if (phase < 0.75) {
|
|
20
|
+
return 1 - (phase - 0.25) * 4; // +1 to -1
|
|
21
|
+
}
|
|
22
|
+
return -1 + (phase - 0.75) * 4; // -1 to 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sine waveform - Bipolar
|
|
27
|
+
* Standard sine wave starting at 0
|
|
28
|
+
*/
|
|
29
|
+
export function generateSine(phase: number): number {
|
|
30
|
+
return Math.sin(phase * 2 * Math.PI);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Square waveform - Bipolar
|
|
35
|
+
* +1 for first half, -1 for second half
|
|
36
|
+
*/
|
|
37
|
+
export function generateSquare(phase: number): number {
|
|
38
|
+
return phase < 0.5 ? 1 : -1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sawtooth waveform - Bipolar
|
|
43
|
+
* Linear rise from -1 to +1
|
|
44
|
+
*/
|
|
45
|
+
export function generateSawtooth(phase: number): number {
|
|
46
|
+
return phase * 2 - 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Exponential waveform - Unipolar (0 to +1)
|
|
51
|
+
* Accelerating curve from 0 to 1
|
|
52
|
+
*/
|
|
53
|
+
export function generateExponential(phase: number): number {
|
|
54
|
+
const k = 4; // Steepness factor
|
|
55
|
+
return (Math.exp(phase * k) - 1) / (Math.exp(k) - 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ramp waveform - Unipolar (0 to +1)
|
|
60
|
+
* Linear fall from +1 to 0
|
|
61
|
+
*/
|
|
62
|
+
export function generateRamp(phase: number): number {
|
|
63
|
+
return 1 - phase;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Random waveform - Bipolar (-1 to +1)
|
|
68
|
+
* Sample-and-hold with 16 steps per cycle (16x frequency)
|
|
69
|
+
*
|
|
70
|
+
* Returns the current random value and potentially a new random value
|
|
71
|
+
* if a step boundary was crossed.
|
|
72
|
+
*/
|
|
73
|
+
export function generateRandom(
|
|
74
|
+
phase: number,
|
|
75
|
+
state: LFOState
|
|
76
|
+
): { value: number; newRandomValue: number; newRandomStep: number } {
|
|
77
|
+
// 16 steps per cycle
|
|
78
|
+
const stepsPerCycle = 16;
|
|
79
|
+
const currentStep = Math.floor(phase * stepsPerCycle);
|
|
80
|
+
|
|
81
|
+
// Check if we crossed a step boundary
|
|
82
|
+
if (currentStep !== state.randomStep) {
|
|
83
|
+
// Generate new random value between -1 and +1
|
|
84
|
+
const newRandomValue = Math.random() * 2 - 1;
|
|
85
|
+
return {
|
|
86
|
+
value: newRandomValue,
|
|
87
|
+
newRandomValue,
|
|
88
|
+
newRandomStep: currentStep,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// No step change, return current value
|
|
93
|
+
return {
|
|
94
|
+
value: state.randomValue,
|
|
95
|
+
newRandomValue: state.randomValue,
|
|
96
|
+
newRandomStep: state.randomStep,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Generate waveform output for a given phase and waveform type
|
|
102
|
+
*/
|
|
103
|
+
export function generateWaveform(
|
|
104
|
+
waveform: Waveform,
|
|
105
|
+
phase: number,
|
|
106
|
+
state: LFOState
|
|
107
|
+
): { value: number; newRandomValue?: number; newRandomStep?: number } {
|
|
108
|
+
switch (waveform) {
|
|
109
|
+
case 'TRI':
|
|
110
|
+
return { value: generateTriangle(phase) };
|
|
111
|
+
case 'SIN':
|
|
112
|
+
return { value: generateSine(phase) };
|
|
113
|
+
case 'SQR':
|
|
114
|
+
return { value: generateSquare(phase) };
|
|
115
|
+
case 'SAW':
|
|
116
|
+
return { value: generateSawtooth(phase) };
|
|
117
|
+
case 'EXP':
|
|
118
|
+
return { value: generateExponential(phase) };
|
|
119
|
+
case 'RMP':
|
|
120
|
+
return { value: generateRamp(phase) };
|
|
121
|
+
case 'RND': {
|
|
122
|
+
const result = generateRandom(phase, state);
|
|
123
|
+
return {
|
|
124
|
+
value: result.value,
|
|
125
|
+
newRandomValue: result.newRandomValue,
|
|
126
|
+
newRandomStep: result.newRandomStep,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
default: {
|
|
130
|
+
// Exhaustive check
|
|
131
|
+
const _exhaustive: never = waveform;
|
|
132
|
+
throw new Error(`Unknown waveform: ${_exhaustive}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if a waveform is unipolar (0 to +1) vs bipolar (-1 to +1)
|
|
139
|
+
*/
|
|
140
|
+
export function isUnipolar(waveform: Waveform): boolean {
|
|
141
|
+
return waveform === 'EXP' || waveform === 'RMP';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the range of a waveform
|
|
146
|
+
*/
|
|
147
|
+
export function getWaveformRange(waveform: Waveform): { min: number; max: number } {
|
|
148
|
+
if (isUnipolar(waveform)) {
|
|
149
|
+
return { min: 0, max: 1 };
|
|
150
|
+
}
|
|
151
|
+
return { min: -1, max: 1 };
|
|
152
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elektron LFO
|
|
3
|
+
*
|
|
4
|
+
* A TypeScript implementation of the Elektron Digitakt II LFO engine
|
|
5
|
+
* with real-time terminal visualization.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { LFO } from 'elektron-lfo';
|
|
10
|
+
*
|
|
11
|
+
* const lfo = new LFO({
|
|
12
|
+
* waveform: 'SIN',
|
|
13
|
+
* speed: 16,
|
|
14
|
+
* multiplier: 8,
|
|
15
|
+
* mode: 'TRG',
|
|
16
|
+
* depth: 63,
|
|
17
|
+
* }, 120);
|
|
18
|
+
*
|
|
19
|
+
* // Update at 60fps
|
|
20
|
+
* setInterval(() => {
|
|
21
|
+
* const state = lfo.update(performance.now());
|
|
22
|
+
* console.log('Output:', state.output);
|
|
23
|
+
* }, 1000 / 60);
|
|
24
|
+
*
|
|
25
|
+
* // Trigger the LFO
|
|
26
|
+
* lfo.trigger();
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Re-export everything from engine
|
|
31
|
+
export * from './engine';
|