/** * Automation utility functions for creating and manipulating automation data */ import type { AutomationLane, AutomationPoint, AutomationCurveType, AutomationMode, CreateAutomationPointInput, } from '@/types/automation'; /** * Generate unique automation point ID */ export function generateAutomationPointId(): string { return `autopoint-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Generate unique automation lane ID */ export function generateAutomationLaneId(): string { return `autolane-${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: string, parameterName: string, valueRange: { min: number; max: number; unit?: string; formatter?: (value: number) => string; } ): AutomationLane { return { id: generateAutomationLaneId(), trackId, parameterId, parameterName, visible: true, height: 80, points: [], mode: 'read', valueRange, }; } /** * Linear interpolation between two values */ function lerp(a: number, b: number, t: number): number { return a + (b - a) * t; } /** * Evaluate automation value at a specific time using linear interpolation */ export function evaluateAutomationLinear( points: AutomationPoint[], time: number ): number { if (points.length === 0) return 0.5; // Default middle value if (points.length === 1) return points[0].value; // Sort points by time (should already be sorted, but ensure it) 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 p1 = sortedPoints[i]; const p2 = sortedPoints[i + 1]; if (time >= p1.time && time <= p2.time) { // Handle step curve if (p1.curve === 'step') { return p1.value; } // Linear interpolation const t = (time - p1.time) / (p2.time - p1.time); return lerp(p1.value, p2.value, t); } } return sortedPoints[sortedPoints.length - 1].value; } /** * Add an automation point to a lane, maintaining time-sorted order */ export function addAutomationPoint( lane: AutomationLane, point: CreateAutomationPointInput ): AutomationLane { const newPoint = createAutomationPoint(point); const points = [...lane.points, newPoint].sort((a, b) => a.time - b.time); return { ...lane, points, }; } /** * Remove an automation point by ID */ export function removeAutomationPoint( lane: AutomationLane, pointId: string ): AutomationLane { return { ...lane, points: lane.points.filter((p) => p.id !== pointId), }; } /** * Update an automation point's time and/or value */ export function updateAutomationPoint( lane: AutomationLane, pointId: string, updates: { time?: number; value?: number; curve?: AutomationCurveType } ): AutomationLane { const points = lane.points.map((p) => p.id === pointId ? { ...p, ...updates } : p ); // Re-sort by time if time was updated if (updates.time !== undefined) { points.sort((a, b) => a.time - b.time); } return { ...lane, points, }; } /** * Remove all automation points in a time range */ export function clearAutomationRange( lane: AutomationLane, startTime: number, endTime: number ): AutomationLane { return { ...lane, points: lane.points.filter((p) => p.time < startTime || p.time > endTime), }; } /** * Format automation value for display based on lane's value range */ export function formatAutomationValue( lane: AutomationLane, normalizedValue: number ): string { const { min, max, unit, formatter } = lane.valueRange; if (formatter) { const actualValue = lerp(min, max, normalizedValue); return formatter(actualValue); } const actualValue = lerp(min, max, normalizedValue); // Format based on unit if (unit === 'dB') { // Convert to dB scale const db = normalizedValue === 0 ? -Infinity : 20 * Math.log10(normalizedValue); return db === -Infinity ? '-∞ dB' : `${db.toFixed(1)} dB`; } if (unit === '%') { return `${(actualValue * 100).toFixed(0)}%`; } if (unit === 'ms') { return `${actualValue.toFixed(1)} ms`; } if (unit === 'Hz') { return `${actualValue.toFixed(0)} Hz`; } // Default: 2 decimal places with unit return unit ? `${actualValue.toFixed(2)} ${unit}` : actualValue.toFixed(2); } /** * Snap value to grid (useful for user input) */ export function snapToGrid(value: number, gridSize: number = 0.25): number { return Math.round(value / gridSize) * gridSize; } /** * Clamp value between 0 and 1 */ export function clampNormalized(value: number): number { return Math.max(0, Math.min(1, value)); }