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.
This commit is contained in:
2026-08-10 22:10:12 +07:00
parent d31e226f00
commit aa1cd8d5de
7 changed files with 238 additions and 32 deletions
+110 -15
View File
@@ -89,6 +89,23 @@ const ensureSonicInstrument = (ctx) => {
// track dùng VSTi + có Carla local. Dùng chung cho mi đưng playback
// (main timeline, local loop, piano roll, ghost notes) đ MIDI item PHI play
// qua Carla bridge khi VSTi đưc loaded.
const fireCarlaNote = (ch, pitch, vel, delay, durMs) => {
setTimeout(function () { window.SonicCarlaMidi.noteOn(ch, pitch, vel); }, delay);
setTimeout(function () { window.SonicCarlaMidi.noteOff(ch, pitch); }, delay + durMs + 30);
};
// FIX (Bug 2): Carla m ASYNC lúc play (ensureCarlaForPlayback) nt gi
// trưc khi OSC engine bind cng b mt item câm dù keybed kêu. Nt khi
// chưa ready đưc nhét queue, flush khi bridge OSC ready (gi đúng th t).
const flushCarlaNoteQueue = () => {
const q = window.__carlaNoteQueue || [];
if (!q.length) return;
window.__carlaNoteQueue = [];
q.forEach(item => {
const remaining = item.delay - (Date.now() - item.scheduledAt);
if (remaining <= 0) fireCarlaNote(item.ch, item.pitch, item.vel, 0, item.durMs);
else fireCarlaNote(item.ch, item.pitch, item.vel, remaining, item.durMs);
});
};
const scheduleCarlaNote = (synthEngine, channel, pitch, velocity, startWallTime, durMs) => {
try {
if (!window.SonicCarlaMidi || !window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) return;
@@ -99,13 +116,19 @@ const scheduleCarlaNote = (synthEngine, channel, pitch, velocity, startWallTime,
// Chn đoán: note MIDI item Carla (bt console.log đ verify route)
if (window.__carlaNoteLog === undefined) window.__carlaNoteLog = (window.__carlaNoteLog || 0) + 1;
console.log('[Carla] item note ch=' + carlaCh + ' pitch=' + pitch + ' vel=' + carlaVel + ' delay=' + delay.toFixed(0) + 'ms dur=' + (durMs || 300) + 'ms');
setTimeout(function () { window.SonicCarlaMidi.noteOn(carlaCh, pitch, carlaVel); }, delay);
setTimeout(function () { window.SonicCarlaMidi.noteOff(carlaCh, pitch); }, delay + (durMs || 300) + 30);
const dur = durMs || 300;
if (window.__carlaRunning !== true) {
window.__carlaNoteQueue = window.__carlaNoteQueue || [];
window.__carlaNoteQueue.push({ ch: carlaCh, pitch, vel: carlaVel, scheduledAt: Date.now(), delay, durMs: dur });
return;
}
fireCarlaNote(carlaCh, pitch, carlaVel, delay, dur);
} catch (e) { console.warn('[Carla] scheduleCarlaNote error:', e); }
};
// Carla bridge alive tracking + auto-open
// window.__carlaRunning: undefined = chưa biết | true = đang chy | false = đã chết.
// window.__carlaNoteQueue: nt ch flush khi Carla chưa ready (cold start).
// Quyết đnh route MIDI item EXCLUSIVE qua Carla hay fallback FluidSynth
// tránh "câm toàn phn" khi Carla b đóng (route ch-Carla mà Carla chết = im lng).
const refreshCarlaStatus = () => {
@@ -113,26 +136,43 @@ const refreshCarlaStatus = () => {
if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return;
window.SonicAPI.carlaStatus().then(st => {
window.__carlaRunning = !!(st && st.running);
if (window.__carlaRunning) flushCarlaNoteQueue();
}).catch(() => { window.__carlaRunning = false; });
} catch (e) {}
};
// Carla chưa chy T ĐNG m vi VSTi ca track (fire-and-forget) đ MIDI
// item play qua Carla bridge đúng yêu cu.
// Carla chưa chy T ĐNG m vi VSTi ca track ri CH OSC ready (poll
// carla-status ti đa 10s) nt ch gi khi bridge thc s nhn đưc.
// Có gate __carlaOpening chng spawn trùng (mi play ch m 1 ln).
const ensureCarlaForPlayback = (synthEngine) => {
try {
if (!window.SonicCarlaMidi || !window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) return;
if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return;
if (window.__carlaRunning === true) { flushCarlaNoteQueue(); return; }
if (window.__carlaOpening) return;
window.__carlaOpening = true;
const finish = (ok) => {
window.__carlaRunning = !!ok;
window.__carlaOpening = false;
flushCarlaNoteQueue();
};
window.SonicAPI.carlaStatus().then(st => {
if (st && st.running) { window.__carlaRunning = true; return; }
window.__carlaRunning = false;
if (st && st.running) { finish(true); return; }
const pid = synthEngine && synthEngine.plugin_id;
if (pid) {
window.SonicAPI.openInCarla(pid).then(r => {
if (r && r.success) { window.__carlaRunning = true; }
else { window.__carlaRunning = false; }
}).catch(() => { window.__carlaRunning = false; });
}
}).catch(() => { window.__carlaRunning = false; });
if (!pid) { finish(false); return; }
window.SonicAPI.openInCarla(pid).then(r => {
if (!r || !r.success) { finish(false); return; }
// Carla spawn (hoc đã chy) poll ti khi OSC ready, mi flush nt
const deadline = Date.now() + 10000;
const tick = () => {
window.SonicAPI.carlaStatus().then(s2 => {
if (s2 && s2.running) { finish(true); return; }
if (Date.now() > deadline) { finish(false); return; }
setTimeout(tick, 400);
}).catch(() => finish(false));
};
tick();
}).catch(() => finish(false));
}).catch(() => finish(false));
} catch (e) {}
};
@@ -17312,7 +17352,8 @@ const App = () => {
const buildTempPayload = () => {
try {
const schemaObj = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings);
return JSON.stringify({ data_json: JSON.stringify(schemaObj) });
const cid = (window.SonicStorage && window.SonicStorage.getClientId) ? window.SonicStorage.getClientId() : '';
return JSON.stringify({ data_json: JSON.stringify(schemaObj), client_id: cid });
} catch (e) { return null; }
};
const saveTempServer = () => {
@@ -17348,6 +17389,59 @@ const App = () => {
};
}, [currentProjectId, projectName, bpm, tracks, subTabs, sessionTabs, masteringSettings]);
// Realtime sync gia các client (LAN browser Tauri standalone)
// Bug 3: 2 session cùng tài khon không sync. Poll revision nh 3s; khi
// updated_at mi hơn bn đã apply + client_id KHÁC mình ti full temp
// deserialize apply. Skip nếu data_json ging ht bn cui (chng churn
// khi 30s writer bump updated_at vi ni dung không đi). Ping-pong chn
// bi: b qua bn do mình ghi (client_id) + hash-skip khi autosave.
useEffect(() => {
if (!window.SonicAPI || !window.SonicAPI.getTempRevision) return;
const myId = (window.SonicStorage && window.SonicStorage.getClientId) ? window.SonicStorage.getClientId() : '';
let lastAppliedAt = 0;
let lastAppliedJson = '';
let timer = null;
let stopped = false;
const applyRemote = async () => {
try {
const rev = await window.SonicAPI.getTempRevision();
if (!rev || !rev.updated_at) return;
if (rev.client_id === myId) { lastAppliedAt = rev.updated_at; return; } // bn do mình ghi
if (rev.updated_at <= lastAppliedAt) return; // không mi hơn bn đã apply
const tmp = await window.SonicAPI.getTempProject();
if (!tmp || !tmp.has_temp || !tmp.data_json) return;
const projStr = typeof tmp.data_json === 'string' ? tmp.data_json : JSON.stringify(tmp.data_json);
let proj = tmp.data_json;
if (typeof proj === 'string') { try { proj = JSON.parse(proj); } catch (e) { proj = null; } }
if (!proj || !proj.main_session) return;
if (projStr === lastAppliedJson) { lastAppliedAt = rev.updated_at; return; } // ni dung không đi
const result = deserializeProjectFromSchema(proj);
if (result && result.tracks && result.tracks.length > 0) {
lastAppliedJson = projStr;
lastAppliedAt = rev.updated_at;
setTracks(result.tracks);
loadAudioBuffersForTracks(result.tracks).catch(function (err) { console.warn('loadAudioBuffersForTracks (sync) error:', err); });
setBpm((result.bpm || 120).toString());
if (result.sessionTabs && result.sessionTabs.length > 0) setSessionTabs(result.sessionTabs);
if (result.subTabs && result.subTabs.length > 0) setSubTabs(result.subTabs);
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
setProjectName(tmpName);
localStorage.setItem('sonic_project_name', tmpName);
showToast('Đã đồng bộ dự án từ thiết bị khác', 'info');
}
} catch (e) { console.warn('[Sync] error:', e); }
};
const tick = () => {
if (stopped) return;
applyRemote().finally(() => {
if (!stopped) timer = setTimeout(tick, 3000);
});
};
timer = setTimeout(tick, 4000); // sau mount + restore temp
return () => { stopped = true; if (timer) clearTimeout(timer); };
}, []);
// Timer-based auto-save (5 min) + backup (30 min)
useEffect(() => {
const BACKUP_MAX_KEY = 'sonic_backup_max_count';
@@ -27204,7 +27298,7 @@ STRICT CONSTRAINTS:
return { success: true, trackId: rearrangeNewId, totalBars };
},
createMidiItem: (args) => {
const trackId = args.track_id || selectedTrackId;
const trackId = String(args.track_id || selectedTrackId);
if (!trackId) return { success: false, error: 'No track_id provided' };
const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
const startBar = args.start_bar !== undefined ? parseFloat(args.start_bar) : 0;
@@ -27215,6 +27309,7 @@ STRICT CONSTRAINTS:
parent_track_id: trackId,
startTime: startBar * secondsPerBar,
duration: lengthBars * secondsPerBar,
length_bars: lengthBars,
notes: [],
color: '#a78bfa'
};
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -45,8 +45,9 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
deleteUser: (userId) => apiRequest(`/api/v1/admin/users/${userId}`, { method: 'DELETE' }),
createUser: (username, email, password, role = 'standard') => apiRequest('/api/v1/admin/users', { method: 'POST', body: JSON.stringify({ username, email, password, role }) }),
saveTempProject: (dataJson) => apiRequest('/api/v1/projects/temp', { method: 'POST', body: JSON.stringify({ data_json: dataJson }) }),
saveTempProject: (dataJson, clientId) => apiRequest('/api/v1/projects/temp', { method: 'POST', body: JSON.stringify({ data_json: dataJson, client_id: clientId || '' }) }),
getTempProject: () => apiRequest('/api/v1/projects/temp', { method: 'GET' }),
getTempRevision: () => apiRequest('/api/v1/projects/temp/revision', { method: 'GET' }),
saveCloudProject: (name, dataJson) => apiRequest('/api/v1/projects/cloud', { method: 'POST', body: JSON.stringify({ name, data_json: dataJson }) }),
listCloudProjects: () => apiRequest('/api/v1/projects/cloud', { method: 'GET' }),
getCloudProject: (projectId) => apiRequest(`/api/v1/projects/cloud/${projectId}`, { method: 'GET' }),
+23 -3
View File
@@ -61,6 +61,21 @@
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);
@@ -69,9 +84,11 @@
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).catch(() => {});
await window.SonicAPI.saveTempProject(dataJson, getClientId()).catch(() => {});
}
} catch (e) {
console.warn("Auto-save temp project warning:", e);
@@ -85,9 +102,11 @@
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).catch(() => {});
await window.SonicAPI.saveTempProject(dataJson, getClientId()).catch(() => {});
}
} catch (e) {
console.warn("Flush temp project warning:", e);
@@ -98,6 +117,7 @@
exportProjectToSFS,
importProjectFromSFSFile,
scheduleTempAutoSave,
flushTempAutoSave
flushTempAutoSave,
getClientId
};
})();