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>
723 lines
22 KiB
TypeScript
723 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Slider } from '@/components/ui/Slider';
|
|
import type { ChainEffect, EffectType } from '@/lib/audio/effects/chain';
|
|
import type {
|
|
PitchShifterParameters,
|
|
TimeStretchParameters,
|
|
DistortionParameters,
|
|
BitcrusherParameters,
|
|
} from '@/lib/audio/effects/advanced';
|
|
import type {
|
|
CompressorParameters,
|
|
LimiterParameters,
|
|
GateParameters,
|
|
} from '@/lib/audio/effects/dynamics';
|
|
import type {
|
|
DelayParameters,
|
|
ReverbParameters,
|
|
ChorusParameters,
|
|
FlangerParameters,
|
|
PhaserParameters,
|
|
} from '@/lib/audio/effects/time-based';
|
|
import type { FilterOptions } from '@/lib/audio/effects/filters';
|
|
|
|
export interface EffectParametersProps {
|
|
effect: ChainEffect;
|
|
onUpdateParameters?: (parameters: any) => void;
|
|
}
|
|
|
|
export function EffectParameters({ effect, onUpdateParameters }: EffectParametersProps) {
|
|
const params = effect.parameters || {};
|
|
|
|
const updateParam = (key: string, value: any) => {
|
|
if (onUpdateParameters) {
|
|
onUpdateParameters({ ...params, [key]: value });
|
|
}
|
|
};
|
|
|
|
// Filter effects
|
|
if (['lowpass', 'highpass', 'bandpass', 'notch', 'lowshelf', 'highshelf', 'peaking'].includes(effect.type)) {
|
|
const filterParams = params as FilterOptions;
|
|
return (
|
|
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Frequency: {Math.round(filterParams.frequency || 1000)} Hz
|
|
</label>
|
|
<Slider
|
|
value={[filterParams.frequency || 1000]}
|
|
onValueChange={([value]) => updateParam('frequency', value)}
|
|
min={20}
|
|
max={20000}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Q: {(filterParams.Q || 1).toFixed(2)}
|
|
</label>
|
|
<Slider
|
|
value={[filterParams.Q || 1]}
|
|
onValueChange={([value]) => updateParam('Q', value)}
|
|
min={0.1}
|
|
max={20}
|
|
step={0.1}
|
|
/>
|
|
</div>
|
|
{['lowshelf', 'highshelf', 'peaking'].includes(effect.type) && (
|
|
<div className="space-y-1 col-span-2">
|
|
<label className="text-xs font-medium">
|
|
Gain: {(filterParams.gain || 0).toFixed(1)} dB
|
|
</label>
|
|
<Slider
|
|
value={[filterParams.gain || 0]}
|
|
onValueChange={([value]) => updateParam('gain', value)}
|
|
min={-40}
|
|
max={40}
|
|
step={0.5}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Compressor
|
|
if (effect.type === 'compressor') {
|
|
const compParams = params as CompressorParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Threshold: {(compParams.threshold || -24).toFixed(1)} dB
|
|
</label>
|
|
<Slider
|
|
value={[compParams.threshold || -24]}
|
|
onValueChange={([value]) => updateParam('threshold', value)}
|
|
min={-60}
|
|
max={0}
|
|
step={0.5}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Ratio: {(compParams.ratio || 4).toFixed(1)}:1
|
|
</label>
|
|
<Slider
|
|
value={[compParams.ratio || 4]}
|
|
onValueChange={([value]) => updateParam('ratio', value)}
|
|
min={1}
|
|
max={20}
|
|
step={0.5}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Knee: {(compParams.knee || 30).toFixed(1)} dB
|
|
</label>
|
|
<Slider
|
|
value={[compParams.knee || 30]}
|
|
onValueChange={([value]) => updateParam('knee', value)}
|
|
min={0}
|
|
max={40}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Attack: {(compParams.attack || 0.003).toFixed(3)} s
|
|
</label>
|
|
<Slider
|
|
value={[compParams.attack || 0.003]}
|
|
onValueChange={([value]) => updateParam('attack', value)}
|
|
min={0.001}
|
|
max={1}
|
|
step={0.001}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Release: {(compParams.release || 0.25).toFixed(3)} s
|
|
</label>
|
|
<Slider
|
|
value={[compParams.release || 0.25]}
|
|
onValueChange={([value]) => updateParam('release', value)}
|
|
min={0.01}
|
|
max={3}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Limiter
|
|
if (effect.type === 'limiter') {
|
|
const limParams = params as LimiterParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Threshold: {(limParams.threshold || -3).toFixed(1)} dB
|
|
</label>
|
|
<Slider
|
|
value={[limParams.threshold || -3]}
|
|
onValueChange={([value]) => updateParam('threshold', value)}
|
|
min={-30}
|
|
max={0}
|
|
step={0.5}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Release: {(limParams.release || 0.05).toFixed(3)} s
|
|
</label>
|
|
<Slider
|
|
value={[limParams.release || 0.05]}
|
|
onValueChange={([value]) => updateParam('release', value)}
|
|
min={0.01}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Makeup: {(limParams.makeupGain || 0).toFixed(1)} dB
|
|
</label>
|
|
<Slider
|
|
value={[limParams.makeupGain || 0]}
|
|
onValueChange={([value]) => updateParam('makeupGain', value)}
|
|
min={0}
|
|
max={20}
|
|
step={0.5}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Gate
|
|
if (effect.type === 'gate') {
|
|
const gateParams = params as GateParameters;
|
|
return (
|
|
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Threshold: {(gateParams.threshold || -40).toFixed(1)} dB
|
|
</label>
|
|
<Slider
|
|
value={[gateParams.threshold || -40]}
|
|
onValueChange={([value]) => updateParam('threshold', value)}
|
|
min={-80}
|
|
max={0}
|
|
step={0.5}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Ratio: {(gateParams.ratio || 10).toFixed(1)}:1
|
|
</label>
|
|
<Slider
|
|
value={[gateParams.ratio || 10]}
|
|
onValueChange={([value]) => updateParam('ratio', value)}
|
|
min={1}
|
|
max={20}
|
|
step={0.5}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Attack: {(gateParams.attack || 0.001).toFixed(3)} s
|
|
</label>
|
|
<Slider
|
|
value={[gateParams.attack || 0.001]}
|
|
onValueChange={([value]) => updateParam('attack', value)}
|
|
min={0.0001}
|
|
max={0.5}
|
|
step={0.0001}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Release: {(gateParams.release || 0.1).toFixed(3)} s
|
|
</label>
|
|
<Slider
|
|
value={[gateParams.release || 0.1]}
|
|
onValueChange={([value]) => updateParam('release', value)}
|
|
min={0.01}
|
|
max={3}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Delay
|
|
if (effect.type === 'delay') {
|
|
const delayParams = params as DelayParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Time: {(delayParams.time || 0.5).toFixed(3)} s
|
|
</label>
|
|
<Slider
|
|
value={[delayParams.time || 0.5]}
|
|
onValueChange={([value]) => updateParam('time', value)}
|
|
min={0.001}
|
|
max={2}
|
|
step={0.001}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Feedback: {((delayParams.feedback || 0.3) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[delayParams.feedback || 0.3]}
|
|
onValueChange={([value]) => updateParam('feedback', value)}
|
|
min={0}
|
|
max={0.9}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Mix: {((delayParams.mix || 0.5) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[delayParams.mix || 0.5]}
|
|
onValueChange={([value]) => updateParam('mix', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Reverb
|
|
if (effect.type === 'reverb') {
|
|
const reverbParams = params as ReverbParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Room Size: {((reverbParams.roomSize || 0.5) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[reverbParams.roomSize || 0.5]}
|
|
onValueChange={([value]) => updateParam('roomSize', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Damping: {((reverbParams.damping || 0.5) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[reverbParams.damping || 0.5]}
|
|
onValueChange={([value]) => updateParam('damping', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Mix: {((reverbParams.mix || 0.3) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[reverbParams.mix || 0.3]}
|
|
onValueChange={([value]) => updateParam('mix', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Chorus
|
|
if (effect.type === 'chorus') {
|
|
const chorusParams = params as ChorusParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Rate: {(chorusParams.rate || 1.5).toFixed(2)} Hz
|
|
</label>
|
|
<Slider
|
|
value={[chorusParams.rate || 1.5]}
|
|
onValueChange={([value]) => updateParam('rate', value)}
|
|
min={0.1}
|
|
max={10}
|
|
step={0.1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Depth: {((chorusParams.depth || 0.002) * 1000).toFixed(2)} ms
|
|
</label>
|
|
<Slider
|
|
value={[chorusParams.depth || 0.002]}
|
|
onValueChange={([value]) => updateParam('depth', value)}
|
|
min={0.0001}
|
|
max={0.01}
|
|
step={0.0001}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Mix: {((chorusParams.mix || 0.5) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[chorusParams.mix || 0.5]}
|
|
onValueChange={([value]) => updateParam('mix', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Flanger
|
|
if (effect.type === 'flanger') {
|
|
const flangerParams = params as FlangerParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Rate: {(flangerParams.rate || 0.5).toFixed(2)} Hz
|
|
</label>
|
|
<Slider
|
|
value={[flangerParams.rate || 0.5]}
|
|
onValueChange={([value]) => updateParam('rate', value)}
|
|
min={0.1}
|
|
max={10}
|
|
step={0.1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Depth: {((flangerParams.depth || 0.002) * 1000).toFixed(2)} ms
|
|
</label>
|
|
<Slider
|
|
value={[flangerParams.depth || 0.002]}
|
|
onValueChange={([value]) => updateParam('depth', value)}
|
|
min={0.0001}
|
|
max={0.01}
|
|
step={0.0001}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Feedback: {((flangerParams.feedback || 0.5) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[flangerParams.feedback || 0.5]}
|
|
onValueChange={([value]) => updateParam('feedback', value)}
|
|
min={0}
|
|
max={0.95}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Mix: {((flangerParams.mix || 0.5) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[flangerParams.mix || 0.5]}
|
|
onValueChange={([value]) => updateParam('mix', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Phaser
|
|
if (effect.type === 'phaser') {
|
|
const phaserParams = params as PhaserParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Rate: {(phaserParams.rate || 0.5).toFixed(2)} Hz
|
|
</label>
|
|
<Slider
|
|
value={[phaserParams.rate || 0.5]}
|
|
onValueChange={([value]) => updateParam('rate', value)}
|
|
min={0.1}
|
|
max={10}
|
|
step={0.1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Depth: {((phaserParams.depth || 0.5) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[phaserParams.depth || 0.5]}
|
|
onValueChange={([value]) => updateParam('depth', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Stages: {phaserParams.stages || 4}
|
|
</label>
|
|
<Slider
|
|
value={[phaserParams.stages || 4]}
|
|
onValueChange={([value]) => updateParam('stages', Math.round(value))}
|
|
min={2}
|
|
max={12}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Mix: {((phaserParams.mix || 0.5) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[phaserParams.mix || 0.5]}
|
|
onValueChange={([value]) => updateParam('mix', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Pitch Shifter
|
|
if (effect.type === 'pitch') {
|
|
const pitchParams = params as PitchShifterParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Semitones: {pitchParams.semitones || 0}
|
|
</label>
|
|
<Slider
|
|
value={[pitchParams.semitones || 0]}
|
|
onValueChange={([value]) => updateParam('semitones', Math.round(value))}
|
|
min={-12}
|
|
max={12}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Cents: {pitchParams.cents || 0}
|
|
</label>
|
|
<Slider
|
|
value={[pitchParams.cents || 0]}
|
|
onValueChange={([value]) => updateParam('cents', Math.round(value))}
|
|
min={-100}
|
|
max={100}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Mix: {((pitchParams.mix || 1) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[pitchParams.mix || 1]}
|
|
onValueChange={([value]) => updateParam('mix', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Time Stretch
|
|
if (effect.type === 'timestretch') {
|
|
const stretchParams = params as TimeStretchParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Rate: {(stretchParams.rate || 1).toFixed(2)}x
|
|
</label>
|
|
<Slider
|
|
value={[stretchParams.rate || 1]}
|
|
onValueChange={([value]) => updateParam('rate', value)}
|
|
min={0.5}
|
|
max={2}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2 py-1 px-2 border-b border-border/30">
|
|
<input
|
|
type="checkbox"
|
|
id={`preserve-pitch-${effect.id}`}
|
|
checked={stretchParams.preservePitch ?? true}
|
|
onChange={(e) => updateParam('preservePitch', e.target.checked)}
|
|
className="h-3 w-3 rounded border-border"
|
|
/>
|
|
<label htmlFor={`preserve-pitch-${effect.id}`} className="text-xs">
|
|
Preserve Pitch
|
|
</label>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Mix: {((stretchParams.mix || 1) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[stretchParams.mix || 1]}
|
|
onValueChange={([value]) => updateParam('mix', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Distortion
|
|
if (effect.type === 'distortion') {
|
|
const distParams = params as DistortionParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">Type</label>
|
|
<div className="grid grid-cols-3 gap-1">
|
|
{(['soft', 'hard', 'tube'] as const).map((type) => (
|
|
<Button
|
|
key={type}
|
|
variant={(distParams.type || 'soft') === type ? 'secondary' : 'outline'}
|
|
size="sm"
|
|
onClick={() => updateParam('type', type)}
|
|
className="text-xs py-1 h-auto"
|
|
>
|
|
{type}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Drive: {((distParams.drive || 0.5) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[distParams.drive || 0.5]}
|
|
onValueChange={([value]) => updateParam('drive', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Tone: {((distParams.tone || 0.5) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[distParams.tone || 0.5]}
|
|
onValueChange={([value]) => updateParam('tone', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Output: {((distParams.output || 0.7) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[distParams.output || 0.7]}
|
|
onValueChange={([value]) => updateParam('output', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Mix: {((distParams.mix || 1) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[distParams.mix || 1]}
|
|
onValueChange={([value]) => updateParam('mix', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Bitcrusher
|
|
if (effect.type === 'bitcrusher') {
|
|
const crushParams = params as BitcrusherParameters;
|
|
return (
|
|
<div className="grid grid-cols-3 gap-x-4 gap-y-2">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Bit Depth: {crushParams.bitDepth || 8} bits
|
|
</label>
|
|
<Slider
|
|
value={[crushParams.bitDepth || 8]}
|
|
onValueChange={([value]) => updateParam('bitDepth', Math.round(value))}
|
|
min={1}
|
|
max={16}
|
|
step={1}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Sample Rate: {crushParams.sampleRate || 8000} Hz
|
|
</label>
|
|
<Slider
|
|
value={[crushParams.sampleRate || 8000]}
|
|
onValueChange={([value]) => updateParam('sampleRate', Math.round(value))}
|
|
min={100}
|
|
max={48000}
|
|
step={100}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-medium">
|
|
Mix: {((crushParams.mix || 1) * 100).toFixed(0)}%
|
|
</label>
|
|
<Slider
|
|
value={[crushParams.mix || 1]}
|
|
onValueChange={([value]) => updateParam('mix', value)}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Fallback for unknown effects
|
|
return (
|
|
<div className="text-xs text-muted-foreground/70 italic text-center py-4">
|
|
No parameters available
|
|
</div>
|
|
);
|
|
}
|