ativar/desativar tracks de áudio na playlist, knobs funcionais

This commit is contained in:
JotaChina committed 2025-12-27 23:21:58 -03:00
1 parent 73a5257aec
commit ce9a9dfda8
2 files changed
+323 -33

No files matched your search

+187 -31
View File
@@ -4,7 +4,7 @@ import * as Tone from "https://esm.sh/tone";
import { appState } from "../state.js";
import { highlightStep } from "./pattern_ui.js";
import { getTotalSteps } from "../utils.js";
import { initializeAudioContext } from "../audio.js";
import { initializeAudioContext, getMainGainNode } from "../audio.js";
import { TripleOscillator } from "../../audio/plugins/TripleOscillator.js";
import { Nes } from "../../audio/plugins/Nes.js";
@@ -12,6 +12,50 @@ import { SuperSaw } from "../../audio/plugins/SuperSaw.js";
import { Lb302 } from "../../audio/plugins/Lb302.js";
import { Kicker } from "../../audio/plugins/Kicker.js";
const MAX_VOL = 1.5;
const clamp = (n, a, b) => Math.max(a, Math.min(b, Number(n) || 0));
function getBasslineByPatternIndex(pi) {
return (appState.pattern.tracks || []).find(t => t.type === "bassline" && Number(t.patternIndex) === Number(pi));
}
function getSongMix(track, patternIndex) {
track._songMix ??= {};
if (track._songMix[patternIndex]) return track._songMix[patternIndex];
const instVol = new Tone.Volume(0);
const instPan = new Tone.Panner(0);
const pattVol = new Tone.Volume(0);
const pattPan = new Tone.Panner(0);
instVol.connect(instPan);
instPan.connect(pattVol);
pattVol.connect(pattPan);
pattPan.connect(getMainGainNode());
track._songMix[patternIndex] = { instVol, instPan, pattVol, pattPan };
return track._songMix[patternIndex];
}
function refreshSongMixFor(track, patternIndex) {
const b = getBasslineByPatternIndex(patternIndex);
const mix = getSongMix(track, patternIndex);
const instMuted = !!(track.isMuted || track.muted);
const pattMuted = !!(b?.isMuted || b?.muted);
const instV = clamp(track.volume ?? 1, 0, MAX_VOL);
const pattV = clamp(b?.volume ?? 1, 0, MAX_VOL);
mix.instVol.volume.value = (instMuted || instV === 0) ? -Infinity : Tone.gainToDb(instV);
mix.pattVol.volume.value = (pattMuted || pattV === 0) ? -Infinity : Tone.gainToDb(pattV);
mix.instPan.pan.value = clamp(track.pan ?? 0, -1, 1);
mix.pattPan.pan.value = clamp(b?.pan ?? 0, -1, 1);
return mix;
}
function getActivePatternForTrack(track) {
const idx = appState.pattern?.activePatternIndex ?? track.activePatternIndex ?? 0;
return track.patterns?.[idx] ?? null;
@@ -130,26 +174,67 @@ export function playSample(filePath, trackId) {
}
}
function getSongStepPlayer(track, patternIndex) {
if (track.type !== "sampler" || !track.buffer) return null;
function playSamplerNoteAtTime(track, midi, time, durationSec) {
if (!track?.buffer || !track.volumeNode) return;
track._songStepPlayer ??= {};
if (track._songStepPlayer[patternIndex]) return track._songStepPlayer[patternIndex];
const mix = refreshSongMixFor(track, patternIndex);
const p = new Tone.Player(track.buffer);
p.connect(mix.instVol);
track._songStepPlayer[patternIndex] = p;
return p;
}
function _getPluginKey(track) {
return String(
track.pluginName ||
track.instrumentName ||
track.instrument?.constructor?.name ||
""
).toLowerCase();
}
function getSongInstrument(track, patternIndex) {
if (track.type !== "plugin") return null;
track._songInstrument ??= {};
if (track._songInstrument[patternIndex]) return track._songInstrument[patternIndex];
const key = _getPluginKey(track);
const Cls = PLUGIN_CLASSES[key];
if (!Cls) {
console.warn("[Song] Plugin não encontrado:", key, "track:", track.name);
return null;
}
const mix = refreshSongMixFor(track, patternIndex);
const inst = new Cls(null, track.params || track.pluginData || {});
inst.connect(mix.instVol);
track._songInstrument[patternIndex] = inst;
return inst;
}
function playSamplerNoteAtTime(track, midi, time, durationSec, destinationNode = null) {
if (!track?.buffer) return;
const base = track.baseNote ?? 60;
const semitones = (midi - base);
const semitones = midi - base;
const rate = Math.pow(2, semitones / 12);
const player = new Tone.Player(track.buffer);
player.playbackRate = rate;
player.connect(track.volumeNode);
// destino: song bus (mix.instVol) OU cadeia normal do pattern editor
const dest = destinationNode || track.volumeNode || getMainGainNode();
player.connect(dest);
player.start(time);
// se quiser respeitar duração (bem básico)
if (durationSec && durationSec > 0) {
player.stop(time + durationSec);
}
// limpeza
if (durationSec && durationSec > 0) player.stop(time + durationSec);
player.onstop = () => player.dispose();
}
@@ -587,7 +672,7 @@ export function startSongPatternPlaybackOnTransport() {
// Patterns ativas neste tick (pelas basslines/playlist clips)
const basslineTracks = appState.pattern.tracks.filter(
(t) => t.type === "bassline" && !t.isMuted
(t) => t.type === "bassline" && !(t.isMuted || t.muted)
);
const activePatternHits = [];
@@ -600,12 +685,36 @@ export function startSongPatternPlaybackOnTransport() {
activePatternHits.push({ patternIndex: b.patternIndex, localStep });
}
// Quais patternIndex estão ativas AGORA (tem clip tocando neste tick)
const activePatternSet = new Set(activePatternHits.map(h => Number(h.patternIndex)));
// Sempre atualiza buses já existentes e “fecha o gate” quando a pattern não está ativa
for (const tr of appState.pattern.tracks || []) {
if (tr.type === "bassline") continue;
const mixMap = tr._songMix;
if (!mixMap) continue;
for (const piStr of Object.keys(mixMap)) {
const pi = Number(piStr);
// atualiza vol/pan/mute do instrumento + vol/pan/mute da pattern
const mix = refreshSongMixFor(tr, pi);
// 🔑 gate: se essa patternIndex NÃO está ativa neste tick, corta IMEDIATAMENTE
if (!activePatternSet.has(pi)) {
mix.pattVol.volume.value = -Infinity;
}
}
}
// Agora sim: se não tem hits, sai (mas já “gateou” o sustain)
if (activePatternHits.length === 0) return;
// Dispara tracks reais (samplers/plugins)
for (const track of appState.pattern.tracks) {
if (track.type === "bassline") continue;
if (track.muted) continue;
if (track.muted || track.isMuted) continue;
for (const hit of activePatternHits) {
const patt = track.patterns?.[hit.patternIndex];
@@ -641,13 +750,19 @@ export function startSongPatternPlaybackOnTransport() {
? (Math.floor(tickInPattern / LMMS_TICKS_PER_STEP) % pattLenSteps)
: hit.localStep;
// ✅ 1) PLUGIN com piano roll (notes)
// ✅ 1) PLUGIN com piano roll (notes) — USAR instrumento por patternIndex
if (
track.type === "plugin" &&
track.instrument &&
Array.isArray(patt.notes) &&
patt.notes.length > 0
) {
// cria/recupera a instância do plugin para esta patternIndex
const inst = getSongInstrument(track, hit.patternIndex);
if (!inst) continue;
// garante que o mix/bus exista e esteja atualizado
refreshSongMixFor(track, hit.patternIndex);
const stepStartTick = tickInPattern;
const stepEndTick = stepStartTick + LMMS_TICKS_PER_STEP;
@@ -677,7 +792,7 @@ export function startSongPatternPlaybackOnTransport() {
const freq = Tone.Frequency(midi, "midi").toFrequency();
try {
track.instrument.triggerAttackRelease(freq, durSec, t2, vel);
inst.triggerAttackRelease(freq, durSec, t2, vel);
} catch (e) {
console.warn("[Playlist] Falha ao tocar plugin note:", track.name, e);
}
@@ -717,7 +832,8 @@ export function startSongPatternPlaybackOnTransport() {
const lenTicks = rawLen < 0 ? LMMS_TICKS_PER_STEP : Math.max(rawLen, LMMS_TICKS_PER_STEP);
const durSec = Math.max(0.01, ticksToSec(lenTicks, stepIntervalSec));
playSamplerNoteAtTime(track, Number(n.key) || 0, t2, durSec);
const mix = refreshSongMixFor(track, hit.patternIndex);
playSamplerNoteAtTime(track, Number(n.key) || 0, t2, durSec, mix.instVol);
}
continue; // não cai na lógica de steps
@@ -727,22 +843,33 @@ export function startSongPatternPlaybackOnTransport() {
if (!patt.steps) continue;
if (patt.steps[stepInPattern]) {
if (track.type === "sampler" && track.player) {
try {
// ✅ retrigger LMMS-like: não “some” quando sample é longo
if (typeof track.player.restart === "function") {
track.player.restart(time);
} else {
if (track.player.state === "started") track.player.stop(time);
track.player.start(time);
const mix = refreshSongMixFor(track, hit.patternIndex);
// sampler step -> player por patternIndex
if (track.type === "sampler" && track.buffer) {
const p = getSongStepPlayer(track, hit.patternIndex);
if (p) {
try {
if (typeof p.restart === "function") {
p.restart(time);
} else {
if (p.state === "started") p.stop(time);
p.start(time);
}
} catch (e) {
console.warn("[Playlist] Falha ao retrigger step:", track.name, e);
}
} catch (e) {
console.warn("[Playlist] Falha ao retrigger sample:", track.name, e);
}
} else if (track.type === "plugin" && track.instrument) {
try {
track.instrument.triggerAttackRelease("C5", "16n", time);
} catch {}
}
// plugin step -> instrumento por patternIndex
else if (track.type === "plugin") {
const inst = getSongInstrument(track, hit.patternIndex);
if (inst) {
try {
inst.triggerAttackRelease("C5", "16n", time);
} catch {}
}
}
}
}
@@ -752,6 +879,35 @@ export function startSongPatternPlaybackOnTransport() {
export function stopSongPatternPlaybackOnTransport() {
if (songPatternScheduleId === null) return;
for (const tr of appState.pattern.tracks || []) {
// mix nodes
if (tr._songMix) {
for (const pi of Object.keys(tr._songMix)) {
const m = tr._songMix[pi];
try { m.instVol?.dispose(); } catch {}
try { m.instPan?.dispose(); } catch {}
try { m.pattVol?.dispose(); } catch {}
try { m.pattPan?.dispose(); } catch {}
}
tr._songMix = null;
}
// plugin por pattern
if (tr._songInstrument) {
for (const pi of Object.keys(tr._songInstrument)) {
try { tr._songInstrument[pi]?.dispose?.(); } catch {}
}
tr._songInstrument = null;
}
// step players por pattern
if (tr._songStepPlayer) {
for (const pi of Object.keys(tr._songStepPlayer)) {
try { tr._songStepPlayer[pi]?.dispose?.(); } catch {}
}
tr._songStepPlayer = null;
}
}
try {
Tone.Transport.clear(songPatternScheduleId);
} catch {}