From d23c3aee4dca40262fb6b4239daec7b5975110b0 Mon Sep 17 00:00:00 2001 From: JotaChina Date: Sat, 27 Dec 2025 11:15:36 -0300 Subject: [PATCH] =?UTF-8?q?samples=20de=20=C3=A1udio=20n=C3=A3o=20reinicia?= =?UTF-8?q?vam=20ao=20fim=20do=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/creations/audio/audio_audio.js | 130 +++++++++++++++++------ 1 file changed, 95 insertions(+), 35 deletions(-) diff --git a/assets/js/creations/audio/audio_audio.js b/assets/js/creations/audio/audio_audio.js index 8a2d36b0..b53b8c0a 100755 --- a/assets/js/creations/audio/audio_audio.js +++ b/assets/js/creations/audio/audio_audio.js @@ -164,46 +164,36 @@ function _schedulerTick() { const now = audioCtx.currentTime; const logicalTime = now - startTime + (appState.audio.audioEditorSeekTime || 0); + const scheduleWindowStartSec = logicalTime; const scheduleWindowEndSec = logicalTime + SCHEDULE_AHEAD_TIME_SEC; for (const clip of appState.audio.clips) { const clipRuntime = runtimeClipState.get(clip.id) || { isScheduled: false }; if (clipRuntime.isScheduled) continue; - if (!clip.buffer) continue; + if (!clip?.buffer) continue; const clipStartTimeSec = clip.startTimeInSeconds; - const clipDurationSec = clip.durationInSeconds; - if ( - typeof clipStartTimeSec === "undefined" || - typeof clipDurationSec === "undefined" - ) - continue; + const clipDurationSec = + clip.durationInSeconds ?? clip.buffer?.duration; - let occurrenceStartTimeSec = clipStartTimeSec; + if (typeof clipStartTimeSec === "undefined") continue; + if (typeof clipDurationSec === "undefined") continue; + // ✅ Em modo loop: NÃO “trazer” starts de antes do loopStart pra dentro do loop. + // Esses casos são tratados por overlap (clip atravessando loopStart). if (isLoopActive) { const loopDuration = loopEndTimeSec - loopStartTimeSec; if (loopDuration <= 0) continue; - if ( - occurrenceStartTimeSec < loopStartTimeSec && - logicalTime >= loopStartTimeSec - ) { - const offsetFromLoopStart = - (occurrenceStartTimeSec - loopStartTimeSec) % loopDuration; - occurrenceStartTimeSec = - loopStartTimeSec + - (offsetFromLoopStart < 0 - ? offsetFromLoopStart + loopDuration - : offsetFromLoopStart); - } - if (occurrenceStartTimeSec < logicalTime) { - const loopsMissed = - Math.floor((logicalTime - occurrenceStartTimeSec) / loopDuration) + 1; - occurrenceStartTimeSec += loopsMissed * loopDuration; + + // start fora da janela do loop -> não agenda (senão toca errado e pode “matar” o resto) + if (clipStartTimeSec < loopStartTimeSec || clipStartTimeSec >= loopEndTimeSec) { + continue; } } + const occurrenceStartTimeSec = clipStartTimeSec; + if ( occurrenceStartTimeSec >= scheduleWindowStartSec && occurrenceStartTimeSec < scheduleWindowEndSec @@ -211,13 +201,65 @@ function _schedulerTick() { const absolutePlayTime = startTime + (occurrenceStartTimeSec - (appState.audio.audioEditorSeekTime || 0)); + _scheduleClip(clip, absolutePlayTime, clipDurationSec); + clipRuntime.isScheduled = true; runtimeClipState.set(clip.id, clipRuntime); } } } +/** + * ✅ Se a agulha está no meio de um clip (clip começou antes e ainda não acabou), + * precisamos iniciar o Player “agora”, com offset adequado. + * Isso resolve: seek no meio + reinício do loop + play sem stop em certos casos. + */ +function _scheduleOverlappingClipsAtTime(playheadSec) { + if (!audioCtx) return; + + const t = Number(playheadSec); + if (!isFinite(t) || t < 0) return; + + for (const clip of appState.audio.clips) { + if (!clip?.buffer) continue; + + const s = Number(clip.startTimeInSeconds); + if (!isFinite(s)) continue; + + const d = + Number(clip.durationInSeconds) || + clip.buffer?.duration || + 0; + + if (!(d > 0)) continue; + + const e = s + d; + + // clip já começou e ainda estaria tocando nesse playhead + if (!(s < t && e > t)) continue; + + // já foi agendado nessa “volta”? + const clipRuntime = runtimeClipState.get(clip.id) || { isScheduled: false }; + if (clipRuntime.isScheduled) continue; + + const baseOffset = clip.offsetInSeconds ?? clip.offset ?? 0; + + // quanto “dentro” do clip estamos + const delta = t - s; + + const offset = Math.max(0, baseOffset + delta); + const remaining = Math.max(0, e - t); + + // agenda para tocar imediatamente (em termos de AudioContext), + // mas sincronizado ao Transport via _scheduleClip() + _scheduleClip(clip, audioCtx.currentTime, remaining, offset); + + clipRuntime.isScheduled = true; + runtimeClipState.set(clip.id, clipRuntime); + } +} + function _scheduleOverlappingClipsAtLoopStart(loopStartSec, loopEndSec) { const loopLen = loopEndSec - loopStartSec; if (loopLen <= 0) return; @@ -270,17 +312,29 @@ function _animationLoop() { if (isLoopActive) { if (newLogicalTime >= loopEndTimeSec) { const loopDuration = loopEndTimeSec - loopStartTimeSec; - newLogicalTime = - loopStartTimeSec + ((newLogicalTime - loopStartTimeSec) % loopDuration); + if (loopDuration > 0) { + newLogicalTime = + loopStartTimeSec + + ((newLogicalTime - loopStartTimeSec) % loopDuration); + } else { + newLogicalTime = loopStartTimeSec; + } - // 1) reseta relógio lógico + // realinha relógio interno startTime = now; appState.audio.audioEditorSeekTime = newLogicalTime; - // 2) mantém Transport alinhado - try { Tone.Transport.seconds = newLogicalTime; } catch {} + // ✅ força o Transport “pular” junto na virada do loop + try { + // (desativa loop do Transport aqui pra não brigar com a sua lógica de loop da playlist) + Tone.Transport.loop = false; + } catch {} - // 3) limpa runtime e mata players antigos + try { + Tone.Transport.seconds = newLogicalTime; + } catch {} + + // ✅ limpa players/estado pra permitir reagendamento limpo runtimeClipState.clear(); scheduledNodes.forEach(({ player }) => { try { player.unsync(); } catch {} @@ -289,22 +343,23 @@ function _animationLoop() { }); scheduledNodes.clear(); - // 4) reinicia patterns (seu comportamento atual) + // ✅ reinicia patterns do song (seu scheduler da playlist) try { stopSongPatternPlaybackOnTransport(); startSongPatternPlaybackOnTransport(); } catch {} - // ✅ 5) reativa imediatamente os clips que atravessam o loopStart (DAW-like) - _scheduleOverlappingClipsAtLoopStart(loopStartTimeSec, loopEndTimeSec); + // ✅ IMPORTANTÍSSIMO: recomeça clips que atravessam o loopStart + _scheduleOverlappingClipsAtTime(newLogicalTime); - // ✅ 6) agenda imediatamente o que estiver na janela (não espera 25ms) + // e já agenda os próximos inícios sem esperar o próximo interval tick _schedulerTick(); } } appState.audio.audioEditorLogicalTime = newLogicalTime; + // fim do song sem loop if (!isLoopActive) { let maxTime = 0; appState.audio.clips.forEach((clip) => { @@ -313,8 +368,9 @@ function _animationLoop() { const endTime = clipStartTime + clipDuration; if (endTime > maxTime) maxTime = endTime; }); + if (maxTime > 0 && appState.audio.audioEditorLogicalTime >= maxTime) { - stopAudioEditorPlayback(true); // Rebobina no fim + stopAudioEditorPlayback(true); resetPlayheadVisual(); return; } @@ -323,10 +379,12 @@ function _animationLoop() { const pixelsPerSecond = getPixelsPerSecond(); const newPositionPx = appState.audio.audioEditorLogicalTime * pixelsPerSecond; updatePlayheadVisual(newPositionPx); + animationFrameId = requestAnimationFrame(_animationLoop); } + // --- API Pública --- export function updateTransportLoop() { @@ -396,6 +454,8 @@ export async function startAudioEditorPlayback(seekTime) { const bpm = parseFloat(document.getElementById("bpm-input")?.value) || 120; Tone.Transport.bpm.value = bpm; Tone.Transport.start(); + // ✅ se começou no meio de algum clip, inicia com offset correto + _scheduleOverlappingClipsAtTime(timeToStart); } catch {} // mantém seu scheduler/animador