aa1cd8d5de
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.
124 lines
5.1 KiB
JavaScript
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
|
|
};
|
|
})();
|