diff --git a/sound_algo/data_only/matrix.scd b/sound_algo/data_only/matrix.scd index 374448b..f700c38 100644 --- a/sound_algo/data_only/matrix.scd +++ b/sound_algo/data_only/matrix.scd @@ -4,8 +4,8 @@ // A playhead Routine advances bar-by-bar on the launchpad clock, applying cells. // Colors 1-6 map to Pchain overlays (\stretch / \octave / \amp). 0 = Pdef.stop. // Loaded after sections.scd (boot step 6i). Idempotent reload, nil-guarded. -// NOTE: the \amp variation override bypasses ~lpVol per-voice volume; web vol sliders -// have no effect while a matrix variation plays — deferred to v2. +// NOTE: the \amp variation is multiplied by the per-voice fader (~lp[\vol]) so the +// web mixer (/launch/vol) controls matrix output in real time. ( // -- Env init (nil-guarded; idempotent reload) -- @@ -74,7 +74,7 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); }, \stretch, spec[\stretch], \octave, spec[\octave], - \amp, spec[\amp] + \amp, Pfunc({ (spec[\amp] ? 1.0) * ((~lp[\vol].notNil).if({ ~lp[\vol][name.asSymbol] ? 0.8 }, { 0.8 })) }) ), base ) diff --git a/web_realart/public/control/control.css b/web_realart/public/control/control.css index 08c3b73..5a71393 100644 --- a/web_realart/public/control/control.css +++ b/web_realart/public/control/control.css @@ -142,3 +142,14 @@ button.queued { animation: queued-blink 600ms ease-in-out infinite alternate; } .tl-bar.in-loop { background: #1a2a1a; border-color: #2a5a2a; color: #4d9; } .tl-bar.tl-head { background: #fa0; border-color: #fc0; color: #000; } .tl-bar.tl-head.in-loop { background: #fa0; border-color: #fc0; color: #000; } + +/* --- Matrix mixer: 16-strip volume + mute --- */ +.matrix-mixer { display: flex; gap: 4px; margin: 12px 0; overflow-x: auto; + -webkit-overflow-scrolling: touch; padding-bottom: 4px; } +.mix-strip { display: flex; flex-direction: column; align-items: center; gap: 4px; + min-width: 40px; flex-shrink: 0; } +.mix-fader { writing-mode: vertical-lr; direction: rtl; width: 28px; height: 110px; + accent-color: #4c8; cursor: pointer; -webkit-appearance: slider-vertical; appearance: slider-vertical; } +.mix-mute { width: auto; padding: 4px 6px; font-size: 11px; } +.mix-mute.on { background: #622; border-color: #c44; color: #f88; font-weight: 700; } +.mix-label { font-size: 10px; color: #9af; text-align: center; word-break: break-all; } diff --git a/web_realart/public/control/control.js b/web_realart/public/control/control.js index 3800c45..e2e51f5 100644 --- a/web_realart/public/control/control.js +++ b/web_realart/public/control/control.js @@ -407,6 +407,69 @@ function initTimelinePointer() { }); } +// --- Matrix mixer: 16-voice per-strip volume + mute --- +const mixerLevel = {}; +const mixerMuted = {}; +MATRIX_VOICES.forEach(v => { mixerLevel[v] = 0.8; mixerMuted[v] = false; }); + +function renderMixer() { + const container = document.getElementById("matrix-mixer"); + if (!container) return; + container.innerHTML = ""; + MATRIX_VOICES.forEach(voice => { + const strip = document.createElement("div"); + strip.className = "mix-strip"; + strip.dataset.voice = voice; + + const fader = document.createElement("input"); + fader.type = "range"; + fader.min = "0"; + fader.max = "1.5"; + fader.step = "0.05"; + fader.value = String(mixerLevel[voice]); + fader.className = "mix-fader"; + fader.dataset.voice = voice; + + const muteBtn = document.createElement("button"); + muteBtn.className = "mix-mute"; + muteBtn.textContent = "M"; + + const label = document.createElement("span"); + label.className = "mix-label"; + label.textContent = voice; + + fader.addEventListener("input", () => { + const v = +fader.value; + mixerLevel[voice] = v; + // Moving the fader unmutes + sends immediately + if (mixerMuted[voice]) { + mixerMuted[voice] = false; + strip.classList.remove("muted"); + muteBtn.classList.remove("on"); + } + send("/launch/vol", voice, v); + }); + + muteBtn.addEventListener("click", () => { + mixerMuted[voice] = !mixerMuted[voice]; + if (mixerMuted[voice]) { + muteBtn.classList.add("on"); + strip.classList.add("muted"); + send("/launch/vol", voice, 0); + } else { + muteBtn.classList.remove("on"); + strip.classList.remove("muted"); + send("/launch/vol", voice, mixerLevel[voice]); + } + }); + + strip.appendChild(fader); + strip.appendChild(muteBtn); + strip.appendChild(label); + container.appendChild(strip); + }); +} + // --- Matrix audio-reactive glow (trigger envelope; math in matrix_glow.js) --- const voiceLevel = new Array(MATRIX_VOICES.length).fill(0); let glowRaf = null; @@ -634,6 +697,7 @@ document.addEventListener("DOMContentLoaded", () => { renderMatrix(); renderTimeline(); initTimelinePointer(); + renderMixer(); // Name input: rename selected preset and update button labels const melName = document.getElementById("mel-name"); diff --git a/web_realart/public/control/index.html b/web_realart/public/control/index.html index f10856f..c1c5fab 100644 --- a/web_realart/public/control/index.html +++ b/web_realart/public/control/index.html @@ -170,6 +170,7 @@
+