mmpSearch/creation.html

758 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MMPCreator</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
/>
<link rel="stylesheet" href="assets/css/style.css" />
<style>
/* Estilo para clipes de pattern */
.timeline-clip.pattern-clip {
background: linear-gradient(to bottom, #4a4f57, #3b3f45);
height: 70px; /* Mais alto para ver as notas */
overflow: hidden;
}
/* Container para as notas do pattern */
.pattern-clip-view {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 2px 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 1px;
}
/* Linha de trilha dentro do clipe */
.pattern-clip-track-row {
position: relative;
width: 100%;
height: 100%;
}
/* Cada "nota" (bloco branco) */
.pattern-step-note {
position: absolute;
background-color: rgba(255, 255, 255, 0.9);
border-left: 1px solid #333;
border-bottom: 1px solid #333;
border-radius: 1px;
box-sizing: border-box;
}
/* Menu de contexto */
#timeline-context-menu .menu-divider {
height: 1px;
background-color: var(--border-color);
margin: 4px 0;
padding: 0;
}
#timeline-context-menu div#delete-clip:hover {
background-color: var(--accent-red);
color: white;
}
#timeline-context-menu div#paste-clip.disabled {
color: var(--text-dark);
cursor: default;
background-color: transparent;
}
#timeline-context-menu div#paste-clip.disabled:hover {
color: var(--text-dark);
background-color: transparent;
}
</style>
</head>
<body>
<div class="studio-wrapper">
<aside class="sample-browser">
<div class="browser-header">Navegador de Samples</div>
<div class="browser-content" id="browser-content"></div>
</aside>
<button id="sidebar-toggle"><i class="fa-solid fa-caret-left"></i></button>
<div class="app-container">
<header class="global-toolbar">
<div class="control-group">
<i
class="fa-solid fa-file"
id="new-project-btn"
title="Novo Projeto"
></i>
<i
class="fa-solid fa-folder-open"
id="open-mmp-btn"
title="Abrir Projeto do Servidor"
></i>
<i
class="fa-solid fa-save"
id="save-mmp-btn"
title="Salvar Projeto (.mmp)"
></i>
<i
class="fa-solid fa-upload"
id="upload-sample-btn"
title="Carregar Sample do Computador"
></i>
</div>
<div class="divider"></div>
<div class="control-group">
<i
class="fa-solid fa-backward-step"
id="rewind-btn"
title="Voltar ao Início"
></i>
<i class="fa-solid fa-play" title="Play/Pause Global (Futuro)"></i>
<i class="fa-solid fa-stop" title="Stop Global (Futuro)"></i>
<button id="record-btn" class="transport-btn" title="Gravar">
<i class="fa-solid fa-circle-dot"></i>
</button>
</div>
<div class="divider"></div>
<div class="info-display-group">
<div class="info-display">
<div class="interactive-input-container">
<button class="adjust-btn" data-target="bpm" data-step="-1">
-
</button>
<input
type="text"
class="value-input"
id="bpm-input"
value="140"
data-min="20"
data-max="400"
/>
<button class="adjust-btn" data-target="bpm" data-step="1">
+
</button>
</div>
<div class="label">ANDAMENTO/BPM</div>
</div>
<div class="info-display">
<div class="interactive-input-container">
<button class="adjust-btn" data-target="bars" data-step="-1">
-
</button>
<input
type="text"
class="value-input"
id="bars-input"
value="1"
data-min="1"
data-max="64"
/>
<button class="adjust-btn" data-target="bars" data-step="1">
+
</button>
</div>
<div class="label">COMPASSOS</div>
</div>
<div class="info-display">
<div class="interactive-input-container">
<div class="compasso-group">
<button
class="adjust-btn"
data-target="compasso-a"
data-step="-1"
>
-
</button>
<input
type="text"
class="value-input compasso-input"
id="compasso-a-input"
value="4"
data-min="1"
data-max="16"
/>
<button
class="adjust-btn"
data-target="compasso-a"
data-step="1"
>
+
</button>
</div>
<span class="compasso-separator">/</span>
<div class="compasso-group">
<button
class="adjust-btn"
data-target="compasso-b"
data-step="-1"
>
-
</button>
<input
type="text"
class="value-input compasso-input"
id="compasso-b-input"
value="4"
data-min="1"
data-max="16"
/>
<button
class="adjust-btn"
data-target="compasso-b"
data-step="1"
>
+
</button>
</div>
</div>
<div class="label">COMPASSO</div>
</div>
<div class="info-display">
<div
id="timer-display"
class="interactive-input-container"
style="font-size: 0.7rem; color: var(--text-dark)"
>
00:00:00
</div>
<div class="label">MIN:SEC:MSEC</div>
</div>
</div>
<div class="control-group">
<button id="metronome-btn" title="Metrônomo On/Off">Metrônomo</button>
<button
id="create-room-btn"
class="transport-btn"
title="Criar ou entrar em uma sala compartilhada"
>
<i class="fa-solid fa-users"></i>
<span style="margin-left: 8px">Criar Sala</span>
</button>
</div>
<div class="spacer"></div>
<div class="control-group master-controls">
<div class="knob-container">
<div class="knob" id="master-volume-knob">
<div class="knob-indicator"></div>
</div>
<span>VOL MASTER</span>
</div>
<div class="knob-container">
<div class="knob" id="master-pan-knob">
<div class="knob-indicator"></div>
</div>
<span>PAN MASTER</span>
</div>
</div>
</header>
<main class="main-content">
<div class="beat-editor">
<div class="editor-header">
Mostrar/esconder Editor de Bases
<div class="window-controls">
<i class="fa-solid fa-minus"></i
><i class="fa-regular fa-square"></i
><i class="fa-solid fa-xmark"></i>
</div>
</div>
<div class="editor-toolbar">
<div class="playback-controls">
<i class="fa-solid fa-play" id="play-btn" title="Play/Pause"></i>
<i class="fa-solid fa-stop" id="stop-btn" title="Stop"></i>
</div>
<div class="pattern-manager">
<h2 id="beat-bassline-title"></h2>
<select
id="global-pattern-selector"
class="pattern-selector"
disabled
>
<option>Selecione uma faixa</option>
</select>
<button id="add-pattern-btn" class="pattern-btn">+</button>
<button id="remove-pattern-btn" class="pattern-btn">-</button>
<button
id="send-pattern-to-playlist-btn"
class="pattern-btn"
title="Enviar Pattern para a Playlist"
style="width: auto; padding: 0 8px; font-size: 0.9rem"
>
<i class="fa-solid fa-arrow-right-to-bracket"></i> Enviar
</button>
</div>
<div class="tool-icons">
<i class="fa-solid fa-table-cells"></i
><i class="fa-solid fa-bars-staggered"></i>
<i class="fa-solid fa-music" id="open-piano-roll-btn" title="Abrir Piano Roll"></i>
<i
class="fa-solid fa-wave-square"
id="bounce-pattern-btn"
title="Renderizar Pattern para Pista de Áudio"
></i>
<i
class="fa-solid fa-plus"
id="add-bar-btn"
title="Adicionar 1 Compasso"
></i>
</div>
<div id="timeline-context-menu">
<div id="copy-clip">Copiar</div>
<div id="cut-clip">Recortar</div>
<div id="paste-clip">Colar</div>
<div class="menu-divider"></div>
<div id="delete-clip" style="color: var(--accent-red)">
Excluir Clipe
</div>
</div>
<div id="ruler-context-menu">
<div id="ruler-set-loop-start">Definir Início do Loop</div>
<div id="ruler-set-loop-end">Definir Fim do Loop</div>
</div>
<div class="zoom-controls">
<i class="fa-solid fa-minus" id="remove-instrument-btn"></i
><i class="fa-solid fa-plus" id="add-instrument-btn"></i>
</div>
</div>
<div id="track-container"></div>
</div>
<div class="piano-roll-editor" id="piano-roll-editor" style="display: none;">
<div class="editor-header">
<span>Piano Roll - <span id="piano-roll-instrument-name">Instrumento 1</span></span>
<div class="window-controls">
<i class="fa-solid fa-xmark" id="close-piano-roll-btn"></i>
</div>
</div>
<div class="piano-roll-toolbar">
<div class="playback-controls">
<i class="fa-solid fa-pencil active" title="Draw Tool"></i>
<i class="fa-solid fa-eraser" title="Erase Tool"></i>
</div>
<div class="snap-controls">
<label>Snap:</label>
<select>
<option>1/4</option>
<option>1/8</option>
<option selected>1/16</option>
</select>
</div>
</div>
<div class="piano-roll-workspace">
<div class="piano-keys-container" id="piano-keys-container">
<canvas id="piano-keys-canvas"></canvas>
</div>
<div class="piano-grid-container" id="piano-grid-container">
<canvas id="piano-grid-canvas"></canvas>
</div>
</div>
</div>
<div class="audio-editor">
<div class="editor-header">
<span>Editor de Amostras de Áudio</span>
<div class="playback-controls">
<i
class="fa-solid fa-search-minus"
id="zoom-out-btn"
title="Zoom Out"
></i>
<i
class="fa-solid fa-search-plus"
id="zoom-in-btn"
title="Zoom In"
></i>
<i
class="fa-solid fa-scissors"
id="slice-tool-btn"
title="Ferramenta de Corte"
></i>
<i
class="fa-solid fa-arrows-left-right-to-line"
id="resize-tool-trim"
title="Modo de Redimensionamento (Aparar/Trimming)"
></i>
<i
class="fa-solid fa-arrows-left-right"
id="resize-tool-stretch"
title="Modo de Redimensionamento (Esticar/Time Stretch)"
></i>
<i
class="fa-solid fa-play"
id="audio-editor-play-btn"
title="Play/Pause"
></i>
<i
class="fa-solid fa-stop"
id="audio-editor-stop-btn"
title="Stop"
></i>
<i
class="fa-solid fa-repeat"
id="audio-editor-loop-btn"
title="Ativar/Desativar Loop"
></i>
<button
id="sync-mode-btn"
class="control-btn active"
title="Modo de Sincronia de Playback (Global/Local)"
>
Global
</button>
<i
class="fa-solid fa-plus"
id="add-audio-track-btn"
title="Adicionar Pista de Áudio"
></i>
</div>
</div>
<div id="audio-track-container">
<div class="audio-track-lane">
<div class="track-info">
<div class="track-info-header">
<i class="fa-solid fa-gear"></i>
<span class="track-name">Pista de Áudio 1</span>
<div class="track-mute"></div>
</div>
<div class="track-controls">
<div class="knob-container">
<div class="knob" data-control="volume">
<div class="knob-indicator"></div>
</div>
<span>VOL</span>
</div>
<div class="knob-container">
<div class="knob" data-control="pan">
<div class="knob-indicator"></div>
</div>
<span>PAN</span>
</div>
</div>
</div>
<div class="timeline-container">
<div class="spectrogram-view-grid" style="width: 4000px">
<div
class="timeline-clip"
style="left: 100px; width: 400px"
></div>
<div class="playhead"></div>
</div>
</div>
</div>
<div class="audio-track-lane">
<div class="track-info">
<div class="track-info-header">
<i class="fa-solid fa-gear"></i>
<span class="track-name">Pista de Áudio 2</span>
<div class="track-mute"></div>
</div>
<div class="track-controls">
<div class="knob-container">
<div class="knob" data-control="volume">
<div class="knob-indicator"></div>
</div>
<span>VOL</span>
</div>
<div class="knob-container">
<div class="knob" data-control="pan">
<div class="knob-indicator"></div>
</div>
<span>PAN</span>
</div>
</div>
</div>
<div class="timeline-container">
<div id="loop-region" class="loop-region">
<div class="spectrogram-view-grid" style="width: 4000px">
<div class="timeline-clip" style="left: 50px; width: 600px">
<div class="clip-name">jungle01.ogg</div>
</div>
</div>
</div>
<div class="playhead"></div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<input
type="file"
id="mmp-file-input"
accept=".mmp, .mmpz"
style="display: none"
/>
<input
type="file"
id="sample-file-input"
accept=".wav,.flac,.ogg,.mp3"
style="display: none"
/>
<div class="modal-overlay" id="open-project-modal">
<div class="modal-content">
<button class="modal-close" id="open-modal-close-btn">&times;</button>
<h2 class="modal-title">Abrir Projeto</h2>
<div class="modal-section">
<h3>Projetos no Servidor</h3>
<div id="server-projects-list"><p>Carregando...</p></div>
</div>
<div class="modal-section">
<h3>Carregar do Computador</h3>
<button class="modal-button" id="load-from-computer-btn">
<i class="fa-solid fa-desktop"></i> Selecionar arquivo .mmp ou .mmpz
</button>
</div>
</div>
</div>
<script>
// Verifica se a página está sendo carregada com o parâmetro &embed=true
const isEmbed = new URLSearchParams(window.location.search).get("embed");
if (isEmbed === "true") {
document.body.classList.add("embed-mode");
}
</script>
<script type="module">
// Importamos o estado global para ler/salvar notas reais
import { appState } from "./assets/js/creations/state.js"; // Ajuste o caminho se necessário
import { sendAction } from "./assets/js/creations/socket.js";
import { renderAll } from "./assets/js/creations/ui.js";
import * as Tone from "https://esm.sh/tone";
// Variáveis de Controle
let currentTrackId = null;
// --- Configurações ---
const CONSTANTS = {
NOTE_HEIGHT: 20,
KEY_WIDTH: 60,
BEAT_WIDTH: 40, // Largura visual de 1 batida (Quarter Note)
TOTAL_KEYS: 84,
START_NOTE: 24,
// --- CORREÇÃO DE ESCALA ---
// O LMMS usa 192 ticks por batida (Quarter Note).
// Se nossa batida tem 40px de largura, a relação é:
TICKS_PER_PIXEL: 192 / 40 // = 4.8
};
const pianoRollEditor = document.getElementById('piano-roll-editor');
const keysCanvas = document.getElementById('piano-keys-canvas');
const gridCanvas = document.getElementById('piano-grid-canvas');
const keysCtx = keysCanvas.getContext('2d');
const gridCtx = gridCanvas.getContext('2d');
const gridContainer = document.getElementById('piano-grid-container');
const keysContainer = document.getElementById('piano-keys-container');
// Sintetizador de preview
const previewSynth = new Tone.PolySynth(Tone.Synth).toDestination();
previewSynth.volume.value = -10;
// --- FUNÇÃO GLOBAL PARA ABRIR O EDITOR ---
window.openPianoRoll = function(trackId) {
const track = appState.pattern.tracks.find(t => t.id === trackId);
if (!track) return;
currentTrackId = trackId;
document.getElementById('piano-roll-instrument-name').textContent = track.name;
// Mostra o editor
pianoRollEditor.style.display = 'flex';
// Redimensiona e desenha
resizeCanvas();
// Centraliza o scroll vertical (C5)
const middleY = (CONSTANTS.TOTAL_KEYS / 2) * CONSTANTS.NOTE_HEIGHT;
gridContainer.scrollTop = middleY - 200;
};
// --- DESENHO E LÓGICA ---
function resizeCanvas() {
const totalHeight = CONSTANTS.TOTAL_KEYS * CONSTANTS.NOTE_HEIGHT;
// Calcula a largura total baseada nos compassos definidos no input
const barsCount = parseInt(document.getElementById('bars-input')?.value || 1);
// 1 Compasso = 4 Batidas. Largura = Batidas * Largura da Batida
const totalWidth = (barsCount * 4) * CONSTANTS.BEAT_WIDTH;
keysCanvas.width = CONSTANTS.KEY_WIDTH;
keysCanvas.height = totalHeight;
gridCanvas.width = totalWidth; // Ajusta largura ao tamanho real da música
gridCanvas.height = totalHeight;
// REMOVA ESTA LINHA ANTIGA:
// CONSTANTS.TICKS_PER_PIXEL = 48 / CONSTANTS.BEAT_WIDTH;
drawKeys();
drawGrid();
drawNotes();
}
function drawKeys() {
keysCtx.clearRect(0, 0, keysCanvas.width, keysCanvas.height);
for (let i = 0; i < CONSTANTS.TOTAL_KEYS; i++) {
const midiNote = CONSTANTS.START_NOTE + (CONSTANTS.TOTAL_KEYS - 1 - i);
const y = i * CONSTANTS.NOTE_HEIGHT;
const isBlack = isBlackKey(midiNote);
keysCtx.fillStyle = isBlack ? '#333' : '#eee';
keysCtx.fillRect(0, y, CONSTANTS.KEY_WIDTH, CONSTANTS.NOTE_HEIGHT);
keysCtx.strokeStyle = '#555';
keysCtx.strokeRect(0, y, CONSTANTS.KEY_WIDTH, CONSTANTS.NOTE_HEIGHT);
if (midiNote % 12 === 0) {
keysCtx.fillStyle = isBlack ? '#fff' : '#333';
keysCtx.font = '10px Arial';
keysCtx.fillText('C' + (Math.floor(midiNote / 12) - 1), 35, y + 14);
}
}
}
function drawGrid() {
gridCtx.fillStyle = '#292929';
gridCtx.fillRect(0, 0, gridCanvas.width, gridCanvas.height);
// Linhas das notas
for (let i = 0; i < CONSTANTS.TOTAL_KEYS; i++) {
const midiNote = CONSTANTS.START_NOTE + (CONSTANTS.TOTAL_KEYS - 1 - i);
const y = i * CONSTANTS.NOTE_HEIGHT;
const isBlack = isBlackKey(midiNote);
gridCtx.fillStyle = isBlack ? '#222' : '#2a2a2a';
gridCtx.fillRect(0, y, gridCanvas.width, CONSTANTS.NOTE_HEIGHT);
gridCtx.strokeStyle = '#333';
gridCtx.beginPath(); gridCtx.moveTo(0, y); gridCtx.lineTo(gridCanvas.width, y); gridCtx.stroke();
}
// Linhas verticais (Tempo)
for (let x = 0; x < gridCanvas.width; x += CONSTANTS.BEAT_WIDTH) {
gridCtx.beginPath();
gridCtx.strokeStyle = (x % (CONSTANTS.BEAT_WIDTH * 4) === 0) ? '#666' : '#383838';
gridCtx.moveTo(x, 0); gridCtx.lineTo(x, gridCanvas.height); gridCtx.stroke();
}
}
function drawNotes() {
if (!currentTrackId) return;
const track = appState.pattern.tracks.find(t => t.id === currentTrackId);
if (!track) return;
const pattern = track.patterns[track.activePatternIndex];
const notes = pattern.notes || [];
gridCtx.fillStyle = '#ffbb00'; // Cor Laranja
gridCtx.strokeStyle = '#000';
notes.forEach(note => {
// Converter MIDI para Y (Invertido)
const keyIndex = (CONSTANTS.TOTAL_KEYS - 1) - (note.key - CONSTANTS.START_NOTE);
const y = keyIndex * CONSTANTS.NOTE_HEIGHT;
// Converter Ticks (pos) para X
// Se 48 ticks = BEAT_WIDTH (40px) -> pos / 1.2
const x = note.pos / CONSTANTS.TICKS_PER_PIXEL;
const width = note.len / CONSTANTS.TICKS_PER_PIXEL;
gridCtx.fillRect(x + 1, y + 1, width - 2, CONSTANTS.NOTE_HEIGHT - 2);
gridCtx.strokeRect(x + 1, y + 1, width - 2, CONSTANTS.NOTE_HEIGHT - 2);
});
}
// --- INTERAÇÃO: CRIAR NOTAS ---
gridCanvas.addEventListener('mousedown', (e) => {
if (!currentTrackId) return;
const rect = gridCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 1. Calcular Nota (Y)
const keyIndex = Math.floor(y / CONSTANTS.NOTE_HEIGHT);
const midiNote = CONSTANTS.START_NOTE + (CONSTANTS.TOTAL_KEYS - 1 - keyIndex);
// 2. Calcular Posição (X) e Snap
// Snap padrão 1/16 = 12 ticks
const snapTicks = 12;
const rawTicks = x * CONSTANTS.TICKS_PER_PIXEL;
const quantizedPos = Math.floor(rawTicks / snapTicks) * snapTicks;
// 3. Criar Objeto Nota
const newNote = {
pos: quantizedPos,
len: 48, // Duração padrão (1 beat/seminima) - ajuste conforme desejar
key: midiNote,
vol: 100,
pan: 0
};
// 4. Atualizar Estado e Redesenhar
const track = appState.pattern.tracks.find(t => t.id === currentTrackId);
if (track) {
const pattern = track.patterns[track.activePatternIndex];
// Se não existir array de notas, cria
if (!pattern.notes) pattern.notes = [];
pattern.notes.push(newNote);
// Toca som
const noteName = Tone.Frequency(midiNote, "midi").toNote();
previewSynth.triggerAttackRelease(noteName, "8n");
// Redesenha Piano Roll
drawNotes();
// IMPORTANTE: Atualizar a UI da lista de trilhas (para aparecer a miniatura)
// E enviar via Socket para colaboradores
renderAll();
sendAction({
type: "UPDATE_PATTERN_NOTES",
trackId: currentTrackId,
patternIndex: track.activePatternIndex,
notes: pattern.notes
});
}
});
// Scroll Sync
gridContainer.addEventListener('scroll', () => {
keysContainer.scrollTop = gridContainer.scrollTop;
});
// Fechar
document.getElementById('close-piano-roll-btn').addEventListener('click', () => {
pianoRollEditor.style.display = 'none';
currentTrackId = null;
});
function isBlackKey(note) {
const n = note % 12;
return (n === 1 || n === 3 || n === 6 || n === 8 || n === 10);
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.js"></script>
<script src="assets/js/creations/main.js" type="module"></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>
</body>
</html>