samples de áudio não reiniciavam ao fim do loop
Deploy / Deploy (push) Successful in 2m14s
Details
Deploy / Deploy (push) Successful in 2m14s
Details
This commit is contained in:
parent
7a3df9f15c
commit
d23c3aee4d
|
|
@ -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;
|
||||||
newLogicalTime =
|
if (loopDuration > 0) {
|
||||||
loopStartTimeSec + ((newLogicalTime - loopStartTimeSec) % loopDuration);
|
newLogicalTime =
|
||||||
|
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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue