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:
+110
-15
@@ -89,6 +89,23 @@ const ensureSonicInstrument = (ctx) => {
|
||||
// track dùng VSTi + có Carla local. Dùng chung cho mọi đường playback
|
||||
// (main timeline, local loop, piano roll, ghost notes) để MIDI item PHẢI 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) — nốt gửi
|
||||
// trước khi OSC engine bind cổng bị mất → item câm dù keybed kêu. Nốt 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,
|
||||
// Chẩn đoán: note MIDI item → Carla (bật 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 chạy | false = đã chết.
|
||||
// window.__carlaNoteQueue: nốt 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 phần" khi Carla bị đóng (route chỉ-Carla mà Carla chết = im lặng).
|
||||
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 chạy → TỰ ĐỘNG mở với VSTi của track (fire-and-forget) để MIDI
|
||||
// item play qua Carla bridge đúng yêu cầu.
|
||||
// Carla chưa chạy → TỰ ĐỘNG mở với VSTi của track rồi CHỜ OSC ready (poll
|
||||
// carla-status tối đa 10s) — nốt chỉ gửi khi bridge thực sự nhận được.
|
||||
// Có gate __carlaOpening chống spawn trùng (mỗi play chỉ mở 1 lần).
|
||||
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 (hoặc đã chạy) — poll tới khi OSC ready, mới flush nốt
|
||||
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 giữa các client (LAN browser ↔ Tauri standalone) ──
|
||||
// Bug 3: 2 session cùng tài khoản không sync. Poll revision nhẹ 3s; khi
|
||||
// updated_at mới hơn bản đã apply + client_id KHÁC mình → tải full temp →
|
||||
// deserialize → apply. Skip nếu data_json giống hệt bản cuối (chống churn
|
||||
// khi 30s writer bump updated_at với nội dung không đổi). Ping-pong chặn
|
||||
// bởi: bỏ qua bản 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; } // bản do mình ghi
|
||||
if (rev.updated_at <= lastAppliedAt) return; // không mới hơn bản đã 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; } // nội 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
@@ -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' }),
|
||||
|
||||
@@ -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
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user