Files
audio-ui/lib/audio/automation/utils.ts

186 lines
4.3 KiB
TypeScript
Raw Normal View History

feat: Ableton Live-style effects and complete automation system Enhanced visual design: - Improved device rack container with darker background and inner shadow - Device cards now have rounded corners, shadows, and colored indicators - Better visual separation between enabled/disabled effects - Active devices highlighted with accent border Complete automation infrastructure (Phase 9): - Created comprehensive type system for automation lanes and points - Implemented AutomationPoint component with drag-and-drop editing - Implemented AutomationHeader with mode controls (Read/Write/Touch/Latch) - Implemented AutomationLane with canvas-based curve rendering - Integrated automation lanes into Track component below effects - Created automation playback engine with real-time interpolation - Added automation data persistence to localStorage Automation features: - Add/remove automation points by clicking/double-clicking - Drag points to change time and value - Multiple automation modes (Read, Write, Touch, Latch) - Linear and step curve types (bezier planned) - Adjustable lane height (60-180px) - Show/hide automation per lane - Real-time value display at playhead - Color-coded lanes by parameter type - Keyboard delete support (Delete/Backspace) Track type updates: - Added automation field to Track interface - Updated track creation to initialize empty automation - Updated localStorage save/load to include automation data Files created: - components/automation/AutomationPoint.tsx - components/automation/AutomationHeader.tsx - components/automation/AutomationLane.tsx - lib/audio/automation/utils.ts (helper functions) - lib/audio/automation/playback.ts (playback engine) - types/automation.ts (complete type system) Files modified: - components/effects/EffectDevice.tsx (Ableton-style visual improvements) - components/tracks/Track.tsx (automation lanes integration) - types/track.ts (automation field added) - lib/audio/track-utils.ts (automation initialization) - lib/hooks/useMultiTrack.ts (automation persistence) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:30:01 +01:00
/**
* 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<CreateAutomationLaneInput>
): 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;
}
// Linear interpolation
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;
}
/**
* 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;
}