corrigindo play/stop do editor de patterns
Deploy / Deploy (push) Successful in 2m1s Details

This commit is contained in:
JotaChina 2025-12-27 10:12:23 -03:00
parent e24b188d8d
commit bf03931eb8
2 changed files with 189 additions and 134 deletions

View File

@ -65,14 +65,17 @@ export function playMetronomeSound(isDownbeat) {
// Dispara o sample de uma track, garantindo que o player esteja roteado corretamente // Dispara o sample de uma track, garantindo que o player esteja roteado corretamente
export function playSample(filePath, trackId) { export function playSample(filePath, trackId) {
initializeAudioContext(); initializeAudioContext();
const track = trackId const track = trackId
? appState.pattern.tracks.find((t) => t.id == trackId) ? appState.pattern.tracks.find((t) => t.id == trackId)
: null; : null;
// Se a faixa existe e tem um player pré-carregado // Se a track existe e tem player/preload
if (track && track.player) { if (track && (track.previewPlayer || track.player)) {
if (track.player.loaded) { const playerToUse = track.previewPlayer || track.player;
// Ajusta volume/pan sempre que tocar (robustez a alterações em tempo real)
if (playerToUse.loaded) {
// Atualiza volume/pan ao tocar
if (track.volumeNode) { if (track.volumeNode) {
track.volumeNode.volume.value = track.volumeNode.volume.value =
track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume); track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume);
@ -81,29 +84,30 @@ export function playSample(filePath, trackId) {
track.pannerNode.pan.value = track.pan ?? 0; track.pannerNode.pan.value = track.pan ?? 0;
} }
// Garante conexão: player -> volumeNode // roteia playerToUse -> volumeNode
try { try { playerToUse.disconnect(); } catch {}
track.player.disconnect(); if (track.volumeNode) playerToUse.connect(track.volumeNode);
} catch {}
if (track.volumeNode) {
track.player.connect(track.volumeNode);
}
// Dispara imediatamente // Dispara (preview não interfere no player da playlist)
track.player.start(Tone.now()); try {
playerToUse.start(Tone.now());
} catch (e) {
console.warn("Falha ao tocar preview/sample:", track.name, e);
}
} else { } else {
console.warn( console.warn(`Player da trilha "${track.name}" ainda não carregado — pulando.`);
`Player da trilha "${track.name}" ainda não carregado — pulando este tick.`
);
} }
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(); const previewPlayer = new Tone.Player(filePath).toDestination();
previewPlayer.autostart = true; previewPlayer.autostart = true;
} }
} }
function playSamplerNoteAtTime(track, midi, time, durationSec) { function playSamplerNoteAtTime(track, midi, time, durationSec) {
if (!track?.buffer || !track.volumeNode) return; if (!track?.buffer || !track.volumeNode) return;
@ -179,12 +183,18 @@ export function stopPlayback(rewind = true) {
Tone.Transport.cancel(); Tone.Transport.cancel();
stopScheduledPianoRoll(); 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) { if (rewind) {
currentStep = 0; currentStep = 0;
updateStepHighlight(currentStep); updateStepHighlight(currentStep);
} }
} }
export function rewindPlayback() { export function rewindPlayback() {
const lastStep = const lastStep =
appState.global.currentStep > 0 appState.global.currentStep > 0
@ -539,20 +549,17 @@ export function startSongPatternPlaybackOnTransport() {
if (songPatternScheduleId !== null) return; if (songPatternScheduleId !== null) return;
songPatternScheduleId = Tone.Transport.scheduleRepeat((time) => { songPatternScheduleId = Tone.Transport.scheduleRepeat((time) => {
// bpm atual
const bpm = parseInt(document.getElementById("bpm-input")?.value, 10) || 120; const bpm = parseInt(document.getElementById("bpm-input")?.value, 10) || 120;
const stepIntervalSec = 60 / (bpm * 4); const stepIntervalSec = 60 / (bpm * 4);
// step absoluto do song (considera seek do Transport)
const transportSec = Tone.Transport.getSecondsAtTime const transportSec = Tone.Transport.getSecondsAtTime
? Tone.Transport.getSecondsAtTime(time) ? Tone.Transport.getSecondsAtTime(time)
: Tone.Transport.seconds; : Tone.Transport.seconds;
const songStep = Math.floor(transportSec / stepIntervalSec + 1e-6); const songStep = Math.floor(transportSec / stepIntervalSec + 1e-6);
const songTick = songStep * LMMS_TICKS_PER_STEP; 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( const basslineTracks = appState.pattern.tracks.filter(
(t) => t.type === "bassline" && !t.isMuted (t) => t.type === "bassline" && !t.isMuted
); );
@ -569,7 +576,7 @@ export function startSongPatternPlaybackOnTransport() {
if (activePatternHits.length === 0) return; if (activePatternHits.length === 0) return;
// dispara instrumentos reais (samplers/plugins) // Dispara tracks reais (samplers/plugins)
for (const track of appState.pattern.tracks) { for (const track of appState.pattern.tracks) {
if (track.type === "bassline") continue; if (track.type === "bassline") continue;
if (track.muted) continue; if (track.muted) continue;
@ -584,24 +591,29 @@ export function startSongPatternPlaybackOnTransport() {
for (const n of patt.notes) { for (const n of patt.notes) {
const pos = Number(n.pos) || 0; const pos = Number(n.pos) || 0;
const rawLen = Number(n.len) || 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); 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(
const pattLenTicks = Math.max(pattLenTicksByNotes, pattLenTicksBySteps, LMMS_TICKS_PER_STEP); pattLenTicksByNotes,
pattLenTicksBySteps,
LMMS_TICKS_PER_STEP
);
// tick atual dentro do pattern (loopando) // tick atual dentro do pattern (loop interno ao esticar clip)
const tickInPattern = (hit.localStep * LMMS_TICKS_PER_STEP) % pattLenTicks; 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 pattLenSteps = patt.steps?.length || 0;
const stepInPattern = pattLenSteps > 0 const stepInPattern =
? (Math.floor(tickInPattern / LMMS_TICKS_PER_STEP) % pattLenSteps) pattLenSteps > 0
: hit.localStep; ? (Math.floor(tickInPattern / LMMS_TICKS_PER_STEP) % pattLenSteps)
: hit.localStep;
// ✅ 1) PLUGIN com piano roll (notes) // ✅ 1) PLUGIN com piano roll (notes)
if ( if (
@ -623,39 +635,44 @@ export function startSongPatternPlaybackOnTransport() {
if (!inWindow) continue; if (!inWindow) continue;
const offsetTicks = wraps && nPos < stepStartTick const offsetTicks =
? (pattLenTicks - stepStartTick) + nPos wraps && nPos < stepStartTick
: nPos - stepStartTick; ? (pattLenTicks - stepStartTick) + nPos
: nPos - stepStartTick;
const t2 = time + ticksToSec(offsetTicks, stepIntervalSec); 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 { try {
track.instrument.triggerAttackRelease(freq, durSec, t2, vel); track.instrument.triggerAttackRelease(freq, durSec, t2, vel);
} catch { } catch (e) {
try { console.warn("[Playlist] Falha ao tocar plugin note:", track.name, e);
track.instrument.triggerAttackRelease(noteName, durSec, t2);
} catch {}
} }
} }
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 ( if (
track.type === "sampler" && track.type === "sampler" &&
track.buffer && track.buffer &&
Array.isArray(patt.notes) && Array.isArray(patt.notes) &&
patt.notes.length > 0 patt.notes.length > 0
) { ) {
// 👇 chave do loop interno: usa tickInPattern (já calculado acima)
const stepStartTick = tickInPattern; const stepStartTick = tickInPattern;
const stepEndTick = stepStartTick + LMMS_TICKS_PER_STEP; const stepEndTick = stepStartTick + LMMS_TICKS_PER_STEP;
for (const n of patt.notes) { for (const n of patt.notes) {
const nPos = Number(n.pos) || 0; const nPos = Number(n.pos) || 0;
// janela do step, tratando “wrap” no fim do pattern
const wraps = stepEndTick > pattLenTicks; const wraps = stepEndTick > pattLenTicks;
const inWindow = wraps const inWindow = wraps
? (nPos >= stepStartTick || nPos < (stepEndTick - pattLenTicks)) ? (nPos >= stepStartTick || nPos < (stepEndTick - pattLenTicks))
@ -663,38 +680,45 @@ export function startSongPatternPlaybackOnTransport() {
if (!inWindow) continue; if (!inWindow) continue;
const offsetTicks = wraps && nPos < stepStartTick const offsetTicks =
? (pattLenTicks - stepStartTick) + nPos wraps && nPos < stepStartTick
: nPos - stepStartTick; ? (pattLenTicks - stepStartTick) + nPos
: nPos - stepStartTick;
const t2 = time + ticksToSec(offsetTicks, stepIntervalSec); 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)); const durSec = Math.max(0.01, ticksToSec(lenTicks, stepIntervalSec));
playSamplerNoteAtTime(track, Number(n.key) || 0, t2, durSec); 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) continue;
if (patt.steps[stepInPattern]) { if (patt.steps[stepInPattern]) {
if (track.type === "sampler" && track.player) { 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); if (track.player.state === "started") track.player.stop(time);
track.player.start(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) { } else if (track.type === "plugin" && track.instrument) {
// plugin sem piano roll try {
try { track.instrument.triggerAttackRelease("C5", "16n", time); } catch {} track.instrument.triggerAttackRelease("C5", "16n", time);
} catch {}
} }
} }
} }
} }
}, "16n"); }, "16n");

View File

@ -21,6 +21,7 @@ export function initializePatternState() {
try { track.player?.dispose(); } catch {} try { track.player?.dispose(); } catch {}
try { track.buffer?.dispose?.(); } catch {} try { track.buffer?.dispose?.(); } catch {}
try { track.instrument?.dispose(); } catch {} try { track.instrument?.dispose(); } catch {}
try { track.previewPlayer?.dispose(); } catch {}
}); });
appState.pattern.tracks = []; appState.pattern.tracks = [];
@ -29,7 +30,7 @@ export function initializePatternState() {
} }
export async function loadAudioForTrack(track) { export async function loadAudioForTrack(track) {
// 1. Garante a criação dos nós de Volume e Pan // 1) Garante Volume/Pan
try { try {
if (!track.volumeNode) { if (!track.volumeNode) {
track.volumeNode = new Tone.Volume( track.volumeNode = new Tone.Volume(
@ -46,130 +47,157 @@ export async function loadAudioForTrack(track) {
track.pannerNode.pan.value = track.pan ?? 0; track.pannerNode.pan.value = track.pan ?? 0;
} }
// Desconecta o que existir
try { track.instrument?.disconnect(); } catch {} try { track.instrument?.disconnect(); } catch {}
try { track.player?.disconnect(); } catch {} try { track.player?.disconnect(); } catch {}
try { track.previewPlayer?.disconnect(); } catch {}
// Reconecta cadeia base
try { track.volumeNode.disconnect(); } catch {} try { track.volumeNode.disconnect(); } catch {}
try { track.pannerNode.disconnect(); } catch {} try { track.pannerNode.disconnect(); } catch {}
track.volumeNode.connect(track.pannerNode); track.volumeNode.connect(track.pannerNode);
track.pannerNode.connect(getMainGainNode()); track.pannerNode.connect(getMainGainNode());
} catch (e) { } catch (e) {
console.error("Erro ao criar nós de áudio base:", e); console.error("Erro ao criar nós de áudio base:", e);
} }
// --- DETECÇÃO DE TIPO DE ARQUIVO --- // 2) Detecta se é um arquivo de áudio suportado
// Verifica se é um formato de áudio que o navegador suporta const isStandardAudio =
const isStandardAudio = track.samplePath && /\.(wav|mp3|ogg|flac|m4a)$/i.test(track.samplePath); track.samplePath && /\.(wav|mp3|ogg|flac|m4a)$/i.test(track.samplePath);
// Se não for áudio padrão, assumimos Plugin (ou Kicker padrão) // 3) Se não for áudio padrão → Plugin (ou fallback)
if (!track.samplePath || !isStandardAudio) { if (!track.samplePath || !isStandardAudio) {
try { 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; let synth;
// Normaliza o nome do instrumento. Se vazio, assume kicker.
const name = (track.instrumentName || "kicker").toLowerCase(); const name = (track.instrumentName || "kicker").toLowerCase();
const pluginData = {}; const pluginData = {};
// SELETOR DE PLUGINS
switch (name) { switch (name) {
case "tripleoscillator": case "tripleoscillator":
case "3osc": case "3osc":
synth = new TripleOscillator(Tone.getContext(), pluginData); synth = new TripleOscillator(Tone.getContext(), pluginData);
break; break;
case "kicker": case "kicker":
synth = new Kicker(Tone.getContext(), pluginData); synth = new Kicker(Tone.getContext(), pluginData);
break; break;
case "lb302": case "lb302":
synth = new Lb302(Tone.getContext(), pluginData); synth = new Lb302(Tone.getContext(), pluginData);
break; break;
case "nes": case "nes":
case "freeboy": case "freeboy":
case "papu": case "papu":
case "sid": case "sid":
synth = new Nes(Tone.getContext(), pluginData); synth = new Nes(Tone.getContext(), pluginData);
break; break;
case "zynaddsubfx": case "zynaddsubfx":
case "watsyn": case "watsyn":
case "monstro": case "monstro":
case "vibedstrings": case "vibedstrings":
case "supersaw": case "supersaw":
synth = new SuperSaw(Tone.getContext(), pluginData); synth = new SuperSaw(Tone.getContext(), pluginData);
break; break;
case "organic": case "organic":
synth = new Tone.PolySynth(Tone.Synth, { synth = new Tone.PolySynth(Tone.Synth, {
oscillator: { type: "sine", count: 8, spread: 20 } oscillator: { type: "sine", count: 8, spread: 20 },
}); });
break; break;
default: default:
console.warn(`Plugin ${name} desconhecido, usando fallback (Kicker).`); console.warn(`Plugin ${name} desconhecido, usando fallback (Kicker).`);
// Fallback seguro: Kicker synth = new Kicker(Tone.getContext(), pluginData);
synth = new Kicker(Tone.getContext(), pluginData);
} }
if (synth.output) { // Conecta plugin na cadeia
synth.connect(track.volumeNode); if (synth.output) synth.connect(track.volumeNode);
} else { else synth.connect(track.volumeNode);
synth.connect(track.volumeNode);
}
track.instrument = synth; track.instrument = synth;
track.player = null; track.type = "plugin";
track.type = 'plugin';
// Atualiza o nome se ele estava vazio
if (!track.instrumentName) track.instrumentName = name; if (!track.instrumentName) track.instrumentName = name;
console.log(`[Audio] Plugin carregado: ${name}`); console.log(`[Audio] Plugin carregado: ${name}`);
return track;
} catch (e) { } catch (e) {
console.error("Erro ao carregar plugin:", track.instrumentName, e); console.error("Erro ao carregar plugin:", track.instrumentName, e);
return track;
} }
return track;
} }
// 3. Lógica para SAMPLERS // 4) SAMPLER (áudio)
try { try {
try { track.player?.dispose(); } catch {} // limpa plugin
track.player = null;
try { track.buffer?.dispose?.(); } catch {}
track.buffer = null;
if (track.instrument) { if (track.instrument) {
try { track.instrument.dispose(); } catch {} try { track.instrument.dispose(); } catch {}
track.instrument = null; 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: // redundância segura p/ builds diferentes do Tone:
try { player.retrigger = true; } catch {} try { player.retrigger = true; } catch {}
await player.load(track.samplePath); await player.load(track.samplePath);
player.connect(track.volumeNode); player.connect(track.volumeNode);
const buffer = new Tone.Buffer(); // ✅ reutiliza o MESMO buffer do player (sem segundo download)
await buffer.load(track.samplePath); track.buffer = player.buffer;
track.player = player; 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) { } 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.player?.dispose(); } catch {}
try { track.previewPlayer?.dispose(); } catch {}
try { track.buffer?.dispose?.(); } catch {} try { track.buffer?.dispose?.(); } catch {}
track.player = null; track.player = null;
track.previewPlayer = null;
track.buffer = null; track.buffer = null;
return track;
} }
return track;
} }
export function addTrackToState() { export function addTrackToState() {
@ -229,6 +257,7 @@ export function removeTrackById(trackId) {
try { trackToRemove.instrument?.dispose(); } catch {} try { trackToRemove.instrument?.dispose(); } catch {}
try { trackToRemove.pannerNode?.disconnect(); } catch {} try { trackToRemove.pannerNode?.disconnect(); } catch {}
try { trackToRemove.volumeNode?.disconnect(); } catch {} try { trackToRemove.volumeNode?.disconnect(); } catch {}
try { trackToRemove.previewPlayer?.dispose(); } catch {}
// Remove do array // Remove do array
appState.pattern.tracks.splice(index, 1); appState.pattern.tracks.splice(index, 1);
@ -248,6 +277,7 @@ export function removeLastTrackFromState() {
const trackToRemove = appState.pattern.tracks[appState.pattern.tracks.length - 1]; const trackToRemove = appState.pattern.tracks[appState.pattern.tracks.length - 1];
try { trackToRemove.player?.dispose(); } catch {} try { trackToRemove.player?.dispose(); } catch {}
try { trackToRemove.previewPlayer?.dispose(); } catch {}
try { trackToRemove.buffer?.dispose?.(); } catch {} try { trackToRemove.buffer?.dispose?.(); } catch {}
try { trackToRemove.instrument?.dispose(); } catch {} try { trackToRemove.instrument?.dispose(); } catch {}
try { trackToRemove.pannerNode?.disconnect(); } catch {} try { trackToRemove.pannerNode?.disconnect(); } catch {}
@ -260,6 +290,7 @@ export function removeLastTrackFromState() {
} }
} }
export async function updateTrackSample(trackIndex, samplePath) { export async function updateTrackSample(trackIndex, samplePath) {
const track = appState.pattern.tracks[trackIndex]; const track = appState.pattern.tracks[trackIndex];
if (track) { if (track) {