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;
}