diff --git a/assets/js/creations/audio/audio_audio.js b/assets/js/creations/audio/audio_audio.js
index a92d9807..9e306f0f 100644
--- a/assets/js/creations/audio/audio_audio.js
+++ b/assets/js/creations/audio/audio_audio.js
@@ -1,7 +1,15 @@
// js/audio/audio_audio.js
import { appState } from "../state.js";
-import { updateAudioEditorUI, updatePlayheadVisual, resetPlayheadVisual } from "./audio_ui.js";
-import { initializeAudioContext, getAudioContext, getMainGainNode } from "../audio.js";
+import {
+ updateAudioEditorUI,
+ updatePlayheadVisual,
+ resetPlayheadVisual,
+} from "./audio_ui.js";
+import {
+ initializeAudioContext,
+ getAudioContext,
+ getMainGainNode,
+} from "../audio.js";
import { getPixelsPerSecond } from "../utils.js";
// 🔊 ADIÇÃO: usar a MESMA instância do Tone que o projeto usa
import * as Tone from "https://esm.sh/tone";
@@ -17,19 +25,19 @@ let schedulerIntervalId = null;
let animationFrameId = null;
// Sincronização de Tempo
-let startTime = 0;
+let startTime = 0;
// (seek/logical ficam em appState.audio)
// Configurações de Loop
let isLoopActive = false;
let loopStartTimeSec = 0;
-let loopEndTimeSec = 8;
+let loopEndTimeSec = 8;
// estado runtime
const runtimeClipState = new Map();
// ⚠️ agora armazenamos Tone.Player em vez de BufferSource
const scheduledNodes = new Map(); // eventId -> { player, clipId }
-let nextEventId = 0;
+let nextEventId = 0;
const callbacks = {
onClipScheduled: null,
@@ -41,9 +49,15 @@ function _getBpm() {
const bpmInput = document.getElementById("bpm-input");
return parseFloat(bpmInput.value) || 120;
}
-function _getSecondsPerBeat() { return 60.0 / _getBpm(); }
-function _convertBeatToSeconds(beat) { return beat * _getSecondsPerBeat(); }
-function _convertSecondsToBeat(seconds) { return seconds / _getSecondsPerBeat(); }
+function _getSecondsPerBeat() {
+ return 60.0 / _getBpm();
+}
+function _convertBeatToSeconds(beat) {
+ return beat * _getSecondsPerBeat();
+}
+function _convertSecondsToBeat(seconds) {
+ return seconds / _getSecondsPerBeat();
+}
// garante um único contexto — o rawContext do Tone
function _initContext() {
@@ -75,8 +89,14 @@ function _scheduleClip(clip, absolutePlayTime, durationSec) {
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 : new Tone.Gain(clip.volume ?? 1);
- const pan = clip.pannerNode instanceof Tone.Panner ? clip.pannerNode : new Tone.Panner(clip.pan ?? 0);
+ const gain =
+ clip.gainNode instanceof Tone.Gain
+ ? clip.gainNode
+ : new Tone.Gain(clip.volume ?? 1);
+ const pan =
+ clip.pannerNode instanceof Tone.Panner
+ ? clip.pannerNode
+ : new Tone.Panner(clip.pan ?? 0);
// conecta no destino principal (é um ToneAudioNode)
try {
@@ -91,19 +111,35 @@ function _scheduleClip(clip, absolutePlayTime, durationSec) {
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;
+ 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
- const occurrenceInTransportSec = (absolutePlayTime - startTime) + (appState.audio.audioEditorSeekTime || 0);
+ const occurrenceInTransportSec =
+ absolutePlayTime - startTime + (appState.audio.audioEditorSeekTime || 0);
const offset = clip.offsetInSeconds ?? clip.offset ?? 0;
const dur = durationSec ?? toneBuf.duration;
- // agenda
- player.start(occurrenceInTransportSec, offset, dur);
+ // --- 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 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 ---
+
+ // agenda (agora usando os valores seguros)
+ player.start(safeOccurrence, safeOffset, safeDur);
const eventId = nextEventId++;
scheduledNodes.set(eventId, { player, clipId: clip.id });
@@ -115,17 +151,21 @@ function _scheduleClip(clip, absolutePlayTime, durationSec) {
// 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);
+ runtimeClipState.delete(clipId);
if (callbacks.onClipPlayed) {
- const clip = appState.audio.clips.find(c => c.id == clipId);
+ const clip = appState.audio.clips.find((c) => c.id == clipId);
if (clip) callbacks.onClipPlayed(clip);
}
}
@@ -134,7 +174,8 @@ function _schedulerTick() {
if (!isPlaying || !audioCtx) return;
const now = audioCtx.currentTime;
- const logicalTime = (now - startTime) + (appState.audio.audioEditorSeekTime || 0);
+ const logicalTime =
+ now - startTime + (appState.audio.audioEditorSeekTime || 0);
const scheduleWindowStartSec = logicalTime;
const scheduleWindowEndSec = logicalTime + SCHEDULE_AHEAD_TIME_SEC;
@@ -145,19 +186,32 @@ function _schedulerTick() {
const clipStartTimeSec = clip.startTimeInSeconds;
const clipDurationSec = clip.durationInSeconds;
- if (typeof clipStartTimeSec === 'undefined' || typeof clipDurationSec === 'undefined') continue;
+ if (
+ typeof clipStartTimeSec === "undefined" ||
+ typeof clipDurationSec === "undefined"
+ )
+ continue;
let occurrenceStartTimeSec = clipStartTimeSec;
if (isLoopActive) {
const loopDuration = loopEndTimeSec - loopStartTimeSec;
- if (loopDuration <= 0) continue;
- if (occurrenceStartTimeSec < loopStartTimeSec && logicalTime >= loopStartTimeSec) {
- const offsetFromLoopStart = (occurrenceStartTimeSec - loopStartTimeSec) % loopDuration;
- occurrenceStartTimeSec = loopStartTimeSec + (offsetFromLoopStart < 0 ? offsetFromLoopStart + loopDuration : offsetFromLoopStart);
+ if (loopDuration <= 0) continue;
+ if (
+ occurrenceStartTimeSec < loopStartTimeSec &&
+ logicalTime >= loopStartTimeSec
+ ) {
+ const offsetFromLoopStart =
+ (occurrenceStartTimeSec - loopStartTimeSec) % loopDuration;
+ occurrenceStartTimeSec =
+ loopStartTimeSec +
+ (offsetFromLoopStart < 0
+ ? offsetFromLoopStart + loopDuration
+ : offsetFromLoopStart);
}
if (occurrenceStartTimeSec < logicalTime) {
- const loopsMissed = Math.floor((logicalTime - occurrenceStartTimeSec) / loopDuration) + 1;
+ const loopsMissed =
+ Math.floor((logicalTime - occurrenceStartTimeSec) / loopDuration) + 1;
occurrenceStartTimeSec += loopsMissed * loopDuration;
}
}
@@ -166,7 +220,9 @@ function _schedulerTick() {
occurrenceStartTimeSec >= scheduleWindowStartSec &&
occurrenceStartTimeSec < scheduleWindowEndSec
) {
- const absolutePlayTime = startTime + (occurrenceStartTimeSec - (appState.audio.audioEditorSeekTime || 0));
+ const absolutePlayTime =
+ startTime +
+ (occurrenceStartTimeSec - (appState.audio.audioEditorSeekTime || 0));
_scheduleClip(clip, absolutePlayTime, clipDurationSec);
clipRuntime.isScheduled = true;
runtimeClipState.set(clip.id, clipRuntime);
@@ -181,31 +237,33 @@ function _animationLoop() {
return;
}
const now = audioCtx.currentTime;
- let newLogicalTime = (now - startTime) + (appState.audio.audioEditorSeekTime || 0);
-
+ let newLogicalTime =
+ now - startTime + (appState.audio.audioEditorSeekTime || 0);
+
if (isLoopActive) {
if (newLogicalTime >= loopEndTimeSec) {
const loopDuration = loopEndTimeSec - loopStartTimeSec;
- newLogicalTime = loopStartTimeSec + ((newLogicalTime - loopStartTimeSec) % loopDuration);
+ newLogicalTime =
+ loopStartTimeSec + ((newLogicalTime - loopStartTimeSec) % loopDuration);
startTime = now;
appState.audio.audioEditorSeekTime = newLogicalTime;
}
}
-
- appState.audio.audioEditorLogicalTime = newLogicalTime;
-
+
+ appState.audio.audioEditorLogicalTime = newLogicalTime;
+
if (!isLoopActive) {
let maxTime = 0;
- appState.audio.clips.forEach(clip => {
- const clipStartTime = clip.startTimeInSeconds || 0;
- const clipDuration = clip.durationInSeconds || 0;
- const endTime = clipStartTime + clipDuration;
- if (endTime > maxTime) maxTime = endTime;
+ appState.audio.clips.forEach((clip) => {
+ const clipStartTime = clip.startTimeInSeconds || 0;
+ const clipDuration = clip.durationInSeconds || 0;
+ const endTime = clipStartTime + clipDuration;
+ if (endTime > maxTime) maxTime = endTime;
});
if (maxTime > 0 && appState.audio.audioEditorLogicalTime >= maxTime) {
- stopAudioEditorPlayback(true); // Rebobina no fim
- resetPlayheadVisual();
- return;
+ stopAudioEditorPlayback(true); // Rebobina no fim
+ resetPlayheadVisual();
+ return;
}
}
const pixelsPerSecond = getPixelsPerSecond();
@@ -222,23 +280,30 @@ export function updateTransportLoop() {
loopEndTimeSec = appState.global.loopEndTime;
runtimeClipState.clear();
-
+
// parar e descartar players agendados
scheduledNodes.forEach(({ player }) => {
- try { player.unsync(); } catch {}
- try { player.stop(); } catch {}
- try { player.dispose(); } catch {}
+ try {
+ player.unsync();
+ } catch {}
+ try {
+ player.stop();
+ } catch {}
+ try {
+ player.dispose();
+ } catch {}
});
scheduledNodes.clear();
}
-export async function startAudioEditorPlayback(seekTime) { // 1. Aceita 'seekTime' como parâmetro
+export async function startAudioEditorPlayback(seekTime) {
+ // 1. Aceita 'seekTime' como parâmetro
if (isPlaying) return;
_initContext();
// garante contexto ativo do Tone (gesto do usuário já ocorreu antes)
await Tone.start();
- if (audioCtx.state === 'suspended') {
+ if (audioCtx.state === "suspended") {
await audioCtx.resume();
}
@@ -251,23 +316,24 @@ export async function startAudioEditorPlayback(seekTime) { // 1. Aceita 'seekTim
// =================================================================
// 👇 INÍCIO DA CORREÇÃO (Bugs 1 & 2)
// =================================================================
-
+
// 1. Determine o tempo de início:
- // Use o 'seekTime' recebido (da ação global) se for um número válido (>= 0).
- // Caso contrário, use o tempo de seek local atual.
- const timeToStart = (seekTime !== null && seekTime !== undefined && !isNaN(seekTime))
- ? seekTime
- : (appState.audio.audioEditorSeekTime || 0); // 👈 Usa sua variável de estado
+ let timeToStart =
+ seekTime !== null && seekTime !== undefined && !isNaN(seekTime)
+ ? seekTime
+ : appState.audio.audioEditorSeekTime || 0; // 👈 Usa sua variável de estado
- // 2. Atualize o estado global (para a agulha pular)
- // Isso garante que o estado local E o Tone estejam sincronizados.
- appState.audio.audioEditorSeekTime = timeToStart;
+ // 2. Clampa o valor (parte da correção do RangeError)
+ timeToStart = Math.max(0, timeToStart);
- // 3. Alinhe o Tone.Transport a esse tempo
+ // 3. Atualize o estado global (para a agulha pular)
+ appState.audio.audioEditorSeekTime = timeToStart;
+
+ // 4. Alinhe o Tone.Transport a esse tempo
try {
Tone.Transport.seconds = timeToStart; // 👈 Usa o tempo sincronizado
} catch {}
-
+
// =================================================================
// 👆 FIM DA CORREÇÃO
// =================================================================
@@ -295,37 +361,48 @@ export function stopAudioEditorPlayback(rewind = false) {
isPlaying = false;
appState.global.isAudioEditorPlaying = false;
-
+
console.log(`%cParando Playback... (Rewind: ${rewind})`, "color: #d9534f;");
// para o Transport (para Players .sync())
- try { Tone.Transport.stop(); } catch {}
+ try {
+ Tone.Transport.stop();
+ } catch {}
clearInterval(schedulerIntervalId);
schedulerIntervalId = null;
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
- appState.audio.audioEditorSeekTime = appState.audio.audioEditorLogicalTime || 0;
- appState.audio.audioEditorLogicalTime = 0;
+ appState.audio.audioEditorSeekTime =
+ appState.audio.audioEditorLogicalTime || 0;
+ appState.audio.audioEditorLogicalTime = 0;
if (rewind) {
appState.audio.audioEditorSeekTime = 0;
- try { Tone.Transport.seconds = 0; } catch {}
+ try {
+ Tone.Transport.seconds = 0;
+ } catch {}
}
-
+
// parar e descartar players agendados
scheduledNodes.forEach(({ player }) => {
- try { player.unsync(); } catch {}
- try { player.stop(); } catch {}
- try { player.dispose(); } catch {}
+ try {
+ player.unsync();
+ } catch {}
+ try {
+ player.stop();
+ } catch {}
+ try {
+ player.dispose();
+ } catch {}
});
scheduledNodes.clear();
runtimeClipState.clear();
updateAudioEditorUI();
const playBtn = document.getElementById("audio-editor-play-btn");
- if (playBtn) playBtn.className = 'fa-solid fa-play';
-
+ if (playBtn) playBtn.className = "fa-solid fa-play";
+
if (rewind) {
resetPlayheadVisual();
}
@@ -343,16 +420,21 @@ export function seekAudioEditor(newTime) {
if (wasPlaying) {
stopAudioEditorPlayback(false); // Pausa
}
-
+
+ // Clampa o novo tempo
+ newTime = Math.max(0, newTime);
+
appState.audio.audioEditorSeekTime = newTime;
- appState.audio.audioEditorLogicalTime = newTime;
-
- try { Tone.Transport.seconds = newTime; } catch {}
+ appState.audio.audioEditorLogicalTime = newTime;
+
+ try {
+ Tone.Transport.seconds = newTime;
+ } catch {}
const pixelsPerSecond = getPixelsPerSecond();
const newPositionPx = newTime * pixelsPerSecond;
updatePlayheadVisual(newPositionPx);
-
+
if (wasPlaying) {
startAudioEditorPlayback();
}
diff --git a/assets/js/creations/audio/audio_state.js b/assets/js/creations/audio/audio_state.js
index 4fe23016..4cff2e49 100644
--- a/assets/js/creations/audio/audio_state.js
+++ b/assets/js/creations/audio/audio_state.js
@@ -5,38 +5,43 @@ import { getMainGainNode, getAudioContext } from "../audio.js";
import * as Tone from "https://esm.sh/tone";
export let audioState = {
- tracks: [],
- clips: [],
- // --- TEMPOS MOVIDOS DO audio_audio.js PARA O ESTADO GLOBAL ---
- audioEditorSeekTime: 0,
- audioEditorLogicalTime: 0,
- // --- FIM DA MUDANÇA ---
- audioEditorStartTime: 0,
- audioEditorAnimationId: null,
- audioEditorPlaybackTime: 0,
- isAudioEditorLoopEnabled: false,
+ tracks: [],
+ clips: [],
+ // --- TEMPOS MOVIDOS DO audio_audio.js PARA O ESTADO GLOBAL ---
+ audioEditorSeekTime: 0,
+ audioEditorLogicalTime: 0,
+ // --- FIM DA MUDANÇA ---
+ audioEditorStartTime: 0,
+ audioEditorAnimationId: null,
+ audioEditorPlaybackTime: 0,
+ isAudioEditorLoopEnabled: false,
};
// ==== SNAPSHOT: exportação do estado atual (tracks + clips) ====
export function getAudioSnapshot() {
// Se seu estado “oficial” é audioState.* use ele;
// se for appState.audio.* troque abaixo.
- const tracks = (audioState.tracks || []).map(t => ({
- id: t.id, name: t.name
+ const tracks = (audioState.tracks || []).map((t) => ({
+ id: t.id,
+ name: t.name,
}));
- const clips = (audioState.clips || []).map(c => ({
+ const clips = (audioState.clips || []).map((c) => ({
id: c.id,
trackId: c.trackId,
name: c.name,
- sourcePath: c.sourcePath || null, // URL do asset (precisa ser acessível)
+ sourcePath: c.sourcePath || null, // URL do asset (precisa ser acessível)
startTimeInSeconds: c.startTimeInSeconds || 0,
- durationInSeconds: c.durationInSeconds || (c.buffer?.duration || 0),
+ durationInSeconds: c.durationInSeconds || c.buffer?.duration || 0,
offset: c.offset || 0,
pitch: c.pitch || 0,
volume: c.volume ?? 1,
pan: c.pan ?? 0,
- originalDuration: c.originalDuration || (c.buffer?.duration || 0),
+ originalDuration: c.originalDuration || c.buffer?.duration || 0,
+
+ // --- NOVA MODIFICAÇÃO (SNAPSHOT) ---
+ // Também enviamos os dados do pattern se existirem
+ patternData: c.patternData || null,
}));
return { tracks, clips };
@@ -48,32 +53,47 @@ export async function applyAudioSnapshot(snapshot) {
// aplica trilhas (mantém ids/nome)
if (Array.isArray(snapshot.tracks) && snapshot.tracks.length) {
- audioState.tracks = snapshot.tracks.map(t => ({ id: t.id, name: t.name }));
+ audioState.tracks = snapshot.tracks.map((t) => ({
+ id: t.id,
+ name: t.name,
+ }));
}
// insere clipes usando os MESMOS ids do emissor (idempotente)
if (Array.isArray(snapshot.clips)) {
for (const c of snapshot.clips) {
// evita duplicar se já existir (idempotência)
- if (audioState.clips.some(x => String(x.id) === String(c.id))) continue;
+ if (audioState.clips.some((x) => String(x.id) === String(c.id))) continue;
// usa a própria função de criação (agora ela aceita id e nome)
- // assinatura: addAudioClipToTimeline(samplePath, trackId, start, clipId, name)
- addAudioClipToTimeline(c.sourcePath, c.trackId, c.startTimeInSeconds, c.id, c.name);
+ // --- NOVA MODIFICAÇÃO (SNAPSHOT) ---
+ // Passamos o patternData para a função de criação
+ addAudioClipToTimeline(
+ c.sourcePath,
+ c.trackId,
+ c.startTimeInSeconds,
+ c.id,
+ c.name,
+ c.patternData // <-- Passa os dados do pattern
+ );
// aplica propriedades adicionais (dur/offset/pitch/vol/pan) no mesmo id
- const idx = audioState.clips.findIndex(x => String(x.id) === String(c.id));
+ const idx = audioState.clips.findIndex(
+ (x) => String(x.id) === String(c.id)
+ );
if (idx >= 0) {
const clip = audioState.clips[idx];
- clip.durationInSeconds = c.durationInSeconds;
- clip.offset = c.offset;
- clip.pitch = c.pitch;
- clip.volume = c.volume;
- clip.pan = c.pan;
- clip.originalDuration = c.originalDuration;
+ clip.durationInSeconds = c.durationInSeconds;
+ clip.offset = c.offset;
+ clip.pitch = c.pitch;
+ clip.volume = c.volume;
+ clip.pan = c.pan;
+ clip.originalDuration = c.originalDuration;
+
+ // (patternData já foi definido durante a criação acima)
// reflete nos nós Tone já criados
- if (clip.gainNode) clip.gainNode.gain.value = clip.volume ?? 1;
+ if (clip.gainNode) clip.gainNode.gain.value = clip.volume ?? 1;
if (clip.pannerNode) clip.pannerNode.pan.value = clip.pan ?? 0;
}
}
@@ -83,52 +103,57 @@ export async function applyAudioSnapshot(snapshot) {
renderAudioEditor();
}
-
export function initializeAudioState() {
- audioState.clips.forEach(clip => {
- if (clip.pannerNode) clip.pannerNode.dispose();
- if (clip.gainNode) clip.gainNode.dispose();
- });
- Object.assign(audioState, {
- tracks: [],
- clips: [],
- // --- ADICIONADO ---
- audioEditorSeekTime: 0,
- audioEditorLogicalTime: 0,
- // --- FIM ---
- audioEditorStartTime: 0,
- audioEditorAnimationId: null,
- audioEditorPlaybackTime: 0,
- isAudioEditorLoopEnabled: false,
- });
+ audioState.clips.forEach((clip) => {
+ if (clip.pannerNode) clip.pannerNode.dispose();
+ if (clip.gainNode) clip.gainNode.dispose();
+ });
+ Object.assign(audioState, {
+ tracks: [],
+ clips: [],
+ // --- ADICIONADO ---
+ audioEditorSeekTime: 0,
+ audioEditorLogicalTime: 0,
+ // --- FIM ---
+ audioEditorStartTime: 0,
+ audioEditorAnimationId: null,
+ audioEditorPlaybackTime: 0,
+ isAudioEditorLoopEnabled: false,
+ });
}
export async function loadAudioForClip(clip) {
// --- ADIÇÃO ---
// Se já temos um buffer (do bounce ou colagem), não faz fetch
if (clip.buffer) {
- // Garante que as durações estão corretas
- if (clip.originalDuration === 0) clip.originalDuration = clip.buffer.duration;
- if (clip.durationInSeconds === 0) clip.durationInSeconds = clip.buffer.duration;
- return clip;
+ // Garante que as durações estão corretas
+ if (clip.originalDuration === 0)
+ clip.originalDuration = clip.buffer.duration;
+ if (clip.durationInSeconds === 0)
+ clip.durationInSeconds = clip.buffer.duration;
+ return clip;
}
// --- FIM DA ADIÇÃO ---
- if (!clip.sourcePath) return clip;
-
+ if (!clip.sourcePath || clip.sourcePath.startsWith("blob:")) {
+ // Se não há caminho ou se é um blob, não há nada para buscar.
+ return clip;
+ }
+
const audioCtx = getAudioContext();
if (!audioCtx) {
- console.error("AudioContext não disponível para carregar áudio.");
- return clip;
+ console.error("AudioContext não disponível para carregar áudio.");
+ return clip;
}
-
+
try {
const response = await fetch(clip.sourcePath);
- if (!response.ok) throw new Error(`Falha ao buscar áudio: ${clip.sourcePath}`);
+ if (!response.ok)
+ throw new Error(`Falha ao buscar áudio: ${clip.sourcePath}`);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
- clip.buffer = audioBuffer;
+ clip.buffer = audioBuffer;
// --- CORREÇÃO: Salva a duração original ---
if (clip.durationInSeconds === 0) {
@@ -136,7 +161,6 @@ export async function loadAudioForClip(clip) {
}
// Salva a duração real do buffer para cálculos de stretch
clip.originalDuration = audioBuffer.duration;
-
} catch (error) {
console.error(`Falha ao carregar áudio para o clipe ${clip.name}:`, error);
}
@@ -145,123 +169,182 @@ export async function loadAudioForClip(clip) {
// helper de id (fallback se o emissor não mandar)
function genClipId() {
- return (crypto?.randomUUID?.() || `clip_${Date.now()}_${Math.floor(Math.random()*1e6)}`);
+ return (
+ crypto?.randomUUID?.() ||
+ `clip_${Date.now()}_${Math.floor(Math.random() * 1e6)}`
+ );
}
// --- FUNÇÃO MODIFICADA ---
-// agora aceita clipId e clipName vindos do emissor; mantém compat com chamadas antigas
-export function addAudioClipToTimeline(samplePath, trackId = 1, startTime = 0, clipIdOrName = null, nameOrBuffer = null, maybeBuffer = null) {
- // compat: se passaram (filePath, trackId, start, clipId)
- // mas versões antigas chamavam (filePath, trackId, start) ou (filePath, trackId, start, name, buffer)
- let incomingId = null;
- let clipName = null;
- let existingBuffer = null;
+// O 6º argumento (maybeBuffer) agora é tratado para aceitar (Buffer) OU (patternData)
+export function addAudioClipToTimeline(
+ samplePath,
+ trackId = 1,
+ startTime = 0,
+ clipIdOrName = null,
+ nameOrBuffer = null,
+ maybeBuffer = null
+) {
+ let incomingId = null;
+ let clipName = null;
+ let existingBuffer = null;
+ let incomingPatternData = null; // <-- NOSSO NOVO DADO
- // heurística: se clipIdOrName parece um UUID/clip_ → é id, senão é nome
- if (typeof clipIdOrName === 'string' && (clipIdOrName.startsWith('clip_') || clipIdOrName.length >= 16)) {
- incomingId = clipIdOrName;
- clipName = (typeof nameOrBuffer === 'string') ? nameOrBuffer : null;
- existingBuffer = maybeBuffer || (nameOrBuffer && typeof nameOrBuffer !== 'string' ? nameOrBuffer : null);
- } else {
- // assinatura antiga: 4º arg era nome
- clipName = (typeof clipIdOrName === 'string') ? clipIdOrName : null;
- existingBuffer = (nameOrBuffer && typeof nameOrBuffer !== 'string') ? nameOrBuffer : null;
+ // Função helper para checar se é um buffer (para evitar bugs)
+ // (Um Tone.ToneAudioBuffer tem a prop ._buffer que é o AudioBuffer)
+ const isBuffer = (obj) =>
+ obj &&
+ (obj instanceof AudioBuffer || (obj && obj._buffer instanceof AudioBuffer));
+
+ // heurística: se clipIdOrName parece um UUID/clip_ → é id, senão é nome
+ if (
+ typeof clipIdOrName === "string" &&
+ (clipIdOrName.startsWith("clip_") ||
+ clipIdOrName.startsWith("bounced_") ||
+ clipIdOrName.length >= 16)
+ ) {
+ incomingId = clipIdOrName; // 4º arg é ID
+ clipName = typeof nameOrBuffer === "string" ? nameOrBuffer : null; // 5º arg é Nome
+
+ // --- INÍCIO DA CORREÇÃO (Passo 3) ---
+ // O 6º argumento (maybeBuffer) pode ser o buffer OU o patternData.
+ // O 5º (nameOrBuffer) pode ser o nome OU o buffer.
+
+ // 1. Checa se o 6º argumento é o buffer
+ if (isBuffer(maybeBuffer)) {
+ existingBuffer = maybeBuffer;
+ }
+ // 2. Checa se o 6º argumento é o patternData (array)
+ else if (Array.isArray(maybeBuffer)) {
+ incomingPatternData = maybeBuffer;
}
- const finalId = incomingId || genClipId();
-
- // idempotência: se o id já existe, não duplica
- if (audioState.clips.some(c => String(c.id) === String(finalId))) {
- return;
+ // 3. Se o buffer não veio no 6º, checa se veio no 5º (assinatura antiga)
+ if (!existingBuffer && isBuffer(nameOrBuffer)) {
+ existingBuffer = nameOrBuffer;
}
+ // --- FIM DA CORREÇÃO ---
+ } else {
+ // assinatura antiga: 4º arg era nome
+ clipName = typeof clipIdOrName === "string" ? clipIdOrName : null;
+ // 5º arg era buffer
+ existingBuffer = isBuffer(nameOrBuffer) ? nameOrBuffer : null;
+ }
- const newClip = {
- id: finalId,
- trackId: trackId,
- sourcePath: samplePath, // Pode ser null se existingBuffer for fornecido
- name: clipName || (samplePath ? String(samplePath).split('/').pop() : 'Bounced Clip'),
-
- startTimeInSeconds: startTime,
- offset: 0,
- durationInSeconds: 0,
- originalDuration: 0,
+ const finalId = incomingId || genClipId();
- pitch: 0,
- volume: DEFAULT_VOLUME,
- pan: DEFAULT_PAN,
-
- buffer: existingBuffer || null,
- player: null,
- };
-
- // volume linear (0–1)
- newClip.gainNode = new Tone.Gain(DEFAULT_VOLUME);
- newClip.pannerNode = new Tone.Panner(DEFAULT_PAN);
+ // idempotência: se o id já existe, não duplica
+ if (audioState.clips.some((c) => String(c.id) === String(finalId))) {
+ return;
+ }
- // conecta tudo no grafo do Tone (mesmo contexto)
- newClip.gainNode.connect(newClip.pannerNode);
- newClip.pannerNode.connect(getMainGainNode());
+ const newClip = {
+ id: finalId,
+ trackId: trackId,
+ sourcePath: samplePath, // Pode ser null se existingBuffer for fornecido
+ name:
+ clipName ||
+ (samplePath ? String(samplePath).split("/").pop() : "Bounced Clip"),
- audioState.clips.push(newClip);
-
- // loadAudioForClip agora vai lidar com 'existingBuffer'
- loadAudioForClip(newClip).then(() => {
- renderAudioEditor();
- });
+ startTimeInSeconds: startTime,
+ offset: 0,
+ durationInSeconds: 0,
+ originalDuration: 0,
+
+ pitch: 0,
+ volume: DEFAULT_VOLUME,
+ pan: DEFAULT_PAN,
+
+ buffer: existingBuffer || null,
+ player: null,
+
+ // --- INÍCIO DA CORREÇÃO (Passo 3) ---
+ // A "partitura" é finalmente armazenada no objeto do clipe!
+ patternData: incomingPatternData || null,
+ // --- FIM DA CORREÇÃO ---
+ };
+
+ // volume linear (0–1)
+ newClip.gainNode = new Tone.Gain(DEFAULT_VOLUME);
+ newClip.pannerNode = new Tone.Panner(DEFAULT_PAN);
+
+ // --- INÍCIO DA CORREÇÃO (O BUG ESTÁ AQUI) ---
+ // Precisamos de ligar os nós do novo clipe à saída principal,
+ // caso contrário, ele será criado "mudo".
+ newClip.gainNode.connect(newClip.pannerNode);
+ newClip.pannerNode.connect(getMainGainNode());
+ // --- FIM DA CORREÇÃO ---
+
+ audioState.clips.push(newClip);
+
+ // loadAudioForClip agora vai lidar com 'existingBuffer'
+ loadAudioForClip(newClip).then(() => {
+ renderAudioEditor();
+ });
}
export function updateAudioClipProperties(clipId, properties) {
- const clip = audioState.clips.find(c => String(c.id) == String(clipId));
- if (clip) {
- Object.assign(clip, properties);
- }
+ const clip = audioState.clips.find((c) => String(c.id) == String(clipId));
+ if (clip) {
+ Object.assign(clip, properties);
+ }
}
export function sliceAudioClip(clipId, sliceTimeInTimeline) {
- const originalClip = audioState.clips.find(c => String(c.id) == String(clipId));
-
- if (!originalClip ||
- sliceTimeInTimeline <= originalClip.startTimeInSeconds ||
- sliceTimeInTimeline >= (originalClip.startTimeInSeconds + originalClip.durationInSeconds)) {
- console.warn("Corte inválido: fora dos limites do clipe.");
- return;
- }
+ const originalClip = audioState.clips.find(
+ (c) => String(c.id) == String(clipId)
+ );
- const originalOffset = originalClip.offset || 0;
- const cutPointInClip = sliceTimeInTimeline - originalClip.startTimeInSeconds;
+ if (
+ !originalClip ||
+ sliceTimeInTimeline <= originalClip.startTimeInSeconds ||
+ sliceTimeInTimeline >=
+ originalClip.startTimeInSeconds + originalClip.durationInSeconds
+ ) {
+ console.warn("Corte inválido: fora dos limites do clipe.");
+ return;
+ }
- const newClip = {
- id: genClipId(),
- trackId: originalClip.trackId,
- sourcePath: originalClip.sourcePath,
- name: originalClip.name,
- buffer: originalClip.buffer,
-
- startTimeInSeconds: sliceTimeInTimeline,
- offset: originalOffset + cutPointInClip,
- durationInSeconds: originalClip.durationInSeconds - cutPointInClip,
-
- // --- CORREÇÃO: Propaga a duração original ---
- originalDuration: originalClip.originalDuration,
+ const originalOffset = originalClip.offset || 0;
+ const cutPointInClip = sliceTimeInTimeline - originalClip.startTimeInSeconds;
- pitch: originalClip.pitch,
- volume: originalClip.volume,
- pan: originalClip.pan,
+ const newClip = {
+ id: genClipId(),
+ trackId: originalClip.trackId,
+ sourcePath: originalClip.sourcePath,
+ name: originalClip.name,
+ buffer: originalClip.buffer,
- gainNode: new Tone.Gain(originalClip.volume),
- pannerNode: new Tone.Panner(originalClip.pan),
-
- player: null
- };
+ startTimeInSeconds: sliceTimeInTimeline,
+ offset: originalOffset + cutPointInClip,
+ durationInSeconds: originalClip.durationInSeconds - cutPointInClip,
- newClip.gainNode.connect(newClip.pannerNode);
- newClip.pannerNode.connect(getMainGainNode());
+ // --- CORREÇÃO: Propaga a duração original ---
+ originalDuration: originalClip.originalDuration,
- originalClip.durationInSeconds = cutPointInClip;
+ pitch: originalClip.pitch,
+ volume: originalClip.volume,
+ pan: originalClip.pan,
- audioState.clips.push(newClip);
-
- console.log("Clipe dividido. Original:", originalClip, "Novo:", newClip);
+ gainNode: new Tone.Gain(originalClip.volume),
+ pannerNode: new Tone.Panner(originalClip.pan),
+
+ player: null,
+
+ // --- NOVA MODIFICAÇÃO (SLICE) ---
+ // Se o clip original tinha dados de pattern, o novo clip (parte 2)
+ // também deve tê-los, pois a referência é a mesma.
+ patternData: originalClip.patternData || null,
+ };
+
+ newClip.gainNode.connect(newClip.pannerNode);
+ newClip.pannerNode.connect(getMainGainNode());
+
+ originalClip.durationInSeconds = cutPointInClip;
+
+ audioState.clips.push(newClip);
+
+ console.log("Clipe dividido. Original:", originalClip, "Novo:", newClip);
}
export function updateClipVolume(clipId, volume) {
@@ -270,7 +353,7 @@ export function updateClipVolume(clipId, volume) {
const clampedVolume = Math.max(0, Math.min(1.5, volume));
clip.volume = clampedVolume;
if (clip.gainNode) {
- clip.gainNode.gain.value = clampedVolume;
+ clip.gainNode.gain.value = clampedVolume;
}
}
}
@@ -281,35 +364,45 @@ export function updateClipPan(clipId, pan) {
const clampedPan = Math.max(-1, Math.min(1, pan));
clip.pan = clampedPan;
if (clip.pannerNode) {
- clip.pannerNode.pan.value = clampedPan;
+ clip.pannerNode.pan.value = clampedPan;
}
}
}
export function addAudioTrackLane() {
- const newTrackName = `Pista de Áudio ${audioState.tracks.length + 1}`;
- audioState.tracks.push({ id: Date.now(), name: newTrackName });
+ const newTrackName = `Pista de Áudio ${audioState.tracks.length + 1}`;
+ audioState.tracks.push({ id: Date.now(), name: newTrackName });
}
export function removeAudioClip(clipId) {
- const clipIndex = audioState.clips.findIndex(c => String(c.id) == String(clipId));
- if (clipIndex === -1) return false; // Retorna false se não encontrou
+ const clipIndex = audioState.clips.findIndex(
+ (c) => String(c.id) == String(clipId)
+ );
+ if (clipIndex === -1) return false; // Retorna false se não encontrou
- const clip = audioState.clips[clipIndex];
+ const clip = audioState.clips[clipIndex];
- // 1. Limpa os nós de áudio do Tone.js
- if (clip.gainNode) {
- try { clip.gainNode.disconnect(); } catch {}
- try { clip.gainNode.dispose(); } catch {}
- }
- if (clip.pannerNode) {
- try { clip.pannerNode.disconnect(); } catch {}
- try { clip.pannerNode.dispose(); } catch {}
- }
-
- // 2. Remove o clipe do array de estado
- audioState.clips.splice(clipIndex, 1);
+ // 1. Limpa os nós de áudio do Tone.js
+ if (clip.gainNode) {
+ try {
+ clip.gainNode.disconnect();
+ } catch {}
+ try {
+ clip.gainNode.dispose();
+ } catch {}
+ }
+ if (clip.pannerNode) {
+ try {
+ clip.pannerNode.disconnect();
+ } catch {}
+ try {
+ clip.pannerNode.dispose();
+ } catch {}
+ }
- // 3. Retorna true para o chamador (Controller)
- return true;
+ // 2. Remove o clipe do array de estado
+ audioState.clips.splice(clipIndex, 1);
+
+ // 3. Retorna true para o chamador (Controller)
+ return true;
}
diff --git a/assets/js/creations/audio/audio_ui.js b/assets/js/creations/audio/audio_ui.js
index 71b50b74..6cbfe933 100644
--- a/assets/js/creations/audio/audio_ui.js
+++ b/assets/js/creations/audio/audio_ui.js
@@ -320,7 +320,7 @@ export function renderAudioEditor() {
grid.style.setProperty("--bar-width", `${barWidthPx}px`);
});
- // Render Clips (sem alterações)
+ // Render Clips (MODIFICADO)
appState.audio.clips.forEach((clip) => {
const parentGrid = newTrackContainer.querySelector(
`.audio-track-lane[data-track-id="${clip.trackId}"] .spectrogram-view-grid`
@@ -342,8 +342,44 @@ export function renderAudioEditor() {
let pitchStr =
clip.pitch > 0 ? `+${clip.pitch.toFixed(1)}` : `${clip.pitch.toFixed(1)}`;
if (clip.pitch === 0) pitchStr = "";
+
+ // Define o HTML base (sem a visualização de steps ainda)
clipElement.innerHTML = `
${clip.name} ${pitchStr} `;
+
+ // --- INÍCIO DA MODIFICAÇÃO (Passo 4: Desenhar Steps) ---
+ // (Fazemos isso DEPOIS de definir o innerHTML)
+
+ // 1. Verifica se este clipe tem os dados da "partitura" (steps)
+ if (
+ clip.patternData &&
+ Array.isArray(clip.patternData) &&
+ clip.patternData.length > 0
+ ) {
+ // 2. Adiciona a classe CSS principal (do creation.html)
+ clipElement.classList.add("pattern-clip");
+
+ // 3. Determina o número de steps (do primeiro array de trilha)
+ // Assumimos que todos têm o mesmo comprimento, pois vieram do mesmo pattern.
+ const totalSteps = clip.patternData[0]?.length || 0;
+
+ if (totalSteps > 0) {
+ // 4. Chama a nova função (adicionada no final deste arquivo)
+ // para construir o HTML da visualização
+ const patternViewEl = createPatternViewElement(
+ clip.patternData,
+ totalSteps
+ );
+
+ // 5. Adiciona a visualização ao clipe
+ // (O CSS .pattern-clip-view o posicionará sobre o canvas)
+ clipElement.appendChild(patternViewEl);
+ }
+ }
+ // --- FIM DA MODIFICAÇÃO ---
+
parentGrid.appendChild(clipElement);
+
+ // Renderização do Canvas (Waveform)
if (clip.buffer) {
const canvas = clipElement.querySelector(".waveform-canvas-clip");
const canvasWidth = (clip.durationInSeconds || 0) * pixelsPerSecond;
@@ -365,6 +401,8 @@ export function renderAudioEditor() {
);
}
}
+
+ // Wheel listener (pitch)
clipElement.addEventListener("wheel", (e) => {
e.preventDefault();
const clipToUpdate = appState.audio.clips.find(
@@ -817,3 +855,57 @@ export function resetPlayheadVisual() {
ph.style.left = "0px";
});
}
+
+// --- INÍCIO DA NOVA FUNÇÃO (Passo 4: A Função de Desenho) ---
+// (Adicionada ao final de audio_ui.js)
+
+/**
+ * Cria o elemento HTML (e seus filhos) para a visualização
+ * dos steps de um pattern clip.
+ * * @param {Array>} patternData - ex: [[true, false], [true, true]]
+ * @returns {HTMLElement} Um
com a classe 'pattern-clip-view'
+ */
+function createPatternViewElement(patternData) {
+ const view = document.createElement("div");
+ view.className = "pattern-clip-view"; // (do creation.html)
+
+ // Filtra trilhas que possam ser vazias ou inválidas no array
+ const validTracksData = patternData.filter(
+ (steps) => Array.isArray(steps) && steps.length > 0
+ );
+
+ // Encontra o total de steps (usando a trilha mais longa como referência)
+ const totalSteps = validTracksData.reduce(
+ (max, steps) => Math.max(max, steps.length),
+ 0
+ );
+ if (totalSteps === 0) return view; // Retorna view vazia se não houver steps
+
+ validTracksData.forEach((trackSteps) => {
+ const row = document.createElement("div");
+ row.className = "pattern-clip-track-row"; // (do creation.html)
+
+ // Calcula a largura de cada step como porcentagem
+ const stepWidthPercent = (1 / totalSteps) * 100;
+
+ for (let i = 0; i < totalSteps; i++) {
+ // Se o step[i] for true, desenha a nota
+ if (trackSteps[i] === true) {
+ const note = document.createElement("div");
+ note.className = "pattern-step-note"; // (do creation.html)
+
+ // Define a posição (left) e a largura (width) em porcentagem
+ // Isso permite que o clip seja redimensionado (stretch)
+ // e as notas se ajustem.
+ note.style.left = `${(i / totalSteps) * 100}%`;
+ note.style.width = `${stepWidthPercent}%`;
+
+ row.appendChild(note);
+ }
+ }
+ view.appendChild(row);
+ });
+
+ return view;
+}
+// --- FIM DA NOVA FUNÇÃO ---
diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js
index 1c2ded85..75739adf 100644
--- a/assets/js/creations/file.js
+++ b/assets/js/creations/file.js
@@ -1,14 +1,65 @@
// js/file.js
-import { appState, resetProjectState } from "./state.js";
+import { appState, saveStateToSession, resetProjectState } from "./state.js";
import { loadAudioForTrack } from "./pattern/pattern_state.js";
import { renderAll, getSamplePathMap } from "./ui.js";
import { DEFAULT_PAN, DEFAULT_VOLUME, NOTE_LENGTH } from "./config.js";
-import { initializeAudioContext, getAudioContext, getMainGainNode } from "./audio.js";
+import {
+ initializeAudioContext,
+ getAudioContext,
+ getMainGainNode,
+} from "./audio.js";
import * as Tone from "https://esm.sh/tone";
// --- NOVA IMPORTAÇÃO ---
import { sendAction } from "./socket.js";
+// --- NOVA ADIÇÃO ---
+// Conteúdo do 'teste.mmp' (projeto em branco)
+const BLANK_PROJECT_XML = `
+
+
+
+
+
+
+
+
+
+`;
+
+/**
+ * Executa um reset completo do estado local do projeto.
+ * Limpa o backup da sessão, reseta o appState e renderiza a UI.
+ */
+export function handleLocalProjectReset() {
+ console.log("Recebido comando de reset. Limpando estado local...");
+
+ // 1. Limpa o backup da sessão
+ if (window.ROOM_NAME) {
+ try {
+ sessionStorage.removeItem(`temp_state_${window.ROOM_NAME}`);
+ console.log("Estado da sessão local limpo.");
+ } catch (e) {
+ console.error("Falha ao limpar estado da sessão:", e);
+ }
+ }
+
+ // 2. Reseta o estado da memória (appState)
+ // (Isso deve zerar o appState.pattern.tracks, etc)
+ resetProjectState();
+
+ // 3. Reseta a UI global para os padrões
+ document.getElementById("bpm-input").value = 140;
+ document.getElementById("bars-input").value = 1;
+ document.getElementById("compasso-a-input").value = 4;
+ document.getElementById("compasso-b-input").value = 4;
+
+ // 4. Renderiza a UI vazia
+ renderAll(); // Isso deve redesenhar o editor de patterns vazio
+
+ console.log("Reset local concluído.");
+}
+
export async function handleFileLoad(file) {
let xmlContent = "";
try {
@@ -19,17 +70,18 @@ export async function handleFileLoad(file) {
name.toLowerCase().endsWith(".mmp")
);
if (!projectFile)
- throw new Error("Não foi possível encontrar um arquivo .mmp dentro do .mmpz");
+ throw new Error(
+ "Não foi possível encontrar um arquivo .mmp dentro do .mmpz"
+ );
xmlContent = await zip.files[projectFile].async("string");
} else {
xmlContent = await file.text();
}
-
+
// ANTES: await parseMmpContent(xmlContent);
// DEPOIS:
// Envia o XML para o servidor, que o transmitirá para todos (incluindo nós)
- sendAction({ type: 'LOAD_PROJECT', xml: xmlContent });
-
+ sendAction({ type: "LOAD_PROJECT", xml: xmlContent });
} catch (error) {
console.error("Erro ao carregar o projeto:", error);
alert(`Erro ao carregar projeto: ${error.message}`);
@@ -41,21 +93,20 @@ export async function loadProjectFromServer(fileName) {
const response = await fetch(`mmp/${fileName}`);
if (!response.ok)
throw new Error(`Não foi possível carregar o arquivo ${fileName}`);
-
+
const xmlContent = await response.text();
- // ANTES:
+ // ANTES:
// await parseMmpContent(xmlContent);
// return true;
// DEPOIS:
// Envia o XML para o servidor
- sendAction({ type: 'LOAD_PROJECT', xml: xmlContent });
+ sendAction({ type: "LOAD_PROJECT", xml: xmlContent });
return true; // Retorna true para que o modal de UI feche
-
} catch (error) {
console.error("Erro ao carregar projeto do servidor:", error);
- console.error(error);
+ console.error(error);
alert(`Erro ao carregar projeto: ${error.message}`);
return false;
}
@@ -67,6 +118,13 @@ export async function loadProjectFromServer(fileName) {
export async function parseMmpContent(xmlString) {
resetProjectState();
initializeAudioContext();
+ appState.global.justReset = xmlString === BLANK_PROJECT_XML;
+ // Limpa manualmente a UI de áudio, pois resetProjectState()
+ // só limpa os *dados* (appState.audio.clips).
+ const audioContainer = document.getElementById("audio-track-container");
+ if (audioContainer) {
+ audioContainer.innerHTML = "";
+ }
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "application/xml");
@@ -76,125 +134,202 @@ export async function parseMmpContent(xmlString) {
const head = xmlDoc.querySelector("head");
if (head) {
- document.getElementById("bpm-input").value = head.getAttribute("bpm") || 140;
- document.getElementById("compasso-a-input").value = head.getAttribute("timesig_numerator") || 4;
- document.getElementById("compasso-b-input").value = head.getAttribute("timesig_denominator") || 4;
+ document.getElementById("bpm-input").value =
+ head.getAttribute("bpm") || 140;
+ document.getElementById("compasso-a-input").value =
+ head.getAttribute("timesig_numerator") || 4;
+ document.getElementById("compasso-b-input").value =
+ head.getAttribute("timesig_denominator") || 4;
}
- const allBBTrackNodes = Array.from(xmlDoc.querySelectorAll('song > trackcontainer[type="song"] > track[type="1"]'));
+ const allBBTrackNodes = Array.from(
+ xmlDoc.querySelectorAll(
+ 'song > trackcontainer[type="song"] > track[type="1"]'
+ )
+ );
if (allBBTrackNodes.length === 0) {
- appState.pattern.tracks = [];
- renderAll();
- return;
+ const allBBTrackNodes = Array.from(
+ xmlDoc.querySelectorAll(
+ 'song > trackcontainer[type="song"] > track[type="1"]'
+ )
+ );
+ if (allBBTrackNodes.length === 0) {
+ appState.pattern.tracks = [];
+
+ // --- INÍCIO DA CORREÇÃO ---
+ // O resetProjectState() [na linha 105] já limpou o appState.audio.
+ // No entanto, a UI (DOM) do editor de áudio não foi limpa.
+ // Vamos forçar a limpeza do container aqui:
+ const audioContainer = document.getElementById("audio-track-container");
+ if (audioContainer) {
+ audioContainer.innerHTML = ""; // Limpa a UI de áudio
+ }
+ // --- FIM DA CORREÇÃO ---
+
+ renderAll(); //
+ return; //
+ }
}
-
+
const sortedBBTrackNameNodes = [...allBBTrackNodes].sort((a, b) => {
- const bbtcoA = a.querySelector('bbtco');
- const bbtcoB = b.querySelector('bbtco');
- const posA = bbtcoA ? parseInt(bbtcoA.getAttribute('pos'), 10) : Infinity;
- const posB = bbtcoB ? parseInt(bbtcoB.getAttribute('pos'), 10) : Infinity;
+ const bbtcoA = a.querySelector("bbtco");
+ const bbtcoB = a.querySelector("bbtco");
+ const posA = bbtcoA ? parseInt(bbtcoA.getAttribute("pos"), 10) : Infinity;
+ const posB = bbtcoB ? parseInt(bbtcoB.getAttribute("pos"), 10) : Infinity;
return posA - posB;
});
-
- const dataSourceTrack = allBBTrackNodes[0];
- appState.global.currentBeatBasslineName = dataSourceTrack.getAttribute("name") || "Beat/Bassline";
- const bbTrackContainer = dataSourceTrack.querySelector('bbtrack > trackcontainer');
- if (!bbTrackContainer) {
- appState.pattern.tracks = [];
- renderAll();
+ // --- INÍCIO DA CORREÇÃO 1: Lendo TODAS as Basslines (Tracks type="1") ---
+ // O bug anterior era que o código só lia os instrumentos (tracks type="0")
+ // da PRIMEIRA bassline encontrada (allBBTrackNodes[0]).
+ // A correção abaixo itera em TODAS as basslines (allBBTrackNodes.forEach)
+ // e coleta os instrumentos de CADA UMA delas.
+
+ // Define um nome global (pode usar o da primeira track, se existir)
+ appState.global.currentBeatBasslineName =
+ allBBTrackNodes[0]?.getAttribute("name") || "Beat/Bassline";
+
+ // Cria um array para guardar TODOS os instrumentos de TODAS as basslines
+ const allInstrumentTrackNodes = [];
+
+ // Loop em CADA bassline (allBBTrackNodes) em vez de apenas na [0]
+ allBBTrackNodes.forEach((bbTrackNode) => {
+ const bbTrackContainer = bbTrackNode.querySelector(
+ "bbtrack > trackcontainer"
+ );
+ if (bbTrackContainer) {
+ // Encontra os instrumentos (type="0") DENTRO desta bassline
+ const instrumentTracks =
+ bbTrackContainer.querySelectorAll('track[type="0"]');
+ // Adiciona os instrumentos encontrados ao array principal
+ allInstrumentTrackNodes.push(...Array.from(instrumentTracks));
+ }
+ });
+
+ // Se não achou NENHUM instrumento em NENHUMA bassline, encerra
+ if (allInstrumentTrackNodes.length === 0) {
+ appState.pattern.tracks = [];
+ renderAll();
return;
}
+ // --- FIM DA CORREÇÃO 1 ---
- const instrumentTracks = bbTrackContainer.querySelectorAll('track[type="0"]');
const pathMap = getSamplePathMap();
-
- newTracks = Array.from(instrumentTracks).map(trackNode => {
- const instrumentNode = trackNode.querySelector("instrument");
- const instrumentTrackNode = trackNode.querySelector("instrumenttrack");
- if (!instrumentNode || !instrumentTrackNode) return null;
-
- const trackName = trackNode.getAttribute("name");
-
- if (instrumentNode.getAttribute("name") === 'tripleoscillator') {
- return null;
- }
- const allPatternsNodeList = trackNode.querySelectorAll("pattern");
- const allPatternsArray = Array.from(allPatternsNodeList).sort((a, b) => {
- const posA = parseInt(a.getAttribute('pos'), 10) || 0;
- const posB = parseInt(b.getAttribute('pos'), 10) || 0;
- return posB - posA;
- });
-
- const patterns = sortedBBTrackNameNodes.map((bbTrack, index) => {
+ // Agora o map usa o array corrigido (allInstrumentTrackNodes)
+ newTracks = Array.from(allInstrumentTrackNodes)
+ .map((trackNode) => {
+ const instrumentNode = trackNode.querySelector("instrument");
+ const instrumentTrackNode = trackNode.querySelector("instrumenttrack");
+ if (!instrumentNode || !instrumentTrackNode) return null;
+
+ const trackName = trackNode.getAttribute("name");
+
+ if (instrumentNode.getAttribute("name") === "tripleoscillator") {
+ return null;
+ }
+
+ const allPatternsNodeList = trackNode.querySelectorAll("pattern");
+ const allPatternsArray = Array.from(allPatternsNodeList).sort((a, b) => {
+ const posA = parseInt(a.getAttribute("pos"), 10) || 0;
+ const posB = parseInt(b.getAttribute("pos"), 10) || 0;
+
+ // --- CORREÇÃO 2: Ordenação dos Patterns ---
+ // O bug aqui era `posB - posA`, que invertia a ordem dos patterns
+ // (o "Pattern 1" recebia as notas do "Pattern 8", etc.)
+ // `posA - posB` garante a ordem correta (crescente: P1, P2, P3...).
+ return posA - posB;
+ });
+
+ const patterns = sortedBBTrackNameNodes.map((bbTrack, index) => {
const patternNode = allPatternsArray[index];
- const bbTrackName = bbTrack.getAttribute("name") || `Pattern ${index + 1}`;
+ const bbTrackName =
+ bbTrack.getAttribute("name") || `Pattern ${index + 1}`;
if (!patternNode) {
- const firstPattern = allPatternsArray[0];
- const stepsLength = firstPattern ? parseInt(firstPattern.getAttribute("steps"), 10) || 16 : 16;
- return { name: bbTrackName, steps: new Array(stepsLength).fill(false), pos: 0 };
+ const firstPattern = allPatternsArray[0];
+ const stepsLength = firstPattern
+ ? parseInt(firstPattern.getAttribute("steps"), 10) || 16
+ : 16;
+ return {
+ name: bbTrackName,
+ steps: new Array(stepsLength).fill(false),
+ pos: 0,
+ };
}
- const patternSteps = parseInt(patternNode.getAttribute("steps"), 10) || 16;
+ const patternSteps =
+ parseInt(patternNode.getAttribute("steps"), 10) || 16;
const steps = new Array(patternSteps).fill(false);
const ticksPerStep = 12;
patternNode.querySelectorAll("note").forEach((noteNode) => {
- const noteLocalPos = parseInt(noteNode.getAttribute("pos"), 10);
- const stepIndex = Math.round(noteLocalPos / ticksPerStep);
- if (stepIndex < patternSteps) {
- steps[stepIndex] = true;
- }
+ const noteLocalPos = parseInt(noteNode.getAttribute("pos"), 10);
+ const stepIndex = Math.round(noteLocalPos / ticksPerStep);
+ if (stepIndex < patternSteps) {
+ steps[stepIndex] = true;
+ }
});
return {
- name: bbTrackName,
- steps: steps,
- pos: parseInt(patternNode.getAttribute("pos"), 10) || 0
+ name: bbTrackName,
+ steps: steps,
+ pos: parseInt(patternNode.getAttribute("pos"), 10) || 0,
};
- });
+ });
- const hasNotes = patterns.some(p => p.steps.includes(true));
- if (!hasNotes) return null;
+ const hasNotes = patterns.some((p) => p.steps.includes(true));
+ if (!hasNotes) return null;
- const afpNode = instrumentNode.querySelector("audiofileprocessor");
- const sampleSrc = afpNode ? afpNode.getAttribute("src") : null;
- let finalSamplePath = null;
- if (sampleSrc) {
+ const afpNode = instrumentNode.querySelector("audiofileprocessor");
+ const sampleSrc = afpNode ? afpNode.getAttribute("src") : null;
+ let finalSamplePath = null;
+ if (sampleSrc) {
const filename = sampleSrc.split("/").pop();
if (pathMap[filename]) {
- finalSamplePath = pathMap[filename];
+ finalSamplePath = pathMap[filename];
} else {
- let cleanSrc = sampleSrc;
- if (cleanSrc.startsWith('samples/')) {
- cleanSrc = cleanSrc.substring('samples/'.length);
- }
- finalSamplePath = `src/samples/${cleanSrc}`;
+ let cleanSrc = sampleSrc;
+ if (cleanSrc.startsWith("samples/")) {
+ cleanSrc = cleanSrc.substring("samples/".length);
+ }
+ finalSamplePath = `src/samples/${cleanSrc}`;
}
- }
-
- const volFromFile = parseFloat(instrumentTrackNode.getAttribute("vol"));
- const panFromFile = parseFloat(instrumentTrackNode.getAttribute("pan"));
- const firstPatternWithNotesIndex = patterns.findIndex(p => p.steps.includes(true));
+ }
- return {
- id: Date.now() + Math.random(),
- name: trackName,
- samplePath: finalSamplePath,
- patterns: patterns,
- activePatternIndex: firstPatternWithNotesIndex !== -1 ? firstPatternWithNotesIndex : 0,
- volume: !isNaN(volFromFile) ? volFromFile / 100 : DEFAULT_VOLUME,
- pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN,
- instrumentName: instrumentNode.getAttribute("name"),
- instrumentXml: instrumentNode.innerHTML,
- };
- }).filter(track => track !== null);
+ const volFromFile = parseFloat(instrumentTrackNode.getAttribute("vol"));
+ const panFromFile = parseFloat(instrumentTrackNode.getAttribute("pan"));
+ const firstPatternWithNotesIndex = patterns.findIndex((p) =>
+ p.steps.includes(true)
+ );
+
+ return {
+ id: Date.now() + Math.random(),
+ name: trackName,
+ samplePath: finalSamplePath,
+ patterns: patterns,
+
+ // --- INÍCIO DA CORREÇÃO ---
+ // ANTES:
+ // activePatternIndex:
+ // firstPatternWithNotesIndex !== -1 ? firstPatternWithNotesIndex : 0, //
+
+ // DEPOIS (force o Padrão 1):
+ activePatternIndex: 0,
+ // --- FIM DA CORREÇÃO ---
+
+ volume: !isNaN(volFromFile) ? volFromFile / 100 : DEFAULT_VOLUME,
+ pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN,
+ instrumentName: instrumentNode.getAttribute("name"),
+ instrumentXml: instrumentNode.innerHTML,
+ };
+ })
+ .filter((track) => track !== null);
let isFirstTrackWithNotes = true;
- newTracks.forEach(track => {
+ newTracks.forEach((track) => {
// --- INÍCIO DA CORREÇÃO ---
+ // (Esta parte já existia no seu arquivo, mantida)
// Agora usando Volume em dB (Opção B)
track.volumeNode = new Tone.Volume(Tone.gainToDb(track.volume));
track.pannerNode = new Tone.Panner(track.pan);
@@ -211,14 +346,17 @@ export async function parseMmpContent(xmlString) {
const firstPatternSteps = activePattern.steps.length;
const stepsPerBar = 16;
const requiredBars = Math.ceil(firstPatternSteps / stepsPerBar);
- document.getElementById("bars-input").value = requiredBars > 0 ? requiredBars : 1;
+ document.getElementById("bars-input").value =
+ requiredBars > 0 ? requiredBars : 1;
isFirstTrackWithNotes = false;
}
}
});
try {
- const trackLoadPromises = newTracks.map(track => loadAudioForTrack(track));
+ const trackLoadPromises = newTracks.map((track) =>
+ loadAudioForTrack(track)
+ );
await Promise.all(trackLoadPromises);
} catch (error) {
console.error("Ocorreu um erro ao carregar os áudios do projeto:", error);
@@ -226,13 +364,87 @@ export async function parseMmpContent(xmlString) {
appState.pattern.tracks = newTracks;
appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id || null;
+ // --- INÍCIO DA CORREÇÃO ---
+ // Define o estado global para também ser o Padrão 1 (índice 0)
+ appState.pattern.activePatternIndex = 0;
+ // --- FIM DA CORREÇÃO ---
- // força atualização total da UI e dos editores de pattern
- await Promise.resolve(); // garante que os tracks estejam no estado
+ // --- A MÁGICA DO F5 (Versão 2.0 - Corrigida) ---
+ try {
+ const roomName = window.ROOM_NAME || "default_room";
+ const tempStateJSON = sessionStorage.getItem(`temp_state_${roomName}`);
+
+ if (tempStateJSON) {
+ console.log("Restaurando estado temporário da sessão (pós-F5)...");
+ const tempState = JSON.parse(tempStateJSON);
+
+ // NÃO FAÇA: appState.pattern = tempState.pattern; (Isso apaga os Tone.js nodes)
+
+ // EM VEZ DISSO, FAÇA O "MERGE" (MESCLAGEM):
+
+ // 1. Mescla os 'tracks'
+ // Itera nos tracks "vivos" (com nós de áudio) que acabamos de criar
+ appState.pattern.tracks.forEach((liveTrack) => {
+ // Encontra o track salvo correspondente
+ const savedTrack = tempState.pattern.tracks.find(
+ (t) => t.id === liveTrack.id
+ );
+
+ if (savedTrack) {
+ // Copia os dados do 'savedTrack' para o 'liveTrack'
+ liveTrack.name = savedTrack.name;
+ liveTrack.patterns = savedTrack.patterns;
+ liveTrack.activePatternIndex = savedTrack.activePatternIndex;
+ liveTrack.volume = savedTrack.volume;
+ liveTrack.pan = savedTrack.pan;
+
+ // ATUALIZA OS NÓS DO TONE.JS com os valores salvos!
+ if (liveTrack.volumeNode) {
+ liveTrack.volumeNode.volume.value = Tone.gainToDb(
+ savedTrack.volume
+ );
+ }
+ if (liveTrack.pannerNode) {
+ liveTrack.pannerNode.pan.value = savedTrack.pan;
+ }
+ }
+ });
+
+ // 2. Remove tracks "vivos" que não existem mais no estado salvo
+ // (Ex: se o usuário deletou um track antes de dar F5)
+ appState.pattern.tracks = appState.pattern.tracks.filter((liveTrack) =>
+ tempState.pattern.tracks.some((t) => t.id === liveTrack.id)
+ );
+
+ // 3. Restaura valores globais da UI
+ document.getElementById("bpm-input").value = tempState.global.bpm;
+ document.getElementById("compasso-a-input").value =
+ tempState.global.compassoA;
+ document.getElementById("compasso-b-input").value =
+ tempState.global.compassoB;
+ document.getElementById("bars-input").value = tempState.global.bars;
+
+ // 4. Restaura o ID do track ativo
+ appState.pattern.activeTrackId = tempState.pattern.activeTrackId;
+
+ console.log("Estado da sessão restaurado com sucesso.");
+ }
+ } catch (e) {
+ console.error(
+ "Erro ao restaurar estado da sessão (pode estar corrompido)",
+ e
+ );
+ if (window.ROOM_NAME) {
+ sessionStorage.removeItem(`temp_state_${window.ROOM_NAME}`);
+ }
+ }
+ // --- FIM DA MÁGICA (V2.0) ---
+
+ // Agora sim, renderiza com o estado CORRIGIDO E MESCLADO
+ await Promise.resolve();
renderAll();
- console.log('[UI] Projeto renderizado após parseMmpContent');
-
+ console.log("[UI] Projeto renderizado após parseMmpContent");
}
export function generateMmpFile() {
@@ -247,12 +459,14 @@ export function generateMmpFile() {
// Copiada de generateMmpFile/modifyAndSaveExistingMmp
function generateXmlFromState() {
if (!appState.global.originalXmlDoc) {
- // Se não houver XML original, precisamos gerar um novo
- // Por simplicidade, para este fix, vamos retornar o estado atual do LMMS
- // mas o ideal seria gerar o XML completo (como generateNewMmp)
- console.warn("Não há XML original para modificar. Usando a base atual do appState.");
- // No seu caso, use o conteúdo de generateNewMmp()
- return "";
+ // Se não houver XML original, precisamos gerar um novo
+ // Por simplicidade, para este fix, vamos retornar o estado atual do LMMS
+ // mas o ideal seria gerar o XML completo (como generateNewMmp)
+ console.warn(
+ "Não há XML original para modificar. Usando a base atual do appState."
+ );
+ // No seu caso, use o conteúdo de generateNewMmp()
+ return "";
}
const xmlDoc = appState.global.originalXmlDoc.cloneNode(true);
@@ -260,14 +474,29 @@ function generateXmlFromState() {
if (head) {
head.setAttribute("bpm", document.getElementById("bpm-input").value);
head.setAttribute("num_bars", document.getElementById("bars-input").value);
- head.setAttribute("timesig_numerator", document.getElementById("compasso-a-input").value);
- head.setAttribute("timesig_denominator", document.getElementById("compasso-b-input").value);
+ head.setAttribute(
+ "timesig_numerator",
+ document.getElementById("compasso-a-input").value
+ );
+ head.setAttribute(
+ "timesig_denominator",
+ document.getElementById("compasso-b-input").value
+ );
}
- const bbTrackContainer = xmlDoc.querySelector('track[type="1"] > bbtrack > trackcontainer');
+ const bbTrackContainer = xmlDoc.querySelector(
+ 'track[type="1"] > bbtrack > trackcontainer'
+ );
if (bbTrackContainer) {
- bbTrackContainer.querySelectorAll('track[type="0"]').forEach(node => node.remove());
- const tracksXml = appState.pattern.tracks.map((track) => createTrackXml(track)).join("");
- const tempDoc = new DOMParser().parseFromString(`${tracksXml}`, "application/xml");
+ bbTrackContainer
+ .querySelectorAll('track[type="0"]')
+ .forEach((node) => node.remove());
+ const tracksXml = appState.pattern.tracks
+ .map((track) => createTrackXml(track))
+ .join("");
+ const tempDoc = new DOMParser().parseFromString(
+ `${tracksXml}`,
+ "application/xml"
+ );
Array.from(tempDoc.documentElement.children).forEach((newTrackNode) => {
bbTrackContainer.appendChild(newTrackNode);
});
@@ -282,34 +511,39 @@ function generateXmlFromState() {
* Deve ser chamado APÓS alterações significativas no padrão (steps, tracks).
*/
export function syncPatternStateToServer() {
- if (!window.ROOM_NAME) return; // Não faz nada se não estiver em sala
+ if (!window.ROOM_NAME) return;
+ const currentXml = generateXmlFromState();
- const currentXml = generateXmlFromState();
-
- // NOTA: Usamos um novo tipo de ação para não confundir com o carregamento de arquivo
- sendAction({
- type: 'SYNC_PATTERN_STATE',
- xml: currentXml
- });
+ sendAction({
+ type: "SYNC_PATTERN_STATE",
+ xml: currentXml,
+ });
+
+ // Salva o estado localmente também!
+ saveStateToSession(); // <-- ADICIONE ISSO
}
function createTrackXml(track) {
if (track.patterns.length === 0) return "";
- const ticksPerStep = 12;
+ const ticksPerStep = 12;
const lmmsVolume = Math.round(track.volume * 100);
const lmmsPan = Math.round(track.pan * 100);
- const patternsXml = track.patterns.map(pattern => {
- const patternNotes = pattern.steps.map((isActive, index) => {
- if (isActive) {
+ const patternsXml = track.patterns
+ .map((pattern) => {
+ const patternNotes = pattern.steps
+ .map((isActive, index) => {
+ if (isActive) {
const notePos = Math.round(index * ticksPerStep);
return ``;
- }
- return "";
- }).join("\n ");
- return `
+ }
+ return "";
+ })
+ .join("\n ");
+ return `
${patternNotes}
`;
- }).join('\n ');
+ })
+ .join("\n ");
return `