fix(export): scale TPDF dither to 1 LSB — full-scale noise made bounce exports sound like white noise
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<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*4);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*4,true);writeStr(8,'WAVE');writeStr(12,'fmt ');view.setUint32(16,16,true);view.setUint16(20,3,true);// IEEE float
|
||||
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;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*4);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*4,true);writeStr(8,'WAVE');writeStr(12,'fmt ');view.setUint32(16,16,true);view.setUint16(20,3,true);// IEEE float
|
||||
view.setUint16(22,numCh,true);view.setUint32(24,sr,true);view.setUint32(28,sr*numCh*4,true);view.setUint16(32,numCh*4,true);view.setUint16(34,32,true);writeStr(36,'data');view.setUint32(40,interleaved.length*4,true);for(let i=0;i<interleaved.length;i++)view.setFloat32(44+i*4,interleaved[i],true);return new Blob([buffer],{type:'audio/wav'});};const uploadToServer=async(file,trackId)=>{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;i<maxPoll;i++){await new Promise(r=>setTimeout(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;i<s.length;i++)view.setUint8(o+i,s.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,fmtTag,true);view.setUint16(22,outChannels,true);view.setUint32(24,captureRate,true);view.setUint32(28,captureRate*bytesPerSample*outChannels,true);view.setUint16(32,bytesPerSample,true);view.setUint16(34,bitDepth,true);writeString(36,'data');view.setUint32(40,frames*bytesPerSample*outChannels,true);let o=44;for(let i=0;i<frames;i++){for(let ch=0;ch<outChannels;ch++){const raw=stereoBuf[i*2+ch];if(isFloatBit){view.setFloat32(o,raw,true);// bit-exact, no clamp/quantize
|
||||
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;i<s.length;i++)view.setUint8(o+i,s.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,fmtTag,true);view.setUint16(22,outChannels,true);view.setUint32(24,captureRate,true);view.setUint32(28,captureRate*bytesPerSample*outChannels,true);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];if(isFloatBit){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)));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ừ
|
||||
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 ──
|
||||
|
||||
@@ -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=202608241954" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608250101" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user