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
Binary file not shown.
@@ -0,0 +1,183 @@
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix doap: <http://usefulinc.com/ns/doap#> .
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
@prefix mod: <http://moddevices.com/ns/mod#> .
@prefix state: <http://lv2plug.in/ns/ext/state#> .
@prefix patch: <http://lv2plug.in/ns/ext/patch#> .
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix pipedal_ui: <http://github.com/rerdavies/pipedal/ui#> .
@prefix modgui: <http://moddevices.com/ns/modgui#> .
@prefix epp: <http://lv2plug.in/ns/ext/port-props#> .
@prefix pg: <http://lv2plug.in/ns/ext/port-groups#> .
@prefix param: <http://lv2plug.in/ns/ext/parameters#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
<http://ourpad.casa/plugins/oplabs-band-channel>
a lv2:Plugin , lv2:UtilityPlugin ;
doap:name "OPLabs Band Channel" ;
doap:license <http://opensource.org/licenses/MIT> ;
lv2:project <http://ourpad.casa/plugins/oplabs-band-channel> ;
mod:brand "OPLabs" ;
mod:label "Band Channel" ;
mod:model "Band Channel" ;
# ModGUI for PiPedal
modgui:gui [
modgui:resourceDirectory <modgui/> ;
modgui:iconTemplate <modgui/index.html> ;
modgui:settingsTemplate <modgui/index.html> ;
] ;
# State extension for save/restore
lv2:extensionData state:interface ;
# Required features for state and path mapping
lv2:requiredFeature urid:map ;
lv2:optionalFeature state:mapPath ,
state:freePath ;
# Port definitions
lv2:port
## --- CONTROL INPUT PORTS ---
# Volume: -60 to +6 dB, default 0
[
a lv2:InputPort , lv2:ControlPort ;
lv2:index 0 ;
lv2:symbol "volume" ;
lv2:name "Volume" ;
lv2:default 0.0 ;
lv2:minimum -60.0 ;
lv2:maximum 6.0 ;
units:unit units:db ;
rdfs:comment "Channel volume in dB"
] ,
# Pan: -1 to +1, default 0 (center)
[
a lv2:InputPort , lv2:ControlPort ;
lv2:index 1 ;
lv2:symbol "pan" ;
lv2:name "Pan" ;
lv2:default 0.0 ;
lv2:minimum -1.0 ;
lv2:maximum 1.0 ;
rdfs:comment "Stereo pan position (-1 full left, +1 full right)"
] ,
# Mute: 0/1 toggled
[
a lv2:InputPort , lv2:ControlPort ;
lv2:index 2 ;
lv2:symbol "mute" ;
lv2:name "Mute" ;
lv2:default 0.0 ;
lv2:minimum 0.0 ;
lv2:maximum 1.0 ;
lv2:portProperty lv2:toggled ;
lv2:scalePoint [ rdfs:label "Normal" ; rdf:value 0.0 ] ,
[ rdfs:label "Muted" ; rdf:value 1.0 ] ;
rdfs:comment "Mute channel audio"
] ,
# Solo: 0/1 toggled
[
a lv2:InputPort , lv2:ControlPort ;
lv2:index 3 ;
lv2:symbol "solo" ;
lv2:name "Solo" ;
lv2:default 0.0 ;
lv2:minimum 0.0 ;
lv2:maximum 1.0 ;
lv2:portProperty lv2:toggled ;
lv2:scalePoint [ rdfs:label "Normal" ; rdf:value 0.0 ] ,
[ rdfs:label "Solo" ; rdf:value 1.0 ] ;
rdfs:comment "Solo this channel (mutes all others)"
] ,
# Instrument: enumeration
[
a lv2:InputPort , lv2:ControlPort ;
lv2:index 4 ;
lv2:symbol "instrument" ;
lv2:name "Instrument" ;
lv2:default 0.0 ;
lv2:minimum 0.0 ;
lv2:maximum 4.0 ;
lv2:portProperty lv2:enumeration ;
lv2:scalePoint [ rdfs:label "Guitar" ; rdf:value 0.0 ] ,
[ rdfs:label "Bass" ; rdf:value 1.0 ] ,
[ rdfs:label "Keys" ; rdf:value 2.0 ] ,
[ rdfs:label "Vocals" ; rdf:value 3.0 ] ,
[ rdfs:label "Backing"; rdf:value 4.0 ] ;
rdfs:comment "Instrument type for channel strip"
] ,
## --- CONTROL OUTPUT PORTS (Meters) ---
# Level L: peak meter, -60 to 0 dB
[
a lv2:OutputPort , lv2:ControlPort ;
lv2:index 5 ;
lv2:symbol "levelL" ;
lv2:name "Level L" ;
lv2:default 0.0 ;
lv2:minimum -60.0 ;
lv2:maximum 0.0 ;
units:unit units:db ;
lv2:portProperty epp:notOnGUI ;
rdfs:comment "Left channel peak level in dB"
] ,
# Level R: peak meter, -60 to 0 dB
[
a lv2:OutputPort , lv2:ControlPort ;
lv2:index 6 ;
lv2:symbol "levelR" ;
lv2:name "Level R" ;
lv2:default 0.0 ;
lv2:minimum -60.0 ;
lv2:maximum 0.0 ;
units:unit units:db ;
lv2:portProperty epp:notOnGUI ;
rdfs:comment "Right channel peak level in dB"
] ,
## --- AUDIO PORTS ---
# Left audio input
[
a lv2:AudioPort , lv2:InputPort ;
lv2:index 7 ;
lv2:symbol "inL" ;
lv2:name "In L"
] ,
# Right audio input
[
a lv2:AudioPort , lv2:InputPort ;
lv2:index 8 ;
lv2:symbol "inR" ;
lv2:name "In R"
] ,
# Left audio output
[
a lv2:AudioPort , lv2:OutputPort ;
lv2:index 9 ;
lv2:symbol "outL" ;
lv2:name "Out L"
] ,
# Right audio output
[
a lv2:AudioPort , lv2:OutputPort ;
lv2:index 10 ;
lv2:symbol "outR" ;
lv2:name "Out R"
]
.
+7
View File
@@ -0,0 +1,7 @@
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<http://ourpad.casa/plugins/oplabs-band-channel>
a lv2:Plugin ;
lv2:binary <OPLabsBandChannel.so> ;
rdfs:seeAlso <OPLabsBandChannel.ttl> .
+453
View File
@@ -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);
});
})();
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OPLabs Band Channel</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="channel-strip" id="channelStrip">
<!-- Header: label + instrument badge -->
<div class="ch-header">
<span class="ch-label">Band Channel</span>
<span class="ch-instrument-badge" id="instrumentBadge">Guitar</span>
</div>
<!-- Mute & Solo buttons -->
<div class="ch-mute-solo">
<button class="ch-btn mute" id="btnMute" title="Mute">M</button>
<button class="ch-btn solo" id="btnSolo" title="Solo">S</button>
</div>
<!-- Level Meter -->
<div class="ch-meter-section">
<div class="meter-stereo">
<div class="meter-channel">
<span class="meter-label">L</span>
<div class="meter-track" id="meterTrackL">
<div class="meter-fill" id="meterFillL"></div>
<div class="meter-peak" id="meterPeakL"></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="meter-channel">
<span class="meter-label">R</span>
<div class="meter-track" id="meterTrackR">
<div class="meter-fill" id="meterFillR"></div>
<div class="meter-peak" id="meterPeakR"></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>
<div class="meter-db-readout" id="meterDbReadout">
<span>-∞</span>
<span>-∞</span>
</div>
</div>
<!-- Rotary Knobs Row -->
<div class="ch-knobs-row">
<!-- Volume knob -->
<div class="knob-container">
<div class="knob-label">Volume</div>
<div class="knob-rotary" id="knobVolume">
<div class="knob-arc" id="knobVolumeArc">
<div class="knob-grip" id="knobVolumeGrip"></div>
</div>
</div>
<div class="knob-value" id="volumeReadout">0.0 dB</div>
</div>
<!-- Pan knob -->
<div class="knob-container">
<div class="knob-label">Pan</div>
<div class="knob-rotary" id="knobPan">
<div class="knob-arc" id="knobPanArc">
<div class="knob-grip" id="knobPanGrip"></div>
</div>
</div>
<div class="knob-value" id="panReadout">C</div>
</div>
</div>
<!-- Instrument selector -->
<div class="ch-instrument-section">
<label class="ch-instrument-label">Instrument</label>
<select class="ch-instrument-select" id="instrumentSelect">
<option value="0">Guitar</option>
<option value="1">Bass</option>
<option value="2">Keys</option>
<option value="3">Vocals</option>
<option value="4">Backing</option>
</select>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
+334
View File
@@ -0,0 +1,334 @@
/* ===== OPLabs Band Channel - ModGUI Styles ===== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-surface: #0f3460;
--bg-knob: #2a2a4a;
--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;
--knob-size: 56px;
}
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;
}
.channel-strip {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
height: 100%;
width: 100%;
background: var(--bg-secondary);
border-radius: 6px;
border: 1px solid var(--border-color);
border-top: 3px solid var(--accent-orange);
}
/* ===== Header ===== */
.ch-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
}
.ch-label {
font-size: 12px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: 0.5px;
}
.ch-instrument-badge {
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
border: 1px solid var(--accent-orange);
color: var(--accent-orange);
background: rgba(255, 140, 0, 0.12);
}
/* ===== Mute & Solo Buttons ===== */
.ch-mute-solo {
display: flex;
gap: 6px;
justify-content: center;
}
.ch-btn {
flex: 1;
height: 28px;
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);
letter-spacing: 0.5px;
}
.ch-btn:hover {
filter: brightness(1.3);
}
.ch-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);
}
.ch-btn.solo.active {
background: var(--solo-yellow);
color: #1a1a2e;
border-color: var(--solo-yellow);
box-shadow: 0 0 8px rgba(255, 193, 7, 0.4);
}
/* ===== Level Meter ===== */
.ch-meter-section {
display: flex;
flex-direction: column;
gap: 2px;
}
.meter-stereo {
display: flex;
gap: 2px;
height: 70px;
}
.meter-channel {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.meter-label {
font-size: 9px;
font-weight: 700;
color: var(--text-secondary);
}
.meter-track {
flex: 1;
width: 100%;
background: var(--bg-primary);
border-radius: 2px;
position: relative;
overflow: hidden;
border: 1px solid var(--border-color);
}
.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;
}
.meter-fill.warm {
background: var(--meter-yellow);
}
.meter-fill.hot {
background: var(--meter-red);
}
.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;
}
.meter-tick {
position: absolute;
left: 0;
right: 0;
height: 1px;
background: rgba(255, 255, 255, 0.1);
z-index: 1;
}
.meter-db-readout {
display: flex;
justify-content: space-between;
padding: 0 4px;
font-size: 9px;
font-family: 'Courier New', monospace;
color: var(--text-secondary);
}
.meter-db-readout span.hot {
color: var(--meter-red);
font-weight: 700;
}
/* ===== Knobs Row ===== */
.ch-knobs-row {
display: flex;
gap: 4px;
justify-content: space-around;
}
.knob-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.knob-label {
font-size: 9px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
}
.knob-rotary {
width: var(--knob-size);
height: var(--knob-size);
position: relative;
cursor: pointer;
touch-action: none;
}
.knob-arc {
width: 100%;
height: 100%;
border-radius: 50%;
background: var(--bg-knob);
border: 2px solid var(--border-color);
position: relative;
transition: border-color 0.15s;
}
.knob-arc:hover {
border-color: var(--accent-blue);
}
.knob-grip {
position: absolute;
width: 4px;
height: 18px;
background: var(--accent-blue);
border-radius: 2px;
left: 50%;
top: 6px;
transform-origin: 2px 22px;
margin-left: -2px;
transition: background 0.15s;
}
.knob-arc:hover .knob-grip {
background: #81d4fa;
}
.knob-value {
font-size: 10px;
font-weight: 600;
font-family: 'Courier New', monospace;
color: var(--text-primary);
text-align: center;
min-width: 50px;
}
/* ===== Instrument Selector ===== */
.ch-instrument-section {
display: flex;
flex-direction: column;
gap: 2px;
}
.ch-instrument-label {
font-size: 9px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
}
.ch-instrument-select {
width: 100%;
height: 28px;
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--accent-orange);
border-radius: 4px;
padding: 0 6px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
outline: none;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23ff8c00'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 24px;
}
.ch-instrument-select:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 4px rgba(79, 195, 247, 0.3);
}
.ch-instrument-select option {
background: var(--bg-primary);
color: var(--text-primary);
}
/* ===== Responsive ===== */
@media (max-height: 280px) {
.channel-strip { gap: 2px; padding: 4px; }
.meter-stereo { height: 40px; }
.ch-knobs-row { gap: 2px; }
:root { --knob-size: 40px; }
.knob-grip { height: 12px; top: 4px; transform-origin: 2px 16px; }
.ch-label { font-size: 10px; }
}
@media (min-height: 400px) {
.meter-stereo { height: 90px; }
:root { --knob-size: 64px; }
}