From bf03931eb833720d350affa344d771b2cfc51263 Mon Sep 17 00:00:00 2001 From: JotaChina Date: Sat, 27 Dec 2025 10:12:23 -0300 Subject: [PATCH] corrigindo play/stop do editor de patterns --- assets/js/creations/pattern/pattern_audio.js | 138 ++++++++------ assets/js/creations/pattern/pattern_state.js | 185 +++++++++++-------- 2 files changed, 189 insertions(+), 134 deletions(-) diff --git a/assets/js/creations/pattern/pattern_audio.js b/assets/js/creations/pattern/pattern_audio.js index f64abbd3..35685fc8 100755 --- a/assets/js/creations/pattern/pattern_audio.js +++ b/assets/js/creations/pattern/pattern_audio.js @@ -65,14 +65,17 @@ 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; - // Se a faixa existe e tem um player pré-carregado - if (track && track.player) { - if (track.player.loaded) { - // Ajusta volume/pan sempre que tocar (robustez a alterações em tempo real) + // Se a track existe e tem player/preload + if (track && (track.previewPlayer || track.player)) { + const playerToUse = track.previewPlayer || track.player; + + if (playerToUse.loaded) { + // Atualiza volume/pan ao tocar if (track.volumeNode) { track.volumeNode.volume.value = track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume); @@ -81,29 +84,30 @@ export function playSample(filePath, trackId) { track.pannerNode.pan.value = track.pan ?? 0; } - // Garante conexão: player -> volumeNode - try { - track.player.disconnect(); - } catch {} - if (track.volumeNode) { - track.player.connect(track.volumeNode); - } + // roteia playerToUse -> volumeNode + try { playerToUse.disconnect(); } catch {} + if (track.volumeNode) playerToUse.connect(track.volumeNode); - // Dispara imediatamente - track.player.start(Tone.now()); + // Dispara (preview não interfere no player da playlist) + try { + playerToUse.start(Tone.now()); + } catch (e) { + console.warn("Falha ao tocar preview/sample:", track.name, e); + } } 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.`); } + return; } - // Fallback para preview de sample sem trackId - else if (!trackId && filePath) { + + // Fallback: preview sem trackId + if (!trackId && filePath) { const previewPlayer = new Tone.Player(filePath).toDestination(); previewPlayer.autostart = true; } } + function playSamplerNoteAtTime(track, midi, time, durationSec) { if (!track?.buffer || !track.volumeNode) return; @@ -179,12 +183,18 @@ export function stopPlayback(rewind = true) { Tone.Transport.cancel(); stopScheduledPianoRoll(); + // ✅ Pattern Editor: para apenas o preview (não mexe no track.player da playlist) + appState.pattern.tracks.forEach((track) => { + try { track.previewPlayer?.stop(); } catch {} + }); + if (rewind) { currentStep = 0; updateStepHighlight(currentStep); } } + export function rewindPlayback() { const lastStep = appState.global.currentStep > 0 @@ -539,20 +549,17 @@ export function startSongPatternPlaybackOnTransport() { if (songPatternScheduleId !== null) return; songPatternScheduleId = Tone.Transport.scheduleRepeat((time) => { - // bpm atual const bpm = parseInt(document.getElementById("bpm-input")?.value, 10) || 120; const stepIntervalSec = 60 / (bpm * 4); - // step absoluto do song (considera seek do Transport) const transportSec = Tone.Transport.getSecondsAtTime ? Tone.Transport.getSecondsAtTime(time) : Tone.Transport.seconds; const songStep = Math.floor(transportSec / stepIntervalSec + 1e-6); - const songTick = songStep * LMMS_TICKS_PER_STEP; - // quais patterns (colunas) estão ativas neste tick? + // Patterns ativas neste tick (pelas basslines/playlist clips) const basslineTracks = appState.pattern.tracks.filter( (t) => t.type === "bassline" && !t.isMuted ); @@ -569,7 +576,7 @@ export function startSongPatternPlaybackOnTransport() { if (activePatternHits.length === 0) return; - // dispara instrumentos reais (samplers/plugins) + // Dispara tracks reais (samplers/plugins) for (const track of appState.pattern.tracks) { if (track.type === "bassline") continue; if (track.muted) continue; @@ -584,24 +591,29 @@ export function startSongPatternPlaybackOnTransport() { for (const n of patt.notes) { const pos = Number(n.pos) || 0; const rawLen = Number(n.len) || 0; - const len = Math.max(rawLen, LMMS_TICKS_PER_STEP); // mínimo 1 step + const len = rawLen < 0 ? LMMS_TICKS_PER_STEP : Math.max(rawLen, LMMS_TICKS_PER_STEP); pattLenTicksByNotes = Math.max(pattLenTicksByNotes, pos + len); } } - const pattLenTicksBySteps = (patt.steps?.length || 0) * LMMS_TICKS_PER_STEP; + const pattLenTicksBySteps = + (patt.steps?.length || 0) * LMMS_TICKS_PER_STEP; - // garante pelo menos 1 step - const pattLenTicks = Math.max(pattLenTicksByNotes, pattLenTicksBySteps, LMMS_TICKS_PER_STEP); + const pattLenTicks = Math.max( + pattLenTicksByNotes, + pattLenTicksBySteps, + LMMS_TICKS_PER_STEP + ); - // tick atual dentro do pattern (loopando) - const tickInPattern = (hit.localStep * LMMS_TICKS_PER_STEP) % pattLenTicks; + // tick atual dentro do pattern (loop interno ao esticar clip) + const tickInPattern = + (hit.localStep * LMMS_TICKS_PER_STEP) % pattLenTicks; - // step index (só pra lógica de steps) + // step index (para patterns de steps) const pattLenSteps = patt.steps?.length || 0; - const stepInPattern = pattLenSteps > 0 - ? (Math.floor(tickInPattern / LMMS_TICKS_PER_STEP) % pattLenSteps) - : hit.localStep; - + const stepInPattern = + pattLenSteps > 0 + ? (Math.floor(tickInPattern / LMMS_TICKS_PER_STEP) % pattLenSteps) + : hit.localStep; // ✅ 1) PLUGIN com piano roll (notes) if ( @@ -623,39 +635,44 @@ export function startSongPatternPlaybackOnTransport() { if (!inWindow) continue; - const offsetTicks = wraps && nPos < stepStartTick - ? (pattLenTicks - stepStartTick) + nPos - : nPos - stepStartTick; + const offsetTicks = + wraps && nPos < stepStartTick + ? (pattLenTicks - stepStartTick) + nPos + : nPos - stepStartTick; const t2 = time + ticksToSec(offsetTicks, stepIntervalSec); + const rawLen = Number(n.len) || 0; + const lenTicks = rawLen < 0 ? LMMS_TICKS_PER_STEP : Math.max(rawLen, LMMS_TICKS_PER_STEP); + const durSec = Math.max(0.01, ticksToSec(lenTicks, stepIntervalSec)); + const vel = (Number(n.vol) || 100) / 100; + + const midi = Number(n.key) || 0; + const freq = Tone.Frequency(midi, "midi").toFrequency(); + try { track.instrument.triggerAttackRelease(freq, durSec, t2, vel); - } catch { - try { - track.instrument.triggerAttackRelease(noteName, durSec, t2); - } catch {} + } catch (e) { + console.warn("[Playlist] Falha ao tocar plugin note:", track.name, e); } } - continue; // 👈 importante: não cair na lógica abaixo + continue; // não cai na lógica de steps } - // ✅ 1b) SAMPLER com piano roll (notes) — loop interno ao esticar o clip + // ✅ 1b) SAMPLER com piano roll (notes) if ( track.type === "sampler" && track.buffer && Array.isArray(patt.notes) && patt.notes.length > 0 ) { - // 👇 chave do loop interno: usa tickInPattern (já calculado acima) const stepStartTick = tickInPattern; const stepEndTick = stepStartTick + LMMS_TICKS_PER_STEP; for (const n of patt.notes) { const nPos = Number(n.pos) || 0; - // janela do step, tratando “wrap” no fim do pattern const wraps = stepEndTick > pattLenTicks; const inWindow = wraps ? (nPos >= stepStartTick || nPos < (stepEndTick - pattLenTicks)) @@ -663,38 +680,45 @@ export function startSongPatternPlaybackOnTransport() { if (!inWindow) continue; - const offsetTicks = wraps && nPos < stepStartTick - ? (pattLenTicks - stepStartTick) + nPos - : nPos - stepStartTick; + const offsetTicks = + wraps && nPos < stepStartTick + ? (pattLenTicks - stepStartTick) + nPos + : nPos - stepStartTick; const t2 = time + ticksToSec(offsetTicks, stepIntervalSec); - const lenTicks = Math.max(1, Number(n.len) || LMMS_TICKS_PER_STEP); + const rawLen = Number(n.len) || 0; + const lenTicks = rawLen < 0 ? LMMS_TICKS_PER_STEP : Math.max(rawLen, LMMS_TICKS_PER_STEP); const durSec = Math.max(0.01, ticksToSec(lenTicks, stepIntervalSec)); playSamplerNoteAtTime(track, Number(n.key) || 0, t2, durSec); } - continue; // mantém: não cair na lógica de steps + continue; // não cai na lógica de steps } - // ✅ 2) Lógica de STEP (sampler / plugin sem notes) + // ✅ 2) STEP (sampler/plugin sem notes) if (!patt.steps) continue; if (patt.steps[stepInPattern]) { if (track.type === "sampler" && track.player) { - if (track.type === "sampler" && track.player) { - try { + try { + // ✅ retrigger LMMS-like: não “some” quando sample é longo + if (typeof track.player.restart === "function") { + track.player.restart(time); + } else { if (track.player.state === "started") track.player.stop(time); track.player.start(time); - } catch {} + } + } catch (e) { + console.warn("[Playlist] Falha ao retrigger sample:", track.name, e); } } else if (track.type === "plugin" && track.instrument) { - // plugin sem piano roll - try { track.instrument.triggerAttackRelease("C5", "16n", time); } catch {} + try { + track.instrument.triggerAttackRelease("C5", "16n", time); + } catch {} } } - } } }, "16n"); diff --git a/assets/js/creations/pattern/pattern_state.js b/assets/js/creations/pattern/pattern_state.js index 2f134369..89c328f7 100755 --- a/assets/js/creations/pattern/pattern_state.js +++ b/assets/js/creations/pattern/pattern_state.js @@ -21,6 +21,7 @@ export function initializePatternState() { try { track.player?.dispose(); } catch {} try { track.buffer?.dispose?.(); } catch {} try { track.instrument?.dispose(); } catch {} + try { track.previewPlayer?.dispose(); } catch {} }); appState.pattern.tracks = []; @@ -29,7 +30,7 @@ export function initializePatternState() { } export async function loadAudioForTrack(track) { - // 1. Garante a criação dos nós de Volume e Pan + // 1) Garante Volume/Pan try { if (!track.volumeNode) { track.volumeNode = new Tone.Volume( @@ -46,130 +47,157 @@ export async function loadAudioForTrack(track) { track.pannerNode.pan.value = track.pan ?? 0; } - try { track.instrument?.disconnect(); } catch {} - try { track.player?.disconnect(); } catch {} + // Desconecta o que existir + try { track.instrument?.disconnect(); } catch {} + try { track.player?.disconnect(); } catch {} + try { track.previewPlayer?.disconnect(); } catch {} + + // Reconecta cadeia base try { track.volumeNode.disconnect(); } catch {} try { track.pannerNode.disconnect(); } catch {} - track.volumeNode.connect(track.pannerNode); track.pannerNode.connect(getMainGainNode()); - } catch (e) { console.error("Erro ao criar nós de áudio base:", e); } - // --- DETECÇÃO DE TIPO DE ARQUIVO --- - // Verifica se é um formato de áudio que o navegador suporta - const isStandardAudio = track.samplePath && /\.(wav|mp3|ogg|flac|m4a)$/i.test(track.samplePath); - - // Se não for áudio padrão, assumimos Plugin (ou Kicker padrão) + // 2) Detecta se é um arquivo de áudio suportado + const isStandardAudio = + track.samplePath && /\.(wav|mp3|ogg|flac|m4a)$/i.test(track.samplePath); + + // 3) Se não for áudio padrão → Plugin (ou fallback) if (!track.samplePath || !isStandardAudio) { try { - if (track.instrument) { try { track.instrument.dispose(); } catch {} } + // limpa sampler/preview/buffer + try { track.player?.dispose(); } catch {} + try { track.previewPlayer?.dispose(); } catch {} + try { track.buffer?.dispose?.(); } catch {} + track.player = null; + track.previewPlayer = null; + track.buffer = null; + + if (track.instrument) { + try { track.instrument.dispose(); } catch {} + track.instrument = null; + } let synth; - // Normaliza o nome do instrumento. Se vazio, assume kicker. const name = (track.instrumentName || "kicker").toLowerCase(); - - const pluginData = {}; + const pluginData = {}; - // SELETOR DE PLUGINS switch (name) { case "tripleoscillator": case "3osc": - synth = new TripleOscillator(Tone.getContext(), pluginData); - break; - - case "kicker": - synth = new Kicker(Tone.getContext(), pluginData); - break; - - case "lb302": - synth = new Lb302(Tone.getContext(), pluginData); - break; - - case "nes": - case "freeboy": - case "papu": - case "sid": - synth = new Nes(Tone.getContext(), pluginData); - break; + synth = new TripleOscillator(Tone.getContext(), pluginData); + break; - case "zynaddsubfx": - case "watsyn": - case "monstro": - case "vibedstrings": + case "kicker": + synth = new Kicker(Tone.getContext(), pluginData); + break; + + case "lb302": + synth = new Lb302(Tone.getContext(), pluginData); + break; + + case "nes": + case "freeboy": + case "papu": + case "sid": + synth = new Nes(Tone.getContext(), pluginData); + break; + + case "zynaddsubfx": + case "watsyn": + case "monstro": + case "vibedstrings": case "supersaw": - synth = new SuperSaw(Tone.getContext(), pluginData); - break; - - case "organic": - synth = new Tone.PolySynth(Tone.Synth, { - oscillator: { type: "sine", count: 8, spread: 20 } - }); - break; + synth = new SuperSaw(Tone.getContext(), pluginData); + break; + + case "organic": + synth = new Tone.PolySynth(Tone.Synth, { + oscillator: { type: "sine", count: 8, spread: 20 }, + }); + break; default: - console.warn(`Plugin ${name} desconhecido, usando fallback (Kicker).`); - // Fallback seguro: Kicker - synth = new Kicker(Tone.getContext(), pluginData); + console.warn(`Plugin ${name} desconhecido, usando fallback (Kicker).`); + synth = new Kicker(Tone.getContext(), pluginData); } - if (synth.output) { - synth.connect(track.volumeNode); - } else { - synth.connect(track.volumeNode); - } + // Conecta plugin na cadeia + if (synth.output) synth.connect(track.volumeNode); + else synth.connect(track.volumeNode); track.instrument = synth; - track.player = null; - track.type = 'plugin'; - // Atualiza o nome se ele estava vazio - if (!track.instrumentName) track.instrumentName = name; - - console.log(`[Audio] Plugin carregado: ${name}`); + track.type = "plugin"; + if (!track.instrumentName) track.instrumentName = name; + + console.log(`[Audio] Plugin carregado: ${name}`); + return track; } catch (e) { console.error("Erro ao carregar plugin:", track.instrumentName, e); + return track; } - return track; } - // 3. Lógica para SAMPLERS + // 4) SAMPLER (áudio) try { - try { track.player?.dispose(); } catch {} - track.player = null; - try { track.buffer?.dispose?.(); } catch {} - track.buffer = null; - + // limpa plugin if (track.instrument) { - try { track.instrument.dispose(); } catch {} - track.instrument = null; + try { track.instrument.dispose(); } catch {} + track.instrument = null; } - const player = new Tone.Player({ url: track.samplePath, autostart: false, retrigger: true }); + // limpa players/buffer antigos + try { track.player?.dispose(); } catch {} + try { track.previewPlayer?.dispose(); } catch {} + try { track.buffer?.dispose?.(); } catch {} + track.player = null; + track.previewPlayer = null; + track.buffer = null; + + // Player principal (Playlist/steps) + const player = new Tone.Player({ + url: track.samplePath, + autostart: false, + retrigger: true, + }); + // redundância segura p/ builds diferentes do Tone: try { player.retrigger = true; } catch {} - await player.load(track.samplePath); - + await player.load(track.samplePath); player.connect(track.volumeNode); - const buffer = new Tone.Buffer(); - await buffer.load(track.samplePath); - + // ✅ reutiliza o MESMO buffer do player (sem segundo download) + track.buffer = player.buffer; track.player = player; - track.buffer = buffer; - track.type = 'sampler'; + // Preview player (Pattern Editor) — separado pra não brigar com a playlist + const previewPlayer = new Tone.Player({ + autostart: false, + retrigger: true, + }); + try { previewPlayer.retrigger = true; } catch {} + previewPlayer.buffer = track.buffer; + previewPlayer.connect(track.volumeNode); + + track.previewPlayer = previewPlayer; + + track.type = "sampler"; + return track; } catch (error) { - console.error('Erro ao carregar sample:', track.samplePath); + console.error("Erro ao carregar sample:", track.samplePath, error); try { track.player?.dispose(); } catch {} + try { track.previewPlayer?.dispose(); } catch {} try { track.buffer?.dispose?.(); } catch {} track.player = null; + track.previewPlayer = null; track.buffer = null; + return track; } - return track; } export function addTrackToState() { @@ -229,6 +257,7 @@ export function removeTrackById(trackId) { try { trackToRemove.instrument?.dispose(); } catch {} try { trackToRemove.pannerNode?.disconnect(); } catch {} try { trackToRemove.volumeNode?.disconnect(); } catch {} + try { trackToRemove.previewPlayer?.dispose(); } catch {} // Remove do array appState.pattern.tracks.splice(index, 1); @@ -248,6 +277,7 @@ export function removeLastTrackFromState() { const trackToRemove = appState.pattern.tracks[appState.pattern.tracks.length - 1]; try { trackToRemove.player?.dispose(); } catch {} + try { trackToRemove.previewPlayer?.dispose(); } catch {} try { trackToRemove.buffer?.dispose?.(); } catch {} try { trackToRemove.instrument?.dispose(); } catch {} try { trackToRemove.pannerNode?.disconnect(); } catch {} @@ -260,6 +290,7 @@ export function removeLastTrackFromState() { } } + export async function updateTrackSample(trackIndex, samplePath) { const track = appState.pattern.tracks[trackIndex]; if (track) {