tentando resolver conflitos do tone no mmpCreator
Deploy / Deploy (push) Successful in 2m1s Details

This commit is contained in:
JotaChina 2025-12-26 21:20:00 -03:00
parent 2ce14a5f02
commit 05bfc6794f
2 changed files with 174 additions and 167 deletions

View File

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

View File

@ -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();
}
tick();
appState.global.playbackIntervalId = setInterval(tick, stepInterval);
export function stopPlayback(rewind = true) {
if (!appState.global.isPlaying) return;
appState.global.isPlaying = false;
if (stepEventId) {
Tone.Transport.clear(stepEventId);
stepEventId = null;
}
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
appState.pattern.tracks.forEach((track) => {
if (track.muted) return;
const pattern = track.patterns[track.activePatternIndex];
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 bpm = parseFloat(document.getElementById("bpm-input").value) || 120;
const stepSec = 60 / (bpm * 4); // 1/16
const events = pattern.notes.map((note) => {
const posSteps = (note.pos || 0) / TICKS_PER_STEP;
// 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;
const rawLen = note.len || 0;
const lenTicks = rawLen < 0 ? TICKS_PER_STEP : rawLen; // defesa extra
const lenSteps = Math.max(1, lenTicks / TICKS_PER_STEP);
// 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) => {
const pat = getActivePatternForTrack(track);
if (!pat?.notes?.length) return;
let maxEndTick = 0;
pat.notes.forEach((n) => {
const end = (n.pos ?? 0) + (n.len ?? 0);
if (end > maxEndTick) maxEndTick = end;
});
const barsForThis = Math.max(1, Math.ceil(maxEndTick / TICKS_PER_BAR));
if (barsForThis > barsNeeded) barsNeeded = barsForThis;
});
// 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 }));
}
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;
// plugin -> track.instrument
// sampler -> track.buffer
const canPlay =
(track.type === "plugin" && track.instrument) ||
(track.type === "sampler" && track.buffer);
if (!canPlay) return;
const events = pat.notes.map((note) => {
const posSteps = (note.pos ?? 0) / TICKS_PER_STEP;
const durSteps = (note.len ?? TICKS_PER_STEP) / TICKS_PER_STEP;
return {
time: posSteps * stepSec, // segundos
time: posSteps * stepSec,
midi: note.key,
duration: lenSteps * stepSec, // segundos
velocity: (note.vol || 100) / 100,
duration: Math.max(stepSec / 4, durSteps * stepSec),
velocity: (note.vol ?? 100) / 100,
};
});
const part = new Tone.Part((time, value) => {
if (track.muted) return;
const freq = Tone.Frequency(value.midi, "midi");
// 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
);
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);
// Loop deve cobrir toda a extensão do pianoroll (última nota)
const barsInput =
parseInt(document.getElementById("bars-input")?.value || 1, 10) || 1;
let maxEndTick = 0;
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 stepsNeeded = Math.max(1, Math.ceil(maxEndTick / TICKS_PER_STEP));
const barsNeeded = Math.max(1, Math.ceil(stepsNeeded / STEPS_PER_BAR));
// 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`;
part.loopEnd = `${barsNeeded}m`;
}
scheduledParts.push(part);
});
}
// =========================================================================
// Renderizar o Pattern atual para um Blob de Áudio
// =========================================================================