Upgrade interface do MMPSearch
Deploy / Deploy (push) Successful in 1m15s
Details
Deploy / Deploy (push) Successful in 1m15s
Details
This commit is contained in:
parent
d3f8cd1184
commit
7964f9a75e
|
|
@ -0,0 +1,787 @@
|
|||
---
|
||||
layout: default
|
||||
title: MMPCreator
|
||||
permalink: /creation/
|
||||
---
|
||||
|
||||
<!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/creator.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>
|
||||
<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>
|
||||
<span style="margin-left: 8px">Novo projeto</span>
|
||||
<i
|
||||
class="fa-solid fa-folder-open"
|
||||
id="open-mmp-btn"
|
||||
title="Abrir Projeto do Servidor"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Abrir projetos</span>
|
||||
<i
|
||||
class="fa-solid fa-save"
|
||||
id="save-mmp-btn"
|
||||
title="Salvar Projeto (.mmp)"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Salvar projeto</span>
|
||||
<i
|
||||
class="fa-solid fa-upload"
|
||||
id="upload-sample-btn"
|
||||
title="Carregar Sample do Computador"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Enviar sample</span>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="control-group">
|
||||
<button id="record-btn" class="transport-btn" title="Gravar">
|
||||
<i class="fa-solid fa-circle-dot"></i>
|
||||
<span style="margin-left: 8px">Gravar</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<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>
|
||||
<button
|
||||
id="toggle-mixer-btn"
|
||||
class="control-btn"
|
||||
title="Abrir Mixer (Futuro)"
|
||||
>
|
||||
<i class="fa-solid fa-sliders"></i>
|
||||
<span style="margin-left: 8px">Abrir Mixer</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="control-group master-controls">
|
||||
<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-toolbar">
|
||||
<div class="editor-header">
|
||||
<div class="toolbar-group">
|
||||
<button id="play-btn" class="control-btn" title="Play Patterns">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
</button>
|
||||
<button id="stop-btn" class="control-btn" title="Stop">
|
||||
<i class="fa-solid fa-stop"></i>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<select
|
||||
id="global-pattern-selector"
|
||||
title="Selecionar Pattern Ativo"
|
||||
>
|
||||
<option value="0">Pattern 1</option>
|
||||
<option value="1">Pattern 2</option>
|
||||
<option value="2">Pattern 3</option>
|
||||
<option value="3">Pattern 4</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
class="control-btn"
|
||||
id="add-pattern-btn"
|
||||
title="Novo Pattern (Não implementado no JS ainda)"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
<button
|
||||
class="control-btn"
|
||||
id="remove-pattern-btn"
|
||||
title="Remover Pattern (Não implementado no JS ainda)"
|
||||
>
|
||||
<i class="fa-solid fa-minus"></i>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button
|
||||
id="send-pattern-to-playlist-btn"
|
||||
class="control-btn"
|
||||
title="Renderizar para Áudio"
|
||||
>
|
||||
<i class="fa-solid fa-arrow-right-to-bracket"></i>
|
||||
<span style="margin-left: 5px">Enviar p/ Playlist</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button
|
||||
id="add-instrument-btn"
|
||||
class="control-btn"
|
||||
title="Adicionar Instrumento"
|
||||
>
|
||||
<i class="fa-solid fa-music"></i>
|
||||
<i
|
||||
class="fa-solid fa-plus"
|
||||
style="font-size: 0.7em; margin-left: 3px"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Adicionar Instrumento</span>
|
||||
</button>
|
||||
<button
|
||||
id="remove-instrument-btn"
|
||||
class="control-btn"
|
||||
title="Remover Instrumento Selecionado"
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<span style="margin-left: 8px">Remover Instrumento</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sequencer-grid" class="sequencer-container"></div>
|
||||
<div class="tool-icons"></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>
|
||||
<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" id="audio-editor-header">
|
||||
<div class="toolbar-group">
|
||||
<span class="panel-title" style="margin-right: 10px"
|
||||
><i class="fa-solid fa-wave-square"></i> Playlist</span
|
||||
>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button
|
||||
id="audio-editor-play-btn"
|
||||
class="control-btn"
|
||||
title="Play Playlist"
|
||||
>
|
||||
<i class="fa-solid fa-play"></i>
|
||||
</button>
|
||||
<button
|
||||
id="audio-editor-stop-btn"
|
||||
class="control-btn"
|
||||
title="Stop Playlist"
|
||||
>
|
||||
<i class="fa-solid fa-stop"></i>
|
||||
</button>
|
||||
<button
|
||||
id="audio-editor-loop-btn"
|
||||
class="control-btn"
|
||||
title="Loop Mode"
|
||||
>
|
||||
<i class="fa-solid fa-repeat"></i>
|
||||
<span style="margin-left: 8px">Loop</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button
|
||||
id="slice-tool-btn"
|
||||
class="control-btn"
|
||||
title="Ferramenta Corte (Slice)"
|
||||
>
|
||||
<i class="fa-solid fa-scissors"></i>
|
||||
<span style="margin-left: 8px">Cortar</span>
|
||||
</button>
|
||||
<button
|
||||
id="resize-tool-trim"
|
||||
class="control-btn active"
|
||||
title="Redimensionar (Trim)"
|
||||
>
|
||||
<i class="fa-solid fa-arrows-left-right-to-line"></i>
|
||||
<span style="margin-left: 8px">Redimensionar</span>
|
||||
</button>
|
||||
<button
|
||||
id="resize-tool-stretch"
|
||||
class="control-btn"
|
||||
title="Esticar (Stretch)"
|
||||
>
|
||||
<i class="fa-solid fa-expand"></i>
|
||||
<span style="margin-left: 8px">Esticar</span>
|
||||
</button>
|
||||
<button
|
||||
id="delete-clip"
|
||||
class="control-btn"
|
||||
title="Excluir Clip Selecionado"
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<span style="margin-left: 8px">Rem. Instrumento</span>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button id="zoom-out-btn" class="control-btn" title="Zoom Out">
|
||||
<i class="fa-solid fa-magnifying-glass-minus"></i>
|
||||
</button>
|
||||
<button id="zoom-in-btn" class="control-btn" title="Zoom In">
|
||||
<i class="fa-solid fa-magnifying-glass-plus"></i>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button
|
||||
id="add-audio-track-btn"
|
||||
class="control-btn"
|
||||
title="Adicionar Pista"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span style="margin-left: 8px">Add Track</span>
|
||||
</button>
|
||||
<button
|
||||
id="remove-audio-track-btn"
|
||||
class="control-btn"
|
||||
title="Remover Última Pista"
|
||||
>
|
||||
<i class="fa-solid fa-minus"></i>
|
||||
<span style="margin-left: 8px">Rem. Track</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="audio-tracks-wrapper">
|
||||
<div id="audio-timeline-ruler" class="timeline-ruler"></div>
|
||||
<div id="loop-region" class="loop-region"></div>
|
||||
<div id="playhead" class="playhead"></div>
|
||||
<div id="audio-track-container" class="track-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</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">×</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;
|
||||
const CONSTANTS = {
|
||||
NOTE_HEIGHT: 20,
|
||||
KEY_WIDTH: 60,
|
||||
BEAT_WIDTH: 40,
|
||||
TOTAL_KEYS: 84, // 7 oitavas
|
||||
START_NOTE: 24, // C1
|
||||
TICKS_PER_PIXEL: 0, // Calculado dinamicamente
|
||||
};
|
||||
|
||||
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;
|
||||
// 64 compassos * 192 ticks / (ticks por beat) * largura... simplificando:
|
||||
// Vamos fixar uma largura grande por enquanto
|
||||
const totalWidth = 3000;
|
||||
|
||||
keysCanvas.width = CONSTANTS.KEY_WIDTH;
|
||||
keysCanvas.height = totalHeight;
|
||||
gridCanvas.width = totalWidth;
|
||||
gridCanvas.height = totalHeight;
|
||||
|
||||
// Importante: Sincronizar a conversão de Pixel <-> Tick
|
||||
// 1 Beat = 48 ticks (em 16th) ou 192 ticks por bar?
|
||||
// No seu file.js: ticksPerStep = 12 (1/16).
|
||||
// Então BEAT_WIDTH (40px) = 4 steps = 48 ticks.
|
||||
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>
|
||||
|
|
@ -1,678 +0,0 @@
|
|||
<!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/creator.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>
|
||||
<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>
|
||||
<span style="margin-left: 8px">Novo projeto</span>
|
||||
<i
|
||||
class="fa-solid fa-folder-open"
|
||||
id="open-mmp-btn"
|
||||
title="Abrir Projeto do Servidor"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Abrir projetos</span>
|
||||
<i
|
||||
class="fa-solid fa-save"
|
||||
id="save-mmp-btn"
|
||||
title="Salvar Projeto (.mmp)"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Salvar projeto</span>
|
||||
<i
|
||||
class="fa-solid fa-upload"
|
||||
id="upload-sample-btn"
|
||||
title="Carregar Sample do Computador"
|
||||
></i>
|
||||
<span style="margin-left: 8px">Enviar sample</span>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="control-group">
|
||||
<button id="record-btn" class="transport-btn" title="Gravar">
|
||||
<i class="fa-solid fa-circle-dot"></i>
|
||||
<span style="margin-left: 8px">Gravar</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<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>
|
||||
<button id="toggle-mixer-btn" class="control-btn" title="Abrir Mixer (Futuro)">
|
||||
<i class="fa-solid fa-sliders"></i>
|
||||
<span style="margin-left: 8px">Abrir Mixer</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="control-group master-controls">
|
||||
<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-toolbar">
|
||||
<div class="editor-header">
|
||||
<div class="toolbar-group">
|
||||
<button id="play-btn" class="control-btn" title="Play Patterns"><i class="fa-solid fa-play"></i></button>
|
||||
<button id="stop-btn" class="control-btn" title="Stop"><i class="fa-solid fa-stop"></i></button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<select id="global-pattern-selector" title="Selecionar Pattern Ativo">
|
||||
<option value="0">Pattern 1</option>
|
||||
<option value="1">Pattern 2</option>
|
||||
<option value="2">Pattern 3</option>
|
||||
<option value="3">Pattern 4</option>
|
||||
</select>
|
||||
|
||||
<button class="control-btn" id="add-pattern-btn" title="Novo Pattern (Não implementado no JS ainda)"><i class="fa-solid fa-plus"></i></button>
|
||||
<button class="control-btn" id="remove-pattern-btn" title="Remover Pattern (Não implementado no JS ainda)"><i class="fa-solid fa-minus"></i></button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button id="send-pattern-to-playlist-btn" class="control-btn" title="Renderizar para Áudio">
|
||||
<i class="fa-solid fa-arrow-right-to-bracket"></i> <span style="margin-left:5px;">Enviar p/ Playlist</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button id="add-instrument-btn" class="control-btn" title="Adicionar Instrumento">
|
||||
<i class="fa-solid fa-music"></i> <i class="fa-solid fa-plus" style="font-size: 0.7em; margin-left: 3px;"></i>
|
||||
<span style="margin-left: 8px">Adicionar Instrumento</span>
|
||||
</button>
|
||||
<button id="remove-instrument-btn" class="control-btn" title="Remover Instrumento Selecionado">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<span style="margin-left: 8px">Remover Instrumento</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sequencer-grid" class="sequencer-container">
|
||||
</div>
|
||||
<div class="tool-icons">
|
||||
</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>
|
||||
<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" id="audio-editor-header">
|
||||
<div class="toolbar-group">
|
||||
<span class="panel-title" style="margin-right: 10px;"><i class="fa-solid fa-wave-square"></i> Playlist</span>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button id="audio-editor-play-btn" class="control-btn" title="Play Playlist">
|
||||
<i class="fa-solid fa-play"></i>
|
||||
</button>
|
||||
<button id="audio-editor-stop-btn" class="control-btn" title="Stop Playlist">
|
||||
<i class="fa-solid fa-stop"></i>
|
||||
</button>
|
||||
<button id="audio-editor-loop-btn" class="control-btn" title="Loop Mode">
|
||||
<i class="fa-solid fa-repeat"></i>
|
||||
<span style="margin-left: 8px">Loop</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button id="slice-tool-btn" class="control-btn" title="Ferramenta Corte (Slice)">
|
||||
<i class="fa-solid fa-scissors"></i>
|
||||
<span style="margin-left: 8px">Cortar</span>
|
||||
</button>
|
||||
<button id="resize-tool-trim" class="control-btn active" title="Redimensionar (Trim)">
|
||||
<i class="fa-solid fa-arrows-left-right-to-line"></i>
|
||||
<span style="margin-left: 8px">Redimensionar</span>
|
||||
</button>
|
||||
<button id="resize-tool-stretch" class="control-btn" title="Esticar (Stretch)">
|
||||
<i class="fa-solid fa-expand"></i>
|
||||
<span style="margin-left: 8px">Esticar</span>
|
||||
</button>
|
||||
<button id="delete-clip" class="control-btn" title="Excluir Clip Selecionado">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<span style="margin-left: 8px">Rem. Instrumento</span>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button id="zoom-out-btn" class="control-btn" title="Zoom Out">
|
||||
<i class="fa-solid fa-magnifying-glass-minus"></i>
|
||||
</button>
|
||||
<button id="zoom-in-btn" class="control-btn" title="Zoom In">
|
||||
<i class="fa-solid fa-magnifying-glass-plus"></i>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<button id="add-audio-track-btn" class="control-btn" title="Adicionar Pista">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span style="margin-left: 8px">Add Track</span>
|
||||
</button>
|
||||
<button id="remove-audio-track-btn" class="control-btn" title="Remover Última Pista">
|
||||
<i class="fa-solid fa-minus"></i>
|
||||
<span style="margin-left: 8px">Rem. Track</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="audio-tracks-wrapper">
|
||||
<div id="audio-timeline-ruler" class="timeline-ruler"></div>
|
||||
<div id="loop-region" class="loop-region"></div>
|
||||
<div id="playhead" class="playhead"></div>
|
||||
<div id="audio-track-container" class="track-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</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">×</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;
|
||||
const CONSTANTS = {
|
||||
NOTE_HEIGHT: 20,
|
||||
KEY_WIDTH: 60,
|
||||
BEAT_WIDTH: 40,
|
||||
TOTAL_KEYS: 84, // 7 oitavas
|
||||
START_NOTE: 24, // C1
|
||||
TICKS_PER_PIXEL: 0 // Calculado dinamicamente
|
||||
};
|
||||
|
||||
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;
|
||||
// 64 compassos * 192 ticks / (ticks por beat) * largura... simplificando:
|
||||
// Vamos fixar uma largura grande por enquanto
|
||||
const totalWidth = 3000;
|
||||
|
||||
keysCanvas.width = CONSTANTS.KEY_WIDTH;
|
||||
keysCanvas.height = totalHeight;
|
||||
gridCanvas.width = totalWidth;
|
||||
gridCanvas.height = totalHeight;
|
||||
|
||||
// Importante: Sincronizar a conversão de Pixel <-> Tick
|
||||
// 1 Beat = 48 ticks (em 16th) ou 192 ticks por bar?
|
||||
// No seu file.js: ticksPerStep = 12 (1/16).
|
||||
// Então BEAT_WIDTH (40px) = 4 steps = 48 ticks.
|
||||
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>
|
||||
Loading…
Reference in New Issue