Padronização da interface e atualização do sync entre clientes
This commit is contained in:
1 parent
3f714c88de
commit
7d9d2b0f63
10 files changed
+796
-377
No files matched your search
@@ -8,3 +8,4 @@ mmp
|
|||||||
venv
|
venv
|
||||||
.bundle
|
.bundle
|
||||||
src
|
src
|
||||||
|
assets/js/creations/server/data
|
||||||
+104
-3
@@ -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;
|
color: var(--text-light); width: 25px; height: 40px; cursor: pointer;
|
||||||
border-radius: 0 4px 4px 0; display: flex; align-items: center; justify-content: center;
|
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
|
/* Á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 { 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; }
|
.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; }
|
.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; }
|
/* --- CORREÇÃO DO COMPASSO --- */
|
||||||
.compasso-separator { color: var(--accent-green); font-weight: 700; font-size: 1.4rem; font-family: Courier New, Courier, monospace; margin: 0 2px; }
|
|
||||||
|
/* 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::-webkit-outer-spin-button, .value-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
||||||
.value-input[type=number] { -moz-appearance: textfield; }
|
.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; }
|
.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;
|
border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, .3); overflow: hidden;
|
||||||
display: flex; flex-direction: column;
|
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; }
|
.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; }
|
#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;
|
border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, .3); overflow: hidden;
|
||||||
display: flex; flex-direction: column; --track-info-width: 255px;
|
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-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 { 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; }
|
.audio-track-lane.drag-over { background-color: #40454d; }
|
||||||
|
|||||||
@@ -23,23 +23,43 @@ import { sendAction } from "../socket.js";
|
|||||||
|
|
||||||
export function renderAudioEditor() {
|
export function renderAudioEditor() {
|
||||||
const audioEditor = document.querySelector(".audio-editor");
|
const audioEditor = document.querySelector(".audio-editor");
|
||||||
const existingTrackContainer = document.getElementById(
|
const existingTrackContainer = document.getElementById("audio-track-container");
|
||||||
"audio-track-container"
|
|
||||||
);
|
|
||||||
if (!audioEditor || !existingTrackContainer) return;
|
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 ---
|
// --- 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) {
|
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 = document.createElement("div");
|
||||||
rulerWrapper.className = "ruler-wrapper";
|
rulerWrapper.className = "ruler-wrapper";
|
||||||
rulerWrapper.innerHTML = `
|
rulerWrapper.innerHTML = `
|
||||||
<div class="ruler-spacer"></div>
|
<div class="ruler-spacer"></div>
|
||||||
<div class="timeline-ruler"></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");
|
const ruler = rulerWrapper.querySelector(".timeline-ruler");
|
||||||
ruler.innerHTML = "";
|
ruler.innerHTML = "";
|
||||||
|
|
||||||
@@ -243,7 +263,9 @@ export function renderAudioEditor() {
|
|||||||
|
|
||||||
// Recriação Container Pistas (sem alterações)
|
// Recriação Container Pistas (sem alterações)
|
||||||
const newTrackContainer = existingTrackContainer.cloneNode(false);
|
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)
|
// Render Pistas (sem alterações)
|
||||||
appState.audio.tracks.forEach((trackData) => {
|
appState.audio.tracks.forEach((trackData) => {
|
||||||
@@ -725,7 +747,6 @@ export function renderAudioEditor() {
|
|||||||
const constrainedLeftPx = Math.max(0, newLeftPx);
|
const constrainedLeftPx = Math.max(0, newLeftPx);
|
||||||
let newStartTime = constrainedLeftPx / currentPixelsPerSecond;
|
let newStartTime = constrainedLeftPx / currentPixelsPerSecond;
|
||||||
newStartTime = quantizeTime(newStartTime);
|
newStartTime = quantizeTime(newStartTime);
|
||||||
// (Correção Bug 4 - remove Number())
|
|
||||||
updateAudioClipProperties(clipId, {
|
updateAudioClipProperties(clipId, {
|
||||||
trackId: newTrackId,
|
trackId: newTrackId,
|
||||||
startTimeInSeconds: newStartTime,
|
startTimeInSeconds: newStartTime,
|
||||||
@@ -756,13 +777,8 @@ export function renderAudioEditor() {
|
|||||||
const clickX = event.clientX - rect.left;
|
const clickX = event.clientX - rect.left;
|
||||||
const absoluteX = clickX + scrollLeft;
|
const absoluteX = clickX + scrollLeft;
|
||||||
const newTime = absoluteX / currentPixelsPerSecond;
|
const newTime = absoluteX / currentPixelsPerSecond;
|
||||||
// =================================================================
|
// Sincronia de Seek na Pista)
|
||||||
// 👇 INÍCIO DA CORREÇÃO (Sincronia de Seek na Pista)
|
|
||||||
// =================================================================
|
|
||||||
sendAction({ type: "SET_SEEK_TIME", seekTime: newTime });
|
sendAction({ type: "SET_SEEK_TIME", seekTime: newTime });
|
||||||
// seekAudioEditor(newTime); // 👈 Substituído
|
|
||||||
// =================================================================
|
|
||||||
// 👆 FIM DA CORREÇÃO
|
|
||||||
};
|
};
|
||||||
handleSeek(e); // Aplica no mousedown
|
handleSeek(e); // Aplica no mousedown
|
||||||
const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent);
|
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)
|
// (Adicionada ao final de audio_ui.js)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -907,5 +923,4 @@ function createPatternViewElement(patternData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
// --- FIM DA NOVA FUNÇÃO ---
|
|
||||||
+23
-31
@@ -1,4 +1,6 @@
|
|||||||
// js/file.js
|
//--------------------------------------------------------------
|
||||||
|
// IMPORTS NECESSÁRIOS
|
||||||
|
//--------------------------------------------------------------
|
||||||
import { appState, saveStateToSession, resetProjectState, loadStateFromSession } from "./state.js";
|
import { appState, saveStateToSession, resetProjectState, loadStateFromSession } from "./state.js";
|
||||||
import { loadAudioForTrack } from "./pattern/pattern_state.js";
|
import { loadAudioForTrack } from "./pattern/pattern_state.js";
|
||||||
import { renderAll, getSamplePathMap } from "./ui.js";
|
import { renderAll, getSamplePathMap } from "./ui.js";
|
||||||
@@ -7,21 +9,13 @@ import {
|
|||||||
initializeAudioContext,
|
initializeAudioContext,
|
||||||
getMainGainNode,
|
getMainGainNode,
|
||||||
} from "./audio.js";
|
} from "./audio.js";
|
||||||
|
import { DEFAULT_PROJECT_XML } from "./utils.js"
|
||||||
import * as Tone from "https://esm.sh/tone";
|
import * as Tone from "https://esm.sh/tone";
|
||||||
|
|
||||||
import { sendAction } from "./socket.js";
|
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() {
|
export function handleLocalProjectReset() {
|
||||||
console.log("Recebido comando de reset. Limpando estado local...");
|
console.log("Recebido comando de reset. Limpando estado local...");
|
||||||
@@ -89,7 +83,7 @@ export async function loadProjectFromServer(fileName) {
|
|||||||
export async function parseMmpContent(xmlString) {
|
export async function parseMmpContent(xmlString) {
|
||||||
resetProjectState();
|
resetProjectState();
|
||||||
initializeAudioContext();
|
initializeAudioContext();
|
||||||
appState.global.justReset = xmlString === BLANK_PROJECT_XML;
|
appState.global.justReset = xmlString === DEFAULT_PROJECT_XML;
|
||||||
|
|
||||||
const audioContainer = document.getElementById("audio-track-container");
|
const audioContainer = document.getElementById("audio-track-container");
|
||||||
if (audioContainer) {
|
if (audioContainer) {
|
||||||
@@ -305,10 +299,7 @@ export async function parseMmpContent(xmlString) {
|
|||||||
appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id || null;
|
appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id || null;
|
||||||
appState.pattern.activePatternIndex = 0;
|
appState.pattern.activePatternIndex = 0;
|
||||||
|
|
||||||
// --- SUBSTUIÇÃO DO BLOCO DE RESTAURAÇÃO ---
|
|
||||||
// Em vez daquele bloco try/catch gigante, apenas chamamos a função:
|
|
||||||
loadStateFromSession();
|
loadStateFromSession();
|
||||||
// ------------------------------------------
|
|
||||||
|
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
renderAll();
|
renderAll();
|
||||||
@@ -324,17 +315,19 @@ export function generateMmpFile() {
|
|||||||
|
|
||||||
function generateXmlFromState() {
|
function generateXmlFromState() {
|
||||||
if (!appState.global.originalXmlDoc) {
|
if (!appState.global.originalXmlDoc) {
|
||||||
console.warn("Não há XML original. Retornando vazio.");
|
console.log("Gerando XML a partir do template em branco...");
|
||||||
return "";
|
const parser = new DOMParser();
|
||||||
|
appState.global.originalXmlDoc = parser.parseFromString(DEFAULT_PROJECT_XML, "application/xml");
|
||||||
}
|
}
|
||||||
|
|
||||||
const xmlDoc = appState.global.originalXmlDoc.cloneNode(true);
|
const xmlDoc = appState.global.originalXmlDoc.cloneNode(true);
|
||||||
const head = xmlDoc.querySelector("head");
|
const head = xmlDoc.querySelector("head");
|
||||||
|
|
||||||
if (head) {
|
if (head) {
|
||||||
head.setAttribute("bpm", document.getElementById("bpm-input").value);
|
head.setAttribute("bpm", document.getElementById("bpm-input").value || 140);
|
||||||
head.setAttribute("num_bars", document.getElementById("bars-input").value);
|
head.setAttribute("num_bars", document.getElementById("bars-input").value || 1);
|
||||||
head.setAttribute("timesig_numerator", document.getElementById("compasso-a-input").value);
|
head.setAttribute("timesig_numerator", document.getElementById("compasso-a-input").value || 4);
|
||||||
head.setAttribute("timesig_denominator", document.getElementById("compasso-b-input").value);
|
head.setAttribute("timesig_denominator", document.getElementById("compasso-b-input").value || 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bbTrackContainer = xmlDoc.querySelector('track[type="1"] > bbtrack > trackcontainer');
|
const bbTrackContainer = xmlDoc.querySelector('track[type="1"] > bbtrack > trackcontainer');
|
||||||
@@ -344,7 +337,6 @@ function generateXmlFromState() {
|
|||||||
.map((track) => createTrackXml(track))
|
.map((track) => createTrackXml(track))
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
// Gambiarra para inserir o XML gerado como nós DOM
|
|
||||||
const tempDoc = new DOMParser().parseFromString(`<root>${tracksXml}</root>`, "application/xml");
|
const tempDoc = new DOMParser().parseFromString(`<root>${tracksXml}</root>`, "application/xml");
|
||||||
Array.from(tempDoc.documentElement.children).forEach((newTrackNode) => {
|
Array.from(tempDoc.documentElement.children).forEach((newTrackNode) => {
|
||||||
bbTrackContainer.appendChild(newTrackNode);
|
bbTrackContainer.appendChild(newTrackNode);
|
||||||
@@ -369,21 +361,22 @@ function createTrackXml(track) {
|
|||||||
const lmmsVolume = Math.round(track.volume * 100);
|
const lmmsVolume = Math.round(track.volume * 100);
|
||||||
const lmmsPan = Math.round(track.pan * 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) => {
|
const patternsXml = track.patterns.map((pattern) => {
|
||||||
let patternNotesXml = "";
|
let patternNotesXml = "";
|
||||||
|
|
||||||
// SE for plugin e tiver notas detalhadas, usa elas
|
|
||||||
if (track.type === "plugin" && pattern.notes && pattern.notes.length > 0) {
|
if (track.type === "plugin" && pattern.notes && pattern.notes.length > 0) {
|
||||||
patternNotesXml = pattern.notes.map(note => {
|
patternNotesXml = pattern.notes.map(note => {
|
||||||
return `<note vol="${note.vol}" len="${note.len}" pos="${note.pos}" pan="${note.pan}" key="${note.key}"/>`;
|
return `<note vol="${note.vol}" len="${note.len}" pos="${note.pos}" pan="${note.pan}" key="${note.key}"/>`;
|
||||||
}).join("\n ");
|
}).join("\n ");
|
||||||
}
|
}
|
||||||
// SE for sampler (ou plugin sem notas detalhadas), usa os steps convertidos em notas simples
|
|
||||||
else {
|
else {
|
||||||
patternNotesXml = pattern.steps.map((isActive, index) => {
|
patternNotesXml = pattern.steps.map((isActive, index) => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
const notePos = Math.round(index * ticksPerStep);
|
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 `<note vol="100" len="${NOTE_LENGTH}" pos="${notePos}" pan="0" key="57"/>`;
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
@@ -398,8 +391,8 @@ function createTrackXml(track) {
|
|||||||
return `
|
return `
|
||||||
<track type="0" solo="0" muted="0" name="${track.name}">
|
<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}">
|
<instrumenttrack vol="${lmmsVolume}" pitch="0" fxch="0" pitchrange="1" basenote="57" usemasterpitch="1" pan="${lmmsPan}">
|
||||||
<instrument name="${track.instrumentName}">
|
<instrument name="${instrName}">
|
||||||
${track.instrumentXml}
|
${instrXml}
|
||||||
</instrument>
|
</instrument>
|
||||||
<fxchain enabled="0" numofeffects="0"/>
|
<fxchain enabled="0" numofeffects="0"/>
|
||||||
</instrumenttrack>
|
</instrumenttrack>
|
||||||
@@ -461,5 +454,4 @@ function downloadFile(content, fileName) {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { generateXmlFromState as generateXmlFromStateExported };
|
export { generateXmlFromState as generateXmlFromStateExported };
|
||||||
export { BLANK_PROJECT_XML }; // Mantenha o que já estava
|
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
restartAudioEditorIfPlaying,
|
restartAudioEditorIfPlaying,
|
||||||
} from "./audio/audio_audio.js";
|
} from "./audio/audio_audio.js";
|
||||||
import { initializeAudioContext } from "./audio.js";
|
import { initializeAudioContext } from "./audio.js";
|
||||||
import { handleFileLoad, generateMmpFile, BLANK_PROJECT_XML } from "./file.js";
|
import { handleFileLoad, generateMmpFile } from "./file.js";
|
||||||
import {
|
import {
|
||||||
renderAll,
|
renderAll,
|
||||||
loadAndRenderSampleBrowser,
|
loadAndRenderSampleBrowser,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
closeOpenProjectModal,
|
closeOpenProjectModal,
|
||||||
} from "./ui.js";
|
} from "./ui.js";
|
||||||
import { renderAudioEditor } from "./audio/audio_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 { ZOOM_LEVELS } from "./config.js";
|
||||||
import { loadProjectFromServer } from "./file.js";
|
import { loadProjectFromServer } from "./file.js";
|
||||||
import { sendAction, joinRoom, setUserName } from "./socket.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
|
||||||
: `${PROJECT_NAME}.mmp`;
|
: `${PROJECT_NAME}.mmp`;
|
||||||
loadProjectFromServer(filename);
|
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
|
})(); // Fim do initApp
|
||||||
@@ -313,7 +318,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
sendAction({ type: "LOAD_PROJECT", xml: BLANK_PROJECT_XML });
|
sendAction({ type: "LOAD_PROJECT", xml: DEFAULT_PROJECT_XML });
|
||||||
});
|
});
|
||||||
|
|
||||||
addBarBtn?.addEventListener("click", () => {
|
addBarBtn?.addEventListener("click", () => {
|
||||||
@@ -341,6 +346,29 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
initializeAudioContext();
|
initializeAudioContext();
|
||||||
sendAction({ type: "REMOVE_LAST_TRACK" });
|
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", () => {
|
playBtn?.addEventListener("click", () => {
|
||||||
initializeAudioContext();
|
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)
|
// Navegador de Samples (local)
|
||||||
loadAndRenderSampleBrowser();
|
loadAndRenderSampleBrowser();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// js/pattern_state.js
|
// js/pattern/pattern_state.js
|
||||||
import * as Tone from "https://esm.sh/tone";
|
import * as Tone from "https://esm.sh/tone";
|
||||||
import { TripleOscillator } from "../../audio/plugins/TripleOscillator.js";
|
import { TripleOscillator } from "../../audio/plugins/TripleOscillator.js";
|
||||||
import { Kicker } from "../../audio/plugins/Kicker.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 { getMainGainNode } from "../audio.js";
|
||||||
import { getTotalSteps } from "../utils.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() {
|
export function initializePatternState() {
|
||||||
appState.pattern.tracks.forEach(track => {
|
appState.pattern.tracks.forEach(track => {
|
||||||
try { track.player?.dispose(); } catch {}
|
try { track.player?.dispose(); } catch {}
|
||||||
@@ -55,20 +60,17 @@ export async function loadAudioForTrack(track) {
|
|||||||
|
|
||||||
// --- DETECÇÃO DE TIPO DE ARQUIVO ---
|
// --- DETECÇÃO DE TIPO DE ARQUIVO ---
|
||||||
// Verifica se é um formato de áudio que o navegador suporta
|
// 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 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) {
|
if (!track.samplePath || !isStandardAudio) {
|
||||||
try {
|
try {
|
||||||
if (track.instrument) { try { track.instrument.dispose(); } catch {} }
|
if (track.instrument) { try { track.instrument.dispose(); } catch {} }
|
||||||
|
|
||||||
let synth;
|
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 = {};
|
const pluginData = {};
|
||||||
|
|
||||||
// SELETOR DE PLUGINS
|
// SELETOR DE PLUGINS
|
||||||
@@ -87,64 +89,53 @@ export async function loadAudioForTrack(track) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "nes":
|
case "nes":
|
||||||
case "freeboy": // Freeboy é parecido com NES
|
case "freeboy":
|
||||||
case "papu": // Papu também é Gameboy
|
case "papu":
|
||||||
case "sid": // SID é 8-bit também, usaremos NES como fallback por enquanto
|
case "sid":
|
||||||
synth = new Nes(Tone.getContext(), pluginData);
|
synth = new Nes(Tone.getContext(), pluginData);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// --- PACOTE SUPER SAW ---
|
case "zynaddsubfx":
|
||||||
case "zynaddsubfx": // O clássico
|
case "watsyn":
|
||||||
case "watsyn": // Wavetable Synth
|
case "monstro":
|
||||||
case "monstro": // 3 Osciladores monstruosos
|
case "vibedstrings":
|
||||||
case "vibedstrings": // Strings vibrantes (fatsaw funciona bem como base)
|
case "supersaw":
|
||||||
case "SuperSaw":
|
|
||||||
synth = new SuperSaw(Tone.getContext(), pluginData);
|
synth = new SuperSaw(Tone.getContext(), pluginData);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "organic": // Fallback simples para Organic (Additive)
|
case "organic":
|
||||||
synth = new Tone.PolySynth(Tone.Synth, {
|
synth = new Tone.PolySynth(Tone.Synth, {
|
||||||
oscillator: { type: "sine", count: 8, spread: 20 }
|
oscillator: { type: "sine", count: 8, spread: 20 }
|
||||||
});
|
});
|
||||||
break;
|
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:
|
default:
|
||||||
console.warn(`Plugin ${name} desconhecido, usando fallback.`);
|
console.warn(`Plugin ${name} desconhecido, usando fallback (Kicker).`);
|
||||||
// Fallback genérico
|
// Fallback seguro: Kicker
|
||||||
synth = new Tone.PolySynth(Tone.Synth, { oscillator: { type: "triangle" } });
|
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) {
|
if (synth.output) {
|
||||||
// Nossas classes customizadas
|
|
||||||
synth.connect(track.volumeNode);
|
synth.connect(track.volumeNode);
|
||||||
} else {
|
} else {
|
||||||
// Objetos Tone.js puros
|
|
||||||
synth.connect(track.volumeNode);
|
synth.connect(track.volumeNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
track.instrument = synth;
|
track.instrument = synth;
|
||||||
track.player = null;
|
track.player = null;
|
||||||
track.type = 'plugin';
|
track.type = 'plugin';
|
||||||
|
// Atualiza o nome se ele estava vazio
|
||||||
|
if (!track.instrumentName) track.instrumentName = name;
|
||||||
|
|
||||||
console.log(`[Audio] Plugin carregado: ${name}`);
|
console.log(`[Audio] Plugin carregado: ${name}`);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Erro ao carregar plugin:", name, e);
|
console.error("Erro ao carregar plugin:", track.instrumentName, e);
|
||||||
}
|
}
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Lógica para SAMPLERS (Arquivos de Áudio Reais)
|
// 3. Lógica para SAMPLERS
|
||||||
try {
|
try {
|
||||||
try { track.player?.dispose(); } catch {}
|
try { track.player?.dispose(); } catch {}
|
||||||
track.player = null;
|
track.player = null;
|
||||||
@@ -166,7 +157,7 @@ export async function loadAudioForTrack(track) {
|
|||||||
|
|
||||||
track.player = player;
|
track.player = player;
|
||||||
track.buffer = buffer;
|
track.buffer = buffer;
|
||||||
track.type = 'sampler'; // Garante o tipo correto
|
track.type = 'sampler';
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao carregar sample:', track.samplePath);
|
console.error('Erro ao carregar sample:', track.samplePath);
|
||||||
@@ -181,21 +172,29 @@ export async function loadAudioForTrack(track) {
|
|||||||
export function addTrackToState() {
|
export function addTrackToState() {
|
||||||
const totalSteps = getTotalSteps();
|
const totalSteps = getTotalSteps();
|
||||||
const referenceTrack = appState.pattern.tracks[0];
|
const referenceTrack = appState.pattern.tracks[0];
|
||||||
|
const newId = Date.now() + Math.random();
|
||||||
|
|
||||||
const newTrack = {
|
const newTrack = {
|
||||||
id: Date.now() + Math.random(),
|
id: newId,
|
||||||
name: "novo instrumento",
|
name: `Novo Instrumento ${appState.pattern.tracks.length + 1}`,
|
||||||
samplePath: null,
|
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,
|
player: null,
|
||||||
buffer: null,
|
buffer: null,
|
||||||
patterns: referenceTrack
|
patterns: referenceTrack
|
||||||
? referenceTrack.patterns.map(p => ({
|
? referenceTrack.patterns.map(p => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
steps: new Array(p.steps.length).fill(false),
|
steps: new Array(p.steps.length).fill(false),
|
||||||
|
notes: [],
|
||||||
pos: p.pos
|
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,
|
activePatternIndex: 0,
|
||||||
volume: DEFAULT_VOLUME,
|
volume: DEFAULT_VOLUME,
|
||||||
pan: DEFAULT_PAN,
|
pan: DEFAULT_PAN,
|
||||||
@@ -206,10 +205,39 @@ export function addTrackToState() {
|
|||||||
newTrack.volumeNode.connect(newTrack.pannerNode);
|
newTrack.volumeNode.connect(newTrack.pannerNode);
|
||||||
newTrack.pannerNode.connect(getMainGainNode());
|
newTrack.pannerNode.connect(getMainGainNode());
|
||||||
|
|
||||||
|
// Carrega o áudio (vai cair no case "kicker" do loadAudioForTrack)
|
||||||
loadAudioForTrack(newTrack);
|
loadAudioForTrack(newTrack);
|
||||||
|
|
||||||
appState.pattern.tracks.push(newTrack);
|
appState.pattern.tracks.push(newTrack);
|
||||||
appState.pattern.activeTrackId = newTrack.id;
|
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() {
|
export function removeLastTrackFromState() {
|
||||||
|
|||||||
@@ -9,7 +9,87 @@ const { Server } = require("socket.io");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const pino = require("pino");
|
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)
|
// LOGGER DINÂMICO (GERENCIADOR)
|
||||||
@@ -111,7 +191,8 @@ function ensureRoom(roomName) {
|
|||||||
if (fs.existsSync(p)) {
|
if (fs.existsSync(p)) {
|
||||||
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
||||||
roomStates[roomName] = {
|
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: [] },
|
audio: j.audio || { tracks: [], clips: [] },
|
||||||
seq: j.seq || 0,
|
seq: j.seq || 0,
|
||||||
tokensSeen: new Set(),
|
tokensSeen: new Set(),
|
||||||
@@ -122,8 +203,10 @@ function ensureRoom(roomName) {
|
|||||||
console.warn(`[persist] falha ao carregar estado da sala ${roomName}:`, e);
|
console.warn(`[persist] falha ao carregar estado da sala ${roomName}:`, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cria sala NOVA na memória
|
||||||
roomStates[roomName] = {
|
roomStates[roomName] = {
|
||||||
projectXml: null,
|
// 🔥 CORREÇÃO: Inicia com o XML do LMMS completo, não mais null
|
||||||
|
projectXml: DEFAULT_PROJECT_XML,
|
||||||
audio: { tracks: [], clips: [] },
|
audio: { tracks: [], clips: [] },
|
||||||
seq: 0,
|
seq: 0,
|
||||||
tokensSeen: new Set(),
|
tokensSeen: new Set(),
|
||||||
@@ -245,9 +328,6 @@ function applyAuthoritativeAction(roomName, action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "UPDATE_AUDIO_CLIP": {
|
case "UPDATE_AUDIO_CLIP": {
|
||||||
// =================================================================
|
|
||||||
// 👇 INÍCIO DA CORREÇÃO (Blindagem do Servidor para Bug 4)
|
|
||||||
// =================================================================
|
|
||||||
if (!action.clipId || !action.props) {
|
if (!action.clipId || !action.props) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (dados base inválidos):`,
|
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (dados base inválidos):`,
|
||||||
@@ -277,9 +357,6 @@ function applyAuthoritativeAction(roomName, action) {
|
|||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// =================================================================
|
|
||||||
// 👆 FIM DA CORREÇÃO
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
const c = state.clips.find((x) => String(x.id) === String(action.clipId));
|
const c = state.clips.find((x) => String(x.id) === String(action.clipId));
|
||||||
if (c && action.props && typeof action.props === "object") {
|
if (c && action.props && typeof action.props === "object") {
|
||||||
@@ -300,6 +377,29 @@ function applyAuthoritativeAction(roomName, action) {
|
|||||||
break;
|
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:
|
default:
|
||||||
// outras ações não são da persistência do editor de áudio
|
// outras ações não são da persistência do editor de áudio
|
||||||
mutated = false;
|
mutated = false;
|
||||||
@@ -414,13 +514,19 @@ io.on("connection", (socket) => {
|
|||||||
// Carrega estado da sala
|
// Carrega estado da sala
|
||||||
const room = ensureRoom(roomName);
|
const room = ensureRoom(roomName);
|
||||||
|
|
||||||
// Persiste estado se for LOAD_PROJECT (compat)
|
// Persiste estado do Pattern (Notas/Sequenciador)
|
||||||
if (action.type === "LOAD_PROJECT" && action.xml) {
|
// Aceita tanto carregamento total quanto atualização de notas
|
||||||
room.projectXml = action.xml;
|
if ((action.type === "LOAD_PROJECT" || action.type === "SYNC_PATTERN_STATE") && action.xml) {
|
||||||
saveRoom(roomName);
|
// Proteção: Não salva se o XML for vazio (evita corromper a sala com o bug antigo)
|
||||||
console.log(
|
if (action.xml.trim().length > 0) {
|
||||||
`[broadcast_action] Estado da sala ${roomName} atualizado (LOAD_PROJECT).`
|
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
|
// Aplica ações autoritativas do editor de áudio
|
||||||
|
|||||||
+164
-57
@@ -8,6 +8,7 @@ import {
|
|||||||
addTrackToState,
|
addTrackToState,
|
||||||
removeLastTrackFromState,
|
removeLastTrackFromState,
|
||||||
updateTrackSample,
|
updateTrackSample,
|
||||||
|
removeTrackById,
|
||||||
} from "./pattern/pattern_state.js";
|
} from "./pattern/pattern_state.js";
|
||||||
import {
|
import {
|
||||||
addAudioTrackLane,
|
addAudioTrackLane,
|
||||||
@@ -37,13 +38,13 @@ import {
|
|||||||
parseMmpContent,
|
parseMmpContent,
|
||||||
handleLocalProjectReset,
|
handleLocalProjectReset,
|
||||||
syncPatternStateToServer,
|
syncPatternStateToServer,
|
||||||
BLANK_PROJECT_XML,
|
|
||||||
generateXmlFromStateExported,
|
generateXmlFromStateExported,
|
||||||
} from "./file.js";
|
} from "./file.js";
|
||||||
|
|
||||||
import { renderAll, showToast } from "./ui.js"; // showToast()
|
import { renderAll, showToast } from "./ui.js"; // showToast()
|
||||||
import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
|
import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js";
|
||||||
import { PORT_SOCK } from "./config.js";
|
import { PORT_SOCK } from "./config.js";
|
||||||
|
import { DEFAULT_PROJECT_XML } from "./utils.js"
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Gera um ID único otimista (ex: "track_1678886401000_abc123")
|
// 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
|
// 🔥 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
|
// Isso garante que suas alterações locais (BPM, steps) "ganhem" do servidor
|
||||||
if (window.ROOM_NAME) {
|
if (window.ROOM_NAME) {
|
||||||
const raw = sessionStorage.getItem(`temp_state_${window.ROOM_NAME}`);
|
const raw = sessionStorage.getItem(`temp_state_${window.ROOM_NAME}`);
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const parsed = JSON.parse(raw);
|
// Se existe 'raw', confiamos que é o estado mais recente do usuário.
|
||||||
const hasLocalAudio = parsed.audioSnapshot?.clips?.length > 0 ||
|
console.log("Re-aplicando sessão local (mesmo se vazia)...");
|
||||||
parsed.audioSnapshot?.tracks?.length > 0;
|
|
||||||
if (hasLocalAudio) {
|
|
||||||
console.log("Re-aplicando sessão local sobre o XML do servidor...");
|
|
||||||
await loadStateFromSession();
|
await loadStateFromSession();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAll();
|
renderAll();
|
||||||
@@ -513,6 +509,44 @@ async function handleActionBroadcast(action) {
|
|||||||
showToast(`▶ ${who} Play bases`, "info");
|
showToast(`▶ ${who} Play bases`, "info");
|
||||||
break;
|
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": {
|
case "STOP_PLAYBACK": {
|
||||||
setTimeout(stopPlayback, delayMs);
|
setTimeout(stopPlayback, delayMs);
|
||||||
const who = actorOf(action);
|
const who = actorOf(action);
|
||||||
@@ -551,9 +585,6 @@ async function handleActionBroadcast(action) {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 👇 INÍCIO DA CORREÇÃO (Handlers Sincronia de Loop/Seek/SyncMode)
|
|
||||||
// =================================================================
|
|
||||||
case "SET_LOOP_STATE": {
|
case "SET_LOOP_STATE": {
|
||||||
const changed =
|
const changed =
|
||||||
appState.global.isLoopActive !== !!action.isLoopActive ||
|
appState.global.isLoopActive !== !!action.isLoopActive ||
|
||||||
@@ -614,9 +645,6 @@ async function handleActionBroadcast(action) {
|
|||||||
saveStateToSession();
|
saveStateToSession();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// =================================================================
|
|
||||||
// 👆 FIM DA CORREÇÃO
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
// Estado Global
|
// Estado Global
|
||||||
case "LOAD_PROJECT": //
|
case "LOAD_PROJECT": //
|
||||||
@@ -647,17 +675,22 @@ async function handleActionBroadcast(action) {
|
|||||||
isLoadingProject = false;
|
isLoadingProject = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "SYNC_PATTERN_STATE": //
|
case "SYNC_PATTERN_STATE":
|
||||||
// Esta ação agora só será recebida de *outros* usuários,
|
// 🔥 CORREÇÃO CRÍTICA:
|
||||||
// ou quando o servidor enviar, não de você mesmo.
|
// Esta ação serve apenas para o SERVIDOR atualizar o arquivo .json no disco.
|
||||||
try {
|
// Os clientes online NÃO devem recarregar o XML, pois eles já atualizaram
|
||||||
await parseMmpContent(action.xml); //
|
// o estado via ações atômicas (TOGGLE_NOTE, ADD_TRACK, etc).
|
||||||
renderAll();
|
// Recarregar aqui causa o "resetProjectState" que mata o áudio.
|
||||||
saveStateToSession(); //
|
|
||||||
console.log("Socket: Pattern state sincronizado.");
|
console.log("Socket: XML salvo no servidor (Sync silencioso).");
|
||||||
} catch (e) {
|
|
||||||
console.error("Erro SYNC_PATTERN_STATE:", e);
|
// Se quiser garantir, salvamos apenas na sessão local do navegador
|
||||||
showToast("❌ Erro sync pattern", "error");
|
// 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;
|
break;
|
||||||
|
|
||||||
@@ -713,17 +746,55 @@ async function handleActionBroadcast(action) {
|
|||||||
renderPatternEditor();
|
renderPatternEditor();
|
||||||
const who = actorOf(action);
|
const who = actorOf(action);
|
||||||
showToast(`🥁 Faixa add por ${who}`, "info");
|
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();
|
saveStateToSession();
|
||||||
break;
|
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": {
|
case "REMOVE_LAST_TRACK": {
|
||||||
removeLastTrackFromState();
|
removeLastTrackFromState();
|
||||||
renderPatternEditor();
|
renderPatternEditor();
|
||||||
const who = actorOf(action);
|
const who = actorOf(action);
|
||||||
showToast(`❌ Faixa remov. por ${who}`, "warning");
|
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();
|
saveStateToSession();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -751,6 +822,31 @@ async function handleActionBroadcast(action) {
|
|||||||
break;
|
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":
|
case "REMOVE_AUDIO_CLIP":
|
||||||
if (removeAudioClip(action.clipId)) {
|
if (removeAudioClip(action.clipId)) {
|
||||||
appState.global.selectedClipId = null;
|
appState.global.selectedClipId = null;
|
||||||
@@ -887,47 +983,32 @@ async function handleActionBroadcast(action) {
|
|||||||
|
|
||||||
// Snapshots
|
// Snapshots
|
||||||
case "AUDIO_SNAPSHOT_REQUEST": {
|
case "AUDIO_SNAPSHOT_REQUEST": {
|
||||||
const clips = appState.audio?.clips?.length || 0,
|
// 🛑 BLOQUEADO: Agora o Servidor (backend) é quem manda o snapshot autoritativo.
|
||||||
tracks = appState.audio?.tracks?.length || 0;
|
// Se deixarmos os clientes responderem, eles vão enviar estados antigos ("zumbis").
|
||||||
const iHave = clips > 0 || tracks > 0;
|
console.log("Socket: Ignorando pedido de snapshot (Server Authoritative Mode).");
|
||||||
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();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "AUDIO_SNAPSHOT": {
|
case "AUDIO_SNAPSHOT": {
|
||||||
if (action.__target && action.__target !== socket.id) break;
|
if (action.__target && action.__target !== socket.id) break;
|
||||||
if (appState.global.justReset) {
|
|
||||||
console.warn("Socket: Snapshot de áudio ignorado (justReset=true).");
|
// Removemos a verificação "if (hasClips) break" que impedia
|
||||||
break; // Ignora o snapshot
|
// de carregar um estado vazio se o local já tivesse algo (como as faixas zumbis).
|
||||||
}
|
|
||||||
const hasClips = (appState.audio?.clips?.length || 0) > 0;
|
|
||||||
if (hasClips) break;
|
|
||||||
try {
|
try {
|
||||||
|
console.log("Socket: Aplicando Snapshot de Áudio Autoritativo...");
|
||||||
await applyAudioSnapshot(action.snapshot);
|
await applyAudioSnapshot(action.snapshot);
|
||||||
renderAll();
|
renderAll();
|
||||||
|
|
||||||
const who = actorOf(action);
|
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) {
|
} catch (e) {
|
||||||
console.error("Erro AUDIO_SNAPSHOT:", 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();
|
saveStateToSession();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1016,6 +1097,32 @@ async function handleActionBroadcast(action) {
|
|||||||
break;
|
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:
|
default:
|
||||||
console.warn("Ação desconhecida:", action.type);
|
console.warn("Ação desconhecida:", action.type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,86 @@
|
|||||||
import { appState } from './state.js';
|
import { appState } from './state.js';
|
||||||
import { PIXELS_PER_STEP, ZOOM_LEVELS } from './config.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.
|
* Helper interna para ler o BPM do input.
|
||||||
* @returns {number} O BPM atual.
|
* @returns {number} O BPM atual.
|
||||||
|
|||||||
+144
-212
@@ -84,33 +84,31 @@
|
|||||||
id="new-project-btn"
|
id="new-project-btn"
|
||||||
title="Novo Projeto"
|
title="Novo Projeto"
|
||||||
></i>
|
></i>
|
||||||
|
<span style="margin-left: 8px">Novo projeto</span>
|
||||||
<i
|
<i
|
||||||
class="fa-solid fa-folder-open"
|
class="fa-solid fa-folder-open"
|
||||||
id="open-mmp-btn"
|
id="open-mmp-btn"
|
||||||
title="Abrir Projeto do Servidor"
|
title="Abrir Projeto do Servidor"
|
||||||
></i>
|
></i>
|
||||||
|
<span style="margin-left: 8px">Abrir projetos</span>
|
||||||
<i
|
<i
|
||||||
class="fa-solid fa-save"
|
class="fa-solid fa-save"
|
||||||
id="save-mmp-btn"
|
id="save-mmp-btn"
|
||||||
title="Salvar Projeto (.mmp)"
|
title="Salvar Projeto (.mmp)"
|
||||||
></i>
|
></i>
|
||||||
|
<span style="margin-left: 8px">Salvar projeto</span>
|
||||||
<i
|
<i
|
||||||
class="fa-solid fa-upload"
|
class="fa-solid fa-upload"
|
||||||
id="upload-sample-btn"
|
id="upload-sample-btn"
|
||||||
title="Carregar Sample do Computador"
|
title="Carregar Sample do Computador"
|
||||||
></i>
|
></i>
|
||||||
|
<span style="margin-left: 8px">Enviar sample</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="control-group">
|
<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">
|
<button id="record-btn" class="transport-btn" title="Gravar">
|
||||||
<i class="fa-solid fa-circle-dot"></i>
|
<i class="fa-solid fa-circle-dot"></i>
|
||||||
|
<span style="margin-left: 8px">Gravar</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
@@ -228,6 +226,10 @@
|
|||||||
<i class="fa-solid fa-users"></i>
|
<i class="fa-solid fa-users"></i>
|
||||||
<span style="margin-left: 8px">Criar Sala</span>
|
<span style="margin-left: 8px">Criar Sala</span>
|
||||||
</button>
|
</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>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="control-group master-controls">
|
<div class="control-group master-controls">
|
||||||
@@ -248,54 +250,50 @@
|
|||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<div class="beat-editor">
|
<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="editor-toolbar">
|
||||||
<div class="playback-controls">
|
<div class="editor-header">
|
||||||
<i class="fa-solid fa-play" id="play-btn" title="Play/Pause"></i>
|
<div class="toolbar-group">
|
||||||
<i class="fa-solid fa-stop" id="stop-btn" title="Stop"></i>
|
<button id="play-btn" class="control-btn" title="Play Patterns"><i class="fa-solid fa-play"></i></button>
|
||||||
</div>
|
<button id="stop-btn" class="control-btn" title="Stop"><i class="fa-solid fa-stop"></i></button>
|
||||||
<div class="pattern-manager">
|
|
||||||
<h2 id="beat-bassline-title"></h2>
|
<div class="toolbar-divider"></div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<button
|
<select id="global-pattern-selector" title="Selecionar Pattern Ativo">
|
||||||
id="send-pattern-to-playlist-btn"
|
<option value="0">Pattern 1</option>
|
||||||
class="pattern-btn"
|
<option value="1">Pattern 2</option>
|
||||||
title="Enviar Pattern para a Playlist"
|
<option value="2">Pattern 3</option>
|
||||||
style="width: auto; padding: 0 8px; font-size: 0.9rem"
|
<option value="3">Pattern 4</option>
|
||||||
>
|
</select>
|
||||||
<i class="fa-solid fa-arrow-right-to-bracket"></i> Enviar
|
|
||||||
</button>
|
<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>
|
||||||
</div>
|
<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">
|
<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>
|
||||||
<div id="timeline-context-menu">
|
<div id="timeline-context-menu">
|
||||||
<div id="copy-clip">Copiar</div>
|
<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-start">Definir Início do Loop</div>
|
||||||
<div id="ruler-set-loop-end">Definir Fim do Loop</div>
|
<div id="ruler-set-loop-end">Definir Fim do Loop</div>
|
||||||
</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>
|
||||||
<div id="track-container"></div>
|
<div id="track-container"></div>
|
||||||
</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">
|
<div class="piano-roll-editor" id="piano-roll-editor" style="display: none;">
|
||||||
<canvas id="piano-grid-canvas"></canvas>
|
<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>
|
|
||||||
<div class="audio-editor">
|
<div class="audio-editor">
|
||||||
<div class="editor-header">
|
<div class="editor-header" id="audio-editor-header">
|
||||||
<span>Editor de Amostras de Áudio</span>
|
<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">
|
<button id="audio-editor-play-btn" class="control-btn" title="Play Playlist">
|
||||||
<i
|
<i class="fa-solid fa-play"></i>
|
||||||
class="fa-solid fa-search-minus"
|
</button>
|
||||||
id="zoom-out-btn"
|
<button id="audio-editor-stop-btn" class="control-btn" title="Stop Playlist">
|
||||||
title="Zoom Out"
|
<i class="fa-solid fa-stop"></i>
|
||||||
></i>
|
</button>
|
||||||
<i
|
<button id="audio-editor-loop-btn" class="control-btn" title="Loop Mode">
|
||||||
class="fa-solid fa-search-plus"
|
<i class="fa-solid fa-repeat"></i>
|
||||||
id="zoom-in-btn"
|
<span style="margin-left: 8px">Loop</span>
|
||||||
title="Zoom In"
|
</button>
|
||||||
></i>
|
</div>
|
||||||
<i
|
|
||||||
class="fa-solid fa-scissors"
|
|
||||||
id="slice-tool-btn"
|
|
||||||
title="Ferramenta de Corte"
|
|
||||||
></i>
|
|
||||||
|
|
||||||
<i
|
<div class="toolbar-group">
|
||||||
class="fa-solid fa-arrows-left-right-to-line"
|
<button id="slice-tool-btn" class="control-btn" title="Ferramenta Corte (Slice)">
|
||||||
id="resize-tool-trim"
|
<i class="fa-solid fa-scissors"></i>
|
||||||
title="Modo de Redimensionamento (Aparar/Trimming)"
|
<span style="margin-left: 8px">Cortar</span>
|
||||||
></i>
|
</button>
|
||||||
<i
|
<button id="resize-tool-trim" class="control-btn active" title="Redimensionar (Trim)">
|
||||||
class="fa-solid fa-arrows-left-right"
|
<i class="fa-solid fa-arrows-left-right-to-line"></i>
|
||||||
id="resize-tool-stretch"
|
<span style="margin-left: 8px">Redimensionar</span>
|
||||||
title="Modo de Redimensionamento (Esticar/Time Stretch)"
|
</button>
|
||||||
></i>
|
<button id="resize-tool-stretch" class="control-btn" title="Esticar (Stretch)">
|
||||||
<i
|
<i class="fa-solid fa-expand"></i>
|
||||||
class="fa-solid fa-play"
|
<span style="margin-left: 8px">Esticar</span>
|
||||||
id="audio-editor-play-btn"
|
</button>
|
||||||
title="Play/Pause"
|
<button id="delete-clip" class="control-btn" title="Excluir Clip Selecionado">
|
||||||
></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
<i
|
<span style="margin-left: 8px">Rem. Instrumento</span>
|
||||||
class="fa-solid fa-stop"
|
</button>
|
||||||
id="audio-editor-stop-btn"
|
|
||||||
title="Stop"
|
<div class="toolbar-divider"></div>
|
||||||
></i>
|
|
||||||
<i
|
<button id="zoom-out-btn" class="control-btn" title="Zoom Out">
|
||||||
class="fa-solid fa-repeat"
|
<i class="fa-solid fa-magnifying-glass-minus"></i>
|
||||||
id="audio-editor-loop-btn"
|
</button>
|
||||||
title="Ativar/Desativar Loop"
|
<button id="zoom-in-btn" class="control-btn" title="Zoom In">
|
||||||
></i>
|
<i class="fa-solid fa-magnifying-glass-plus"></i>
|
||||||
<button
|
</button>
|
||||||
id="sync-mode-btn"
|
|
||||||
class="control-btn active"
|
<div class="toolbar-divider"></div>
|
||||||
title="Modo de Sincronia de Playback (Global/Local)"
|
|
||||||
>
|
<button id="add-audio-track-btn" class="control-btn" title="Adicionar Pista">
|
||||||
Global
|
<i class="fa-solid fa-plus"></i>
|
||||||
</button>
|
<span style="margin-left: 8px">Add Track</span>
|
||||||
<i
|
</button>
|
||||||
class="fa-solid fa-plus"
|
<button id="remove-audio-track-btn" class="control-btn" title="Remover Última Pista">
|
||||||
id="add-audio-track-btn"
|
<i class="fa-solid fa-minus"></i>
|
||||||
title="Adicionar Pista de Áudio"
|
<span style="margin-left: 8px">Rem. Track</span>
|
||||||
></i>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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="audio-tracks-wrapper">
|
||||||
<div class="spectrogram-view-grid" style="width: 4000px">
|
<div id="audio-timeline-ruler" class="timeline-ruler"></div>
|
||||||
<div
|
<div id="loop-region" class="loop-region"></div>
|
||||||
class="timeline-clip"
|
<div id="playhead" class="playhead"></div>
|
||||||
style="left: 100px; width: 400px"
|
<div id="audio-track-container" class="track-container"></div>
|
||||||
></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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -743,4 +675,4 @@
|
|||||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||||
<script src="assets/js/creations/socket.js" type="module"></script>
|
<script src="assets/js/creations/socket.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user