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

+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();
}
}