// js/pattern/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"; // XML padrão para o instrumento Kicker (bateria simples) const DEFAULT_KICKER_XML = ` `; 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 const isStandardAudio = track.samplePath && /\.(wav|mp3|ogg|flac|m4a)$/i.test(track.samplePath); // Se não for áudio padrão, assumimos Plugin (ou Kicker padrão) if (!track.samplePath || !isStandardAudio) { try { if (track.instrument) { try { track.instrument.dispose(); } catch {} } let synth; // Normaliza o nome do instrumento. Se vazio, assume kicker. const name = (track.instrumentName || "kicker").toLowerCase(); 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": case "papu": case "sid": synth = new Nes(Tone.getContext(), pluginData); break; case "zynaddsubfx": case "watsyn": case "monstro": case "vibedstrings": case "supersaw": synth = new SuperSaw(Tone.getContext(), pluginData); break; case "organic": synth = new Tone.PolySynth(Tone.Synth, { oscillator: { type: "sine", count: 8, spread: 20 } }); break; default: console.warn(`Plugin ${name} desconhecido, usando fallback (Kicker).`); // Fallback seguro: Kicker synth = new Kicker(Tone.getContext(), pluginData); } if (synth.output) { synth.connect(track.volumeNode); } else { synth.connect(track.volumeNode); } track.instrument = synth; track.player = null; track.type = 'plugin'; // Atualiza o nome se ele estava vazio if (!track.instrumentName) track.instrumentName = name; console.log(`[Audio] Plugin carregado: ${name}`); } catch (e) { console.error("Erro ao carregar plugin:", track.instrumentName, e); } return track; } // 3. Lógica para SAMPLERS 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'; } 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 newId = Date.now() + Math.random(); const newTrack = { id: newId, name: `Novo Instrumento ${appState.pattern.tracks.length + 1}`, samplePath: null, type: 'plugin', // 🔥 CORREÇÃO: Definir instrumento padrão (Kicker) e XML instrumentName: "kicker", instrumentXml: DEFAULT_KICKER_XML, player: null, buffer: null, patterns: referenceTrack ? referenceTrack.patterns.map(p => ({ name: p.name, steps: new Array(p.steps.length).fill(false), notes: [], pos: p.pos })) : [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), notes: [], 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()); // Carrega o áudio (vai cair no case "kicker" do loadAudioForTrack) loadAudioForTrack(newTrack); appState.pattern.tracks.push(newTrack); appState.pattern.activeTrackId = newTrack.id; console.log("Faixa adicionada ao estado com Kicker padrão."); } export function removeTrackById(trackId) { const index = appState.pattern.tracks.findIndex(t => t.id === trackId); if (index !== -1) { const trackToRemove = appState.pattern.tracks[index]; // Limpeza de memória 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 {} // Remove do array appState.pattern.tracks.splice(index, 1); // Ajusta seleção se necessário if (appState.pattern.activeTrackId === trackId) { appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id ?? null; } return true; // Retorna sucesso } return false; // Não achou (o fantasma não existe aqui) } 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]; } } }