Initial: OPLabsBandChannel.lv2 + OPLabsBandBus.lv2
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
/* ===== OPLabs Band Channel - ModGUI Application ===== */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ---- Constants ----
|
||||
var INSTRUMENTS = ['Guitar', 'Bass', 'Keys', 'Vocals', 'Backing'];
|
||||
var INSTRUMENT_COLORS = ['#ff8c00', '#2196f3', '#9c27b0', '#e91e63', '#009688'];
|
||||
|
||||
var DB_MIN = -60;
|
||||
var DB_MAX_VOL = 6; // volume max
|
||||
var DB_MAX_METER = 0; // meter max
|
||||
|
||||
// ---- State ----
|
||||
var state = {
|
||||
volume: 0,
|
||||
pan: 0,
|
||||
mute: 0,
|
||||
solo: 0,
|
||||
instrument: 0,
|
||||
levelL: -60,
|
||||
levelR: -60
|
||||
};
|
||||
|
||||
// Peak hold state for meters
|
||||
var peakL = -60;
|
||||
var peakR = -60;
|
||||
var peakTimerL = null;
|
||||
var peakTimerR = null;
|
||||
var PEAK_HOLD_MS = 1500;
|
||||
|
||||
// Drag state for knobs
|
||||
var dragState = null; // { knob, startY, startVal, min, max, onChange }
|
||||
|
||||
// ---- DOM References ----
|
||||
var els = {};
|
||||
|
||||
function cacheElements() {
|
||||
els.btnMute = document.getElementById('btnMute');
|
||||
els.btnSolo = document.getElementById('btnSolo');
|
||||
els.instrumentBadge = document.getElementById('instrumentBadge');
|
||||
els.instrumentSelect = document.getElementById('instrumentSelect');
|
||||
els.meterFillL = document.getElementById('meterFillL');
|
||||
els.meterFillR = document.getElementById('meterFillR');
|
||||
els.meterPeakL = document.getElementById('meterPeakL');
|
||||
els.meterPeakR = document.getElementById('meterPeakR');
|
||||
els.meterDbReadout = document.getElementById('meterDbReadout');
|
||||
els.volumeReadout = document.getElementById('volumeReadout');
|
||||
els.panReadout = document.getElementById('panReadout');
|
||||
els.knobVolumeGrip = document.getElementById('knobVolumeGrip');
|
||||
els.knobPanGrip = document.getElementById('knobPanGrip');
|
||||
els.knobVolume = document.getElementById('knobVolume');
|
||||
els.knobPan = document.getElementById('knobPan');
|
||||
els.channelStrip = document.getElementById('channelStrip');
|
||||
}
|
||||
|
||||
// ---- Utility ----
|
||||
function clamp(val, min, max) {
|
||||
return Math.min(max, Math.max(min, val));
|
||||
}
|
||||
|
||||
function dbToPercent(db, dbMax) {
|
||||
dbMax = dbMax || DB_MAX_METER;
|
||||
var clamped = clamp(db, DB_MIN, dbMax);
|
||||
var normalized = (clamped - DB_MIN) / (dbMax - DB_MIN); // 0..1
|
||||
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)';
|
||||
}
|
||||
|
||||
// ---- Knob angle mapping ----
|
||||
// Maps a value in [min, max] to rotation angle [-135, 135] degrees
|
||||
function valueToAngle(val, min, max) {
|
||||
var t = (val - min) / (max - min);
|
||||
return -135 + t * 270;
|
||||
}
|
||||
|
||||
function setKnobAngle(gripEl, angleDeg) {
|
||||
gripEl.style.transform = 'rotate(' + angleDeg + 'deg)';
|
||||
}
|
||||
|
||||
// ---- Rotary knob drag handler ----
|
||||
function initKnob(knobEl, gripEl, symbol, min, max, step, displayFn) {
|
||||
function onPointerDown(e) {
|
||||
e.preventDefault();
|
||||
if (dragState) return;
|
||||
|
||||
var startVal = state[symbol];
|
||||
if (typeof startVal !== 'number') startVal = min;
|
||||
|
||||
dragState = {
|
||||
symbol: symbol,
|
||||
startY: e.clientY || e.touches[0].clientY,
|
||||
startVal: startVal,
|
||||
min: min,
|
||||
max: max,
|
||||
step: step,
|
||||
gripEl: gripEl,
|
||||
knobEl: knobEl,
|
||||
displayFn: displayFn
|
||||
};
|
||||
|
||||
document.addEventListener('pointermove', onPointerMove);
|
||||
document.addEventListener('pointerup', onPointerUp);
|
||||
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
document.addEventListener('touchend', onPointerUp);
|
||||
}
|
||||
|
||||
function onPointerMove(e) {
|
||||
if (!dragState) return;
|
||||
e.preventDefault();
|
||||
var clientY = e.clientY;
|
||||
dragKnob(clientY);
|
||||
}
|
||||
|
||||
function onTouchMove(e) {
|
||||
if (!dragState) return;
|
||||
e.preventDefault();
|
||||
var touch = e.touches[0];
|
||||
dragKnob(touch.clientY);
|
||||
}
|
||||
|
||||
function dragKnob(clientY) {
|
||||
var d = dragState;
|
||||
var deltaY = d.startY - clientY; // positive = drag up = increase
|
||||
var range = d.max - d.min;
|
||||
// Sensitivity: 300px for full range
|
||||
var deltaVal = (deltaY / 300) * range;
|
||||
var newVal = clamp(d.startVal + deltaVal, d.min, d.max);
|
||||
|
||||
// Apply step
|
||||
if (d.step > 0) {
|
||||
newVal = Math.round(newVal / d.step) * d.step;
|
||||
newVal = clamp(newVal, d.min, d.max);
|
||||
}
|
||||
|
||||
// Update state and host
|
||||
state[d.symbol] = newVal;
|
||||
sendValue(d.symbol, newVal);
|
||||
updateKnobVisual(d);
|
||||
if (d.displayFn) d.displayFn(newVal);
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
if (!dragState) return;
|
||||
dragState = null;
|
||||
document.removeEventListener('pointermove', onPointerMove);
|
||||
document.removeEventListener('pointerup', onPointerUp);
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
document.removeEventListener('touchend', onPointerUp);
|
||||
}
|
||||
|
||||
// Mouse
|
||||
knobEl.addEventListener('pointerdown', onPointerDown);
|
||||
// Touch
|
||||
knobEl.addEventListener('touchstart', function (e) {
|
||||
// For touch, prevent default to avoid scrolling
|
||||
e.preventDefault();
|
||||
onPointerDown(e);
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
function updateKnobVisual(d) {
|
||||
var val = state[d.symbol];
|
||||
var angle = valueToAngle(val, d.min, d.max);
|
||||
setKnobAngle(d.gripEl, angle);
|
||||
}
|
||||
|
||||
// ---- Send value to host ----
|
||||
function sendValue(symbol, value) {
|
||||
if (window.modguijs && typeof window.modguijs.setValue === 'function') {
|
||||
window.modguijs.setValue(symbol, value);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Update UI from state ----
|
||||
function updateVolumeDisplay(val) {
|
||||
var display = val <= DB_MIN ? '-∞' : (val >= 0 ? '+' : '') + val.toFixed(1) + ' dB';
|
||||
els.volumeReadout.textContent = display;
|
||||
var angle = valueToAngle(val, DB_MIN, DB_MAX_VOL);
|
||||
setKnobAngle(els.knobVolumeGrip, angle);
|
||||
}
|
||||
|
||||
function updatePanDisplay(val) {
|
||||
var display;
|
||||
if (val === 0) display = 'C';
|
||||
else if (val < 0) display = 'L' + Math.abs(Math.round(val * 100));
|
||||
else display = 'R' + Math.round(val * 100);
|
||||
els.panReadout.textContent = display;
|
||||
var angle = valueToAngle(val, -1, 1);
|
||||
setKnobAngle(els.knobPanGrip, angle);
|
||||
}
|
||||
|
||||
function updateMuteDisplay(val) {
|
||||
var active = val >= 0.5;
|
||||
els.btnMute.classList.toggle('active', active);
|
||||
}
|
||||
|
||||
function updateSoloDisplay(val) {
|
||||
var active = val >= 0.5;
|
||||
els.btnSolo.classList.toggle('active', active);
|
||||
}
|
||||
|
||||
function updateInstrumentDisplay(val) {
|
||||
var idx = Math.round(val) || 0;
|
||||
idx = clamp(idx, 0, 4);
|
||||
els.instrumentSelect.value = String(idx);
|
||||
els.instrumentBadge.textContent = INSTRUMENTS[idx];
|
||||
els.instrumentBadge.style.color = INSTRUMENT_COLORS[idx];
|
||||
els.instrumentBadge.style.borderColor = INSTRUMENT_COLORS[idx];
|
||||
els.instrumentBadge.style.background = INSTRUMENT_COLORS[idx] + '22';
|
||||
// Update border accent on channel strip
|
||||
els.channelStrip.style.borderTopColor = INSTRUMENT_COLORS[idx];
|
||||
}
|
||||
|
||||
function updateMeterDisplay(levelL, levelR) {
|
||||
// Update fill heights
|
||||
var pctL = dbToPercent(levelL, DB_MAX_METER);
|
||||
var pctR = dbToPercent(levelR, DB_MAX_METER);
|
||||
els.meterFillL.style.height = Math.min(100, pctL) + '%';
|
||||
els.meterFillR.style.height = Math.min(100, pctR) + '%';
|
||||
|
||||
// Update fill colors
|
||||
els.meterFillL.style.background = getMeterColor(pctL);
|
||||
els.meterFillR.style.background = getMeterColor(pctR);
|
||||
els.meterFillL.className = 'meter-fill' + (pctL >= 85 ? ' hot' : pctL >= 60 ? ' warm' : '');
|
||||
els.meterFillR.className = 'meter-fill' + (pctR >= 85 ? ' hot' : pctR >= 60 ? ' warm' : '');
|
||||
|
||||
// Peak hold logic
|
||||
updatePeak('L', levelL, pctL);
|
||||
updatePeak('R', levelR, pctR);
|
||||
|
||||
// dB readout
|
||||
var dbL = levelL <= DB_MIN ? '-∞' : levelL.toFixed(1);
|
||||
var dbR = levelR <= DB_MIN ? '-∞' : levelR.toFixed(1);
|
||||
els.meterDbReadout.innerHTML =
|
||||
'<span class="' + (levelL > -10 ? 'hot' : '') + '">' + dbL + '</span>' +
|
||||
'<span class="' + (levelR > -10 ? 'hot' : '') + '">' + dbR + '</span>';
|
||||
}
|
||||
|
||||
function updatePeak(ch, level, pct) {
|
||||
var peakEl = ch === 'L' ? els.meterPeakL : els.meterPeakR;
|
||||
|
||||
if (ch === 'L') {
|
||||
if (level > peakL) {
|
||||
peakL = level;
|
||||
if (peakTimerL) { clearTimeout(peakTimerL); peakTimerL = null; }
|
||||
peakTimerL = setTimeout(function () {
|
||||
peakL = Math.max(state.levelL, DB_MIN);
|
||||
peakEl.style.bottom = Math.min(100, dbToPercent(peakL, DB_MAX_METER)) + '%';
|
||||
}, PEAK_HOLD_MS);
|
||||
}
|
||||
} else {
|
||||
if (level > peakR) {
|
||||
peakR = level;
|
||||
if (peakTimerR) { clearTimeout(peakTimerR); peakTimerR = null; }
|
||||
peakTimerR = setTimeout(function () {
|
||||
peakR = Math.max(state.levelR, DB_MIN);
|
||||
peakEl.style.bottom = Math.min(100, dbToPercent(peakR, DB_MAX_METER)) + '%';
|
||||
}, PEAK_HOLD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
peakEl.style.bottom = Math.min(100, pct) + '%';
|
||||
}
|
||||
|
||||
// ---- Full UI update ----
|
||||
function updateAll() {
|
||||
updateVolumeDisplay(state.volume);
|
||||
updatePanDisplay(state.pan);
|
||||
updateMuteDisplay(state.mute);
|
||||
updateSoloDisplay(state.solo);
|
||||
updateInstrumentDisplay(state.instrument);
|
||||
updateMeterDisplay(state.levelL, state.levelR);
|
||||
}
|
||||
|
||||
// ---- UI Event Handlers ----
|
||||
function setupUIHandlers() {
|
||||
// Mute button
|
||||
els.btnMute.addEventListener('click', function () {
|
||||
var newVal = state.mute >= 0.5 ? 0 : 1;
|
||||
state.mute = newVal;
|
||||
sendValue('mute', newVal);
|
||||
updateMuteDisplay(newVal);
|
||||
});
|
||||
|
||||
// Solo button
|
||||
els.btnSolo.addEventListener('click', function () {
|
||||
var newVal = state.solo >= 0.5 ? 0 : 1;
|
||||
state.solo = newVal;
|
||||
sendValue('solo', newVal);
|
||||
updateSoloDisplay(newVal);
|
||||
});
|
||||
|
||||
// Instrument selector
|
||||
els.instrumentSelect.addEventListener('change', function () {
|
||||
var newVal = parseInt(this.value, 10);
|
||||
state.instrument = newVal;
|
||||
sendValue('instrument', newVal);
|
||||
updateInstrumentDisplay(newVal);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Knobs setup ----
|
||||
function setupKnobs() {
|
||||
// Volume knob: -60 to +6, step 0.5
|
||||
initKnob(
|
||||
els.knobVolume,
|
||||
els.knobVolumeGrip,
|
||||
'volume',
|
||||
DB_MIN,
|
||||
DB_MAX_VOL,
|
||||
0.5,
|
||||
function (val) { updateVolumeDisplay(val); }
|
||||
);
|
||||
|
||||
// Pan knob: -1 to 1, step 0.01
|
||||
initKnob(
|
||||
els.knobPan,
|
||||
els.knobPanGrip,
|
||||
'pan',
|
||||
-1,
|
||||
1,
|
||||
0.01,
|
||||
function (val) { updatePanDisplay(val); }
|
||||
);
|
||||
|
||||
// Initial knob angles
|
||||
updateVolumeDisplay(state.volume);
|
||||
updatePanDisplay(state.pan);
|
||||
}
|
||||
|
||||
// ---- ModGUI API Integration ----
|
||||
function setupModGUI() {
|
||||
// Try to read initial values
|
||||
if (window.modguijs) {
|
||||
// Get initial values from host
|
||||
var params = ['volume', 'pan', 'mute', 'solo', 'instrument', 'levelL', 'levelR'];
|
||||
params.forEach(function (symbol) {
|
||||
if (typeof window.modguijs.getValue === 'function') {
|
||||
try {
|
||||
var val = window.modguijs.getValue(symbol);
|
||||
if (val !== undefined && val !== null) {
|
||||
state[symbol] = val;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to changes
|
||||
if (typeof window.modguijs.onChange === 'function') {
|
||||
params.forEach(function (symbol) {
|
||||
window.modguijs.onChange(symbol, function (value) {
|
||||
state[symbol] = value;
|
||||
switch (symbol) {
|
||||
case 'volume': updateVolumeDisplay(value); break;
|
||||
case 'pan': updatePanDisplay(value); break;
|
||||
case 'mute': updateMuteDisplay(value); break;
|
||||
case 'solo': updateSoloDisplay(value); break;
|
||||
case 'instrument': updateInstrumentDisplay(value); break;
|
||||
case 'levelL': updateMeterDisplay(state.levelL, state.levelR); break;
|
||||
case 'levelR': updateMeterDisplay(state.levelL, state.levelR); break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If no onChange, poll manually
|
||||
if (typeof window.modguijs.onChange !== 'function') {
|
||||
startPolling();
|
||||
}
|
||||
} else {
|
||||
// Fallback: poll if modguijs not ready yet, wait then retry
|
||||
setTimeout(function () {
|
||||
if (window.modguijs) {
|
||||
setupModGUI();
|
||||
} else {
|
||||
// Still no API — run in demo mode
|
||||
startDemoMode();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Polling fallback (if onChange not supported) ----
|
||||
var pollInterval = null;
|
||||
|
||||
function startPolling() {
|
||||
if (pollInterval) return;
|
||||
pollInterval = setInterval(function () {
|
||||
if (!window.modguijs || typeof window.modguijs.getValue !== 'function') return;
|
||||
var params = ['volume', 'pan', 'mute', 'solo', 'instrument', 'levelL', 'levelR'];
|
||||
params.forEach(function (symbol) {
|
||||
try {
|
||||
var val = window.modguijs.getValue(symbol);
|
||||
if (val !== undefined && val !== null && val !== state[symbol]) {
|
||||
state[symbol] = val;
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
// Batch update meter
|
||||
updateMeterDisplay(state.levelL, state.levelR);
|
||||
}, 50); // 20fps for meters
|
||||
}
|
||||
|
||||
// ---- Demo mode (when not in PiPedal) ----
|
||||
var demoInterval = null;
|
||||
|
||||
function startDemoMode() {
|
||||
console.log('OPLabs Band Channel: No modguijs API found, running demo mode');
|
||||
// Animate levels for visual testing
|
||||
var t = 0;
|
||||
demoInterval = setInterval(function () {
|
||||
t += 0.05;
|
||||
var l = -40 + 30 * (Math.sin(t) * 0.5 + 0.5);
|
||||
var r = -40 + 30 * (Math.cos(t * 0.7) * 0.5 + 0.5);
|
||||
state.levelL = l;
|
||||
state.levelR = r;
|
||||
updateMeterDisplay(l, r);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// ---- Initialization ----
|
||||
function init() {
|
||||
cacheElements();
|
||||
setupUIHandlers();
|
||||
setupKnobs();
|
||||
updateAll();
|
||||
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);
|
||||
if (peakTimerL) clearTimeout(peakTimerL);
|
||||
if (peakTimerR) clearTimeout(peakTimerR);
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user