diff --git a/app/templates/index.html b/app/templates/index.html index 3690e5b..be75df2 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1825,91 +1825,87 @@ }; // ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ── - const applySubTab = (tabId) => { - const subTab = subTabs.find(s => s.id === tabId); - if (!subTab || !subTab.buffer) return; - const track = tracks.find(t => t.id === subTab.trackId); - if (!track || !track.buffer) return; + const applySubTab = (tabId) => { + const subTab = subTabs.find(s => s.id === tabId); + if (!subTab || !subTab.buffer) return; + const track = tracks.find(t => t.id === subTab.trackId); + if (!track || !track.buffer) return; - const beforeSnap = captureTrackSnapshot(subTab.trackId); + const beforeSnap = captureTrackSnapshot(subTab.trackId); - // Clone buffer and apply effects - const ctx = getAudioContext(); - const eff = subTab.buffer.getChannelData(0); - const edBuffer = ctx.createBuffer(1, eff.length, subTab.buffer.sampleRate); - const edData = edBuffer.getChannelData(0); - edData.set(eff); + // Clone buffer and apply effects + const ctx = getAudioContext(); + const sr = subTab.buffer.sampleRate; + const eff = subTab.buffer.getChannelData(0); + let resultBuffer = ctx.createBuffer(1, eff.length, sr); + let resultData = resultBuffer.getChannelData(0); + resultData.set(eff); - // Apply effects inline - const fx = subTab.effects || {}; - // Reverse - if (fx.reverse) { - const reversed = new Float32Array(edData); - for (let i = 0; i < edData.length; i++) reversed[i] = edData[edData.length - 1 - i]; - edBuffer.copyToChannel(reversed, 0); - } - // 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++) { + // Apply effects inline + const fx = subTab.effects || {}; + + const applyResample = (data, ratio) => { + const newLen = Math.round(data.length * ratio); + const out = new Float32Array(newLen); + for (let i = 0; i < newLen; i++) { const srcIdx = i / ratio; 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; - 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) { @@ -1920,7 +1916,7 @@ if (c.id === subTab.clipId) { return { ...c, - buffer: edBuffer, + buffer: resultBuffer, 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 = endSample; i < track.buffer.length; i++) mergedData[i] = origData[i]; - for (let i = 0; i < edData.length; i++) { - const globalIdx = startSample + i; - let val = edData[i]; - if (i < crossfadeLen) { - const alpha = i / crossfadeLen; - val = (1 - alpha) * (origData[globalIdx] || 0) + alpha * edData[i]; - } else if (i > edData.length - crossfadeLen) { - const distFromEnd = edData.length - 1 - i; - const alpha = distFromEnd / crossfadeLen; - const origEndIdx = endSample - (edData.length - i); - val = alpha * (origEndIdx >= 0 ? origData[origEndIdx] : 0) + (1 - alpha) * edData[i]; - } + for (let i = 0; i < resultData.length; i++) { + const globalIdx = startSample + i; + let val = resultData[i]; + if (i < crossfadeLen) { + const alpha = i / crossfadeLen; + val = (1 - alpha) * (origData[globalIdx] || 0) + alpha * resultData[i]; + } else if (i > resultData.length - crossfadeLen) { + const distFromEnd = resultData.length - 1 - i; + const alpha = distFromEnd / crossfadeLen; + const origEndIdx = endSample - (resultData.length - i); + val = alpha * (origEndIdx >= 0 ? origData[origEndIdx] : 0) + (1 - alpha) * resultData[i]; + } mergedData[globalIdx] = val; }