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

This commit is contained in:
JotaChina 2025-12-27 11:15:36 -03:00
parent 7a3df9f15c
commit d23c3aee4d
1 changed files with 95 additions and 35 deletions

View File

@ -164,46 +164,36 @@ function _schedulerTick() {
const now = audioCtx.currentTime; const now = audioCtx.currentTime;
const logicalTime = const logicalTime =
now - startTime + (appState.audio.audioEditorSeekTime || 0); now - startTime + (appState.audio.audioEditorSeekTime || 0);
const scheduleWindowStartSec = logicalTime; const scheduleWindowStartSec = logicalTime;
const scheduleWindowEndSec = logicalTime + SCHEDULE_AHEAD_TIME_SEC; const scheduleWindowEndSec = logicalTime + SCHEDULE_AHEAD_TIME_SEC;
for (const clip of appState.audio.clips) { for (const clip of appState.audio.clips) {
const clipRuntime = runtimeClipState.get(clip.id) || { isScheduled: false }; const clipRuntime = runtimeClipState.get(clip.id) || { isScheduled: false };
if (clipRuntime.isScheduled) continue; if (clipRuntime.isScheduled) continue;
if (!clip.buffer) continue; if (!clip?.buffer) continue;
const clipStartTimeSec = clip.startTimeInSeconds; const clipStartTimeSec = clip.startTimeInSeconds;
const clipDurationSec = clip.durationInSeconds; const clipDurationSec =
if ( clip.durationInSeconds ?? clip.buffer?.duration;
typeof clipStartTimeSec === "undefined" ||
typeof clipDurationSec === "undefined"
)
continue;
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) { if (isLoopActive) {
const loopDuration = loopEndTimeSec - loopStartTimeSec; const loopDuration = loopEndTimeSec - loopStartTimeSec;
if (loopDuration <= 0) continue; if (loopDuration <= 0) continue;
if (
occurrenceStartTimeSec < loopStartTimeSec && // start fora da janela do loop -> não agenda (senão toca errado e pode “matar” o resto)
logicalTime >= loopStartTimeSec if (clipStartTimeSec < loopStartTimeSec || clipStartTimeSec >= loopEndTimeSec) {
) { continue;
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;
} }
} }
const occurrenceStartTimeSec = clipStartTimeSec;
if ( if (
occurrenceStartTimeSec >= scheduleWindowStartSec && occurrenceStartTimeSec >= scheduleWindowStartSec &&
occurrenceStartTimeSec < scheduleWindowEndSec occurrenceStartTimeSec < scheduleWindowEndSec
@ -211,13 +201,65 @@ function _schedulerTick() {
const absolutePlayTime = const absolutePlayTime =
startTime + startTime +
(occurrenceStartTimeSec - (appState.audio.audioEditorSeekTime || 0)); (occurrenceStartTimeSec - (appState.audio.audioEditorSeekTime || 0));
_scheduleClip(clip, absolutePlayTime, clipDurationSec); _scheduleClip(clip, absolutePlayTime, clipDurationSec);
clipRuntime.isScheduled = true; clipRuntime.isScheduled = true;
runtimeClipState.set(clip.id, clipRuntime); 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) { function _scheduleOverlappingClipsAtLoopStart(loopStartSec, loopEndSec) {
const loopLen = loopEndSec - loopStartSec; const loopLen = loopEndSec - loopStartSec;
if (loopLen <= 0) return; if (loopLen <= 0) return;
@ -270,17 +312,29 @@ function _animationLoop() {
if (isLoopActive) { if (isLoopActive) {
if (newLogicalTime >= loopEndTimeSec) { if (newLogicalTime >= loopEndTimeSec) {
const loopDuration = loopEndTimeSec - loopStartTimeSec; const loopDuration = loopEndTimeSec - loopStartTimeSec;
if (loopDuration > 0) {
newLogicalTime = newLogicalTime =
loopStartTimeSec + ((newLogicalTime - loopStartTimeSec) % loopDuration); loopStartTimeSec +
((newLogicalTime - loopStartTimeSec) % loopDuration);
} else {
newLogicalTime = loopStartTimeSec;
}
// 1) reseta relógio lógico // realinha relógio interno
startTime = now; startTime = now;
appState.audio.audioEditorSeekTime = newLogicalTime; appState.audio.audioEditorSeekTime = newLogicalTime;
// 2) mantém Transport alinhado // ✅ força o Transport “pular” junto na virada do loop
try { Tone.Transport.seconds = newLogicalTime; } catch {} 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(); runtimeClipState.clear();
scheduledNodes.forEach(({ player }) => { scheduledNodes.forEach(({ player }) => {
try { player.unsync(); } catch {} try { player.unsync(); } catch {}
@ -289,22 +343,23 @@ function _animationLoop() {
}); });
scheduledNodes.clear(); scheduledNodes.clear();
// 4) reinicia patterns (seu comportamento atual) // ✅ reinicia patterns do song (seu scheduler da playlist)
try { try {
stopSongPatternPlaybackOnTransport(); stopSongPatternPlaybackOnTransport();
startSongPatternPlaybackOnTransport(); startSongPatternPlaybackOnTransport();
} catch {} } catch {}
// ✅ 5) reativa imediatamente os clips que atravessam o loopStart (DAW-like) // ✅ IMPORTANTÍSSIMO: recomeça clips que atravessam o loopStart
_scheduleOverlappingClipsAtLoopStart(loopStartTimeSec, loopEndTimeSec); _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(); _schedulerTick();
} }
} }
appState.audio.audioEditorLogicalTime = newLogicalTime; appState.audio.audioEditorLogicalTime = newLogicalTime;
// fim do song sem loop
if (!isLoopActive) { if (!isLoopActive) {
let maxTime = 0; let maxTime = 0;
appState.audio.clips.forEach((clip) => { appState.audio.clips.forEach((clip) => {
@ -313,8 +368,9 @@ function _animationLoop() {
const endTime = clipStartTime + clipDuration; const endTime = clipStartTime + clipDuration;
if (endTime > maxTime) maxTime = endTime; if (endTime > maxTime) maxTime = endTime;
}); });
if (maxTime > 0 && appState.audio.audioEditorLogicalTime >= maxTime) { if (maxTime > 0 && appState.audio.audioEditorLogicalTime >= maxTime) {
stopAudioEditorPlayback(true); // Rebobina no fim stopAudioEditorPlayback(true);
resetPlayheadVisual(); resetPlayheadVisual();
return; return;
} }
@ -323,10 +379,12 @@ function _animationLoop() {
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() {
@ -396,6 +454,8 @@ export async function startAudioEditorPlayback(seekTime) {
const bpm = parseFloat(document.getElementById("bpm-input")?.value) || 120; const bpm = parseFloat(document.getElementById("bpm-input")?.value) || 120;
Tone.Transport.bpm.value = bpm; Tone.Transport.bpm.value = bpm;
Tone.Transport.start(); Tone.Transport.start();
// ✅ se começou no meio de algum clip, inicia com offset correto
_scheduleOverlappingClipsAtTime(timeToStart);
} catch {} } catch {}
// mantém seu scheduler/animador // mantém seu scheduler/animador