feat: complete Phase 9.3 - automation recording with write/touch/latch modes

Implemented comprehensive automation recording system for volume, pan, and effect parameters:

- Added automation recording modes:
  - Write: Records continuously during playback when values change
  - Touch: Records only while control is being touched/moved
  - Latch: Records from first touch until playback stops

- Implemented value change detection (0.001 threshold) to prevent infinite loops
- Fixed React setState-in-render errors by:
  - Using queueMicrotask() to defer state updates
  - Moving lane creation logic to useEffect
  - Properly memoizing touch handlers with useMemo

- Added proper value ranges for effect parameters:
  - Frequency: 20-20000 Hz
  - Q: 0.1-20
  - Gain: -40-40 dB

- Enhanced automation lane auto-creation with parameter-specific ranges
- Added touch callbacks to all parameter controls (volume, pan, effects)
- Implemented throttling (100ms) to avoid excessive automation points

Technical improvements:
- Used tracksRef and onRecordAutomationRef to ensure latest values in animation loops
- Added proper cleanup on playback stop
- Optimized recording to only trigger when values actually change

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 23:29:18 +01:00
parent a1f230a6e6
commit c54d5089c5
13 changed files with 1040 additions and 70 deletions

View File

@@ -13,6 +13,8 @@ export interface CircularKnobProps {
className?: string;
label?: string;
formatValue?: (value: number) => string;
onTouchStart?: () => void;
onTouchEnd?: () => void;
}
export function CircularKnob({
@@ -25,6 +27,8 @@ export function CircularKnob({
className,
label,
formatValue,
onTouchStart,
onTouchEnd,
}: CircularKnobProps) {
const knobRef = React.useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = React.useState(false);
@@ -68,8 +72,9 @@ export function CircularKnob({
y: e.clientY,
value,
};
onTouchStart?.();
},
[value]
[value, onTouchStart]
);
const handleMouseMove = React.useCallback(
@@ -83,7 +88,8 @@ export function CircularKnob({
const handleMouseUp = React.useCallback(() => {
setIsDragging(false);
}, []);
onTouchEnd?.();
}, [onTouchEnd]);
React.useEffect(() => {
if (isDragging) {

View File

@@ -13,6 +13,8 @@ export interface SliderProps
step?: number;
label?: string;
showValue?: boolean;
onTouchStart?: () => void;
onTouchEnd?: () => void;
}
const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
@@ -28,6 +30,8 @@ const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
label,
showValue = false,
disabled,
onTouchStart,
onTouchEnd,
...props
},
ref
@@ -41,6 +45,21 @@ const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
onValueChange?.([numValue]);
};
const handleMouseDown = () => {
onTouchStart?.();
};
const handleMouseUp = () => {
onTouchEnd?.();
};
React.useEffect(() => {
if (onTouchEnd) {
window.addEventListener('mouseup', handleMouseUp);
return () => window.removeEventListener('mouseup', handleMouseUp);
}
}, [onTouchEnd]);
return (
<div className={cn('w-full', className)}>
{(label || showValue) && (
@@ -63,6 +82,7 @@ const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
step={step}
value={currentValue}
onChange={handleChange}
onMouseDown={handleMouseDown}
disabled={disabled}
className={cn(
'w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer',

View File

@@ -12,6 +12,8 @@ export interface VerticalFaderProps {
step?: number;
className?: string;
showDb?: boolean;
onTouchStart?: () => void;
onTouchEnd?: () => void;
}
export function VerticalFader({
@@ -23,6 +25,8 @@ export function VerticalFader({
step = 0.01,
className,
showDb = true,
onTouchStart,
onTouchEnd,
}: VerticalFaderProps) {
const trackRef = React.useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = React.useState(false);
@@ -58,8 +62,9 @@ export function VerticalFader({
e.preventDefault();
setIsDragging(true);
updateValue(e.clientY);
onTouchStart?.();
},
[updateValue]
[updateValue, onTouchStart]
);
const handleMouseMove = React.useCallback(
@@ -73,7 +78,8 @@ export function VerticalFader({
const handleMouseUp = React.useCallback(() => {
setIsDragging(false);
}, []);
onTouchEnd?.();
}, [onTouchEnd]);
React.useEffect(() => {
if (isDragging) {