Initial: OPLabsBandChannel.lv2 + OPLabsBandBus.lv2

This commit is contained in:
2026-06-21 00:03:37 -04:00
commit f8e697a064
68 changed files with 9591 additions and 0 deletions
+679
View File
@@ -0,0 +1,679 @@
/* ===== OPLabs Band Bus - ModGUI Application ===== */
(function () {
'use strict';
// ---- Constants ----
var NUM_CHANNELS = 8;
var DB_MIN = -70; // meter min
var DB_MIN_VOL = -60; // volume min
var DB_MAX = 6; // max value (both vol and meter)
var CHANNEL_NAMES = [
'Ch 1', 'Ch 2', 'Ch 3', 'Ch 4',
'Ch 5', 'Ch 6', 'Ch 7', 'Ch 8'
];
// ---- State ----
var state = {
channels: [],
masterVol: 0,
masterMute: 0,
masterLevelL: DB_MIN,
masterLevelR: DB_MIN
};
// Initialize channel state
for (var i = 0; i < NUM_CHANNELS; i++) {
state.channels.push({
vol: 0,
mute: 0,
levelL: DB_MIN,
levelR: DB_MIN
});
}
// Peak hold state
var peakHold = {};
var peakTimers = {};
var PEAK_HOLD_MS = 1500;
// Drag state for faders
var dragState = null;
// ---- DOM References ----
var els = {};
// ---- Build Channel Strips ----
function buildChannelStrips() {
var container = document.getElementById('busChannels');
for (var i = 0; i < NUM_CHANNELS; i++) {
var ch = document.createElement('div');
ch.className = 'ch-strip';
ch.dataset.channel = i;
ch.id = 'chStrip' + i;
// Label
var label = document.createElement('div');
label.className = 'ch-strip-label';
label.textContent = CHANNEL_NAMES[i];
// Mute button
var muteBtn = document.createElement('button');
muteBtn.className = 'ch-strip-mute';
muteBtn.id = 'chMute' + i;
muteBtn.title = 'Mute ' + CHANNEL_NAMES[i];
muteBtn.textContent = 'M';
muteBtn.addEventListener('click', makeMuteHandler(i));
// Mini stereo meter
var meterDiv = document.createElement('div');
meterDiv.className = 'ch-strip-meter';
var meterL = makeMiniMeter('ch' + i + 'L');
var meterR = makeMiniMeter('ch' + i + 'R');
meterDiv.appendChild(meterL);
meterDiv.appendChild(meterR);
// Mini fader
var faderDiv = document.createElement('div');
faderDiv.className = 'ch-strip-fader';
var faderLabel = document.createElement('div');
faderLabel.className = 'fader-label';
faderLabel.textContent = 'Vol';
var faderTrack = document.createElement('div');
faderTrack.className = 'fader-track';
faderTrack.id = 'chFaderTrack' + i;
var faderFill = document.createElement('div');
faderFill.className = 'fader-fill';
faderFill.id = 'chFaderFill' + i;
var faderThumb = document.createElement('div');
faderThumb.className = 'fader-thumb';
faderThumb.id = 'chFaderThumb' + i;
faderThumb.addEventListener('pointerdown', makeFaderPointerDown(i));
faderThumb.addEventListener('touchstart', makeFaderTouchStart(i), { passive: false });
faderTrack.appendChild(faderFill);
faderTrack.appendChild(faderThumb);
var faderValue = document.createElement('div');
faderValue.className = 'fader-value';
faderValue.id = 'chFaderValue' + i;
faderValue.textContent = '0.0';
faderDiv.appendChild(faderLabel);
faderDiv.appendChild(faderTrack);
faderDiv.appendChild(faderValue);
// Assemble
ch.appendChild(label);
ch.appendChild(muteBtn);
ch.appendChild(meterDiv);
ch.appendChild(faderDiv);
container.appendChild(ch);
// Cache DOM refs
els['chMute' + i] = muteBtn;
els['chMeterFill' + i + 'L'] = meterL.querySelector('.meter-fill');
els['chMeterFill' + i + 'R'] = meterR.querySelector('.meter-fill');
els['chMeterPeak' + i + 'L'] = meterL.querySelector('.meter-peak');
els['chMeterPeak' + i + 'R'] = meterR.querySelector('.meter-peak');
els['chFaderFill' + i] = faderFill;
els['chFaderThumb' + i] = faderThumb;
els['chFaderValue' + i] = faderValue;
els['chStrip' + i] = ch;
// Init peak hold
peakHold['ch' + i + 'L'] = DB_MIN;
peakHold['ch' + i + 'R'] = DB_MIN;
peakTimers['ch' + i + 'L'] = null;
peakTimers['ch' + i + 'R'] = null;
}
}
function makeMiniMeter(idPrefix) {
var div = document.createElement('div');
div.className = 'meter-channel';
var label = document.createElement('span');
label.className = 'meter-label';
label.textContent = idPrefix.charAt(idPrefix.length - 1); // L or R
var track = document.createElement('div');
track.className = 'meter-track';
var fill = document.createElement('div');
fill.className = 'meter-fill';
var peak = document.createElement('div');
peak.className = 'meter-peak';
var tick20 = document.createElement('div');
tick20.className = 'meter-tick';
tick20.style.bottom = '80%';
var tick50 = document.createElement('div');
tick50.className = 'meter-tick';
tick50.style.bottom = '50%';
var tick80 = document.createElement('div');
tick80.className = 'meter-tick';
tick80.style.bottom = '20%';
track.appendChild(fill);
track.appendChild(peak);
track.appendChild(tick20);
track.appendChild(tick50);
track.appendChild(tick80);
div.appendChild(label);
div.appendChild(track);
return div;
}
// ---- Mute button handler factory ----
function makeMuteHandler(chIdx) {
return function () {
var symbol = 'ch' + (chIdx + 1) + 'Mute';
var newVal = state.channels[chIdx].mute >= 0.5 ? 0 : 1;
state.channels[chIdx].mute = newVal;
sendValue(symbol, newVal);
updateChannelMute(chIdx, newVal);
};
}
// ---- Fader drag handler factories ----
function makeFaderPointerDown(chIdx) {
return function (e) {
e.preventDefault();
startFaderDrag(chIdx, e.clientY);
};
}
function makeFaderTouchStart(chIdx) {
return function (e) {
e.preventDefault();
startFaderDrag(chIdx, e.touches[0].clientY);
};
}
function startFaderDrag(chIdx, clientY) {
if (dragState) return;
var symbol = 'ch' + (chIdx + 1) + 'Vol';
dragState = {
chIdx: chIdx,
symbol: symbol,
startY: clientY,
startVal: state.channels[chIdx].vol
};
document.addEventListener('pointermove', onFaderPointerMove);
document.addEventListener('pointerup', onFaderPointerUp);
document.addEventListener('touchmove', onFaderTouchMove, { passive: false });
document.addEventListener('touchend', onFaderPointerUp);
}
function onFaderPointerMove(e) {
if (!dragState) return;
e.preventDefault();
dragFader(e.clientY);
}
function onFaderTouchMove(e) {
if (!dragState) return;
e.preventDefault();
dragFader(e.touches[0].clientY);
}
function dragFader(clientY) {
var d = dragState;
var deltaY = d.startY - clientY;
var range = DB_MAX - DB_MIN_VOL; // 66
var deltaVal = (deltaY / 200) * range; // 200px for full range
var newVal = clamp(d.startVal + deltaVal, DB_MIN_VOL, DB_MAX);
// Snap to 0.5 dB steps
newVal = Math.round(newVal * 2) / 2;
newVal = clamp(newVal, DB_MIN_VOL, DB_MAX);
state.channels[d.chIdx].vol = newVal;
sendValue(d.symbol, newVal);
updateChannelFader(d.chIdx, newVal);
}
function onFaderPointerUp() {
if (!dragState) return;
dragState = null;
document.removeEventListener('pointermove', onFaderPointerMove);
document.removeEventListener('pointerup', onFaderPointerUp);
document.removeEventListener('touchmove', onFaderTouchMove);
document.removeEventListener('touchend', onFaderPointerUp);
}
// ---- Master fader drag ----
function masterFaderPointerDown(e) {
e.preventDefault();
if (dragState) return;
dragState = {
master: true,
startY: e.clientY,
startVal: state.masterVol
};
document.addEventListener('pointermove', onMasterFaderMove);
document.addEventListener('pointerup', onMasterFaderUp);
document.addEventListener('touchmove', onMasterFaderTouchMove, { passive: false });
document.addEventListener('touchend', onMasterFaderUp);
}
function masterFaderTouchStart(e) {
e.preventDefault();
if (dragState) return;
dragState = {
master: true,
startY: e.touches[0].clientY,
startVal: state.masterVol
};
document.addEventListener('pointermove', onMasterFaderMove);
document.addEventListener('pointerup', onMasterFaderUp);
document.addEventListener('touchmove', onMasterFaderTouchMove, { passive: false });
document.addEventListener('touchend', onMasterFaderUp);
}
function onMasterFaderMove(e) { if (dragState) { e.preventDefault(); dragMasterFader(e.clientY); } }
function onMasterFaderTouchMove(e) { if (dragState) { e.preventDefault(); dragMasterFader(e.touches[0].clientY); } }
function dragMasterFader(clientY) {
var d = dragState;
var deltaY = d.startY - clientY;
var range = DB_MAX - DB_MIN_VOL;
var deltaVal = (deltaY / 200) * range;
var newVal = clamp(d.startVal + deltaVal, DB_MIN_VOL, DB_MAX);
newVal = Math.round(newVal * 2) / 2;
newVal = clamp(newVal, DB_MIN_VOL, DB_MAX);
state.masterVol = newVal;
sendValue('masterVol', newVal);
updateMasterFader(newVal);
}
function onMasterFaderUp() {
if (!dragState) return;
dragState = null;
document.removeEventListener('pointermove', onMasterFaderMove);
document.removeEventListener('pointerup', onMasterFaderUp);
document.removeEventListener('touchmove', onMasterFaderTouchMove);
document.removeEventListener('touchend', onMasterFaderUp);
}
// ---- Cache static DOM elements ----
function cacheElements() {
els.masterBtnMute = document.getElementById('masterBtnMute');
els.masterMeterFillL = document.getElementById('masterMeterFillL');
els.masterMeterFillR = document.getElementById('masterMeterFillR');
els.masterMeterPeakL = document.getElementById('masterMeterPeakL');
els.masterMeterPeakR = document.getElementById('masterMeterPeakR');
els.masterFaderFill = document.getElementById('masterFaderFill');
els.masterFaderThumb = document.getElementById('masterFaderThumb');
els.masterFaderReadout = document.getElementById('masterFaderReadout');
els.masterDbReadout = document.getElementById('masterDbReadout');
els.masterSection = document.querySelector('.master-section');
els.masterFaderTrack = document.getElementById('masterFaderTrack');
// Init peak holds for master
peakHold['masterL'] = DB_MIN;
peakHold['masterR'] = DB_MIN;
peakTimers['masterL'] = null;
peakTimers['masterR'] = null;
}
// ---- Utility ----
function clamp(val, min, max) {
return Math.min(max, Math.max(min, val));
}
function dbToPercent(db) {
var clamped = clamp(db, DB_MIN, DB_MAX);
var normalized = (clamped - DB_MIN) / (DB_MAX - DB_MIN);
return Math.sqrt(normalized) * 100;
}
function getMeterColor(pct) {
if (pct < 60) return 'var(--meter-green)';
if (pct < 85) return 'var(--meter-yellow)';
return 'var(--meter-red)';
}
// ---- Send value to host ----
function sendValue(symbol, value) {
if (window.modguijs && typeof window.modguijs.setValue === 'function') {
window.modguijs.setValue(symbol, value);
}
}
// ---- Update functions ----
// Channel mute
function updateChannelMute(chIdx, val) {
var active = val >= 0.5;
var btn = els['chMute' + chIdx];
var strip = els['chStrip' + chIdx];
btn.classList.toggle('active', active);
strip.classList.toggle('muted', active);
}
// Channel fader
function updateChannelFader(chIdx, val) {
var pct = dbToPercent(val);
var fill = els['chFaderFill' + chIdx];
var thumb = els['chFaderThumb' + chIdx];
var valueEl = els['chFaderValue' + chIdx];
// Fader fill: percentage from bottom
fill.style.height = Math.min(100, pct) + '%';
// Thumb position: also from bottom
var thumbPct = Math.min(100, pct);
thumb.style.bottom = 'calc(' + thumbPct + '% - 3px)';
// Display
var display = val <= DB_MIN_VOL ? '-∞' : val.toFixed(1);
valueEl.textContent = display;
}
// Channel meter
function updateChannelMeter(chIdx) {
var ch = state.channels[chIdx];
updateMeterBar('ch' + chIdx, 'L', ch.levelL, els['chMeterFill' + chIdx + 'L'], els['chMeterPeak' + chIdx + 'L']);
updateMeterBar('ch' + chIdx, 'R', ch.levelR, els['chMeterFill' + chIdx + 'R'], els['chMeterPeak' + chIdx + 'R']);
}
function updateMeterBar(chKey, side, level, fillEl, peakEl) {
var pct = dbToPercent(level);
fillEl.style.height = Math.min(100, pct) + '%';
fillEl.style.background = getMeterColor(pct);
fillEl.className = 'meter-fill' + (pct >= 85 ? ' hot' : pct >= 60 ? ' warm' : '');
// Peak hold
var pkKey = chKey + side;
if (level > peakHold[pkKey]) {
peakHold[pkKey] = level;
if (peakTimers[pkKey]) { clearTimeout(peakTimers[pkKey]); }
peakTimers[pkKey] = setTimeout(function () {
peakHold[pkKey] = DB_MIN;
var pkPct = dbToPercent(DB_MIN);
peakEl.style.bottom = Math.min(100, pkPct) + '%';
}, PEAK_HOLD_MS);
}
peakEl.style.bottom = Math.min(100, pct) + '%';
}
// Master mute
function updateMasterMute(val) {
var active = val >= 0.5;
els.masterBtnMute.classList.toggle('active', active);
els.masterSection.classList.toggle('muted', active);
}
// Master fader
function updateMasterFader(val) {
var pct = dbToPercent(val);
els.masterFaderFill.style.height = Math.min(100, pct) + '%';
var thumbPct = Math.min(100, pct);
els.masterFaderThumb.style.bottom = 'calc(' + thumbPct + '% - 4px)';
var display = val <= DB_MIN_VOL ? '-∞' : val.toFixed(1);
els.masterFaderReadout.textContent = display;
}
// Master meter
function updateMasterMeter() {
updateMeterBar('master', 'L', state.masterLevelL, els.masterMeterFillL, els.masterMeterPeakL);
updateMeterBar('master', 'R', state.masterLevelR, els.masterMeterFillR, els.masterMeterPeakR);
// dB readout
var dbL = state.masterLevelL <= DB_MIN ? '-∞' : state.masterLevelL.toFixed(1);
var dbR = state.masterLevelR <= DB_MIN ? '-∞' : state.masterLevelR.toFixed(1);
els.masterDbReadout.innerHTML =
'L: <span class="' + (state.masterLevelL > -10 ? 'hot' : '') + '">' + dbL + '</span>' +
' R: <span class="' + (state.masterLevelR > -10 ? 'hot' : '') + '">' + dbR + '</span>';
}
// ---- Full UI update ----
function updateAll() {
// Channels
for (var i = 0; i < NUM_CHANNELS; i++) {
updateChannelMute(i, state.channels[i].mute);
updateChannelFader(i, state.channels[i].vol);
updateChannelMeter(i);
}
// Master
updateMasterMute(state.masterMute);
updateMasterFader(state.masterVol);
updateMasterMeter();
}
// ---- UI Event Handlers ----
function setupUIHandlers() {
// Master mute button
els.masterBtnMute.addEventListener('click', function () {
var newVal = state.masterMute >= 0.5 ? 0 : 1;
state.masterMute = newVal;
sendValue('masterMute', newVal);
updateMasterMute(newVal);
});
// Master fader
els.masterFaderThumb.addEventListener('pointerdown', masterFaderPointerDown);
els.masterFaderThumb.addEventListener('touchstart', masterFaderTouchStart, { passive: false });
}
// ---- ModGUI API Integration ----
function setupModGUI() {
var paramSymbols = [];
// Per-channel params
for (var i = 1; i <= NUM_CHANNELS; i++) {
paramSymbols.push('ch' + i + 'Vol');
paramSymbols.push('ch' + i + 'Mute');
}
// Master params
paramSymbols.push('masterVol');
paramSymbols.push('masterMute');
// Meter symbols (output only, for reading)
var meterSymbols = [];
for (var i = 1; i <= NUM_CHANNELS; i++) {
meterSymbols.push('ch' + i + 'LevelL');
meterSymbols.push('ch' + i + 'LevelR');
}
meterSymbols.push('masterLevelL');
meterSymbols.push('masterLevelR');
// Helper to get channel index from port symbol
function chIdxFromSymbol(symbol) {
var m = symbol.match(/^ch(\d+)/);
return m ? (parseInt(m[1], 10) - 1) : -1;
}
if (window.modguijs) {
// Read initial values
var allSymbols = paramSymbols.concat(meterSymbols);
allSymbols.forEach(function (symbol) {
if (typeof window.modguijs.getValue === 'function') {
try {
var val = window.modguijs.getValue(symbol);
if (val !== undefined && val !== null) {
applyValue(symbol, val);
}
} catch (e) { /* ignore */ }
}
});
// Subscribe to changes
if (typeof window.modguijs.onChange === 'function') {
allSymbols.forEach(function (symbol) {
window.modguijs.onChange(symbol, function (value) {
applyValue(symbol, value);
});
});
}
// Fallback polling if no onChange
if (typeof window.modguijs.onChange !== 'function') {
startPolling(paramSymbols, meterSymbols);
}
} else {
// Retry after delay
setTimeout(function () {
if (window.modguijs) {
setupModGUI();
} else {
startDemoMode();
}
}, 500);
}
}
// ---- Apply a value to state and update UI ----
function applyValue(symbol, value) {
var m = symbol.match(/^ch(\d+)(Vol|Mute|Level[LR])$/);
if (m) {
var chIdx = parseInt(m[1], 10) - 1;
var param = m[2];
if (chIdx >= 0 && chIdx < NUM_CHANNELS) {
if (param === 'Vol') {
state.channels[chIdx].vol = value;
updateChannelFader(chIdx, value);
} else if (param === 'Mute') {
state.channels[chIdx].mute = value;
updateChannelMute(chIdx, value);
} else if (param === 'LevelL') {
state.channels[chIdx].levelL = value;
updateChannelMeter(chIdx);
} else if (param === 'LevelR') {
state.channels[chIdx].levelR = value;
updateChannelMeter(chIdx);
}
return;
}
}
// Master params
if (symbol === 'masterVol') {
state.masterVol = value;
updateMasterFader(value);
} else if (symbol === 'masterMute') {
state.masterMute = value;
updateMasterMute(value);
} else if (symbol === 'masterLevelL') {
state.masterLevelL = value;
updateMasterMeter();
} else if (symbol === 'masterLevelR') {
state.masterLevelR = value;
updateMasterMeter();
}
}
// ---- Polling fallback ----
var pollInterval = null;
var pollSymbols = [];
function startPolling(paramSymbols, meterSymbols) {
pollSymbols = paramSymbols.concat(meterSymbols);
if (pollInterval) return;
pollInterval = setInterval(function () {
if (!window.modguijs || typeof window.modguijs.getValue !== 'function') return;
pollSymbols.forEach(function (symbol) {
try {
var val = window.modguijs.getValue(symbol);
if (val !== undefined && val !== null) {
// Only update if changed
var current = getCurrentValue(symbol);
if (current === undefined || Math.abs(val - current) > 0.001) {
applyValue(symbol, val);
}
}
} catch (e) { /* ignore */ }
});
}, 50);
}
function getCurrentValue(symbol) {
var m = symbol.match(/^ch(\d+)(Vol|Mute|Level[LR])$/);
if (m) {
var chIdx = parseInt(m[1], 10) - 1;
if (chIdx >= 0 && chIdx < NUM_CHANNELS) {
var param = m[2];
if (param === 'Vol') return state.channels[chIdx].vol;
if (param === 'Mute') return state.channels[chIdx].mute;
if (param === 'LevelL') return state.channels[chIdx].levelL;
if (param === 'LevelR') return state.channels[chIdx].levelR;
}
return undefined;
}
if (symbol === 'masterVol') return state.masterVol;
if (symbol === 'masterMute') return state.masterMute;
if (symbol === 'masterLevelL') return state.masterLevelL;
if (symbol === 'masterLevelR') return state.masterLevelR;
return undefined;
}
// ---- Demo mode ----
var demoInterval = null;
function startDemoMode() {
console.log('OPLabs Band Bus: No modguijs API found, running demo mode');
var t = 0;
demoInterval = setInterval(function () {
t += 0.05;
// Simulate different levels per channel
for (var i = 0; i < NUM_CHANNELS; i++) {
var offset = i * 0.3 + Math.sin(t * 0.1 + i) * 0.2;
var lvl = -50 + 45 * (Math.sin(t + offset) * 0.5 + 0.5);
var lvr = -50 + 45 * (Math.cos(t * 0.7 + offset) * 0.5 + 0.5);
state.channels[i].levelL = lvl;
state.channels[i].levelR = lvr;
updateChannelMeter(i);
}
state.masterLevelL = -30 + 25 * (Math.sin(t * 0.5) * 0.5 + 0.5);
state.masterLevelR = -30 + 25 * (Math.cos(t * 0.5 + 0.3) * 0.5 + 0.5);
updateMasterMeter();
}, 100);
}
// ---- Initialization ----
function init() {
buildChannelStrips();
cacheElements();
setupUIHandlers();
// Master fader initial position
updateMasterFader(state.masterVol);
// Draw initial state
updateAll();
// Connect to modguijs
setupModGUI();
}
// Wait for DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Clean up on unload
window.addEventListener('beforeunload', function () {
if (pollInterval) clearInterval(pollInterval);
if (demoInterval) clearInterval(demoInterval);
// Clear all peak timers
for (var key in peakTimers) {
if (peakTimers[key]) clearTimeout(peakTimers[key]);
}
if (dragState) {
document.removeEventListener('pointermove', onFaderPointerMove);
document.removeEventListener('pointerup', onFaderPointerUp);
document.removeEventListener('touchmove', onFaderTouchMove);
document.removeEventListener('touchend', onFaderPointerUp);
}
});
})();
+71
View File
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OPLabs Band Bus</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="bus-bridge" id="busBridge">
<!-- Top section: 8 mini channel strips -->
<div class="bus-channels" id="busChannels">
<!-- Each strip rendered by JS -->
</div>
<!-- Divider -->
<div class="bus-divider"></div>
<!-- Master section -->
<div class="master-section">
<div class="master-label">Master</div>
<div class="master-controls">
<!-- Mute button -->
<button class="master-btn mute" id="masterBtnMute" title="Master Mute">M</button>
<!-- Master stereo level meters -->
<div class="master-meters">
<div class="master-meter-channel">
<span class="meter-label">L</span>
<div class="meter-track" id="masterMeterTrackL">
<div class="meter-fill" id="masterMeterFillL"></div>
<div class="meter-peak" id="masterMeterPeakL"></div>
<div class="meter-tick" style="bottom:80%"></div>
<div class="meter-tick" style="bottom:50%"></div>
<div class="meter-tick" style="bottom:20%"></div>
</div>
</div>
<div class="master-meter-channel">
<span class="meter-label">R</span>
<div class="meter-track" id="masterMeterTrackR">
<div class="meter-fill" id="masterMeterFillR"></div>
<div class="meter-peak" id="masterMeterPeakR"></div>
<div class="meter-tick" style="bottom:80%"></div>
<div class="meter-tick" style="bottom:50%"></div>
<div class="meter-tick" style="bottom:20%"></div>
</div>
</div>
</div>
<!-- Master volume fader -->
<div class="master-fader-section">
<div class="fader-label">Vol</div>
<div class="fader-track" id="masterFaderTrack">
<div class="fader-thumb" id="masterFaderThumb"></div>
<div class="fader-fill" id="masterFaderFill"></div>
</div>
<div class="fader-value" id="masterFaderReadout">0.0</div>
</div>
</div>
</div>
<!-- dB readout row -->
<div class="bus-readout-bar">
<span class="bus-readout-title">OUT:</span>
<span class="bus-readout" id="masterDbReadout">L: -∞ R: -∞</span>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
+532
View File
@@ -0,0 +1,532 @@
/* ===== OPLabs Band Bus - ModGUI Styles ===== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-surface: #0f3460;
--text-primary: #e0e0e0;
--text-secondary: #a0a0b0;
--accent-blue: #4fc3f7;
--accent-orange: #ff8c00;
--meter-green: #4caf50;
--meter-yellow: #ff9800;
--meter-red: #f44336;
--mute-red: #e53935;
--solo-yellow: #ffc107;
--border-color: #2a2a4a;
--channel-width: 80px;
--meter-width: 28px;
--master-meter-width: 36px;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 11px;
user-select: none;
-webkit-user-select: none;
}
/* ===== Bus Bridge Container ===== */
.bus-bridge {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
padding: 4px;
gap: 3px;
background: var(--bg-primary);
}
/* ===== 8-Channel Strip Row ===== */
.bus-channels {
display: flex;
gap: 3px;
flex: 1;
min-height: 0;
}
/* ===== Individual Channel Strip (Mini) ===== */
.ch-strip {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
background: var(--bg-secondary);
border-radius: 4px;
border: 1px solid var(--border-color);
border-top: 2px solid var(--accent-orange);
padding: 3px 2px;
gap: 2px;
align-items: center;
position: relative;
}
.ch-strip.muted {
border-top-color: var(--mute-red);
opacity: 0.65;
}
.ch-strip-label {
font-size: 9px;
font-weight: 700;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.3px;
text-align: center;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
/* Mini Mute Button */
.ch-strip-mute {
width: 24px;
height: 18px;
border: 1px solid var(--border-color);
border-radius: 3px;
font-size: 9px;
font-weight: 800;
cursor: pointer;
transition: all 0.15s ease;
background: var(--bg-primary);
color: var(--text-secondary);
line-height: 1;
padding: 0;
flex-shrink: 0;
}
.ch-strip-mute:hover {
filter: brightness(1.3);
}
.ch-strip-mute.active {
background: var(--mute-red);
color: #fff;
border-color: var(--mute-red);
box-shadow: 0 0 6px rgba(229, 57, 53, 0.35);
}
/* ===== Mini Level Meter ===== */
.ch-strip-meter {
display: flex;
gap: 1px;
height: 100%;
width: 100%;
min-height: 20px;
flex: 1;
}
.ch-strip-meter .meter-channel {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
min-width: 0;
}
.ch-strip-meter .meter-label {
font-size: 7px;
font-weight: 700;
color: var(--text-secondary);
line-height: 1;
}
.ch-strip-meter .meter-track {
flex: 1;
width: 100%;
min-width: 8px;
background: var(--bg-primary);
border-radius: 2px;
position: relative;
overflow: hidden;
border: 1px solid var(--border-color);
}
.ch-strip-meter .meter-fill {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 0%;
background: var(--meter-green);
transition: height 0.05s linear, background 0.1s ease;
border-radius: 0 0 1px 1px;
}
.ch-strip-meter .meter-fill.warm {
background: var(--meter-yellow);
}
.ch-strip-meter .meter-fill.hot {
background: var(--meter-red);
}
.ch-strip-meter .meter-peak {
position: absolute;
left: 1px;
right: 1px;
height: 2px;
background: rgba(255, 255, 255, 0.85);
border-radius: 1px;
transition: bottom 0.1s ease;
z-index: 2;
}
/* ===== Mini Vertical Fader ===== */
.ch-strip-fader {
width: 100%;
height: 50px;
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
flex-shrink: 0;
}
.ch-strip-fader .fader-label {
font-size: 7px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
line-height: 1;
}
.ch-strip-fader .fader-track {
width: 12px;
flex: 1;
background: var(--bg-primary);
border-radius: 3px;
border: 1px solid var(--border-color);
position: relative;
cursor: pointer;
touch-action: none;
min-height: 20px;
}
.ch-strip-fader .fader-fill {
position: absolute;
bottom: 0;
left: 1px;
right: 1px;
height: 50%;
background: var(--accent-blue);
border-radius: 0 0 2px 2px;
opacity: 0.5;
pointer-events: none;
}
.ch-strip-fader .fader-thumb {
position: absolute;
left: -3px;
right: -3px;
height: 6px;
background: var(--accent-blue);
border-radius: 2px;
border: 1px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
touch-action: none;
z-index: 2;
transition: background 0.1s;
}
.ch-strip-fader .fader-thumb:hover,
.ch-strip-fader .fader-thumb:active {
background: #81d4fa;
}
.ch-strip-fader .fader-value {
font-size: 7px;
font-weight: 600;
font-family: 'Courier New', monospace;
color: var(--text-secondary);
line-height: 1;
}
/* ===== Divider ===== */
.bus-divider {
height: 1px;
background: var(--border-color);
margin: 1px 0;
flex-shrink: 0;
}
/* ===== Master Section ===== */
.master-section {
display: flex;
flex-direction: column;
gap: 3px;
padding: 3px 4px;
background: var(--bg-secondary);
border-radius: 4px;
border: 1px solid var(--border-color);
border-top: 2px solid var(--accent-orange);
flex-shrink: 0;
}
.master-section.muted {
border-top-color: var(--mute-red);
opacity: 0.7;
}
.master-label {
font-size: 10px;
font-weight: 800;
color: var(--text-primary);
text-transform: uppercase;
letter-spacing: 1px;
text-align: center;
}
.master-controls {
display: flex;
gap: 6px;
align-items: stretch;
}
/* Master Mute Button */
.master-btn {
width: 32px;
min-height: 40px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 13px;
font-weight: 800;
cursor: pointer;
transition: all 0.15s ease;
background: var(--bg-primary);
color: var(--text-secondary);
flex-shrink: 0;
align-self: center;
}
.master-btn:hover {
filter: brightness(1.3);
}
.master-btn.mute.active {
background: var(--mute-red);
color: #fff;
border-color: var(--mute-red);
box-shadow: 0 0 8px rgba(229, 57, 53, 0.4);
}
/* Master Stereo Meters */
.master-meters {
display: flex;
gap: 3px;
flex: 1;
height: 60px;
min-width: 0;
}
.master-meter-channel {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
min-width: 0;
}
.master-meter-channel .meter-label {
font-size: 9px;
font-weight: 700;
color: var(--text-secondary);
}
.master-meter-channel .meter-track {
flex: 1;
width: 100%;
max-width: var(--master-meter-width);
background: var(--bg-primary);
border-radius: 3px;
position: relative;
overflow: hidden;
border: 1px solid var(--border-color);
}
.master-meter-channel .meter-fill {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 0%;
background: var(--meter-green);
transition: height 0.05s linear, background 0.1s ease;
border-radius: 0 0 2px 2px;
}
.master-meter-channel .meter-fill.warm {
background: var(--meter-yellow);
}
.master-meter-channel .meter-fill.hot {
background: var(--meter-red);
}
.master-meter-channel .meter-peak {
position: absolute;
left: 1px;
right: 1px;
height: 2px;
background: rgba(255, 255, 255, 0.9);
border-radius: 1px;
transition: bottom 0.1s ease;
z-index: 2;
}
.master-meter-channel .meter-tick {
position: absolute;
left: 0;
right: 0;
height: 1px;
background: rgba(255, 255, 255, 0.08);
z-index: 1;
}
/* Master Fader */
.master-fader-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
width: 40px;
flex-shrink: 0;
}
.master-fader-section .fader-label {
font-size: 8px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.master-fader-section .fader-track {
width: 16px;
flex: 1;
min-height: 40px;
background: var(--bg-primary);
border-radius: 4px;
border: 1px solid var(--border-color);
position: relative;
cursor: pointer;
touch-action: none;
}
.master-fader-section .fader-fill {
position: absolute;
bottom: 0;
left: 1px;
right: 1px;
height: 50%;
background: var(--accent-orange);
border-radius: 0 0 3px 3px;
opacity: 0.4;
pointer-events: none;
}
.master-fader-section .fader-thumb {
position: absolute;
left: -4px;
right: -4px;
height: 8px;
background: var(--accent-orange);
border-radius: 2px;
border: 1px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
touch-action: none;
z-index: 2;
transition: background 0.1s;
}
.master-fader-section .fader-thumb:hover,
.master-fader-section .fader-thumb:active {
background: #ffab40;
}
.master-fader-section .fader-value {
font-size: 9px;
font-weight: 700;
font-family: 'Courier New', monospace;
color: var(--text-primary);
}
/* ===== Bus Readout Bar ===== */
.bus-readout-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 2px 4px;
flex-shrink: 0;
}
.bus-readout-title {
font-size: 10px;
font-weight: 700;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.bus-readout {
font-size: 10px;
font-weight: 600;
font-family: 'Courier New', monospace;
color: var(--accent-blue);
}
.bus-readout span.hot {
color: var(--meter-red);
font-weight: 700;
}
/* ===== Responsive ===== */
@media (max-height: 280px) {
.ch-strip-fader { height: 30px; }
.master-meters { height: 36px; }
.master-fader-section .fader-track { min-height: 24px; }
.master-label { font-size: 8px; }
.ch-strip-label { font-size: 7px; }
}
@media (max-height: 350px) {
.ch-strip-fader { height: 36px; }
.master-meters { height: 40px; }
}
@media (min-height: 450px) {
.ch-strip-fader { height: 60px; }
.master-meters { height: 70px; }
:root { --channel-width: 90px; }
:root { --master-meter-width: 44px; }
}
@media (max-width: 600px) {
:root { --channel-width: 60px; }
.ch-strip-label { font-size: 7px; }
.ch-strip-mute { width: 20px; height: 16px; font-size: 8px; }
.ch-strip-fader .fader-track { width: 10px; }
}
@media (max-width: 480px) {
:root { --channel-width: 50px; }
.ch-strip { padding: 2px 1px; }
.bus-channels { gap: 2px; }
}