mmpSearch/pages/creation.md

26 KiB

<html lang="pt-br"> <head>
<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>
<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">&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;
  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 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>
</html>