mmpSearch/assets/js/creations/pattern/pattern_state.js

347 lines
11 KiB
JavaScript
Executable File

// 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 = `<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 {}
try { track.buffer?.dispose?.(); } catch {}
try { track.instrument?.dispose(); } catch {}
try { track.previewPlayer?.dispose(); } catch {}
});
appState.pattern.tracks = [];
appState.pattern.activeTrackId = null;
appState.pattern.activePatternIndex = 0;
}
export async function loadAudioForTrack(track) {
// 1) Garante Volume/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;
}
// Desconecta o que existir
try { track.instrument?.disconnect(); } catch {}
try { track.player?.disconnect(); } catch {}
try { track.previewPlayer?.disconnect(); } catch {}
// Reconecta cadeia base
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);
}
// 2) Detecta se é um arquivo de áudio suportado
const isStandardAudio =
track.samplePath && /\.(wav|mp3|ogg|flac|m4a)$/i.test(track.samplePath);
// 3) Se não for áudio padrão → Plugin (ou fallback)
if (!track.samplePath || !isStandardAudio) {
try {
// limpa sampler/preview/buffer
try { track.player?.dispose(); } catch {}
try { track.previewPlayer?.dispose(); } catch {}
try { track.buffer?.dispose?.(); } catch {}
track.player = null;
track.previewPlayer = null;
track.buffer = null;
if (track.instrument) {
try { track.instrument.dispose(); } catch {}
track.instrument = null;
}
let synth;
const name = (track.instrumentName || "kicker").toLowerCase();
const pluginData = {};
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).`);
synth = new Kicker(Tone.getContext(), pluginData);
}
// Conecta plugin na cadeia
if (synth.output) synth.connect(track.volumeNode);
else synth.connect(track.volumeNode);
track.instrument = synth;
track.type = "plugin";
if (!track.instrumentName) track.instrumentName = name;
console.log(`[Audio] Plugin carregado: ${name}`);
return track;
} catch (e) {
console.error("Erro ao carregar plugin:", track.instrumentName, e);
return track;
}
}
// 4) SAMPLER (áudio)
try {
// limpa plugin
if (track.instrument) {
try { track.instrument.dispose(); } catch {}
track.instrument = null;
}
// limpa players/buffer antigos
try { track.player?.dispose(); } catch {}
try { track.previewPlayer?.dispose(); } catch {}
try { track.buffer?.dispose?.(); } catch {}
track.player = null;
track.previewPlayer = null;
track.buffer = null;
// Player principal (Playlist/steps)
const player = new Tone.Player({
url: track.samplePath,
autostart: false,
retrigger: true,
});
// redundância segura p/ builds diferentes do Tone:
try { player.retrigger = true; } catch {}
await player.load(track.samplePath);
player.connect(track.volumeNode);
// ✅ reutiliza o MESMO buffer do player (sem segundo download)
track.buffer = player.buffer;
track.player = player;
// Preview player (Pattern Editor) — separado pra não brigar com a playlist
const previewPlayer = new Tone.Player({
autostart: false,
retrigger: true,
});
try { previewPlayer.retrigger = true; } catch {}
previewPlayer.buffer = track.buffer;
previewPlayer.connect(track.volumeNode);
track.previewPlayer = previewPlayer;
track.type = "sampler";
return track;
} catch (error) {
console.error("Erro ao carregar sample:", track.samplePath, error);
try { track.player?.dispose(); } catch {}
try { track.previewPlayer?.dispose(); } catch {}
try { track.buffer?.dispose?.(); } catch {}
track.player = null;
track.previewPlayer = null;
track.buffer = null;
return track;
}
}
export function addTrackToState() {
const totalSteps = getTotalSteps();
// ✅ define o "pai" correto pra UI conseguir renderizar
const focusedId = appState.pattern.focusedBasslineId || null;
let parentBasslineId = null;
if (focusedId) {
const basslineTrack = appState.pattern.tracks.find(
(t) => t.type === "bassline" && t.id === focusedId
);
// mesmo critério do pattern_ui: srcId = instrumentSourceId || focusedId
parentBasslineId = basslineTrack?.instrumentSourceId || focusedId;
} else {
parentBasslineId = null; // IMPORTANTÍSSIMO: não deixar undefined
}
// ✅ pega referência do mesmo pai (pra clonar patterns compatíveis)
const referenceTrack =
appState.pattern.tracks.find(
(t) =>
t.type !== "bassline" &&
(t.parentBasslineId ?? null) === parentBasslineId &&
Array.isArray(t.patterns) &&
t.patterns.length > 0
) ||
appState.pattern.tracks.find(
(t) => t.type !== "bassline" && Array.isArray(t.patterns) && t.patterns.length > 0
) ||
null;
const newId = Date.now() + Math.random();
const newTrack = {
id: newId,
name: `Novo Instrumento ${appState.pattern.tracks.filter(t => t.type !== "bassline").length + 1}`,
samplePath: null,
type: "plugin",
// ✅ AQUI a chave: agora a track entra no lugar certo (rack ou root)
parentBasslineId,
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 || totalSteps)).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());
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 {}
try { trackToRemove.previewPlayer?.dispose(); } 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.previewPlayer?.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];
}
}
}