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 ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "elektron-lfo",
6
+ "devDependencies": {
7
+ "@types/bun": "latest",
8
+ "typescript": "^5.0.0",
9
+ },
10
+ },
11
+ },
12
+ "packages": {
13
+ "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
14
+
15
+ "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
16
+
17
+ "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
18
+
19
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
20
+
21
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
22
+ }
23
+ }
package/bunfig.toml ADDED
@@ -0,0 +1,3 @@
1
+ [test]
2
+ coverage = true
3
+ coverageDir = "./coverage"
package/docs/PLAN.md ADDED
@@ -0,0 +1,446 @@
1
+ # Elektron LFO - Implementation Plan
2
+
3
+ A TypeScript implementation of the Elektron Digitakt II LFO engine with real-time terminal visualization, built with Bun.
4
+
5
+ ## Project Requirements
6
+
7
+ **Runtime & Build:**
8
+ - Runtime: Bun (fast TypeScript/JavaScript runtime)
9
+ - Language: TypeScript with strict type checking
10
+ - Testing: Bun's built-in test runner (`bun test`)
11
+
12
+ **Core Features:**
13
+ - Complete Digitakt II LFO engine with all 7 waveforms (TRI, SIN, SQR, SAW, EXP, RMP, RND)
14
+ - All 5 trigger modes (FRE, TRG, HLD, ONE, HLF)
15
+ - Full parameter support: SPD (-64 to +63), MULT (1 through 2k), SPH (0-127), DEP (-64 to +63), FADE (-64 to +63)
16
+ - Accurate timing mathematics matching hardware behavior
17
+ - Support for both BPM-synced and fixed 120 BPM modes
18
+
19
+ ---
20
+
21
+ ## 1. Project Structure
22
+
23
+ ```
24
+ elektron-lfo/
25
+ ├── src/
26
+ │ ├── engine/
27
+ │ │ ├── index.ts # Main exports
28
+ │ │ ├── types.ts # LFOConfig, LFOState, enums
29
+ │ │ ├── waveforms.ts # All 7 waveform generators
30
+ │ │ ├── timing.ts # Timing/phase calculations
31
+ │ │ ├── triggers.ts # Trigger mode handling
32
+ │ │ ├── fade.ts # Fade in/out logic
33
+ │ │ └── lfo.ts # Main LFO class
34
+ │ ├── cli/
35
+ │ │ ├── index.ts # CLI entry point
36
+ │ │ ├── args.ts # Argument parsing
37
+ │ │ ├── display.ts # Terminal visualization
38
+ │ │ └── keyboard.ts # Keyboard input handling
39
+ │ └── index.ts # Library exports
40
+ ├── tests/
41
+ │ ├── waveforms.test.ts
42
+ │ ├── timing.test.ts
43
+ │ ├── triggers.test.ts
44
+ │ ├── phase.test.ts
45
+ │ ├── depth-fade.test.ts
46
+ │ └── presets.test.ts # Integration tests with 5 presets
47
+ ├── package.json
48
+ ├── tsconfig.json
49
+ ├── bunfig.toml
50
+ └── README.md
51
+ ```
52
+
53
+ **Dependencies:**
54
+ - `@types/bun` (dev)
55
+ - `typescript` (dev)
56
+ - For terminal UI: Start with raw ANSI escape codes (zero dependencies)
57
+
58
+ ---
59
+
60
+ ## 2. Core Types (`src/engine/types.ts`)
61
+
62
+ ```typescript
63
+ export type Waveform = 'TRI' | 'SIN' | 'SQR' | 'SAW' | 'EXP' | 'RMP' | 'RND';
64
+ export type TriggerMode = 'FRE' | 'TRG' | 'HLD' | 'ONE' | 'HLF';
65
+ export type Multiplier = 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048;
66
+
67
+ export interface LFOConfig {
68
+ waveform: Waveform;
69
+ speed: number; // -64.00 to +63.00
70
+ multiplier: Multiplier;
71
+ useFixedBPM: boolean; // true = 120 BPM (dot suffix)
72
+ startPhase: number; // 0 to 127
73
+ mode: TriggerMode;
74
+ depth: number; // -64.00 to +63.00
75
+ fade: number; // -64 to +63
76
+ }
77
+
78
+ export interface LFOState {
79
+ phase: number; // 0.0 to 1.0
80
+ output: number;
81
+ rawOutput: number;
82
+ isRunning: boolean;
83
+ fadeMultiplier: number;
84
+ fadeProgress: number;
85
+ randomValue: number;
86
+ previousPhase: number;
87
+ heldOutput: number;
88
+ startPhaseNormalized: number;
89
+ cycleCount: number;
90
+ triggerCount: number;
91
+ }
92
+
93
+ export interface TimingInfo {
94
+ cycleTimeMs: number;
95
+ noteValue: string;
96
+ frequencyHz: number;
97
+ cyclesPerBar: number;
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 3. Waveform Generation (`src/engine/waveforms.ts`)
104
+
105
+ Implement exact formulas from the Digitakt II specification:
106
+
107
+ ```typescript
108
+ // Triangle: Bipolar, starts at 0, peak at 0.25, trough at 0.75
109
+ function generateTriangle(phase: number): number {
110
+ if (phase < 0.25) return phase * 4;
111
+ if (phase < 0.75) return 1 - (phase - 0.25) * 4;
112
+ return -1 + (phase - 0.75) * 4;
113
+ }
114
+
115
+ // Sine: Bipolar, standard sine wave
116
+ function generateSine(phase: number): number {
117
+ return Math.sin(phase * 2 * Math.PI);
118
+ }
119
+
120
+ // Square: Bipolar, +1 first half, -1 second half
121
+ function generateSquare(phase: number): number {
122
+ return phase < 0.5 ? 1 : -1;
123
+ }
124
+
125
+ // Sawtooth: Bipolar, linear rise -1 to +1
126
+ function generateSawtooth(phase: number): number {
127
+ return phase * 2 - 1;
128
+ }
129
+
130
+ // Exponential: Unipolar 0 to +1, accelerating curve
131
+ function generateExponential(phase: number): number {
132
+ const k = 4;
133
+ return (Math.exp(phase * k) - 1) / (Math.exp(k) - 1);
134
+ }
135
+
136
+ // Ramp: Unipolar +1 to 0, linear fall
137
+ function generateRamp(phase: number): number {
138
+ return 1 - phase;
139
+ }
140
+
141
+ // Random: Bipolar, sample-and-hold, 8 steps per cycle
142
+ function generateRandom(phase: number, state: LFOState): {
143
+ value: number;
144
+ newRandomValue: number
145
+ }
146
+ ```
147
+
148
+ ---
149
+
150
+ ## 4. Timing Calculations (`src/engine/timing.ts`)
151
+
152
+ From the spec:
153
+ ```
154
+ phase_steps_per_bar = |SPD| × MULT
155
+ cycle_time_ms = (60000 / BPM) × 4 × (128 / (|SPD| × MULT))
156
+ frequency_hz = (BPM / 60) × (|SPD| × MULT / 128)
157
+ ```
158
+
159
+ Key functions:
160
+ - `calculatePhaseIncrement(config, bpm)` - phase change per millisecond
161
+ - `calculateTimingInfo(config, bpm)` - returns cycle time, note value, frequency
162
+ - `calculateNoteValue(product)` - converts SPD × MULT product to musical notation
163
+
164
+ ---
165
+
166
+ ## 5. Test Plan
167
+
168
+ ### Waveform Tests (`tests/waveforms.test.ts`)
169
+
170
+ For each waveform, test:
171
+ - Starting value at phase 0
172
+ - Peak/trough positions
173
+ - End value near phase 1
174
+ - Correct polarity range (bipolar -1 to +1, unipolar 0 to +1)
175
+ - Shape characteristics (linear vs exponential)
176
+
177
+ ### Timing Tests (`tests/timing.test.ts`)
178
+
179
+ Verify against spec examples:
180
+ | SPD | MULT | Expected Result |
181
+ |-----|------|-----------------|
182
+ | 32 | 64 | 1/16th note, 125ms at 120 BPM |
183
+ | 16 | 8 | 1 bar, 2000ms at 120 BPM |
184
+ | 1 | 1 | 128 bars, 256000ms at 120 BPM |
185
+ | 8 | 16 | 1 bar, 2000ms at 120 BPM |
186
+
187
+ Test fixed BPM mode (always 120 regardless of project BPM).
188
+
189
+ ### Trigger Mode Tests (`tests/triggers.test.ts`)
190
+
191
+ - **FRE**: Phase unchanged after trigger
192
+ - **TRG**: Phase resets to startPhase after trigger, fade resets
193
+ - **HLD**: Output value held after trigger, LFO continues in background
194
+ - **ONE**: Runs one cycle then stops, can be retriggered
195
+ - **HLF**: Stops at phase 0.5 from start (or backward equivalent)
196
+
197
+ ### Phase Tests (`tests/phase.test.ts`)
198
+
199
+ - Phase wraps correctly from 1 back to 0
200
+ - Phase stays within 0-1 range
201
+ - Negative speed runs phase backwards
202
+ - startPhase correctly positions initial phase
203
+ - ONE/HLF modes stop correctly with non-zero startPhase
204
+
205
+ ### Depth/Fade Tests (`tests/depth-fade.test.ts`)
206
+
207
+ - Depth 0 produces 0 output
208
+ - Negative depth inverts output
209
+ - Negative fade starts at 0 and increases (fade IN)
210
+ - Positive fade starts at 1 and decreases (fade OUT)
211
+ - Fade resets on trigger for TRG/ONE/HLF modes
212
+
213
+ ### Preset Integration Tests (`tests/presets.test.ts`)
214
+
215
+ Test all 5 presets from the DIGITAKT_II_LFO_PRESETS.md document:
216
+
217
+ 1. **Fade-In One-Shot**: RMP, SPD=8, MULT=16, ONE mode, FADE=-32
218
+ - Verify 2000ms cycle at 120 BPM
219
+ - Verify stops after one cycle
220
+ - Verify fade multiplier increases over time
221
+
222
+ 2. **Ambient Drift**: SIN, SPD=1, MULT=1, FRE mode
223
+ - Verify 256000ms (4+ minutes) cycle
224
+ - Verify continuous running despite triggers
225
+
226
+ 3. **Hi-Hat Humanizer**: RND, SPD=32, MULT=64, FRE mode
227
+ - Verify 125ms cycle (1/16 note)
228
+ - Verify random values change
229
+ - Verify output within depth range
230
+
231
+ 4. **Pumping Sidechain**: EXP, SPD=32, MULT=4, TRG mode, DEP=-63
232
+ - Verify 2000ms cycle
233
+ - Verify restarts on trigger
234
+ - Verify inverted (negative) output
235
+
236
+ 5. **Wobble Bass**: SIN, SPD=16, MULT=8, TRG mode, SPH=32
237
+ - Verify 2000ms cycle (1 bar)
238
+ - Verify starts at peak (phase 90 degrees)
239
+
240
+ ---
241
+
242
+ ## 6. CLI Tool
243
+
244
+ ### Command-Line Arguments (`src/cli/args.ts`)
245
+
246
+ ```
247
+ Usage: elektron-lfo [options]
248
+
249
+ Options:
250
+ -w, --waveform <type> TRI, SIN, SQR, SAW, EXP, RMP, RND (default: TRI)
251
+ -s, --speed <value> -64.00 to +63.00 (default: 16)
252
+ -m, --multiplier <val> 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048 (default: 8)
253
+ -f, --fixed Use fixed 120 BPM instead of project BPM
254
+ -p, --phase <value> 0-127 start phase (default: 0)
255
+ -M, --mode <mode> FRE, TRG, HLD, ONE, HLF (default: FRE)
256
+ -d, --depth <value> -64.00 to +63.00 (default: 63)
257
+ -F, --fade <value> -64 to +63 (default: 0)
258
+ -b, --bpm <value> 1-999 project BPM (default: 120)
259
+ -h, --help Show help
260
+ ```
261
+
262
+ ### Terminal Display (`src/cli/display.ts`)
263
+
264
+ Display should include:
265
+ - Parameter values (WAVE, SPD, MULT, MODE, SPH, DEP, FADE)
266
+ - BPM and timing info (cycle time in ms, note value, Hz)
267
+ - ASCII waveform with current phase indicator (vertical line)
268
+ - Current output value (numerical + visual bar)
269
+ - Phase percentage, fade percentage, cycle count
270
+ - Status (STOPPED/RUNNING/FREE RUNNING)
271
+ - Control hints
272
+
273
+ Example display:
274
+ ```
275
+ === Elektron LFO Visualizer ===
276
+
277
+ WAVE: SIN SPD: 16.00 MULT: 8 MODE: TRG
278
+ SPH: 32 (90°) DEP: +48.00 FADE: 0
279
+ BPM: 120 Cycle: 2000.0ms (1 bar) Hz: 0.500
280
+
281
+ .-""-.
282
+ / \
283
+ / | \ <- Phase indicator
284
+ / \
285
+ '-.____.____.-
286
+
287
+ Output: +0.7500 [=======| ]
288
+ Phase: 25.0% Fade: 100% Cycles: 3 Triggers: 5
289
+
290
+ [RUNNING] - Press SPACE to trigger
291
+
292
+ Controls: [SPACE] Trigger [Q] Quit [↑/↓] BPM [←/→] Speed
293
+ ```
294
+
295
+ ### Keyboard Handling (`src/cli/keyboard.ts`)
296
+
297
+ - **Space**: Send trigger
298
+ - **Q / Ctrl+C**: Quit
299
+ - **Up/Down arrows**: Adjust BPM (+/- 1)
300
+ - **Left/Right arrows**: Adjust speed (+/- 1)
301
+
302
+ Use `process.stdin.setRawMode(true)` for single-keypress detection.
303
+
304
+ ---
305
+
306
+ ## 7. Package Configuration
307
+
308
+ ### package.json
309
+
310
+ ```json
311
+ {
312
+ "name": "elektron-lfo",
313
+ "version": "1.0.0",
314
+ "description": "Digitakt II LFO engine implementation with CLI visualization",
315
+ "main": "dist/index.js",
316
+ "module": "src/index.ts",
317
+ "types": "dist/index.d.ts",
318
+ "bin": {
319
+ "elektron-lfo": "./src/cli/index.ts"
320
+ },
321
+ "scripts": {
322
+ "dev": "bun run src/cli/index.ts",
323
+ "test": "bun test",
324
+ "test:watch": "bun test --watch",
325
+ "build": "bun build src/index.ts --outdir dist --target node",
326
+ "cli": "bun run src/cli/index.ts",
327
+ "typecheck": "tsc --noEmit"
328
+ },
329
+ "devDependencies": {
330
+ "@types/bun": "latest",
331
+ "typescript": "^5.0.0"
332
+ },
333
+ "engines": {
334
+ "bun": ">=1.0.0"
335
+ }
336
+ }
337
+ ```
338
+
339
+ ### tsconfig.json
340
+
341
+ ```json
342
+ {
343
+ "compilerOptions": {
344
+ "target": "ES2022",
345
+ "module": "ESNext",
346
+ "moduleResolution": "bundler",
347
+ "lib": ["ES2022"],
348
+ "strict": true,
349
+ "noEmit": true,
350
+ "esModuleInterop": true,
351
+ "types": ["bun-types"]
352
+ },
353
+ "include": ["src/**/*"]
354
+ }
355
+ ```
356
+
357
+ ### bunfig.toml
358
+
359
+ ```toml
360
+ [test]
361
+ coverage = true
362
+ coverageDir = "./coverage"
363
+ ```
364
+
365
+ ---
366
+
367
+ ## 8. Implementation Order
368
+
369
+ ### Phase 1: Foundation (Day 1)
370
+ 1. Project setup (bun init, configs)
371
+ 2. Types definition (`src/engine/types.ts`)
372
+
373
+ ### Phase 2: Core Engine (Days 2-3)
374
+ 3. Waveform generators + tests
375
+ 4. Timing system + tests
376
+ 5. Trigger handling + tests
377
+
378
+ ### Phase 3: Advanced Features (Day 4)
379
+ 6. Fade system + tests
380
+ 7. Main LFO class + integration tests
381
+
382
+ ### Phase 4: CLI (Days 5-6)
383
+ 8. Argument parsing
384
+ 9. Display rendering
385
+ 10. Keyboard handling
386
+ 11. CLI entry point with 60fps loop
387
+
388
+ ### Phase 5: Verification (Day 7)
389
+ 12. Preset integration tests (all 5 presets)
390
+ 13. Documentation
391
+
392
+ ### Dependency Graph
393
+
394
+ ```
395
+ types.ts
396
+ |
397
+ +-- waveforms.ts
398
+ | |
399
+ +-- timing.ts
400
+ | |
401
+ +-- triggers.ts
402
+ | |
403
+ +-- fade.ts
404
+ | |
405
+ +-------+-- lfo.ts
406
+ |
407
+ +--------+--------+
408
+ | | |
409
+ args.ts display.ts keyboard.ts
410
+ | | |
411
+ +--------+--------+
412
+ |
413
+ cli/index.ts
414
+ ```
415
+
416
+ ---
417
+
418
+ ## 9. Key Implementation Notes
419
+
420
+ 1. **Phase is normalized to 0-1**, not 0-127. Convert on input/output.
421
+
422
+ 2. **Random waveform state** needs special handling - must track previous phase to detect step changes.
423
+
424
+ 3. **ONE mode stopping** requires careful detection of phase wrapping past the start phase. Must work for both positive AND negative speed.
425
+
426
+ 4. **HLF mode** stops at the phase 0.5 beyond start phase (wrapping), not absolute 0.5.
427
+
428
+ 5. **HLD mode** captures the current output when triggered and holds it until the next trigger, but the LFO continues running in the background.
429
+
430
+ 6. **Fade timing** is relative to LFO cycles, not absolute time. This ensures consistency across BPM changes.
431
+
432
+ 7. **Fade resets** on trigger for TRG, ONE, and HLF modes. FRE mode does not reset fade.
433
+
434
+ 8. **60fps update rate** is sufficient for visualization but may need adjustment for audio-rate LFO applications.
435
+
436
+ 9. **BPM can be project-synced or fixed** - the `useFixedBPM` config option determines whether to use the passed `bpm` value or always use 120 BPM.
437
+
438
+ 10. **ANSI terminal codes** work on most modern terminals (macOS Terminal, iTerm2, VSCode terminal) but may need fallbacks for Windows CMD.
439
+
440
+ ---
441
+
442
+ ## 10. Reference Documents
443
+
444
+ - `/Users/brent/code/field-rec/DIGITAKT_II_LFO_SPEC.md` - Complete LFO specification
445
+ - `/Users/brent/code/field-rec/DIGITAKT_II_LFO_PRESETS.md` - 5 preset examples with timing calculations
446
+ - `/Users/brent/code/field-rec/EXPO_LFO_VISUALIZER_PLAN.md` - React Native Skia visualizer component plan