Files
SonicForgeStudio/app/static/js/services/storage.js
T
3dtours aa1cd8d5de fix: 3 bug standalone — soundfont preview/play, Carla bridge play item, realtime sync LAN/Tauri
Bug1: dispatcher CREATE_MIDI_ITEM track_id number vs state string -> String(); play+preview native soundfont verified.
Bug2: Carla play item — chờ OSC ready (poll carla-status, queue nốt khi cold start), chống spawn trùng (carla-status dò port bind, open-in-carla skip khi đã chạy).
Bug3: realtime sync — GET /temp/revision (updated_at+client_id), poll 3s, apply qua deserializeProjectFromSchema; hash-skip autosave + client_id chống ping-pong.
2026-08-10 22:10:12 +07:00

124 lines
5.1 KiB
JavaScript

// SonicForge Studio Project Storage & .sfs File Service
(function() {
const SFS_VERSION = "1.0.0";
function exportProjectToSFS(projectState) {
let projectObj = {};
if (projectState.main_session) {
projectObj = projectState;
} else {
projectObj = {
id: projectState.id || `proj_${Date.now()}`,
name: projectState.name || "Dự án mới",
tracks: (projectState.tracks || []).map(t => ({
id: t.id,
name: t.name,
startTime: t.startTime,
height: t.height,
volumeDb: t.volumeDb,
pan: t.pan,
muted: t.muted,
solo: t.solo,
color: t.color,
markers: t.markers || [],
serverFileId: t.serverFileId || null,
clips: t.clips || [],
sections: t.sections || [],
midiItems: t.midiItems || [],
channelInfo: t.channelInfo || null
}))
};
}
const sfsBundle = {
format: "SONICFORGE_STUDIO_PROJECT",
version: SFS_VERSION,
timestamp: Date.now(),
domain: window.location.origin,
project: projectObj
};
const jsonStr = JSON.stringify(sfsBundle, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const displayName = projectObj.metadata?.title || projectObj.name || 'project';
a.download = `${displayName.replace(/\s+/g, '_')}.sfs`;
a.click();
URL.revokeObjectURL(url);
}
async function importProjectFromSFSFile(file) {
const text = await file.text();
const sfsBundle = JSON.parse(text);
if (sfsBundle.format !== "SONICFORGE_STUDIO_PROJECT") {
throw new Error("Tệp tin không đúng định dạng .sfs của SonicForge Studio");
}
return sfsBundle.project;
}
let autoSaveTimer = null;
let lastGetProjectStateCallback = null;
// ⚠️ FIX (Bug 3): hash-skip — nếu state serialize GIỐNG HỆT bản đã lưu
// thì KHÔNG ghi lại (server + localStorage). Chặn ping-pong realtime sync:
// client B apply state của A rồi autosave lại → server bump updated_at →
// A tưởng mới → apply → ... vô hạn. Bản giống hệt → bỏ qua → hội tụ.
let lastSavedJson = '';
function getClientId() {
try {
let c = localStorage.getItem('sonic_client_id');
if (!c) {
c = 'c_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
localStorage.setItem('sonic_client_id', c);
}
return c;
} catch (e) { return ''; }
}
function scheduleTempAutoSave(getProjectStateCallback) {
if (getProjectStateCallback) lastGetProjectStateCallback = getProjectStateCallback;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(async () => {
try {
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
if (!state || (!state.tracks && !state.main_session)) return;
const dataJson = JSON.stringify(state);
if (dataJson === lastSavedJson) return; // không đổi → không ghi
lastSavedJson = dataJson;
localStorage.setItem('sonic_temp_project', dataJson);
if (window.SonicAPI && localStorage.getItem('sonic_token')) {
await window.SonicAPI.saveTempProject(dataJson, getClientId()).catch(() => {});
}
} catch (e) {
console.warn("Auto-save temp project warning:", e);
}
}, 2000);
}
// Lưu NGAY (bỏ debounce 2s) — dùng cho thay đổi cần bền vững tức thì (đổi màu track)
async function flushTempAutoSave() {
if (autoSaveTimer) { clearTimeout(autoSaveTimer); autoSaveTimer = null; }
try {
const state = lastGetProjectStateCallback ? lastGetProjectStateCallback() : null;
if (!state || (!state.tracks && !state.main_session)) return;
const dataJson = JSON.stringify(state);
if (dataJson === lastSavedJson) return; // không đổi → không ghi
lastSavedJson = dataJson;
localStorage.setItem('sonic_temp_project', dataJson);
if (window.SonicAPI && localStorage.getItem('sonic_token')) {
await window.SonicAPI.saveTempProject(dataJson, getClientId()).catch(() => {});
}
} catch (e) {
console.warn("Flush temp project warning:", e);
}
}
window.SonicStorage = {
exportProjectToSFS,
importProjectFromSFSFile,
scheduleTempAutoSave,
flushTempAutoSave,
getClientId
};
})();