samples de áudio não reiniciavam ao fim do loop
Deploy / Deploy (push) Successful in 2m9s Details

This commit is contained in:
JotaChina 2025-12-27 10:55:04 -03:00
parent 23258e7d02
commit 7a3df9f15c
1 changed files with 75 additions and 44 deletions

View File

@ -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() {