2026-04-15 16:10:35 -06:00
|
|
|
import { useMemo, useState } from 'react';
|
|
|
|
|
import { useEditorStore } from '../store/editorStore';
|
|
|
|
|
import { Trash2, Volume2 } from 'lucide-react';
|
|
|
|
|
|
2026-04-15 16:36:21 -06:00
|
|
|
interface VolumePanelProps {
|
|
|
|
|
gainMode: boolean;
|
|
|
|
|
onToggleGainMode: () => void;
|
|
|
|
|
timelineGainDb: number;
|
|
|
|
|
onTimelineGainDbChange: (gainDb: number) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function VolumePanel({
|
|
|
|
|
gainMode,
|
|
|
|
|
onToggleGainMode,
|
|
|
|
|
timelineGainDb,
|
|
|
|
|
onTimelineGainDbChange,
|
|
|
|
|
}: VolumePanelProps) {
|
2026-04-15 16:10:35 -06:00
|
|
|
const {
|
|
|
|
|
words,
|
|
|
|
|
selectedWordIndices,
|
|
|
|
|
globalGainDb,
|
|
|
|
|
gainRanges,
|
|
|
|
|
setGlobalGainDb,
|
|
|
|
|
addGainRange,
|
|
|
|
|
updateGainRange,
|
|
|
|
|
removeGainRange,
|
|
|
|
|
} = useEditorStore();
|
|
|
|
|
|
|
|
|
|
const [selectionGainDb, setSelectionGainDb] = useState(3);
|
|
|
|
|
|
|
|
|
|
const canApplySelection = selectedWordIndices.length > 0;
|
|
|
|
|
|
|
|
|
|
const selectedRange = useMemo(() => {
|
|
|
|
|
if (!canApplySelection) return null;
|
|
|
|
|
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
|
|
|
|
|
const startWord = words[sorted[0]];
|
|
|
|
|
const endWord = words[sorted[sorted.length - 1]];
|
|
|
|
|
if (!startWord || !endWord) return null;
|
|
|
|
|
return {
|
|
|
|
|
start: startWord.start,
|
|
|
|
|
end: endWord.end,
|
|
|
|
|
};
|
|
|
|
|
}, [canApplySelection, selectedWordIndices, words]);
|
|
|
|
|
|
|
|
|
|
const applySelectionGain = () => {
|
|
|
|
|
if (!selectedRange) return;
|
|
|
|
|
addGainRange(selectedRange.start, selectedRange.end, selectionGainDb);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-4 space-y-4">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<h3 className="text-sm font-semibold flex items-center gap-2">
|
|
|
|
|
<Volume2 className="w-4 h-4" />
|
|
|
|
|
Volume / Gain
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-xs text-editor-text-muted">
|
|
|
|
|
Apply global gain or per-selection gain ranges.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label className="text-xs text-editor-text-muted font-medium">Global Gain (dB)</label>
|
|
|
|
|
<input
|
|
|
|
|
type="range"
|
|
|
|
|
min={-24}
|
|
|
|
|
max={24}
|
|
|
|
|
step={0.5}
|
|
|
|
|
value={globalGainDb}
|
|
|
|
|
onChange={(e) => setGlobalGainDb(Number(e.target.value))}
|
|
|
|
|
className="w-full"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex items-center justify-between text-xs">
|
|
|
|
|
<span className="text-editor-text-muted">-24 dB</span>
|
|
|
|
|
<span className="font-medium text-editor-text">{globalGainDb.toFixed(1)} dB</span>
|
|
|
|
|
<span className="text-editor-text-muted">+24 dB</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-15 16:36:21 -06:00
|
|
|
<div className="space-y-2 pt-1 border-t border-editor-border">
|
|
|
|
|
<label className="text-xs text-editor-text-muted font-medium">Timeline Gain Zone (dB)</label>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
min={-24}
|
|
|
|
|
max={24}
|
|
|
|
|
step={0.5}
|
|
|
|
|
value={timelineGainDb}
|
|
|
|
|
onChange={(e) => onTimelineGainDbChange(Math.max(-24, Math.min(24, Number(e.target.value) || 0)))}
|
|
|
|
|
className="w-24 px-2 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={onToggleGainMode}
|
|
|
|
|
className={`px-3 py-1.5 text-xs rounded transition-colors ${
|
|
|
|
|
gainMode
|
|
|
|
|
? 'bg-editor-accent text-white hover:bg-editor-accent-hover'
|
|
|
|
|
: 'bg-editor-accent/20 text-editor-accent hover:bg-editor-accent/30'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{gainMode ? 'Exit Zone Mode' : 'Add Gain Zones'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-[11px] text-editor-text-muted">
|
|
|
|
|
In gain zone mode, drag on the timeline to create a zone with this dB value.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-15 16:10:35 -06:00
|
|
|
<div className="space-y-2 pt-1 border-t border-editor-border">
|
|
|
|
|
<label className="text-xs text-editor-text-muted font-medium">Selection Gain (dB)</label>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
min={-24}
|
|
|
|
|
max={24}
|
|
|
|
|
step={0.5}
|
|
|
|
|
value={selectionGainDb}
|
|
|
|
|
onChange={(e) => setSelectionGainDb(Number(e.target.value) || 0)}
|
|
|
|
|
className="w-24 px-2 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={applySelectionGain}
|
|
|
|
|
disabled={!canApplySelection || !selectedRange}
|
|
|
|
|
className="px-3 py-1.5 text-xs rounded bg-editor-accent/20 text-editor-accent hover:bg-editor-accent/30 disabled:opacity-40"
|
|
|
|
|
>
|
|
|
|
|
Apply To Selection
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-[11px] text-editor-text-muted">
|
|
|
|
|
{canApplySelection
|
|
|
|
|
? `${selectedWordIndices.length} selected words${selectedRange ? ` (${selectedRange.start.toFixed(2)}s - ${selectedRange.end.toFixed(2)}s)` : ''}`
|
|
|
|
|
: 'Select transcript words to apply a gain range.'}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{gainRanges.length > 0 && (
|
|
|
|
|
<div className="space-y-2 pt-1 border-t border-editor-border">
|
|
|
|
|
<div className="text-xs font-medium">Gain Ranges</div>
|
|
|
|
|
<div className="max-h-56 overflow-y-auto space-y-1 pr-1">
|
|
|
|
|
{gainRanges.map((range) => (
|
|
|
|
|
<div
|
|
|
|
|
key={range.id}
|
|
|
|
|
className="px-2 py-1.5 rounded bg-editor-surface border border-editor-border text-xs flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<div className="font-medium truncate">
|
|
|
|
|
{range.start.toFixed(2)}s - {range.end.toFixed(2)}s
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-editor-text-muted">{range.id}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
min={-24}
|
|
|
|
|
max={24}
|
|
|
|
|
step={0.5}
|
|
|
|
|
value={range.gainDb}
|
|
|
|
|
onChange={(e) => updateGainRange(range.id, Number(e.target.value) || 0)}
|
|
|
|
|
className="w-20 px-2 py-1 text-xs bg-editor-bg border border-editor-border rounded"
|
|
|
|
|
title="Gain dB"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => removeGainRange(range.id)}
|
|
|
|
|
className="p-1 rounded hover:bg-editor-danger/20 text-editor-danger"
|
|
|
|
|
title="Delete gain range"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|