refactor: export/import projects as ZIP files instead of JSON
Changed from single JSON file to ZIP archive format to avoid string length limits when serializing large audio buffers. New ZIP structure: - project.json (metadata, track info, effects, automation) - track_0.wav, track_1.wav, etc. (audio files in WAV format) Benefits: - No more RangeError: Invalid string length - Smaller file sizes (WAV compression vs base64 JSON) - Human-readable audio files in standard format - Can extract and inspect individual tracks - Easier to edit/debug project metadata Added jszip dependency for ZIP file handling. Changed file picker to accept .zip files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
deserializeAudioBuffer,
|
||||
type ProjectData,
|
||||
type SerializedTrack,
|
||||
type SerializedAudioBuffer,
|
||||
} from './db';
|
||||
import type { ProjectMetadata } from './db';
|
||||
import { getAudioContext } from '../audio/context';
|
||||
@@ -212,21 +213,55 @@ export async function duplicateProject(sourceProjectId: string, newName: string)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export project as JSON file
|
||||
* Export project as ZIP file with separate audio files
|
||||
*/
|
||||
export async function exportProjectAsJSON(projectId: string): Promise<void> {
|
||||
const JSZip = (await import('jszip')).default;
|
||||
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);
|
||||
const zip = new JSZip();
|
||||
const audioContext = getAudioContext();
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
// Create metadata without audio buffers
|
||||
const metadata = {
|
||||
...project,
|
||||
tracks: project.tracks.map((track, index) => ({
|
||||
...track,
|
||||
audioBuffer: track.audioBuffer ? {
|
||||
fileName: `track_${index}.wav`,
|
||||
sampleRate: track.audioBuffer.sampleRate,
|
||||
length: track.audioBuffer.length,
|
||||
numberOfChannels: track.audioBuffer.numberOfChannels,
|
||||
} : null,
|
||||
})),
|
||||
};
|
||||
|
||||
// Add project.json to ZIP
|
||||
zip.file('project.json', JSON.stringify(metadata, null, 2));
|
||||
|
||||
// Convert audio buffers to WAV and add to ZIP
|
||||
for (let i = 0; i < project.tracks.length; i++) {
|
||||
const track = project.tracks[i];
|
||||
if (track.audioBuffer) {
|
||||
// Deserialize audio buffer
|
||||
const buffer = deserializeAudioBuffer(track.audioBuffer, audioContext);
|
||||
|
||||
// Convert to WAV
|
||||
const { audioBufferToWav } = await import('@/lib/audio/export');
|
||||
const wavBlob = await audioBufferToWav(buffer);
|
||||
|
||||
// Add to ZIP
|
||||
zip.file(`track_${i}.wav`, wavBlob);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate ZIP and download
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
const url = URL.createObjectURL(zipBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${project.metadata.name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${Date.now()}.json`;
|
||||
a.download = `${project.metadata.name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${Date.now()}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
@@ -234,40 +269,69 @@ export async function exportProjectAsJSON(projectId: string): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Import project from JSON file
|
||||
* Import project from ZIP file
|
||||
*/
|
||||
export async function importProjectFromJSON(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
const JSZip = (await import('jszip')).default;
|
||||
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const json = e.target?.result as string;
|
||||
const project = JSON.parse(json) as ProjectData;
|
||||
try {
|
||||
const zip = await JSZip.loadAsync(file);
|
||||
|
||||
// Generate new ID to avoid conflicts
|
||||
const newId = generateProjectId();
|
||||
const now = Date.now();
|
||||
// Read project.json
|
||||
const projectJsonFile = zip.file('project.json');
|
||||
if (!projectJsonFile) throw new Error('Invalid project file: missing project.json');
|
||||
|
||||
const importedProject: ProjectData = {
|
||||
...project,
|
||||
metadata: {
|
||||
...project.metadata,
|
||||
id: newId,
|
||||
name: `${project.metadata.name} (Imported)`,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
};
|
||||
const projectJson = await projectJsonFile.async('text');
|
||||
const metadata = JSON.parse(projectJson);
|
||||
|
||||
await saveProject(importedProject);
|
||||
resolve(newId);
|
||||
} catch (error) {
|
||||
reject(new Error('Failed to parse project file'));
|
||||
// Read audio files and reconstruct tracks
|
||||
const audioContext = getAudioContext();
|
||||
const tracks: SerializedTrack[] = [];
|
||||
|
||||
for (let i = 0; i < metadata.tracks.length; i++) {
|
||||
const trackMeta = metadata.tracks[i];
|
||||
let audioBuffer: SerializedAudioBuffer | null = null;
|
||||
|
||||
if (trackMeta.audioBuffer?.fileName) {
|
||||
const audioFile = zip.file(trackMeta.audioBuffer.fileName);
|
||||
if (audioFile) {
|
||||
// Read WAV file as array buffer
|
||||
const arrayBuffer = await audioFile.async('arraybuffer');
|
||||
|
||||
// Decode audio data
|
||||
const decodedBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
|
||||
// Serialize for storage
|
||||
audioBuffer = serializeAudioBuffer(decodedBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
tracks.push({
|
||||
...trackMeta,
|
||||
audioBuffer,
|
||||
});
|
||||
}
|
||||
|
||||
// Generate new ID to avoid conflicts
|
||||
const newId = generateProjectId();
|
||||
const now = Date.now();
|
||||
|
||||
const importedProject: ProjectData = {
|
||||
...metadata,
|
||||
tracks,
|
||||
metadata: {
|
||||
...metadata.metadata,
|
||||
id: newId,
|
||||
name: `${metadata.metadata.name} (Imported)`,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
};
|
||||
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
await saveProject(importedProject);
|
||||
return newId;
|
||||
} catch (error) {
|
||||
console.error('Import error:', error);
|
||||
throw new Error('Failed to import project file');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user