plugin v1.0.0

This commit is contained in:
JotaChina committed 2025-11-20 20:54:29 -03:00
1 parent 12c5b55bce
commit 44fce48bb5
12 files changed
+1316 -635

No files matched your search

+249 -167
View File
@@ -1,7 +1,7 @@
// js/pattern_ui.js
import { appState } from "../state.js";
import {
    updateTrackSample
updateTrackSample
} from "./pattern_state.js";
import { playSample, stopPlayback } from "./pattern_audio.js";
import { getTotalSteps } from "../utils.js";
@@ -10,199 +10,292 @@ import { initializeAudioContext } from '../audio.js';
// Função principal de renderização para o editor de patterns
export function renderPatternEditor() {
  const trackContainer = document.getElementById("track-container");
  trackContainer.innerHTML = "";
const trackContainer = document.getElementById("track-container");
trackContainer.innerHTML = "";
// (V7) Adicionado 'trackIndex'
  appState.pattern.tracks.forEach((trackData, trackIndex) => {
    const trackLane = document.createElement("div");
    trackLane.className = "track-lane";
    trackLane.dataset.trackIndex = trackIndex; // (V7) Usando índice
appState.pattern.tracks.forEach((trackData, trackIndex) => {
const trackLane = document.createElement("div");
trackLane.className = "track-lane";
trackLane.dataset.trackIndex = trackIndex;
    if (trackData.id === appState.pattern.activeTrackId) {
        trackLane.classList.add('active-track');
    }
if (trackData.id === appState.pattern.activeTrackId) {
trackLane.classList.add('active-track');
}
    trackLane.innerHTML = `
      <div class="track-info">
        <i class="fa-solid fa-gear"></i>
        <div class="track-mute"></div>
        <span class="track-name">${trackData.name}</span>
      </div>
      <div class="track-controls">
        <div class="knob-container">
          <div class="knob" data-control="volume" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
          <span>VOL</span>
        </div>
        <div class="knob-container">
          <div class="knob" data-control="pan" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
          <span>PAN</span>
        </div>
      </div>
      <div class="step-sequencer-wrapper"></div>
    `;
trackLane.innerHTML = `
<div class="track-info">
<i class="fa-solid fa-gear"></i>
<div class="track-mute"></div>
<span class="track-name">${trackData.name}</span>
</div>
<div class="track-controls">
<div class="knob-container">
<div class="knob" data-control="volume" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
<span>VOL</span>
</div>
<div class="knob-container">
<div class="knob" data-control="pan" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
<span>PAN</span>
</div>
</div>
<div class="step-sequencer-wrapper"></div>
`;
// (Listener de clique da track é local, sem mudanças)
    trackLane.addEventListener('click', () => {
        if (appState.pattern.activeTrackId === trackData.id) return;
        stopPlayback();
        appState.pattern.activeTrackId = trackData.id;
        document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track'));
        trackLane.classList.add('active-track');
        updateGlobalPatternSelector();
        redrawSequencer();
    });
trackLane.addEventListener('click', () => {
if (appState.pattern.activeTrackId === trackData.id) return;
stopPlayback();
appState.pattern.activeTrackId = trackData.id;
document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track'));
trackLane.classList.add('active-track');
updateGlobalPatternSelector();
redrawSequencer();
});
    trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); });
    trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over"));
trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); });
trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over"));
// (V9) Listener de "drop" (arrastar) agora usa 'sendAction'
    trackLane.addEventListener("drop", (e) => {
      e.preventDefault();
      trackLane.classList.remove("drag-over");
      const filePath = e.dataTransfer.getData("text/plain");
     
      if (filePath) {
trackLane.addEventListener("drop", (e) => {
e.preventDefault();
trackLane.classList.remove("drag-over");
const filePath = e.dataTransfer.getData("text/plain");
if (filePath) {
sendAction({
type: 'SET_TRACK_SAMPLE',
trackIndex: trackIndex,
filePath: filePath
});
      }
    });
}
});
    trackContainer.appendChild(trackLane);
  });
 
  updateGlobalPatternSelector();
  redrawSequencer();
