254 lines
8.4 KiB
JavaScript
254 lines
8.4 KiB
JavaScript
// js/pattern_state.js
|
|
import * as Tone from "https://esm.sh/tone";
|
|
import { TripleOscillator } from "../../audio/plugins/TripleOscillator.js";
|
|
import { Kicker } from "../../audio/plugins/Kicker.js";
|
|
import { Lb302 } from "../../audio/plugins/Lb302.js";
|
|
import { Nes } from "../../audio/plugins/Nes.js";
|
|
import { SuperSaw } from "../../audio/plugins/SuperSaw.js";
|
|
|
|
import { appState } from "../state.js";
|
|
import { DEFAULT_VOLUME, DEFAULT_PAN } from "../config.js";
|
|
import { getMainGainNode } from "../audio.js";
|
|
import { getTotalSteps } from "../utils.js";
|
|
|
|
export function initializePatternState() {
|
|
appState.pattern.tracks.forEach(track => {
|
|
try { track.player?.dispose(); } catch {}
|
|
try { track.buffer?.dispose?.(); } catch {}
|
|
try { track.instrument?.dispose(); } catch {}
|
|
});
|
|
|
|
appState.pattern.tracks = [];
|
|
appState.pattern.activeTrackId = null;
|
|
appState.pattern.activePatternIndex = 0;
|
|
}
|
|
|
|
export async function loadAudioForTrack(track) {
|
|
// 1. Garante a criação dos nós de Volume e Pan
|
|
try {
|
|
if (!track.volumeNode) {
|
|
track.volumeNode = new Tone.Volume(
|
|
track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume)
|
|
);
|
|
} else {
|
|
track.volumeNode.volume.value =
|
|
track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume);
|
|
}
|
|
|
|
if (!track.pannerNode) {
|
|
track.pannerNode = new Tone.Panner(track.pan ?? 0);
|
|
} else {
|
|
track.pannerNode.pan.value = track.pan ?? 0;
|
|
}
|
|
|
|
try { track.instrument?.disconnect(); } catch {}
|
|
try { track.player?.disconnect(); } catch {}
|
|
try { track.volumeNode.disconnect(); } catch {}
|
|
try { track.pannerNode.disconnect(); } catch {}
|
|
|
|
track.volumeNode.connect(track.pannerNode);
|
|
track.pannerNode.connect(getMainGainNode());
|
|
|
|
} catch (e) {
|
|
console.error("Erro ao criar nós de áudio base:", e);
|
|
}
|
|
|
|
// --- DETECÇÃO DE TIPO DE ARQUIVO ---
|
|
// Verifica se é um formato de áudio que o navegador suporta
|
|
// Verifica tipo de arquivo
|
|
const isStandardAudio = track.samplePath && /\.(wav|mp3|ogg|flac|m4a)$/i.test(track.samplePath);
|
|
const isDrumSynth = track.samplePath && /\.ds$/i.test(track.samplePath);
|
|
|
|
// Se não for áudio (ou for .ds/.xpf), é Plugin
|
|
if (!track.samplePath || !isStandardAudio) {
|
|
try {
|
|
if (track.instrument) { try { track.instrument.dispose(); } catch {} }
|
|
|
|
let synth;
|
|
const name = (track.instrumentName || "").toLowerCase();
|
|
|
|
// DADOS DO PLUGIN (Tenta parsear XML se for string, ou usa vazio)
|
|
// Dica: O ideal seria ter um parser real aqui, mas vamos passar {} por enquanto
|
|
const pluginData = {};
|
|
|
|
// SELETOR DE PLUGINS
|
|
switch (name) {
|
|
case "tripleoscillator":
|
|
case "3osc":
|
|
synth = new TripleOscillator(Tone.getContext(), pluginData);
|
|
break;
|
|
|
|
case "kicker":
|
|
synth = new Kicker(Tone.getContext(), pluginData);
|
|
break;
|
|
|
|
case "lb302":
|
|
synth = new Lb302(Tone.getContext(), pluginData);
|
|
break;
|
|
|
|
case "nes":
|
|
case "freeboy": // Freeboy é parecido com NES
|
|
case "papu": // Papu também é Gameboy
|
|
case "sid": // SID é 8-bit também, usaremos NES como fallback por enquanto
|
|
synth = new Nes(Tone.getContext(), pluginData);
|
|
break;
|
|
|
|
// --- PACOTE SUPER SAW ---
|
|
case "zynaddsubfx": // O clássico
|
|
case "watsyn": // Wavetable Synth
|
|
case "monstro": // 3 Osciladores monstruosos
|
|
case "vibedstrings": // Strings vibrantes (fatsaw funciona bem como base)
|
|
case "SuperSaw":
|
|
synth = new SuperSaw(Tone.getContext(), pluginData);
|
|
break;
|
|
|
|
case "organic": // Fallback simples para Organic (Additive)
|
|
synth = new Tone.PolySynth(Tone.Synth, {
|
|
oscillator: { type: "sine", count: 8, spread: 20 }
|
|
});
|
|
break;
|
|
|
|
case "zynaddsubfx": // O monstro!
|
|
// Zyn é impossível de clonar rápido. Vamos usar um "SuperSaw" gordo como placeholder
|
|
synth = new Tone.PolySynth(Tone.Synth, {
|
|
oscillator: { type: "fatsawtooth", count: 3, spread: 30 },
|
|
envelope: { attack: 0.1, decay: 0.3, sustain: 0.8, release: 1 }
|
|
});
|
|
break;
|
|
|
|
default:
|
|
console.warn(`Plugin ${name} desconhecido, usando fallback.`);
|
|
// Fallback genérico
|
|
synth = new Tone.PolySynth(Tone.Synth, { oscillator: { type: "triangle" } });
|
|
}
|
|
|
|
// Se o plugin criou uma classe wrapper (nossas classes .js),
|
|
// ele tem o método .connect. Se for Tone nativo (Organic/Zyn fallback), também tem.
|
|
if (synth.output) {
|
|
// Nossas classes customizadas
|
|
synth.connect(track.volumeNode);
|
|
} else {
|
|
// Objetos Tone.js puros
|
|
synth.connect(track.volumeNode);
|
|
}
|
|
|
|
track.instrument = synth;
|
|
track.player = null;
|
|
track.type = 'plugin';
|
|
|
|
console.log(`[Audio] Plugin carregado: ${name}`);
|
|
|
|
} catch (e) {
|
|
console.error("Erro ao carregar plugin:", name, e);
|
|
}
|
|
return track;
|
|
}
|
|
|
|
// 3. Lógica para SAMPLERS (Arquivos de Áudio Reais)
|
|
try {
|
|
try { track.player?.dispose(); } catch {}
|
|
track.player = null;
|
|
try { track.buffer?.dispose?.(); } catch {}
|
|
track.buffer = null;
|
|
|
|
if (track.instrument) {
|
|
try { track.instrument.dispose(); } catch {}
|
|
track.instrument = null;
|
|
}
|
|
|
|
const player = new Tone.Player({ url: track.samplePath, autostart: false });
|
|
await player.load(track.samplePath);
|
|
|
|
player.connect(track.volumeNode);
|
|
|
|
const buffer = new Tone.Buffer();
|
|
await buffer.load(track.samplePath);
|
|
|
|
track.player = player;
|
|
track.buffer = buffer;
|
|
track.type = 'sampler'; // Garante o tipo correto
|
|
|
|
} catch (error) {
|
|
console.error('Erro ao carregar sample:', track.samplePath);
|
|
try { track.player?.dispose(); } catch {}
|
|
try { track.buffer?.dispose?.(); } catch {}
|
|
track.player = null;
|
|
track.buffer = null;
|
|
}
|
|
return track;
|
|
}
|
|
|
|
export function addTrackToState() {
|
|
const totalSteps = getTotalSteps();
|
|
const referenceTrack = appState.pattern.tracks[0];
|
|
|
|
const newTrack = {
|
|
id: Date.now() + Math.random(),
|
|
name: "novo instrumento",
|
|
samplePath: null,
|
|
type: 'plugin', // Padrão
|
|
player: null,
|
|
buffer: null,
|
|
patterns: referenceTrack
|
|
? referenceTrack.patterns.map(p => ({
|
|
name: p.name,
|
|
steps: new Array(p.steps.length).fill(false),
|
|
pos: p.pos
|
|
}))
|
|
: [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), pos: 0 }],
|
|
activePatternIndex: 0,
|
|
volume: DEFAULT_VOLUME,
|
|
pan: DEFAULT_PAN,
|
|
volumeNode: new Tone.Volume(Tone.gainToDb(DEFAULT_VOLUME)),
|
|
pannerNode: new Tone.Panner(DEFAULT_PAN),
|
|
};
|
|
|
|
newTrack.volumeNode.connect(newTrack.pannerNode);
|
|
newTrack.pannerNode.connect(getMainGainNode());
|
|
|
|
loadAudioForTrack(newTrack);
|
|
|
|
appState.pattern.tracks.push(newTrack);
|
|
appState.pattern.activeTrackId = newTrack.id;
|
|
}
|
|
|
|
export function removeLastTrackFromState() {
|
|
if (appState.pattern.tracks.length > 0) {
|
|
const trackToRemove = appState.pattern.tracks[appState.pattern.tracks.length - 1];
|
|
|
|
try { trackToRemove.player?.dispose(); } catch {}
|
|
try { trackToRemove.buffer?.dispose?.(); } catch {}
|
|
try { trackToRemove.instrument?.dispose(); } catch {}
|
|
try { trackToRemove.pannerNode?.disconnect(); } catch {}
|
|
try { trackToRemove.volumeNode?.disconnect(); } catch {}
|
|
|
|
appState.pattern.tracks.pop();
|
|
if (appState.pattern.activeTrackId === trackToRemove.id) {
|
|
appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id ?? null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function updateTrackSample(trackIndex, samplePath) {
|
|
const track = appState.pattern.tracks[trackIndex];
|
|
if (track) {
|
|
track.samplePath = samplePath;
|
|
track.name = samplePath.split("/").pop();
|
|
|
|
// Reseta o tipo baseado no novo arquivo
|
|
const isAudio = /\.(wav|mp3|ogg|flac|m4a)$/i.test(samplePath);
|
|
track.type = isAudio ? 'sampler' : 'plugin';
|
|
|
|
await loadAudioForTrack(track);
|
|
}
|
|
}
|
|
|
|
export function toggleStepState(trackIndex, stepIndex) {
|
|
const track = appState.pattern.tracks[trackIndex];
|
|
if (track && track.patterns && track.patterns.length > 0) {
|
|
const activePattern = track.patterns[track.activePatternIndex];
|
|
if (activePattern && activePattern.steps.length > stepIndex) {
|
|
activePattern.steps[stepIndex] = !activePattern.steps[stepIndex];
|
|
}
|
|
}
|
|
} |