FIX: Đã fix block lồng + mất MIDI (v202608062330)
This commit is contained in:
+159
-28
@@ -2491,7 +2491,9 @@ const WaveformLane = ({
|
|||||||
ctx.font = 'bold 9px sans-serif';
|
ctx.font = 'bold 9px sans-serif';
|
||||||
ctx.fillText(sec.name || 'Section', Math.max(secStartLocal + 4, 4), 14);
|
ctx.fillText(sec.name || 'Section', Math.max(secStartLocal + 4, 4), 14);
|
||||||
|
|
||||||
// Draw sub-tracks within section
|
// Draw sub-tracks within section (khôi phục 19:45 — section item phải
|
||||||
|
// vẽ lại các item chứa bên trong sau khi mở project; buffers đã được
|
||||||
|
// nạp đầy đủ bởi loadAudioBuffersForTracks recursion 19:00)
|
||||||
const subTracks = sec.tracks || [];
|
const subTracks = sec.tracks || [];
|
||||||
const subTrackCount = Math.min(subTracks.length, 4);
|
const subTrackCount = Math.min(subTracks.length, 4);
|
||||||
const subTrackHeight = (height - 20) / Math.max(1, subTrackCount);
|
const subTrackHeight = (height - 20) / Math.max(1, subTrackCount);
|
||||||
@@ -9005,7 +9007,10 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
|
|||||||
duration_bars: s.duration / secondsPerBar,
|
duration_bars: s.duration / secondsPerBar,
|
||||||
clip_start_offset_bars: 0.0,
|
clip_start_offset_bars: 0.0,
|
||||||
source_data: {
|
source_data: {
|
||||||
referenced_section_id: s.sectionId || s.id
|
// KHÔNG fallback s.id: section item thiếu sectionId (insert thiếu
|
||||||
|
// field) → fallback = chính id item → TỰ TRỎ → block lồng + mất
|
||||||
|
// nội dung. Chỉ dùng sectionId; undefined → deserialize block rỗng.
|
||||||
|
referenced_section_id: s.sectionId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -9225,7 +9230,8 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
|||||||
bpm: parseFloat(bpmVal || 120),
|
bpm: parseFloat(bpmVal || 120),
|
||||||
time_signature_numerator: 4,
|
time_signature_numerator: 4,
|
||||||
time_signature_denominator: 4,
|
time_signature_denominator: 4,
|
||||||
sample_rate: 44100
|
sample_rate: 44100,
|
||||||
|
zoom: window.__currentZoom || 1.0
|
||||||
},
|
},
|
||||||
main_session: {
|
main_session: {
|
||||||
id: "main",
|
id: "main",
|
||||||
@@ -9330,7 +9336,8 @@ const deserializeProjectFromSchema = (schemaObj) => {
|
|||||||
tracks: restoredTracks,
|
tracks: restoredTracks,
|
||||||
sessionTabs: restoredSessionTabs,
|
sessionTabs: restoredSessionTabs,
|
||||||
subTabs: restoredSubTabs,
|
subTabs: restoredSubTabs,
|
||||||
masteringSettings: _migrateMasteringSettings(schemaObj.mastering_settings)
|
masteringSettings: _migrateMasteringSettings(schemaObj.mastering_settings),
|
||||||
|
zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -11538,6 +11545,9 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
|
|||||||
}()));
|
}()));
|
||||||
|
|
||||||
const [zoom, setZoom] = React.useState(1.0);
|
const [zoom, setZoom] = React.useState(1.0);
|
||||||
|
// Đồng bộ zoom ra window ref — serializeProjectToSchema lưu vào metadata.zoom
|
||||||
|
// (module-level không truy cập state) → restore/open khôi phục đúng zoom.
|
||||||
|
React.useEffect(() => { window.__currentZoom = zoom; }, [zoom]);
|
||||||
const [scrollOffset, setScrollOffset] = React.useState(0);
|
const [scrollOffset, setScrollOffset] = React.useState(0);
|
||||||
const scrollOffsetRef = React.useRef(0);
|
const scrollOffsetRef = React.useRef(0);
|
||||||
scrollOffsetRef.current = scrollOffset;
|
scrollOffsetRef.current = scrollOffset;
|
||||||
@@ -14937,12 +14947,33 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
return { ...c, buffer: clipBuffer };
|
return { ...c, buffer: clipBuffer };
|
||||||
}));
|
}));
|
||||||
|
// Section items: nạp CẢ audioclip bên trong section (trước đây bỏ sót →
|
||||||
|
// audioclip trong section hiện "(audio chưa được tải)" sau save+open).
|
||||||
|
const updatedSections = await Promise.all((t.sections || []).map(async s => {
|
||||||
|
const updatedSTracks = await Promise.all((s.tracks || []).map(async st => {
|
||||||
|
const updatedSClips = await Promise.all((st.clips || []).map(async c => {
|
||||||
|
let clipBuffer = c.buffer;
|
||||||
|
const targetFileId = c.serverFileId || st.serverFileId;
|
||||||
|
if (targetFileId && !clipBuffer) {
|
||||||
|
const result = await tryLoad(targetFileId);
|
||||||
|
if (result) {
|
||||||
|
clipBuffer = result.audioBuffer;
|
||||||
|
hasLoadedAny = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...c, buffer: clipBuffer };
|
||||||
|
}));
|
||||||
|
return { ...st, clips: updatedSClips };
|
||||||
|
}));
|
||||||
|
return { ...s, tracks: updatedSTracks };
|
||||||
|
}));
|
||||||
if (trackBuffer && updatedClips.length === 0) {
|
if (trackBuffer && updatedClips.length === 0) {
|
||||||
const clipId = `default_${t.id}`;
|
const clipId = `default_${t.id}`;
|
||||||
return { ...t, buffer: trackBuffer, channelInfo: trackChannelInfo,
|
return { ...t, buffer: trackBuffer, channelInfo: trackChannelInfo,
|
||||||
clips: [{ id: clipId, buffer: trackBuffer, startTime: t.startTime || 0, name: t.name, speed: 1.0 }] };
|
clips: [{ id: clipId, buffer: trackBuffer, startTime: t.startTime || 0, name: t.name, speed: 1.0 }],
|
||||||
|
sections: updatedSections };
|
||||||
}
|
}
|
||||||
return { ...t, buffer: trackBuffer, channelInfo: trackChannelInfo, clips: updatedClips };
|
return { ...t, buffer: trackBuffer, channelInfo: trackChannelInfo, clips: updatedClips, sections: updatedSections };
|
||||||
}));
|
}));
|
||||||
if (hasLoadedAny) {
|
if (hasLoadedAny) {
|
||||||
setTracks(prev => {
|
setTracks(prev => {
|
||||||
@@ -14964,10 +14995,21 @@ const App = () => {
|
|||||||
return { ...ut, clips: mergedClips };
|
return { ...ut, clips: mergedClips };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// Merge cho SESSION TABS: track của session tab có id TRÙNG track MAIN
|
||||||
|
// (vd id 1) — match vào allLoadedTracks (main đứng trước) → session tab
|
||||||
|
// Track 01 bị THAY bằng track MAIN (chứa SECTION_ITEM + không có midi)
|
||||||
|
// → block lồng + mất MIDI. FIX: session tab CHỈ match với INNER TRACKS
|
||||||
|
// của section items (đúng tầng), KHÔNG match track main top-level.
|
||||||
|
const sectionInnerTracks = [];
|
||||||
|
updatedTracks.forEach(function (ut) {
|
||||||
|
(ut.sections || []).forEach(function (s) {
|
||||||
|
(s.tracks || []).forEach(function (st) { sectionInnerTracks.push(st); });
|
||||||
|
});
|
||||||
|
});
|
||||||
setSessionTabs(prev => prev.map(st => ({
|
setSessionTabs(prev => prev.map(st => ({
|
||||||
...st,
|
...st,
|
||||||
tracks: (st.tracks || []).map(t => {
|
tracks: (st.tracks || []).map(t => {
|
||||||
const found = updatedTracks.find(u => u.id === t.id);
|
const found = sectionInnerTracks.find(u => u.id === t.id);
|
||||||
return found || t;
|
return found || t;
|
||||||
})
|
})
|
||||||
})));
|
})));
|
||||||
@@ -15022,6 +15064,7 @@ const App = () => {
|
|||||||
restoredBpm = result.bpm;
|
restoredBpm = result.bpm;
|
||||||
restoredSessionTabs = result.sessionTabs;
|
restoredSessionTabs = result.sessionTabs;
|
||||||
restoredSubTabs = result.subTabs;
|
restoredSubTabs = result.subTabs;
|
||||||
|
if (result.zoom && setZoom) setZoom(result.zoom);
|
||||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||||
} else {
|
} else {
|
||||||
restoredTracks = (parsed.tracks || []).map(function(t) {
|
restoredTracks = (parsed.tracks || []).map(function(t) {
|
||||||
@@ -15040,6 +15083,18 @@ const App = () => {
|
|||||||
localStorage.setItem('sonic_project_id', lastId);
|
localStorage.setItem('sonic_project_id', lastId);
|
||||||
setSessionTabs(restoredSessionTabs);
|
setSessionTabs(restoredSessionTabs);
|
||||||
setSubTabs(restoredSubTabs);
|
setSubTabs(restoredSubTabs);
|
||||||
|
// DIAG section-tab block: log tracks + sections của section tab sau restore
|
||||||
|
try {
|
||||||
|
(restoredSessionTabs || []).forEach(function(st) {
|
||||||
|
var secCount = 0, midiCount = 0, clipCount = 0;
|
||||||
|
(st.tracks || []).forEach(function(tr) {
|
||||||
|
secCount += (tr.sections || []).length;
|
||||||
|
midiCount += (tr.midiItems || []).length;
|
||||||
|
clipCount += (tr.clips || []).length;
|
||||||
|
});
|
||||||
|
console.log('[Restore] sessionTab', st.id, 'sectionId', st.sectionId, 'tracks', (st.tracks || []).length, 'sections', secCount, 'midi', midiCount, 'clips', clipCount);
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
var restoredItemCount = 0;
|
var restoredItemCount = 0;
|
||||||
restoredTracks.forEach(function(rt) {
|
restoredTracks.forEach(function(rt) {
|
||||||
if (rt.clips) restoredItemCount += rt.clips.length;
|
if (rt.clips) restoredItemCount += rt.clips.length;
|
||||||
@@ -16420,14 +16475,48 @@ const App = () => {
|
|||||||
showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!', 'success');
|
showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!', 'success');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveSectionTab = (tabId) => {
|
const handleSaveSectionTab = async (tabId) => {
|
||||||
const tab = sessionTabs.find(s => s.id === tabId);
|
const tab = sessionTabs.find(s => s.id === tabId);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
const bpmVal = parseInt(bpm) || 120;
|
const bpmVal = parseInt(bpm) || 120;
|
||||||
const secondsPerBeat = 60.0 / bpmVal;
|
const secondsPerBeat = 60.0 / bpmVal;
|
||||||
const secondsPerBar = secondsPerBeat * 4;
|
const secondsPerBar = secondsPerBeat * 4;
|
||||||
|
|
||||||
const contentTracks = tab.tracks ? tab.tracks.filter(tr => tr.clips?.length > 0 || tr.midiItems?.length > 0) : [];
|
// Upload buffer-only clips (chưa có serverFileId — upload sớm thất bại/
|
||||||
|
// race) TRƯỚC khi ghi vào section item → serialized AUDIO_ITEM có file →
|
||||||
|
// reload không mất audioclip.
|
||||||
|
const ensureClipsUploaded = async (tracksArr) => {
|
||||||
|
for (const tr of tracksArr || []) {
|
||||||
|
for (const c of (tr.clips || [])) {
|
||||||
|
if (!c.serverFileId && c.buffer && c.buffer.duration > 0.05) {
|
||||||
|
try {
|
||||||
|
const blob = encodeWavBlob(c.buffer);
|
||||||
|
const up = await uploadToServer(new File([blob], (c.name || 'clip').replace(/\.[^.]+$/, '') + '.wav', { type: 'audio/wav' }), tr.id);
|
||||||
|
if (up && up.file_id) { c.serverFileId = up.file_id; }
|
||||||
|
} catch (err) { console.warn('section clip upload failed:', err); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gồm CẢ track buffer-only (audioclip vừa chèn, upload chưa tạo clip —
|
||||||
|
// race: save trước khi upload xong → trước đây bị LỌC mất → audioclip
|
||||||
|
// không lưu). Buffer-only → tạo default clip để serialize bắt được item.
|
||||||
|
// LỌC BỎ section item TỰ TRỎ (sectionId/section item trỏ về CHÍNH section
|
||||||
|
// đang lưu — insertSectionAtPlayhead tạo item thiếu sectionId → fallback
|
||||||
|
// s.id → block lồng + ghi đè sectionStore → mất track MIDI).
|
||||||
|
const contentTracks = tab.tracks ? tab.tracks
|
||||||
|
.filter(tr => tr.clips?.length > 0 || tr.midiItems?.length > 0 || !!tr.buffer)
|
||||||
|
.map(tr => {
|
||||||
|
const nextTr = { ...tr };
|
||||||
|
if (nextTr.sections && nextTr.sections.length > 0) {
|
||||||
|
nextTr.sections = nextTr.sections.filter(s => (s.sectionId || s.id) !== tab.sectionId);
|
||||||
|
}
|
||||||
|
if (tr.buffer && (!tr.clips || tr.clips.length === 0)) {
|
||||||
|
return { ...nextTr, clips: [{ id: 'default_' + tr.id, buffer: tr.buffer, startTime: tr.startTime || 0, name: tr.name, speed: 1.0 }] };
|
||||||
|
}
|
||||||
|
return nextTr;
|
||||||
|
}) : [];
|
||||||
let maxEndTime = 0;
|
let maxEndTime = 0;
|
||||||
(contentTracks || []).forEach(tr => {
|
(contentTracks || []).forEach(tr => {
|
||||||
(tr.clips || []).forEach(c => {
|
(tr.clips || []).forEach(c => {
|
||||||
@@ -16441,6 +16530,9 @@ const App = () => {
|
|||||||
});
|
});
|
||||||
const durationSec = Math.max(maxEndTime, 4 * secondsPerBar);
|
const durationSec = Math.max(maxEndTime, 4 * secondsPerBar);
|
||||||
|
|
||||||
|
// Đảm bảo mọi clip có serverFileId (upload buffer-only trước khi lưu)
|
||||||
|
await ensureClipsUploaded(contentTracks);
|
||||||
|
|
||||||
setTracks(prev => prev.map(t => {
|
setTracks(prev => prev.map(t => {
|
||||||
if (!t.sections || t.sections.length === 0) return t;
|
if (!t.sections || t.sections.length === 0) return t;
|
||||||
return {
|
return {
|
||||||
@@ -16473,18 +16565,25 @@ const App = () => {
|
|||||||
if (existing) { setActiveTab(existing.id); showToast(`Tab "${section.name}" already open.`, 'info'); return; }
|
if (existing) { setActiveTab(existing.id); showToast(`Tab "${section.name}" already open.`, 'info'); return; }
|
||||||
const tabId = 'session_' + Date.now();
|
const tabId = 'session_' + Date.now();
|
||||||
const tabName = section.name || 'Section';
|
const tabName = section.name || 'Section';
|
||||||
const clonedTracks = section.tracks ? (JSON.parse(JSON.stringify(section.tracks))).map(t => ({
|
const clonedTracks = section.tracks ? section.tracks.map(t => {
|
||||||
...t,
|
// Giữ CLIPS + gắn lại buffer THẬT (JSON.parse(JSON.stringify()) DROP
|
||||||
clips: [],
|
// AudioBuffer → audioclip mất khỏi section editor). Trước đây clips:[]
|
||||||
sections: [],
|
// → section item mở bằng nhấp đôi không chứa audioclip.
|
||||||
markers: [],
|
const base = JSON.parse(JSON.stringify(t));
|
||||||
isArmed: false,
|
const srcClips = (t.clips || []).map(c => ({ ...c, buffer: c.buffer || null }));
|
||||||
monitoringEnabled: true,
|
return {
|
||||||
instrumentId: t.instrumentId || null,
|
...base,
|
||||||
instrumentProgram: t.instrumentProgram,
|
clips: srcClips,
|
||||||
instrumentName: t.instrumentName || null,
|
sections: [],
|
||||||
_isSectionClone: true
|
markers: [],
|
||||||
})) : tracks.filter(t => t.id === trackId).map(t => ({
|
isArmed: false,
|
||||||
|
monitoringEnabled: true,
|
||||||
|
instrumentId: t.instrumentId || null,
|
||||||
|
instrumentProgram: t.instrumentProgram,
|
||||||
|
instrumentName: t.instrumentName || null,
|
||||||
|
_isSectionClone: true
|
||||||
|
};
|
||||||
|
}) : tracks.filter(t => t.id === trackId).map(t => ({
|
||||||
...t,
|
...t,
|
||||||
clips: [],
|
clips: [],
|
||||||
sections: [],
|
sections: [],
|
||||||
@@ -17150,9 +17249,11 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No item clicked under cursor -> Attempt to delete the track itself
|
// No item clicked under cursor -> Attempt to delete the track itself
|
||||||
const hasClips = (track.clips && track.clips.length > 0) || !!track.buffer;
|
// Item LỖI/rỗng (clip không buffer — khung viền "audio chưa được tải",
|
||||||
const hasMidi = track.midiItems && track.midiItems.length > 0;
|
// midi không notes, section không nội dung) KHÔNG chặn xoá track.
|
||||||
const hasSections = track.sections && track.sections.length > 0;
|
const hasClips = (track.clips && track.clips.some(c => c.buffer)) || !!track.buffer;
|
||||||
|
const hasMidi = track.midiItems && track.midiItems.some(m => (m.notes && m.notes.length > 0));
|
||||||
|
const hasSections = track.sections && track.sections.some(s => (s.tracks && s.tracks.some(st => (st.clips && st.clips.some(c => c.buffer)) || (st.midiItems && st.midiItems.some(m => m.notes && m.notes.length > 0)))));
|
||||||
const isTrackEmpty = !hasClips && !hasMidi && !hasSections;
|
const isTrackEmpty = !hasClips && !hasMidi && !hasSections;
|
||||||
|
|
||||||
if (!isTrackEmpty) {
|
if (!isTrackEmpty) {
|
||||||
@@ -17802,6 +17903,31 @@ const App = () => {
|
|||||||
window.showToast = showToast;
|
window.showToast = showToast;
|
||||||
|
|
||||||
// ── Server-side upload ──
|
// ── Server-side upload ──
|
||||||
|
// WAV encoder tối thiểu — upload buffer-only clip khi LƯU (nếu chưa có
|
||||||
|
// serverFileId — upload sớm thất bại/race → clip mất sau reload).
|
||||||
|
const encodeWavBlob = (audioBuffer) => {
|
||||||
|
const numCh = Math.max(1, audioBuffer.numberOfChannels || 1);
|
||||||
|
const sr = audioBuffer.sampleRate || 44100;
|
||||||
|
const len = audioBuffer.length;
|
||||||
|
const interleaved = new Float32Array(len * numCh);
|
||||||
|
for (let ch = 0; ch < numCh; ch++) {
|
||||||
|
const data = audioBuffer.getChannelData(ch);
|
||||||
|
for (let i = 0; i < len; i++) interleaved[i * numCh + ch] = data[i];
|
||||||
|
}
|
||||||
|
const buffer = new ArrayBuffer(44 + interleaved.length * 2);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
const writeStr = (off, s) => { for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i)); };
|
||||||
|
writeStr(0, 'RIFF'); view.setUint32(4, 36 + interleaved.length * 2, true); writeStr(8, 'WAVE');
|
||||||
|
writeStr(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true);
|
||||||
|
view.setUint16(22, numCh, true); view.setUint32(24, sr, true);
|
||||||
|
view.setUint32(28, sr * numCh * 2, true); view.setUint16(32, numCh * 2, true); view.setUint16(34, 16, true);
|
||||||
|
writeStr(36, 'data'); view.setUint32(40, interleaved.length * 2, true);
|
||||||
|
for (let i = 0; i < interleaved.length; i++) {
|
||||||
|
const s = Math.max(-1, Math.min(1, interleaved[i]));
|
||||||
|
view.setInt16(44 + i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||||
|
}
|
||||||
|
return new Blob([buffer], { type: 'audio/wav' });
|
||||||
|
};
|
||||||
const uploadToServer = async (file, trackId) => {
|
const uploadToServer = async (file, trackId) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
@@ -18083,6 +18209,10 @@ const App = () => {
|
|||||||
const _anySubPlaying = subTabsRef.current.some(s => s.isPlaying);
|
const _anySubPlaying = subTabsRef.current.some(s => s.isPlaying);
|
||||||
const activeSub = subTabsRef.current.find(s => s.id === activeTabRef.current);
|
const activeSub = subTabsRef.current.find(s => s.id === activeTabRef.current);
|
||||||
const isPianoRoll = activeSub && activeSub.type === 'PIANO_ROLL';
|
const isPianoRoll = activeSub && activeSub.type === 'PIANO_ROLL';
|
||||||
|
// SECTION-TAB cũng chỉ rebuild khi NaN (20:00): audioclip mất/rest tự
|
||||||
|
// nhiên → im lặng > 750ms là BÌNH THƯỜNG — pk<0.001 → recovery hủy play +
|
||||||
|
// playhead về đầu track (đúng lỗi user: "playhead về đầu + không play").
|
||||||
|
const isSectionTab = activeTabRef.current && activeTabRef.current.startsWith('session_');
|
||||||
if ((isPlaying || _anySubPlaying) && masterBus && masterBus.analyser && (isPianoRoll || activeSourcesRef.current.length > 0)) {
|
if ((isPlaying || _anySubPlaying) && masterBus && masterBus.analyser && (isPianoRoll || activeSourcesRef.current.length > 0)) {
|
||||||
try {
|
try {
|
||||||
// "Đáng lẽ đang có âm" — quyết định watchdog có được rebuild không:
|
// "Đáng lẽ đang có âm" — quyết định watchdog có được rebuild không:
|
||||||
@@ -18124,7 +18254,7 @@ const App = () => {
|
|||||||
// pk<0.001 (im lặng) KHÔNG trigger cho piano roll — rests tự nhiên
|
// pk<0.001 (im lặng) KHÔNG trigger cho piano roll — rests tự nhiên
|
||||||
// giữa các note > 750ms là BÌNH THƯỜNG → false-positive = recovery
|
// giữa các note > 750ms là BÌNH THƯỜNG → false-positive = recovery
|
||||||
// hủy play + restart notes (glitch) — đúng chuỗi log recovery trước.
|
// hủy play + restart notes (glitch) — đúng chuỗi log recovery trước.
|
||||||
if ((isPianoRoll ? nanOut : (pk < 0.001 || nanOut))) {
|
if ((isPianoRoll || isSectionTab ? nanOut : (pk < 0.001 || nanOut))) {
|
||||||
masterSilenceFramesRef.current++;
|
masterSilenceFramesRef.current++;
|
||||||
const sinceRebuild = performance.now() - (lastMasterRebuildTimeRef.current || 0);
|
const sinceRebuild = performance.now() - (lastMasterRebuildTimeRef.current || 0);
|
||||||
// NaN = chain CHẾT chắc chắn → rebuild NGAY (3 frame ≈ 50ms).
|
// NaN = chain CHẾT chắc chắn → rebuild NGAY (3 frame ≈ 50ms).
|
||||||
@@ -20158,9 +20288,9 @@ const App = () => {
|
|||||||
const track = trackList.find(t => t.id === trackId);
|
const track = trackList.find(t => t.id === trackId);
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
|
|
||||||
const hasClips = (track.clips && track.clips.length > 0) || !!track.buffer;
|
const hasClips = (track.clips && track.clips.some(c => c.buffer)) || !!track.buffer;
|
||||||
const hasMidi = track.midiItems && track.midiItems.length > 0;
|
const hasMidi = track.midiItems && track.midiItems.some(m => (m.notes && m.notes.length > 0));
|
||||||
const hasSections = track.sections && track.sections.length > 0;
|
const hasSections = track.sections && track.sections.some(s => (s.tracks && s.tracks.some(st => (st.clips && st.clips.some(c => c.buffer)) || (st.midiItems && st.midiItems.some(m => m.notes && m.notes.length > 0)))));
|
||||||
const isTrackEmpty = !hasClips && !hasMidi && !hasSections;
|
const isTrackEmpty = !hasClips && !hasMidi && !hasSections;
|
||||||
|
|
||||||
if (!isTrackEmpty) {
|
if (!isTrackEmpty) {
|
||||||
@@ -24542,6 +24672,7 @@ STRICT CONSTRAINTS:
|
|||||||
restoredBpm = result.bpm;
|
restoredBpm = result.bpm;
|
||||||
restoredSessionTabs = result.sessionTabs;
|
restoredSessionTabs = result.sessionTabs;
|
||||||
restoredSubTabs = result.subTabs;
|
restoredSubTabs = result.subTabs;
|
||||||
|
if (result.zoom && setZoom) setZoom(result.zoom);
|
||||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||||
} else {
|
} else {
|
||||||
restoredTracks = (parsed.tracks || []).map(function (t) {
|
restoredTracks = (parsed.tracks || []).map(function (t) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@
|
|||||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||||
<script src="/static/js/app.precompiled.js?v=202608061830" defer></script>
|
<script src="/static/js/app.precompiled.js?v=202608062330" defer></script>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -2174,3 +2174,94 @@
|
|||||||
- **FIX (app.jsx — guard `Math.max(1, ...)`):** sub-tab Cut (15637/15644), Copy (15672), Paste (15727), clonedBuffer ×3 (15554/15577/15601). Chỗ 11802 đã guard sẵn.
|
- **FIX (app.jsx — guard `Math.max(1, ...)`):** sub-tab Cut (15637/15644), Copy (15672), Paste (15727), clonedBuffer ×3 (15554/15577/15601). Chỗ 11802 đã guard sẵn.
|
||||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061830), `wiki.md`. Rebuild precompiled.
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061830), `wiki.md`. Rebuild precompiled.
|
||||||
- **Ghi chú/Test:** `npm run build` → hard refresh → thao tác cut/copy audio vùng chọn rỗng → hết crash.
|
- **Ghi chú/Test:** `npm run build` → hard refresh → thao tác cut/copy audio vùng chọn rỗng → hết crash.
|
||||||
|
|
||||||
|
### [2026-08-06 18:45] Task: Section block MAIN không vẽ inner items preview (items chỉ thuộc SECTION-TAB)
|
||||||
|
- **Báo cáo user:** open project → các item của SECTION-TAB xuất hiện như clip/midi trên MAIN SESSION (ngoài block) — muốn sửa.
|
||||||
|
- **Chẩn đoán:** file .sfs SẠCH (main track chỉ SECTION_ITEM; section_store chứa nội dung ✓) — KHÔNG leak dữ liệu. Thủ phạm = **RENDER**: main timeline vẽ **preview inner items** bên trong section block (2492-2557 — sub-tracks + waveform + note bars) → trông như "items tải vào MAIN".
|
||||||
|
- **FIX (app.jsx render):** GỠ preview sub-tracks — section block trên MAIN chỉ hiện **tên + màu + bounds**. Dữ liệu `sec.tracks` VẪN giữ (play section item + SECTION-TAB không ảnh hưởng).
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061845), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → open Section_test → MAIN chỉ thấy block section (tên + màu), không còn item bên trong; mở SECTION-TAB → nội dung đầy đủ; play section item → vẫn có âm.
|
||||||
|
|
||||||
|
### [2026-08-06 19:00] Task: Audioclip trong section mất sau save+open — loadAudioBuffersForTracks không nạp clip bên trong section
|
||||||
|
- **Báo cáo user:** project MAIN chỉ có 1 section item (trong section: 1 midi + 1 audioclip). Sau khi lưu (ghi đè) + mở lại → audioclip bị mất, chỉ còn khung "(audio chưa được tải)".
|
||||||
|
- **Nguyên nhân:** `loadAudioBuffersForTracks` (14841) chỉ nạp `t.buffer` + `t.clips` (cấp MAIN track) — **KHÔNG đệ quy vào section item's `tracks`** → audioclip bên trong section không bao giờ được fetch/decode → buffer null → "(audio chưa được tải)".
|
||||||
|
- **FIX (app.jsx):** thêm vòng lặp section — `t.sections[].tracks[].clips` — `tryLoad(serverFileId)` mỗi clip → buffer; trả về `sections: updatedSections` trong cả 2 nhánh return.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061900), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → save project → mở lại → audioclip trong section hiện đầy đủ (hết "(audio chưa được tải)"); play section → có âm cả midi + audio.
|
||||||
|
|
||||||
|
### [2026-08-06 19:30] Task: Lưu zoom vào project + cho xóa item lỗi/rỗng (khung viền)
|
||||||
|
- **Yêu cầu user:** (1) lưu zoom khi save + khôi phục khi mở; (2) cho phép xóa item lỗi (khung viền "audio chưa được tải"/item rỗng/dữ liệu lỗi).
|
||||||
|
- **FIX (app.jsx):**
|
||||||
|
(1) **Zoom**: App effect `window.__currentZoom = zoom` → serialize metadata thêm `zoom` → deserialize trả `zoom` → restore + open (2 chỗ) `setZoom(result.zoom)`.
|
||||||
|
(2) **Guard xóa track** (2 chỗ 17124/20131): item LỖI/rỗng không chặn — `hasClips = clips.some(c => c.buffer)` (clip không buffer = khung viền → không tính); `hasMidi = midiItems.some(m => m.notes?.length)`; `hasSections = sections.some(s => s.tracks?.some(st => st.clips?.some(c => c.buffer) || st.midiItems?.some(m => m.notes?.length)))` — track chỉ chứa item lỗi → xóa được.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061930), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → zoom vào 2x → save → mở lại → zoom giữ 2x. Xóa track chỉ chứa item "(audio chưa được tải)" → xóa được.
|
||||||
|
|
||||||
|
### [2026-08-06 19:45] Task: Khôi phục preview items trong section block (sau mở project)
|
||||||
|
- **Báo cáo user:** sau khi mở lại dự án, section item không vẽ lại các item chứa trong nó.
|
||||||
|
- **FIX (app.jsx render 2491):** khôi phục preview sub-tracks trong section block (waveform bars + midi note bars) — bị gỡ ở 18:45. Kết hợp fix 19:00 (loadAudioBuffersForTracks đệ quy nạp buffers section) → preview hiện đầy đủ sau open.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608061945), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → open project → section block hiện waveform + note bars của items bên trong.
|
||||||
|
|
||||||
|
### [2026-08-06 20:00] Task: Audioclip chèn trong SECTION-TAB không lưu — race upload + track buffer-only bị lọc
|
||||||
|
- **Báo cáo user:** audioclip chèn trong SECTION-TAB không được lưu.
|
||||||
|
- **Nguyên nhân:** `handleSaveSectionTab` lọc `contentTracks = tracks.filter(tr => tr.clips?.length > 0 || ...)` — track vừa chèn audio (upload async chưa tạo clip — hoặc buffer-only) bị LỌC → audioclip không vào section item → save mất.
|
||||||
|
- **FIX (app.jsx handleSaveSectionTab):**
|
||||||
|
(1) Filter thêm `|| !!tr.buffer` — gồm track buffer-only.
|
||||||
|
(2) Buffer-only → tạo **default clip** (`default_<id>` — buffer, startTime, name) → serialize bắt được AUDIO_ITEM.
|
||||||
|
(3) Serialize đã fallback `c.serverFileId || t.serverFileId` (8959) ✓ — file id lấy từ track nếu clip chưa có.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062000), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → section: chèn audioclip → lưu section → save project → mở lại → audioclip còn đầy đủ (có âm).
|
||||||
|
|
||||||
|
### [2026-08-06 20:30] Task: Watchdog SECTION-TAB — chỉ rebuild khi NaN (hết playhead reset khi play)
|
||||||
|
- **Báo cáo user:** audioclip mất → di chuyển playhead đến vị trí đó nhấn play → playhead về đầu track + không play.
|
||||||
|
- **Nguyên nhân:** watchdog (18168) — section tab không phải piano roll → nhánh `pk < 0.001` — audioclip mất/rest tự nhiên → im lặng 750ms → recovery `stopAllPlayback` (playhead reset 0) + restart → "playhead về đầu + không play".
|
||||||
|
- **FIX (app.jsx):** thêm `isSectionTab` (`activeTab` bắt đầu `session_`) → nhánh `(isPianoRoll || isSectionTab ? nanOut : ...)` — section tab chỉ rebuild khi output NaN (chain chết), im lặng bình thường KHÔNG recovery → playhead giữ vị trí.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062030), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → section tab: play tại vị trí audioclip đã mất → playhead giữ nguyên (không về đầu); play bình thường vẫn hoạt động.
|
||||||
|
|
||||||
|
### [2026-08-06 21:00] Task: Section item mở nhấp đôi không chứa audioclip — clone xóa clips
|
||||||
|
- **Báo cáo user:** mở lại dự án → nhấp đôi section item → chỉ là section item, không chứa các items như trước khi lưu.
|
||||||
|
- **Nguyên nhân:** `handleEditSectionInTab` (16515) clone `JSON.parse(JSON.stringify(section.tracks))` với **`clips: []`** (reset cố định) → audioclip bị xóa khỏi section editor; JSON clone cũng DROP AudioBuffer.
|
||||||
|
- **FIX (app.jsx):** clone giữ **clips + buffer THẬT** (`srcClips = (t.clips||[]).map(c => ({...c, buffer: c.buffer||null}))`) — JSON chỉ dùng cho field metadata; buffer gắn lại từ source live.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062100), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → open project → nhấp đôi section item → section editor đầy đủ clips (audioclip có waveform) + midi; play nghe được.
|
||||||
|
|
||||||
|
### [2026-08-06 21:30] Task: Reload mất audioclip section — clip thiếu serverFileId → upload buffer-only tại thời điểm lưu
|
||||||
|
- **Báo cáo user:** reload → mất audioclip đã lưu trong SECTION-TAB.
|
||||||
|
- **Chẩn đoán (DB):** save mới nhất — `AUDIO_ITEM: server_file_id: None | url: ''` — clip KHÔNG có file reference (upload sớm thất bại/race — uploadToServer trả null "client-side only") → reload → load fail → audioclip mất.
|
||||||
|
- **FIX (app.jsx):**
|
||||||
|
(1) Thêm `encodeWavBlob` (WAV encoder tối thiểu — PCM 16-bit).
|
||||||
|
(2) `handleSaveSectionTab` → async + `ensureClipsUploaded(contentTracks)` — clip chưa serverFileId + có buffer → encode WAV → uploadToServer → gán `c.serverFileId` TRƯỚC khi ghi vào section item → serialized AUDIO_ITEM có file_id → reload nạp được.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062130), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → section: chèn audioclip → Lưu section (chờ upload — console không có "upload failed") → save project → reload → audioclip còn + có âm.
|
||||||
|
|
||||||
|
### [2026-08-06 22:00] Task: Section editor (nhấp đôi) trống sau reload — session tab không nhận buffers (merge nested)
|
||||||
|
- **Báo cáo user:** section item MAIN vẽ đúng content sau reload, nhưng CHỈNH SỬA (nhấp đôi) → chỉ là block, không phải nội dung đã lưu.
|
||||||
|
- **Nguyên nhân:** loadAudioBuffersForTracks merge session tabs (14995): `updatedTracks.find(u => u.id === t.id)` — find ở TOP-LEVEL — track của session tab có id TRÙNG track bên TRONG section item (nested) → không tìm thấy → session tab giữ tracks deserialize (clips không buffer) → editor trống/không waveform.
|
||||||
|
- **FIX (app.jsx):** build `allLoadedTracks` PHẲNG (updatedTracks + section items' inner tracks) → merge session tabs tìm trong map phẳng → session tab nhận tracks có buffers.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062200), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → save + reload → nhấp đôi section item → section editor đầy đủ (audioclip waveform + midi); play nghe được.
|
||||||
|
|
||||||
|
### [2026-08-06 22:30] Task: Section item TỰ TRỎ (block lồng) + mất MIDI track — insert thiếu sectionId + fallback s.id
|
||||||
|
- **Báo cáo user:** SECTION-TAB hiện block lồng (section item) + MIDI item chỉ là hình + play không lên.
|
||||||
|
- **Chẩn đoán (DB):** section track 01 chứa `SECTION_ITEM: sec_1786020136359 → sec_1786020136359` (TỰ TRỎ chính section); track MIDI (play_VST 292 notes) biến mất khỏi section (console tracks=2).
|
||||||
|
- **Nguyên nhân:** `insertSectionAtPlayhead` (21822) tạo section item thiếu `sectionId` → serialize (9008) `referenced_section_id: s.sectionId || s.id` fallback → **chính id item = id section → tự trỏ** → deserialize block lồng + sectionStore ghi đè → mất track MIDI.
|
||||||
|
- **FIX (app.jsx):**
|
||||||
|
(1) `handleSaveSectionTab` — LỌC section item tự-trỏ khỏi contentTracks (`(s.sectionId||s.id) !== tab.sectionId`).
|
||||||
|
(2) Serialize (9008) — BỎ fallback `s.id` — chỉ `s.sectionId` (thiếu → block rỗng, không tự-trỏ).
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062230), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → hard refresh → project CŨ (đã dính tự-trỏ): xóa block lồng trong section tab + thêm lại MIDI track → lưu → save → reload → section tab đầy đủ (audioclip + midi), play được.
|
||||||
|
|
||||||
|
### [2026-08-06 23:00] Task: DIAG — section tab vẫn hiện block sau reload (data sạch + bundle mới)
|
||||||
|
- **Trạng thái:** DB save sạch (section: 2 tracks midi+audio, không SECTION_ITEM); production đã deploy 2230 (referenced_section_id:s.section + allLoadedTracks ✓) — NHƯNG user vẫn thấy block trong SECTION-TAB + midi mất + play block không âm.
|
||||||
|
- **Hành động:** thêm diagnostic log sau restore — `[Restore] sessionTab <id> sectionId <sid> tracks N sections N midi N clips N` — để xác định block sinh từ đâu (restored sessionTabs có sections không; hay sinh lúc nhấp đôi).
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062300), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Chờ user:** build + deploy 2300 → reload → dán log `[Restore] sessionTab` + log lúc nhấp đôi (có `[Play]` + `[SonicSF] noteon` không).
|
||||||
|
|
||||||
|
### [2026-08-06 23:30] Task: FIX ID COLLISION merge session tabs — block lồng + mất MIDI (bug từ fix 2200)
|
||||||
|
- **Báo cáo user:** mất midi item + vẫn hiển thị section block trong SECTION-TAB.
|
||||||
|
- **Nguyên nhân:** fix 2200 (merge session tabs) dùng `allLoadedTracks` (main tracks + section inner tracks) — `find(u => u.id === t.id)` lấy track ĐẦU TIÊN — **inner track id 1 trùng MAIN track id 1** → session tab Track 01 bị THAY bằng MAIN track 01 (chứa SECTION_ITEM + không midi) → **block lồng + mất MIDI + play block không âm**.
|
||||||
|
- **FIX (app.jsx):** merge session tabs CHỈ match với `sectionInnerTracks` (inner tracks của section items) — không match track main top-level.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608062330), `wiki.md`. Rebuild precompiled.
|
||||||
|
- **Ghi chú/Test:** `npm run build` → deploy → reload → nhấp đôi section item → SECTION-TAB đầy đủ (midi + audioclip đúng vị trí) + play cả 2.
|
||||||
|
|||||||
Reference in New Issue
Block a user