888 lines
30 KiB
HTML
Executable File
888 lines
30 KiB
HTML
Executable File
<!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-file-zipper" id="download-package-btn" title="Baixar Pacote Completo (.zip)" style="margin-left: 15px; color: #ffdd57; cursor: pointer;"></i>
|
|
<span style="margin-left: 8px; color: #ffdd57;">Baixar ZIP</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>
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const downloadBtn = document.getElementById("download-package-btn");
|
|
|
|
if (downloadBtn) {
|
|
downloadBtn.addEventListener("click", () => {
|
|
// 1. Pega o parâmetro 'project' da URL (ex: creation.html?project=drake)
|
|
const params = new URLSearchParams(window.location.search);
|
|
let projectName = params.get("project");
|
|
|
|
if (projectName) {
|
|
// 2. Garante a extensão para a API (opcional, mas seguro)
|
|
if (!projectName.toLowerCase().endsWith(".mmp")) {
|
|
projectName += ".mmp";
|
|
}
|
|
|
|
// 3. Monta a URL da API Python e inicia o download
|
|
const apiUrl = `/api/download/${projectName}`;
|
|
|
|
// Feedback visual rápido
|
|
downloadBtn.style.opacity = "0.5";
|
|
setTimeout(() => downloadBtn.style.opacity = "1", 500);
|
|
|
|
// Dispara o download
|
|
window.location.href = apiUrl;
|
|
} else {
|
|
// Caso o usuário tenha entrado direto em creation.html sem parâmetro
|
|
alert("Nenhum projeto selecionado na URL. Abra ou Salve um projeto primeiro.");
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
// --- LÓGICA DE UPLOAD DE SAMPLE AVULSO ---
|
|
const uploadSampleBtn = document.getElementById("upload-sample-btn");
|
|
const sampleInput = document.getElementById("sample-file-input");
|
|
|
|
if (uploadSampleBtn && sampleInput) {
|
|
|
|
// 1. Botão clica no input invisível
|
|
uploadSampleBtn.addEventListener("click", () => {
|
|
sampleInput.click();
|
|
});
|
|
|
|
// 2. Quando o arquivo é selecionado
|
|
sampleInput.addEventListener("change", async () => {
|
|
if (sampleInput.files.length === 0) return;
|
|
|
|
const file = sampleInput.files[0];
|
|
|
|
// Pergunta a categoria para organizar no servidor (opcional)
|
|
// O backend usa isso para criar pastas: samples/drums, samples/vocals, etc.
|
|
const category = prompt(
|
|
"Em qual categoria este sample se encaixa? (Ex: drums, effects, vocals)",
|
|
"imported"
|
|
);
|
|
|
|
if (category === null) {
|
|
// Usuário cancelou
|
|
sampleInput.value = "";
|
|
return;
|
|
}
|
|
|
|
// Prepara o formulário
|
|
const formData = new FormData();
|
|
formData.append("sample_file", file); // Deve bater com 'sample_file' no Python
|
|
formData.append("subfolder", category); // Deve bater com 'subfolder' no Python
|
|
|
|
// Feedback Visual (ícone girando ou ficando transparente)
|
|
const originalIcon = uploadSampleBtn.className;
|
|
uploadSampleBtn.className = "fa-solid fa-spinner fa-spin"; // Ícone de loading
|
|
uploadSampleBtn.style.pointerEvents = "none";
|
|
|
|
try {
|
|
const response = await fetch("/api/upload/sample", {
|
|
method: "POST",
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
alert("Sucesso! " + result.message);
|
|
// Opcional: Recarregar a lista de samples lateral se você tiver uma função para isso
|
|
// reloadBrowser();
|
|
} else {
|
|
alert("Erro ao enviar: " + (result.error || "Desconhecido"));
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error("Erro no upload:", error);
|
|
alert("Erro de conexão com o servidor.");
|
|
} finally {
|
|
// Restaura o botão e limpa o input
|
|
uploadSampleBtn.className = originalIcon;
|
|
uploadSampleBtn.style.pointerEvents = "auto";
|
|
sampleInput.value = "";
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</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>
|