fix: apply audio clip từ subtab về lại main session

This commit is contained in:
2026-07-19 17:32:24 +07:00
parent b1f9659f06
commit 32fab9d366
+87 -91
View File
@@ -1825,91 +1825,87 @@
}; };
// ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ── // ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ──
const applySubTab = (tabId) => { const applySubTab = (tabId) => {
const subTab = subTabs.find(s => s.id === tabId); const subTab = subTabs.find(s => s.id === tabId);
if (!subTab || !subTab.buffer) return; if (!subTab || !subTab.buffer) return;
const track = tracks.find(t => t.id === subTab.trackId); const track = tracks.find(t => t.id === subTab.trackId);
if (!track || !track.buffer) return; if (!track || !track.buffer) return;
const beforeSnap = captureTrackSnapshot(subTab.trackId); const beforeSnap = captureTrackSnapshot(subTab.trackId);
// Clone buffer and apply effects // Clone buffer and apply effects
const ctx = getAudioContext(); const ctx = getAudioContext();
const eff = subTab.buffer.getChannelData(0); const sr = subTab.buffer.sampleRate;
const edBuffer = ctx.createBuffer(1, eff.length, subTab.buffer.sampleRate); const eff = subTab.buffer.getChannelData(0);
const edData = edBuffer.getChannelData(0); let resultBuffer = ctx.createBuffer(1, eff.length, sr);
edData.set(eff); let resultData = resultBuffer.getChannelData(0);
resultData.set(eff);
// Apply effects inline // Apply effects inline
const fx = subTab.effects || {}; const fx = subTab.effects || {};
// Reverse
if (fx.reverse) { const applyResample = (data, ratio) => {
const reversed = new Float32Array(edData); const newLen = Math.round(data.length * ratio);
for (let i = 0; i < edData.length; i++) reversed[i] = edData[edData.length - 1 - i]; const out = new Float32Array(newLen);
edBuffer.copyToChannel(reversed, 0); for (let i = 0; i < newLen; i++) {
}
// Gain
if (fx.gainDb !== 0) {
const gain = Math.pow(10, fx.gainDb / 20);
for (let i = 0; i < edData.length; i++) edData[i] = Math.max(-1, Math.min(1, edData[i] * gain));
}
// Fade in
if (fx.fadeInMs > 0) {
const sr = edBuffer.sampleRate;
const fadeSamples = Math.min(edData.length, Math.floor(fx.fadeInMs / 1000 * sr));
for (let i = 0; i < fadeSamples; i++) edData[i] = edData[i] * (i / fadeSamples);
}
// Fade out
if (fx.fadeOutMs > 0) {
const sr = edBuffer.sampleRate;
const fadeSamples = Math.min(edData.length, Math.floor(fx.fadeOutMs / 1000 * sr));
for (let i = edData.length - fadeSamples; i < edData.length; i++) {
edData[i] = edData[i] * ((edData.length - 1 - i) / fadeSamples);
}
}
// Normalize (apply to selection or entire buffer)
if (fx.normalizeDb !== 0) {
const targetDb = fx.normalizeDb;
let maxVal = 0;
for (let i = 0; i < edData.length; i++) {
const abs = Math.abs(edData[i]);
if (abs > maxVal) maxVal = abs;
}
if (maxVal > 0) {
const targetAmp = Math.pow(10, targetDb / 20);
const scale = targetAmp / maxVal;
for (let i = 0; i < edData.length; i++) {
edData[i] = Math.max(-1, Math.min(1, edData[i] * scale));
}
}
}
// Pitch shift (simple resample)
if (fx.pitch !== 0) {
const ratio = Math.pow(2, fx.pitch / 12);
const newLength = Math.round(edData.length / ratio);
const newData = new Float32Array(newLength);
for (let i = 0; i < newLength; i++) {
const srcIdx = i * ratio;
const idx0 = Math.floor(srcIdx);
const idx1 = Math.min(idx0 + 1, edData.length - 1);
const frac = srcIdx - idx0;
newData[i] = edData[idx0] * (1 - frac) + edData[idx1] * frac;
}
edData.set(newData);
}
// Speed stretch
if (fx.speedStretch !== 100) {
const ratio = fx.speedStretch / 100;
const newLength = Math.round(edData.length * ratio);
const newData = new Float32Array(newLength);
for (let i = 0; i < newLength; i++) {
const srcIdx = i / ratio; const srcIdx = i / ratio;
const idx0 = Math.floor(srcIdx); const idx0 = Math.floor(srcIdx);
const idx1 = Math.min(idx0 + 1, edData.length - 1); const idx1 = Math.min(idx0 + 1, data.length - 1);
const frac = srcIdx - idx0; const frac = srcIdx - idx0;
newData[i] = edData[idx0] * (1 - frac) + edData[idx1] * frac; out[i] = data[idx0] * (1 - frac) + data[idx1] * frac;
} }
edData.set(newData); return out;
};
if (fx.reverse) {
const rev = new Float32Array(resultData);
for (let i = 0; i < resultData.length; i++) rev[i] = resultData[resultData.length - 1 - i];
resultData.set(rev);
}
if (fx.gainDb !== 0) {
const gain = Math.pow(10, fx.gainDb / 20);
for (let i = 0; i < resultData.length; i++) resultData[i] = Math.max(-1, Math.min(1, resultData[i] * gain));
}
if (fx.fadeInMs > 0) {
const fadeSamples = Math.min(resultData.length, Math.floor(fx.fadeInMs / 1000 * sr));
for (let i = 0; i < fadeSamples; i++) resultData[i] *= i / fadeSamples;
}
if (fx.fadeOutMs > 0) {
const fadeSamples = Math.min(resultData.length, Math.floor(fx.fadeOutMs / 1000 * sr));
for (let i = resultData.length - fadeSamples; i < resultData.length; i++) {
resultData[i] *= (resultData.length - 1 - i) / fadeSamples;
}
}
if (fx.normalizeDb !== 0) {
let maxVal = 0;
for (let i = 0; i < resultData.length; i++) { const abs = Math.abs(resultData[i]); if (abs > maxVal) maxVal = abs; }
if (maxVal > 0) {
const targetAmp = Math.pow(10, fx.normalizeDb / 20);
const scale = targetAmp / maxVal;
for (let i = 0; i < resultData.length; i++) resultData[i] = Math.max(-1, Math.min(1, resultData[i] * scale));
}
}
if (fx.pitch !== 0) {
const ratio = Math.pow(2, fx.pitch / 12);
const newData = applyResample(resultData, 1 / ratio);
resultBuffer = ctx.createBuffer(1, newData.length, sr);
resultData = resultBuffer.getChannelData(0);
resultData.set(newData);
}
const stretchSpeed = subTab.speed || 1.0;
if (stretchSpeed !== 1.0) {
const ratio = 1.0 / stretchSpeed;
const newData = applyResample(resultData, ratio);
resultBuffer = ctx.createBuffer(1, newData.length, sr);
resultData = resultBuffer.getChannelData(0);
resultData.set(newData);
}
if (fx.speedStretch !== 100) {
const ratio = fx.speedStretch / 100;
const newData = applyResample(resultData, ratio);
resultBuffer = ctx.createBuffer(1, newData.length, sr);
resultData = resultBuffer.getChannelData(0);
resultData.set(newData);
} }
if (subTab.clipId) { if (subTab.clipId) {
@@ -1920,7 +1916,7 @@
if (c.id === subTab.clipId) { if (c.id === subTab.clipId) {
return { return {
...c, ...c,
buffer: edBuffer, buffer: resultBuffer,
name: c.name.endsWith('(edited)') ? c.name : c.name + ' (edited)' name: c.name.endsWith('(edited)') ? c.name : c.name + ' (edited)'
}; };
} }
@@ -1948,18 +1944,18 @@
for (let i = 0; i < startSample; i++) mergedData[i] = origData[i]; for (let i = 0; i < startSample; i++) mergedData[i] = origData[i];
for (let i = endSample; i < track.buffer.length; i++) mergedData[i] = origData[i]; for (let i = endSample; i < track.buffer.length; i++) mergedData[i] = origData[i];
for (let i = 0; i < edData.length; i++) { for (let i = 0; i < resultData.length; i++) {
const globalIdx = startSample + i; const globalIdx = startSample + i;
let val = edData[i]; let val = resultData[i];
if (i < crossfadeLen) { if (i < crossfadeLen) {
const alpha = i / crossfadeLen; const alpha = i / crossfadeLen;
val = (1 - alpha) * (origData[globalIdx] || 0) + alpha * edData[i]; val = (1 - alpha) * (origData[globalIdx] || 0) + alpha * resultData[i];
} else if (i > edData.length - crossfadeLen) { } else if (i > resultData.length - crossfadeLen) {
const distFromEnd = edData.length - 1 - i; const distFromEnd = resultData.length - 1 - i;
const alpha = distFromEnd / crossfadeLen; const alpha = distFromEnd / crossfadeLen;
const origEndIdx = endSample - (edData.length - i); const origEndIdx = endSample - (resultData.length - i);
val = alpha * (origEndIdx >= 0 ? origData[origEndIdx] : 0) + (1 - alpha) * edData[i]; val = alpha * (origEndIdx >= 0 ? origData[origEndIdx] : 0) + (1 - alpha) * resultData[i];
} }
mergedData[globalIdx] = val; mergedData[globalIdx] = val;
} }