/* ===== 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: ' + dbL + '' + ' R: ' + dbR + ''; } // ---- 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); } }); })();