Files
audio-ui/lib/audio/effects/chain.ts
Sebastian Krüger 17381221d8 feat: refine UI with effects panel improvements and visual polish
Major improvements:
- Fixed multi-file import (FileList to Array conversion)
- Auto-select first track when adding to empty project
- Global effects panel folding state (independent of track selection)
- Effects panel collapsed/disabled when no track selected
- Effect device expansion state persisted per-device
- Effect browser with searchable descriptions

Visual refinements:
- Removed center dot from pan knob for cleaner look
- Simplified fader: removed volume fill overlay, dynamic level meter visible through semi-transparent handle
- Level meter capped at fader position (realistic mixer behavior)
- Solid background instead of gradient for fader track
- Subtle volume overlay up to fader handle
- Fixed track control width flickering (consistent 4px border)
- Effect devices: removed shadows/rounded corners for flatter DAW-style look, added consistent border-radius
- Added border between track control and waveform area

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 18:13:38 +01:00

306 lines
6.6 KiB
TypeScript

/**
* Effect Chain System
* Manages chains of audio effects with bypass, reordering, and preset support
*/
import type {
PitchShifterParameters,
TimeStretchParameters,
DistortionParameters,
BitcrusherParameters,
} from './advanced';
import type {
CompressorParameters,
LimiterParameters,
GateParameters,
} from './dynamics';
import type {
DelayParameters,
ReverbParameters,
ChorusParameters,
FlangerParameters,
PhaserParameters,
} from './time-based';
import type { FilterOptions } from './filters';
// Effect type identifier
export type EffectType =
// Filters
| 'lowpass'
| 'highpass'
| 'bandpass'
| 'notch'
| 'lowshelf'
| 'highshelf'
| 'peaking'
// Dynamics
| 'compressor'
| 'limiter'
| 'gate'
// Time-based
| 'delay'
| 'reverb'
| 'chorus'
| 'flanger'
| 'phaser'
// Advanced
| 'pitch'
| 'timestretch'
| 'distortion'
| 'bitcrusher';
// Union of all effect parameter types
export type EffectParameters =
| FilterOptions
| CompressorParameters
| LimiterParameters
| GateParameters
| DelayParameters
| ReverbParameters
| ChorusParameters
| FlangerParameters
| PhaserParameters
| PitchShifterParameters
| TimeStretchParameters
| DistortionParameters
| BitcrusherParameters
| Record<string, never>; // For effects without parameters
// Effect instance in a chain
export interface ChainEffect {
id: string;
type: EffectType;
name: string;
enabled: boolean;
expanded?: boolean; // UI state for effect device expansion
parameters?: EffectParameters;
}
// Effect chain
export interface EffectChain {
id: string;
name: string;
effects: ChainEffect[];
}
// Effect preset
export interface EffectPreset {
id: string;
name: string;
description?: string;
chain: EffectChain;
createdAt: number;
}
/**
* Generate a unique ID for effects/chains
*/
export function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Create a new effect instance
*/
export function createEffect(
type: EffectType,
name: string,
parameters?: EffectParameters
): ChainEffect {
return {
id: generateId(),
type,
name,
enabled: true,
parameters: parameters || getDefaultParameters(type),
};
}
/**
* Create a new effect chain
*/
export function createEffectChain(name: string = 'New Chain'): EffectChain {
return {
id: generateId(),
name,
effects: [],
};
}
/**
* Add effect to chain
*/
export function addEffectToChain(
chain: EffectChain,
effect: ChainEffect
): EffectChain {
return {
...chain,
effects: [...chain.effects, effect],
};
}
/**
* Remove effect from chain
*/
export function removeEffectFromChain(
chain: EffectChain,
effectId: string
): EffectChain {
return {
...chain,
effects: chain.effects.filter((e) => e.id !== effectId),
};
}
/**
* Toggle effect enabled state
*/
export function toggleEffect(
chain: EffectChain,
effectId: string
): EffectChain {
return {
...chain,
effects: chain.effects.map((e) =>
e.id === effectId ? { ...e, enabled: !e.enabled } : e
),
};
}
/**
* Update effect parameters
*/
export function updateEffectParameters(
chain: EffectChain,
effectId: string,
parameters: EffectParameters
): EffectChain {
return {
...chain,
effects: chain.effects.map((e) =>
e.id === effectId ? { ...e, parameters } : e
),
};
}
/**
* Reorder effects in chain
*/
export function reorderEffects(
chain: EffectChain,
fromIndex: number,
toIndex: number
): EffectChain {
const effects = [...chain.effects];
const [removed] = effects.splice(fromIndex, 1);
effects.splice(toIndex, 0, removed);
return {
...chain,
effects,
};
}
/**
* Create a preset from a chain
*/
export function createPreset(
chain: EffectChain,
name: string,
description?: string
): EffectPreset {
return {
id: generateId(),
name,
description,
chain: JSON.parse(JSON.stringify(chain)), // Deep clone
createdAt: Date.now(),
};
}
/**
* Load preset (returns a new chain)
*/
export function loadPreset(preset: EffectPreset): EffectChain {
return JSON.parse(JSON.stringify(preset.chain)); // Deep clone
}
/**
* Get default parameters for an effect type
*/
export function getDefaultParameters(type: EffectType): EffectParameters {
switch (type) {
// Filters
case 'lowpass':
case 'highpass':
return { frequency: 1000, Q: 1 } as FilterOptions;
case 'bandpass':
case 'notch':
return { frequency: 1000, Q: 1 } as FilterOptions;
case 'lowshelf':
case 'highshelf':
return { frequency: 1000, Q: 1, gain: 0 } as FilterOptions;
case 'peaking':
return { frequency: 1000, Q: 1, gain: 0 } as FilterOptions;
// Dynamics
case 'compressor':
return { threshold: -24, ratio: 4, attack: 0.003, release: 0.25, knee: 30, makeupGain: 0 } as CompressorParameters;
case 'limiter':
return { threshold: -3, attack: 0.001, release: 0.05, makeupGain: 0 } as LimiterParameters;
case 'gate':
return { threshold: -40, ratio: 10, attack: 0.001, release: 0.1, knee: 0 } as GateParameters;
// Time-based
case 'delay':
return { time: 0.5, feedback: 0.3, mix: 0.5 } as DelayParameters;
case 'reverb':
return { roomSize: 0.5, damping: 0.5, mix: 0.3 } as ReverbParameters;
case 'chorus':
return { rate: 1.5, depth: 0.002, mix: 0.5 } as ChorusParameters;
case 'flanger':
return { rate: 0.5, depth: 0.002, feedback: 0.5, mix: 0.5 } as FlangerParameters;
case 'phaser':
return { rate: 0.5, depth: 0.5, stages: 4, mix: 0.5 } as PhaserParameters;
// Advanced
case 'distortion':
return { drive: 0.5, type: 'soft', output: 0.7, mix: 1 } as DistortionParameters;
case 'pitch':
return { semitones: 0, cents: 0, mix: 1 } as PitchShifterParameters;
case 'timestretch':
return { rate: 1.0, preservePitch: false, mix: 1 } as TimeStretchParameters;
case 'bitcrusher':
return { bitDepth: 8, sampleRate: 8000, mix: 1 } as BitcrusherParameters;
default:
return {};
}
}
/**
* Get effect display name
*/
export const EFFECT_NAMES: Record<EffectType, string> = {
lowpass: 'Low-Pass Filter',
highpass: 'High-Pass Filter',
bandpass: 'Band-Pass Filter',
notch: 'Notch Filter',
lowshelf: 'Low Shelf',
highshelf: 'High Shelf',
peaking: 'Peaking EQ',
compressor: 'Compressor',
limiter: 'Limiter',
gate: 'Gate/Expander',
delay: 'Delay/Echo',
reverb: 'Reverb',
chorus: 'Chorus',
flanger: 'Flanger',
phaser: 'Phaser',
pitch: 'Pitch Shifter',
timestretch: 'Time Stretch',
distortion: 'Distortion',
bitcrusher: 'Bitcrusher',
};