diff --git a/components/tracks/Track.tsx b/components/tracks/Track.tsx
index eff7d6c..78694f5 100644
--- a/components/tracks/Track.tsx
+++ b/components/tracks/Track.tsx
@@ -596,6 +596,27 @@ export function Track({
>
{track.mute ? : }
+
+ {/* Automation Toggle */}
+
diff --git a/lib/audio/automation-utils.ts b/lib/audio/automation-utils.ts
new file mode 100644
index 0000000..9595708
--- /dev/null
+++ b/lib/audio/automation-utils.ts
@@ -0,0 +1,233 @@
+/**
+ * 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));
+}
diff --git a/lib/audio/track-utils.ts b/lib/audio/track-utils.ts
index 0fc144f..7665242 100644
--- a/lib/audio/track-utils.ts
+++ b/lib/audio/track-utils.ts
@@ -5,6 +5,7 @@
import type { Track, TrackColor } from '@/types/track';
import { DEFAULT_TRACK_HEIGHT, TRACK_COLORS } from '@/types/track';
import { createEffectChain } from '@/lib/audio/effects/chain';
+import { createAutomationLane } from '@/lib/audio/automation-utils';
/**
* Generate a unique track ID
@@ -23,8 +24,10 @@ export function createTrack(name?: string, color?: TrackColor): Track {
// Ensure name is always a string, handle cases where event objects might be passed
const trackName = typeof name === 'string' && name.trim() ? name.trim() : 'New Track';
+ const trackId = generateTrackId();
+
return {
- id: generateTrackId(),
+ id: trackId,
name: trackName,
color: TRACK_COLORS[color || randomColor],
height: DEFAULT_TRACK_HEIGHT,
@@ -36,7 +39,22 @@ export function createTrack(name?: string, color?: TrackColor): Track {
recordEnabled: false,
effectChain: createEffectChain(`${trackName} Effects`),
automation: {
- lanes: [],
+ lanes: [
+ createAutomationLane(trackId, 'volume', 'Volume', {
+ min: 0,
+ max: 1,
+ unit: 'dB',
+ }),
+ createAutomationLane(trackId, 'pan', 'Pan', {
+ min: -1,
+ max: 1,
+ formatter: (value: number) => {
+ if (value === 0) return 'C';
+ if (value < 0) return `${Math.abs(value * 100).toFixed(0)}L`;
+ return `${(value * 100).toFixed(0)}R`;
+ },
+ }),
+ ],
showAutomation: false,
},
collapsed: false,