trackContainer.appendChild(trackLane);
});
updateGlobalPatternSelector();
redrawSequencer();
}
export function redrawSequencer() {
  const totalGridSteps = getTotalSteps();
  document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => {
    let sequencerContainer = wrapper.querySelector(".step-sequencer");
    if (!sequencerContainer) {
      sequencerContainer = document.createElement("div");
      sequencerContainer.className = "step-sequencer";
      wrapper.appendChild(sequencerContainer);
    }
   
    const parentTrackElement = wrapper.closest(".track-lane");
    const trackIndex = parseInt(parentTrackElement.dataset.trackIndex, 10); // (V7)
    // ... dentro da função redrawSequencer() ...
const totalGridSteps = getTotalSteps();
document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => {
let sequencerContainer = wrapper.querySelector(".step-sequencer");
if (!sequencerContainer) {
sequencerContainer = document.createElement("div");
sequencerContainer.className = "step-sequencer";
wrapper.appendChild(sequencerContainer);
} else {
sequencerContainer.innerHTML = "";
}
const parentTrackElement = wrapper.closest(".track-lane");
const trackIndex = parseInt(parentTrackElement.dataset.trackIndex, 10);
const trackData = appState.pattern.tracks[trackIndex];
    const trackData = appState.pattern.tracks[trackIndex];
    if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
      sequencerContainer.innerHTML = ""; return;
    }
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
return;
}
const activePatternIndex = trackData.activePatternIndex;
    const activePattern = trackData.patterns[activePatternIndex];
const activePattern = trackData.patterns[activePatternIndex];
    if (!activePattern) {
        sequencerContainer.innerHTML = ""; return;
    }
    const patternSteps = activePattern.steps;
// --- INÍCIO DA CORREÇÃO ---
// Precisamos verificar se 'patternSteps' é um array real.
// Se for 'null' ou 'undefined' (um bug de dados do .mmp),
// o loop 'for' abaixo quebraria ANTES de limpar a UI.
if (!patternSteps || !Array.isArray(patternSteps)) {
// Limpa a UI (remove os steps antigos)
sequencerContainer.innerHTML = "";
// E para a execução desta track, deixando o sequenciador vazio.
return;
if (!activePattern) {
return;
}
// --- FIM DA CORREÇÃO ---
    sequencerContainer.innerHTML = ""; // Agora é seguro limpar a UI
    for (let i = 0; i < totalGridSteps; i++) {
      const stepWrapper = document.createElement("div");
      stepWrapper.className = "step-wrapper";
      const stepElement = document.createElement("div");
      stepElement.className = "step";
     
      if (patternSteps[i] === true) {
        stepElement.classList.add("active");
      }
// ============================================================
// LÓGICA DE DECISÃO V2: STEPS OU PIANO ROLL?
// ============================================================
const notes = activePattern.notes || [];
const hasNotes = notes.length > 0;
let renderMode = 'steps';
      stepElement.addEventListener("click", () => {
initializeAudioContext(); // (V8)
if (hasNotes) {
const firstKey = notes[0].key;
const isMelodic = notes.some(n => n.key !== firstKey);
const hasLongNotes = notes.some(n => n.len > 48);
// Sobreposição de notas (Acordes)
const sortedNotes = [...notes].sort((a, b) => a.pos - b.pos);
let hasOverlap = false;
for (let i = 0; i < sortedNotes.length - 1; i++) {
if (sortedNotes[i].pos + sortedNotes[i].len > sortedNotes[i+1].pos) {
hasOverlap = true;
break;
}
}
const currentState = activePattern.steps[i] || false;
const isActive = !currentState;
if (isMelodic || hasLongNotes || hasOverlap) {
renderMode = 'piano_roll';
} else {
renderMode = 'steps';
}
}
sendAction({ // (V7)
type: 'TOGGLE_NOTE',
trackIndex: trackIndex,
patternIndex: activePatternIndex,
stepIndex: i,
isActive: isActive
// ============================================================
// RENDERIZAÇÃO
// ============================================================
if (renderMode === 'piano_roll') {
// --- MODO PIANO ROLL ---
sequencerContainer.classList.add('mode-piano');
const miniView = document.createElement('div');
miniView.className = 'track-mini-piano-roll';
miniView.title = "Clique duplo para abrir o Piano Roll";
miniView.addEventListener('dblclick', (e) => {
e.stopPropagation();
if (window.openPianoRoll) {
window.openPianoRoll(trackData.id);
}
});
        if (isActive && trackData && trackData.samplePath) {
          playSample(trackData.samplePath, trackData.id);
        }
      });
activePattern.notes.forEach(note => {
const noteEl = document.createElement('div');
noteEl.className = 'mini-note';
const barsInput = document.getElementById('bars-input');
const barsCount = barsInput ? parseInt(barsInput.value) || 1 : 1;
const totalTicks = 192 * barsCount;
const leftPercent = (note.pos / totalTicks) * 100;
const widthPercent = (note.len / totalTicks) * 100;
const keyRange = 48;
const baseKey = 36;
let relativeKey = note.key - baseKey;
if(relativeKey < 0) relativeKey = 0;
if(relativeKey > keyRange) relativeKey = keyRange;
const topPercent = 100 - ((relativeKey / keyRange) * 100);
      const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4;
      const groupIndex = Math.floor(i / beatsPerBar);
      if (groupIndex % 2 === 0) {
        stepElement.classList.add("step-dark");
      }
noteEl.style.left = `${leftPercent}%`;
noteEl.style.width = `${widthPercent}%`;
noteEl.style.top = `${topPercent}%`;
miniView.appendChild(noteEl);
});
      const stepsPerBar = 16;
      if (i > 0 && i % stepsPerBar === 0) {
        const marker = document.createElement("div");
        marker.className = "step-marker";
        marker.textContent = Math.floor(i / stepsPerBar) + 1;
        stepWrapper.appendChild(marker);
      }
     
      stepWrapper.appendChild(stepElement);
      sequencerContainer.appendChild(stepWrapper);
    }
  });
sequencerContainer.appendChild(miniView);
} else {
// --- MODO STEP SEQUENCER ---
sequencerContainer.classList.remove('mode-piano');
const patternSteps = activePattern.steps;
if (!patternSteps || !Array.isArray(patternSteps)) return;
for (let i = 0; i < totalGridSteps; i++) {
const stepWrapper = document.createElement("div");
stepWrapper.className = "step-wrapper";
const stepElement = document.createElement("div");
stepElement.className = "step";
if (patternSteps[i] === true) {
stepElement.classList.add("active");
}
// --- EVENTO DE CLIQUE (CORRIGIDO PARA PLUGINS) ---
stepElement.addEventListener("click", (e) => {
e.stopPropagation();
initializeAudioContext();
const currentState = activePattern.steps[i] || false;
const isActive = !currentState;
sendAction({
type: 'TOGGLE_NOTE',
trackIndex: trackIndex,
patternIndex: activePatternIndex,
stepIndex: i,
isActive: isActive
});
// --- AQUI ESTAVA O PROBLEMA: TOCA O SOM ---
if (isActive) {
// Caso 1: Sampler (Áudio)
if (trackData.type === 'sampler' && trackData.samplePath) {
playSample(trackData.samplePath, trackData.id);
}
// Caso 2: Plugin (Sintetizador)
else if (trackData.type === 'plugin' && trackData.instrument) {
// Toca um C5 (Dó) curto para feedback visual/sonoro
try {
// triggerAttackRelease(nota, duração)
trackData.instrument.triggerAttackRelease("C5", "16n");
} catch(err) {
console.warn("Erro ao tocar preview do synth:", err);
}
}
}
});
const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4;
const groupIndex = Math.floor(i / beatsPerBar);
if (groupIndex % 2 === 0) {
stepElement.classList.add("step-dark");
}
const stepsPerBar = 16;
if (i > 0 && i % stepsPerBar === 0) {
const marker = document.createElement("div");
marker.className = "step-marker";
marker.textContent = Math.floor(i / stepsPerBar) + 1;
stepWrapper.appendChild(marker);
}
stepWrapper.appendChild(stepElement);
sequencerContainer.appendChild(stepWrapper);
}
}
});
}
export function highlightStep(stepIndex, isActive) {
  if (stepIndex < 0) return;
  document.querySelectorAll(".track-lane").forEach((track) => {
    const stepWrapper = track.querySelector(
      `.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`
    );
    if (stepWrapper) {
      const stepElement = stepWrapper.querySelector(".step");
      if (stepElement) {
        stepElement.classList.toggle("playing", isActive);
      }
    }
  });
if (stepIndex < 0) return;
document.querySelectorAll(".track-lane").forEach((track) => {
const stepWrapper = track.querySelector(
`.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`
);
if (stepWrapper) {
const stepElement = stepWrapper.querySelector(".step");
if (stepElement) {
stepElement.classList.toggle("playing", isActive);
}
}
});
}
export function updateStepUI(trackIndex, patternIndex, stepIndex, isActive) {
// --- INÍCIO DA CORREÇÃO ---
// A lógica antiga (if (patternIndex !== appState.pattern.activePatternIndex))
// estava errada, pois usava uma variável global.
const trackElement = document.querySelector(`.track-lane[data-track-index="${trackIndex}"]`);
if (!trackElement) return;
const trackData = appState.pattern.tracks[trackIndex];
if (!trackData) return;
// A UI só deve ser atualizada cirurgicamente se o pattern clicado
// for o MESMO pattern que está VISÍVEL no sequenciador dessa trilha.
if (patternIndex !== trackData.activePatternIndex) {
// O estado mudou, mas não é o pattern que estamos vendo,
// então não faz nada na UI (mas o estado no appState está correto).
return;
const activePattern = trackData.patterns[patternIndex];
const notes = activePattern.notes || [];
const hasNotes = notes.length > 0;
let isComplex = false;
if (hasNotes) {
const isMelodic = notes.some(n => n.key !== notes[0].key);
const hasLongNotes = notes.some(n => n.len > 48);
if (isMelodic || hasLongNotes) isComplex = true;
}
// --- FIM DA CORREÇÃO ---
if (isComplex) {
redrawSequencer();
return;
}
if (patternIndex !== trackData.activePatternIndex) return;
const stepWrapper = trackElement.querySelector(
`.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`
@@ -217,38 +310,27 @@ export function updateGlobalPatternSelector() {
const globalPatternSelector = document.getElementById('global-pattern-selector');
if (!globalPatternSelector) return;
// 1. Encontra a track que está ATIVA no momento
const activeTrackId = appState.pattern.activeTrackId;
const activeTrack = appState.pattern.tracks.find(t => t.id === activeTrackId);
// 2. Usa a track[0] como referência para os NOMES dos patterns
const referenceTrack = appState.pattern.tracks[0];
globalPatternSelector.innerHTML = ''; // Limpa as <options> anteriores
globalPatternSelector.innerHTML = '';
if (referenceTrack && referenceTrack.patterns.length > 0) {
// 3. Popula a lista de <option>
referenceTrack.patterns.forEach((pattern, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = pattern.name; // ex: "Pattern 1"
option.textContent = pattern.name;
globalPatternSelector.appendChild(option);
});
// 4. CORREÇÃO PRINCIPAL: Define o item selecionado no <select>
if (activeTrack) {
// O valor do seletor (ex: "2") deve ser igual ao índice
// do pattern ativo da track selecionada.
globalPatternSelector.value = activeTrack.activePatternIndex || 0;
} else {
globalPatternSelector.value = 0; // Padrão
globalPatternSelector.value = 0;
}
globalPatternSelector.disabled = false;
} else {
// 5. Estado desabilitado (nenhum pattern)
const option = document.createElement('option');
option.textContent = 'Sem patterns';
globalPatternSelector.appendChild(option);