// 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]; } } }