melhorando a leitura de projetos no mmpCreator
Deploy / Deploy (push) Successful in 2m8s Details

This commit is contained in:
JotaChina 2025-12-23 22:22:59 -03:00
parent 79f716e8a6
commit c384a4a92f
2 changed files with 170 additions and 1 deletions

View File

@ -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}`);

View File

@ -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") {