Padronização da interface e atualização do sync entre clientes

This commit is contained in:
JotaChina committed 2025-11-27 22:02:02 -03:00
1 parent 3f714c88de
commit 7d9d2b0f63
10 files changed
+796 -377

No files matched your search

+67 -39
View File
@@ -1,4 +1,4 @@
// js/pattern_state.js
// 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";
@@ -11,6 +11,11 @@ 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 = `<kicker>
<env amt="0" attack="0.01" hold="0.1" decay="0.1" release="0.1" sustain="0.5" sync_mode="0"/>
</kicker>`;
export function initializePatternState() {
appState.pattern.tracks.forEach(track => {
try { track.player?.dispose(); } catch {}
@@ -55,20 +60,17 @@ export async function loadAudioForTrack(track) {
// --- 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
// 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;
const name = (track.instrumentName || "").toLowerCase();
// Normaliza o nome do instrumento. Se vazio, assume kicker.
const name = (track.instrumentName || "kicker").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
@@ -87,64 +89,53 @@ export async function loadAudioForTrack(track) {
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
case "freeboy":
case "papu":
case "sid":
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":
case "zynaddsubfx":
case "watsyn":
case "monstro":
case "vibedstrings":
case "supersaw":
synth = new SuperSaw(Tone.getContext(), pluginData);
break;
case "organic": // Fallback simples para Organic (Additive)
case "organic":
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" } });
console.warn(`Plugin ${name} desconhecido, usando fallback (Kicker).`);
// Fallback seguro: Kicker
synth = new Kicker(Tone.getContext(), pluginData);
}
// 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';
// 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:", name, e);
console.error("Erro ao carregar plugin:", track.instrumentName, e);
}
return track;
}
// 3. Lógica para SAMPLERS (Arquivos de Áudio Reais)
// 3. Lógica para SAMPLERS
try {
try { track.player?.dispose(); } catch {}
track.player = null;
@@ -166,7 +157,7 @@ export async function loadAudioForTrack(track) {
track.player = player;
track.buffer = buffer;
track.type = 'sampler'; // Garante o tipo correto
track.type = 'sampler';
} catch (error) {
console.error('Erro ao carregar sample:', track.samplePath);
@@ -181,21 +172,29 @@ export async function loadAudioForTrack(track) {
export function addTrackToState() {
const totalSteps = getTotalSteps();
const referenceTrack = appState.pattern.tracks[0];
const newId = Date.now() + Math.random();
const newTrack = {
id: Date.now() + Math.random(),
name: "novo instrumento",
id: newId,
name: `Novo Instrumento ${appState.pattern.tracks.length + 1}`,
samplePath: null,
type: 'plugin', // Padrão
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), pos: 0 }],
: [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), notes: [], pos: 0 }],
activePatternIndex: 0,
volume: DEFAULT_VOLUME,
pan: DEFAULT_PAN,
@@ -206,10 +205,39 @@ export function addTrackToState() {
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() {