// js/pattern_ui.js import { appState } from "../state.js"; import { updateTrackSample } from "./pattern_state.js"; import { playSample, stopPlayback } from "./pattern_audio.js"; import { getTotalSteps } from "../utils.js"; import { sendAction } from '../socket.js'; import { initializeAudioContext } from '../audio.js'; import * as Tone from "https://esm.sh/tone"; // Função principal de renderização para o editor de patterns export function renderPatternEditor() { const trackContainer = document.getElementById("track-container"); trackContainer.innerHTML = ""; 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'); } trackLane.innerHTML = `
${trackData.name}
VOL
PAN
`; 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("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(); } 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); } else { sequencerContainer.innerHTML = ""; } const parentTrackElement = wrapper.closest(".track-lane"); const trackIndex = parseInt(parentTrackElement.dataset.trackIndex, 10); const trackData = appState.pattern.tracks[trackIndex]; if (!trackData || !trackData.patterns || trackData.patterns.length === 0) { return; } const activePatternIndex = trackData.activePatternIndex; const activePattern = trackData.patterns[activePatternIndex]; if (!activePattern) { return; } // ============================================================ // LÓGICA DE DECISÃO V2: STEPS OU PIANO ROLL? (MANTIDA) // ============================================================ const notes = activePattern.notes || []; const hasNotes = notes.length > 0; let renderMode = 'steps'; if (hasNotes) { const firstKey = notes[0].key; const isMelodic = notes.some(n => n.key !== firstKey); const hasLongNotes = notes.some(n => n.len > 48); 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; } } if (isMelodic || hasLongNotes || hasOverlap) { renderMode = 'piano_roll'; } else { renderMode = 'steps'; } } // ============================================================ // 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); } }); // --- CÁLCULO REVERTIDO (VOLTA AO PADRÃO ANTERIOR) --- const barsInput = document.getElementById('bars-input'); const barsCount = barsInput ? parseInt(barsInput.value) || 1 : 1; // Revertido: Removemos a multiplicação por beatsPerBar que causou o bug visual const totalTicks = 192 * barsCount; // ---------------------------------------------------- activePattern.notes.forEach(note => { const noteEl = document.createElement('div'); noteEl.className = 'mini-note'; 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); noteEl.style.left = `${leftPercent}%`; noteEl.style.width = `${widthPercent}%`; noteEl.style.top = `${topPercent}%`; miniView.appendChild(noteEl); }); 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"); } 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 }); if (isActive) { if (trackData.type === 'sampler' && trackData.samplePath) { playSample(trackData.samplePath, trackData.id); } else if (trackData.type === 'plugin' && trackData.instrument) { try { trackData.instrument.triggerAttackRelease("C5", "16n", Tone.now()); } 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); } } }); } export function updateStepUI(trackIndex, patternIndex, stepIndex, isActive) { const trackElement = document.querySelector(`.track-lane[data-track-index="${trackIndex}"]`); if (!trackElement) return; const trackData = appState.pattern.tracks[trackIndex]; if (!trackData) 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; } if (isComplex) { redrawSequencer(); return; } if (patternIndex !== trackData.activePatternIndex) return; const stepWrapper = trackElement.querySelector( `.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})` ); if (!stepWrapper) return; const stepElement = stepWrapper.querySelector(".step"); if (!stepElement) return; stepElement.classList.toggle("active", isActive); } export function updateGlobalPatternSelector() { const globalPatternSelector = document.getElementById('global-pattern-selector'); if (!globalPatternSelector) return; const activeTrackId = appState.pattern.activeTrackId; const activeTrack = appState.pattern.tracks.find(t => t.id === activeTrackId); const referenceTrack = appState.pattern.tracks[0]; globalPatternSelector.innerHTML = ''; if (referenceTrack && referenceTrack.patterns.length > 0) { referenceTrack.patterns.forEach((pattern, index) => { const option = document.createElement('option'); option.value = index; option.textContent = pattern.name; globalPatternSelector.appendChild(option); }); if (activeTrack) { globalPatternSelector.value = activeTrack.activePatternIndex || 0; } else { globalPatternSelector.value = 0; } globalPatternSelector.disabled = false; } else { const option = document.createElement('option'); option.textContent = 'Sem patterns'; globalPatternSelector.appendChild(option); globalPatternSelector.disabled = true; } }