versão 2.0

This commit is contained in:
JotaChina committed 2025-10-20 21:53:09 -03:00
1 parent d506332268
commit fe735bf0a6
16 files changed
+1735 -1626

No files matched your search

@@ -0,0 +1,141 @@
// js/pattern_audio.js
import { appState } from "../state.js";
import { highlightStep } from "./pattern_ui.js";
import { getTotalSteps } from "../utils.js";
import { initializeAudioContext } from "../audio.js";
const timerDisplay = document.getElementById('timer-display');
function formatTime(milliseconds) {
const totalSeconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
const seconds = (totalSeconds % 60).toString().padStart(2, '0');
const centiseconds = Math.floor((milliseconds % 1000) / 10).toString().padStart(2, '0');
return `${minutes}:${seconds}:${centiseconds}`;
}
export function playMetronomeSound(isDownbeat) {
initializeAudioContext();
const synth = new Tone.Synth().toDestination();
const freq = isDownbeat ? 1000 : 800;
synth.triggerAttackRelease(freq, "8n", Tone.now());
}
// --- FUNÇÃO CORRIGIDA E EFICIENTE ---
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.
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)
else if (!trackId && filePath) {
const previewPlayer = new Tone.Player(filePath).toDestination();
previewPlayer.autostart = true;
}
}
function tick() {
if (!appState.global.isPlaying) {
stopPlayback();
return;
}
const totalSteps = getTotalSteps();
const lastStepIndex = appState.global.currentStep === 0 ? totalSteps - 1 : appState.global.currentStep - 1;
highlightStep(lastStepIndex, false);
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
const stepInterval = (60 * 1000) / (bpm * 4);
const currentTime = appState.global.currentStep * stepInterval;
if (timerDisplay) {
timerDisplay.textContent = formatTime(currentTime);
}
if (appState.global.metronomeEnabled) {
const noteValue = parseInt(document.getElementById("compasso-b-input").value, 10) || 4;
const stepsPerBeat = 16 / noteValue;
if (appState.global.currentStep % stepsPerBeat === 0) {
playMetronomeSound(appState.global.currentStep % (stepsPerBeat * 4) === 0);
}
}
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) {
playSample(track.samplePath, track.id);
}
});
highlightStep(appState.global.currentStep, true);
appState.global.currentStep = (appState.global.currentStep + 1) % totalSteps;
}
export function startPlayback() {
if (appState.global.isPlaying || appState.pattern.tracks.length === 0) return;
initializeAudioContext();
if (appState.global.currentStep === 0) {
rewindPlayback();
}
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
Tone.Transport.bpm.value = bpm;
const stepInterval = (60 * 1000) / (bpm * 4);
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");
tick();
appState.global.playbackIntervalId = setInterval(tick, stepInterval);
}
export function stopPlayback() {
if(appState.global.playbackIntervalId) {
clearInterval(appState.global.playbackIntervalId);
}
appState.global.playbackIntervalId = null;
appState.global.isPlaying = false;
document.querySelectorAll('.step.playing').forEach(s => s.classList.remove('playing'));
appState.global.currentStep = 0;
if (timerDisplay) timerDisplay.textContent = '00:00:00';
const playBtn = document.getElementById("play-btn");
if (playBtn) {
playBtn.classList.remove("fa-pause");
playBtn.classList.add("fa-play");
}
}
export function rewindPlayback() {
const lastStep = appState.global.currentStep > 0 ? appState.global.currentStep - 1 : getTotalSteps() - 1;
appState.global.currentStep = 0;
if (!appState.global.isPlaying) {
if (timerDisplay) timerDisplay.textContent = '00:00:00';
highlightStep(lastStep, false);
}
}
export function togglePlayback() {
initializeAudioContext();
if (appState.global.isPlaying) {
stopPlayback();
} else {
appState.global.currentStep = 0;
startPlayback();
}
}
@@ -0,0 +1,97 @@
// js/pattern_state.js
import { DEFAULT_VOLUME, DEFAULT_PAN } from "../config.js";
import { getAudioContext, getMainGainNode } from "../audio.js";
import { renderPatternEditor } from "./pattern_ui.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: [] });
}
// --- 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);
track.player = null;
}
return track;
}
export function addTrackToState() {
const mainGainNode = getMainGainNode();
const totalSteps = getTotalSteps();
const referenceTrack = patternState.tracks[0];
const newTrack = {
id: Date.now(),
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 }))
: [{ 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)),
pannerNode: new Tone.Panner(DEFAULT_PAN),
};
newTrack.gainNode.chain(newTrack.pannerNode, mainGainNode);
patternState.tracks.push(newTrack);
renderPatternEditor();
}
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();
}
}
export async function updateTrackSample(trackId, samplePath) {
const track = patternState.tracks.find((t) => t.id == trackId);
if (track) {
track.samplePath = samplePath;
track.name = samplePath.split("/").pop();
await loadAudioForTrack(track); // Carrega o novo player
renderPatternEditor();
}
}
export function toggleStepState(trackId, stepIndex) {
const track = patternState.tracks.find((t) => t.id == trackId);
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];
}
}
}
+171
View File
@@ -0,0 +1,171 @@
// js/pattern_ui.js
import { appState } from "../state.js";
import {
toggleStepState,
updateTrackSample
} from "./pattern_state.js";
import { playSample, stopPlayback } from "./pattern_audio.js"; // Será criado no próximo passo
import { getTotalSteps } from "../utils.js";
// 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) => {
const trackLane = document.createElement("div");
trackLane.className = "track-lane";
trackLane.dataset.trackId = trackData.id;
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.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);
}
});
trackContainer.appendChild(trackLane);
// A lógica dos knobs precisará ser reimplementada ou movida para um arquivo de componentes
});
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);
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
sequencerContainer.innerHTML = ""; return;
}
const activePattern = trackData.patterns[appState.pattern.activePatternIndex];
if (!activePattern) {
sequencerContainer.innerHTML = ""; return;
}
const patternSteps = activePattern.steps;
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");
}
stepElement.addEventListener("click", () => {
toggleStepState(trackData.id, i);
stepElement.classList.toggle("active");
if (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 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);
}
}
});
}