From a3b432ae2960bb3688c565f41d5b7562d2a8c1f4 Mon Sep 17 00:00:00 2001 From: JotaChina Date: Sat, 27 Dec 2025 22:10:08 -0300 Subject: [PATCH] =?UTF-8?q?ativar/desativar=20tracks=20de=20=C3=A1udio,=20?= =?UTF-8?q?knobs=20funcionais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/creator.css | 3 + assets/js/creations/audio/audio_ui.js | 5 +- assets/js/creations/pattern/pattern_state.js | 103 ++++++++++--- assets/js/creations/pattern/pattern_ui.js | 147 ++++++++++++++++++- assets/js/creations/socket.js | 30 ++++ 5 files changed, 260 insertions(+), 28 deletions(-) diff --git a/assets/css/creator.css b/assets/css/creator.css index e7fbf043..4d104de5 100755 --- a/assets/css/creator.css +++ b/assets/css/creator.css @@ -278,6 +278,9 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none right: 0; } +.track-mute { background: #2ecc71; } +.track-mute.muted { background: #e74c3c; } + /* =============================================== */ /* BEAT EDITOR / STEP SEQUENCER /* =============================================== */ diff --git a/assets/js/creations/audio/audio_ui.js b/assets/js/creations/audio/audio_ui.js index 3897951d..da38db1b 100755 --- a/assets/js/creations/audio/audio_ui.js +++ b/assets/js/creations/audio/audio_ui.js @@ -456,7 +456,10 @@ export function renderAudioEditor() { // pega os instrumentos que pertencem a esse rack (mesma lógica do pattern_ui) :contentReference[oaicite:1]{index=1} const srcId = basslineTrack.instrumentSourceId || basslineTrack.id; const children = (appState.pattern.tracks || []).filter( - (t) => t.type !== "bassline" && t.parentBasslineId === srcId && !t.muted + (t) => + t.type !== "bassline" && + t.parentBasslineId === srcId && + !(t.isMuted || t.muted) ); let maxSteps = 0; diff --git a/assets/js/creations/pattern/pattern_state.js b/assets/js/creations/pattern/pattern_state.js index 7d7c95c7..ea01df6f 100755 --- a/assets/js/creations/pattern/pattern_state.js +++ b/assets/js/creations/pattern/pattern_state.js @@ -16,6 +16,82 @@ const DEFAULT_KICKER_XML = ` `; +// ------------------------------------------------------------ +// MIX / MUTE HELPERS (Volume, Pan) +// ------------------------------------------------------------ +const MAX_TRACK_VOLUME = 1.5; + +function _clamp(n, min, max) { + const x = Number(n); + if (!Number.isFinite(x)) return min; + return Math.min(max, Math.max(min, x)); +} + +function _ensureMuteFields(track) { + if (!track) return; + if (track.isMuted == null) track.isMuted = !!track.muted; + if (track.muted == null) track.muted = !!track.isMuted; +} + +function applyTrackMix(track) { + if (!track) return; + + _ensureMuteFields(track); + + const vol = _clamp(track.volume ?? DEFAULT_VOLUME, 0, MAX_TRACK_VOLUME); + const pan = _clamp(track.pan ?? DEFAULT_PAN, -1, 1); + const isMuted = !!(track.isMuted || track.muted); + + // Volume (dB): mute => -Infinity + if (!track.volumeNode) { + track.volumeNode = new Tone.Volume( + isMuted || vol === 0 ? -Infinity : Tone.gainToDb(vol) + ); + } else { + track.volumeNode.volume.value = + isMuted || vol === 0 ? -Infinity : Tone.gainToDb(vol); + } + + // Pan + if (!track.pannerNode) { + track.pannerNode = new Tone.Panner(pan); + } else { + track.pannerNode.pan.value = pan; + } + + // reconecta cadeia base (idempotente) + try { track.volumeNode.disconnect(); } catch {} + try { track.pannerNode.disconnect(); } catch {} + track.volumeNode.connect(track.pannerNode); + track.pannerNode.connect(getMainGainNode()); +} + +// API pública (usada pelo UI/socket) +export function setPatternTrackMute(trackId, isMuted) { + const t = (appState.pattern.tracks || []).find((x) => String(x.id) === String(trackId)); + if (!t) return false; + t.isMuted = !!isMuted; + t.muted = !!isMuted; + applyTrackMix(t); + return true; +} + +export function setPatternTrackVolume(trackId, volume) { + const t = (appState.pattern.tracks || []).find((x) => String(x.id) === String(trackId)); + if (!t) return false; + t.volume = _clamp(volume, 0, MAX_TRACK_VOLUME); + applyTrackMix(t); + return true; +} + +export function setPatternTrackPan(trackId, pan) { + const t = (appState.pattern.tracks || []).find((x) => String(x.id) === String(trackId)); + if (!t) return false; + t.pan = _clamp(pan, -1, 1); + applyTrackMix(t); + return true; +} + export function initializePatternState() { appState.pattern.tracks.forEach(track => { try { track.player?.dispose(); } catch {} @@ -30,33 +106,14 @@ export function initializePatternState() { } export async function loadAudioForTrack(track) { - // 1) Garante Volume/Pan + // 1) Garante Volume/Pan/Mute e reconecta a cadeia base 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); - } + applyTrackMix(track); - if (!track.pannerNode) { - track.pannerNode = new Tone.Panner(track.pan ?? 0); - } else { - track.pannerNode.pan.value = track.pan ?? 0; - } - - // Desconecta o que existir + // desconecta o que existir (para reconectar sem duplicar rotas) 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); } @@ -257,6 +314,8 @@ export function addTrackToState() { patterns, activePatternIndex: appState.pattern.activePatternIndex ?? 0, + isMuted: false, + muted: false, volume: DEFAULT_VOLUME, pan: DEFAULT_PAN, volumeNode: new Tone.Volume(Tone.gainToDb(DEFAULT_VOLUME)), diff --git a/assets/js/creations/pattern/pattern_ui.js b/assets/js/creations/pattern/pattern_ui.js index 16b8f1a8..d8e897e0 100755 --- a/assets/js/creations/pattern/pattern_ui.js +++ b/assets/js/creations/pattern/pattern_ui.js @@ -1,11 +1,83 @@ // js/pattern/pattern_ui.js import { appState } from "../state.js"; -import { updateTrackSample } from "./pattern_state.js"; -import { playSample, stopPlayback } from "./pattern_audio.js"; +import { + updateTrackSample, + setPatternTrackMute, + setPatternTrackVolume, + setPatternTrackPan, +} from "./pattern_state.js"; +import { playSample, stopPlayback } from "./pattern_audio.js"; import { getTotalSteps } from "../utils.js"; -import { sendAction } from '../socket.js'; -import { initializeAudioContext } from '../audio.js'; -import * as Tone from "https://esm.sh/tone"; +import { sendAction } from "../socket.js"; +import { initializeAudioContext } from "../audio.js"; +import * as Tone from "https://esm.sh/tone"; + +// ------------------------------------------------------------ +// Knob helpers (drag vertical para ajustar) +// ------------------------------------------------------------ +const _KNOB_MIN_DEG = -135; +const _KNOB_MAX_DEG = 135; +const _MAX_TRACK_VOLUME = 1.5; + +function _clamp(n, min, max) { + const x = Number(n); + if (!Number.isFinite(x)) return min; + return Math.min(max, Math.max(min, x)); +} + +function _valueToDeg(control, value) { + if (control === "pan") { + const v = _clamp(value, -1, 1); + const norm = (v + 1) / 2; // 0..1 + return _KNOB_MIN_DEG + norm * (_KNOB_MAX_DEG - _KNOB_MIN_DEG); + } + const v = _clamp(value, 0, _MAX_TRACK_VOLUME); + const norm = v / _MAX_TRACK_VOLUME; + return _KNOB_MIN_DEG + norm * (_KNOB_MAX_DEG - _KNOB_MIN_DEG); +} + +function _setKnobIndicator(knobEl, control, value) { + const ind = knobEl.querySelector(".knob-indicator"); + if (!ind) return; + ind.style.transform = `rotate(${_valueToDeg(control, value)}deg)`; +} + +function _attachKnobDrag(knobEl, { control, getCurrent, setLocal, commit }) { + if (!knobEl) return; + + knobEl.addEventListener("mousedown", (e) => { + e.preventDefault(); + e.stopPropagation(); + initializeAudioContext(); + + const startY = e.clientY; + const startVal = Number(getCurrent?.() ?? 0); + const sensitivity = control === "pan" ? 0.01 : 0.005; + + let lastVal = startVal; + + const onMove = (ev) => { + const delta = (startY - ev.clientY) * sensitivity; + let v = startVal + delta; + + if (control === "pan") v = _clamp(v, -1, 1); + else v = _clamp(v, 0, _MAX_TRACK_VOLUME); + + lastVal = v; + setLocal?.(v); + _setKnobIndicator(knobEl, control, v); + }; + + const onUp = () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + commit?.(lastVal); + }; + + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }); +} // Função principal de renderização para o editor de patterns export function renderPatternEditor() { @@ -140,6 +212,71 @@ export function renderPatternEditor() {
`; + // -------------------------- + // Mute (ativar/desativar canal) + // -------------------------- + const muteBtn = trackLane.querySelector(".track-mute"); + if (muteBtn) { + const isMutedNow = !!(trackData.isMuted || trackData.muted); + muteBtn.classList.toggle("muted", isMutedNow); + muteBtn.title = isMutedNow ? "Unmute" : "Mute"; + + muteBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + initializeAudioContext(); + + const t = appState.pattern.tracks[originalIndex]; + const cur = !!(t?.isMuted || t?.muted); + const next = !cur; + + // aplica local (instantâneo) + setPatternTrackMute(trackData.id, next); + + // feedback visual + muteBtn.classList.toggle("muted", next); + muteBtn.title = next ? "Unmute" : "Mute"; + + // broadcast (colaboração) + sendAction({ + type: "SET_PATTERN_TRACK_MUTE", + trackId: trackData.id, + isMuted: next, + }); + }); + } + + // -------------------------- + // Knobs (VOL / PAN) + // -------------------------- + trackLane.querySelectorAll(".knob").forEach((knobEl) => { + const control = knobEl.dataset.control; + const trackId = knobEl.dataset.trackId; + + const t = appState.pattern.tracks[originalIndex]; + const initial = control === "pan" ? (t?.pan ?? 0) : (t?.volume ?? 1); + _setKnobIndicator(knobEl, control, initial); + + _attachKnobDrag(knobEl, { + control, + getCurrent: () => { + const tr = appState.pattern.tracks.find((x) => String(x.id) === String(trackId)); + return control === "pan" ? (tr?.pan ?? 0) : (tr?.volume ?? 1); + }, + setLocal: (v) => { + if (control === "pan") setPatternTrackPan(trackId, v); + else setPatternTrackVolume(trackId, v); + }, + commit: (v) => { + if (control === "pan") { + sendAction({ type: "SET_PATTERN_TRACK_PAN", trackId, pan: v }); + } else { + sendAction({ type: "SET_PATTERN_TRACK_VOLUME", trackId, volume: v }); + } + }, + }); + }); + // Eventos de Seleção trackLane.addEventListener('click', () => { if (appState.pattern.activeTrackId === trackData.id) return; diff --git a/assets/js/creations/socket.js b/assets/js/creations/socket.js index bb37776d..f71dd67d 100755 --- a/assets/js/creations/socket.js +++ b/assets/js/creations/socket.js @@ -9,6 +9,9 @@ import { removeLastTrackFromState, updateTrackSample, removeTrackById, + setPatternTrackMute, + setPatternTrackVolume, + setPatternTrackPan, } from "./pattern/pattern_state.js"; import { addAudioTrackLane, @@ -1352,6 +1355,33 @@ async function handleActionBroadcast(action) { break; } + case "SET_PATTERN_TRACK_MUTE": { + const { trackId, isMuted } = action; + setPatternTrackMute(trackId, !!isMuted); + renderAll(); + saveStateToSession(); + restartAudioEditorIfPlaying?.(); + break; + } + + case "SET_PATTERN_TRACK_VOLUME": { + const { trackId, volume } = action; + setPatternTrackVolume(trackId, volume); + renderAll(); + saveStateToSession(); + restartAudioEditorIfPlaying?.(); + break; + } + + case "SET_PATTERN_TRACK_PAN": { + const { trackId, pan } = action; + setPatternTrackPan(trackId, pan); + renderAll(); + saveStateToSession(); + restartAudioEditorIfPlaying?.(); + break; + } + case "SET_ACTIVE_PATTERN": { // índice que veio do seletor global const { patternIndex } = action;