plugin v1.0.0
This commit is contained in:
1 parent
12c5b55bce
commit
44fce48bb5
12 files changed
+1316
-635
No files matched your search
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user