From c384a4a92fa70a9d39879a63972d46fed876ae5a Mon Sep 17 00:00:00 2001 From: JotaChina Date: Tue, 23 Dec 2025 22:22:59 -0300 Subject: [PATCH] melhorando a leitura de projetos no mmpCreator --- assets/js/creations/file.js | 148 +++++++++++++++++++++++++++++++++- assets/js/creations/socket.js | 23 ++++++ 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index 73fa06f2..42c97758 100755 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -13,10 +13,142 @@ 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, getMainGainNode } from "./audio.js"; -import { DEFAULT_PROJECT_XML } from "./utils.js"; +import { DEFAULT_PROJECT_XML, getSecondsPerStep } from "./utils.js"; import * as Tone from "https://esm.sh/tone"; import { sendAction } from "./socket.js"; +// ⚠️ vem do módulo de áudio (o mesmo que audio_ui usa) +import { + addAudioTrackLane, + addAudioClipToTimeline, + updateAudioClipProperties, +} from "./audio/audio_state.js"; + +const TICKS_PER_STEP = 12; + +function safeId(prefix) { + return (crypto?.randomUUID?.() || `${prefix}_${Date.now()}_${Math.floor(Math.random() * 1e6)}`); +} + +function basename(path) { + return String(path || "").split(/[\\/]/).pop(); +} + +function resolveSamplePath(sampleName, pathMap) { + // 1) tenta pelo manifest (melhor) + if (sampleName && pathMap[sampleName]) return pathMap[sampleName]; + + // 2) fallback simples (se você tiver essa convenção) + // ajuste se necessário + if (sampleName) return `src/samples/${sampleName}`; + + return null; +} + +export async function parseBeatIndexJson(data) { + resetProjectState(); + initializeAudioContext(); + + // BPM + const bpm = Number(data?.bpm || 140); + const bpmInput = document.getElementById("bpm-input"); + if (bpmInput) bpmInput.value = bpm; + + // (opcional) nome do projeto + if (data?.original_title) { + appState.global.currentBeatBasslineName = data.original_title; + } + + const pathMap = getSamplePathMap(); // vem do samples-manifest :contentReference[oaicite:6]{index=6} + const secondsPerStep = getSecondsPerStep(); + + const newPatternTracks = []; + + // 1) monta pattern.tracks (plugin/bassline etc) + (data.tracks || []).forEach((t, idx) => { + if (t.type === "sample") return; + + const id = t.id || safeId("ptrk"); + + // normaliza nome (teu JSON usa track_name / bassline_name) + const name = + t.track_name || + t.bassline_name || + t.instrument_name || + t.instrumentName || + `Track ${idx + 1}`; + + newPatternTracks.push({ + ...t, + id, + name, + }); + }); + + appState.pattern.tracks = newPatternTracks; + + // 2) cria lanes/clips de áudio a partir dos sample-tracks + const sampleTracks = (data.tracks || []).filter((t) => t.type === "sample"); + + for (const st of sampleTracks) { + const laneId = st.id || safeId("audioTrack"); + const laneName = st.track_name || "Áudio"; + + // lane (pista) + addAudioTrackLane({ + id: laneId, + name: laneName, + volume: Number(st.sample_info?.vol ?? 100) / 100, + pan: Number(st.sample_info?.pan ?? 0) / 100, + }); + + // arquivo + const sampleName = + st.sample_name || + basename(st.sample_info?.src) || // só pra extrair "#2.wav" + null; + + const filePath = resolveSamplePath(sampleName, pathMap); + if (!filePath) continue; + + // seus dados podem vir num único sample_info, OU você pode evoluir pra playlist_clips também + const clipDefs = + Array.isArray(st.playlist_clips) && st.playlist_clips.length + ? st.playlist_clips + : [ + { + pos: Number(st.sample_info?.pos ?? 0), + len: Number(st.sample_info?.len ?? 0), + name: sampleName, + }, + ]; + + for (const c of clipDefs) { + const startTimeInSeconds = (Number(c.pos || 0) / TICKS_PER_STEP) * secondsPerStep; + const durationInSeconds = (Number(c.len || 0) / TICKS_PER_STEP) * secondsPerStep; + + const clipId = safeId("clip"); + + // isso decodifica e já deixa pronto pra desenhar waveform (clip.buffer) :contentReference[oaicite:7]{index=7} + await addAudioClipToTimeline(filePath, laneId, startTimeInSeconds, clipId, c.name || sampleName); + + // garante que o “tamanho na playlist” respeita seu len do beat-index + if (durationInSeconds > 0) { + updateAudioClipProperties(clipId, { durationInSeconds, offset: 0, pitch: 0 }); + } + } + } + + // escolha de track ativa no pattern editor + const firstInst = newPatternTracks.find((t) => t.type !== "bassline"); + appState.pattern.activeTrackId = firstInst ? firstInst.id : null; + appState.pattern.activePatternIndex = 0; + + // mantém seu comportamento atual + await loadStateFromSession(); // se existir snapshot local, aplica + renderAll(); +} + //-------------------------------------------------------------- // MANIPULAÇÃO DE ARQUIVOS //-------------------------------------------------------------- @@ -48,6 +180,13 @@ export function handleLocalProjectReset() { export async function handleFileLoad(file) { let xmlContent = ""; try { + const lower = file.name.toLowerCase(); + + if (lower.endsWith(".json")) { + const json = JSON.parse(await file.text()); + sendAction({ type: "LOAD_BEAT_INDEX", data: json }); + return; + } if (file.name.toLowerCase().endsWith(".mmpz")) { // eslint-disable-next-line no-undef const jszip = new JSZip(); @@ -71,6 +210,13 @@ export async function handleFileLoad(file) { } } +export async function loadBeatIndexFromServer(fileName) { + const response = await fetch(`src_mmpSearch/index/${fileName}.json`); + if (!response.ok) throw new Error("Não foi possível carregar beat index"); + const data = await response.json(); + sendAction({ type: "LOAD_BEAT_INDEX", data }); +} + export async function loadProjectFromServer(fileName) { try { const response = await fetch(`src_mmpSearch/mmp/${fileName}`); diff --git a/assets/js/creations/socket.js b/assets/js/creations/socket.js index ee0250ca..57bdfaf5 100755 --- a/assets/js/creations/socket.js +++ b/assets/js/creations/socket.js @@ -36,6 +36,7 @@ import { import { parseMmpContent, + parseBeatIndexJson, handleLocalProjectReset, syncPatternStateToServer, generateXmlFromStateExported, @@ -1086,6 +1087,28 @@ async function handleActionBroadcast(action) { break; } + case "LOAD_BEAT_INDEX": { + isLoadingProject = true; + showToast("📂 Carregando beat index...", "info"); + + if (window.ROOM_NAME) { + sessionStorage.removeItem(`temp_state_${window.ROOM_NAME}`); + } + + try { + await parseBeatIndexJson(action.data); + renderAll(); + showToast("🎵 Beat index carregado", "success"); + saveStateToSession(); + } catch (e) { + console.error("Erro LOAD_BEAT_INDEX:", e); + showToast("❌ Erro ao carregar beat index", "error"); + } + + isLoadingProject = false; + break; + } + case "UPDATE_AUDIO_CLIP": { try { if (action.props?.__operation === "slice") {