Implemented comprehensive real-time effect processing for multi-track audio: Core Features: - Per-track effect chains with drag-and-drop reordering - Effect bypass/enable toggle per effect - Real-time parameter updates (filters, dynamics, time-based, distortion, bitcrusher, pitch, timestretch) - Add/remove effects during playback without interruption - Effect chain persistence via localStorage - Automatic playback stop when tracks are deleted Technical Implementation: - Effect processor with dry/wet routing for bypass functionality - Real-time effect parameter updates using AudioParam setValueAtTime - Structure change detection for add/remove/reorder operations - Stale closure fix using refs for latest track state - ScriptProcessorNode for bitcrusher, pitch shifter, and time stretch - Dual-tap delay line for pitch shifting - Overlap-add synthesis for time stretching UI Components: - EffectBrowser dialog with categorized effects - EffectDevice component with parameter controls - EffectParameters for all 19 real-time effect types - Device rack with horizontal scrolling (Ableton-style) Removed offline-only effects (normalize, fadeIn, fadeOut, reverse) as they don't fit the real-time processing model. Completed all items in Phase 7.4: - [x] Per-track effect chain - [x] Effect rack UI - [x] Effect bypass per track - [x] Real-time effect processing during playback - [x] Add/remove effects during playback - [x] Real-time parameter updates - [x] Effect chain persistence 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
305 lines
6.5 KiB
TypeScript
305 lines
6.5 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;
|
|
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',
|
|
};
|