corrigindo play/stop do editor de patterns
Deploy / Deploy (push) Successful in 2m1s
Details
Deploy / Deploy (push) Successful in 2m1s
Details
This commit is contained in:
parent
e24b188d8d
commit
bf03931eb8
|
|
@ -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
|
||||
// roteia playerToUse -> volumeNode
|
||||
try { playerToUse.disconnect(); } catch {}
|
||||
if (track.volumeNode) playerToUse.connect(track.volumeNode);
|
||||
|
||||
// Dispara (preview não interfere no player da playlist)
|
||||
try {
|
||||
track.player.disconnect();
|
||||
} catch {}
|
||||
if (track.volumeNode) {
|
||||
track.player.connect(track.volumeNode);
|
||||
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.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispara imediatamente
|
||||
track.player.start(Tone.now());
|
||||
} else {
|
||||
console.warn(
|
||||
`Player da trilha "${track.name}" ainda não carregado — pulando este tick.`
|
||||
);
|
||||
}
|
||||
}
|
||||
// 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,25 +591,30 @@ 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
|
||||
const stepInPattern =
|
||||
pattLenSteps > 0
|
||||
? (Math.floor(tickInPattern / LMMS_TICKS_PER_STEP) % pattLenSteps)
|
||||
: hit.localStep;
|
||||
|
||||
|
||||
// ✅ 1) PLUGIN com piano roll (notes)
|
||||
if (
|
||||
track.type === "plugin" &&
|
||||
|
|
@ -623,39 +635,44 @@ export function startSongPatternPlaybackOnTransport() {
|
|||
|
||||
if (!inWindow) continue;
|
||||
|
||||
const offsetTicks = wraps && 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
|
||||
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 {
|
||||
// ✅ 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");
|
||||
|
|
|
|||
|
|
@ -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,34 +47,44 @@ export async function loadAudioForTrack(track) {
|
|||
track.pannerNode.pan.value = track.pan ?? 0;
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 2) Detecta se é um arquivo de áudio suportado
|
||||
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)
|
||||
// 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 = {};
|
||||
|
||||
// SELETOR DE PLUGINS
|
||||
switch (name) {
|
||||
case "tripleoscillator":
|
||||
case "3osc":
|
||||
|
|
@ -105,72 +116,89 @@ export async function loadAudioForTrack(track) {
|
|||
|
||||
case "organic":
|
||||
synth = new Tone.PolySynth(Tone.Synth, {
|
||||
oscillator: { type: "sine", count: 8, spread: 20 }
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export function addTrackToState() {
|
||||
const totalSteps = getTotalSteps();
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue