plugin v1.0.0
This commit is contained in:
1 parent
12c5b55bce
commit
44fce48bb5
12 files changed
+1316
-635
No files matched your search
@@ -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