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