From 8c972c3df2d31994b2a286f8d9b5aac071012f7b Mon Sep 17 00:00:00 2001 From: locphamtran Date: Mon, 24 Aug 2026 18:35:24 +0700 Subject: [PATCH] fix(export): tu dong dong modal khi hoan tat + throttle setProgress 150ms (toc do export) + dong modal path server --- app/static/js/app.jsx | 19 +++++++++++++++++-- app/static/js/app.precompiled.js | 13 +++++++++---- app/templates/index.html | 2 +- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index e60de55..a14aa7b 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -28101,6 +28101,8 @@ const App = () => { a.click(); } }); + // Export server hoàn tất → tự đóng modal (toast Tải về vẫn hiện). + setShowExportPanel(false); } } else { throw new Error(result.error || 'Server processing failed'); @@ -28486,6 +28488,7 @@ const App = () => { // Yield 1 frame → React vẽ thanh "Đang xử lý..." trước khi graph build + // OfflineAudioContext render chặn main thread. await new Promise(res => setTimeout(res, 30)); + let lastPaint = 0; // throttle setProgress 150ms — tránh re-render toàn App mỗi chunk try { const targetRate = parseInt(exportSettings.sampleRate); const isFloatWav = exportSettings.bitDepth === 'float'; @@ -28602,7 +28605,11 @@ const App = () => { let mix = null; let vstTi = 0; for (const t of activeTracks) { - setExportProgress(10 + Math.round((vstTi / Math.max(1, activeTracks.length)) * 40)); + const now = Date.now(); + if (now - lastPaint >= 150) { + setExportProgress(10 + Math.round((vstTi / Math.max(1, activeTracks.length)) * 40)); + lastPaint = now; + } vstTi++; const chain = activeVstChain(t.vstFxChain || []); const tctx = new OfflineAudioContext(2, Math.ceil(targetRate * Math.max(0.1, durationLimit)), targetRate); @@ -28703,7 +28710,13 @@ const App = () => { offset += bytesPerSample; } } - setExportProgress(60 + Math.round((encEnd / Math.max(1, exportLength)) * 32)); + // Throttle setState 150ms → tránh re-render toàn App (1.7MB JSX) mỗi + // chunk — đó là thứ làm export chậm rõ rệt. + const now = Date.now(); + if (now - lastPaint >= 150) { + setExportProgress(60 + Math.round((encEnd / Math.max(1, exportLength)) * 32)); + lastPaint = now; + } await new Promise(res => setTimeout(res, 0)); } if (peak < 0.001) { @@ -28779,6 +28792,8 @@ const App = () => { showToast("Xuất bản âm thanh hoàn tất!", "success"); } } + // Export hoàn tất → tự đóng modal. + setShowExportPanel(false); } finally { // Restore the LIVE master bus after offline render: initMasterBus(offlineCtx) // ghi đè cả `masterBus` lẫn `window.masterBus` bằng graph OfflineAudioContext diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 6c5d1dc..11b89d8 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -2120,7 +2120,8 @@ const allOnServer=exportTracks.every(t=>serverFileIdMap[t.id]);// Loop đã cắ if(!exportSettings.loop&&allOnServer&&serverStatus==='connected'){// Use server-side export setIsExporting(true);setExportProgress(-1);showToast("Đang gửi yêu cầu xuất âm thanh đến máy chủ...","info");try{const sessionId=`session_${Date.now()}`;const tracksMeta=exportTracks.map(t=>({track_id:t.id,file_id:serverFileIdMap[t.id],volume_db:t.volumeDb,muted:false,clips:[{clip_id:`clip_${t.id}`,start_time_seconds:t.startTime||0,end_time_seconds:(t.startTime||0)+t.buffer.duration,loop_count:1,apply_zero_crossing:true,fade_in_ms:0,fade_out_ms:0}]}));const resp=await fetch(`${API_MULTITRACK}/mix`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session_id:sessionId,export_settings:{sample_rate:parseInt(exportSettings.sampleRate),bit_depth:parseInt(exportSettings.bitDepth),format:exportSettings.format,channels:exportSettings.channels},tracks:tracksMeta})});if(!resp.ok)throw new Error(`Server export failed: ${resp.status}`);const data=await resp.json();showToast("Đang xử lý trên máy chủ...","info");// Poll for result const result=await pollTaskResult(data.task_id,20);if(result.success){// Get the uploaded file from server -const fileId=result.output_file_id||result.output_path?.split('/').pop();if(fileId){const downloadUrl=`${API_AUDIO}/download/${fileId}`;showToast("Xuất bản âm thanh từ máy chủ hoàn tất!","success","Tải về",async()=>{try{const br=await fetch(downloadUrl);if(!br.ok)throw new Error('HTTP '+br.status);const b=await br.blob();const sp=await saveExportViaDialog((projectName||'Project')+'.wav',b);if(sp)showToast(`Đã lưu → ${sp}`,"success");}catch(e){const a=document.createElement('a');a.href=downloadUrl;a.download=fileId;a.click();}});}}else{throw new Error(result.error||'Server processing failed');}}catch(err){showToast("Lỗi xuất máy chủ: "+err.message+". Chuyển sang xuất client.","warning");// Fall back to client-side export +const fileId=result.output_file_id||result.output_path?.split('/').pop();if(fileId){const downloadUrl=`${API_AUDIO}/download/${fileId}`;showToast("Xuất bản âm thanh từ máy chủ hoàn tất!","success","Tải về",async()=>{try{const br=await fetch(downloadUrl);if(!br.ok)throw new Error('HTTP '+br.status);const b=await br.blob();const sp=await saveExportViaDialog((projectName||'Project')+'.wav',b);if(sp)showToast(`Đã lưu → ${sp}`,"success");}catch(e){const a=document.createElement('a');a.href=downloadUrl;a.download=fileId;a.click();}});// Export server hoàn tất → tự đóng modal (toast Tải về vẫn hiện). +setShowExportPanel(false);}}else{throw new Error(result.error||'Server processing failed');}}catch(err){showToast("Lỗi xuất máy chủ: "+err.message+". Chuyển sang xuất client.","warning");// Fall back to client-side export clientSideExport(exportTracks);}finally{setIsExporting(false);}}else{// Client-side export (existing working code) clientSideExport(exportTracks,{loopStart,loopDur});}};// ── D4: Mastering render qua engine (D3 export stage) ── // /api/v1/plugins/render — project schema + mastering chain qua native_bridge @@ -2155,7 +2156,8 @@ const handleGlobalSave=async()=>{try{const finalName=projectName||'Dự án mớ const res=await window.SonicAPI.saveProjectToDisk(finalName,dataJson);showToast('Đã lưu dự án: '+(res&&res.path||finalName),'success');}else if(window.SonicAPI){// Docker / headless → Cloud (nếu đã đăng nhập), ngược lại local if(currentProjectId&&!currentProjectId.startsWith('local_')&¤tUser){await window.SonicAPI.updateCloudProject(currentProjectId,finalName,dataJson);showToast('Đã lưu dự án lên Cloud','success');}else if(currentUser){await handleSaveAsCloud(finalName);}else{handleSaveLocalProject(finalName);}}}catch(err){showToast('Lỗi lưu dự án: '+(err.message||err),'error');}};const handleGlobalSaveRef=useRef(handleGlobalSave);handleGlobalSaveRef.current=handleGlobalSave;useEffect(()=>{const handler=e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='s'){const target=e.target;if(target&&(target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.isContentEditable))return;e.preventDefault();handleGlobalSaveRef.current();}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[]);const handleSaveAsCloud=async newName=>{const projectSchemaObj=serializeProjectToSchema('project_'+Date.now(),newName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataJson=JSON.stringify(projectSchemaObj);try{const res=await window.SonicAPI.saveCloudProject(newName,dataJson);const newProjId=res.project_id;setProjectName(newName);localStorage.setItem('sonic_project_name',newName);if(newProjId){setCurrentProjectId(newProjId);localStorage.setItem('sonic_project_id',newProjId);}showToast(`Đã lưu dự án dưới tên mới "${newName}" lên server!`,"success");}catch(err){showToast(err.message||"Lỗi Save As lên server","error");}};const handleSaveCloud=handleSaveProject;const handleExportSFS=(customName=null)=>{const finalName=customName||projectName||'Dự án SonicForge';const projectSchemaObj=serializeProjectToSchema(currentProjectId||'proj_'+Date.now(),finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast("Đã xuất dự án (.sfs) thành công!","success");};const handleImportSFS=()=>{const input=document.createElement('input');input.type='file';input.accept='.sfs,application/json';input.onchange=async e=>{if(!e.target.files[0])return;try{const proj=await window.SonicStorage.importProjectFromSFSFile(e.target.files[0]);let restored=[];let restoredBpm=bpm;let restoredSessionTabs=[];let restoredSubTabs=[];if(proj.main_session){const result=deserializeProjectFromSchema(proj);restored=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;if(result.masteringSettings)setMasteringSettings(result.masteringSettings);if(result.masterVolume!==undefined&&result.masterVolume!==null)setMasterVolume(result.masterVolume);if(result.masterVolL!==undefined&&result.masterVolL!==null)setMasterVolL(result.masterVolL);if(result.masterVolR!==undefined&&result.masterVolR!==null)setMasterVolR(result.masterVolR);if(result.masterLink!==undefined&&result.masterLink!==null)setMasterLink(result.masterLink);if(result.masterMuted!==undefined&&result.masterMuted!==null)setMasterMuted(result.masterMuted);if(result.masterAutoVol!==undefined)setMasterAutoVol(result.masterAutoVol);if(result.masterAutoOn!==undefined)setMasterAutoOn(result.masterAutoOn);}else{restored=(proj.tracks||[]).map(t=>({...t,buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null}));}if(restored.length>0){setTracks(restored);loadAudioBuffersForTracks(restored);setBpm(restoredBpm.toString());setProjectName(proj.name||'Dự án mới');setCurrentProjectId(null);if(restoredSessionTabs.length>0)setSessionTabs(restoredSessionTabs);if(restoredSubTabs.length>0)setSubTabs(restoredSubTabs);localStorage.setItem('sonic_project_name',proj.name||'Dự án mới');localStorage.removeItem('sonic_project_id');showToast(`Đã nạp dự án "${proj.name||'Dự án mới'}" từ tệp .sfs thành công!`,"success");}}catch(err){showToast(err.message||"Lỗi mở tệp .sfs","error");}};input.click();};const clientSideExport=async(activeTracks,loopInfo)=>{setIsExporting(true);setExportProgress(5);showToast("Đang trộn âm thanh đa kênh (Offline Mixdown)...","info");// Yield 1 frame → React vẽ thanh "Đang xử lý..." trước khi graph build + // OfflineAudioContext render chặn main thread. -await new Promise(res=>setTimeout(res,30));try{const targetRate=parseInt(exportSettings.sampleRate);const isFloatWav=exportSettings.bitDepth==='float';const bitDepth=isFloatWav?32:parseInt(exportSettings.bitDepth);// MAIN SESSION end time (items trên track main — sections theo bounds) — +await new Promise(res=>setTimeout(res,30));let lastPaint=0;// throttle setProgress 150ms — tránh re-render toàn App mỗi chunk +try{const targetRate=parseInt(exportSettings.sampleRate);const isFloatWav=exportSettings.bitDepth==='float';const bitDepth=isFloatWav?32:parseInt(exportSettings.bitDepth);// MAIN SESSION end time (items trên track main — sections theo bounds) — // offline render không cắt sớm; MIDI cache (preview) giữ max với cache. // LOOP export: độ dài chính xác = loopDur (không kéo đuôi, không fade) let durationLimit=loopInfo&&loopInfo.loopDur?loopInfo.loopDur:Math.max(0.1,computeMainSessionEndTime(activeTracks));// Include preview-captured MIDI cache length (soundfont tracks have no clips) @@ -2173,9 +2175,12 @@ try{if(masterBus&&masterBus.output){const mv=typeof masterVolume==='number'&&isF // VST chain qua fx_vst_bridge → cộng mix → mastering built-in 1 lần // trên tổng → master VST chain cuối chain. Đúng thứ tự live: track fx // → (sum) → master fx → out. -let mix=null;let vstTi=0;for(const t of activeTracks){setExportProgress(10+Math.round(vstTi/Math.max(1,activeTracks.length)*40));vstTi++;const chain=activeVstChain(t.vstFxChain||[]);const tctx=new OfflineAudioContext(2,Math.ceil(targetRate*Math.max(0.1,durationLimit)),targetRate);const tNodes={};const node=scheduleTrackInto(t,tctx,tNodes,true);if(!node)continue;const tb=await tctx.startRendering();try{if(node.fxStopFn)node.fxStopFn();}catch(e){}let pcm=audioBufferToInterleaved(tb).pcm;if(chain.length)pcm=await applyVstChainToPcm(pcm,targetRate,chain,t.name||'track');mix=mix?mixInterleaved(mix,pcm):pcm;}if(!mix)mix=new Float32Array(Math.ceil(targetRate*Math.max(0.1,durationLimit))*2);const mctx=new OfflineAudioContext(2,Math.floor(mix.length/2),targetRate);masterBus=null;initMasterBus(mctx);try{toggleMasteringOnMaster(!!(masteringSettings&&masteringSettings.masterConnected),!!(masteringSettings&&masteringSettings.isBypassed));}catch(e){console.warn('offline mastering toggle error (vst):',e);}try{if(masterBus&&masterBus.output){const mv=typeof masterVolume==='number'&&isFinite(masterVolume)?masterVolume:0;applyMasterVolumesTo(masterBus,masterVolL,masterLink?masterVolL:masterVolR,!!masterMuted,0);}}catch(e){console.warn('offline main-out volume error (vst):',e);}const src=mctx.createBufferSource();src.buffer=interleavedToAudioBuffer(mix,targetRate,2,mctx);src.connect(masterBus.input);src.start(0);renderedBuffer=await mctx.startRendering();if(masterVstOn){const conv=audioBufferToInterleaved(renderedBuffer);let outPcm=await applyVstChainToPcm(conv.pcm,targetRate,masterVstChain,'master');if(outChannels===1){const n=Math.floor(outPcm.length/2);const mono=new Float32Array(n);for(let i=0;i{scheduleTrackInto(t,offlineCtx,offlineNodes,false);});renderedBuffer=await offlineCtx.startRendering();}setExportProgress(60);const numExportCh=renderedBuffer.numberOfChannels;const exportLength=renderedBuffer.length;const bytesPerSample=bitDepth/8;const headerSize=44;const fileSizeBytes=headerSize+exportLength*bytesPerSample*numExportCh;const fileBuffer=new ArrayBuffer(fileSizeBytes);const view=new DataView(fileBuffer);const writeString=(offset,string)=>{for(let i=0;i=150){setExportProgress(10+Math.round(vstTi/Math.max(1,activeTracks.length)*40));lastPaint=now;}vstTi++;const chain=activeVstChain(t.vstFxChain||[]);const tctx=new OfflineAudioContext(2,Math.ceil(targetRate*Math.max(0.1,durationLimit)),targetRate);const tNodes={};const node=scheduleTrackInto(t,tctx,tNodes,true);if(!node)continue;const tb=await tctx.startRendering();try{if(node.fxStopFn)node.fxStopFn();}catch(e){}let pcm=audioBufferToInterleaved(tb).pcm;if(chain.length)pcm=await applyVstChainToPcm(pcm,targetRate,chain,t.name||'track');mix=mix?mixInterleaved(mix,pcm):pcm;}if(!mix)mix=new Float32Array(Math.ceil(targetRate*Math.max(0.1,durationLimit))*2);const mctx=new OfflineAudioContext(2,Math.floor(mix.length/2),targetRate);masterBus=null;initMasterBus(mctx);try{toggleMasteringOnMaster(!!(masteringSettings&&masteringSettings.masterConnected),!!(masteringSettings&&masteringSettings.isBypassed));}catch(e){console.warn('offline mastering toggle error (vst):',e);}try{if(masterBus&&masterBus.output){const mv=typeof masterVolume==='number'&&isFinite(masterVolume)?masterVolume:0;applyMasterVolumesTo(masterBus,masterVolL,masterLink?masterVolL:masterVolR,!!masterMuted,0);}}catch(e){console.warn('offline main-out volume error (vst):',e);}const src=mctx.createBufferSource();src.buffer=interleavedToAudioBuffer(mix,targetRate,2,mctx);src.connect(masterBus.input);src.start(0);renderedBuffer=await mctx.startRendering();if(masterVstOn){const conv=audioBufferToInterleaved(renderedBuffer);let outPcm=await applyVstChainToPcm(conv.pcm,targetRate,masterVstChain,'master');if(outChannels===1){const n=Math.floor(outPcm.length/2);const mono=new Float32Array(n);for(let i=0;i{scheduleTrackInto(t,offlineCtx,offlineNodes,false);});renderedBuffer=await offlineCtx.startRendering();}setExportProgress(60);const numExportCh=renderedBuffer.numberOfChannels;const exportLength=renderedBuffer.length;const bytesPerSample=bitDepth/8;const headerSize=44;const fileSizeBytes=headerSize+exportLength*bytesPerSample*numExportCh;const fileBuffer=new ArrayBuffer(fileSizeBytes);const view=new DataView(fileBuffer);const writeString=(offset,string)=>{for(let i=0;ipeak)peak=Math.abs(sample);if(bitDepth===8){view.setUint8(offset,Math.floor((sample+1.0)*127.5));}else if(bitDepth===16){view.setInt16(offset,Math.floor(sample<0?sample*0x8000:sample*0x7FFF),true);}else if(bitDepth===24){const val24=Math.floor(sample<0?sample*0x800000:sample*0x7FFFFF);view.setUint8(offset,val24&0xFF);view.setUint8(offset+1,val24>>8&0xFF);view.setUint8(offset+2,val24>>16&0xFF);}else if(isFloatWav){view.setFloat32(offset,sample,true);}offset+=bytesPerSample;}}setExportProgress(60+Math.round(encEnd/Math.max(1,exportLength)*32));await new Promise(res=>setTimeout(res,0));}if(peak<0.001){showToast("Cảnh báo: file export gần như CÂM (không có tín hiệu âm) — kiểm tra nguồn track / MIDI cache preview.","warning");}const blob=new Blob([view],{type:'audio/wav'});const wavName=`sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;setExportProgress(95);if((exportSettings.format==='mp3'||exportSettings.format==='ogg')&&serverStatus==='connected'){setIsExporting(true);setExportProgress(-1);showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...","info");try{const file=new File([blob],`session_export.wav`,{type:'audio/wav'});const formData=new FormData();formData.append('file',file);const uploadResp=await fetch(`${API_AUDIO}/upload`,{method:'POST',body:formData});if(!uploadResp.ok)throw new Error("Upload to transcode server failed");const uploadData=await uploadResp.json();const fileId=uploadData.file_id;const exportResp=await fetch(`${API_AUDIO}/export`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_id:fileId,format:exportSettings.format,sample_rate:targetRate,bit_depth:bitDepth})});if(!exportResp.ok)throw new Error("Transcode request failed");const exportData=await exportResp.json();const result=await pollTaskResult(exportData.task_id,20);if(result.success){const outId=result.output_file_id;const downloadUrl=`${API_AUDIO}/download/${outId}`;showToast(`Xuất bản âm thanh định dạng ${exportSettings.format.toUpperCase()} thành công!`,"success","Tải về",async()=>{try{const br=await fetch(downloadUrl);if(!br.ok)throw new Error('HTTP '+br.status);const b=await br.blob();const sp=await saveExportViaDialog(`sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`,b);if(sp)showToast(`Đã lưu → ${sp}`,"success");}catch(e){const a=document.createElement('a');a.href=downloadUrl;a.download=`sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`;a.click();}});}else{throw new Error(result.error||'Server encoding failed');}}catch(transcodeErr){showToast(`Lỗi chuyển đổi: ${transcodeErr.message}.`,"warning","Tải WAV thay thế",()=>{downloadBlobViaServer(blob,wavName).catch(()=>{});});}}else{if(exportSettings.format!=='wav'){showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.","warning");}const dres=await downloadBlobViaServer(blob,wavName).catch(()=>null);if(dres&&dres.saved_path){showToast(`Xuất bản âm thanh hoàn tất → ${dres.saved_path}`,"success");}else{showToast("Xuất bản âm thanh hoàn tất!","success");}}}finally{// Restore the LIVE master bus after offline render: initMasterBus(offlineCtx) +const ENC_CHUNK=65536;for(let base=0;basepeak)peak=Math.abs(sample);if(bitDepth===8){view.setUint8(offset,Math.floor((sample+1.0)*127.5));}else if(bitDepth===16){view.setInt16(offset,Math.floor(sample<0?sample*0x8000:sample*0x7FFF),true);}else if(bitDepth===24){const val24=Math.floor(sample<0?sample*0x800000:sample*0x7FFFFF);view.setUint8(offset,val24&0xFF);view.setUint8(offset+1,val24>>8&0xFF);view.setUint8(offset+2,val24>>16&0xFF);}else if(isFloatWav){view.setFloat32(offset,sample,true);}offset+=bytesPerSample;}}// Throttle setState 150ms → tránh re-render toàn App (1.7MB JSX) mỗi +// chunk — đó là thứ làm export chậm rõ rệt. +const now=Date.now();if(now-lastPaint>=150){setExportProgress(60+Math.round(encEnd/Math.max(1,exportLength)*32));lastPaint=now;}await new Promise(res=>setTimeout(res,0));}if(peak<0.001){showToast("Cảnh báo: file export gần như CÂM (không có tín hiệu âm) — kiểm tra nguồn track / MIDI cache preview.","warning");}const blob=new Blob([view],{type:'audio/wav'});const wavName=`sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;setExportProgress(95);if((exportSettings.format==='mp3'||exportSettings.format==='ogg')&&serverStatus==='connected'){setIsExporting(true);setExportProgress(-1);showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...","info");try{const file=new File([blob],`session_export.wav`,{type:'audio/wav'});const formData=new FormData();formData.append('file',file);const uploadResp=await fetch(`${API_AUDIO}/upload`,{method:'POST',body:formData});if(!uploadResp.ok)throw new Error("Upload to transcode server failed");const uploadData=await uploadResp.json();const fileId=uploadData.file_id;const exportResp=await fetch(`${API_AUDIO}/export`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_id:fileId,format:exportSettings.format,sample_rate:targetRate,bit_depth:bitDepth})});if(!exportResp.ok)throw new Error("Transcode request failed");const exportData=await exportResp.json();const result=await pollTaskResult(exportData.task_id,20);if(result.success){const outId=result.output_file_id;const downloadUrl=`${API_AUDIO}/download/${outId}`;showToast(`Xuất bản âm thanh định dạng ${exportSettings.format.toUpperCase()} thành công!`,"success","Tải về",async()=>{try{const br=await fetch(downloadUrl);if(!br.ok)throw new Error('HTTP '+br.status);const b=await br.blob();const sp=await saveExportViaDialog(`sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`,b);if(sp)showToast(`Đã lưu → ${sp}`,"success");}catch(e){const a=document.createElement('a');a.href=downloadUrl;a.download=`sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`;a.click();}});}else{throw new Error(result.error||'Server encoding failed');}}catch(transcodeErr){showToast(`Lỗi chuyển đổi: ${transcodeErr.message}.`,"warning","Tải WAV thay thế",()=>{downloadBlobViaServer(blob,wavName).catch(()=>{});});}}else{if(exportSettings.format!=='wav'){showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.","warning");}const dres=await downloadBlobViaServer(blob,wavName).catch(()=>null);if(dres&&dres.saved_path){showToast(`Xuất bản âm thanh hoàn tất → ${dres.saved_path}`,"success");}else{showToast("Xuất bản âm thanh hoàn tất!","success");}}// Export hoàn tất → tự đóng modal. +setShowExportPanel(false);}finally{// Restore the LIVE master bus after offline render: initMasterBus(offlineCtx) // ghi đè cả `masterBus` lẫn `window.masterBus` bằng graph OfflineAudioContext // (state closed) → track route (window.masterBus.input) đổ vào context câm → // play/bounce sau export CÂM. Khôi phục bus live (giữ wiring cũ nếu còn sống, diff --git a/app/templates/index.html b/app/templates/index.html index 259ef95..15852a2 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +