tentando resolver conflitos do tone no mmpCreator
Deploy / Deploy (push) Successful in 2m1s
Details
Deploy / Deploy (push) Successful in 2m1s
Details
This commit is contained in:
parent
2ce14a5f02
commit
05bfc6794f
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
export function stopPlayback(rewind = true) {
|
||||
if (!appState.global.isPlaying) return;
|
||||
|
||||
appState.global.isPlaying = false;
|
||||
|
||||
if (stepEventId) {
|
||||
Tone.Transport.clear(stepEventId);
|
||||
stepEventId = null;
|
||||
}
|
||||
|
||||
tick();
|
||||
appState.global.playbackIntervalId = setInterval(tick, stepInterval);
|
||||
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
|
||||
// =========================================================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue