fix(export): tu dong dong modal khi hoan tat + throttle setProgress 150ms (toc do export) + dong modal path server
This commit is contained in:
+17
-2
@@ -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
|
||||
|
||||
@@ -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<n;i++)mono[i]=(outPcm[i*2]+outPcm[i*2+1])*0.5;outPcm=mono;}renderedBuffer=interleavedToAudioBuffer(outPcm,targetRate,outChannels,mctx);}}else{activeTracks.forEach(t=>{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<string.length;i++){view.setUint8(offset+i,string.charCodeAt(i));}};writeString(0,'RIFF');view.setUint32(4,fileSizeBytes-8,true);writeString(8,'WAVE');writeString(12,'fmt ');view.setUint32(16,16,true);view.setUint16(20,isFloatWav?3:1,true);view.setUint16(22,numExportCh,true);view.setUint32(24,targetRate,true);view.setUint32(28,targetRate*bytesPerSample*numExportCh,true);view.setUint16(32,bytesPerSample,true);view.setUint16(34,bitDepth,true);writeString(36,'data');view.setUint32(40,exportLength*bytesPerSample*numExportCh,true);let offset=44;let peak=0;// Chunk encode + yield giữa các chunk → React vẽ thanh tiến độ thật sự
|
||||
let mix=null;let vstTi=0;for(const t of activeTracks){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);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<n;i++)mono[i]=(outPcm[i*2]+outPcm[i*2+1])*0.5;outPcm=mono;}renderedBuffer=interleavedToAudioBuffer(outPcm,targetRate,outChannels,mctx);}}else{activeTracks.forEach(t=>{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<string.length;i++){view.setUint8(offset+i,string.charCodeAt(i));}};writeString(0,'RIFF');view.setUint32(4,fileSizeBytes-8,true);writeString(8,'WAVE');writeString(12,'fmt ');view.setUint32(16,16,true);view.setUint16(20,isFloatWav?3:1,true);view.setUint16(22,numExportCh,true);view.setUint32(24,targetRate,true);view.setUint32(28,targetRate*bytesPerSample*numExportCh,true);view.setUint16(32,bytesPerSample,true);view.setUint16(34,bitDepth,true);writeString(36,'data');view.setUint32(40,exportLength*bytesPerSample*numExportCh,true);let offset=44;let peak=0;// Chunk encode + yield giữa các chunk → React vẽ thanh tiến độ thật sự
|
||||
// (vòng lặp đồng bộ dài sẽ chặn main thread, bar không bao giờ hiện).
|
||||
const ENC_CHUNK=65536;for(let base=0;base<exportLength;base+=ENC_CHUNK){const encEnd=Math.min(exportLength,base+ENC_CHUNK);for(let i=base;i<encEnd;i++){for(let ch=0;ch<numExportCh;ch++){const chData=renderedBuffer.getChannelData(ch);const sample=Math.max(-1,Math.min(1,chData[i]));if(Math.abs(sample)>peak)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;base<exportLength;base+=ENC_CHUNK){const encEnd=Math.min(exportLength,base+ENC_CHUNK);for(let i=base;i<encEnd;i++){for(let ch=0;ch<numExportCh;ch++){const chData=renderedBuffer.getChannelData(ch);const sample=Math.max(-1,Math.min(1,chData[i]));if(Math.abs(sample)>peak)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,
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<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/chordTheory.js?v=202608211300"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608240040" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608240110" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user