sync mode on
This commit is contained in:
1 parent
1818932244
commit
1174b328e5
415 files changed
+44294
-1182
No files matched your search
@@ -1,4 +1,6 @@
|
||||
// js/pattern_audio.js
|
||||
import * as Tone from "https://esm.sh/tone";
|
||||
|
||||
import { appState } from "../state.js";
|
||||
import { highlightStep } from "./pattern_ui.js";
|
||||
import { getTotalSteps } from "../utils.js";
|
||||
@@ -21,24 +23,39 @@ export function playMetronomeSound(isDownbeat) {
|
||||
synth.triggerAttackRelease(freq, "8n", Tone.now());
|
||||
}
|
||||
|
||||
// --- FUNÇÃO CORRIGIDA E EFICIENTE ---
|
||||
// Dispara o sample de uma track, garantindo que o player esteja roteado corretamente
|
||||
export function playSample(filePath, trackId) {
|
||||
initializeAudioContext();
|
||||
const track = trackId ? appState.pattern.tracks.find((t) => t.id == trackId) : null;
|
||||
|
||||
// Se a faixa existe e tem um player pré-carregado, apenas o dispara.
|
||||
// Se a faixa existe e tem um player pré-carregado
|
||||
if (track && track.player) {
|
||||
// Atualiza o volume/pan caso tenham sido alterados
|
||||
track.gainNode.gain.value = Tone.gainToDb(track.volume);
|
||||
track.pannerNode.pan.value = track.pan;
|
||||
|
||||
// Dispara o som imediatamente. Esta operação é instantânea.
|
||||
track.player.start(Tone.now());
|
||||
}
|
||||
// Fallback para preview de samples no navegador (sem trackId)
|
||||
if (track.player.loaded) {
|
||||
// Ajusta volume/pan sempre que tocar (robustez a alterações em tempo real)
|
||||
if (track.volumeNode) {
|
||||
track.volumeNode.volume.value =
|
||||
track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume);
|
||||
}
|
||||
if (track.pannerNode) {
|
||||
track.pannerNode.pan.value = track.pan ?? 0;
|
||||
}
|
||||
|
||||
// Garante conexão: player -> volumeNode (não usar mais gainNode)
|
||||
try { track.player.disconnect(); } catch {}
|
||||
if (track.volumeNode) {
|
||||
track.player.connect(track.volumeNode);
|
||||
}
|
||||
|
||||
// Dispara imediatamente
|
||||
track.player.start(Tone.now());
|
||||
} else {
|
||||
console.warn(`Player da trilha "${track.name}" ainda não carregado — pulando este tick.`);
|
||||
}
|
||||
}
|
||||
// Fallback para preview de sample sem trackId
|
||||
else if (!trackId && filePath) {
|
||||
const previewPlayer = new Tone.Player(filePath).toDestination();
|
||||
previewPlayer.autostart = true;
|
||||
const previewPlayer = new Tone.Player(filePath).toDestination();
|
||||
previewPlayer.autostart = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +76,7 @@ function tick() {
|
||||
timerDisplay.textContent = formatTime(currentTime);
|
||||
}
|
||||
|
||||
// Metrônomo
|
||||
if (appState.global.metronomeEnabled) {
|
||||
const noteValue = parseInt(document.getElementById("compasso-b-input").value, 10) || 4;
|
||||
const stepsPerBeat = 16 / noteValue;
|
||||
@@ -67,12 +85,16 @@ function tick() {
|
||||
}
|
||||
}
|
||||
|
||||
// Percorre tracks e toca o step atual se ativo
|
||||
appState.pattern.tracks.forEach((track) => {
|
||||
if (!track.patterns || track.patterns.length === 0) return;
|
||||
|
||||
const activePattern = track.patterns[appState.pattern.activePatternIndex];
|
||||
|
||||
if (activePattern && activePattern.steps[appState.global.currentStep] && track.samplePath) {
|
||||
// IMPORTANTE: usar o pattern ativo da PRÓPRIA TRILHA
|
||||
const activePattern = track.patterns[track.activePatternIndex];
|
||||
|
||||
if (activePattern &&
|
||||
activePattern.steps[appState.global.currentStep] &&
|
||||
track.samplePath) {
|
||||
playSample(track.samplePath, track.id);
|
||||
}
|
||||
});
|
||||
@@ -84,9 +106,9 @@ function tick() {
|
||||
export function startPlayback() {
|
||||
if (appState.global.isPlaying || appState.pattern.tracks.length === 0) return;
|
||||
initializeAudioContext();
|
||||
|
||||
|
||||
if (appState.global.currentStep === 0) {
|
||||
rewindPlayback();
|
||||
rewindPlayback();
|
||||
}
|
||||
|
||||
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
||||
@@ -96,15 +118,18 @@ export function startPlayback() {
|
||||
if (appState.global.playbackIntervalId) clearInterval(appState.global.playbackIntervalId);
|
||||
|
||||
appState.global.isPlaying = true;
|
||||
document.getElementById("play-btn").classList.remove("fa-play");
|
||||
document.getElementById("play-btn").classList.add("fa-pause");
|
||||
const playBtn = document.getElementById("play-btn");
|
||||
if (playBtn) {
|
||||
playBtn.classList.remove("fa-play");
|
||||
playBtn.classList.add("fa-pause");
|
||||
}
|
||||
|
||||
tick();
|
||||
appState.global.playbackIntervalId = setInterval(tick, stepInterval);
|
||||
}
|
||||
|
||||
export function stopPlayback() {
|
||||
if(appState.global.playbackIntervalId) {
|
||||
if (appState.global.playbackIntervalId) {
|
||||
clearInterval(appState.global.playbackIntervalId);
|
||||
}
|
||||
appState.global.playbackIntervalId = null;
|
||||
@@ -138,4 +163,4 @@ export function togglePlayback() {
|
||||
appState.global.currentStep = 0;
|
||||
startPlayback();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// js/pattern/pattern_bounce.js
|
||||
import { appState } from '../state.js';
|
||||
import { getSecondsPerStep } from '../utils.js';
|
||||
import { addAudioClipToTimeline, addAudioTrackLane } from '../audio/audio_state.js';
|
||||
import { getAudioContext } from '../audio.js';
|
||||
import { renderAudioEditor } from '../audio/audio_ui.js';
|
||||
|
||||
/**
|
||||
* Renderiza (bounce) o pattern de beat atualmente ativo para uma nova pista de áudio.
|
||||
*/
|
||||
export async function bounceActivePatternToAudio() {
|
||||
console.log("Iniciando 'bounce' do pattern...");
|
||||
|
||||
// 1. Encontrar o pattern ativo
|
||||
const activeTrackId = appState.pattern.activeTrackId;
|
||||
|
||||
// --- DEBUG ---
|
||||
console.log(`[DEBUG bounce] activeTrackId lido do estado: ${activeTrackId}`);
|
||||
|
||||
const activeTrack = appState.pattern.tracks.find(t => t.id === activeTrackId);
|
||||
|
||||
// --- DEBUG ---
|
||||
if (activeTrack) {
|
||||
console.log(`[DEBUG bounce] Pista ativa encontrada:`, activeTrack.name);
|
||||
console.log(`[DEBUG bounce] Verificando track.buffer:`, activeTrack.buffer);
|
||||
} else {
|
||||
console.error(`[DEBUG bounce] NENHUMA PISTA ATIVA ENCONTRADA. activeTrackId é nulo ou inválido.`);
|
||||
console.log(`[DEBUG bounce] Pistas disponíveis no estado:`, appState.pattern.tracks.map(t => ({id: t.id, name: t.name})));
|
||||
}
|
||||
// --- FIM DEBUG ---
|
||||
|
||||
if (!activeTrack) {
|
||||
alert('Nenhuma pista de pattern selecionada para renderizar.');
|
||||
return;
|
||||
}
|
||||
|
||||
const activePattern = activeTrack.patterns[activeTrack.activePatternIndex];
|
||||
if (!activePattern) {
|
||||
alert('Nenhum pattern ativo encontrado na pista.');
|
||||
return;
|
||||
}
|
||||
|
||||
const trackBuffer = activeTrack.buffer;
|
||||
if (!trackBuffer) {
|
||||
alert('O áudio (sample) desta pista ainda não foi carregado.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Calcular a duração do pattern
|
||||
const steps = activePattern.steps;
|
||||
const totalSteps = steps.length;
|
||||
const secondsPerStep = getSecondsPerStep(); //
|
||||
const duration = totalSteps * secondsPerStep;
|
||||
|
||||
if (duration <= 0) {
|
||||
alert("Pattern está vazio ou com duração zero.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 3. Usar Tone.Offline para renderizar o áudio
|
||||
const audioBuffer = await Tone.Offline(async (offlineCtx) => {
|
||||
|
||||
const gainNode = new Tone.Gain(Tone.gainToDb(activeTrack.volume)).connect(offlineCtx.destination);
|
||||
const pannerNode = new Tone.Panner(activeTrack.pan).connect(gainNode); //
|
||||
|
||||
const now = offlineCtx.currentTime;
|
||||
|
||||
// --- INÍCIO DA CORREÇÃO (que estava na versão anterior) ---
|
||||
const offlineBuffer = offlineCtx.createBuffer(
|
||||
trackBuffer.numberOfChannels,
|
||||
trackBuffer.length,
|
||||
trackBuffer.sampleRate
|
||||
);
|
||||
|
||||
for (let i = 0; i < trackBuffer.numberOfChannels; i++) {
|
||||
offlineBuffer.copyToChannel(trackBuffer.getChannelData(i), i);
|
||||
}
|
||||
// --- FIM DA CORREÇÃO ---
|
||||
|
||||
// Agendar todas as notas (steps)
|
||||
steps.forEach((isActive, index) => {
|
||||
if (isActive) {
|
||||
const time = now + (index * secondsPerStep);
|
||||
const source = new Tone.BufferSource(offlineBuffer).connect(pannerNode);
|
||||
source.start(time);
|
||||
}
|
||||
});
|
||||
|
||||
}, duration);
|
||||
|
||||
// 4. Áudio renderizado. Agora, adicione-o ao editor de áudio.
|
||||
|
||||
addAudioTrackLane(); //
|
||||
const newTrack = appState.audio.tracks[appState.audio.tracks.length - 1]; //
|
||||
const newTrackId = newTrack.id;
|
||||
|
||||
const clipName = `${activeTrack.name}_(${activePattern.name})`;
|
||||
|
||||
addAudioClipToTimeline(null, newTrackId, 0, clipName, audioBuffer); //
|
||||
|
||||
console.log("Pattern renderizado com sucesso!");
|
||||
|
||||
} catch (err) {
|
||||
console.error("Erro ao renderizar o pattern:", err);
|
||||
alert(`Erro ao renderizar pattern: ${err.message}`);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +1,148 @@
|
||||
// js/pattern_state.js
|
||||
import * as Tone from "https://esm.sh/tone";
|
||||
|
||||
import { appState } from "../state.js";
|
||||
import { DEFAULT_VOLUME, DEFAULT_PAN } from "../config.js";
|
||||
import { getAudioContext, getMainGainNode } from "../audio.js";
|
||||
import { renderPatternEditor } from "./pattern_ui.js";
|
||||
import { getMainGainNode } from "../audio.js";
|
||||
import { getTotalSteps } from "../utils.js";
|
||||
|
||||
const initialState = {
|
||||
tracks: [],
|
||||
activeTrackId: null,
|
||||
activePatternIndex: 0,
|
||||
};
|
||||
|
||||
export let patternState = { ...initialState };
|
||||
|
||||
export function initializePatternState() {
|
||||
Object.assign(patternState, initialState, { tracks: [] });
|
||||
// Limpa players/buffers existentes
|
||||
appState.pattern.tracks.forEach(track => {
|
||||
try { track.player?.dispose(); } catch {}
|
||||
try { track.buffer?.dispose?.(); } catch {}
|
||||
});
|
||||
|
||||
// Reseta estado do editor de pattern
|
||||
appState.pattern.tracks = [];
|
||||
appState.pattern.activeTrackId = null;
|
||||
appState.pattern.activePatternIndex = 0;
|
||||
}
|
||||
|
||||
// --- FUNÇÃO CORRIGIDA ---
|
||||
// Agora, esta função cria e pré-carrega um Tone.Player para a faixa.
|
||||
export async function loadAudioForTrack(track) {
|
||||
if (!track.samplePath) return track;
|
||||
|
||||
try {
|
||||
// Se já existir um player antigo, o descartamos para liberar memória.
|
||||
if (track.player) {
|
||||
track.player.dispose();
|
||||
}
|
||||
|
||||
// Cria um novo Tone.Player e o conecta à cadeia de áudio da faixa.
|
||||
// O 'await' garante que o áudio seja totalmente carregado antes de prosseguirmos.
|
||||
track.player = await new Tone.Player(track.samplePath).toDestination();
|
||||
track.player.chain(track.gainNode, track.pannerNode, getMainGainNode());
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Falha ao carregar áudio para a trilha ${track.name}:`, error);
|
||||
// Descartar player/buffer anteriores com segurança
|
||||
try { track.player?.dispose(); } catch {}
|
||||
track.player = null;
|
||||
try { track.buffer?.dispose?.(); } catch {}
|
||||
track.buffer = null;
|
||||
|
||||
// Garante nós de volume/pan (Opção B: Volume em dB)
|
||||
if (!track.volumeNode) {
|
||||
track.volumeNode = new Tone.Volume(
|
||||
track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume)
|
||||
);
|
||||
} else {
|
||||
track.volumeNode.volume.value =
|
||||
track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume);
|
||||
}
|
||||
|
||||
if (!track.pannerNode) {
|
||||
track.pannerNode = new Tone.Panner(track.pan ?? 0);
|
||||
} else {
|
||||
track.pannerNode.pan.value = track.pan ?? 0;
|
||||
}
|
||||
|
||||
// Encadeia: Volume(dB) -> Panner -> Master
|
||||
try { track.volumeNode.disconnect(); } catch {}
|
||||
try { track.pannerNode.disconnect(); } catch {}
|
||||
track.volumeNode.connect(track.pannerNode);
|
||||
track.pannerNode.connect(getMainGainNode());
|
||||
|
||||
// Cria e carrega o Player
|
||||
const player = new Tone.Player({ url: track.samplePath, autostart: false });
|
||||
await player.load(track.samplePath); // garante buffer carregado
|
||||
|
||||
// Conecta o player ao volumeNode
|
||||
player.connect(track.volumeNode);
|
||||
|
||||
// Buffer separado (se você usar waveform em outro lugar)
|
||||
const buffer = new Tone.Buffer();
|
||||
await buffer.load(track.samplePath);
|
||||
|
||||
// Atribuições finais
|
||||
track.player = player;
|
||||
track.buffer = buffer;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar sample:', track.samplePath);
|
||||
console.error(`Falha ao carregar áudio para a trilha "${track.name}":`, error);
|
||||
try { track.player?.dispose(); } catch {}
|
||||
try { track.buffer?.dispose?.(); } catch {}
|
||||
track.player = null;
|
||||
track.buffer = null;
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
export function addTrackToState() {
|
||||
const mainGainNode = getMainGainNode();
|
||||
const totalSteps = getTotalSteps();
|
||||
const referenceTrack = patternState.tracks[0];
|
||||
const referenceTrack = appState.pattern.tracks[0];
|
||||
|
||||
const newTrack = {
|
||||
id: Date.now(),
|
||||
id: Date.now() + Math.random(),
|
||||
name: "novo instrumento",
|
||||
samplePath: null,
|
||||
player: null, // <-- ADICIONADO: O player começará como nulo
|
||||
patterns: referenceTrack
|
||||
? referenceTrack.patterns.map(p => ({ name: p.name, steps: new Array(p.steps.length).fill(false), pos: p.pos }))
|
||||
player: null,
|
||||
buffer: null,
|
||||
patterns: referenceTrack
|
||||
? referenceTrack.patterns.map(p => ({
|
||||
name: p.name,
|
||||
steps: new Array(p.steps.length).fill(false),
|
||||
pos: p.pos
|
||||
}))
|
||||
: [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), pos: 0 }],
|
||||
activePatternIndex: 0,
|
||||
volume: DEFAULT_VOLUME,
|
||||
pan: DEFAULT_PAN,
|
||||
gainNode: new Tone.Gain(Tone.gainToDb(DEFAULT_VOLUME)),
|
||||
// Opção B: controlar volume em dB
|
||||
volumeNode: new Tone.Volume(Tone.gainToDb(DEFAULT_VOLUME)),
|
||||
pannerNode: new Tone.Panner(DEFAULT_PAN),
|
||||
};
|
||||
|
||||
newTrack.gainNode.chain(newTrack.pannerNode, mainGainNode);
|
||||
// Cadeia de áudio nova
|
||||
newTrack.volumeNode.connect(newTrack.pannerNode);
|
||||
newTrack.pannerNode.connect(getMainGainNode());
|
||||
|
||||
patternState.tracks.push(newTrack);
|
||||
renderPatternEditor();
|
||||
appState.pattern.tracks.push(newTrack);
|
||||
appState.pattern.activeTrackId = newTrack.id;
|
||||
}
|
||||
|
||||
export function removeLastTrackFromState() {
|
||||
if (patternState.tracks.length > 0) {
|
||||
const trackToRemove = patternState.tracks[patternState.tracks.length - 1];
|
||||
if (trackToRemove.player) trackToRemove.player.dispose();
|
||||
if (trackToRemove.pannerNode) trackToRemove.pannerNode.dispose();
|
||||
if (trackToRemove.gainNode) trackToRemove.gainNode.dispose();
|
||||
|
||||
patternState.tracks.pop();
|
||||
renderPatternEditor();
|
||||
if (appState.pattern.tracks.length > 0) {
|
||||
const trackToRemove = appState.pattern.tracks[appState.pattern.tracks.length - 1];
|
||||
|
||||
try { trackToRemove.player?.dispose(); } catch {}
|
||||
try { trackToRemove.buffer?.dispose?.(); } catch {}
|
||||
try { trackToRemove.pannerNode?.disconnect(); } catch {}
|
||||
try { trackToRemove.volumeNode?.disconnect(); } catch {}
|
||||
|
||||
appState.pattern.tracks.pop();
|
||||
if (appState.pattern.activeTrackId === trackToRemove.id) {
|
||||
appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTrackSample(trackId, samplePath) {
|
||||
const track = patternState.tracks.find((t) => t.id == trackId);
|
||||
export async function updateTrackSample(trackIndex, samplePath) {
|
||||
const track = appState.pattern.tracks[trackIndex];
|
||||
if (track) {
|
||||
track.samplePath = samplePath;
|
||||
track.name = samplePath.split("/").pop();
|
||||
await loadAudioForTrack(track); // Carrega o novo player
|
||||
renderPatternEditor();
|
||||
|
||||
// (re)carrega e reconecta corretamente o player nesta trilha
|
||||
await loadAudioForTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleStepState(trackId, stepIndex) {
|
||||
const track = patternState.tracks.find((t) => t.id == trackId);
|
||||
export function toggleStepState(trackIndex, stepIndex) {
|
||||
const track = appState.pattern.tracks[trackIndex];
|
||||
if (track && track.patterns && track.patterns.length > 0) {
|
||||
const activePattern = track.patterns[track.activePatternIndex];
|
||||
if (activePattern && activePattern.steps.length > stepIndex) {
|
||||
activePattern.steps[stepIndex] = !activePattern.steps[stepIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,171 +1,218 @@
|
||||
// js/pattern_ui.js
|
||||
import { appState } from "../state.js";
|
||||
import {
|
||||
toggleStepState,
|
||||
updateTrackSample
|
||||
updateTrackSample
|
||||
} from "./pattern_state.js";
|
||||
import { playSample, stopPlayback } from "./pattern_audio.js"; // Será criado no próximo passo
|
||||
import { playSample, stopPlayback } from "./pattern_audio.js";
|
||||
import { getTotalSteps } from "../utils.js";
|
||||
import { sendAction } from '../socket.js';
|
||||
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 = "";
|
||||
|
||||
appState.pattern.tracks.forEach((trackData) => {
|
||||
const trackLane = document.createElement("div");
|
||||
trackLane.className = "track-lane";
|
||||
trackLane.dataset.trackId = trackData.id;
|
||||
// (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
|
||||
|
||||
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>
|
||||
`;
|
||||
|
||||
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();
|
||||
});
|
||||
// (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("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) {
|
||||
updateTrackSample(trackData.id, filePath);
|
||||
}
|
||||
});
|
||||
trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); });
|
||||
trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over"));
|
||||
|
||||
trackContainer.appendChild(trackLane);
|
||||
// A lógica dos knobs precisará ser reimplementada ou movida para um arquivo de componentes
|
||||
});
|
||||
|
||||
updateGlobalPatternSelector();
|
||||
redrawSequencer();
|
||||
// (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) {
|
||||
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);
|
||||
}
|
||||
|
||||
const parentTrackElement = wrapper.closest(".track-lane");
|
||||
const trackId = parentTrackElement.dataset.trackId;
|
||||
const trackData = appState.pattern.tracks.find((t) => t.id == trackId);
|
||||
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() ...
|
||||
|
||||
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
|
||||
sequencerContainer.innerHTML = ""; return;
|
||||
}
|
||||
const trackData = appState.pattern.tracks[trackIndex];
|
||||
|
||||
const activePattern = trackData.patterns[appState.pattern.activePatternIndex];
|
||||
if (!activePattern) {
|
||||
sequencerContainer.innerHTML = ""; return;
|
||||
}
|
||||
const patternSteps = activePattern.steps;
|
||||
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
|
||||
sequencerContainer.innerHTML = ""; return;
|
||||
}
|
||||
|
||||
sequencerContainer.innerHTML = "";
|
||||
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");
|
||||
}
|
||||
// --- CORRIJA ESTAS DUAS LINHAS ---
|
||||
// ANTES:
|
||||
// const activePatternIndex = appState.pattern.activePatternIndex;
|
||||
// const activePattern = trackData.patterns[activePatternIndex];
|
||||
//
|
||||
// DEPOIS:
|
||||
const activePatternIndex = trackData.activePatternIndex;
|
||||
const activePattern = trackData.patterns[activePatternIndex];
|
||||
|
||||
stepElement.addEventListener("click", () => {
|
||||
toggleStepState(trackData.id, i);
|
||||
stepElement.classList.toggle("active");
|
||||
if (trackData && trackData.samplePath) {
|
||||
playSample(trackData.samplePath, trackData.id);
|
||||
}
|
||||
});
|
||||
if (!activePattern) {
|
||||
sequencerContainer.innerHTML = ""; return;
|
||||
}
|
||||
// ... resto da função ...
|
||||
const patternSteps = activePattern.steps;
|
||||
|
||||
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");
|
||||
}
|
||||
sequencerContainer.innerHTML = "";
|
||||
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");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
stepElement.addEventListener("click", () => {
|
||||
initializeAudioContext(); // (V8)
|
||||
|
||||
const currentState = activePattern.steps[i] || false;
|
||||
const isActive = !currentState;
|
||||
|
||||
sendAction({ // (V7)
|
||||
type: 'TOGGLE_NOTE',
|
||||
trackIndex: trackIndex,
|
||||
patternIndex: activePatternIndex,
|
||||
stepIndex: i,
|
||||
isActive: isActive
|
||||
});
|
||||
|
||||
if (isActive && trackData && trackData.samplePath) {
|
||||
playSample(trackData.samplePath, trackData.id);
|
||||
}
|
||||
});
|
||||
|
||||
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 updateGlobalPatternSelector() {
|
||||
const globalPatternSelector = document.getElementById('global-pattern-selector');
|
||||
if (!globalPatternSelector) return;
|
||||
const globalPatternSelector = document.getElementById('global-pattern-selector');
|
||||
if (!globalPatternSelector) return;
|
||||
|
||||
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);
|
||||
});
|
||||
globalPatternSelector.selectedIndex = appState.pattern.activePatternIndex;
|
||||
globalPatternSelector.disabled = false;
|
||||
} else {
|
||||
const option = document.createElement('option');
|
||||
option.textContent = 'Sem patterns';
|
||||
globalPatternSelector.appendChild(option);
|
||||
globalPatternSelector.disabled = true;
|
||||
}
|
||||
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);
|
||||
});
|
||||
globalPatternSelector.selectedIndex = appState.pattern.activePatternIndex;
|
||||
globalPatternSelector.disabled = false;
|
||||
} else {
|
||||
const option = document.createElement('option');
|
||||
option.textContent = 'Sem patterns';
|
||||
globalPatternSelector.appendChild(option);
|
||||
globalPatternSelector.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// (V7) Função de UI "cirúrgica"
|
||||
export function updateStepUI(trackIndex, patternIndex, stepIndex, isActive) {
|
||||
if (patternIndex !== appState.pattern.activePatternIndex) {
|
||||
return;
|
||||
}
|
||||
const trackElement = document.querySelector(`.track-lane[data-track-index="${trackIndex}"]`);
|
||||
if (!trackElement) 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);
|
||||
}
|
||||
Reference in new issue
Block a user