feat: VSTi autosample -> SF2/SF3 (tools/autosample_vsti.py) + bit_depth 16/24/32 render/export WAV (render_engine, plugins API, audio_editor, app.jsx encoders) + tests + task/walkthrough docs (53)

This commit is contained in:
2026-08-11 12:09:02 +07:00
parent f8cb2c6a5e
commit 7293d7ac7e
12 changed files with 2266 additions and 18 deletions
+12 -7
View File
@@ -11352,6 +11352,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="32">32</option>
</select>
</div>
</div>
@@ -13856,7 +13857,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
const fid = f.file_id || f.fileId;
if (!fid) { setPeaks(null); return; }
try {
const resp = await fetch(`${API_BASE_URL}/api/v1/audio/waveform/${fid}?num_peaks=600`);
const resp = await window.SonicAPI.authFetch(`${API_BASE_URL}/api/v1/audio/waveform/${fid}?num_peaks=600`);
const data = await resp.json();
if (selectTokenRef.current !== token) return;
setPeaks(data.peaks || []);
@@ -13916,7 +13917,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
setComputerMode('server');
setComputerRoots(null);
try {
const resp = await fetch(`${API_BASE_URL}/api/v1/media/computer`);
const resp = await window.SonicAPI.authFetch(`${API_BASE_URL}/api/v1/media/computer`);
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
const roots = data.roots || [];
@@ -13977,7 +13978,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
const path = entry.path || entry;
if (!path) return null;
try {
const resp = await fetch(`${API_BASE_URL}/api/v1/media/browse?path=${encodeURIComponent(path)}`);
const resp = await window.SonicAPI.authFetch(`${API_BASE_URL}/api/v1/media/browse?path=${encodeURIComponent(path)}`);
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
setComputerPath(data.path);
@@ -14119,7 +14120,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
}
const url = filePreviewUrl(f);
if (!url) return null;
const resp = await fetch(url);
const resp = await window.SonicAPI.authFetch(url);
return await resp.arrayBuffer();
};
@@ -24354,12 +24355,12 @@ const App = () => {
}
if (mef.file_id || mef.fileId) {
const fid = mef.file_id || mef.fileId;
const resp = await fetch(`${API_BASE_URL}/api/v1/audio/download/${fid}`);
const resp = await window.SonicAPI.authFetch(`${API_BASE_URL}/api/v1/audio/download/${fid}`);
const blob = await resp.blob();
return new File([blob], mef.name || mef.original_name || fid, { type: blob.type || 'audio/wav' });
}
if (mef.path) {
const resp = await fetch(`${API_BASE_URL}/api/v1/media/file?path=${encodeURIComponent(mef.path)}`);
const resp = await window.SonicAPI.authFetch(`${API_BASE_URL}/api/v1/media/file?path=${encodeURIComponent(mef.path)}`);
const blob = await resp.blob();
return new File([blob], mef.name || mef.path.split(/[\\/]/).pop(), { type: blob.type || 'application/octet-stream' });
}
@@ -25040,6 +25041,8 @@ const App = () => {
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 if (bitDepth === 24) { const v24 = Math.floor(s < 0 ? s * 0x800000 : s * 0x7FFFFF); view.setUint8(o, v24 & 0xFF); view.setUint8(o + 1, (v24 >> 8) & 0xFF); view.setUint8(o + 2, (v24 >> 16) & 0xFF); }
else if (bitDepth === 32) view.setInt32(o, Math.floor(s < 0 ? s * 0x80000000 : s * 0x7FFFFFFF), true);
else view.setUint8(o, Math.floor((s + 1) * 127.5), true);
o += bytesPerSample;
}
@@ -25362,6 +25365,8 @@ const App = () => {
view.setUint8(offset, val24 & 0xFF);
view.setUint8(offset + 1, val24 >> 8 & 0xFF);
view.setUint8(offset + 2, val24 >> 16 & 0xFF);
} else if (bitDepth === 32) {
view.setInt32(offset, Math.floor(sample < 0 ? sample * 0x80000000 : sample * 0x7FFFFFFF), true);
}
offset += bytesPerSample;
}
@@ -28697,7 +28702,7 @@ STRICT CONSTRAINTS:
value: exportSettings.bitDepth,
onChange: e => setExportSettings(p => ({ ...p, bitDepth: e.target.value })),
className: "w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"
}, /*#__PURE__*/React.createElement("option", { value: "8" }, "8"), /*#__PURE__*/React.createElement("option", { value: "16" }, "16"), /*#__PURE__*/React.createElement("option", { value: "24" }, "24")))) : /*#__PURE__*/React.createElement("div", {
}, /*#__PURE__*/React.createElement("option", { value: "8" }, "8"), /*#__PURE__*/React.createElement("option", { value: "16" }, "16"), /*#__PURE__*/React.createElement("option", { value: "24" }, "24"), /*#__PURE__*/React.createElement("option", { value: "32" }, "32")))) : /*#__PURE__*/React.createElement("div", {
className: "grid grid-cols-2 gap-1"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
className: "block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"
File diff suppressed because one or more lines are too long
+6
View File
@@ -141,6 +141,12 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
throw new Error(err.detail || 'Upload failed');
}
return resp.json();
},
// fetch thô có Authorization header — trả Response (dùng cho download bytes/media browse)
authFetch: (url, options = {}) => {
const headers = { ...getAuthHeaders(), ...options.headers };
if (options.body instanceof FormData) delete headers['Content-Type'];
return fetch(url, { ...options, headers });
}
};
})();
+2 -2
View File
@@ -6,9 +6,9 @@
if (window.__FLUIDSYNTH_CDN) {
FLUIDSYNTH_JS_URL = window.__FLUIDSYNTH_CDN;
} else if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
FLUIDSYNTH_JS_URL = 'https://cdn.jsdelivr.net/npm/@enikey87/fluidsynth-emscripten@0.1.1/dist/libfluidsynth-2.3.0-sf3.js';
} else {
// Luôn dùng WASM local — Tauri webview hostname=localhost nên CDN (jsdelivr) bị
// chặn làm FluidSynth không load, phải rơi về beep. File có sẵn trong source+dist.
FLUIDSYNTH_JS_URL = '/static/js/vendor/libfluidsynth-2.3.0-sf3.js';
}