/** * Project management service */ import type { Track } from '@/types/track'; import { saveProject, loadProject, getAllProjects, deleteProject, serializeAudioBuffer, deserializeAudioBuffer, type ProjectData, type SerializedTrack, } from './db'; import type { ProjectMetadata } from './db'; import { getAudioContext } from '../audio/context'; import { generateId } from '../audio/effects/chain'; // Re-export ProjectMetadata for easier importing export type { ProjectMetadata } from './db'; /** * Generate unique project ID */ export function generateProjectId(): string { return `project_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Serialize effects by removing any non-serializable data (functions, nodes, etc.) */ function serializeEffects(effects: any[]): any[] { return effects.map(effect => ({ id: effect.id, type: effect.type, name: effect.name, enabled: effect.enabled, expanded: effect.expanded, parameters: effect.parameters ? JSON.parse(JSON.stringify(effect.parameters)) : undefined, })); } /** * Convert tracks to serialized format */ function serializeTracks(tracks: Track[]): SerializedTrack[] { return tracks.map(track => { // Serialize automation by deep cloning to remove any functions const automation = track.automation ? JSON.parse(JSON.stringify(track.automation)) : { lanes: [], showAutomation: false }; return { id: track.id, name: track.name, color: track.color, volume: track.volume, pan: track.pan, muted: track.mute, soloed: track.solo, collapsed: track.collapsed, height: track.height, audioBuffer: track.audioBuffer ? serializeAudioBuffer(track.audioBuffer) : null, effects: serializeEffects(track.effectChain?.effects || []), automation, recordEnabled: track.recordEnabled, }; }); } /** * Convert serialized tracks back to Track format */ function deserializeTracks(serialized: SerializedTrack[]): Track[] { const audioContext = getAudioContext(); return serialized.map(track => ({ id: track.id, name: track.name, color: track.color, volume: track.volume, pan: track.pan, mute: track.muted, solo: track.soloed, collapsed: track.collapsed, height: track.height, audioBuffer: track.audioBuffer ? deserializeAudioBuffer(track.audioBuffer, audioContext) : null, effectChain: { id: generateId(), name: `${track.name} FX`, effects: track.effects, }, automation: track.automation, recordEnabled: track.recordEnabled, selected: false, showEffects: false, selection: null, // Reset selection on load })); } /** * Calculate total project duration */ function calculateDuration(tracks: Track[]): number { let maxDuration = 0; for (const track of tracks) { if (track.audioBuffer) { maxDuration = Math.max(maxDuration, track.audioBuffer.duration); } } return maxDuration; } /** * Save current project state */ export async function saveCurrentProject( projectId: string | null, projectName: string, tracks: Track[], settings: { zoom: number; currentTime: number; sampleRate: number; }, description?: string ): Promise { const id = projectId || generateProjectId(); const now = Date.now(); const metadata: ProjectMetadata = { id, name: projectName, description, createdAt: projectId ? (await loadProject(id))?.metadata.createdAt || now : now, updatedAt: now, duration: calculateDuration(tracks), sampleRate: settings.sampleRate, trackCount: tracks.length, }; const projectData: ProjectData = { metadata, tracks: serializeTracks(tracks), settings, }; await saveProject(projectData); return id; } /** * Load project and restore state */ export async function loadProjectById(projectId: string): Promise<{ tracks: Track[]; settings: { zoom: number; currentTime: number; sampleRate: number; }; metadata: ProjectMetadata; } | null> { const project = await loadProject(projectId); if (!project) return null; return { tracks: deserializeTracks(project.tracks), settings: project.settings, metadata: project.metadata, }; } /** * Get list of all projects */ export async function listProjects(): Promise { return getAllProjects(); } /** * Delete a project */ export async function removeProject(projectId: string): Promise { return deleteProject(projectId); } /** * Duplicate a project */ export async function duplicateProject(sourceProjectId: string, newName: string): Promise { const project = await loadProject(sourceProjectId); if (!project) throw new Error('Project not found'); const newId = generateProjectId(); const now = Date.now(); const newProject: ProjectData = { ...project, metadata: { ...project.metadata, id: newId, name: newName, createdAt: now, updatedAt: now, }, }; await saveProject(newProject); return newId; } /** * Export project as JSON file */ export async function exportProjectAsJSON(projectId: string): Promise { const project = await loadProject(projectId); if (!project) throw new Error('Project not found'); // Convert the project to JSON const json = JSON.stringify(project, null, 2); // Create blob and download const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${project.metadata.name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${Date.now()}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * Import project from JSON file */ export async function importProjectFromJSON(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = async (e) => { try { const json = e.target?.result as string; const project = JSON.parse(json) as ProjectData; // Generate new ID to avoid conflicts const newId = generateProjectId(); const now = Date.now(); const importedProject: ProjectData = { ...project, metadata: { ...project.metadata, id: newId, name: `${project.metadata.name} (Imported)`, createdAt: now, updatedAt: now, }, }; await saveProject(importedProject); resolve(newId); } catch (error) { reject(new Error('Failed to parse project file')); } }; reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsText(file); }); }