This commit is contained in:
JotaChina committed 2025-11-20 13:43:09 -03:00
1 parent 42da68afa0
commit 12c5b55bce
29 files changed
+162410 -343433

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
import { TripleOscillator } from "./plugins/TripleOscillator.js";
import { PLUGINS } from "utils.js";
export class InstrumentFactory {
constructor() {
// Cria um contexto de áudio único para a página
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
play(instrumentData) {
// Pega o nome do plugin (que vem do python em minúsculo)
// O parser as vezes bota o nome na raiz ou dentro de tags.
// Vamos assumir que o "instrument_name" é a chave principal.
// Normalização de nome (ex: "TripleOscillator" -> "tripleoscillator")
const pluginName = (
instrumentData.instrument_name ||
instrumentData.name ||
""
).toLowerCase();
if (PLUGINS[pluginName]) {
const InstrumentClass = PLUGINS[pluginName];
const instr = new InstrumentClass(this.audioCtx, instrumentData);
// Toca uma nota de teste (Dó central - MIDI 60)
instr.playNote(60, this.audioCtx.currentTime, 0.5);
} else {
console.warn(`Plugin não implementado no JS: ${pluginName}`);
alert(`O plugin ${pluginName} ainda não tem implementação Web Audio.`);
}
}
}
+115
View File
@@ -0,0 +1,115 @@
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);
}
}
+29
View File
@@ -0,0 +1,29 @@
// Mapeamento de Strings do LMMS para Classes JS
const PLUGINS = {
tripleoscillator: TripleOscillator,
// 'kicker': Kicker, // Implementaremos em breve
// 'organic': Organic
};
SUPPORTED_PLUGINS = [
"audiofileprocessor",
"bitinvader",
"freeboy",
"papu",
"gigplayer",
"kicker",
"lb302",
"malletsstk",
"monstro",
"nes",
"opl2",
"organic",
"patman",
"sf2player",
"sfxr",
"sid",
"tripleoscillator",
"vibedstrings",
"watsyn",
"zynaddsubfx",
]