v1.0.13 - botao de reset funcional para limpar as salas
This commit is contained in:
1 parent
611aec9ba1
commit
c8323d56cf
9 files changed
+1593
-532
No files matched your search
@@ -6,13 +6,17 @@ import { highlightStep } from "./pattern_ui.js";
|
||||
import { getTotalSteps } from "../utils.js";
|
||||
import { initializeAudioContext } from "../audio.js";
|
||||
|
||||
const timerDisplay = document.getElementById('timer-display');
|
||||
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');
|
||||
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}`;
|
||||
}
|
||||
|
||||
@@ -26,7 +30,9 @@ export function playMetronomeSound(isDownbeat) {
|
||||
// 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;
|
||||
const track = trackId
|
||||
? appState.pattern.tracks.find((t) => t.id == trackId)
|
||||
: null;
|
||||
|
||||
// Se a faixa existe e tem um player pré-carregado
|
||||
if (track && track.player) {
|
||||
@@ -41,7 +47,9 @@ export function playSample(filePath, trackId) {
|
||||
}
|
||||
|
||||
// Garante conexão: player -> volumeNode (não usar mais gainNode)
|
||||
try { track.player.disconnect(); } catch {}
|
||||
try {
|
||||
track.player.disconnect();
|
||||
} catch {}
|
||||
if (track.volumeNode) {
|
||||
track.player.connect(track.volumeNode);
|
||||
}
|
||||
@@ -49,7 +57,9 @@ export function playSample(filePath, trackId) {
|
||||
// Dispara imediatamente
|
||||
track.player.start(Tone.now());
|
||||
} else {
|
||||
console.warn(`Player da trilha "${track.name}" ainda não carregado — pulando este tick.`);
|
||||
console.warn(
|
||||
`Player da trilha "${track.name}" ainda não carregado — pulando este tick.`
|
||||
);
|
||||
}
|
||||
}
|
||||
// Fallback para preview de sample sem trackId
|
||||
@@ -66,7 +76,10 @@ function tick() {
|
||||
}
|
||||
|
||||
const totalSteps = getTotalSteps();
|
||||
const lastStepIndex = appState.global.currentStep === 0 ? totalSteps - 1 : appState.global.currentStep - 1;
|
||||
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;
|
||||
@@ -78,10 +91,13 @@ function tick() {
|
||||
|
||||
// Metrônomo
|
||||
if (appState.global.metronomeEnabled) {
|
||||
const noteValue = parseInt(document.getElementById("compasso-b-input").value, 10) || 4;
|
||||
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);
|
||||
playMetronomeSound(
|
||||
appState.global.currentStep % (stepsPerBeat * 4) === 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +108,11 @@ function tick() {
|
||||
// IMPORTANTE: usar o pattern ativo da PRÓPRIA TRILHA
|
||||
const activePattern = track.patterns[track.activePatternIndex];
|
||||
|
||||
if (activePattern &&
|
||||
activePattern.steps[appState.global.currentStep] &&
|
||||
track.samplePath) {
|
||||
if (
|
||||
activePattern &&
|
||||
activePattern.steps[appState.global.currentStep] &&
|
||||
track.samplePath
|
||||
) {
|
||||
playSample(track.samplePath, track.id);
|
||||
}
|
||||
});
|
||||
@@ -115,7 +133,8 @@ export function startPlayback() {
|
||||
Tone.Transport.bpm.value = bpm;
|
||||
const stepInterval = (60 * 1000) / (bpm * 4);
|
||||
|
||||
if (appState.global.playbackIntervalId) clearInterval(appState.global.playbackIntervalId);
|
||||
if (appState.global.playbackIntervalId)
|
||||
clearInterval(appState.global.playbackIntervalId);
|
||||
|
||||
appState.global.isPlaying = true;
|
||||
const playBtn = document.getElementById("play-btn");
|
||||
@@ -135,9 +154,11 @@ export function stopPlayback() {
|
||||
appState.global.playbackIntervalId = null;
|
||||
appState.global.isPlaying = false;
|
||||
|
||||
document.querySelectorAll('.step.playing').forEach(s => s.classList.remove('playing'));
|
||||
document
|
||||
.querySelectorAll(".step.playing")
|
||||
.forEach((s) => s.classList.remove("playing"));
|
||||
appState.global.currentStep = 0;
|
||||
if (timerDisplay) timerDisplay.textContent = '00:00:00';
|
||||
if (timerDisplay) timerDisplay.textContent = "00:00:00";
|
||||
|
||||
const playBtn = document.getElementById("play-btn");
|
||||
if (playBtn) {
|
||||
@@ -147,10 +168,13 @@ export function stopPlayback() {
|
||||
}
|
||||
|
||||
export function rewindPlayback() {
|
||||
const lastStep = appState.global.currentStep > 0 ? appState.global.currentStep - 1 : getTotalSteps() - 1;
|
||||
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';
|
||||
if (timerDisplay) timerDisplay.textContent = "00:00:00";
|
||||
highlightStep(lastStep, false);
|
||||
}
|
||||
}
|
||||
@@ -164,3 +188,165 @@ export function togglePlayback() {
|
||||
startPlayback();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// FUNÇÃO CORRIGIDA v3: Renderizar o Pattern atual para um Blob de Áudio
|
||||
// =========================================================================
|
||||
|
||||
export async function renderActivePatternToBlob() {
|
||||
initializeAudioContext(); // Garante que o contexto de áudio principal existe
|
||||
|
||||
// 1. Obter configs atuais
|
||||
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
||||
const totalSteps = getTotalSteps();
|
||||
const stepInterval = 60 / (bpm * 4); // Duração de 1 step (em segundos)
|
||||
const duration = totalSteps * stepInterval; // Duração total em segundos
|
||||
|
||||
// 2. Descobrir qual pattern está ativo (assume que todos estão no mesmo)
|
||||
const activePatternIndex =
|
||||
appState.pattern.tracks[0]?.activePatternIndex || 0;
|
||||
|
||||
// 3. Renderizar offline usando Tone.Offline
|
||||
const buffer = await Tone.Offline(async () => {
|
||||
// ----------------------------------------------------
|
||||
// Contexto de Áudio OFFLINE
|
||||
// ----------------------------------------------------
|
||||
const masterGain = new Tone.Gain().toDestination();
|
||||
|
||||
// --- INÍCIO DA CORREÇÃO (Lógica de Polifonia) ---
|
||||
|
||||
// 1. Criamos as 'Parts'.
|
||||
const offlineTracksParts = appState.pattern.tracks
|
||||
.map((track) => {
|
||||
const pattern = track.patterns[activePatternIndex];
|
||||
|
||||
// Verificação crucial: Precisamos do 'track.buffer' (áudio carregado)
|
||||
if (!pattern || !track.buffer || !pattern.steps.includes(true)) {
|
||||
return null; // Pula trilha se não tiver áudio ou notas
|
||||
}
|
||||
|
||||
// Obtém o buffer de áudio (que já está carregado)
|
||||
const trackBuffer = track.buffer;
|
||||
|
||||
// Cria a cadeia de áudio (Volume/Pan) para esta *trilha*
|
||||
const panner = new Tone.Panner(track.pan).connect(masterGain);
|
||||
const volume = new Tone.Volume(Tone.gainToDb(track.volume)).connect(
|
||||
panner
|
||||
);
|
||||
|
||||
// Cria a lista de eventos (tempos em que as notas devem tocar)
|
||||
const events = [];
|
||||
pattern.steps.forEach((isActive, stepIndex) => {
|
||||
if (isActive) {
|
||||
const time = stepIndex * stepInterval;
|
||||
events.push(time);
|
||||
}
|
||||
});
|
||||
|
||||
// Cria a Tone.Part
|
||||
const part = new Tone.Part((time) => {
|
||||
// *** ESTA É A CORREÇÃO CRÍTICA ***
|
||||
// Para cada nota (cada 'time' na lista de 'events'),
|
||||
// nós criamos um PLAYER "ONE-SHOT" (descartável).
|
||||
// Isso permite que vários sons da mesma trilha
|
||||
// se sobreponham (polifonia).
|
||||
|
||||
new Tone.Player(trackBuffer) // Usa o buffer carregado
|
||||
.connect(volume) // Conecta na cadeia de áudio (Volume->Pan->Master)
|
||||
.start(time); // Toca no tempo agendado
|
||||
}, events); // Passa a lista de tempos [0, 0.25, 0.5, ...]
|
||||
|
||||
return part; // Retorna a Part (que sabe quando disparar)
|
||||
})
|
||||
.filter((t) => t !== null); // Remove trilhas nulas
|
||||
|
||||
// 2. Como estamos usando buffers já carregados,
|
||||
// não precisamos esperar (remover 'await Tone.loaded()')
|
||||
|
||||
// 3. Agenda todas as 'Parts' para começar
|
||||
offlineTracksParts.forEach((part) => {
|
||||
part.start(0);
|
||||
});
|
||||
// --- FIM DA CORREÇÃO ---
|
||||
|
||||
// Define o BPM do transporte offline
|
||||
Tone.Transport.bpm.value = bpm;
|
||||
|
||||
// Inicia o transporte (para a renderização)
|
||||
Tone.Transport.start();
|
||||
// ----------------------------------------------------
|
||||
}, duration); // Duração total da renderização
|
||||
|
||||
// 5. Converte o AudioBuffer resultante em um Blob (arquivo .wav)
|
||||
const blob = bufferToWave(buffer);
|
||||
return blob;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// FUNÇÃO UTILITÁRIA: Converte AudioBuffer para Blob WAV
|
||||
// (Mantenha esta função como está)
|
||||
// =========================================================================
|
||||
|
||||
function bufferToWave(abuffer) {
|
||||
let numOfChan = abuffer.numberOfChannels;
|
||||
let length = abuffer.length * numOfChan * 2 + 44;
|
||||
let buffer = new ArrayBuffer(length);
|
||||
let view = new DataView(buffer);
|
||||
let channels = [],
|
||||
i,
|
||||
sample;
|
||||
let offset = 0;
|
||||
let pos = 0;
|
||||
|
||||
// setAll e setString são helpers
|
||||
function setAll(data) {
|
||||
for (i = 0; i < data.length; i++) {
|
||||
view.setUint8(pos + i, data[i]);
|
||||
}
|
||||
pos += data.length;
|
||||
}
|
||||
function setString(s) {
|
||||
setAll(s.split("").map((c) => c.charCodeAt(0)));
|
||||
}
|
||||
|
||||
// Cabeçalho WAV
|
||||
setString("RIFF");
|
||||
view.setUint32(pos, length - 8, true);
|
||||
pos += 4;
|
||||
setString("WAVE");
|
||||
setString("fmt ");
|
||||
view.setUint32(pos, 16, true);
|
||||
pos += 4; // Sub-chunk size
|
||||
view.setUint16(pos, 1, true);
|
||||
pos += 2; // Audio format 1
|
||||
view.setUint16(pos, numOfChan, true);
|
||||
pos += 2;
|
||||
view.setUint32(pos, abuffer.sampleRate, true);
|
||||
pos += 4;
|
||||
view.setUint32(pos, abuffer.sampleRate * 2 * numOfChan, true);
|
||||
pos += 4; // Byte rate
|
||||
view.setUint16(pos, numOfChan * 2, true);
|
||||
pos += 2; // Block align
|
||||
view.setUint16(pos, 16, true);
|
||||
pos += 2; // Bits per sample
|
||||
setString("data");
|
||||
view.setUint32(pos, length - 44, true);
|
||||
pos += 4;
|
||||
|
||||
// Pega os dados dos canais
|
||||
for (i = 0; i < numOfChan; i++) {
|
||||
channels.push(abuffer.getChannelData(i));
|
||||
}
|
||||
|
||||
// Escreve os dados (intercalando canais)
|
||||
for (i = 0; i < abuffer.length; i++) {
|
||||
for (let j = 0; j < numOfChan; j++) {
|
||||
sample = Math.max(-1, Math.min(1, channels[j][i]));
|
||||
sample = (0.5 + sample * 32767.5) | 0; // Converte para 16-bit PCM
|
||||
view.setInt16(pos, sample, true);
|
||||
pos += 2;
|
||||
}
|
||||
}
|
||||
|
||||
return new Blob([buffer], { type: "audio/wav" });
|
||||
}
|
||||
@@ -98,22 +98,28 @@ export function redrawSequencer() {
|
||||
sequencerContainer.innerHTML = ""; return;
|
||||
}
|
||||
|
||||
// --- CORRIJA ESTAS DUAS LINHAS ---
|
||||
// ANTES:
|
||||
// const activePatternIndex = appState.pattern.activePatternIndex;
|
||||
// const activePattern = trackData.patterns[activePatternIndex];
|
||||
//
|
||||
// DEPOIS:
|
||||
const activePatternIndex = trackData.activePatternIndex;
|
||||
const activePatternIndex = trackData.activePatternIndex;
|
||||
const activePattern = trackData.patterns[activePatternIndex];
|
||||
|
||||
if (!activePattern) {
|
||||
sequencerContainer.innerHTML = ""; return;
|
||||
}
|
||||
// ... resto da função ...
|
||||
|
||||
const patternSteps = activePattern.steps;
|
||||
|
||||
sequencerContainer.innerHTML = "";
|
||||
// --- 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;
|
||||
}
|
||||
// --- 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";
|
||||
@@ -163,29 +169,6 @@ export function redrawSequencer() {
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -201,13 +184,26 @@ export function highlightStep(stepIndex, isActive) {
|
||||
});
|
||||
}
|
||||
|
||||
// (V7) Função de UI "cirúrgica"
|
||||
export function updateStepUI(trackIndex, patternIndex, stepIndex, isActive) {
|
||||
if (patternIndex !== appState.pattern.activePatternIndex) {
|
||||
return;
|
||||
}
|
||||
// --- 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;
|
||||
}
|
||||
// --- FIM DA CORREÇÃO ---
|
||||
|
||||
const stepWrapper = trackElement.querySelector(
|
||||
`.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`
|
||||
);
|
||||
@@ -215,4 +211,47 @@ export function updateStepUI(trackIndex, patternIndex, stepIndex, isActive) {
|
||||
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;
|
||||
|
||||
// 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
|
||||
|
||||
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"
|
||||
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.disabled = false;
|
||||
|
||||
} else {
|
||||
// 5. Estado desabilitado (nenhum pattern)
|
||||
const option = document.createElement('option');
|
||||
option.textContent = 'Sem patterns';
|
||||
globalPatternSelector.appendChild(option);
|
||||
globalPatternSelector.disabled = true;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user