FIX: Đã fix block lồng + mất MIDI (v202608062330)

This commit is contained in:
2026-08-06 20:42:27 +07:00
parent 93dc13b17a
commit 71b9231b57
4 changed files with 302 additions and 45 deletions
+159 -28
View File
@@ -2491,7 +2491,9 @@ const WaveformLane = ({
ctx.font = 'bold 9px sans-serif';
ctx.fillText(sec.name || 'Section', Math.max(secStartLocal + 4, 4), 14);
// Draw sub-tracks within section
// Draw sub-tracks within section (khôi phc 19:45 section item phi
// v li các item cha bên trong sau khi m project; buffers đã đưc
// np đy đ bi loadAudioBuffersForTracks recursion 19:00)
const subTracks = sec.tracks || [];
const subTrackCount = Math.min(subTracks.length, 4);
const subTrackHeight = (height - 20) / Math.max(1, subTrackCount);
@@ -9005,7 +9007,10 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
duration_bars: s.duration / secondsPerBar,
clip_start_offset_bars: 0.0,
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 lng + mt
// ni dung. Ch dùng sectionId; undefined deserialize block rng.
referenced_section_id: s.sectionId
}
});
});
@@ -9225,7 +9230,8 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
bpm: parseFloat(bpmVal || 120),
time_signature_numerator: 4,
time_signature_denominator: 4,
sample_rate: 44100
sample_rate: 44100,
zoom: window.__currentZoom || 1.0
},
main_session: {
id: "main",
@@ -9330,7 +9336,8 @@ const deserializeProjectFromSchema = (schemaObj) => {
tracks: restoredTracks,
sessionTabs: restoredSessionTabs,
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);
// Đng b zoom ra window ref serializeProjectToSchema lưu vào metadata.zoom
// (module-level không truy cp state) restore/open khôi phc đúng zoom.
React.useEffect(() => { window.__currentZoom = zoom; }, [zoom]);
const [scrollOffset, setScrollOffset] = React.useState(0);
const scrollOffsetRef = React.useRef(0);
scrollOffsetRef.current = scrollOffset;
@@ -14937,12 +14947,33 @@ const App = () => {
}
return { ...c, buffer: clipBuffer };
}));
// Section items: np C audioclip bên trong section (trưc đây b sót
// audioclip trong section hin "(audio chưa đưc ti)" 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) {
const clipId = `default_${t.id}`;
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) {
setTracks(prev => {
@@ -14964,10 +14995,21 @@ const App = () => {
return { ...ut, clips: mergedClips };
});
});
// Merge cho SESSION TABS: track ca 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 bng track MAIN (cha SECTION_ITEM + không có midi)
// block lng + mt MIDI. FIX: session tab CH match vi INNER TRACKS
// ca section items (đúng tng), 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 => ({
...st,
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;
})
})));
@@ -15022,6 +15064,7 @@ const App = () => {
restoredBpm = result.bpm;
restoredSessionTabs = result.sessionTabs;
restoredSubTabs = result.subTabs;
if (result.zoom && setZoom) setZoom(result.zoom);
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
} else {
restoredTracks = (parsed.tracks || []).map(function(t) {
@@ -15040,6 +15083,18 @@ const App = () => {
localStorage.setItem('sonic_project_id', lastId);
setSessionTabs(restoredSessionTabs);
setSubTabs(restoredSubTabs);
// DIAG section-tab block: log tracks + sections ca 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;
restoredTracks.forEach(function(rt) {
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');
};
const handleSaveSectionTab = (tabId) => {
const handleSaveSectionTab = async (tabId) => {
const tab = sessionTabs.find(s => s.id === tabId);
if (!tab) return;
const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal;
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 sm tht bi/
// race) TRƯC khi ghi vào section item serialized AUDIO_ITEM có file
// reload không mt 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); }
}
}
}
};
// Gm C track buffer-only (audioclip va chèn, upload chưa to clip
// race: save trưc khi upload xong trưc đây b LC mt audioclip
// không lưu). Buffer-only to default clip đ serialize bt đưc item.
// LC B section item T TR (sectionId/section item tr v CHÍNH section
// đang lưu insertSectionAtPlayhead to item thiếu sectionId fallback
// s.id block lng + ghi đè sectionStore mt 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;
(contentTracks || []).forEach(tr => {
(tr.clips || []).forEach(c => {
@@ -16441,6 +16530,9 @@ const App = () => {
});
const durationSec = Math.max(maxEndTime, 4 * secondsPerBar);
// Đm bo mi clip có serverFileId (upload buffer-only trưc khi lưu)
await ensureClipsUploaded(contentTracks);
setTracks(prev => prev.map(t => {
if (!t.sections || t.sections.length === 0) return t;
return {
@@ -16473,18 +16565,25 @@ const App = () => {
if (existing) { setActiveTab(existing.id); showToast(`Tab "${section.name}" already open.`, 'info'); return; }
const tabId = 'session_' + Date.now();
const tabName = section.name || 'Section';
const clonedTracks = section.tracks ? (JSON.parse(JSON.stringify(section.tracks))).map(t => ({
...t,
clips: [],
sections: [],
markers: [],
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 => ({
const clonedTracks = section.tracks ? section.tracks.map(t => {
// Gi CLIPS + gn li buffer THT (JSON.parse(JSON.stringify()) DROP
// AudioBuffer audioclip mt khi section editor). Trưc đây clips:[]
// section item m bng nhp đôi không cha audioclip.
const base = JSON.parse(JSON.stringify(t));
const srcClips = (t.clips || []).map(c => ({ ...c, buffer: c.buffer || null }));
return {
...base,
clips: srcClips,
sections: [],
markers: [],
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,
clips: [],
sections: [],
@@ -17150,9 +17249,11 @@ const App = () => {
}
// No item clicked under cursor -> Attempt to delete the track itself
const hasClips = (track.clips && track.clips.length > 0) || !!track.buffer;
const hasMidi = track.midiItems && track.midiItems.length > 0;
const hasSections = track.sections && track.sections.length > 0;
// Item LI/rng (clip không buffer khung vin "audio chưa đưc ti",
// midi không notes, section không ni dung) KHÔNG chn xoá track.
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;
if (!isTrackEmpty) {
@@ -17802,6 +17903,31 @@ const App = () => {
window.showToast = showToast;
// Server-side upload
// WAV encoder ti thiu upload buffer-only clip khi LƯU (nếu chưa có
// serverFileId upload sm tht bi/race clip mt 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 formData = new FormData();
formData.append('file', file);
@@ -18083,6 +18209,10 @@ const App = () => {
const _anySubPlaying = subTabsRef.current.some(s => s.isPlaying);
const activeSub = subTabsRef.current.find(s => s.id === activeTabRef.current);
const isPianoRoll = activeSub && activeSub.type === 'PIANO_ROLL';
// SECTION-TAB cũng ch rebuild khi NaN (20:00): audioclip mt/rest t
// nhiên im lng > 750ms là BÌNH THƯNG pk<0.001 recovery hy play +
// playhead v đu track (đúng li 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)) {
try {
// "Đáng l đang có âm" quyết đnh watchdog có đưc rebuild không:
@@ -18124,7 +18254,7 @@ const App = () => {
// pk<0.001 (im lng) KHÔNG trigger cho piano roll rests t nhiên
// gia các note > 750ms là BÌNH THƯNG false-positive = recovery
// hy play + restart notes (glitch) đúng chui log recovery trưc.
if ((isPianoRoll ? nanOut : (pk < 0.001 || nanOut))) {
if ((isPianoRoll || isSectionTab ? nanOut : (pk < 0.001 || nanOut))) {
masterSilenceFramesRef.current++;
const sinceRebuild = performance.now() - (lastMasterRebuildTimeRef.current || 0);
// NaN = chain CHT chc chn rebuild NGAY (3 frame 50ms).
@@ -20158,9 +20288,9 @@ const App = () => {
const track = trackList.find(t => t.id === trackId);
if (!track) return;
const hasClips = (track.clips && track.clips.length > 0) || !!track.buffer;
const hasMidi = track.midiItems && track.midiItems.length > 0;
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;
if (!isTrackEmpty) {
@@ -24542,6 +24672,7 @@ STRICT CONSTRAINTS:
restoredBpm = result.bpm;
restoredSessionTabs = result.sessionTabs;
restoredSubTabs = result.subTabs;
if (result.zoom && setZoom) setZoom(result.zoom);
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
} else {
restoredTracks = (parsed.tracks || []).map(function (t) {
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -24,7 +24,7 @@
<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/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">
<style>
:root {