/** * Automation utility functions */ import type { AutomationLane, AutomationPoint, CreateAutomationLaneInput, CreateAutomationPointInput, AutomationParameterId, } from '@/types/automation'; /** * Generate a unique automation point ID */ export function generateAutomationPointId(): string { return `point-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Generate a unique automation lane ID */ export function generateAutomationLaneId(): string { return `lane-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Create a new automation point */ export function createAutomationPoint( input: CreateAutomationPointInput ): AutomationPoint { return { id: generateAutomationPointId(), ...input, }; } /** * Create a new automation lane */ export function createAutomationLane( trackId: string, parameterId: AutomationParameterId, parameterName: string, input?: Partial ): AutomationLane { return { id: generateAutomationLaneId(), trackId, parameterId, parameterName, visible: input?.visible ?? true, height: input?.height ?? 80, points: input?.points ?? [], mode: input?.mode ?? 'read', color: input?.color, valueRange: input?.valueRange ?? { min: 0, max: 1, }, }; } /** * Create a volume automation lane */ export function createVolumeAutomationLane(trackId: string): AutomationLane { return createAutomationLane(trackId, 'volume', 'Volume', { valueRange: { min: 0, max: 1, formatter: (value) => `${(value * 100).toFixed(0)}%`, }, color: 'rgb(34, 197, 94)', // green }); } /** * Create a pan automation lane */ export function createPanAutomationLane(trackId: string): AutomationLane { return createAutomationLane(trackId, 'pan', 'Pan', { valueRange: { min: -1, max: 1, formatter: (value) => { const normalized = value * 2 - 1; // Convert 0-1 to -1-1 if (normalized === 0) return 'C'; if (normalized < 0) return `L${Math.abs(Math.round(normalized * 100))}`; return `R${Math.round(normalized * 100)}`; }, }, color: 'rgb(59, 130, 246)', // blue }); } /** * Interpolate automation value at a specific time */ export function interpolateAutomationValue( points: AutomationPoint[], time: number ): number { if (points.length === 0) return 0; const sortedPoints = [...points].sort((a, b) => a.time - b.time); // Before first point if (time <= sortedPoints[0].time) { return sortedPoints[0].value; } // After last point if (time >= sortedPoints[sortedPoints.length - 1].time) { return sortedPoints[sortedPoints.length - 1].value; } // Find surrounding points for (let i = 0; i < sortedPoints.length - 1; i++) { const prevPoint = sortedPoints[i]; const nextPoint = sortedPoints[i + 1]; if (time >= prevPoint.time && time <= nextPoint.time) { // Handle step curve if (prevPoint.curve === 'step') { return prevPoint.value; } // Handle bezier curve if (prevPoint.curve === 'bezier') { const timeDelta = nextPoint.time - prevPoint.time; const t = (time - prevPoint.time) / timeDelta; return interpolateBezier(prevPoint, nextPoint, t); } // Linear interpolation (default) const timeDelta = nextPoint.time - prevPoint.time; const valueDelta = nextPoint.value - prevPoint.value; const progress = (time - prevPoint.time) / timeDelta; return prevPoint.value + valueDelta * progress; } } return 0; } /** * Interpolate value using cubic Bezier curve * Uses the control handles from both points to create smooth curves */ function interpolateBezier( p0: AutomationPoint, p1: AutomationPoint, t: number ): number { // Default handle positions if not specified // Out handle defaults to 1/3 towards next point // In handle defaults to 1/3 back from current point const timeDelta = p1.time - p0.time; // Control point 1 (out handle from p0) const c1x = p0.handleOut?.x ?? timeDelta / 3; const c1y = p0.handleOut?.y ?? 0; // Control point 2 (in handle from p1) const c2x = p1.handleIn?.x ?? -timeDelta / 3; const c2y = p1.handleIn?.y ?? 0; // Convert handles to absolute positions const cp1Value = p0.value + c1y; const cp2Value = p1.value + c2y; // Cubic Bezier formula: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ const mt = 1 - t; const mt2 = mt * mt; const mt3 = mt2 * mt; const t2 = t * t; const t3 = t2 * t; const value = mt3 * p0.value + 3 * mt2 * t * cp1Value + 3 * mt * t2 * cp2Value + t3 * p1.value; return value; } /** * Create smooth bezier handles for a point based on surrounding points * This creates an "auto-smooth" effect similar to DAWs */ export function createSmoothHandles( prevPoint: AutomationPoint | null, currentPoint: AutomationPoint, nextPoint: AutomationPoint | null ): { handleIn: { x: number; y: number }; handleOut: { x: number; y: number } } { // If no surrounding points, return horizontal handles if (!prevPoint && !nextPoint) { return { handleIn: { x: -0.1, y: 0 }, handleOut: { x: 0.1, y: 0 }, }; } // Calculate slope from surrounding points let slope = 0; if (prevPoint && nextPoint) { // Use average slope from both neighbors const timeDelta = nextPoint.time - prevPoint.time; const valueDelta = nextPoint.value - prevPoint.value; slope = valueDelta / timeDelta; } else if (nextPoint) { // Only have next point const timeDelta = nextPoint.time - currentPoint.time; const valueDelta = nextPoint.value - currentPoint.value; slope = valueDelta / timeDelta; } else if (prevPoint) { // Only have previous point const timeDelta = currentPoint.time - prevPoint.time; const valueDelta = currentPoint.value - prevPoint.value; slope = valueDelta / timeDelta; } // Create handles with 1/3 distance to neighbors const handleDistance = 0.1; // Fixed distance for smooth curves const handleY = slope * handleDistance; return { handleIn: { x: -handleDistance, y: -handleY }, handleOut: { x: handleDistance, y: handleY }, }; } /** * Generate points along a bezier curve for rendering * Returns array of {time, value} points */ export function generateBezierCurvePoints( p0: AutomationPoint, p1: AutomationPoint, numPoints: number = 50 ): Array<{ time: number; value: number }> { const points: Array<{ time: number; value: number }> = []; const timeDelta = p1.time - p0.time; for (let i = 0; i <= numPoints; i++) { const t = i / numPoints; const time = p0.time + t * timeDelta; const value = interpolateBezier(p0, p1, t); points.push({ time, value }); } return points; } /** * Apply automation value to track parameter */ export function applyAutomationToTrack( track: any, parameterId: AutomationParameterId, value: number ): any { if (parameterId === 'volume') { return { ...track, volume: value }; } if (parameterId === 'pan') { // Convert 0-1 to -1-1 return { ...track, pan: value * 2 - 1 }; } // Effect parameters (format: "effect.{effectId}.{paramName}") if (parameterId.startsWith('effect.')) { const parts = parameterId.split('.'); if (parts.length === 3) { const [, effectId, paramName] = parts; return { ...track, effectChain: { ...track.effectChain, effects: track.effectChain.effects.map((effect: any) => effect.id === effectId ? { ...effect, parameters: { ...effect.parameters, [paramName]: value, }, } : effect ), }, }; } } return track; }