diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 8f6f7fe..52984b3 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -23083,7 +23083,7 @@ const App = () => { // ── Lossless WAV encode helpers (lossless-audio-compliance skill) ───────── // Internal clip/IPC + default export = IEEE Float32 (format 3), bit-exact. // Single-stage TPDF dither applied ONLY at final PCM quantization (16/24/8). - const tpdfDither = v => v + (Math.random() - Math.random()); + const tpdfDither = (v, lsb) => v + (Math.random() - Math.random()) * lsb * 0.5; // TPDF ±0.5 LSB peak-to-peak = 1 LSB const encodeWavBlob = (audioBuffer) => { const numCh = Math.max(1, audioBuffer.numberOfChannels || 1); const sr = audioBuffer.sampleRate || 44100; @@ -28289,6 +28289,7 @@ const App = () => { view.setUint16(32, bytesPerSample, true); view.setUint16(34, bitDepth, true); writeString(36, 'data'); view.setUint32(40, frames * bytesPerSample * outChannels, true); let o = 44; + const lsb = bitDepth === 24 ? 1 / 0x800000 : bitDepth === 8 ? 1 / 128 : 1 / 0x8000; // 1 LSB in float for (let i = 0; i < frames; i++) { for (let ch = 0; ch < outChannels; ch++) { const raw = stereoBuf[i * 2 + ch]; @@ -28296,7 +28297,7 @@ const App = () => { view.setFloat32(o, raw, true); // bit-exact, no clamp/quantize } else { // Single-stage TPDF dither, then clamp, then quantize (final export only) - const s = Math.max(-1, Math.min(1, tpdfDither(raw))); + const s = Math.max(-1, Math.min(1, tpdfDither(raw, lsb))); if (bitDepth === 24) { const v = Math.floor(s < 0 ? s * 0x800000 : s * 0x7FFFFF) & 0xFFFFFF; view.setUint8(o, v & 0xFF); view.setUint8(o + 1, (v >> 8) & 0xFF); view.setUint8(o + 2, (v >> 16) & 0xFF); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index c101e4c..f1788d8 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -1599,7 +1599,8 @@ if(activeMIDIRecordersRef.current){const msg={data:[0x90,pitch,Math.max(1,Math.r // ── Lossless WAV encode helpers (lossless-audio-compliance skill) ───────── // Internal clip/IPC + default export = IEEE Float32 (format 3), bit-exact. // Single-stage TPDF dither applied ONLY at final PCM quantization (16/24/8). -const tpdfDither=v=>v+(Math.random()-Math.random());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{for(let i=0;iv+(Math.random()-Math.random())*lsb*0.5;// TPDF ±0.5 LSB peak-to-peak = 1 LSB +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{for(let i=0;i{const formData=new FormData();formData.append('file',file);try{const resp=await fetch(`${API_AUDIO}/upload`,{method:'POST',body:formData});if(!resp.ok)throw new Error(`Upload failed: ${resp.status}`);const data=await resp.json();serverFileIdMap[trackId]=data.file_id;return data;}catch(err){console.warn('Server upload failed, using client-side only:',err.message);return null;}};// ── Server-side waveform loading ── const loadServerWaveform=async fileId=>{try{const resp=await fetch(`${API_AUDIO}/waveform/${fileId}?num_peaks=800`);if(!resp.ok)return null;return await resp.json();}catch{return null;}};// ── Check Celery task result ── const pollTaskResult=async(taskId,maxPoll=10)=>{for(let i=0;isetTimeout(r,1500));try{const resp=await fetch(`${API_TASKS}/${taskId}`);if(!resp.ok)continue;const data=await resp.json();if(data.status==='SUCCESS')return data.result;if(data.status==='FAILURE')throw new Error(data.error||'Task failed');}catch(err){throw err;}}throw new Error('Task polling timeout');};// ── Wheel Zoom ── @@ -2153,9 +2154,10 @@ fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime stopAllPlayback();try{if(window.SonicSF&&window.SonicSF.stopAllNotes)window.SonicSF.stopAllNotes();}catch(e){}startTrackPlayback(0);// Realtime wait, then stop & detach await new Promise(res=>setTimeout(res,Math.ceil(durationLimit*1000)+900));stopAllPlayback();try{if(window.SonicSF&&window.SonicSF.stopAllNotes)window.SonicSF.stopAllNotes();}catch(e){}try{script.disconnect();}catch(e){}try{masterBus.output.disconnect(script);}catch(e){}// Encode interleaved PCM chunks → WAV const totalFrames=chunks.reduce((a,c)=>a+c.length/2,0);let stereoBuf=new Float32Array(totalFrames*2);let off=0;chunks.forEach(c=>{stereoBuf.set(c,off);off+=c.length;});// LOOP export: cắt cứng [loopStart, loopStart+loopDur) — bỏ phần trước + phần dư tail -if(loopDur>0){const sf=Math.max(0,Math.floor(loopStart*captureRate))*2;const ef=Math.min(stereoBuf.length,Math.floor((loopStart+loopDur)*captureRate)*2);if(ef>sf)stereoBuf=stereoBuf.subarray(sf,ef);}const frames=stereoBuf.length/2;const bytesPerSample=isFloatBit?4:bitDepth/8;const headerSize=44;const fileSizeBytes=headerSize+frames*bytesPerSample*outChannels;const view=new DataView(new ArrayBuffer(fileSizeBytes));const writeString=(o,s)=>{for(let i=0;i0){const sf=Math.max(0,Math.floor(loopStart*captureRate))*2;const ef=Math.min(stereoBuf.length,Math.floor((loopStart+loopDur)*captureRate)*2);if(ef>sf)stereoBuf=stereoBuf.subarray(sf,ef);}const frames=stereoBuf.length/2;const bytesPerSample=isFloatBit?4:bitDepth/8;const headerSize=44;const fileSizeBytes=headerSize+frames*bytesPerSample*outChannels;const view=new DataView(new ArrayBuffer(fileSizeBytes));const writeString=(o,s)=>{for(let i=0;i>8&0xFF);view.setUint8(o+2,v>>16&0xFF);}else if(bitDepth===16)view.setInt16(o,Math.floor(s<0?s*0x8000:s*0x7FFF),true);else view.setUint8(o,Math.floor((s+1)*127.5),true);}o+=bytesPerSample;}}const blob=new Blob([view],{type:'audio/wav'});await downloadBlobViaServer(blob,`sonicforge-bounce-${captureRate}hz-${bitDepth}bit.wav`).catch(()=>{});showToast("Bounce hoàn tất — file gồm MIDI + FX Rack + Mastering Chain!","success");}catch(err){showToast("Lỗi bounce: "+err.message,"error");}finally{setIsExporting(false);setShowExportProgress(false);}};const handleSaveLocalProject=name=>{const finalName=name||projectName||'Dự án mới';const localId=currentProjectId&¤tProjectId.startsWith('local_')?currentProjectId:'local_'+Date.now();const projectSchemaObj=serializeProjectToSchema(localId,finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataStr=JSON.stringify(projectSchemaObj);localStorage.setItem('sonic_local_project_data',dataStr);localStorage.setItem('sonic_project_id',localId);localStorage.setItem('sonic_project_name',finalName);setCurrentProjectId(localId);setProjectName(finalName);window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast(`Đã lưu dự án local "${finalName}" thành công!`,"success");// Đã đăng nhập → đồng bộ lên Cloud (fire-and-forget): project mở được từ +const s=Math.max(-1,Math.min(1,tpdfDither(raw,lsb)));if(bitDepth===24){const v=Math.floor(s<0?s*0x800000:s*0x7FFFFF)&0xFFFFFF;view.setUint8(o,v&0xFF);view.setUint8(o+1,v>>8&0xFF);view.setUint8(o+2,v>>16&0xFF);}else if(bitDepth===16)view.setInt16(o,Math.floor(s<0?s*0x8000:s*0x7FFF),true);else view.setUint8(o,Math.floor((s+1)*127.5),true);}o+=bytesPerSample;}}const blob=new Blob([view],{type:'audio/wav'});await downloadBlobViaServer(blob,`sonicforge-bounce-${captureRate}hz-${bitDepth}bit.wav`).catch(()=>{});showToast("Bounce hoàn tất — file gồm MIDI + FX Rack + Mastering Chain!","success");}catch(err){showToast("Lỗi bounce: "+err.message,"error");}finally{setIsExporting(false);setShowExportProgress(false);}};const handleSaveLocalProject=name=>{const finalName=name||projectName||'Dự án mới';const localId=currentProjectId&¤tProjectId.startsWith('local_')?currentProjectId:'local_'+Date.now();const projectSchemaObj=serializeProjectToSchema(localId,finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataStr=JSON.stringify(projectSchemaObj);localStorage.setItem('sonic_local_project_data',dataStr);localStorage.setItem('sonic_project_id',localId);localStorage.setItem('sonic_project_name',finalName);setCurrentProjectId(localId);setProjectName(finalName);window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast(`Đã lưu dự án local "${finalName}" thành công!`,"success");// Đã đăng nhập → đồng bộ lên Cloud (fire-and-forget): project mở được từ // máy khác / browser ẩn danh khi đăng nhập cùng tài khoản. if(currentUser&&window.SonicAPI&&window.SonicAPI.saveCloudProject){window.SonicAPI.saveCloudProject(finalName,dataStr).then(function(res){if(res&&res.project_id){console.log('[Cloud] local project synced:',res.project_id);}}).catch(function(e){console.warn('Cloud sync local project failed:',e);});}};const handleSaveCloudProject=async(name,existingProjectId)=>{const finalName=name||projectName||'Dự án mới';var useProjectId=existingProjectId||currentProjectId;const projectSchemaObj=serializeProjectToSchema(useProjectId||'project_'+Date.now(),finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});// Diagnostic: count source vs serialized items var srcMidi=0,srcClip=0,srcSec=0;tracks.forEach(function(t){srcMidi+=(t.midiItems||[]).length;srcClip+=(t.clips||[]).length;srcSec+=(t.sections||[]).length;});var serItems=0;if(projectSchemaObj.main_session&&projectSchemaObj.main_session.tracks){projectSchemaObj.main_session.tracks.forEach(function(st){if(st.items)serItems+=st.items.length;});}Object.keys(projectSchemaObj.section_store||{}).forEach(function(sk){(projectSchemaObj.section_store[sk].tracks||[]).forEach(function(st){if(st.items)serItems+=st.items.length;});});const dataJson=JSON.stringify(projectSchemaObj);try{if(existingProjectId){await window.SonicAPI.updateCloudProject(existingProjectId,finalName,dataJson);setProjectName(finalName);setCurrentProjectId(existingProjectId);localStorage.setItem('sonic_project_name',finalName);localStorage.setItem('sonic_project_id',existingProjectId);showToast('Đã ghi đè Cloud "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+').','success');}else if(currentProjectId&&!currentProjectId.startsWith('local_')){await window.SonicAPI.updateCloudProject(currentProjectId,finalName,dataJson);setProjectName(finalName);localStorage.setItem('sonic_project_name',finalName);localStorage.setItem('sonic_project_id',currentProjectId);showToast('Đã lưu Cloud "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+' id='+currentProjectId+').',serItems===0?'warning':'success');}else{const res=await window.SonicAPI.saveCloudProject(finalName,dataJson);const newProjId=res.project_id;setProjectName(finalName);localStorage.setItem('sonic_project_name',finalName);if(newProjId){setCurrentProjectId(newProjId);localStorage.setItem('sonic_project_id',newProjId);}showToast('Đã lưu Cloud mới "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+').',serItems===0?'warning':'success');}}catch(err){showToast(err.message||"Lỗi lưu dự án lên Cloud","error");}};const handleSaveProject=async()=>{if(!currentProjectId){setSaveProjectModalOpen(true);return;}if(currentProjectId.startsWith('local_')){handleSaveLocalProject(projectName);}else{if(!currentUser){showToast('Cần đăng nhập để lưu Cloud. currentUser='+(currentUser?'OK':'NULL')+' token='+(localStorage.getItem('sonic_token')?'exists':'missing'),'warning');return;}handleSaveCloudProject(projectName);}};const handleSaveProjectRef=useRef(handleSaveProject);handleSaveProjectRef.current=handleSaveProject;// ── Ctrl-S (Save) toàn cục ── diff --git a/app/templates/index.html b/app/templates/index.html index 757f22e..2819d66 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +