plugin v1.0.0

This commit is contained in:
JotaChina committed 2025-11-20 20:54:29 -03:00
1 parent 12c5b55bce
commit 44fce48bb5
12 files changed
+1316 -635

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
import * as Tone from "https://esm.sh/tone";
export class Kicker {
constructor(toneContext, data = {}) {
this.params = data.kicker || data || {};
// MembraneSynth é perfeito para Kicks e Toms
this.synth = new Tone.MembraneSynth({
pitchDecay: 0.05,
octaves: 6, // O Kicker desce muitas oitavas
oscillator: { type: "sine" },
envelope: {
attack: 0.001,
decay: 0.4,
sustain: 0.01,
release: 1.4
}
});
// Distorção opcional (O Kicker tem um drive)
this.dist = new Tone.Distortion(0.1).toDestination();
this.synth.disconnect();
this.synth.connect(this.dist);
this.output = this.dist;
}
triggerAttackRelease(note, duration, time) {
// O Kicker original geralmente ignora a nota MIDI e usa frequências fixas (Start/End freq)
// Mas para facilitar a composição, vamos permitir que ele toque a nota do Piano Roll
// Se quiser ser fiel ao LMMS, teríamos que ler this.params.start_freq
// Vamos usar a nota C1 ou C2 como base se a nota vier muito aguda
this.synth.triggerAttackRelease(note, duration, time);
}
connect(dest) {
this.output.connect(dest);
}
dispose() {
this.synth.dispose();
this.dist.dispose();
}
}
+49
View File
@@ -0,0 +1,49 @@
import * as Tone from "https://esm.sh/tone";
export class Lb302 {
constructor(toneContext, data = {}) {
this.params = data.lb302 || data || {};
// O som clássico do 303 é Sawtooth ou Square com filtro ressonante
const wave = this.params.wave === 1 ? "square" : "sawtooth";
this.synth = new Tone.MonoSynth({
oscillator: { type: wave },
envelope: {
attack: 0.01,
decay: 0.3,
sustain: 0.1,
release: 0.2
},
filterEnvelope: {
attack: 0.01,
decay: 0.3,
sustain: 0.1,
release: 0.2,
baseFrequency: 200,
octaves: 4,
exponent: 2
},
filter: {
Q: 6, // Alta ressonância (o "grito" do acid)
type: "lowpass",
rolloff: -24
}
});
this.output = this.synth;
}
triggerAttackRelease(note, duration, time) {
// O LB302 é monofônico, mas aceita trigger normal
this.synth.triggerAttackRelease(note, duration, time);
}
connect(dest) {
this.output.connect(dest);
}
dispose() {
this.synth.dispose();
}
}
+45
View File
@@ -0,0 +1,45 @@
import * as Tone from "https://esm.sh/tone";
export class Nes {
constructor(toneContext, data = {}) {
this.params = data.nes || data || {};
// Mapear tipo de onda do LMMS para Tone.js
// 0=Pulse12.5, 1=Pulse25, 2=Pulse50, 3=Triangle, 4=Noise
let oscType = "pulse";
let width = 0.5;
const mode = parseInt(this.params.mode || 0);
if (mode === 0) { oscType = "pulse"; width = 0.125; }
else if (mode === 1) { oscType = "pulse"; width = 0.25; }
else if (mode === 2) { oscType = "pulse"; width = 0.5; }
else if (mode === 3) { oscType = "triangle"; }
else if (mode === 4) { oscType = "square"; } // Aproximação para noise tonal
// Chiptunes geralmente não têm polifonia, mas vamos permitir PolySynth para acordes
this.synth = new Tone.PolySynth(Tone.Synth, {
oscillator: { type: oscType, width: width },
envelope: { attack: 0.001, decay: 0.1, sustain: 0.1, release: 0.01 } // Envelope rápido "clicky"
});
// BitCrusher para dar a sujeira 8-bit
this.crusher = new Tone.BitCrusher(4).toDestination(); // 4 bits
this.synth.connect(this.crusher);
this.output = this.crusher;
}
triggerAttackRelease(note, duration, time) {
this.synth.triggerAttackRelease(note, duration, time);
}
connect(dest) {
this.output.connect(dest);
}
dispose() {
this.synth.dispose();
this.crusher.dispose();
}
}
+51
View File
@@ -0,0 +1,51 @@
// js/plugins/SuperSaw.js
import * as Tone from "https://esm.sh/tone";
export class SuperSaw {
constructor(toneContext, data = {}) {
// Tenta pegar dados de qualquer um dos plugins que usam esse motor
this.params = data.zynaddsubfx || data.watsyn || data || {};
// O "FatSawtooth" do Tone.js cria 3 serras desafinadas automaticamente
this.synth = new Tone.PolySynth(Tone.Synth, {
oscillator: {
type: "fatsawtooth",
count: 3, // 3 serras por voz
spread: 20 // Desafinação entre elas (o "gordura")
},
envelope: {
attack: 0.01, // Ataque rápido (lead)
decay: 0.1,
sustain: 0.6,
release: 0.4 // Release médio para "encher" o som
},
volume: -6 // Reduz um pouco pois o Fatsawtooth é alto
});
// Efeitos para dar "largura" estéreo (O segredo do SuperSaw)
// Chorus: duplica o sinal e desafina levemente
this.chorus = new Tone.Chorus(4, 2.5, 0.5).start();
// Filtro para cortar frequências muito agudas e evitar chiado digital
this.filter = new Tone.Filter(8000, "lowpass");
// Cadeia: Synth -> Chorus -> Filter -> Output
this.synth.chain(this.chorus, this.filter);
this.output = this.filter;
}
triggerAttackRelease(note, duration, time) {
this.synth.triggerAttackRelease(note, duration, time);
}
connect(dest) {
this.output.connect(dest);
}
dispose() {
this.synth.dispose();
this.chorus.dispose();
this.filter.dispose();
}
}
+64 -97
View File
@@ -1,115 +1,82 @@
export class TripleOscillator {
constructor(audioCtx, data) {
this.ctx = audioCtx;
// Os dados do plugin geralmente vêm dentro de uma chave com o nome dele (ex: data.tripleoscillator)
// Mas às vezes vêm "flat" dependendo do parser. Vamos garantir.
this.params = data.tripleoscillator || data;
// js/plugins/TripleOscillator.js
import * as Tone from "https://esm.sh/tone";
// Dados do envelope (ADSR) ficam em 'elvol' (Envelope Volume)
this.env = data.elvol || {};
export class TripleOscillator {
constructor(toneContext, data = {}) {
// Não precisamos mais do contexto cru, usamos o Tone direto
this.params = data.tripleoscillator || data || {};
// Volume Mestre do Instrumento
this.output = new Tone.Gain(1); // Começa com volume 1
// Envelope de Amplitude (ADSR)
// Convertendo valores do LMMS (geralmente 0-1 ou 0-100) para segundos/níveis
const envData = data.elvol || {};
this.envelope = new Tone.AmplitudeEnvelope({
attack: this.normalizeParam(envData.att, 0, 0.01),
decay: this.normalizeParam(envData.dec, 0.5, 0.1),
sustain: this.normalizeParam(envData.sustain, 1, 0.5),
release: this.normalizeParam(envData.rel, 0, 0.1)
});
// Conecta Envelope -> Saída
this.envelope.connect(this.output);
}
normalizeParam(val, defaultVal, scale = 1) {
if (val === undefined || val === null) return defaultVal;
let v = parseFloat(val);
// Se vier > 2, assume escala 0-100 e divide
if (v > 2) v /= 100;
return v || defaultVal; // Fallback se for 0 ou NaN
}
getWaveType(lmmsTypeIndex) {
// Mapeamento: 0=Sine, 1=Triangle, 2=Sawtooth, 3=Square, 4=Noise, 5=Exp
const types = [
"sine",
"triangle",
"sawtooth",
"square",
"sawtooth",
"sawtooth",
];
const types = ["sine", "triangle", "sawtooth", "square", "sawtooth", "sawtooth"]; // Mapeamento básico
const idx = parseInt(lmmsTypeIndex);
return types[isNaN(idx) ? 0 : idx]; // Default para sine
return types[isNaN(idx) ? 0 : idx];
}
playNote(midiNote, startTime, duration) {
// Fórmula de conversão MIDI -> Frequência
const freq = 440 * Math.pow(2, (midiNote - 69) / 12);
// Método chamado pela UI (Step Sequencer)
triggerAttackRelease(note, duration, time) {
const freq = Tone.Frequency(note).toFrequency();
const dur = Tone.Time(duration).toSeconds();
const now = time || Tone.now();
// Ganho Mestre desta nota (evita estouro de áudio)
const masterGain = this.ctx.createGain();
masterGain.connect(this.ctx.destination);
// Aplica Envelope ADSR no volume mestre
this.applyEnvelope(masterGain, startTime, duration);
// O TripleOscillator tem 3 osciladores (Osc1, Osc2, Osc3)
// No XML/JSON eles são sufixados com 0, 1 e 2 (ex: vol0, vol1...)
// Cria os 3 osciladores efêmeros (só para esta nota)
// Isso é necessário para polifonia (cada nota tem seus osciladores)
for (let i = 0; i < 3; i++) {
// Volume: O LMMS usa 0-100. O Web Audio usa 0.0-1.0.
const volRaw = this.params[`vol${i}`];
const vol = parseInt(volRaw !== undefined ? volRaw : i === 0 ? 100 : 0);
// Default: Osc 1 (índice 0) = 100%, outros = 0%
const vol = (volRaw !== undefined) ? parseInt(volRaw) : (i === 0 ? 100 : 0);
// Se volume for 0, não gasta processamento criando oscilador
if (vol > 0) {
const osc = this.ctx.createOscillator();
const oscGain = this.ctx.createGain();
const osc = new Tone.Oscillator({
type: this.getWaveType(this.params[`wavetype${i}`]),
frequency: freq,
detune: (parseInt(this.params[`coarse${i}`] || 0) * 100) + parseInt(this.params[`fine${i}`] || 0),
volume: Tone.gainToDb((vol / 100) * 0.3), // 0.3 para evitar clipar a soma
onstop: () => { osc.dispose(); } // Limpa memória ao acabar
});
// Configura Onda
osc.type = this.getWaveType(this.params[`wavetype${i}`]);
// Configura Afinação (Coarse = Semitons, Fine = Cents)
const coarse = parseInt(this.params[`coarse${i}`] || 0);
const fine = parseInt(
this.params[`fine${i}`] || this.params[`finer${i}`] || 0
);
const detuneTotal = coarse * 100 + fine;
osc.frequency.value = freq;
osc.detune.value = detuneTotal;
// Configura Volume do Oscilador
// Dividimos por 300 (3 osciladores x 100) para normalizar e não distorcer
oscGain.gain.value = (vol / 100) * 0.3;
// Conexões: Osc -> OscGain -> MasterGain
osc.connect(oscGain);
oscGain.connect(masterGain);
// Toca
osc.start(startTime);
osc.stop(startTime + duration + 2.0); // +2s de margem para o release do envelope
// Conecta Osc -> Envelope (Mestre)
osc.connect(this.envelope);
osc.start(now);
osc.stop(now + dur + this.envelope.release);
}
}
// Dispara o envelope
this.envelope.triggerAttackRelease(dur, now);
}
applyEnvelope(gainNode, time, duration) {
// Valores padrão do LMMS se não existirem no JSON
// O LMMS usa escala 0-100 para tempo em alguns contextos, mas o parser XML geralmente traz valores brutos.
// Vamos assumir valores pequenos como segundos ou normalizar.
let att = parseFloat(this.env.att || 0);
let dec = parseFloat(this.env.dec || 0.5);
let sus = parseFloat(this.env.sustain || 0.5);
let rel = parseFloat(this.env.rel || 0.1);
// Ajuste empírico: Se os valores forem muito grandes (> 5), provavelmente estão em escala 0-100 ou similar
if (att > 2) att /= 100;
if (dec > 5) dec /= 100;
// Sustain é sempre nível (0-1), mas as vezes vem como 100
if (sus > 1) sus /= 100;
if (rel > 5) rel /= 100;
const now = time;
// Garante que começa zerado
gainNode.gain.cancelScheduledValues(now);
gainNode.gain.setValueAtTime(0, now);
// Attack: Sobe até o volume máximo (1.0 relativo ao masterGain)
gainNode.gain.linearRampToValueAtTime(1.0, now + att + 0.005);
// Decay: Desce até o nível de Sustain
gainNode.gain.exponentialRampToValueAtTime(
Math.max(sus, 0.001),
now + att + dec
);
// Sustain: Mantém o nível até o fim da nota
gainNode.gain.setValueAtTime(Math.max(sus, 0.001), now + duration);
// Release: Desce a zero após soltar a nota
gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration + rel);
connect(dest) {
this.output.connect(dest);
}
}
dispose() {
this.output.dispose();
this.envelope.dispose();
}
}