samples de áudio não reiniciavam ao fim do loop
Deploy / Deploy (push) Successful in 2m9s
Details
Deploy / Deploy (push) Successful in 2m9s
Details
This commit is contained in:
parent
23258e7d02
commit
7a3df9f15c
|
|
@ -83,17 +83,15 @@ function _toToneBuffer(buffer) {
|
||||||
|
|
||||||
// --- Lógica Principal do Scheduler (mantida) ---
|
// --- Lógica Principal do Scheduler (mantida) ---
|
||||||
|
|
||||||
function _scheduleClip(clip, absolutePlayTime, durationSec) {
|
function _scheduleClip(clip, absolutePlayTime, durationSec, overrideOffsetSec) {
|
||||||
if (!clip.buffer) {
|
if (!clip.buffer) {
|
||||||
console.warn(`Clip ${clip.id} não possui áudio buffer carregado.`);
|
console.warn(`Clip ${clip.id} não possui áudio buffer carregado.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// usamos Player .sync() conectando no mesmo grafo do Tone
|
|
||||||
const toneBuf = _toToneBuffer(clip.buffer);
|
const toneBuf = _toToneBuffer(clip.buffer);
|
||||||
if (!toneBuf) return;
|
if (!toneBuf) return;
|
||||||
|
|
||||||
// cadeia de ganho/pan por clipe (se já tiver no estado, use; aqui garantimos)
|
|
||||||
const gain =
|
const gain =
|
||||||
clip.gainNode instanceof Tone.Gain
|
clip.gainNode instanceof Tone.Gain
|
||||||
? clip.gainNode
|
? clip.gainNode
|
||||||
|
|
@ -103,68 +101,53 @@ function _scheduleClip(clip, absolutePlayTime, durationSec) {
|
||||||
? clip.pannerNode
|
? clip.pannerNode
|
||||||
: new Tone.Panner(clip.pan ?? 0);
|
: new Tone.Panner(clip.pan ?? 0);
|
||||||
|
|
||||||
// conecta no destino principal (é um ToneAudioNode)
|
try { gain.disconnect(); } catch {}
|
||||||
try {
|
try { pan.disconnect(); } catch {}
|
||||||
gain.disconnect(); // evita duplicatas caso exista de execuções anteriores
|
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
pan.disconnect();
|
|
||||||
} catch {}
|
|
||||||
gain.connect(pan).connect(getMainGainNode());
|
gain.connect(pan).connect(getMainGainNode());
|
||||||
|
|
||||||
// player sincronizado no Transport
|
|
||||||
const player = new Tone.Player(toneBuf).sync().connect(gain);
|
const player = new Tone.Player(toneBuf).sync().connect(gain);
|
||||||
|
|
||||||
// aplica pitch como rate (semitons → rate)
|
|
||||||
const rate =
|
const rate =
|
||||||
clip.pitch && clip.pitch !== 0 ? Math.pow(2, clip.pitch / 12) : 1;
|
clip.pitch && clip.pitch !== 0 ? Math.pow(2, clip.pitch / 12) : 1;
|
||||||
player.playbackRate = rate;
|
player.playbackRate = rate;
|
||||||
|
|
||||||
// calculamos o "when" no tempo do Transport:
|
// --- tempo no Transport (em segundos) ---
|
||||||
// absolutePlayTime é em audioCtx.currentTime; o "zero" lógico foi quando demos play:
|
|
||||||
// logical = (now - startTime) + seek; => occurrence = (absolutePlayTime - startTime) + seek
|
|
||||||
const occurrenceInTransportSec =
|
const occurrenceInTransportSec =
|
||||||
absolutePlayTime - startTime + (appState.audio.audioEditorSeekTime || 0);
|
absolutePlayTime - startTime + (appState.audio.audioEditorSeekTime || 0);
|
||||||
|
|
||||||
const offset = clip.offsetInSeconds ?? clip.offset ?? 0;
|
const baseOffset = clip.offsetInSeconds ?? clip.offset ?? 0;
|
||||||
const dur = durationSec ?? toneBuf.duration;
|
const offset = overrideOffsetSec ?? baseOffset;
|
||||||
|
const dur = durationSec ?? clip.durationInSeconds ?? 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 safeOccurrence = Math.max(0, occurrenceInTransportSec);
|
const safeOccurrence = Math.max(0, occurrenceInTransportSec);
|
||||||
const safeOffset = Math.max(0, offset);
|
const safeOffset = Math.max(0, offset);
|
||||||
// Duração pode ser 'undefined', mas se existir, não pode ser negativa
|
const safeDur = dur == null ? undefined : Math.max(0, dur);
|
||||||
const safeDur =
|
|
||||||
dur === undefined || dur === null ? undefined : Math.max(0, dur);
|
|
||||||
// --- FIM DA CORREÇÃO ---
|
|
||||||
|
|
||||||
// agenda (agora usando os valores seguros)
|
// ✅ blindagem: nunca agenda no passado (especialmente após “virada” do loop)
|
||||||
player.start(safeOccurrence, safeOffset, safeDur);
|
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++;
|
const eventId = nextEventId++;
|
||||||
scheduledNodes.set(eventId, { player, clipId: clip.id });
|
scheduledNodes.set(eventId, { player, clipId: clip.id });
|
||||||
|
|
||||||
if (callbacks.onClipScheduled) {
|
if (callbacks.onClipScheduled) callbacks.onClipScheduled(clip);
|
||||||
callbacks.onClipScheduled(clip);
|
|
||||||
}
|
|
||||||
|
|
||||||
// quando parar naturalmente, limpamos runtime
|
|
||||||
player.onstop = () => {
|
player.onstop = () => {
|
||||||
_handleClipEnd(eventId, clip.id);
|
_handleClipEnd(eventId, clip.id);
|
||||||
try {
|
try { player.unsync(); } catch {}
|
||||||
player.unsync();
|
try { player.dispose(); } catch {}
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
player.dispose();
|
|
||||||
} catch {}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function _handleClipEnd(eventId, clipId) {
|
function _handleClipEnd(eventId, clipId) {
|
||||||
scheduledNodes.delete(eventId);
|
scheduledNodes.delete(eventId);
|
||||||
runtimeClipState.delete(clipId);
|
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() {
|
function _animationLoop() {
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
animationFrameId = null;
|
animationFrameId = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = audioCtx.currentTime;
|
const now = audioCtx.currentTime;
|
||||||
let newLogicalTime =
|
let newLogicalTime =
|
||||||
now - startTime + (appState.audio.audioEditorSeekTime || 0);
|
now - startTime + (appState.audio.audioEditorSeekTime || 0);
|
||||||
|
|
@ -251,13 +273,14 @@ function _animationLoop() {
|
||||||
newLogicalTime =
|
newLogicalTime =
|
||||||
loopStartTimeSec + ((newLogicalTime - loopStartTimeSec) % loopDuration);
|
loopStartTimeSec + ((newLogicalTime - loopStartTimeSec) % loopDuration);
|
||||||
|
|
||||||
|
// 1) reseta relógio lógico
|
||||||
startTime = now;
|
startTime = now;
|
||||||
appState.audio.audioEditorSeekTime = newLogicalTime;
|
appState.audio.audioEditorSeekTime = newLogicalTime;
|
||||||
|
|
||||||
// ✅ 1) manter o Tone.Transport em sincronia
|
// 2) mantém Transport alinhado
|
||||||
try { Tone.Transport.seconds = newLogicalTime; } catch {}
|
try { Tone.Transport.seconds = newLogicalTime; } catch {}
|
||||||
|
|
||||||
// ✅ 2) permitir reagendamento de clips/patterns
|
// 3) limpa runtime e mata players antigos
|
||||||
runtimeClipState.clear();
|
runtimeClipState.clear();
|
||||||
scheduledNodes.forEach(({ player }) => {
|
scheduledNodes.forEach(({ player }) => {
|
||||||
try { player.unsync(); } catch {}
|
try { player.unsync(); } catch {}
|
||||||
|
|
@ -266,11 +289,17 @@ function _animationLoop() {
|
||||||
});
|
});
|
||||||
scheduledNodes.clear();
|
scheduledNodes.clear();
|
||||||
|
|
||||||
// ✅ 3) reinicia patterns no Transport (se houver scheduleOnce para notes)
|
// 4) reinicia patterns (seu comportamento atual)
|
||||||
try {
|
try {
|
||||||
stopSongPatternPlaybackOnTransport();
|
stopSongPatternPlaybackOnTransport();
|
||||||
startSongPatternPlaybackOnTransport();
|
startSongPatternPlaybackOnTransport();
|
||||||
} catch {}
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pixelsPerSecond = getPixelsPerSecond();
|
const pixelsPerSecond = getPixelsPerSecond();
|
||||||
const newPositionPx = appState.audio.audioEditorLogicalTime * pixelsPerSecond;
|
const newPositionPx = appState.audio.audioEditorLogicalTime * pixelsPerSecond;
|
||||||
updatePlayheadVisual(newPositionPx);
|
updatePlayheadVisual(newPositionPx);
|
||||||
animationFrameId = requestAnimationFrame(_animationLoop);
|
animationFrameId = requestAnimationFrame(_animationLoop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- API Pública ---
|
// --- API Pública ---
|
||||||
|
|
||||||
export function updateTransportLoop() {
|
export function updateTransportLoop() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue