From 05bfc6794f822de8bd8c46cd9e0d249452df992b Mon Sep 17 00:00:00 2001 From: JotaChina Date: Fri, 26 Dec 2025 21:20:00 -0300 Subject: [PATCH] tentando resolver conflitos do tone no mmpCreator --- assets/js/creations/file.js | 62 +++-- assets/js/creations/pattern/pattern_audio.js | 279 +++++++++---------- 2 files changed, 174 insertions(+), 167 deletions(-) diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index c0a01ca5..2f344f57 100755 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -345,20 +345,29 @@ function parseInstrumentNode( const volFromFile = parseFloat(instrumentTrackNode.getAttribute("vol")); const panFromFile = parseFloat(instrumentTrackNode.getAttribute("pan")); + const baseNoteFromFile = parseInt(instrumentTrackNode.getAttribute("basenote"), 10); + const pitchFromFile = parseFloat(instrumentTrackNode.getAttribute("pitch")); - return { + const baseNote = !isNaN(baseNoteFromFile) ? baseNoteFromFile : 60; // fallback C4 (MIDI 60) + const pitch = !isNaN(pitchFromFile) ? pitchFromFile : 0; + + + return { id: Date.now() + Math.random(), name: trackName, type: trackType, samplePath: finalSamplePath, - patterns: patterns, - //activePatternIndex: 0, // Sempre começa mostrando o primeiro pattern disponível + patterns, + activePatternIndex: 0, // ✅ evita index undefined + baseNote, // ✅ importante p/ sample pitch + pitch, // (opcional p/ transposição depois) volume: !isNaN(volFromFile) ? volFromFile / 100 : DEFAULT_VOLUME, pan: !isNaN(panFromFile) ? panFromFile / 100 : DEFAULT_PAN, - instrumentName: instrumentName, + instrumentName, instrumentXml: instrumentNode.innerHTML, - parentBasslineId: parentBasslineId, + parentBasslineId, }; + } // ================================================================= @@ -566,21 +575,40 @@ export async function parseMmpContent(xmlString) { // Configura tamanho da timeline let isFirstTrackWithNotes = true; newTracks.forEach((track) => { - if (track.type !== "bassline" && isFirstTrackWithNotes) { - const activePattern = track.patterns[track.activePatternIndex || 0]; - if ( - activePattern && - activePattern.steps && - activePattern.steps.length > 0 - ) { - const bars = Math.ceil(activePattern.steps.length / 16); - const barsInput = document.getElementById("bars-input"); - if (barsInput) barsInput.value = bars > 0 ? bars : 1; - isFirstTrackWithNotes = false; - } + if (track.type === "bassline" || !isFirstTrackWithNotes) return; + + const activePattern = track.patterns?.[track.activePatternIndex || 0]; + if (!activePattern) return; + + let bars = 1; + + // ✅ Se tiver piano roll, calcula pelo final da última nota + if (activePattern.notes && activePattern.notes.length > 0) { + const TICKS_PER_BAR = 192; // LMMS 4/4 + const TICKS_PER_STEP = 12; // 1/16 + + let maxEndTick = 0; + activePattern.notes.forEach((n) => { + const pos = parseInt(n.pos, 10) || 0; + const rawLen = parseInt(n.len, 10) || 0; + const len = rawLen < 0 ? TICKS_PER_STEP : rawLen; // fallback + maxEndTick = Math.max(maxEndTick, pos + Math.max(len, TICKS_PER_STEP)); + }); + + bars = Math.max(1, Math.ceil(maxEndTick / TICKS_PER_BAR)); } + // ✅ Senão, cai no step sequencer normal + else if (activePattern.steps && activePattern.steps.length > 0) { + bars = Math.max(1, Math.ceil(activePattern.steps.length / 16)); + } + + const barsInput = document.getElementById("bars-input"); + if (barsInput) barsInput.value = String(bars); + + isFirstTrackWithNotes = false; }); + // Carrega samples/plugins try { const promises = newTracks diff --git a/assets/js/creations/pattern/pattern_audio.js b/assets/js/creations/pattern/pattern_audio.js index 1edbf428..bffdcdd1 100755 --- a/assets/js/creations/pattern/pattern_audio.js +++ b/assets/js/creations/pattern/pattern_audio.js @@ -12,6 +12,11 @@ import { SuperSaw } from "../../audio/plugins/SuperSaw.js"; import { Lb302 } from "../../audio/plugins/Lb302.js"; import { Kicker } from "../../audio/plugins/Kicker.js"; +function getActivePatternForTrack(track) { + const idx = appState.pattern?.activePatternIndex ?? track.activePatternIndex ?? 0; + return track.patterns?.[idx] ?? null; +} + const TICKS_PER_STEP = 12; // LMMS: 12 ticks por 1/16 const STEPS_PER_BAR = 16; // 4/4 em 1/16 @@ -90,109 +95,85 @@ export function playSample(filePath, trackId) { } } -function tick() { - if (!appState.global.isPlaying) { - stopPlayback(); - return; +function playSamplerNoteAtTime(track, midi, time, durationSec) { + if (!track?.buffer || !track.volumeNode) return; + + const base = track.baseNote ?? 60; + const semitones = (midi - base); + const rate = Math.pow(2, semitones / 12); + + const player = new Tone.Player(track.buffer); + player.playbackRate = rate; + player.connect(track.volumeNode); + + player.start(time); + + // se quiser respeitar duração (bem básico) + if (durationSec && durationSec > 0) { + player.stop(time + durationSec); } + // limpeza + player.onstop = () => player.dispose(); +} + +let stepEventId = null; + +function tick(time) { const totalSteps = getTotalSteps(); - const lastStepIndex = - appState.global.currentStep === 0 - ? totalSteps - 1 - : appState.global.currentStep - 1; - highlightStep(lastStepIndex, false); + updateStepHighlight(currentStep); - 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); - } - - // Metrônomo - 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 - ); - } - } - - // PERCORRE AS TRACKS appState.pattern.tracks.forEach((track) => { - if (track.muted) return; - if (!track.patterns || track.patterns.length === 0) return; + const pat = getActivePatternForTrack(track); + if (!pat) return; - const activePattern = track.patterns[track.activePatternIndex]; - if (!activePattern) return; + // Se for plugin/sampler e tem piano roll, ele já está agendado via schedulePianoRoll() + const hasNotes = Array.isArray(pat.notes) && pat.notes.length > 0; + if (hasNotes) return; - // Verifica se o step atual está ativo - if (activePattern.steps[appState.global.currentStep]) { - // CASO 1: SAMPLER (Sempre toca no step) - if (track.samplePath) { - playSample(track.samplePath, track.id); - } - // CASO 2: PLUGIN (Sintetizador) - else if (track.type === "plugin" && track.instrument) { - // --- CORREÇÃO DO SOM DUPLICADO --- - // Verifica se existem notas no Piano Roll. - // Se houver notas (array notes > 0), IGNORA o step sequencer. - // O som será gerado APENAS pelo 'schedulePianoRoll'. - - const hasNotes = activePattern.notes && activePattern.notes.length > 0; - - if (!hasNotes) { - // Só toca o C5 do step se NÃO houver melodia desenhada - try { - track.instrument.triggerAttackRelease("C5", "16n", Tone.now()); - } catch (e) {} - } - } + // Step sequencer (one-shots) + if (pat.steps?.[currentStep] && track.type === "sampler" && track.buffer) { + // sem midi -> toca “base” (drum one-shot) + playSamplerNoteAtTime(track, track.baseNote ?? 60, time, null); } }); - highlightStep(appState.global.currentStep, true); - appState.global.currentStep = (appState.global.currentStep + 1) % totalSteps; + currentStep = (currentStep + 1) % totalSteps; } export function startPlayback() { - if (appState.global.isPlaying || appState.pattern.tracks.length === 0) return; - initializeAudioContext(); - - // Garante que o contexto do Tone esteja rodando - if (Tone.context.state !== "running") { - Tone.start(); - } - - 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); - - // --- NOVO: Agenda o Piano Roll (Melodias) --- - schedulePianoRoll(); - Tone.Transport.start(); // Inicia o relógio para as notas melódicas - // -------------------------------------------- + if (appState.global.isPlaying) return; appState.global.isPlaying = true; - const playBtn = document.getElementById("play-btn"); - if (playBtn) { - playBtn.classList.remove("fa-play"); - playBtn.classList.add("fa-pause"); + currentStep = 0; + + Tone.Transport.stop(); + Tone.Transport.cancel(); + stopScheduledPianoRoll(); + schedulePianoRoll(); + + stepEventId = Tone.Transport.scheduleRepeat(tick, "16n"); + Tone.Transport.start(); +} + +export function stopPlayback(rewind = true) { + if (!appState.global.isPlaying) return; + + appState.global.isPlaying = false; + + if (stepEventId) { + Tone.Transport.clear(stepEventId); + stepEventId = null; } - tick(); - appState.global.playbackIntervalId = setInterval(tick, stepInterval); + Tone.Transport.stop(); + Tone.Transport.cancel(); + stopScheduledPianoRoll(); + + if (rewind) { + currentStep = 0; + updateStepHighlight(currentStep); + } } export function stopPlayback() { @@ -262,88 +243,86 @@ export function togglePlayback() { } // 2. Agendador de Piano Roll (Melodia) -function schedulePianoRoll() { - activeParts.forEach((part) => part.dispose()); - activeParts = []; +export function schedulePianoRoll() { + stopScheduledPianoRoll(); // Limpa agendamentos anteriores + + const bpm = parseFloat(document.getElementById("bpm-input").value) || 120; + const stepSec = 60 / (bpm * 4); // 1/16 + + // LMMS: 1 bar (4/4) = 192 ticks, 1 step (1/16) = 12 ticks :contentReference[oaicite:3]{index=3} + const TICKS_PER_STEP = 12; + const TICKS_PER_BAR = 192; + + // 1) Descobrir quantos compassos são necessários (maior nota “end”) + let barsNeeded = parseInt(document.getElementById("bars-input")?.value, 10) || 1; appState.pattern.tracks.forEach((track) => { - if (track.muted) return; - const pattern = track.patterns[track.activePatternIndex]; + const pat = getActivePatternForTrack(track); + if (!pat?.notes?.length) return; - if ( - pattern && - pattern.notes && - pattern.notes.length > 0 && - track.instrument - ) { - // Converte notas para eventos Tone.js - const bpm = parseInt(document.getElementById("bpm-input")?.value, 10) || 120; - const stepSec = 60 / (bpm * 4); // 1/16 + let maxEndTick = 0; + pat.notes.forEach((n) => { + const end = (n.pos ?? 0) + (n.len ?? 0); + if (end > maxEndTick) maxEndTick = end; + }); - const events = pattern.notes.map((note) => { - const posSteps = (note.pos || 0) / TICKS_PER_STEP; + const barsForThis = Math.max(1, Math.ceil(maxEndTick / TICKS_PER_BAR)); + if (barsForThis > barsNeeded) barsNeeded = barsForThis; + }); - const rawLen = note.len || 0; - const lenTicks = rawLen < 0 ? TICKS_PER_STEP : rawLen; // defesa extra - const lenSteps = Math.max(1, lenTicks / TICKS_PER_STEP); + // 2) Sincronizar UI + Transport loop com esse tamanho + const barsInput = document.getElementById("bars-input"); + if (barsInput) { + barsInput.value = String(barsNeeded); + barsInput.dispatchEvent(new Event("input", { bubbles: true })); + } - return { - time: posSteps * stepSec, // segundos - midi: note.key, - duration: lenSteps * stepSec, // segundos - velocity: (note.vol || 100) / 100, - }; - }); + Tone.Transport.loop = true; + Tone.Transport.loopStart = 0; + Tone.Transport.loopEnd = `${barsNeeded}m`; + // 3) Agendar notas (plugins + samplers) + appState.pattern.tracks.forEach((track) => { + const pat = getActivePatternForTrack(track); + if (!pat?.notes?.length) return; - const part = new Tone.Part((time, value) => { - if (track.muted) return; - const freq = Tone.Frequency(value.midi, "midi"); + // plugin -> track.instrument + // sampler -> track.buffer + const canPlay = + (track.type === "plugin" && track.instrument) || + (track.type === "sampler" && track.buffer); - // Dispara nota - if (track.instrument.triggerAttackRelease) { - // Se a duração calculada for muito curta ou inválida, usa 16n - const dur = value.duration || "16n"; - track.instrument.triggerAttackRelease( - freq, - dur, - time, - value.velocity - ); - } - }, events).start(0); + if (!canPlay) return; - // Loop deve cobrir toda a extensão do pianoroll (última nota) - const barsInput = - parseInt(document.getElementById("bars-input")?.value || 1, 10) || 1; + const events = pat.notes.map((note) => { + const posSteps = (note.pos ?? 0) / TICKS_PER_STEP; + const durSteps = (note.len ?? TICKS_PER_STEP) / TICKS_PER_STEP; - let maxEndTick = 0; + return { + time: posSteps * stepSec, + midi: note.key, + duration: Math.max(stepSec / 4, durSteps * stepSec), + velocity: (note.vol ?? 100) / 100, + }; + }); - for (const n of pattern.notes) { - const pos = Number(n.pos) || 0; - const rawLen = Number(n.len) || 0; - - // len negativo acontece em alguns casos (one-shot/edge do LMMS) - const lenTicks = rawLen < 0 ? TICKS_PER_STEP : rawLen; - - // garante no mínimo 1 step - const endTick = pos + Math.max(lenTicks, TICKS_PER_STEP); - if (endTick > maxEndTick) maxEndTick = endTick; + const part = new Tone.Part((time, value) => { + if (track.type === "sampler") { + playSamplerNoteAtTime(track, value.midi, time, value.duration); + } else { + const freq = Tone.Frequency(value.midi, "midi").toFrequency(); + track.instrument.triggerAttackRelease(freq, value.duration, time, value.velocity); } + }, events).start(0); - const stepsNeeded = Math.max(1, Math.ceil(maxEndTick / TICKS_PER_STEP)); - const barsNeeded = Math.max(1, Math.ceil(stepsNeeded / STEPS_PER_BAR)); + part.loop = true; + part.loopEnd = `${barsNeeded}m`; - // respeita o bars-input se o usuário colocar maior, mas nunca menor que o necessário - const loopBars = Math.max(barsInput, barsNeeded); - - part.loop = true; - part.loopEnd = `${loopBars}m`; - - } + scheduledParts.push(part); }); } + // ========================================================================= // Renderizar o Pattern atual para um Blob de Áudio // =========================================================================