From 7a3df9f15cb3faea24b5e797f91546c8cb728ab5 Mon Sep 17 00:00:00 2001 From: JotaChina Date: Sat, 27 Dec 2025 10:55:04 -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 | 119 ++++++++++++++--------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/assets/js/creations/audio/audio_audio.js b/assets/js/creations/audio/audio_audio.js index a90fb05c..8a2d36b0 100755 --- a/assets/js/creations/audio/audio_audio.js +++ b/assets/js/creations/audio/audio_audio.js @@ -83,17 +83,15 @@ function _toToneBuffer(buffer) { // --- Lógica Principal do Scheduler (mantida) --- -function _scheduleClip(clip, absolutePlayTime, durationSec) { +function _scheduleClip(clip, absolutePlayTime, durationSec, overrideOffsetSec) { if (!clip.buffer) { console.warn(`Clip ${clip.id} não possui áudio buffer carregado.`); return; } - // usamos Player .sync() conectando no mesmo grafo do Tone const toneBuf = _toToneBuffer(clip.buffer); if (!toneBuf) return; - // cadeia de ganho/pan por clipe (se já tiver no estado, use; aqui garantimos) const gain = clip.gainNode instanceof Tone.Gain ? clip.gainNode @@ -103,68 +101,53 @@ function _scheduleClip(clip, absolutePlayTime, durationSec) { ? clip.pannerNode : new Tone.Panner(clip.pan ?? 0); - // conecta no destino principal (é um ToneAudioNode) - try { - gain.disconnect(); // evita duplicatas caso exista de execuções anteriores - } catch {} - try { - pan.disconnect(); - } catch {} + try { gain.disconnect(); } catch {} + try { pan.disconnect(); } catch {} gain.connect(pan).connect(getMainGainNode()); - // player sincronizado no Transport const player = new Tone.Player(toneBuf).sync().connect(gain); - // aplica pitch como rate (semitons → rate) const rate = clip.pitch && clip.pitch !== 0 ? Math.pow(2, clip.pitch / 12) : 1; player.playbackRate = rate; - // calculamos o "when" no tempo do Transport: - // absolutePlayTime é em audioCtx.currentTime; o "zero" lógico foi quando demos play: - // logical = (now - startTime) + seek; => occurrence = (absolutePlayTime - startTime) + seek + // --- tempo no Transport (em segundos) --- const occurrenceInTransportSec = absolutePlayTime - startTime + (appState.audio.audioEditorSeekTime || 0); - const offset = clip.offsetInSeconds ?? clip.offset ?? 0; - const dur = durationSec ?? toneBuf.duration; - - // --- INÍCIO DA CORREÇÃO (BUG: RangeError) --- - // O log de erro (RangeError: Value must be within [0, Infinity]) - // indica que um destes valores é um número negativo minúsculo - // (um bug de precisão de ponto flutuante). - // Usamos Math.max(0, ...) para "clampar" os valores e garantir - // que nunca sejam negativos. + const baseOffset = clip.offsetInSeconds ?? clip.offset ?? 0; + const offset = overrideOffsetSec ?? baseOffset; + const dur = durationSec ?? clip.durationInSeconds ?? toneBuf.duration; const safeOccurrence = Math.max(0, occurrenceInTransportSec); const safeOffset = Math.max(0, offset); - // Duração pode ser 'undefined', mas se existir, não pode ser negativa - const safeDur = - dur === undefined || dur === null ? undefined : Math.max(0, dur); - // --- FIM DA CORREÇÃO --- + const safeDur = dur == null ? undefined : Math.max(0, dur); - // agenda (agora usando os valores seguros) - player.start(safeOccurrence, safeOffset, safeDur); + // ✅ blindagem: nunca agenda no passado (especialmente após “virada” do loop) + let transportNow = + Tone.Transport.getSecondsAtTime + ? Tone.Transport.getSecondsAtTime(Tone.now()) + : Tone.Transport.seconds; + + // pequena folga pra não “perder” o start por alguns ms + const EPS = 0.003; + const startAt = Math.max(safeOccurrence, transportNow + EPS); + + player.start(startAt, safeOffset, safeDur); const eventId = nextEventId++; scheduledNodes.set(eventId, { player, clipId: clip.id }); - if (callbacks.onClipScheduled) { - callbacks.onClipScheduled(clip); - } + if (callbacks.onClipScheduled) callbacks.onClipScheduled(clip); - // quando parar naturalmente, limpamos runtime player.onstop = () => { _handleClipEnd(eventId, clip.id); - try { - player.unsync(); - } catch {} - try { - player.dispose(); - } catch {} + try { player.unsync(); } catch {} + try { player.dispose(); } catch {} }; } + function _handleClipEnd(eventId, clipId) { scheduledNodes.delete(eventId); runtimeClipState.delete(clipId); @@ -235,12 +218,51 @@ function _schedulerTick() { } } -// --- Loop de Animação (mantido) --- +function _scheduleOverlappingClipsAtLoopStart(loopStartSec, loopEndSec) { + const loopLen = loopEndSec - loopStartSec; + if (loopLen <= 0) return; + + for (const clip of appState.audio.clips) { + if (!clip?.buffer) continue; + + const s = Number(clip.startTimeInSeconds) || 0; + const d = + Number(clip.durationInSeconds) || + clip.buffer?.duration || + 0; + + if (d <= 0) continue; + + const e = s + d; + + // clip atravessa o loopStart (começou antes e ainda estaria tocando no loopStart) + if (!(s < loopStartSec && e > loopStartSec)) continue; + + // offset interno = offsetDoClip + (loopStart - startDoClip) + const baseOffset = clip.offsetInSeconds ?? clip.offset ?? 0; + const offset = Math.max(0, baseOffset + (loopStartSec - s)); + + // duração restante, mas não deixa vazar além do loopEnd + const remainingToClipEnd = e - loopStartSec; + const remainingToLoopEnd = loopEndSec - loopStartSec; + const dur = Math.max(0, Math.min(remainingToClipEnd, remainingToLoopEnd)); + + // neste instante do loop, startTime foi resetado para "agora" e seekTime virou loopStart + // então absolutePlayTime = startTime dispara exatamente no retorno. + _scheduleClip(clip, startTime, dur, offset); + + // marca como agendado pra não duplicar no tick seguinte + runtimeClipState.set(clip.id, { isScheduled: true }); + } +} + + function _animationLoop() { if (!isPlaying) { animationFrameId = null; return; } + const now = audioCtx.currentTime; let newLogicalTime = now - startTime + (appState.audio.audioEditorSeekTime || 0); @@ -251,13 +273,14 @@ function _animationLoop() { newLogicalTime = loopStartTimeSec + ((newLogicalTime - loopStartTimeSec) % loopDuration); + // 1) reseta relógio lógico startTime = now; appState.audio.audioEditorSeekTime = newLogicalTime; - // ✅ 1) manter o Tone.Transport em sincronia + // 2) mantém Transport alinhado try { Tone.Transport.seconds = newLogicalTime; } catch {} - // ✅ 2) permitir reagendamento de clips/patterns + // 3) limpa runtime e mata players antigos runtimeClipState.clear(); scheduledNodes.forEach(({ player }) => { try { player.unsync(); } catch {} @@ -266,11 +289,17 @@ function _animationLoop() { }); scheduledNodes.clear(); - // ✅ 3) reinicia patterns no Transport (se houver scheduleOnce para notes) + // 4) reinicia patterns (seu comportamento atual) try { stopSongPatternPlaybackOnTransport(); startSongPatternPlaybackOnTransport(); } catch {} + + // ✅ 5) reativa imediatamente os clips que atravessam o loopStart (DAW-like) + _scheduleOverlappingClipsAtLoopStart(loopStartTimeSec, loopEndTimeSec); + + // ✅ 6) agenda imediatamente o que estiver na janela (não espera 25ms) + _schedulerTick(); } } @@ -290,12 +319,14 @@ function _animationLoop() { return; } } + const pixelsPerSecond = getPixelsPerSecond(); const newPositionPx = appState.audio.audioEditorLogicalTime * pixelsPerSecond; updatePlayheadVisual(newPositionPx); animationFrameId = requestAnimationFrame(_animationLoop); } + // --- API Pública --- export function updateTransportLoop() {