116 lines
4.0 KiB
JavaScript
116 lines
4.0 KiB
JavaScript
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;
|
|
|
|
// Dados do envelope (ADSR) ficam em 'elvol' (Envelope Volume)
|
|
this.env = data.elvol || {};
|
|
}
|
|
|
|
getWaveType(lmmsTypeIndex) {
|
|
// Mapeamento: 0=Sine, 1=Triangle, 2=Sawtooth, 3=Square, 4=Noise, 5=Exp
|
|
const types = [
|
|
"sine",
|
|
"triangle",
|
|
"sawtooth",
|
|
"square",
|
|
"sawtooth",
|
|
"sawtooth",
|
|
];
|
|
const idx = parseInt(lmmsTypeIndex);
|
|
return types[isNaN(idx) ? 0 : idx]; // Default para sine
|
|
}
|
|
|
|
playNote(midiNote, startTime, duration) {
|
|
// Fórmula de conversão MIDI -> Frequência
|
|
const freq = 440 * Math.pow(2, (midiNote - 69) / 12);
|
|
|
|
// 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...)
|
|
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);
|
|
|
|
// Se volume for 0, não gasta processamento criando oscilador
|
|
if (vol > 0) {
|
|
const osc = this.ctx.createOscillator();
|
|
const oscGain = this.ctx.createGain();
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|