elektron-lfo 1.0.0 → 1.0.1
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/engine/fade.d.ts +60 -0
- package/dist/engine/index.d.ts +12 -0
- package/dist/engine/lfo.d.ts +76 -0
- package/dist/engine/timing.d.ts +58 -0
- package/dist/engine/triggers.d.ts +38 -0
- package/dist/engine/types.d.ts +82 -0
- package/dist/engine/waveforms.d.ts +69 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +543 -0
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fade envelope system for Elektron Digitakt II LFO
|
|
3
|
+
*
|
|
4
|
+
* Fade behavior:
|
|
5
|
+
* - FADE = 0: No fade, full modulation immediately
|
|
6
|
+
* - FADE < 0 (negative): Fade IN - starts at 0, increases to full over |FADE| cycles
|
|
7
|
+
* - FADE > 0 (positive): Fade OUT - starts at full, decreases to 0 over FADE cycles
|
|
8
|
+
*
|
|
9
|
+
* Important notes from research:
|
|
10
|
+
* - Fade does NOT work in FRE mode (requires trigger to initiate)
|
|
11
|
+
* - Fade resets on trigger for TRG, ONE, HLF, and HLD modes
|
|
12
|
+
* - Fade timing is relative to LFO cycles, not absolute time
|
|
13
|
+
*/
|
|
14
|
+
import type { LFOConfig, LFOState, TriggerMode } from './types';
|
|
15
|
+
/**
|
|
16
|
+
* Calculate the fade multiplier based on fade progress
|
|
17
|
+
*
|
|
18
|
+
* @param fadeValue - The FADE parameter (-64 to +63)
|
|
19
|
+
* @param fadeProgress - Progress through the fade (0.0 to 1.0)
|
|
20
|
+
* @returns The fade multiplier (0.0 to 1.0)
|
|
21
|
+
*/
|
|
22
|
+
export declare function calculateFadeMultiplier(fadeValue: number, fadeProgress: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Calculate fade cycles - how many LFO cycles for complete fade
|
|
25
|
+
*
|
|
26
|
+
* The FADE parameter (-64 to +63) maps to fade duration in cycles:
|
|
27
|
+
* - |FADE| / 64 gives the number of cycles (approximately)
|
|
28
|
+
* - At |FADE| = 64, fade takes 1 cycle
|
|
29
|
+
* - At |FADE| = 32, fade takes 0.5 cycles
|
|
30
|
+
* - At |FADE| = 1, fade takes ~1/64 of a cycle
|
|
31
|
+
*/
|
|
32
|
+
export declare function calculateFadeCycles(fadeValue: number): number;
|
|
33
|
+
/**
|
|
34
|
+
* Update fade progress based on elapsed time
|
|
35
|
+
*
|
|
36
|
+
* @param config - LFO configuration
|
|
37
|
+
* @param state - Current LFO state
|
|
38
|
+
* @param cycleTimeMs - Duration of one LFO cycle in milliseconds
|
|
39
|
+
* @param deltaMs - Time elapsed since last update
|
|
40
|
+
* @returns Updated fade progress and multiplier
|
|
41
|
+
*/
|
|
42
|
+
export declare function updateFade(config: LFOConfig, state: LFOState, cycleTimeMs: number, deltaMs: number): {
|
|
43
|
+
fadeProgress: number;
|
|
44
|
+
fadeMultiplier: number;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Reset fade state (called on trigger for modes that reset fade)
|
|
48
|
+
*/
|
|
49
|
+
export declare function resetFade(config: LFOConfig): {
|
|
50
|
+
fadeProgress: number;
|
|
51
|
+
fadeMultiplier: number;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Check if fade should reset on trigger for a given mode
|
|
55
|
+
*/
|
|
56
|
+
export declare function shouldResetFadeOnTrigger(mode: TriggerMode): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Apply fade multiplier to output value
|
|
59
|
+
*/
|
|
60
|
+
export declare function applyFade(rawOutput: number, fadeMultiplier: number): number;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elektron LFO Engine
|
|
3
|
+
*
|
|
4
|
+
* A TypeScript implementation of the Elektron Digitakt II LFO engine
|
|
5
|
+
*/
|
|
6
|
+
export { LFO } from './lfo';
|
|
7
|
+
export type { Waveform, TriggerMode, Multiplier, LFOConfig, LFOState, TimingInfo, } from './types';
|
|
8
|
+
export { DEFAULT_CONFIG, createInitialState, clamp, VALID_MULTIPLIERS, isValidMultiplier, } from './types';
|
|
9
|
+
export { generateTriangle, generateSine, generateSquare, generateSawtooth, generateExponential, generateRamp, generateRandom, generateWaveform, isUnipolar, getWaveformRange, } from './waveforms';
|
|
10
|
+
export { calculateProduct, calculateCycleTimeMs, calculateFrequencyHz, calculatePhaseIncrement, calculateCyclesPerBar, calculateNoteValue, calculateTimingInfo, formatCycleTime, formatFrequency, } from './timing';
|
|
11
|
+
export { handleTrigger, checkModeStop, requiresTriggerToStart, resetsPhaseOnTrigger, resetsFadeOnTrigger, } from './triggers';
|
|
12
|
+
export { calculateFadeMultiplier, calculateFadeCycles, updateFade, resetFade, shouldResetFadeOnTrigger, applyFade, } from './fade';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main LFO class for Elektron Digitakt II LFO engine
|
|
3
|
+
*
|
|
4
|
+
* This class ties together all the components:
|
|
5
|
+
* - Waveform generation
|
|
6
|
+
* - Timing calculations
|
|
7
|
+
* - Trigger mode handling
|
|
8
|
+
* - Fade envelope
|
|
9
|
+
*/
|
|
10
|
+
import type { LFOConfig, LFOState, TimingInfo } from './types';
|
|
11
|
+
export declare class LFO {
|
|
12
|
+
private config;
|
|
13
|
+
private state;
|
|
14
|
+
private bpm;
|
|
15
|
+
private lastUpdateTime;
|
|
16
|
+
constructor(config?: Partial<LFOConfig>, bpm?: number);
|
|
17
|
+
/**
|
|
18
|
+
* Update the LFO state based on elapsed time
|
|
19
|
+
*
|
|
20
|
+
* @param currentTimeMs - Current time in milliseconds (e.g., performance.now())
|
|
21
|
+
* @returns The current LFO state
|
|
22
|
+
*/
|
|
23
|
+
update(currentTimeMs: number): LFOState;
|
|
24
|
+
/**
|
|
25
|
+
* Trigger the LFO
|
|
26
|
+
*/
|
|
27
|
+
trigger(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Get current state
|
|
30
|
+
*/
|
|
31
|
+
getState(): LFOState;
|
|
32
|
+
/**
|
|
33
|
+
* Get current configuration
|
|
34
|
+
*/
|
|
35
|
+
getConfig(): LFOConfig;
|
|
36
|
+
/**
|
|
37
|
+
* Update configuration
|
|
38
|
+
*/
|
|
39
|
+
setConfig(config: Partial<LFOConfig>): void;
|
|
40
|
+
/**
|
|
41
|
+
* Set BPM
|
|
42
|
+
*/
|
|
43
|
+
setBpm(bpm: number): void;
|
|
44
|
+
/**
|
|
45
|
+
* Get current BPM
|
|
46
|
+
*/
|
|
47
|
+
getBpm(): number;
|
|
48
|
+
/**
|
|
49
|
+
* Get timing information
|
|
50
|
+
*/
|
|
51
|
+
getTimingInfo(): TimingInfo;
|
|
52
|
+
/**
|
|
53
|
+
* Reset the LFO to initial state
|
|
54
|
+
*/
|
|
55
|
+
reset(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Check if the LFO is running
|
|
58
|
+
*/
|
|
59
|
+
isRunning(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Get the current output value
|
|
62
|
+
*/
|
|
63
|
+
getOutput(): number;
|
|
64
|
+
/**
|
|
65
|
+
* Get the current phase (0-1)
|
|
66
|
+
*/
|
|
67
|
+
getPhase(): number;
|
|
68
|
+
/**
|
|
69
|
+
* Start the LFO (for ONE/HLF modes that are stopped)
|
|
70
|
+
*/
|
|
71
|
+
start(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Stop the LFO
|
|
74
|
+
*/
|
|
75
|
+
stop(): void;
|
|
76
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing calculations for Elektron Digitakt II LFO
|
|
3
|
+
*
|
|
4
|
+
* Core formulas from the spec:
|
|
5
|
+
* - phase_steps_per_bar = |SPD| × MULT
|
|
6
|
+
* - cycle_time_ms = (60000 / BPM) × 4 × (128 / (|SPD| × MULT))
|
|
7
|
+
* - frequency_hz = (BPM / 60) × (|SPD| × MULT / 128)
|
|
8
|
+
*/
|
|
9
|
+
import type { LFOConfig, TimingInfo } from './types';
|
|
10
|
+
/**
|
|
11
|
+
* Calculate the product of |SPD| × MULT
|
|
12
|
+
*/
|
|
13
|
+
export declare function calculateProduct(config: LFOConfig): number;
|
|
14
|
+
/**
|
|
15
|
+
* Calculate cycle time in milliseconds
|
|
16
|
+
*
|
|
17
|
+
* Formula: cycle_time_ms = (60000 / BPM) × 4 × (128 / product)
|
|
18
|
+
*/
|
|
19
|
+
export declare function calculateCycleTimeMs(config: LFOConfig, bpm: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* Calculate LFO frequency in Hz
|
|
22
|
+
*
|
|
23
|
+
* Frequency is simply 1 / cycleTime in seconds
|
|
24
|
+
* frequency_hz = 1000 / cycleTimeMs
|
|
25
|
+
*/
|
|
26
|
+
export declare function calculateFrequencyHz(config: LFOConfig, bpm: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Calculate phase increment per millisecond
|
|
29
|
+
*
|
|
30
|
+
* Phase goes from 0 to 1 over one cycle
|
|
31
|
+
* Increment = 1 / cycle_time_ms (for positive speed)
|
|
32
|
+
* Increment = -1 / cycle_time_ms (for negative speed)
|
|
33
|
+
*/
|
|
34
|
+
export declare function calculatePhaseIncrement(config: LFOConfig, bpm: number): number;
|
|
35
|
+
/**
|
|
36
|
+
* Calculate cycles per bar
|
|
37
|
+
*
|
|
38
|
+
* At product = 128, we get exactly 1 cycle per bar
|
|
39
|
+
* At product > 128, we get product/128 cycles per bar
|
|
40
|
+
* At product < 128, we get product/128 cycles per bar (fraction)
|
|
41
|
+
*/
|
|
42
|
+
export declare function calculateCyclesPerBar(config: LFOConfig): number;
|
|
43
|
+
/**
|
|
44
|
+
* Convert product to musical note value string
|
|
45
|
+
*/
|
|
46
|
+
export declare function calculateNoteValue(product: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* Calculate complete timing information
|
|
49
|
+
*/
|
|
50
|
+
export declare function calculateTimingInfo(config: LFOConfig, bpm: number): TimingInfo;
|
|
51
|
+
/**
|
|
52
|
+
* Format cycle time for display
|
|
53
|
+
*/
|
|
54
|
+
export declare function formatCycleTime(cycleTimeMs: number): string;
|
|
55
|
+
/**
|
|
56
|
+
* Format frequency for display
|
|
57
|
+
*/
|
|
58
|
+
export declare function formatFrequency(frequencyHz: number): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
import type { LFOConfig, LFOState, TriggerMode } from './types';
|
|
12
|
+
/**
|
|
13
|
+
* Handle a trigger event based on the current mode
|
|
14
|
+
* Returns updated state after trigger processing
|
|
15
|
+
*/
|
|
16
|
+
export declare function handleTrigger(config: LFOConfig, state: LFOState, currentRawOutput: number): LFOState;
|
|
17
|
+
/**
|
|
18
|
+
* Check if the LFO should stop based on mode and phase
|
|
19
|
+
*
|
|
20
|
+
* For ONE mode: Stop when phase wraps and returns to start phase (one complete cycle)
|
|
21
|
+
* For HLF mode: Stop when phase reaches 0.5 past start phase (half cycle)
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkModeStop(config: LFOConfig, state: LFOState, previousPhase: number, currentPhase: number): {
|
|
24
|
+
shouldStop: boolean;
|
|
25
|
+
cycleCompleted: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Check if a trigger mode requires a trigger to start
|
|
29
|
+
*/
|
|
30
|
+
export declare function requiresTriggerToStart(mode: TriggerMode): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Check if a trigger mode resets phase on trigger
|
|
33
|
+
*/
|
|
34
|
+
export declare function resetsPhaseOnTrigger(mode: TriggerMode): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Check if a trigger mode resets fade on trigger
|
|
37
|
+
*/
|
|
38
|
+
export declare function resetsFadeOnTrigger(mode: TriggerMode): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elektron Digitakt II LFO Engine Types
|
|
3
|
+
*/
|
|
4
|
+
/** Available LFO waveform types */
|
|
5
|
+
export type Waveform = 'TRI' | 'SIN' | 'SQR' | 'SAW' | 'EXP' | 'RMP' | 'RND';
|
|
6
|
+
/** LFO trigger/behavior modes */
|
|
7
|
+
export type TriggerMode = 'FRE' | 'TRG' | 'HLD' | 'ONE' | 'HLF';
|
|
8
|
+
/** Available tempo multiplier values */
|
|
9
|
+
export type Multiplier = 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048;
|
|
10
|
+
/** LFO configuration parameters */
|
|
11
|
+
export interface LFOConfig {
|
|
12
|
+
/** Waveform type */
|
|
13
|
+
waveform: Waveform;
|
|
14
|
+
/** Speed parameter: -64.00 to +63.00 */
|
|
15
|
+
speed: number;
|
|
16
|
+
/** Tempo multiplier */
|
|
17
|
+
multiplier: Multiplier;
|
|
18
|
+
/** Use fixed 120 BPM instead of project BPM */
|
|
19
|
+
useFixedBPM: boolean;
|
|
20
|
+
/** Start phase: 0 to 127 (maps to 0-360 degrees) */
|
|
21
|
+
startPhase: number;
|
|
22
|
+
/** Trigger/behavior mode */
|
|
23
|
+
mode: TriggerMode;
|
|
24
|
+
/** Modulation depth: -64.00 to +63.00 */
|
|
25
|
+
depth: number;
|
|
26
|
+
/** Fade amount: -64 to +63 (negative = fade in, positive = fade out) */
|
|
27
|
+
fade: number;
|
|
28
|
+
}
|
|
29
|
+
/** LFO runtime state */
|
|
30
|
+
export interface LFOState {
|
|
31
|
+
/** Current phase position: 0.0 to 1.0 */
|
|
32
|
+
phase: number;
|
|
33
|
+
/** Final output value after depth and fade processing */
|
|
34
|
+
output: number;
|
|
35
|
+
/** Raw waveform output before depth/fade processing */
|
|
36
|
+
rawOutput: number;
|
|
37
|
+
/** Whether the LFO is currently running */
|
|
38
|
+
isRunning: boolean;
|
|
39
|
+
/** Current fade envelope multiplier: 0.0 to 1.0 */
|
|
40
|
+
fadeMultiplier: number;
|
|
41
|
+
/** Fade envelope progress: 0.0 to 1.0 */
|
|
42
|
+
fadeProgress: number;
|
|
43
|
+
/** Current random value for RND waveform */
|
|
44
|
+
randomValue: number;
|
|
45
|
+
/** Previous phase value for detecting step changes */
|
|
46
|
+
previousPhase: number;
|
|
47
|
+
/** Held output value for HLD mode */
|
|
48
|
+
heldOutput: number;
|
|
49
|
+
/** Normalized start phase: 0.0 to 1.0 */
|
|
50
|
+
startPhaseNormalized: number;
|
|
51
|
+
/** Number of complete cycles */
|
|
52
|
+
cycleCount: number;
|
|
53
|
+
/** Number of triggers received */
|
|
54
|
+
triggerCount: number;
|
|
55
|
+
/** Whether the LFO has been triggered at least once (for ONE/HLF modes) */
|
|
56
|
+
hasTriggered: boolean;
|
|
57
|
+
/** Current random step (0-15 for 16 steps per cycle) */
|
|
58
|
+
randomStep: number;
|
|
59
|
+
}
|
|
60
|
+
/** Timing information for an LFO configuration */
|
|
61
|
+
export interface TimingInfo {
|
|
62
|
+
/** Duration of one complete cycle in milliseconds */
|
|
63
|
+
cycleTimeMs: number;
|
|
64
|
+
/** Musical note value representation (e.g., "1/4", "1 bar", "4 bars") */
|
|
65
|
+
noteValue: string;
|
|
66
|
+
/** LFO frequency in Hz */
|
|
67
|
+
frequencyHz: number;
|
|
68
|
+
/** Number of LFO cycles per bar */
|
|
69
|
+
cyclesPerBar: number;
|
|
70
|
+
/** Product of |SPD| × MULT */
|
|
71
|
+
product: number;
|
|
72
|
+
}
|
|
73
|
+
/** Default LFO configuration */
|
|
74
|
+
export declare const DEFAULT_CONFIG: LFOConfig;
|
|
75
|
+
/** Create initial LFO state */
|
|
76
|
+
export declare function createInitialState(config: LFOConfig): LFOState;
|
|
77
|
+
/** Clamp a value to a range */
|
|
78
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
79
|
+
/** Valid multiplier values for validation */
|
|
80
|
+
export declare const VALID_MULTIPLIERS: readonly Multiplier[];
|
|
81
|
+
/** Check if a number is a valid multiplier */
|
|
82
|
+
export declare function isValidMultiplier(value: number): value is Multiplier;
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
import type { Waveform, LFOState } from './types';
|
|
9
|
+
/**
|
|
10
|
+
* Triangle waveform - Bipolar
|
|
11
|
+
* Starts at 0, peaks at +1 at phase 0.25, troughs at -1 at phase 0.75
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateTriangle(phase: number): number;
|
|
14
|
+
/**
|
|
15
|
+
* Sine waveform - Bipolar
|
|
16
|
+
* Standard sine wave starting at 0
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateSine(phase: number): number;
|
|
19
|
+
/**
|
|
20
|
+
* Square waveform - Bipolar
|
|
21
|
+
* +1 for first half, -1 for second half
|
|
22
|
+
*/
|
|
23
|
+
export declare function generateSquare(phase: number): number;
|
|
24
|
+
/**
|
|
25
|
+
* Sawtooth waveform - Bipolar
|
|
26
|
+
* Linear rise from -1 to +1
|
|
27
|
+
*/
|
|
28
|
+
export declare function generateSawtooth(phase: number): number;
|
|
29
|
+
/**
|
|
30
|
+
* Exponential waveform - Unipolar (0 to +1)
|
|
31
|
+
* Accelerating curve from 0 to 1
|
|
32
|
+
*/
|
|
33
|
+
export declare function generateExponential(phase: number): number;
|
|
34
|
+
/**
|
|
35
|
+
* Ramp waveform - Unipolar (0 to +1)
|
|
36
|
+
* Linear fall from +1 to 0
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateRamp(phase: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* Random waveform - Bipolar (-1 to +1)
|
|
41
|
+
* Sample-and-hold with 16 steps per cycle (16x frequency)
|
|
42
|
+
*
|
|
43
|
+
* Returns the current random value and potentially a new random value
|
|
44
|
+
* if a step boundary was crossed.
|
|
45
|
+
*/
|
|
46
|
+
export declare function generateRandom(phase: number, state: LFOState): {
|
|
47
|
+
value: number;
|
|
48
|
+
newRandomValue: number;
|
|
49
|
+
newRandomStep: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Generate waveform output for a given phase and waveform type
|
|
53
|
+
*/
|
|
54
|
+
export declare function generateWaveform(waveform: Waveform, phase: number, state: LFOState): {
|
|
55
|
+
value: number;
|
|
56
|
+
newRandomValue?: number;
|
|
57
|
+
newRandomStep?: number;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Check if a waveform is unipolar (0 to +1) vs bipolar (-1 to +1)
|
|
61
|
+
*/
|
|
62
|
+
export declare function isUnipolar(waveform: Waveform): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Get the range of a waveform
|
|
65
|
+
*/
|
|
66
|
+
export declare function getWaveformRange(waveform: Waveform): {
|
|
67
|
+
min: number;
|
|
68
|
+
max: number;
|
|
69
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
export * from './engine';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
// src/engine/types.ts
|
|
2
|
+
var DEFAULT_CONFIG = {
|
|
3
|
+
waveform: "TRI",
|
|
4
|
+
speed: 16,
|
|
5
|
+
multiplier: 8,
|
|
6
|
+
useFixedBPM: false,
|
|
7
|
+
startPhase: 0,
|
|
8
|
+
mode: "FRE",
|
|
9
|
+
depth: 63,
|
|
10
|
+
fade: 0
|
|
11
|
+
};
|
|
12
|
+
function createInitialState(config) {
|
|
13
|
+
const startPhaseNormalized = config.startPhase / 128;
|
|
14
|
+
return {
|
|
15
|
+
phase: startPhaseNormalized,
|
|
16
|
+
output: 0,
|
|
17
|
+
rawOutput: 0,
|
|
18
|
+
isRunning: true,
|
|
19
|
+
fadeMultiplier: config.fade < 0 ? 0 : 1,
|
|
20
|
+
fadeProgress: 0,
|
|
21
|
+
randomValue: Math.random() * 2 - 1,
|
|
22
|
+
previousPhase: startPhaseNormalized,
|
|
23
|
+
heldOutput: 0,
|
|
24
|
+
startPhaseNormalized,
|
|
25
|
+
cycleCount: 0,
|
|
26
|
+
triggerCount: 0,
|
|
27
|
+
hasTriggered: false,
|
|
28
|
+
randomStep: 0
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function clamp(value, min, max) {
|
|
32
|
+
return Math.max(min, Math.min(max, value));
|
|
33
|
+
}
|
|
34
|
+
var VALID_MULTIPLIERS = [
|
|
35
|
+
1,
|
|
36
|
+
2,
|
|
37
|
+
4,
|
|
38
|
+
8,
|
|
39
|
+
16,
|
|
40
|
+
32,
|
|
41
|
+
64,
|
|
42
|
+
128,
|
|
43
|
+
256,
|
|
44
|
+
512,
|
|
45
|
+
1024,
|
|
46
|
+
2048
|
|
47
|
+
];
|
|
48
|
+
function isValidMultiplier(value) {
|
|
49
|
+
return VALID_MULTIPLIERS.includes(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/engine/waveforms.ts
|
|
53
|
+
function generateTriangle(phase) {
|
|
54
|
+
if (phase < 0.25) {
|
|
55
|
+
return phase * 4;
|
|
56
|
+
}
|
|
57
|
+
if (phase < 0.75) {
|
|
58
|
+
return 1 - (phase - 0.25) * 4;
|
|
59
|
+
}
|
|
60
|
+
return -1 + (phase - 0.75) * 4;
|
|
61
|
+
}
|
|
62
|
+
function generateSine(phase) {
|
|
63
|
+
return Math.sin(phase * 2 * Math.PI);
|
|
64
|
+
}
|
|
65
|
+
function generateSquare(phase) {
|
|
66
|
+
return phase < 0.5 ? 1 : -1;
|
|
67
|
+
}
|
|
68
|
+
function generateSawtooth(phase) {
|
|
69
|
+
return phase * 2 - 1;
|
|
70
|
+
}
|
|
71
|
+
function generateExponential(phase) {
|
|
72
|
+
const k = 4;
|
|
73
|
+
return (Math.exp(phase * k) - 1) / (Math.exp(k) - 1);
|
|
74
|
+
}
|
|
75
|
+
function generateRamp(phase) {
|
|
76
|
+
return 1 - phase;
|
|
77
|
+
}
|
|
78
|
+
function generateRandom(phase, state) {
|
|
79
|
+
const stepsPerCycle = 16;
|
|
80
|
+
const currentStep = Math.floor(phase * stepsPerCycle);
|
|
81
|
+
if (currentStep !== state.randomStep) {
|
|
82
|
+
const newRandomValue = Math.random() * 2 - 1;
|
|
83
|
+
return {
|
|
84
|
+
value: newRandomValue,
|
|
85
|
+
newRandomValue,
|
|
86
|
+
newRandomStep: currentStep
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
value: state.randomValue,
|
|
91
|
+
newRandomValue: state.randomValue,
|
|
92
|
+
newRandomStep: state.randomStep
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function generateWaveform(waveform, phase, state) {
|
|
96
|
+
switch (waveform) {
|
|
97
|
+
case "TRI":
|
|
98
|
+
return { value: generateTriangle(phase) };
|
|
99
|
+
case "SIN":
|
|
100
|
+
return { value: generateSine(phase) };
|
|
101
|
+
case "SQR":
|
|
102
|
+
return { value: generateSquare(phase) };
|
|
103
|
+
case "SAW":
|
|
104
|
+
return { value: generateSawtooth(phase) };
|
|
105
|
+
case "EXP":
|
|
106
|
+
return { value: generateExponential(phase) };
|
|
107
|
+
case "RMP":
|
|
108
|
+
return { value: generateRamp(phase) };
|
|
109
|
+
case "RND": {
|
|
110
|
+
const result = generateRandom(phase, state);
|
|
111
|
+
return {
|
|
112
|
+
value: result.value,
|
|
113
|
+
newRandomValue: result.newRandomValue,
|
|
114
|
+
newRandomStep: result.newRandomStep
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
default: {
|
|
118
|
+
const _exhaustive = waveform;
|
|
119
|
+
throw new Error(`Unknown waveform: ${_exhaustive}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function isUnipolar(waveform) {
|
|
124
|
+
return waveform === "EXP" || waveform === "RMP";
|
|
125
|
+
}
|
|
126
|
+
function getWaveformRange(waveform) {
|
|
127
|
+
if (isUnipolar(waveform)) {
|
|
128
|
+
return { min: 0, max: 1 };
|
|
129
|
+
}
|
|
130
|
+
return { min: -1, max: 1 };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/engine/timing.ts
|
|
134
|
+
function calculateProduct(config) {
|
|
135
|
+
return Math.abs(config.speed) * config.multiplier;
|
|
136
|
+
}
|
|
137
|
+
function calculateCycleTimeMs(config, bpm) {
|
|
138
|
+
const effectiveBpm = config.useFixedBPM ? 120 : bpm;
|
|
139
|
+
const product = calculateProduct(config);
|
|
140
|
+
if (product === 0) {
|
|
141
|
+
return Infinity;
|
|
142
|
+
}
|
|
143
|
+
return 60000 / effectiveBpm * 4 * (128 / product);
|
|
144
|
+
}
|
|
145
|
+
function calculateFrequencyHz(config, bpm) {
|
|
146
|
+
const cycleTimeMs = calculateCycleTimeMs(config, bpm);
|
|
147
|
+
if (cycleTimeMs === Infinity || cycleTimeMs === 0) {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
return 1000 / cycleTimeMs;
|
|
151
|
+
}
|
|
152
|
+
function calculatePhaseIncrement(config, bpm) {
|
|
153
|
+
const cycleTimeMs = calculateCycleTimeMs(config, bpm);
|
|
154
|
+
if (cycleTimeMs === Infinity || cycleTimeMs === 0) {
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
const direction = config.speed >= 0 ? 1 : -1;
|
|
158
|
+
return direction / cycleTimeMs;
|
|
159
|
+
}
|
|
160
|
+
function calculateCyclesPerBar(config) {
|
|
161
|
+
const product = calculateProduct(config);
|
|
162
|
+
if (product === 0)
|
|
163
|
+
return 0;
|
|
164
|
+
return product / 128;
|
|
165
|
+
}
|
|
166
|
+
function calculateNoteValue(product) {
|
|
167
|
+
if (product === 0)
|
|
168
|
+
return "∞";
|
|
169
|
+
if (product >= 2048)
|
|
170
|
+
return "1/16";
|
|
171
|
+
if (product >= 1024)
|
|
172
|
+
return "1/8";
|
|
173
|
+
if (product >= 512)
|
|
174
|
+
return "1/4";
|
|
175
|
+
if (product >= 256)
|
|
176
|
+
return "1/2";
|
|
177
|
+
if (product >= 128)
|
|
178
|
+
return "1 bar";
|
|
179
|
+
const bars = 128 / product;
|
|
180
|
+
if (bars === Math.floor(bars)) {
|
|
181
|
+
return `${bars} bars`;
|
|
182
|
+
}
|
|
183
|
+
return `${bars.toFixed(1)} bars`;
|
|
184
|
+
}
|
|
185
|
+
function calculateTimingInfo(config, bpm) {
|
|
186
|
+
const product = calculateProduct(config);
|
|
187
|
+
const cycleTimeMs = calculateCycleTimeMs(config, bpm);
|
|
188
|
+
const frequencyHz = calculateFrequencyHz(config, bpm);
|
|
189
|
+
const cyclesPerBar = calculateCyclesPerBar(config);
|
|
190
|
+
const noteValue = calculateNoteValue(product);
|
|
191
|
+
return {
|
|
192
|
+
cycleTimeMs,
|
|
193
|
+
noteValue,
|
|
194
|
+
frequencyHz,
|
|
195
|
+
cyclesPerBar,
|
|
196
|
+
product
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function formatCycleTime(cycleTimeMs) {
|
|
200
|
+
if (cycleTimeMs === Infinity)
|
|
201
|
+
return "∞";
|
|
202
|
+
if (cycleTimeMs >= 60000) {
|
|
203
|
+
const minutes = cycleTimeMs / 60000;
|
|
204
|
+
return `${minutes.toFixed(1)}min`;
|
|
205
|
+
}
|
|
206
|
+
if (cycleTimeMs >= 1000) {
|
|
207
|
+
return `${(cycleTimeMs / 1000).toFixed(2)}s`;
|
|
208
|
+
}
|
|
209
|
+
return `${cycleTimeMs.toFixed(1)}ms`;
|
|
210
|
+
}
|
|
211
|
+
function formatFrequency(frequencyHz) {
|
|
212
|
+
if (frequencyHz === 0)
|
|
213
|
+
return "0 Hz";
|
|
214
|
+
if (frequencyHz < 0.01) {
|
|
215
|
+
return `${(frequencyHz * 1000).toFixed(3)} mHz`;
|
|
216
|
+
}
|
|
217
|
+
if (frequencyHz < 1) {
|
|
218
|
+
return `${frequencyHz.toFixed(3)} Hz`;
|
|
219
|
+
}
|
|
220
|
+
return `${frequencyHz.toFixed(2)} Hz`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/engine/triggers.ts
|
|
224
|
+
function handleTrigger(config, state, currentRawOutput) {
|
|
225
|
+
const newState = { ...state };
|
|
226
|
+
newState.triggerCount++;
|
|
227
|
+
switch (config.mode) {
|
|
228
|
+
case "FRE":
|
|
229
|
+
break;
|
|
230
|
+
case "TRG":
|
|
231
|
+
newState.phase = newState.startPhaseNormalized;
|
|
232
|
+
newState.previousPhase = newState.startPhaseNormalized;
|
|
233
|
+
newState.fadeProgress = 0;
|
|
234
|
+
newState.fadeMultiplier = config.fade < 0 ? 0 : 1;
|
|
235
|
+
newState.cycleCount = 0;
|
|
236
|
+
if (config.waveform === "RND") {
|
|
237
|
+
newState.randomValue = Math.random() * 2 - 1;
|
|
238
|
+
newState.randomStep = Math.floor(newState.phase * 16);
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
case "HLD":
|
|
242
|
+
newState.heldOutput = currentRawOutput;
|
|
243
|
+
newState.fadeProgress = 0;
|
|
244
|
+
newState.fadeMultiplier = config.fade < 0 ? 0 : 1;
|
|
245
|
+
break;
|
|
246
|
+
case "ONE":
|
|
247
|
+
newState.phase = newState.startPhaseNormalized;
|
|
248
|
+
newState.previousPhase = newState.startPhaseNormalized;
|
|
249
|
+
newState.isRunning = true;
|
|
250
|
+
newState.hasTriggered = true;
|
|
251
|
+
newState.fadeProgress = 0;
|
|
252
|
+
newState.fadeMultiplier = config.fade < 0 ? 0 : 1;
|
|
253
|
+
newState.cycleCount = 0;
|
|
254
|
+
if (config.waveform === "RND") {
|
|
255
|
+
newState.randomValue = Math.random() * 2 - 1;
|
|
256
|
+
newState.randomStep = Math.floor(newState.phase * 16);
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
case "HLF":
|
|
260
|
+
newState.phase = newState.startPhaseNormalized;
|
|
261
|
+
newState.previousPhase = newState.startPhaseNormalized;
|
|
262
|
+
newState.isRunning = true;
|
|
263
|
+
newState.hasTriggered = true;
|
|
264
|
+
newState.fadeProgress = 0;
|
|
265
|
+
newState.fadeMultiplier = config.fade < 0 ? 0 : 1;
|
|
266
|
+
newState.cycleCount = 0;
|
|
267
|
+
if (config.waveform === "RND") {
|
|
268
|
+
newState.randomValue = Math.random() * 2 - 1;
|
|
269
|
+
newState.randomStep = Math.floor(newState.phase * 16);
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
return newState;
|
|
274
|
+
}
|
|
275
|
+
function checkModeStop(config, state, previousPhase, currentPhase) {
|
|
276
|
+
if (config.mode !== "ONE" && config.mode !== "HLF") {
|
|
277
|
+
return { shouldStop: false, cycleCompleted: false };
|
|
278
|
+
}
|
|
279
|
+
if (!state.hasTriggered) {
|
|
280
|
+
return { shouldStop: true, cycleCompleted: false };
|
|
281
|
+
}
|
|
282
|
+
const startPhase = state.startPhaseNormalized;
|
|
283
|
+
const isForward = config.speed >= 0;
|
|
284
|
+
if (config.mode === "ONE") {
|
|
285
|
+
if (state.cycleCount >= 1) {
|
|
286
|
+
return { shouldStop: true, cycleCompleted: true };
|
|
287
|
+
}
|
|
288
|
+
} else if (config.mode === "HLF") {
|
|
289
|
+
const halfPhase = (startPhase + 0.5) % 1;
|
|
290
|
+
if (isForward) {
|
|
291
|
+
if (startPhase < 0.5) {
|
|
292
|
+
if (previousPhase < halfPhase && currentPhase >= halfPhase) {
|
|
293
|
+
return { shouldStop: true, cycleCompleted: true };
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
if (state.cycleCount >= 1 || previousPhase < halfPhase && currentPhase >= halfPhase) {
|
|
297
|
+
return { shouldStop: true, cycleCompleted: true };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
const halfPhaseBackward = (startPhase - 0.5 + 1) % 1;
|
|
302
|
+
if (startPhase >= 0.5) {
|
|
303
|
+
if (previousPhase > halfPhaseBackward && currentPhase <= halfPhaseBackward) {
|
|
304
|
+
return { shouldStop: true, cycleCompleted: true };
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
if (state.cycleCount >= 1 || previousPhase > halfPhaseBackward && currentPhase <= halfPhaseBackward) {
|
|
308
|
+
return { shouldStop: true, cycleCompleted: true };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { shouldStop: false, cycleCompleted: false };
|
|
314
|
+
}
|
|
315
|
+
function requiresTriggerToStart(mode) {
|
|
316
|
+
return mode === "ONE" || mode === "HLF";
|
|
317
|
+
}
|
|
318
|
+
function resetsPhaseOnTrigger(mode) {
|
|
319
|
+
return mode === "TRG" || mode === "ONE" || mode === "HLF";
|
|
320
|
+
}
|
|
321
|
+
function resetsFadeOnTrigger(mode) {
|
|
322
|
+
return mode !== "FRE";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/engine/fade.ts
|
|
326
|
+
function calculateFadeMultiplier(fadeValue, fadeProgress) {
|
|
327
|
+
if (fadeValue === 0) {
|
|
328
|
+
return 1;
|
|
329
|
+
}
|
|
330
|
+
const progress = Math.max(0, Math.min(1, fadeProgress));
|
|
331
|
+
if (fadeValue < 0) {
|
|
332
|
+
return progress;
|
|
333
|
+
} else {
|
|
334
|
+
return 1 - progress;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function calculateFadeCycles(fadeValue) {
|
|
338
|
+
if (fadeValue === 0)
|
|
339
|
+
return 0;
|
|
340
|
+
return Math.abs(fadeValue) / 64;
|
|
341
|
+
}
|
|
342
|
+
function updateFade(config, state, cycleTimeMs, deltaMs) {
|
|
343
|
+
if (config.fade === 0 || config.mode === "FRE") {
|
|
344
|
+
return {
|
|
345
|
+
fadeProgress: 1,
|
|
346
|
+
fadeMultiplier: 1
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const fadeCycles = calculateFadeCycles(config.fade);
|
|
350
|
+
const fadeDurationMs = fadeCycles * cycleTimeMs;
|
|
351
|
+
if (fadeDurationMs === 0 || fadeDurationMs === Infinity) {
|
|
352
|
+
return {
|
|
353
|
+
fadeProgress: 1,
|
|
354
|
+
fadeMultiplier: config.fade < 0 ? 0 : 1
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const progressIncrement = deltaMs / fadeDurationMs;
|
|
358
|
+
const newProgress = Math.min(1, state.fadeProgress + progressIncrement);
|
|
359
|
+
return {
|
|
360
|
+
fadeProgress: newProgress,
|
|
361
|
+
fadeMultiplier: calculateFadeMultiplier(config.fade, newProgress)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function resetFade(config) {
|
|
365
|
+
if (config.fade === 0) {
|
|
366
|
+
return { fadeProgress: 1, fadeMultiplier: 1 };
|
|
367
|
+
}
|
|
368
|
+
if (config.fade < 0) {
|
|
369
|
+
return { fadeProgress: 0, fadeMultiplier: 0 };
|
|
370
|
+
} else {
|
|
371
|
+
return { fadeProgress: 0, fadeMultiplier: 1 };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function shouldResetFadeOnTrigger(mode) {
|
|
375
|
+
return mode !== "FRE";
|
|
376
|
+
}
|
|
377
|
+
function applyFade(rawOutput, fadeMultiplier) {
|
|
378
|
+
return rawOutput * fadeMultiplier;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/engine/lfo.ts
|
|
382
|
+
class LFO {
|
|
383
|
+
config;
|
|
384
|
+
state;
|
|
385
|
+
bpm;
|
|
386
|
+
lastUpdateTime;
|
|
387
|
+
constructor(config = {}, bpm = 120) {
|
|
388
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
389
|
+
this.bpm = bpm;
|
|
390
|
+
this.state = createInitialState(this.config);
|
|
391
|
+
this.lastUpdateTime = 0;
|
|
392
|
+
if (this.config.mode === "FRE") {
|
|
393
|
+
this.state.fadeMultiplier = 1;
|
|
394
|
+
this.state.fadeProgress = 1;
|
|
395
|
+
}
|
|
396
|
+
if (this.config.mode === "ONE" || this.config.mode === "HLF") {
|
|
397
|
+
this.state.isRunning = false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
update(currentTimeMs) {
|
|
401
|
+
const deltaMs = this.lastUpdateTime === 0 ? 0 : currentTimeMs - this.lastUpdateTime;
|
|
402
|
+
this.lastUpdateTime = currentTimeMs;
|
|
403
|
+
const shouldUpdatePhase = this.state.isRunning && deltaMs > 0;
|
|
404
|
+
const cycleTimeMs = calculateCycleTimeMs(this.config, this.bpm);
|
|
405
|
+
const phaseIncrement = calculatePhaseIncrement(this.config, this.bpm);
|
|
406
|
+
if (shouldUpdatePhase) {
|
|
407
|
+
const previousPhase = this.state.phase;
|
|
408
|
+
let newPhase = this.state.phase + phaseIncrement * deltaMs;
|
|
409
|
+
if (newPhase >= 1) {
|
|
410
|
+
newPhase = newPhase % 1;
|
|
411
|
+
this.state.cycleCount++;
|
|
412
|
+
} else if (newPhase < 0) {
|
|
413
|
+
newPhase = 1 + newPhase % 1;
|
|
414
|
+
if (newPhase === 1)
|
|
415
|
+
newPhase = 0;
|
|
416
|
+
this.state.cycleCount++;
|
|
417
|
+
}
|
|
418
|
+
const stopCheck = checkModeStop(this.config, this.state, previousPhase, newPhase);
|
|
419
|
+
if (stopCheck.shouldStop) {
|
|
420
|
+
this.state.isRunning = false;
|
|
421
|
+
if (this.config.mode === "ONE") {
|
|
422
|
+
newPhase = this.state.startPhaseNormalized;
|
|
423
|
+
} else if (this.config.mode === "HLF") {
|
|
424
|
+
newPhase = (this.state.startPhaseNormalized + 0.5) % 1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
this.state.previousPhase = previousPhase;
|
|
428
|
+
this.state.phase = newPhase;
|
|
429
|
+
}
|
|
430
|
+
const waveformResult = generateWaveform(this.config.waveform, this.state.phase, this.state);
|
|
431
|
+
if (waveformResult.newRandomValue !== undefined) {
|
|
432
|
+
this.state.randomValue = waveformResult.newRandomValue;
|
|
433
|
+
}
|
|
434
|
+
if (waveformResult.newRandomStep !== undefined) {
|
|
435
|
+
this.state.randomStep = waveformResult.newRandomStep;
|
|
436
|
+
}
|
|
437
|
+
this.state.rawOutput = waveformResult.value;
|
|
438
|
+
if (shouldUpdatePhase) {
|
|
439
|
+
const fadeResult = updateFade(this.config, this.state, cycleTimeMs, deltaMs);
|
|
440
|
+
this.state.fadeProgress = fadeResult.fadeProgress;
|
|
441
|
+
this.state.fadeMultiplier = fadeResult.fadeMultiplier;
|
|
442
|
+
}
|
|
443
|
+
let effectiveRawOutput = this.state.rawOutput;
|
|
444
|
+
if (this.config.mode === "HLD" && this.state.triggerCount > 0) {
|
|
445
|
+
effectiveRawOutput = this.state.heldOutput;
|
|
446
|
+
}
|
|
447
|
+
const depthScale = this.config.depth / 63;
|
|
448
|
+
let scaledOutput = effectiveRawOutput * depthScale;
|
|
449
|
+
scaledOutput *= this.state.fadeMultiplier;
|
|
450
|
+
if (isUnipolar(this.config.waveform)) {}
|
|
451
|
+
this.state.output = scaledOutput;
|
|
452
|
+
return { ...this.state };
|
|
453
|
+
}
|
|
454
|
+
trigger() {
|
|
455
|
+
this.state = handleTrigger(this.config, this.state, this.state.rawOutput);
|
|
456
|
+
}
|
|
457
|
+
getState() {
|
|
458
|
+
return { ...this.state };
|
|
459
|
+
}
|
|
460
|
+
getConfig() {
|
|
461
|
+
return { ...this.config };
|
|
462
|
+
}
|
|
463
|
+
setConfig(config) {
|
|
464
|
+
const previousMode = this.config.mode;
|
|
465
|
+
this.config = { ...this.config, ...config };
|
|
466
|
+
if (config.startPhase !== undefined) {
|
|
467
|
+
this.state.startPhaseNormalized = config.startPhase / 128;
|
|
468
|
+
}
|
|
469
|
+
if ((config.mode === "ONE" || config.mode === "HLF") && previousMode !== config.mode) {
|
|
470
|
+
this.state.isRunning = false;
|
|
471
|
+
this.state.hasTriggered = false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
setBpm(bpm) {
|
|
475
|
+
this.bpm = clamp(bpm, 1, 999);
|
|
476
|
+
}
|
|
477
|
+
getBpm() {
|
|
478
|
+
return this.bpm;
|
|
479
|
+
}
|
|
480
|
+
getTimingInfo() {
|
|
481
|
+
return calculateTimingInfo(this.config, this.bpm);
|
|
482
|
+
}
|
|
483
|
+
reset() {
|
|
484
|
+
this.state = createInitialState(this.config);
|
|
485
|
+
if (this.config.mode === "ONE" || this.config.mode === "HLF") {
|
|
486
|
+
this.state.isRunning = false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
isRunning() {
|
|
490
|
+
return this.state.isRunning;
|
|
491
|
+
}
|
|
492
|
+
getOutput() {
|
|
493
|
+
return this.state.output;
|
|
494
|
+
}
|
|
495
|
+
getPhase() {
|
|
496
|
+
return this.state.phase;
|
|
497
|
+
}
|
|
498
|
+
start() {
|
|
499
|
+
this.state.isRunning = true;
|
|
500
|
+
this.state.hasTriggered = true;
|
|
501
|
+
}
|
|
502
|
+
stop() {
|
|
503
|
+
this.state.isRunning = false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
export {
|
|
507
|
+
updateFade,
|
|
508
|
+
shouldResetFadeOnTrigger,
|
|
509
|
+
resetsPhaseOnTrigger,
|
|
510
|
+
resetsFadeOnTrigger,
|
|
511
|
+
resetFade,
|
|
512
|
+
requiresTriggerToStart,
|
|
513
|
+
isValidMultiplier,
|
|
514
|
+
isUnipolar,
|
|
515
|
+
handleTrigger,
|
|
516
|
+
getWaveformRange,
|
|
517
|
+
generateWaveform,
|
|
518
|
+
generateTriangle,
|
|
519
|
+
generateSquare,
|
|
520
|
+
generateSine,
|
|
521
|
+
generateSawtooth,
|
|
522
|
+
generateRandom,
|
|
523
|
+
generateRamp,
|
|
524
|
+
generateExponential,
|
|
525
|
+
formatFrequency,
|
|
526
|
+
formatCycleTime,
|
|
527
|
+
createInitialState,
|
|
528
|
+
clamp,
|
|
529
|
+
checkModeStop,
|
|
530
|
+
calculateTimingInfo,
|
|
531
|
+
calculateProduct,
|
|
532
|
+
calculatePhaseIncrement,
|
|
533
|
+
calculateNoteValue,
|
|
534
|
+
calculateFrequencyHz,
|
|
535
|
+
calculateFadeMultiplier,
|
|
536
|
+
calculateFadeCycles,
|
|
537
|
+
calculateCyclesPerBar,
|
|
538
|
+
calculateCycleTimeMs,
|
|
539
|
+
applyFade,
|
|
540
|
+
VALID_MULTIPLIERS,
|
|
541
|
+
LFO,
|
|
542
|
+
DEFAULT_CONFIG
|
|
543
|
+
};
|