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
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");

View File

@ -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) {