plugin 1.0.1

This commit is contained in:
JotaChina committed 2025-11-23 13:29:36 -03:00
1 parent 44fce48bb5
commit 018a8c5403
7 files changed
+2681 -26339

No files matched your search

+239 -108
View File
@@ -6,6 +6,22 @@ import { highlightStep } from "./pattern_ui.js";
import { getTotalSteps } from "../utils.js";
import { initializeAudioContext } from "../audio.js";
// Ajuste os caminhos (../plugins/...) conforme a estrutura real das suas pastas
import { TripleOscillator } from "../../audio/plugins/TripleOscillator.js";
import { Nes } from "../../audio/plugins/Nes.js";
import { SuperSaw } from "../../audio/plugins/SuperSaw.js";
import { Lb302 } from "../../audio/plugins/Lb302.js";
import { Kicker } from "../../audio/plugins/Kicker.js";
// Mapa para facilitar a criação dinâmica
const PLUGIN_CLASSES = {
tripleoscillator: TripleOscillator,
nes: Nes,
supersaw: SuperSaw,
lb302: Lb302,
kicker: Kicker,
};
const timerDisplay = document.getElementById("timer-display");
// Variável para armazenar as "Parts" (sequências melódicas) do Tone.js
@@ -104,30 +120,35 @@ function tick() {
}
}
// Percorre tracks e toca o step atual se ativo
// PERCORRE AS TRACKS
appState.pattern.tracks.forEach((track) => {
if (track.muted) return; // Respeita o Mute
if (track.muted) return;
if (!track.patterns || track.patterns.length === 0) return;
const activePattern = track.patterns[track.activePatternIndex];
if (!activePattern) return;
// Verifica se o step atual está ativo
if (activePattern && activePattern.steps[appState.global.currentStep]) {
// CASO 1: SAMPLER (Arquivo de Áudio)
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)
// Se for plugin e tiver step marcado, toca nota padrão (C5)
else if (track.type === 'plugin' && track.instrument) {
// "16n" é a duração de uma semicolcheia
// Usamos um try/catch para evitar travar o loop se o plugin falhar
try {
track.instrument.triggerAttackRelease("C5", "16n", Tone.now());
} catch (e) {
console.warn("Falha ao tocar step do plugin:", e);
}
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) {}
}
}
}
});
@@ -142,7 +163,7 @@ export function startPlayback() {
// Garante que o contexto do Tone esteja rodando
if (Tone.context.state !== "running") {
Tone.start();
Tone.start();
}
if (appState.global.currentStep === 0) {
@@ -181,16 +202,22 @@ export function stopPlayback() {
// --- NOVO: Para o Transport e Limpa Synths ---
Tone.Transport.stop();
// Limpa agendamentos melódicos
activeParts.forEach(part => part.dispose());
activeParts.forEach((part) => part.dispose());
activeParts = [];
// Solta notas travadas de todos os plugins
appState.pattern.tracks.forEach(track => {
try { track.player?.stop(); } catch {}
try { track.instrument?.releaseAll?.(); } catch {} // Para PolySynths
try { track.instrument?.triggerRelease?.(); } catch {} // Para MonoSynths
appState.pattern.tracks.forEach((track) => {
try {
track.player?.stop();
} catch {}
try {
track.instrument?.releaseAll?.();
} catch {} // Para PolySynths
try {
track.instrument?.triggerRelease?.();
} catch {} // Para MonoSynths
});
// --------------------------------------------
@@ -213,7 +240,7 @@ export function rewindPlayback() {
? appState.global.currentStep - 1
: getTotalSteps() - 1;
appState.global.currentStep = 0;
Tone.Transport.position = 0; // Reseta o tempo do Tone.js
if (!appState.global.isPlaying) {
@@ -232,53 +259,64 @@ export function togglePlayback() {
}
}
// =========================================================================
// AGENDADOR DE PIANO ROLL (NOVA FUNÇÃO)
// Agenda as notas melódicas (desenhadas no Piano Roll) para tocar no Tone.Transport
// =========================================================================
// 2. Agendador de Piano Roll (Melodia)
function schedulePianoRoll() {
// Limpa anteriores por segurança
activeParts.forEach(part => part.dispose());
activeParts = [];
activeParts.forEach((part) => part.dispose());
activeParts = [];
appState.pattern.tracks.forEach(track => {
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 events = pattern.notes.map((note) => {
// --- CORREÇÃO DE TEMPO (PPQ) ---
// LMMS usa 192 ticks por batida. Tone.js também usa 192 por padrão.
// Precisamos converter o 'pos' (ticks absolutos) para tempo musical
// 1 Beat = 192 ticks
// Tone.Time("0:0:0").toTicks()
return {
// Passamos 'ticks' diretamente. O Tone converte baseado no BPM.
time:
0 + (note.pos * (Tone.Transport.PPQ / 192)) / Tone.Transport.PPQ,
midi: note.key,
duration: note.len / 192 + "n", // Converte duração baseada em semínimas (192)
velocity: (note.vol || 100) / 100,
};
});
const part = new Tone.Part((time, value) => {
if (track.muted) return;
const pattern = track.patterns[track.activePatternIndex];
// Só agenda se tiver notas E for um instrumento
if (pattern && pattern.notes && pattern.notes.length > 0 && track.instrument) {
// Mapeia as notas para o formato de evento do Tone.js
const events = pattern.notes.map(note => {
return {
// Converte Ticks (pos) para Tempo Musical do Tone.js
// Assumindo 192 PPQ (padrão LMMS) -> Tone PPQ
time: 0 + (note.pos * (Tone.Transport.PPQ / 192) / Tone.Transport.PPQ),
midi: note.key,
duration: note.len + "i", // 'i' em Tone.js significa ticks
velocity: (note.vol || 100) / 100
};
});
const freq = Tone.Frequency(value.midi, "midi");
// Cria uma Part (sequência)
const part = new Tone.Part((time, value) => {
if (track.muted) return;
const freq = Tone.Frequency(value.midi, "midi");
// Dispara o método que padronizamos em todos os plugins
if (track.instrument.triggerAttackRelease) {
track.instrument.triggerAttackRelease(freq, value.duration, time, value.velocity);
}
}, events).start(0);
// Configura o Loop da Melodia
const bars = parseInt(document.getElementById('bars-input')?.value || 1);
part.loop = true;
part.loopEnd = bars + "m"; // 'm' = measure (compasso)
activeParts.push(part);
// 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
);
}
});
}, events).start(0);
const bars = parseInt(document.getElementById("bars-input")?.value || 1);
part.loop = true;
part.loopEnd = bars + "m";
activeParts.push(part);
}
});
}
// =========================================================================
@@ -286,60 +324,153 @@ function schedulePianoRoll() {
// =========================================================================
export async function renderActivePatternToBlob() {
initializeAudioContext();
initializeAudioContext();
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
const totalSteps = getTotalSteps();
const stepInterval = 60 / (bpm * 4);
const duration = totalSteps * stepInterval;
// =========================================================
// 1. CÁLCULO DE DURAÇÃO INTELIGENTE
// =========================================================
const stepInterval = 60 / (bpm * 4);
const activePatternIndex =
appState.pattern.tracks[0]?.activePatternIndex || 0;
let maxStepFound = getTotalSteps(); // Mínimo: tamanho da tela
const buffer = await Tone.Offline(async () => {
// Varre todas as tracks para achar a última nota ou step
appState.pattern.tracks.forEach((track) => {
const p = track.patterns[activePatternIndex];
if (!p) return;
// A. Steps (Bateria)
if (p.steps && p.steps.includes(true)) {
const lastIdx = p.steps.lastIndexOf(true);
if (lastIdx + 1 > maxStepFound) maxStepFound = lastIdx + 1;
}
// B. Notas (Piano Roll) - Assumindo 192 ticks/beat e steps de 1/16 (48 ticks)
if (p.notes && p.notes.length > 0) {
p.notes.forEach((n) => {
const endTick = n.pos + n.len;
const endStep = Math.ceil(endTick / 48);
if (endStep > maxStepFound) maxStepFound = endStep;
});
}
});
// Arredonda para o próximo compasso cheio (múltiplo de 16)
const stepsPerBar = 16;
const totalSteps = Math.ceil(maxStepFound / stepsPerBar) * stepsPerBar;
const duration = totalSteps * stepInterval;
// =========================================================
// 2. RENDERIZAÇÃO OFFLINE
// =========================================================
const buffer = await Tone.Offline(async ({ transport }) => {
const masterGain = new Tone.Gain().toDestination();
const offlineTracksParts = appState.pattern.tracks
.map((track) => {
const pattern = track.patterns[activePatternIndex];
// Loop por cada trilha do projeto
appState.pattern.tracks.forEach((track) => {
const pattern = track.patterns[activePatternIndex];
// Se não tem pattern, ou se é uma track muda/vazia, pula
if (!pattern || track.muted) return;
// Nota: O render atual suporta apenas Samplers (buffers carregados)
// Para suportar Plugins no futuro, precisaríamos recriar o synth aqui dentro.
if (!pattern || !track.buffer || !pattern.steps.includes(true)) {
return null;
}
// Verifica se tem conteúdo (buffer de áudio OU notas MIDI OU steps ativos)
const hasAudio = track.buffer;
const hasNotes = pattern.notes && pattern.notes.length > 0;
const hasSteps = pattern.steps && pattern.steps.includes(true);
const trackBuffer = track.buffer;
const panner = new Tone.Panner(track.pan).connect(masterGain);
const volume = new Tone.Volume(Tone.gainToDb(track.volume)).connect(
panner
);
if (!hasAudio && !hasNotes && !hasSteps) return;
const events = [];
pattern.steps.forEach((isActive, stepIndex) => {
if (isActive) {
const time = stepIndex * stepInterval;
events.push(time);
// Cria canal de volume/pan para essa track no mundo Offline
const panner = new Tone.Panner(track.pan || 0).connect(masterGain);
const volume = new Tone.Volume(
track.volume === 0 ? -100 : Tone.gainToDb(track.volume)
).connect(panner);
// --- CENÁRIO A: É um SAMPLE (Áudio gravado) ---
if (track.samplePath && track.buffer) {
// Lógica original de steps para samples
if (pattern.steps) {
const events = [];
pattern.steps.forEach((isActive, stepIndex) => {
if (isActive) events.push(stepIndex * stepInterval);
});
if (events.length > 0) {
new Tone.Part((time) => {
new Tone.Player(track.buffer).connect(volume).start(time);
}, events).start(0);
}
});
}
}
const part = new Tone.Part((time) => {
new Tone.Player(trackBuffer)
.connect(volume)
.start(time);
}, events);
// --- CENÁRIO B: É um PLUGIN (Sintetizador) ---
else if (track.type === "plugin") {
// Normaliza o nome (ex: "TripleOscillator" -> "tripleoscillator")
// Tenta pegar o nome da propriedade 'pluginName', 'instrument.name' ou do próprio objeto params
const pluginName = (
track.pluginName ||
track.instrument?.constructor?.name ||
""
).toLowerCase();
return part;
})
.filter((t) => t !== null);
const PluginClass = PLUGIN_CLASSES[pluginName];
offlineTracksParts.forEach((part) => {
part.start(0);
if (PluginClass) {
// INSTANCIA O PLUGIN NO MUNDO OFFLINE
// Passamos 'track.params' ou 'track.pluginData' (ajuste conforme seu appState salva os dados)
const instrumentInstance = new PluginClass(
null,
track.params || track.pluginData || {}
);
// Conecta na cadeia de áudio offline
instrumentInstance.connect(volume);
// 1. Agendar Notas do Piano Roll
if (hasNotes) {
const events = pattern.notes.map((note) => ({
time: 0 + note.pos * (48 / 192) * stepInterval, // Conversão aproximada Ticks -> Segundos
// Se quiser precisão exata do Tone, use: note.pos * (Tone.Transport.PPQ / 192) / Tone.Transport.PPQ
midi: note.key,
duration: (note.len / 192) * (60 / bpm), // Duração em segundos
velocity: (note.vol || 100) / 100,
}));
new Tone.Part((time, val) => {
const freq = Tone.Frequency(val.midi, "midi");
instrumentInstance.triggerAttackRelease(freq, val.duration, time);
}, events).start(0);
}
// 2. Agendar Steps (Caso use o TripleOscillator como bateria/efeito no step sequencer)
else if (hasSteps) {
const stepEvents = [];
pattern.steps.forEach((isActive, idx) => {
if (isActive) stepEvents.push(idx * stepInterval);
});
new Tone.Part((time) => {
// Toca um C5 padrão para steps sem nota definida
instrumentInstance.triggerAttackRelease(
Tone.Frequency("C5"),
0.1,
time
);
}, stepEvents).start(0);
}
} else {
console.warn(
`Render: Plugin não suportado ou não encontrado: ${pluginName}`
);
}
}
});
Tone.Transport.bpm.value = bpm;
Tone.Transport.start();
}, duration);
// Configura e inicia o Transport Offline
transport.bpm.value = bpm;
transport.start();
}, duration);
const blob = bufferToWave(buffer);
return blob;
@@ -376,19 +507,19 @@ function bufferToWave(abuffer) {
setString("WAVE");
setString("fmt ");
view.setUint32(pos, 16, true);
pos += 4;
pos += 4;
view.setUint16(pos, 1, true);
pos += 2;
pos += 2;
view.setUint16(pos, numOfChan, true);
pos += 2;
view.setUint32(pos, abuffer.sampleRate, true);
pos += 4;
view.setUint32(pos, abuffer.sampleRate * 2 * numOfChan, true);
pos += 4;
pos += 4;
view.setUint16(pos, numOfChan * 2, true);
pos += 2;
pos += 2;
view.setUint16(pos, 16, true);
pos += 2;
pos += 2;
setString("data");
view.setUint32(pos, length - 44, true);
pos += 4;
@@ -400,11 +531,11 @@ function bufferToWave(abuffer) {
for (i = 0; i < abuffer.length; i++) {
for (let j = 0; j < numOfChan; j++) {
sample = Math.max(-1, Math.min(1, channels[j][i]));
sample = (0.5 + sample * 32767.5) | 0;
sample = (0.5 + sample * 32767.5) | 0;
view.setInt16(pos, sample, true);
pos += 2;
}
}
return new Blob([buffer], { type: "audio/wav" });
}
}
+11 -13
View File
@@ -7,6 +7,7 @@ import { playSample, stopPlayback } from "./pattern_audio.js";
import { getTotalSteps } from "../utils.js";
import { sendAction } from '../socket.js';
import { initializeAudioContext } from '../audio.js';
import * as Tone from "https://esm.sh/tone";
// Função principal de renderização para o editor de patterns
export function renderPatternEditor() {
@@ -105,7 +106,7 @@ export function redrawSequencer() {
}
// ============================================================
// LÓGICA DE DECISÃO V2: STEPS OU PIANO ROLL?
// LÓGICA DE DECISÃO V2: STEPS OU PIANO ROLL? (MANTIDA)
// ============================================================
const notes = activePattern.notes || [];
@@ -117,7 +118,6 @@ export function redrawSequencer() {
const isMelodic = notes.some(n => n.key !== firstKey);
const hasLongNotes = notes.some(n => n.len > 48);
// Sobreposição de notas (Acordes)
const sortedNotes = [...notes].sort((a, b) => a.pos - b.pos);
let hasOverlap = false;
for (let i = 0; i < sortedNotes.length - 1; i++) {
@@ -153,14 +153,18 @@ export function redrawSequencer() {
}
});
// --- CÁLCULO REVERTIDO (VOLTA AO PADRÃO ANTERIOR) ---
const barsInput = document.getElementById('bars-input');
const barsCount = barsInput ? parseInt(barsInput.value) || 1 : 1;
// Revertido: Removemos a multiplicação por beatsPerBar que causou o bug visual
const totalTicks = 192 * barsCount;
// ----------------------------------------------------
activePattern.notes.forEach(note => {
const noteEl = document.createElement('div');
noteEl.className = 'mini-note';
const barsInput = document.getElementById('bars-input');
const barsCount = barsInput ? parseInt(barsInput.value) || 1 : 1;
const totalTicks = 192 * barsCount;
const leftPercent = (note.pos / totalTicks) * 100;
const widthPercent = (note.len / totalTicks) * 100;
@@ -200,7 +204,6 @@ export function redrawSequencer() {
stepElement.classList.add("active");
}
// --- EVENTO DE CLIQUE (CORRIGIDO PARA PLUGINS) ---
stepElement.addEventListener("click", (e) => {
e.stopPropagation();
initializeAudioContext();
@@ -216,18 +219,13 @@ export function redrawSequencer() {
isActive: isActive
});
// --- AQUI ESTAVA O PROBLEMA: TOCA O SOM ---
if (isActive) {
// Caso 1: Sampler (Áudio)
if (trackData.type === 'sampler' && trackData.samplePath) {
playSample(trackData.samplePath, trackData.id);
}
// Caso 2: Plugin (Sintetizador)
else if (trackData.type === 'plugin' && trackData.instrument) {
// Toca um C5 (Dó) curto para feedback visual/sonoro
try {
// triggerAttackRelease(nota, duração)
trackData.instrument.triggerAttackRelease("C5", "16n");
trackData.instrument.triggerAttackRelease("C5", "16n", Tone.now());
} catch(err) {
console.warn("Erro ao tocar preview do synth:", err);
}