ativar/desativar tracks de áudio, knobs funcionais
Deploy / Deploy (push) Successful in 1m57s
Details
Deploy / Deploy (push) Successful in 1m57s
Details
This commit is contained in:
parent
338d13d801
commit
a3b432ae29
|
|
@ -278,6 +278,9 @@ body.sidebar-hidden .sample-browser { width: 0; min-width: 0; border-right: none
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-mute { background: #2ecc71; }
|
||||||
|
.track-mute.muted { background: #e74c3c; }
|
||||||
|
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
/* BEAT EDITOR / STEP SEQUENCER
|
/* BEAT EDITOR / STEP SEQUENCER
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
|
|
|
||||||
|
|
@ -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}
|
// 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 srcId = basslineTrack.instrumentSourceId || basslineTrack.id;
|
||||||
const children = (appState.pattern.tracks || []).filter(
|
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;
|
let maxSteps = 0;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,82 @@ 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"/>
|
<env amt="0" attack="0.01" hold="0.1" decay="0.1" release="0.1" sustain="0.5" sync_mode="0"/>
|
||||||
</kicker>`;
|
</kicker>`;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// 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() {
|
export function initializePatternState() {
|
||||||
appState.pattern.tracks.forEach(track => {
|
appState.pattern.tracks.forEach(track => {
|
||||||
try { track.player?.dispose(); } catch {}
|
try { track.player?.dispose(); } catch {}
|
||||||
|
|
@ -30,33 +106,14 @@ export function initializePatternState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadAudioForTrack(track) {
|
export async function loadAudioForTrack(track) {
|
||||||
// 1) Garante Volume/Pan
|
// 1) Garante Volume/Pan/Mute e reconecta a cadeia base
|
||||||
try {
|
try {
|
||||||
if (!track.volumeNode) {
|
applyTrackMix(track);
|
||||||
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) {
|
// desconecta o que existir (para reconectar sem duplicar rotas)
|
||||||
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.instrument?.disconnect(); } catch {}
|
||||||
try { track.player?.disconnect(); } catch {}
|
try { track.player?.disconnect(); } catch {}
|
||||||
try { track.previewPlayer?.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) {
|
} catch (e) {
|
||||||
console.error("Erro ao criar nós de áudio base:", e);
|
console.error("Erro ao criar nós de áudio base:", e);
|
||||||
}
|
}
|
||||||
|
|
@ -257,6 +314,8 @@ export function addTrackToState() {
|
||||||
patterns,
|
patterns,
|
||||||
activePatternIndex: appState.pattern.activePatternIndex ?? 0,
|
activePatternIndex: appState.pattern.activePatternIndex ?? 0,
|
||||||
|
|
||||||
|
isMuted: false,
|
||||||
|
muted: false,
|
||||||
volume: DEFAULT_VOLUME,
|
volume: DEFAULT_VOLUME,
|
||||||
pan: DEFAULT_PAN,
|
pan: DEFAULT_PAN,
|
||||||
volumeNode: new Tone.Volume(Tone.gainToDb(DEFAULT_VOLUME)),
|
volumeNode: new Tone.Volume(Tone.gainToDb(DEFAULT_VOLUME)),
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,84 @@
|
||||||
// js/pattern/pattern_ui.js
|
// js/pattern/pattern_ui.js
|
||||||
import { appState } from "../state.js";
|
import { appState } from "../state.js";
|
||||||
import { updateTrackSample } from "./pattern_state.js";
|
import {
|
||||||
|
updateTrackSample,
|
||||||
|
setPatternTrackMute,
|
||||||
|
setPatternTrackVolume,
|
||||||
|
setPatternTrackPan,
|
||||||
|
} from "./pattern_state.js";
|
||||||
import { playSample, stopPlayback } from "./pattern_audio.js";
|
import { playSample, stopPlayback } from "./pattern_audio.js";
|
||||||
import { getTotalSteps } from "../utils.js";
|
import { getTotalSteps } from "../utils.js";
|
||||||
import { sendAction } from '../socket.js';
|
import { sendAction } from "../socket.js";
|
||||||
import { initializeAudioContext } from '../audio.js';
|
import { initializeAudioContext } from "../audio.js";
|
||||||
import * as Tone from "https://esm.sh/tone";
|
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
|
// Função principal de renderização para o editor de patterns
|
||||||
export function renderPatternEditor() {
|
export function renderPatternEditor() {
|
||||||
const trackContainer = document.getElementById("track-container");
|
const trackContainer = document.getElementById("track-container");
|
||||||
|
|
@ -140,6 +212,71 @@ export function renderPatternEditor() {
|
||||||
<div class="step-sequencer-wrapper"></div>
|
<div class="step-sequencer-wrapper"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// 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
|
// Eventos de Seleção
|
||||||
trackLane.addEventListener('click', () => {
|
trackLane.addEventListener('click', () => {
|
||||||
if (appState.pattern.activeTrackId === trackData.id) return;
|
if (appState.pattern.activeTrackId === trackData.id) return;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ import {
|
||||||
removeLastTrackFromState,
|
removeLastTrackFromState,
|
||||||
updateTrackSample,
|
updateTrackSample,
|
||||||
removeTrackById,
|
removeTrackById,
|
||||||
|
setPatternTrackMute,
|
||||||
|
setPatternTrackVolume,
|
||||||
|
setPatternTrackPan,
|
||||||
} from "./pattern/pattern_state.js";
|
} from "./pattern/pattern_state.js";
|
||||||
import {
|
import {
|
||||||
addAudioTrackLane,
|
addAudioTrackLane,
|
||||||
|
|
@ -1352,6 +1355,33 @@ async function handleActionBroadcast(action) {
|
||||||
break;
|
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": {
|
case "SET_ACTIVE_PATTERN": {
|
||||||
// índice que veio do seletor global
|
// índice que veio do seletor global
|
||||||
const { patternIndex } = action;
|
const { patternIndex } = action;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue