Normalize Linux audio pipeline to lossless Float32 (skill: lossless-audio-compliance)
- native bridge WAV writers: 16-bit PCM truncation -> IEEE Float32 (fmt 3, bit-exact) - vendor sheredom_json.h (vst3sdk 3.8.1 lacks moduleinfo/json.h) - Dockerfile: drop nonexistent sfizz tag v1.2.3 - Python sf.write -> subtype=FLOAT everywhere - resample_poly -> kaiser(16) window (>=130dB stopband) - audio_editor._write_wav_lossless(): FLOAT passthrough, TPDF dither only for 8/16/24-bit final exports - app.jsx WAV encoders: Float32 default, TPDF dither for integer depths, fix 24-bit - tests: lossless compliance gate (null test < -140dBFS, float32 format) 9/9 pass - TASKS_WINDOWS_AUDIO_NORMALIZE.md: Windows-side remaining tasks
This commit is contained in:
+34
-22
@@ -11683,7 +11683,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
<div>
|
||||
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Định dạng</label>
|
||||
<select value={exportSettings.format}
|
||||
onChange={e => setExportSettings(p => ({ ...p, format: e.target.value, sampleRate: '44100', bitDepth: '16', quality: '44khz' }))}
|
||||
onChange={e => setExportSettings(p => ({ ...p, format: e.target.value, sampleRate: '44100', bitDepth: 'float', quality: '44khz' }))}
|
||||
className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
|
||||
<option value="wav">WAV</option>
|
||||
<option value="mp3">MP3</option>
|
||||
@@ -11708,6 +11708,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
<option value="8">8</option>
|
||||
<option value="16">16</option>
|
||||
<option value="24">24</option>
|
||||
<option value="float">32 float</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16832,7 +16833,7 @@ const App = () => {
|
||||
const [selectedProviderId, setSelectedProviderId] = useState('');
|
||||
const [exportSettings, setExportSettings] = useState({
|
||||
sampleRate: '44100',
|
||||
bitDepth: '16',
|
||||
bitDepth: 'float',
|
||||
format: 'wav',
|
||||
source: 'project',
|
||||
quality: '44khz',
|
||||
@@ -20130,8 +20131,8 @@ const App = () => {
|
||||
const sr = buffer.sampleRate;
|
||||
const monoData = buffer.getChannelData(0);
|
||||
const bufferLength = monoData.length;
|
||||
const bitDepth = 16;
|
||||
const bytesPerSample = 2;
|
||||
const bitDepth = 32;
|
||||
const bytesPerSample = 4;
|
||||
const headerSize = 44;
|
||||
const fileSizeBytes = headerSize + bufferLength * bytesPerSample;
|
||||
const fileBuffer = new ArrayBuffer(fileSizeBytes);
|
||||
@@ -20146,7 +20147,7 @@ const App = () => {
|
||||
writeString(8, 'WAVE');
|
||||
writeString(12, 'fmt ');
|
||||
view.setUint32(16, 16, true);
|
||||
view.setUint16(20, 1, true);
|
||||
view.setUint16(20, 3, true); // IEEE float (lossless)
|
||||
view.setUint16(22, 1, true);
|
||||
view.setUint32(24, sr, true);
|
||||
view.setUint32(28, sr * bytesPerSample, true);
|
||||
@@ -20156,8 +20157,7 @@ const App = () => {
|
||||
view.setUint32(40, bufferLength * bytesPerSample, true);
|
||||
let offset = 44;
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const sample = Math.max(-1, Math.min(1, monoData[i]));
|
||||
view.setInt16(offset, Math.floor(sample < 0 ? sample * 0x8000 : sample * 0x7FFF), true);
|
||||
view.setFloat32(offset, monoData[i], true); // bit-exact, no clamp/quantize
|
||||
offset += bytesPerSample;
|
||||
}
|
||||
const blob = new Blob([view], {
|
||||
@@ -21249,6 +21249,10 @@ const App = () => {
|
||||
// ── Server-side upload ──
|
||||
// WAV encoder tối thiểu — upload buffer-only clip khi LƯU (nếu chưa có
|
||||
// serverFileId — upload sớm thất bại/race → clip mất sau reload).
|
||||
// ── 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;
|
||||
@@ -21258,18 +21262,15 @@ const App = () => {
|
||||
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 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 * 2, true); writeStr(8, 'WAVE');
|
||||
writeStr(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true);
|
||||
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 * 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);
|
||||
}
|
||||
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) => {
|
||||
@@ -25903,8 +25904,10 @@ const App = () => {
|
||||
durationLimit += 1.2; // FX/mastering tail
|
||||
|
||||
const captureRate = audioCtx.sampleRate || 44100;
|
||||
const bitDepth = parseInt(exportSettings.bitDepth) || 16;
|
||||
const isFloatBit = exportSettings.bitDepth === 'float';
|
||||
const bitDepth = isFloatBit ? 32 : parseInt(exportSettings.bitDepth) || 16;
|
||||
const outChannels = exportSettings.channels === 'mono' ? 1 : 2;
|
||||
const fmtTag = isFloatBit ? 3 : 1; // IEEE float vs PCM
|
||||
showToast(`Bounce realtime ~${Math.round(durationLimit)}s — giữ nguyên âm thanh đang phát...`, "info");
|
||||
|
||||
// Capture tap on the master bus output (post-FX + post-mastering)
|
||||
@@ -25940,13 +25943,13 @@ const App = () => {
|
||||
let off = 0;
|
||||
chunks.forEach(c => { stereoBuf.set(c, off); off += c.length; });
|
||||
const frames = totalFrames;
|
||||
const bytesPerSample = bitDepth / 8;
|
||||
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, 1, true);
|
||||
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);
|
||||
@@ -25954,9 +25957,18 @@ const App = () => {
|
||||
let o = 44;
|
||||
for (let i = 0; i < frames; i++) {
|
||||
for (let ch = 0; ch < outChannels; ch++) {
|
||||
const s = Math.max(-1, Math.min(1, stereoBuf[i * 2 + ch]));
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user