Padronização da interface e atualização do sync entre clientes
Deploy / Deploy (push) Successful in 2m1s
Details
Deploy / Deploy (push) Successful in 2m1s
Details
This commit is contained in:
parent
dfe558be1c
commit
0d0cd2d7db
|
|
@ -8,3 +8,4 @@ mmp
|
|||
venv
|
||||
.bundle
|
||||
src
|
||||
assets/js/creations/server/data
|
||||
|
|
@ -142,7 +142,39 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none
|
|||
color: var(--text-light); width: 25px; height: 40px; cursor: pointer;
|
||||
border-radius: 0 4px 4px 0; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
/* Botão Padrão Unificado */
|
||||
.control-btn {
|
||||
height: 32px;
|
||||
min-width: 32px; /* Garante que botões quadrados fiquem quadrados */
|
||||
padding: 0 10px; /* Padding lateral para botões com texto */
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background-color: #464646; /* Cor base mais neutra */
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background-color: #5a5a5a;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Botão Ativo (Toggle) */
|
||||
.control-btn.active {
|
||||
background-color: var(--accent-green);
|
||||
color: #fff;
|
||||
border-color: var(--accent-green);
|
||||
box-shadow: 0 0 8px rgba(46, 204, 113, 0.4);
|
||||
}
|
||||
/* =============================================== */
|
||||
/* ÁREA DE CONTEÚDO E FERRAMENTAS
|
||||
/* =============================================== */
|
||||
|
|
@ -161,8 +193,35 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none
|
|||
.info-display { background-color: #1a1c1e; padding: 5px 8px; border-radius: 3px; text-align: center; }
|
||||
.info-display .label { color: var(--text-dark); font-size: .6rem; text-transform: uppercase; }
|
||||
.value-input { background: 0 0; border: 0; outline: 0; color: var(--accent-green); font-weight: 700; font-size: 1.4rem; font-family: Courier New, Courier, monospace; text-align: center; padding: 0; width: 55px; }
|
||||
.compasso-input { width: 25px; }
|
||||
.compasso-separator { color: var(--accent-green); font-weight: 700; font-size: 1.4rem; font-family: Courier New, Courier, monospace; margin: 0 2px; }
|
||||
/* --- CORREÇÃO DO COMPASSO --- */
|
||||
|
||||
/* Força o container principal a alinhar tudo em uma linha só */
|
||||
.interactive-input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Força cada grupo (botão - input - botão) a ficar em linha */
|
||||
.compasso-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Ajuste opcional para a barra separadora ficar bonita */
|
||||
.compasso-separator {
|
||||
margin: 0 5px;
|
||||
color: var(--accent-green); /* Garante que fique verde como na imagem */
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Garante que os inputs do compasso não fiquem largos demais */
|
||||
.compasso-input {
|
||||
width: 25px !important; /* Força a largura pequena */
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
}
|
||||
.value-input::-webkit-outer-spin-button, .value-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
||||
.value-input[type=number] { -moz-appearance: textfield; }
|
||||
.adjust-btn { background: 0 0; border: 0; color: var(--text-dark); font-size: 1rem; font-weight: 700; cursor: pointer; padding: 0 5px; transition: color .2s; line-height: 1; }
|
||||
|
|
@ -191,7 +250,40 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none
|
|||
border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, .3); overflow: hidden;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.editor-header { background-color: var(--bg-toolbar); padding: 4px 10px; font-size: .8rem; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); flex-shrink: 0; }
|
||||
/* Container do Header do Editor */
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between; /* Espalha grupos para esquerda e direita */
|
||||
align-items: center;
|
||||
height: 50px; /* Altura fixa para ambos */
|
||||
background-color: var(--panel-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 0 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* Inputs dentro da toolbar (Selects, Numbers) */
|
||||
.editor-header select,
|
||||
.editor-header input[type="number"] {
|
||||
height: 32px;
|
||||
background-color: #222;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-light);
|
||||
border-radius: 4px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
/* Grupos de ferramentas (ex: Transporte, Zoom, Edição) */
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px; /* Espaço uniforme entre botões */
|
||||
}
|
||||
/* Divisor Vertical Visual (opcional, para separar grupos) */
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background-color: var(--border-color);
|
||||
margin: 0 8px;
|
||||
}
|
||||
.editor-toolbar { background-color: var(--bg-toolbar); padding: 5px 10px; border-bottom: 2px solid var(--border-color); flex-shrink: 0; display: flex; align-items: center; gap: 15px; }
|
||||
|
||||
#track-container { overflow-y: auto; overflow-x: hidden; flex-grow: 1; }
|
||||
|
|
@ -224,6 +316,15 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none
|
|||
border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, .3); overflow: hidden;
|
||||
display: flex; flex-direction: column; --track-info-width: 255px;
|
||||
}
|
||||
/* CORREÇÃO CSS: Adicionado para garantir layout correto */
|
||||
.audio-tracks-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#audio-track-container { flex-grow: 1; overflow: auto; }
|
||||
.audio-track-lane { display: flex; flex-direction: row; align-items: stretch; background-color: var(--bg-editor); border-bottom: 1px solid var(--bg-toolbar); min-height: 90px; box-sizing: border-box; }
|
||||
.audio-track-lane.drag-over { background-color: #40454d; }
|
||||
|
|
|
|||
|
|
@ -23,23 +23,43 @@ import { sendAction } from "../socket.js";
|
|||
|
||||
export function renderAudioEditor() {
|
||||
const audioEditor = document.querySelector(".audio-editor");
|
||||
const existingTrackContainer = document.getElementById(
|
||||
"audio-track-container"
|
||||
);
|
||||
const existingTrackContainer = document.getElementById("audio-track-container");
|
||||
|
||||
if (!audioEditor || !existingTrackContainer) return;
|
||||
|
||||
// --- CORREÇÃO DO ERRO DOMException ---
|
||||
// Identificamos quem é o pai real do container de tracks (agora é .audio-tracks-wrapper)
|
||||
const tracksParent = existingTrackContainer.parentElement;
|
||||
|
||||
// --- CRIAÇÃO E RENDERIZAÇÃO DA RÉGUA ---
|
||||
let rulerWrapper = audioEditor.querySelector(".ruler-wrapper");
|
||||
// Buscamos a régua dentro desse pai correto
|
||||
let rulerWrapper = tracksParent.querySelector(".ruler-wrapper");
|
||||
|
||||
if (!rulerWrapper) {
|
||||
// Se a régua criada pelo JS ainda não existe, cria ela
|
||||
// (Ignora a régua estática do HTML se houver conflito, priorizando esta estrutura)
|
||||
rulerWrapper = document.createElement("div");
|
||||
rulerWrapper.className = "ruler-wrapper";
|
||||
rulerWrapper.innerHTML = `
|
||||
<div class="ruler-spacer"></div>
|
||||
<div class="timeline-ruler"></div>
|
||||
`;
|
||||
audioEditor.insertBefore(rulerWrapper, existingTrackContainer);
|
||||
|
||||
// Insere ANTES do container de tracks, dentro do pai correto
|
||||
tracksParent.insertBefore(rulerWrapper, existingTrackContainer);
|
||||
}
|
||||
|
||||
// Remove a régua estática antiga se ela ainda estiver lá atrapalhando (opcional, mas recomendado)
|
||||
const staticRuler = tracksParent.querySelector("#audio-timeline-ruler");
|
||||
if (staticRuler && staticRuler.parentElement === tracksParent) {
|
||||
staticRuler.remove();
|
||||
}
|
||||
// Remove elementos soltos antigos se existirem para evitar duplicação
|
||||
const oldLoopRegion = tracksParent.querySelector("#loop-region");
|
||||
const oldPlayhead = tracksParent.querySelector("#playhead");
|
||||
if(oldLoopRegion) oldLoopRegion.remove();
|
||||
if(oldPlayhead) oldPlayhead.remove();
|
||||
|
||||
const ruler = rulerWrapper.querySelector(".timeline-ruler");
|
||||
ruler.innerHTML = "";
|
||||
|
||||
|
|
@ -243,7 +263,9 @@ export function renderAudioEditor() {
|
|||
|
||||
// Recriação Container Pistas (sem alterações)
|
||||
const newTrackContainer = existingTrackContainer.cloneNode(false);
|
||||
audioEditor.replaceChild(newTrackContainer, existingTrackContainer);
|
||||
|
||||
// Substitui no pai correto (tracksParent), não no audioEditor
|
||||
tracksParent.replaceChild(newTrackContainer, existingTrackContainer);
|
||||
|
||||
// Render Pistas (sem alterações)
|
||||
appState.audio.tracks.forEach((trackData) => {
|
||||
|
|
@ -725,7 +747,6 @@ export function renderAudioEditor() {
|
|||
const constrainedLeftPx = Math.max(0, newLeftPx);
|
||||
let newStartTime = constrainedLeftPx / currentPixelsPerSecond;
|
||||
newStartTime = quantizeTime(newStartTime);
|
||||
// (Correção Bug 4 - remove Number())
|
||||
updateAudioClipProperties(clipId, {
|
||||
trackId: newTrackId,
|
||||
startTimeInSeconds: newStartTime,
|
||||
|
|
@ -756,13 +777,8 @@ export function renderAudioEditor() {
|
|||
const clickX = event.clientX - rect.left;
|
||||
const absoluteX = clickX + scrollLeft;
|
||||
const newTime = absoluteX / currentPixelsPerSecond;
|
||||
// =================================================================
|
||||
// 👇 INÍCIO DA CORREÇÃO (Sincronia de Seek na Pista)
|
||||
// =================================================================
|
||||
// Sincronia de Seek na Pista)
|
||||
sendAction({ type: "SET_SEEK_TIME", seekTime: newTime });
|
||||
// seekAudioEditor(newTime); // 👈 Substituído
|
||||
// =================================================================
|
||||
// 👆 FIM DA CORREÇÃO
|
||||
};
|
||||
handleSeek(e); // Aplica no mousedown
|
||||
const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent);
|
||||
|
|
@ -856,7 +872,7 @@ export function resetPlayheadVisual() {
|
|||
});
|
||||
}
|
||||
|
||||
// --- INÍCIO DA NOVA FUNÇÃO (Passo 4: A Função de Desenho) ---
|
||||
// --- A Função de Desenho) ---
|
||||
// (Adicionada ao final de audio_ui.js)
|
||||
|
||||
/**
|
||||
|
|
@ -907,5 +923,4 @@ function createPatternViewElement(patternData) {
|
|||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
// --- FIM DA NOVA FUNÇÃO ---
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
// js/file.js
|
||||
//--------------------------------------------------------------
|
||||
// IMPORTS NECESSÁRIOS
|
||||
//--------------------------------------------------------------
|
||||
import { appState, saveStateToSession, resetProjectState, loadStateFromSession } from "./state.js";
|
||||
import { loadAudioForTrack } from "./pattern/pattern_state.js";
|
||||
import { renderAll, getSamplePathMap } from "./ui.js";
|
||||
|
|
@ -7,21 +9,13 @@ import {
|
|||
initializeAudioContext,
|
||||
getMainGainNode,
|
||||
} from "./audio.js";
|
||||
import { DEFAULT_PROJECT_XML } from "./utils.js"
|
||||
import * as Tone from "https://esm.sh/tone";
|
||||
|
||||
import { sendAction } from "./socket.js";
|
||||
|
||||
const BLANK_PROJECT_XML = `<?xml version="1.0"?>
|
||||
<!DOCTYPE lmms-project>
|
||||
<lmms-project type="song" version="1.0" creatorversion="1.2.2" creator="LMMS">
|
||||
<head bpm="140" mastervol="100" timesig_numerator="4" timesig_denominator="4" masterpitch="0"/>
|
||||
<song>
|
||||
<trackcontainer width="600" height="300" type="song" visible="1" maximized="0" x="5" y="5" minimized="0">
|
||||
</trackcontainer>
|
||||
<timeline lp0pos="0" lp1pos="192" lpstate="0"/>
|
||||
<controllers/>
|
||||
</song>
|
||||
</lmms-project>`;
|
||||
//--------------------------------------------------------------
|
||||
//
|
||||
//--------------------------------------------------------------
|
||||
|
||||
export function handleLocalProjectReset() {
|
||||
console.log("Recebido comando de reset. Limpando estado local...");
|
||||
|
|
@ -89,7 +83,7 @@ export async function loadProjectFromServer(fileName) {
|
|||
export async function parseMmpContent(xmlString) {
|
||||
resetProjectState();
|
||||
initializeAudioContext();
|
||||
appState.global.justReset = xmlString === BLANK_PROJECT_XML;
|
||||
appState.global.justReset = xmlString === DEFAULT_PROJECT_XML;
|
||||
|
||||
const audioContainer = document.getElementById("audio-track-container");
|
||||
if (audioContainer) {
|
||||
|
|
@ -305,10 +299,7 @@ export async function parseMmpContent(xmlString) {
|
|||
appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id || null;
|
||||
appState.pattern.activePatternIndex = 0;
|
||||
|
||||
// --- SUBSTUIÇÃO DO BLOCO DE RESTAURAÇÃO ---
|
||||
// Em vez daquele bloco try/catch gigante, apenas chamamos a função:
|
||||
loadStateFromSession();
|
||||
// ------------------------------------------
|
||||
|
||||
await Promise.resolve();
|
||||
renderAll();
|
||||
|
|
@ -324,17 +315,19 @@ export function generateMmpFile() {
|
|||
|
||||
function generateXmlFromState() {
|
||||
if (!appState.global.originalXmlDoc) {
|
||||
console.warn("Não há XML original. Retornando vazio.");
|
||||
return "";
|
||||
console.log("Gerando XML a partir do template em branco...");
|
||||
const parser = new DOMParser();
|
||||
appState.global.originalXmlDoc = parser.parseFromString(DEFAULT_PROJECT_XML, "application/xml");
|
||||
}
|
||||
|
||||
const xmlDoc = appState.global.originalXmlDoc.cloneNode(true);
|
||||
const head = xmlDoc.querySelector("head");
|
||||
|
||||
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("bpm", document.getElementById("bpm-input").value || 140);
|
||||
head.setAttribute("num_bars", document.getElementById("bars-input").value || 1);
|
||||
head.setAttribute("timesig_numerator", document.getElementById("compasso-a-input").value || 4);
|
||||
head.setAttribute("timesig_denominator", document.getElementById("compasso-b-input").value || 4);
|
||||
}
|
||||
|
||||
const bbTrackContainer = xmlDoc.querySelector('track[type="1"] > bbtrack > trackcontainer');
|
||||
|
|
@ -344,7 +337,6 @@ function generateXmlFromState() {
|
|||
.map((track) => createTrackXml(track))
|
||||
.join("");
|
||||
|
||||
// Gambiarra para inserir o XML gerado como nós DOM
|
||||
const tempDoc = new DOMParser().parseFromString(`<root>${tracksXml}</root>`, "application/xml");
|
||||
Array.from(tempDoc.documentElement.children).forEach((newTrackNode) => {
|
||||
bbTrackContainer.appendChild(newTrackNode);
|
||||
|
|
@ -369,21 +361,22 @@ function createTrackXml(track) {
|
|||
const lmmsVolume = Math.round(track.volume * 100);
|
||||
const lmmsPan = Math.round(track.pan * 100);
|
||||
|
||||
// 🔥 PROTEÇÃO: Se não tiver instrumento definido, usa Kicker padrão
|
||||
const instrName = track.instrumentName || "kicker";
|
||||
const instrXml = track.instrumentXml || `<kicker><env amt="0" attack="0.01" hold="0.1" decay="0.1" release="0.1" sustain="0.5" sync_mode="0"/></kicker>`;
|
||||
|
||||
const patternsXml = track.patterns.map((pattern) => {
|
||||
let patternNotesXml = "";
|
||||
|
||||
// SE for plugin e tiver notas detalhadas, usa elas
|
||||
if (track.type === "plugin" && pattern.notes && pattern.notes.length > 0) {
|
||||
patternNotesXml = pattern.notes.map(note => {
|
||||
return `<note vol="${note.vol}" len="${note.len}" pos="${note.pos}" pan="${note.pan}" key="${note.key}"/>`;
|
||||
}).join("\n ");
|
||||
}
|
||||
// SE for sampler (ou plugin sem notas detalhadas), usa os steps convertidos em notas simples
|
||||
else {
|
||||
patternNotesXml = pattern.steps.map((isActive, index) => {
|
||||
if (isActive) {
|
||||
const notePos = Math.round(index * ticksPerStep);
|
||||
// Key 57 é o padrão do LMMS para samples (Lá)
|
||||
return `<note vol="100" len="${NOTE_LENGTH}" pos="${notePos}" pan="0" key="57"/>`;
|
||||
}
|
||||
return "";
|
||||
|
|
@ -398,8 +391,8 @@ function createTrackXml(track) {
|
|||
return `
|
||||
<track type="0" solo="0" muted="0" name="${track.name}">
|
||||
<instrumenttrack vol="${lmmsVolume}" pitch="0" fxch="0" pitchrange="1" basenote="57" usemasterpitch="1" pan="${lmmsPan}">
|
||||
<instrument name="${track.instrumentName}">
|
||||
${track.instrumentXml}
|
||||
<instrument name="${instrName}">
|
||||
${instrXml}
|
||||
</instrument>
|
||||
<fxchain enabled="0" numofeffects="0"/>
|
||||
</instrumenttrack>
|
||||
|
|
@ -461,5 +454,4 @@ function downloadFile(content, fileName) {
|
|||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export { generateXmlFromState as generateXmlFromStateExported };
|
||||
export { BLANK_PROJECT_XML }; // Mantenha o que já estava
|
||||
export { generateXmlFromState as generateXmlFromStateExported };
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
restartAudioEditorIfPlaying,
|
||||
} from "./audio/audio_audio.js";
|
||||
import { initializeAudioContext } from "./audio.js";
|
||||
import { handleFileLoad, generateMmpFile, BLANK_PROJECT_XML } from "./file.js";
|
||||
import { handleFileLoad, generateMmpFile } from "./file.js";
|
||||
import {
|
||||
renderAll,
|
||||
loadAndRenderSampleBrowser,
|
||||
|
|
@ -13,7 +13,7 @@ import {
|
|||
closeOpenProjectModal,
|
||||
} from "./ui.js";
|
||||
import { renderAudioEditor } from "./audio/audio_ui.js";
|
||||
import { adjustValue, enforceNumericInput } from "./utils.js";
|
||||
import { adjustValue, enforceNumericInput, DEFAULT_PROJECT_XML } from "./utils.js";
|
||||
import { ZOOM_LEVELS } from "./config.js";
|
||||
import { loadProjectFromServer } from "./file.js";
|
||||
import { sendAction, joinRoom, setUserName } from "./socket.js";
|
||||
|
|
@ -61,6 +61,11 @@ const PROJECT_NAME = new URLSearchParams(window.location.search).get("project");
|
|||
? PROJECT_NAME
|
||||
: `${PROJECT_NAME}.mmp`;
|
||||
loadProjectFromServer(filename);
|
||||
} else if (!restored && !PROJECT_NAME) {
|
||||
// CORREÇÃO: Se entrou numa sala vazia sem projeto na URL,
|
||||
// inicializa o XML base para não dar erro depois.
|
||||
const parser = new DOMParser();
|
||||
appState.global.originalXmlDoc = parser.parseFromString(DEFAULT_PROJECT_XML, "application/xml");
|
||||
}
|
||||
|
||||
})(); // Fim do initApp
|
||||
|
|
@ -313,7 +318,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
)
|
||||
)
|
||||
return;
|
||||
sendAction({ type: "LOAD_PROJECT", xml: BLANK_PROJECT_XML });
|
||||
sendAction({ type: "LOAD_PROJECT", xml: DEFAULT_PROJECT_XML });
|
||||
});
|
||||
|
||||
addBarBtn?.addEventListener("click", () => {
|
||||
|
|
@ -341,6 +346,29 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
initializeAudioContext();
|
||||
sendAction({ type: "REMOVE_LAST_TRACK" });
|
||||
});
|
||||
|
||||
// 👇 ATUALIZE ESTE LISTENER
|
||||
removeInstrumentBtn?.addEventListener("click", () => {
|
||||
initializeAudioContext();
|
||||
|
||||
// Pega o ID da faixa que está selecionada/ativa na interface
|
||||
const activeId = appState.pattern.activeTrackId;
|
||||
|
||||
if (activeId) {
|
||||
// Envia comando seguro por ID
|
||||
sendAction({
|
||||
type: "REMOVE_TRACK_BY_ID",
|
||||
trackId: activeId
|
||||
});
|
||||
} else {
|
||||
// Fallback: se não tiver ID ativo, tenta pegar a última (comportamento antigo, mas seguro localmente)
|
||||
const tracks = appState.pattern.tracks;
|
||||
if (tracks.length > 0) {
|
||||
const lastId = tracks[tracks.length - 1].id;
|
||||
sendAction({ type: "REMOVE_TRACK_BY_ID", trackId: lastId });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
playBtn?.addEventListener("click", () => {
|
||||
initializeAudioContext();
|
||||
|
|
@ -496,6 +524,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
});
|
||||
}
|
||||
|
||||
const removeAudioTrackBtn = document.getElementById("remove-audio-track-btn");
|
||||
if (removeAudioTrackBtn) {
|
||||
removeAudioTrackBtn.addEventListener("click", () => {
|
||||
initializeAudioContext();
|
||||
|
||||
// Verifica quantas faixas existem
|
||||
const trackCount = appState.audio.tracks.length;
|
||||
|
||||
if (trackCount > 0) {
|
||||
// Pega a ÚLTIMA faixa da lista
|
||||
const lastTrack = appState.audio.tracks[trackCount - 1];
|
||||
|
||||
// Verifica se tem clips nela (segurança)
|
||||
const hasClips = appState.audio.clips.some(c => c.trackId === lastTrack.id);
|
||||
if (hasClips && !confirm("A última pista contém clips de áudio. Deseja removê-la mesmo assim?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 A MUDANÇA CRUCIAL: Envia o ID, e usa o NOME NOVO do comando
|
||||
sendAction({
|
||||
type: "REMOVE_AUDIO_LANE_BY_ID",
|
||||
trackId: lastTrack.id
|
||||
});
|
||||
} else {
|
||||
showToast("Não há pistas de áudio para remover.", "warning");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Navegador de Samples (local)
|
||||
loadAndRenderSampleBrowser();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// js/pattern_state.js
|
||||
// js/pattern/pattern_state.js
|
||||
import * as Tone from "https://esm.sh/tone";
|
||||
import { TripleOscillator } from "../../audio/plugins/TripleOscillator.js";
|
||||
import { Kicker } from "../../audio/plugins/Kicker.js";
|
||||
|
|
@ -11,6 +11,11 @@ import { DEFAULT_VOLUME, DEFAULT_PAN } from "../config.js";
|
|||
import { getMainGainNode } from "../audio.js";
|
||||
import { getTotalSteps } from "../utils.js";
|
||||
|
||||
// XML padrão para o instrumento Kicker (bateria simples)
|
||||
const DEFAULT_KICKER_XML = `<kicker>
|
||||
<env amt="0" attack="0.01" hold="0.1" decay="0.1" release="0.1" sustain="0.5" sync_mode="0"/>
|
||||
</kicker>`;
|
||||
|
||||
export function initializePatternState() {
|
||||
appState.pattern.tracks.forEach(track => {
|
||||
try { track.player?.dispose(); } catch {}
|
||||
|
|
@ -55,20 +60,17 @@ export async function loadAudioForTrack(track) {
|
|||
|
||||
// --- DETECÇÃO DE TIPO DE ARQUIVO ---
|
||||
// Verifica se é um formato de áudio que o navegador suporta
|
||||
// Verifica tipo de arquivo
|
||||
const isStandardAudio = track.samplePath && /\.(wav|mp3|ogg|flac|m4a)$/i.test(track.samplePath);
|
||||
const isDrumSynth = track.samplePath && /\.ds$/i.test(track.samplePath);
|
||||
|
||||
// Se não for áudio (ou for .ds/.xpf), é Plugin
|
||||
// Se não for áudio padrão, assumimos Plugin (ou Kicker padrão)
|
||||
if (!track.samplePath || !isStandardAudio) {
|
||||
try {
|
||||
if (track.instrument) { try { track.instrument.dispose(); } catch {} }
|
||||
|
||||
let synth;
|
||||
const name = (track.instrumentName || "").toLowerCase();
|
||||
// Normaliza o nome do instrumento. Se vazio, assume kicker.
|
||||
const name = (track.instrumentName || "kicker").toLowerCase();
|
||||
|
||||
// DADOS DO PLUGIN (Tenta parsear XML se for string, ou usa vazio)
|
||||
// Dica: O ideal seria ter um parser real aqui, mas vamos passar {} por enquanto
|
||||
const pluginData = {};
|
||||
|
||||
// SELETOR DE PLUGINS
|
||||
|
|
@ -87,64 +89,53 @@ export async function loadAudioForTrack(track) {
|
|||
break;
|
||||
|
||||
case "nes":
|
||||
case "freeboy": // Freeboy é parecido com NES
|
||||
case "papu": // Papu também é Gameboy
|
||||
case "sid": // SID é 8-bit também, usaremos NES como fallback por enquanto
|
||||
case "freeboy":
|
||||
case "papu":
|
||||
case "sid":
|
||||
synth = new Nes(Tone.getContext(), pluginData);
|
||||
break;
|
||||
|
||||
// --- PACOTE SUPER SAW ---
|
||||
case "zynaddsubfx": // O clássico
|
||||
case "watsyn": // Wavetable Synth
|
||||
case "monstro": // 3 Osciladores monstruosos
|
||||
case "vibedstrings": // Strings vibrantes (fatsaw funciona bem como base)
|
||||
case "SuperSaw":
|
||||
case "zynaddsubfx":
|
||||
case "watsyn":
|
||||
case "monstro":
|
||||
case "vibedstrings":
|
||||
case "supersaw":
|
||||
synth = new SuperSaw(Tone.getContext(), pluginData);
|
||||
break;
|
||||
|
||||
case "organic": // Fallback simples para Organic (Additive)
|
||||
case "organic":
|
||||
synth = new Tone.PolySynth(Tone.Synth, {
|
||||
oscillator: { type: "sine", count: 8, spread: 20 }
|
||||
});
|
||||
break;
|
||||
|
||||
case "zynaddsubfx": // O monstro!
|
||||
// Zyn é impossível de clonar rápido. Vamos usar um "SuperSaw" gordo como placeholder
|
||||
synth = new Tone.PolySynth(Tone.Synth, {
|
||||
oscillator: { type: "fatsawtooth", count: 3, spread: 30 },
|
||||
envelope: { attack: 0.1, decay: 0.3, sustain: 0.8, release: 1 }
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Plugin ${name} desconhecido, usando fallback.`);
|
||||
// Fallback genérico
|
||||
synth = new Tone.PolySynth(Tone.Synth, { oscillator: { type: "triangle" } });
|
||||
console.warn(`Plugin ${name} desconhecido, usando fallback (Kicker).`);
|
||||
// Fallback seguro: Kicker
|
||||
synth = new Kicker(Tone.getContext(), pluginData);
|
||||
}
|
||||
|
||||
// Se o plugin criou uma classe wrapper (nossas classes .js),
|
||||
// ele tem o método .connect. Se for Tone nativo (Organic/Zyn fallback), também tem.
|
||||
if (synth.output) {
|
||||
// Nossas classes customizadas
|
||||
synth.connect(track.volumeNode);
|
||||
} else {
|
||||
// Objetos Tone.js puros
|
||||
synth.connect(track.volumeNode);
|
||||
}
|
||||
|
||||
track.instrument = synth;
|
||||
track.player = null;
|
||||
track.type = 'plugin';
|
||||
// Atualiza o nome se ele estava vazio
|
||||
if (!track.instrumentName) track.instrumentName = name;
|
||||
|
||||
console.log(`[Audio] Plugin carregado: ${name}`);
|
||||
|
||||
} catch (e) {
|
||||
console.error("Erro ao carregar plugin:", name, e);
|
||||
console.error("Erro ao carregar plugin:", track.instrumentName, e);
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
// 3. Lógica para SAMPLERS (Arquivos de Áudio Reais)
|
||||
// 3. Lógica para SAMPLERS
|
||||
try {
|
||||
try { track.player?.dispose(); } catch {}
|
||||
track.player = null;
|
||||
|
|
@ -166,7 +157,7 @@ export async function loadAudioForTrack(track) {
|
|||
|
||||
track.player = player;
|
||||
track.buffer = buffer;
|
||||
track.type = 'sampler'; // Garante o tipo correto
|
||||
track.type = 'sampler';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar sample:', track.samplePath);
|
||||
|
|
@ -181,21 +172,29 @@ export async function loadAudioForTrack(track) {
|
|||
export function addTrackToState() {
|
||||
const totalSteps = getTotalSteps();
|
||||
const referenceTrack = appState.pattern.tracks[0];
|
||||
const newId = Date.now() + Math.random();
|
||||
|
||||
const newTrack = {
|
||||
id: Date.now() + Math.random(),
|
||||
name: "novo instrumento",
|
||||
id: newId,
|
||||
name: `Novo Instrumento ${appState.pattern.tracks.length + 1}`,
|
||||
samplePath: null,
|
||||
type: 'plugin', // Padrão
|
||||
type: 'plugin',
|
||||
|
||||
// 🔥 CORREÇÃO: Definir instrumento padrão (Kicker) e XML
|
||||
instrumentName: "kicker",
|
||||
instrumentXml: DEFAULT_KICKER_XML,
|
||||
|
||||
player: null,
|
||||
buffer: null,
|
||||
patterns: referenceTrack
|
||||
? referenceTrack.patterns.map(p => ({
|
||||
name: p.name,
|
||||
steps: new Array(p.steps.length).fill(false),
|
||||
notes: [],
|
||||
pos: p.pos
|
||||
}))
|
||||
: [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), pos: 0 }],
|
||||
: [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), notes: [], pos: 0 }],
|
||||
|
||||
activePatternIndex: 0,
|
||||
volume: DEFAULT_VOLUME,
|
||||
pan: DEFAULT_PAN,
|
||||
|
|
@ -206,10 +205,39 @@ export function addTrackToState() {
|
|||
newTrack.volumeNode.connect(newTrack.pannerNode);
|
||||
newTrack.pannerNode.connect(getMainGainNode());
|
||||
|
||||
// Carrega o áudio (vai cair no case "kicker" do loadAudioForTrack)
|
||||
loadAudioForTrack(newTrack);
|
||||
|
||||
appState.pattern.tracks.push(newTrack);
|
||||
appState.pattern.activeTrackId = newTrack.id;
|
||||
|
||||
console.log("Faixa adicionada ao estado com Kicker padrão.");
|
||||
}
|
||||
|
||||
export function removeTrackById(trackId) {
|
||||
const index = appState.pattern.tracks.findIndex(t => t.id === trackId);
|
||||
|
||||
if (index !== -1) {
|
||||
const trackToRemove = appState.pattern.tracks[index];
|
||||
|
||||
// Limpeza de memória
|
||||
try { trackToRemove.player?.dispose(); } catch {}
|
||||
try { trackToRemove.buffer?.dispose?.(); } catch {}
|
||||
try { trackToRemove.instrument?.dispose(); } catch {}
|
||||
try { trackToRemove.pannerNode?.disconnect(); } catch {}
|
||||
try { trackToRemove.volumeNode?.disconnect(); } catch {}
|
||||
|
||||
// Remove do array
|
||||
appState.pattern.tracks.splice(index, 1);
|
||||
|
||||
// Ajusta seleção se necessário
|
||||
if (appState.pattern.activeTrackId === trackId) {
|
||||
appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id ?? null;
|
||||
}
|
||||
|
||||
return true; // Retorna sucesso
|
||||
}
|
||||
return false; // Não achou (o fantasma não existe aqui)
|
||||
}
|
||||
|
||||
export function removeLastTrackFromState() {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -9,7 +9,87 @@ const { Server } = require("socket.io");
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const pino = require("pino");
|
||||
//const { PORT_SOCK } = require("../config.js");
|
||||
|
||||
const DEFAULT_PROJECT_XML = `<?xml version="1.0"?>
|
||||
<!DOCTYPE lmms-project>
|
||||
<lmms-project version="1.0" type="song" creatorversion="1.2.2" creator="LMMS">
|
||||
<head mastervol="100" masterpitch="0" timesig_denominator="4" timesig_numerator="4" bpm="140"/>
|
||||
<song>
|
||||
<trackcontainer visible="1" y="5" minimized="0" height="300" type="song" x="5" maximized="0" width="600">
|
||||
<track muted="0" name="TripleOscillator" type="0" solo="0">
|
||||
<instrumenttrack pitchrange="1" fxch="0" usemasterpitch="1" vol="100" pitch="0" pan="0" basenote="57">
|
||||
<instrument name="tripleoscillator">
|
||||
<tripleoscillator wavetype1="0" pan2="0" finer1="0" finel1="0" userwavefile0="" wavetype0="0" userwavefile1="" finer0="0" stphdetun1="0" vol0="33" wavetype2="0" coarse0="0" finel2="0" finer2="0" modalgo2="2" finel0="0" coarse1="-12" phoffset1="0" pan0="0" modalgo3="2" coarse2="-24" phoffset2="0" pan1="0" vol1="33" phoffset0="0" stphdetun0="0" modalgo1="2" vol2="33" stphdetun2="0" userwavefile2=""/>
|
||||
</instrument>
|
||||
<eldata fres="0.5" fwet="0" ftype="0" fcut="14000">
|
||||
<elvol ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
<elcut ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
<elres ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
</eldata>
|
||||
<chordcreator chordrange="1" chord-enabled="0" chord="0"/>
|
||||
<arpeggiator arpcycle="0" arptime_denominator="4" arpgate="100" arpdir="0" arpmode="0" arptime_syncmode="0" arp="0" arpmiss="0" arp-enabled="0" arptime="100" arprange="1" arpskip="0" arptime_numerator="4"/>
|
||||
<midiport inputchannel="0" outputcontroller="0" fixedoutputvelocity="-1" outputchannel="1" fixedinputvelocity="-1" outputprogram="1" inputcontroller="0" readable="0" fixedoutputnote="-1" basevelocity="63" writable="0"/>
|
||||
<fxchain enabled="0" numofeffects="0"/>
|
||||
</instrumenttrack>
|
||||
</track>
|
||||
<track muted="0" name="Sample track" type="2" solo="0">
|
||||
<sampletrack vol="100" pan="0">
|
||||
<fxchain enabled="0" numofeffects="0"/>
|
||||
</sampletrack>
|
||||
</track>
|
||||
<track muted="0" name="Beat/Bassline 0" type="1" solo="0">
|
||||
<bbtrack>
|
||||
<trackcontainer visible="0" y="5" minimized="0" height="400" type="bbtrackcontainer" x="610" maximized="0" width="700">
|
||||
<track muted="0" name="Kicker" type="0" solo="0">
|
||||
<instrumenttrack pitchrange="1" fxch="0" usemasterpitch="1" vol="100" pitch="0" pan="0" basenote="57">
|
||||
<instrument name="kicker">
|
||||
<kicker decay="440" version="1" noise="0" endfreq="40" decay_syncmode="0" decay_denominator="4" startfreq="150" env="0.163" startnote="1" dist="0.8" slope="0.06" decay_numerator="4" click="0.4" distend="0.8" gain="1" endnote="0"/>
|
||||
</instrument>
|
||||
<eldata fres="0.5" fwet="0" ftype="0" fcut="14000">
|
||||
<elvol ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
<elcut ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
<elres ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
</eldata>
|
||||
<chordcreator chordrange="1" chord-enabled="0" chord="0"/>
|
||||
<arpeggiator arpcycle="0" arptime_denominator="4" arpgate="100" arpdir="0" arpmode="0" arptime_syncmode="0" arp="0" arpmiss="0" arp-enabled="0" arptime="100" arprange="1" arpskip="0" arptime_numerator="4"/>
|
||||
<midiport inputchannel="0" outputcontroller="0" fixedoutputvelocity="-1" outputchannel="1" fixedinputvelocity="-1" outputprogram="1" inputcontroller="0" readable="0" fixedoutputnote="-1" basevelocity="63" writable="0"/>
|
||||
<fxchain enabled="0" numofeffects="0"/>
|
||||
</instrumenttrack>
|
||||
<pattern pos="0" muted="0" name="Kicker" steps="16" type="0"/>
|
||||
</track>
|
||||
</trackcontainer>
|
||||
</bbtrack>
|
||||
</track>
|
||||
<track muted="0" name="Automation track" type="5" solo="0">
|
||||
<automationtrack/>
|
||||
</track>
|
||||
</trackcontainer>
|
||||
<track muted="0" name="Automation track" type="6" solo="0">
|
||||
<automationtrack/>
|
||||
<automationpattern pos="0" len="192" name="Numerator" prog="0" tens="1" mute="0"/>
|
||||
<automationpattern pos="0" len="192" name="Denominator" prog="0" tens="1" mute="0"/>
|
||||
<automationpattern pos="0" len="192" name="Tempo" prog="0" tens="1" mute="0"/>
|
||||
<automationpattern pos="0" len="192" name="Master volume" prog="0" tens="1" mute="0"/>
|
||||
<automationpattern pos="0" len="192" name="Master pitch" prog="0" tens="1" mute="0"/>
|
||||
</track>
|
||||
<fxmixer visible="1" y="310" minimized="0" height="333" x="5" maximized="0" width="543">
|
||||
<fxchannel volume="1" muted="0" num="0" name="Master" soloed="0">
|
||||
<fxchain enabled="0" numofeffects="0"/>
|
||||
</fxchannel>
|
||||
</fxmixer>
|
||||
<ControllerRackView visible="1" y="310" minimized="0" height="200" x="680" maximized="0" width="350"/>
|
||||
<pianoroll visible="0" y="5" minimized="0" height="480" x="5" maximized="0" width="860"/>
|
||||
<automationeditor visible="0" y="1" minimized="0" height="400" x="1" maximized="0" width="860"/>
|
||||
<projectnotes visible="0" y="10" minimized="0" height="400" x="700" maximized="0" width="679"><![CDATA[<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||
p, li { white-space: pre-wrap; }
|
||||
</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:7.5pt; font-weight:400; font-style:normal;">
|
||||
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html>]]></projectnotes>
|
||||
<timeline lp1pos="192" lpstate="0" lp0pos="0"/>
|
||||
<controllers/>
|
||||
</song>
|
||||
</lmms-project>`;
|
||||
|
||||
|
||||
// -------------------------
|
||||
// LOGGER DINÂMICO (GERENCIADOR)
|
||||
|
|
@ -111,7 +191,8 @@ function ensureRoom(roomName) {
|
|||
if (fs.existsSync(p)) {
|
||||
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
||||
roomStates[roomName] = {
|
||||
projectXml: j.projectXml || null,
|
||||
// 🔥 CORREÇÃO: Fallback para DEFAULT_PROJECT_XML se o JSON salvo estiver sem XML
|
||||
projectXml: j.projectXml || DEFAULT_PROJECT_XML,
|
||||
audio: j.audio || { tracks: [], clips: [] },
|
||||
seq: j.seq || 0,
|
||||
tokensSeen: new Set(),
|
||||
|
|
@ -122,8 +203,10 @@ function ensureRoom(roomName) {
|
|||
console.warn(`[persist] falha ao carregar estado da sala ${roomName}:`, e);
|
||||
}
|
||||
|
||||
// Cria sala NOVA na memória
|
||||
roomStates[roomName] = {
|
||||
projectXml: null,
|
||||
// 🔥 CORREÇÃO: Inicia com o XML do LMMS completo, não mais null
|
||||
projectXml: DEFAULT_PROJECT_XML,
|
||||
audio: { tracks: [], clips: [] },
|
||||
seq: 0,
|
||||
tokensSeen: new Set(),
|
||||
|
|
@ -245,9 +328,6 @@ function applyAuthoritativeAction(roomName, action) {
|
|||
}
|
||||
|
||||
case "UPDATE_AUDIO_CLIP": {
|
||||
// =================================================================
|
||||
// 👇 INÍCIO DA CORREÇÃO (Blindagem do Servidor para Bug 4)
|
||||
// =================================================================
|
||||
if (!action.clipId || !action.props) {
|
||||
console.warn(
|
||||
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (dados base inválidos):`,
|
||||
|
|
@ -277,9 +357,6 @@ function applyAuthoritativeAction(roomName, action) {
|
|||
);
|
||||
return null;
|
||||
}
|
||||
// =================================================================
|
||||
// 👆 FIM DA CORREÇÃO
|
||||
// =================================================================
|
||||
|
||||
const c = state.clips.find((x) => String(x.id) === String(action.clipId));
|
||||
if (c && action.props && typeof action.props === "object") {
|
||||
|
|
@ -300,6 +377,29 @@ function applyAuthoritativeAction(roomName, action) {
|
|||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_AUDIO_LANE_BY_ID": {
|
||||
// 1. Acha e remove a Track
|
||||
const idx = state.tracks.findIndex((t) => t.id === action.trackId);
|
||||
if (idx !== -1) {
|
||||
state.tracks.splice(idx, 1);
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
// 2. LIMPEZA PROFUNDA: Remove clips dessa track E clips órfãos (trackId null)
|
||||
// Se removemos a track, os clips dela têm que sumir.
|
||||
const prevCount = state.clips.length;
|
||||
state.clips = state.clips.filter((c) => {
|
||||
// Mantém apenas se tiver trackId E se esse trackId ainda existir na lista de tracks
|
||||
const parentTrackExists = state.tracks.some(t => t.id === c.trackId);
|
||||
return c.trackId && parentTrackExists;
|
||||
});
|
||||
|
||||
if (state.clips.length !== prevCount) {
|
||||
mutated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// outras ações não são da persistência do editor de áudio
|
||||
mutated = false;
|
||||
|
|
@ -414,13 +514,19 @@ io.on("connection", (socket) => {
|
|||
// Carrega estado da sala
|
||||
const room = ensureRoom(roomName);
|
||||
|
||||
// Persiste estado se for LOAD_PROJECT (compat)
|
||||
if (action.type === "LOAD_PROJECT" && action.xml) {
|
||||
room.projectXml = action.xml;
|
||||
saveRoom(roomName);
|
||||
console.log(
|
||||
`[broadcast_action] Estado da sala ${roomName} atualizado (LOAD_PROJECT).`
|
||||
);
|
||||
// Persiste estado do Pattern (Notas/Sequenciador)
|
||||
// Aceita tanto carregamento total quanto atualização de notas
|
||||
if ((action.type === "LOAD_PROJECT" || action.type === "SYNC_PATTERN_STATE") && action.xml) {
|
||||
// Proteção: Não salva se o XML for vazio (evita corromper a sala com o bug antigo)
|
||||
if (action.xml.trim().length > 0) {
|
||||
room.projectXml = action.xml;
|
||||
saveRoom(roomName);
|
||||
console.log(
|
||||
`[broadcast_action] XML da sala ${roomName} atualizado via ${action.type}.`
|
||||
);
|
||||
} else {
|
||||
console.warn(`[Server] Ignorando XML vazio vindo de ${action.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica ações autoritativas do editor de áudio
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
addTrackToState,
|
||||
removeLastTrackFromState,
|
||||
updateTrackSample,
|
||||
removeTrackById,
|
||||
} from "./pattern/pattern_state.js";
|
||||
import {
|
||||
addAudioTrackLane,
|
||||
|
|
@ -37,13 +38,13 @@ import {
|
|||
parseMmpContent,
|
||||
handleLocalProjectReset,
|
||||
syncPatternStateToServer,
|
||||
BLANK_PROJECT_XML,
|
||||
generateXmlFromStateExported,
|
||||
} from "./file.js";
|
||||
|
||||
import { renderAll, showToast } from "./ui.js"; // showToast()
|
||||
import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
|
||||
import { PORT_SOCK } from "./config.js";
|
||||
import { DEFAULT_PROJECT_XML } from "./utils.js"
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Gera um ID único otimista (ex: "track_1678886401000_abc123")
|
||||
|
|
@ -200,17 +201,12 @@ socket.on("load_project_state", async (projectXml) => {
|
|||
// 🔥 CORREÇÃO: Força a restauração da sessão LOCAL logo após carregar o XML
|
||||
// Isso garante que suas alterações locais (BPM, steps) "ganhem" do servidor
|
||||
if (window.ROOM_NAME) {
|
||||
const raw = sessionStorage.getItem(`temp_state_${window.ROOM_NAME}`);
|
||||
const raw = sessionStorage.getItem(`temp_state_${window.ROOM_NAME}`);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
const hasLocalAudio = parsed.audioSnapshot?.clips?.length > 0 ||
|
||||
parsed.audioSnapshot?.tracks?.length > 0;
|
||||
if (hasLocalAudio) {
|
||||
console.log("Re-aplicando sessão local sobre o XML do servidor...");
|
||||
// Se existe 'raw', confiamos que é o estado mais recente do usuário.
|
||||
console.log("Re-aplicando sessão local (mesmo se vazia)...");
|
||||
await loadStateFromSession();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
renderAll();
|
||||
|
|
@ -513,6 +509,44 @@ async function handleActionBroadcast(action) {
|
|||
showToast(`▶ ${who} Play bases`, "info");
|
||||
break;
|
||||
}
|
||||
case "UPDATE_PATTERN_NOTES": {
|
||||
const {
|
||||
trackIndex: ti,
|
||||
patternIndex: pi,
|
||||
notes,
|
||||
steps: incomingSteps,
|
||||
} = action;
|
||||
|
||||
const t = appState.pattern.tracks[ti];
|
||||
if (!t) break;
|
||||
|
||||
if (!t.patterns[pi]) t.patterns[pi] = { steps: [], notes: [] };
|
||||
|
||||
t.patterns[pi].notes = Array.isArray(notes) ? notes : [];
|
||||
|
||||
if (Array.isArray(incomingSteps)) {
|
||||
t.patterns[pi].steps = incomingSteps;
|
||||
}
|
||||
|
||||
try {
|
||||
schedulePatternRerender();
|
||||
} catch (e) {
|
||||
console.warn("Erro no render UPDATE_PATTERN_NOTES", e);
|
||||
}
|
||||
|
||||
const isFromSelf = action.__senderId === socket.id;
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({
|
||||
type: "SYNC_PATTERN_STATE",
|
||||
xml,
|
||||
});
|
||||
}
|
||||
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
|
||||
case "STOP_PLAYBACK": {
|
||||
setTimeout(stopPlayback, delayMs);
|
||||
const who = actorOf(action);
|
||||
|
|
@ -551,9 +585,6 @@ async function handleActionBroadcast(action) {
|
|||
);
|
||||
break;
|
||||
|
||||
// =================================================================
|
||||
// 👇 INÍCIO DA CORREÇÃO (Handlers Sincronia de Loop/Seek/SyncMode)
|
||||
// =================================================================
|
||||
case "SET_LOOP_STATE": {
|
||||
const changed =
|
||||
appState.global.isLoopActive !== !!action.isLoopActive ||
|
||||
|
|
@ -614,9 +645,6 @@ async function handleActionBroadcast(action) {
|
|||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
// =================================================================
|
||||
// 👆 FIM DA CORREÇÃO
|
||||
// =================================================================
|
||||
|
||||
// Estado Global
|
||||
case "LOAD_PROJECT": //
|
||||
|
|
@ -647,17 +675,22 @@ async function handleActionBroadcast(action) {
|
|||
isLoadingProject = false;
|
||||
break;
|
||||
|
||||
case "SYNC_PATTERN_STATE": //
|
||||
// Esta ação agora só será recebida de *outros* usuários,
|
||||
// ou quando o servidor enviar, não de você mesmo.
|
||||
try {
|
||||
await parseMmpContent(action.xml); //
|
||||
renderAll();
|
||||
saveStateToSession(); //
|
||||
console.log("Socket: Pattern state sincronizado.");
|
||||
} catch (e) {
|
||||
console.error("Erro SYNC_PATTERN_STATE:", e);
|
||||
showToast("❌ Erro sync pattern", "error");
|
||||
case "SYNC_PATTERN_STATE":
|
||||
// 🔥 CORREÇÃO CRÍTICA:
|
||||
// Esta ação serve apenas para o SERVIDOR atualizar o arquivo .json no disco.
|
||||
// Os clientes online NÃO devem recarregar o XML, pois eles já atualizaram
|
||||
// o estado via ações atômicas (TOGGLE_NOTE, ADD_TRACK, etc).
|
||||
// Recarregar aqui causa o "resetProjectState" que mata o áudio.
|
||||
|
||||
console.log("Socket: XML salvo no servidor (Sync silencioso).");
|
||||
|
||||
// Se quiser garantir, salvamos apenas na sessão local do navegador
|
||||
// sem mexer na engine de áudio ou na tela.
|
||||
if (action.xml) {
|
||||
// Atualiza apenas a string do XML na memória global para referência futura
|
||||
const parser = new DOMParser();
|
||||
appState.global.originalXmlDoc = parser.parseFromString(action.xml, "application/xml");
|
||||
saveStateToSession();
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -713,17 +746,55 @@ async function handleActionBroadcast(action) {
|
|||
renderPatternEditor();
|
||||
const who = actorOf(action);
|
||||
showToast(`🥁 Faixa add por ${who}`, "info");
|
||||
// Salva o estado localmente também!
|
||||
|
||||
// 🔥 CORREÇÃO: Avisa o servidor que o XML mudou!
|
||||
const isFromSelf = action.__senderId === socket.id;
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||
}
|
||||
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_TRACK_BY_ID": {
|
||||
const { trackId } = action;
|
||||
const removed = removeTrackById(trackId); // Tenta remover pelo ID seguro
|
||||
|
||||
if (removed) {
|
||||
renderPatternEditor();
|
||||
const who = actorOf(action);
|
||||
showToast(`❌ Faixa removida por ${who}`, "warning");
|
||||
|
||||
// Sincroniza o XML com o servidor para persistir a remoção
|
||||
const isFromSelf = action.__senderId === socket.id;
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
// Importante: gerar o XML atualizado (sem a faixa)
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||
}
|
||||
saveStateToSession();
|
||||
} else {
|
||||
console.warn(`Tentativa de remover track inexistente/fantasma: ${trackId}`);
|
||||
// Não faz nada, protegendo as faixas locais!
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_LAST_TRACK": {
|
||||
removeLastTrackFromState();
|
||||
renderPatternEditor();
|
||||
const who = actorOf(action);
|
||||
showToast(`❌ Faixa remov. por ${who}`, "warning");
|
||||
// Salva o estado localmente também!
|
||||
|
||||
// 🔥 CORREÇÃO: Avisa o servidor que o XML mudou (removeu o TripleOscillator, por exemplo)
|
||||
const isFromSelf = action.__senderId === socket.id;
|
||||
if (window.ROOM_NAME && isFromSelf) {
|
||||
const xml = generateXmlFromStateExported();
|
||||
sendAction({ type: "SYNC_PATTERN_STATE", xml });
|
||||
}
|
||||
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
|
|
@ -751,6 +822,31 @@ async function handleActionBroadcast(action) {
|
|||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_LAST_AUDIO_LANE": {
|
||||
// 1. Verifica se tem tracks
|
||||
if (appState.audio.tracks.length === 0) break;
|
||||
|
||||
// 2. Pega a última track
|
||||
const lastTrack = appState.audio.tracks.pop();
|
||||
|
||||
// 3. Remove os clips associados a essa track (Limpeza)
|
||||
if (lastTrack && lastTrack.id) {
|
||||
// Filtra mantendo apenas os clips que NÃO pertencem à track removida
|
||||
appState.audio.clips = appState.audio.clips.filter(clip => clip.trackId !== lastTrack.id);
|
||||
}
|
||||
|
||||
// 4. Atualiza a tela
|
||||
renderAll();
|
||||
|
||||
// 5. Notifica
|
||||
const who = actorOf(action);
|
||||
showToast(`🗑️ Pista de áudio removida por ${who}`, "error");
|
||||
|
||||
// 6. Salva sessão
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_AUDIO_CLIP":
|
||||
if (removeAudioClip(action.clipId)) {
|
||||
appState.global.selectedClipId = null;
|
||||
|
|
@ -887,47 +983,32 @@ async function handleActionBroadcast(action) {
|
|||
|
||||
// Snapshots
|
||||
case "AUDIO_SNAPSHOT_REQUEST": {
|
||||
const clips = appState.audio?.clips?.length || 0,
|
||||
tracks = appState.audio?.tracks?.length || 0;
|
||||
const iHave = clips > 0 || tracks > 0;
|
||||
if (!iHave || isFromSelf) break;
|
||||
if (!window.__lastSnapshotSentAt) window.__lastSnapshotSentAt = 0;
|
||||
const now = Date.now();
|
||||
if (now - window.__lastSnapshotSentAt < 1500) break;
|
||||
window.__lastSnapshotSentAt = now;
|
||||
try {
|
||||
const snap = getAudioSnapshot();
|
||||
sendAction({
|
||||
type: "AUDIO_SNAPSHOT",
|
||||
snapshot: snap,
|
||||
__target: action.__senderId,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("Erro AUDIO_SNAPSHOT_REQUEST:", e);
|
||||
}
|
||||
// Salva o estado localmente também!
|
||||
saveStateToSession();
|
||||
// 🛑 BLOQUEADO: Agora o Servidor (backend) é quem manda o snapshot autoritativo.
|
||||
// Se deixarmos os clientes responderem, eles vão enviar estados antigos ("zumbis").
|
||||
console.log("Socket: Ignorando pedido de snapshot (Server Authoritative Mode).");
|
||||
break;
|
||||
}
|
||||
|
||||
case "AUDIO_SNAPSHOT": {
|
||||
if (action.__target && action.__target !== socket.id) break;
|
||||
if (appState.global.justReset) {
|
||||
console.warn("Socket: Snapshot de áudio ignorado (justReset=true).");
|
||||
break; // Ignora o snapshot
|
||||
}
|
||||
const hasClips = (appState.audio?.clips?.length || 0) > 0;
|
||||
if (hasClips) break;
|
||||
|
||||
// Removemos a verificação "if (hasClips) break" que impedia
|
||||
// de carregar um estado vazio se o local já tivesse algo (como as faixas zumbis).
|
||||
|
||||
try {
|
||||
console.log("Socket: Aplicando Snapshot de Áudio Autoritativo...");
|
||||
await applyAudioSnapshot(action.snapshot);
|
||||
renderAll();
|
||||
|
||||
const who = actorOf(action);
|
||||
showToast(`🔁 Sync áudio por ${who}`, "success");
|
||||
// Só mostra toast se realmente tiver conteúdo, para não spammar na entrada
|
||||
if ((action.snapshot?.tracks?.length || 0) > 0) {
|
||||
showToast(`🔁 Sync áudio por ${who}`, "success");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Erro AUDIO_SNAPSHOT:", e);
|
||||
showToast("❌ Erro sync áudio", "error");
|
||||
}
|
||||
// Salva o estado localmente também!
|
||||
// Salva o estado localmente (mesmo que seja vazio!)
|
||||
saveStateToSession();
|
||||
break;
|
||||
}
|
||||
|
|
@ -1016,6 +1097,32 @@ async function handleActionBroadcast(action) {
|
|||
break;
|
||||
}
|
||||
|
||||
case "REMOVE_AUDIO_LANE_BY_ID": { //
|
||||
const { trackId } = action;
|
||||
|
||||
// 1. Encontra o índice da faixa com esse ID específico
|
||||
const trackIndex = appState.audio.tracks.findIndex(t => t.id === trackId);
|
||||
|
||||
if (trackIndex !== -1) {
|
||||
// 2. Remove a faixa exata do array local
|
||||
appState.audio.tracks.splice(trackIndex, 1);
|
||||
|
||||
// 3. Remove os clips associados a este ID (Limpeza local)
|
||||
appState.audio.clips = appState.audio.clips.filter(c => c.trackId !== trackId);
|
||||
|
||||
renderAll(); // Atualiza a tela
|
||||
|
||||
const who = actorOf(action);
|
||||
// showToast(`🗑️ Pista removida por ${who}`, "warning");
|
||||
|
||||
// 4. Salva a sessão para garantir persistência no F5 local
|
||||
saveStateToSession();
|
||||
} else {
|
||||
console.warn(`Tentativa de remover track inexistente: ${trackId}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("Ação desconhecida:", action.type);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,86 @@
|
|||
import { appState } from './state.js';
|
||||
import { PIXELS_PER_STEP, ZOOM_LEVELS } from './config.js';
|
||||
|
||||
export const DEFAULT_PROJECT_XML = `<?xml version="1.0"?>
|
||||
<!DOCTYPE lmms-project>
|
||||
<lmms-project version="1.0" type="song" creatorversion="1.2.2" creator="LMMS">
|
||||
<head mastervol="100" masterpitch="0" timesig_denominator="4" timesig_numerator="4" bpm="140"/>
|
||||
<song>
|
||||
<trackcontainer visible="1" y="5" minimized="0" height="300" type="song" x="5" maximized="0" width="600">
|
||||
<track muted="0" name="TripleOscillator" type="0" solo="0">
|
||||
<instrumenttrack pitchrange="1" fxch="0" usemasterpitch="1" vol="100" pitch="0" pan="0" basenote="57">
|
||||
<instrument name="tripleoscillator">
|
||||
<tripleoscillator wavetype1="0" pan2="0" finer1="0" finel1="0" userwavefile0="" wavetype0="0" userwavefile1="" finer0="0" stphdetun1="0" vol0="33" wavetype2="0" coarse0="0" finel2="0" finer2="0" modalgo2="2" finel0="0" coarse1="-12" phoffset1="0" pan0="0" modalgo3="2" coarse2="-24" phoffset2="0" pan1="0" vol1="33" phoffset0="0" stphdetun0="0" modalgo1="2" vol2="33" stphdetun2="0" userwavefile2=""/>
|
||||
</instrument>
|
||||
<eldata fres="0.5" fwet="0" ftype="0" fcut="14000">
|
||||
<elvol ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
<elcut ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
<elres ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
</eldata>
|
||||
<chordcreator chordrange="1" chord-enabled="0" chord="0"/>
|
||||
<arpeggiator arpcycle="0" arptime_denominator="4" arpgate="100" arpdir="0" arpmode="0" arptime_syncmode="0" arp="0" arpmiss="0" arp-enabled="0" arptime="100" arprange="1" arpskip="0" arptime_numerator="4"/>
|
||||
<midiport inputchannel="0" outputcontroller="0" fixedoutputvelocity="-1" outputchannel="1" fixedinputvelocity="-1" outputprogram="1" inputcontroller="0" readable="0" fixedoutputnote="-1" basevelocity="63" writable="0"/>
|
||||
<fxchain enabled="0" numofeffects="0"/>
|
||||
</instrumenttrack>
|
||||
</track>
|
||||
<track muted="0" name="Sample track" type="2" solo="0">
|
||||
<sampletrack vol="100" pan="0">
|
||||
<fxchain enabled="0" numofeffects="0"/>
|
||||
</sampletrack>
|
||||
</track>
|
||||
<track muted="0" name="Beat/Bassline 0" type="1" solo="0">
|
||||
<bbtrack>
|
||||
<trackcontainer visible="0" y="5" minimized="0" height="400" type="bbtrackcontainer" x="610" maximized="0" width="700">
|
||||
<track muted="0" name="Kicker" type="0" solo="0">
|
||||
<instrumenttrack pitchrange="1" fxch="0" usemasterpitch="1" vol="100" pitch="0" pan="0" basenote="57">
|
||||
<instrument name="kicker">
|
||||
<kicker decay="440" version="1" noise="0" endfreq="40" decay_syncmode="0" decay_denominator="4" startfreq="150" env="0.163" startnote="1" dist="0.8" slope="0.06" decay_numerator="4" click="0.4" distend="0.8" gain="1" endnote="0"/>
|
||||
</instrument>
|
||||
<eldata fres="0.5" fwet="0" ftype="0" fcut="14000">
|
||||
<elvol ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
<elcut ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
<elres ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
|
||||
</eldata>
|
||||
<chordcreator chordrange="1" chord-enabled="0" chord="0"/>
|
||||
<arpeggiator arpcycle="0" arptime_denominator="4" arpgate="100" arpdir="0" arpmode="0" arptime_syncmode="0" arp="0" arpmiss="0" arp-enabled="0" arptime="100" arprange="1" arpskip="0" arptime_numerator="4"/>
|
||||
<midiport inputchannel="0" outputcontroller="0" fixedoutputvelocity="-1" outputchannel="1" fixedinputvelocity="-1" outputprogram="1" inputcontroller="0" readable="0" fixedoutputnote="-1" basevelocity="63" writable="0"/>
|
||||
<fxchain enabled="0" numofeffects="0"/>
|
||||
</instrumenttrack>
|
||||
<pattern pos="0" muted="0" name="Kicker" steps="16" type="0"/>
|
||||
</track>
|
||||
</trackcontainer>
|
||||
</bbtrack>
|
||||
</track>
|
||||
<track muted="0" name="Automation track" type="5" solo="0">
|
||||
<automationtrack/>
|
||||
</track>
|
||||
</trackcontainer>
|
||||
<track muted="0" name="Automation track" type="6" solo="0">
|
||||
<automationtrack/>
|
||||
<automationpattern pos="0" len="192" name="Numerator" prog="0" tens="1" mute="0"/>
|
||||
<automationpattern pos="0" len="192" name="Denominator" prog="0" tens="1" mute="0"/>
|
||||
<automationpattern pos="0" len="192" name="Tempo" prog="0" tens="1" mute="0"/>
|
||||
<automationpattern pos="0" len="192" name="Master volume" prog="0" tens="1" mute="0"/>
|
||||
<automationpattern pos="0" len="192" name="Master pitch" prog="0" tens="1" mute="0"/>
|
||||
</track>
|
||||
<fxmixer visible="1" y="310" minimized="0" height="333" x="5" maximized="0" width="543">
|
||||
<fxchannel volume="1" muted="0" num="0" name="Master" soloed="0">
|
||||
<fxchain enabled="0" numofeffects="0"/>
|
||||
</fxchannel>
|
||||
</fxmixer>
|
||||
<ControllerRackView visible="1" y="310" minimized="0" height="200" x="680" maximized="0" width="350"/>
|
||||
<pianoroll visible="0" y="5" minimized="0" height="480" x="5" maximized="0" width="860"/>
|
||||
<automationeditor visible="0" y="1" minimized="0" height="400" x="1" maximized="0" width="860"/>
|
||||
<projectnotes visible="0" y="10" minimized="0" height="400" x="700" maximized="0" width="679"><![CDATA[<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
|
||||
p, li { white-space: pre-wrap; }
|
||||
</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:7.5pt; font-weight:400; font-style:normal;">
|
||||
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html>]]></projectnotes>
|
||||
<timeline lp1pos="192" lpstate="0" lp0pos="0"/>
|
||||
<controllers/>
|
||||
</song>
|
||||
</lmms-project>`;
|
||||
|
||||
/**
|
||||
* Helper interna para ler o BPM do input.
|
||||
* @returns {number} O BPM atual.
|
||||
|
|
|
|||
356
creation.html
356
creation.html
|
|
@ -84,33 +84,31 @@
|
|||
id="new-project-btn"
|
||||
title="Novo Projeto"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Novo projeto</span>
|
||||
<i
|
||||
class="fa-solid fa-folder-open"
|
||||
id="open-mmp-btn"
|
||||
title="Abrir Projeto do Servidor"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Abrir projetos</span>
|
||||
<i
|
||||
class="fa-solid fa-save"
|
||||
id="save-mmp-btn"
|
||||
title="Salvar Projeto (.mmp)"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Salvar projeto</span>
|
||||
<i
|
||||
class="fa-solid fa-upload"
|
||||
id="upload-sample-btn"
|
||||
title="Carregar Sample do Computador"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Enviar sample</span>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="control-group">
|
||||
<i
|
||||
class="fa-solid fa-backward-step"
|
||||
id="rewind-btn"
|
||||
title="Voltar ao Início"
|
||||
></i>
|
||||
<i class="fa-solid fa-play" title="Play/Pause Global (Futuro)"></i>
|
||||
<i class="fa-solid fa-stop" title="Stop Global (Futuro)"></i>
|
||||
<button id="record-btn" class="transport-btn" title="Gravar">
|
||||
<i class="fa-solid fa-circle-dot"></i>
|
||||
<span style="margin-left: 8px">Gravar</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
|
@ -228,6 +226,10 @@
|
|||
<i class="fa-solid fa-users"></i>
|
||||
<span style="margin-left: 8px">Criar Sala</span>
|
||||
</button>
|
||||
<button id="toggle-mixer-btn" class="control-btn" title="Abrir Mixer (Futuro)">
|
||||
<i class="fa-solid fa-sliders"></i>
|
||||
<span style="margin-left: 8px">Abrir Mixer</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="control-group master-controls">
|
||||
|
|
@ -248,54 +250,50 @@
|
|||
|
||||
<main class="main-content">
|
||||
<div class="beat-editor">
|
||||
<div class="editor-header">
|
||||
Mostrar/esconder Editor de Bases
|
||||
<div class="window-controls">
|
||||
<i class="fa-solid fa-minus"></i
|
||||
><i class="fa-regular fa-square"></i
|
||||
><i class="fa-solid fa-xmark"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-toolbar">
|
||||
<div class="playback-controls">
|
||||
<i class="fa-solid fa-play" id="play-btn" title="Play/Pause"></i>
|
||||
<i class="fa-solid fa-stop" id="stop-btn" title="Stop"></i>
|
||||
</div>
|
||||
<div class="pattern-manager">
|
||||
<h2 id="beat-bassline-title"></h2>
|
||||
<select
|
||||
id="global-pattern-selector"
|
||||
class="pattern-selector"
|
||||
disabled
|
||||
>
|
||||
<option>Selecione uma faixa</option>
|
||||
</select>
|
||||
<button id="add-pattern-btn" class="pattern-btn">+</button>
|
||||
<button id="remove-pattern-btn" class="pattern-btn">-</button>
|
||||
<div class="editor-header">
|
||||
<div class="toolbar-group">
|
||||
<button id="play-btn" class="control-btn" title="Play Patterns"><i class="fa-solid fa-play"></i></button>
|
||||
<button id="stop-btn" class="control-btn" title="Stop"><i class="fa-solid fa-stop"></i></button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button
|
||||
id="send-pattern-to-playlist-btn"
|
||||
class="pattern-btn"
|
||||
title="Enviar Pattern para a Playlist"
|
||||
style="width: auto; padding: 0 8px; font-size: 0.9rem"
|
||||
>
|
||||
<i class="fa-solid fa-arrow-right-to-bracket"></i> Enviar
|
||||
</button>
|
||||
</div>
|
||||
<select id="global-pattern-selector" title="Selecionar Pattern Ativo">
|
||||
<option value="0">Pattern 1</option>
|
||||
<option value="1">Pattern 2</option>
|
||||
<option value="2">Pattern 3</option>
|
||||
<option value="3">Pattern 4</option>
|
||||
</select>
|
||||
|
||||
<button class="control-btn" id="add-pattern-btn" title="Novo Pattern (Não implementado no JS ainda)"><i class="fa-solid fa-plus"></i></button>
|
||||
<button class="control-btn" id="remove-pattern-btn" title="Remover Pattern (Não implementado no JS ainda)"><i class="fa-solid fa-minus"></i></button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button id="send-pattern-to-playlist-btn" class="control-btn" title="Renderizar para Áudio">
|
||||
<i class="fa-solid fa-arrow-right-to-bracket"></i> <span style="margin-left:5px;">Enviar p/ Playlist</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button id="add-instrument-btn" class="control-btn" title="Adicionar Instrumento">
|
||||
<i class="fa-solid fa-music"></i> <i class="fa-solid fa-plus" style="font-size: 0.7em; margin-left: 3px;"></i>
|
||||
<span style="margin-left: 8px">Adicionar Instrumento</span>
|
||||
</button>
|
||||
<button id="remove-instrument-btn" class="control-btn" title="Remover Instrumento Selecionado">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<span style="margin-left: 8px">Remover Instrumento</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sequencer-grid" class="sequencer-container">
|
||||
</div>
|
||||
<div class="tool-icons">
|
||||
<i class="fa-solid fa-table-cells"></i
|
||||
><i class="fa-solid fa-bars-staggered"></i>
|
||||
<i class="fa-solid fa-music" id="open-piano-roll-btn" title="Abrir Piano Roll"></i>
|
||||
<i
|
||||
class="fa-solid fa-wave-square"
|
||||
id="bounce-pattern-btn"
|
||||
title="Renderizar Pattern para Pista de Áudio"
|
||||
></i>
|
||||
<i
|
||||
class="fa-solid fa-plus"
|
||||
id="add-bar-btn"
|
||||
title="Adicionar 1 Compasso"
|
||||
></i>
|
||||
</div>
|
||||
<div id="timeline-context-menu">
|
||||
<div id="copy-clip">Copiar</div>
|
||||
|
|
@ -311,176 +309,110 @@
|
|||
<div id="ruler-set-loop-start">Definir Início do Loop</div>
|
||||
<div id="ruler-set-loop-end">Definir Fim do Loop</div>
|
||||
</div>
|
||||
<div class="zoom-controls">
|
||||
<i class="fa-solid fa-minus" id="remove-instrument-btn"></i
|
||||
><i class="fa-solid fa-plus" id="add-instrument-btn"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="track-container"></div>
|
||||
</div>
|
||||
<div class="piano-roll-editor" id="piano-roll-editor" style="display: none;">
|
||||
<div class="editor-header">
|
||||
<span>Piano Roll - <span id="piano-roll-instrument-name">Instrumento 1</span></span>
|
||||
<div class="window-controls">
|
||||
<i class="fa-solid fa-xmark" id="close-piano-roll-btn"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piano-roll-toolbar">
|
||||
<div class="playback-controls">
|
||||
<i class="fa-solid fa-pencil active" title="Draw Tool"></i>
|
||||
<i class="fa-solid fa-eraser" title="Erase Tool"></i>
|
||||
</div>
|
||||
<div class="snap-controls">
|
||||
<label>Snap:</label>
|
||||
<select>
|
||||
<option>1/4</option>
|
||||
<option>1/8</option>
|
||||
<option selected>1/16</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piano-roll-workspace">
|
||||
<div class="piano-keys-container" id="piano-keys-container">
|
||||
<canvas id="piano-keys-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="piano-grid-container" id="piano-grid-container">
|
||||
<canvas id="piano-grid-canvas"></canvas>
|
||||
<div class="piano-roll-editor" id="piano-roll-editor" style="display: none;">
|
||||
<div class="editor-header">
|
||||
<span>Piano Roll - <span id="piano-roll-instrument-name">Instrumento 1</span></span>
|
||||
<div class="window-controls">
|
||||
<i class="fa-solid fa-xmark" id="close-piano-roll-btn"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piano-roll-toolbar">
|
||||
<div class="playback-controls">
|
||||
<i class="fa-solid fa-pencil active" title="Draw Tool"></i>
|
||||
<i class="fa-solid fa-eraser" title="Erase Tool"></i>
|
||||
</div>
|
||||
<div class="snap-controls">
|
||||
<label>Snap:</label>
|
||||
<select>
|
||||
<option>1/4</option>
|
||||
<option>1/8</option>
|
||||
<option selected>1/16</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piano-roll-workspace">
|
||||
<div class="piano-keys-container" id="piano-keys-container">
|
||||
<canvas id="piano-keys-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="piano-grid-container" id="piano-grid-container">
|
||||
<canvas id="piano-grid-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="audio-editor">
|
||||
<div class="editor-header">
|
||||
<span>Editor de Amostras de Áudio</span>
|
||||
<div class="editor-header" id="audio-editor-header">
|
||||
<div class="toolbar-group">
|
||||
<span class="panel-title" style="margin-right: 10px;"><i class="fa-solid fa-wave-square"></i> Playlist</span>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<div class="playback-controls">
|
||||
<i
|
||||
class="fa-solid fa-search-minus"
|
||||
id="zoom-out-btn"
|
||||
title="Zoom Out"
|
||||
></i>
|
||||
<i
|
||||
class="fa-solid fa-search-plus"
|
||||
id="zoom-in-btn"
|
||||
title="Zoom In"
|
||||
></i>
|
||||
<i
|
||||
class="fa-solid fa-scissors"
|
||||
id="slice-tool-btn"
|
||||
title="Ferramenta de Corte"
|
||||
></i>
|
||||
<button id="audio-editor-play-btn" class="control-btn" title="Play Playlist">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
</button>
|
||||
<button id="audio-editor-stop-btn" class="control-btn" title="Stop Playlist">
|
||||
<i class="fa-solid fa-stop"></i>
|
||||
</button>
|
||||
<button id="audio-editor-loop-btn" class="control-btn" title="Loop Mode">
|
||||
<i class="fa-solid fa-repeat"></i>
|
||||
<span style="margin-left: 8px">Loop</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<i
|
||||
class="fa-solid fa-arrows-left-right-to-line"
|
||||
id="resize-tool-trim"
|
||||
title="Modo de Redimensionamento (Aparar/Trimming)"
|
||||
></i>
|
||||
<i
|
||||
class="fa-solid fa-arrows-left-right"
|
||||
id="resize-tool-stretch"
|
||||
title="Modo de Redimensionamento (Esticar/Time Stretch)"
|
||||
></i>
|
||||
<i
|
||||
class="fa-solid fa-play"
|
||||
id="audio-editor-play-btn"
|
||||
title="Play/Pause"
|
||||
></i>
|
||||
<i
|
||||
class="fa-solid fa-stop"
|
||||
id="audio-editor-stop-btn"
|
||||
title="Stop"
|
||||
></i>
|
||||
<i
|
||||
class="fa-solid fa-repeat"
|
||||
id="audio-editor-loop-btn"
|
||||
title="Ativar/Desativar Loop"
|
||||
></i>
|
||||
<button
|
||||
id="sync-mode-btn"
|
||||
class="control-btn active"
|
||||
title="Modo de Sincronia de Playback (Global/Local)"
|
||||
>
|
||||
Global
|
||||
</button>
|
||||
<i
|
||||
class="fa-solid fa-plus"
|
||||
id="add-audio-track-btn"
|
||||
title="Adicionar Pista de Áudio"
|
||||
></i>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="slice-tool-btn" class="control-btn" title="Ferramenta Corte (Slice)">
|
||||
<i class="fa-solid fa-scissors"></i>
|
||||
<span style="margin-left: 8px">Cortar</span>
|
||||
</button>
|
||||
<button id="resize-tool-trim" class="control-btn active" title="Redimensionar (Trim)">
|
||||
<i class="fa-solid fa-arrows-left-right-to-line"></i>
|
||||
<span style="margin-left: 8px">Redimensionar</span>
|
||||
</button>
|
||||
<button id="resize-tool-stretch" class="control-btn" title="Esticar (Stretch)">
|
||||
<i class="fa-solid fa-expand"></i>
|
||||
<span style="margin-left: 8px">Esticar</span>
|
||||
</button>
|
||||
<button id="delete-clip" class="control-btn" title="Excluir Clip Selecionado">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<span style="margin-left: 8px">Rem. Instrumento</span>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button id="zoom-out-btn" class="control-btn" title="Zoom Out">
|
||||
<i class="fa-solid fa-magnifying-glass-minus"></i>
|
||||
</button>
|
||||
<button id="zoom-in-btn" class="control-btn" title="Zoom In">
|
||||
<i class="fa-solid fa-magnifying-glass-plus"></i>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button id="add-audio-track-btn" class="control-btn" title="Adicionar Pista">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span style="margin-left: 8px">Add Track</span>
|
||||
</button>
|
||||
<button id="remove-audio-track-btn" class="control-btn" title="Remover Última Pista">
|
||||
<i class="fa-solid fa-minus"></i>
|
||||
<span style="margin-left: 8px">Rem. Track</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="audio-track-container">
|
||||
<div class="audio-track-lane">
|
||||
<div class="track-info">
|
||||
<div class="track-info-header">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<span class="track-name">Pista de Áudio 1</span>
|
||||
<div class="track-mute"></div>
|
||||
</div>
|
||||
<div class="track-controls">
|
||||
<div class="knob-container">
|
||||
<div class="knob" data-control="volume">
|
||||
<div class="knob-indicator"></div>
|
||||
</div>
|
||||
<span>VOL</span>
|
||||
</div>
|
||||
<div class="knob-container">
|
||||
<div class="knob" data-control="pan">
|
||||
<div class="knob-indicator"></div>
|
||||
</div>
|
||||
<span>PAN</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-container">
|
||||
<div class="spectrogram-view-grid" style="width: 4000px">
|
||||
<div
|
||||
class="timeline-clip"
|
||||
style="left: 100px; width: 400px"
|
||||
></div>
|
||||
<div class="playhead"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="audio-track-lane">
|
||||
<div class="track-info">
|
||||
<div class="track-info-header">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<span class="track-name">Pista de Áudio 2</span>
|
||||
<div class="track-mute"></div>
|
||||
</div>
|
||||
<div class="track-controls">
|
||||
<div class="knob-container">
|
||||
<div class="knob" data-control="volume">
|
||||
<div class="knob-indicator"></div>
|
||||
</div>
|
||||
<span>VOL</span>
|
||||
</div>
|
||||
<div class="knob-container">
|
||||
<div class="knob" data-control="pan">
|
||||
<div class="knob-indicator"></div>
|
||||
</div>
|
||||
<span>PAN</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline-container">
|
||||
<div id="loop-region" class="loop-region">
|
||||
<div class="spectrogram-view-grid" style="width: 4000px">
|
||||
<div class="timeline-clip" style="left: 50px; width: 600px">
|
||||
<div class="clip-name">jungle01.ogg</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playhead"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audio-tracks-wrapper">
|
||||
<div id="audio-timeline-ruler" class="timeline-ruler"></div>
|
||||
<div id="loop-region" class="loop-region"></div>
|
||||
<div id="playhead" class="playhead"></div>
|
||||
<div id="audio-track-container" class="track-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<input
|
||||
|
|
@ -743,4 +675,4 @@
|
|||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||
<script src="assets/js/creations/socket.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Loading…
Reference in New Issue