Initial: OPLabsBandChannel.lv2 + OPLabsBandBus.lv2
This commit is contained in:
Executable
BIN
Binary file not shown.
@@ -0,0 +1,141 @@
|
||||
@prefix modgui: <http://moddevices.com/ns/modgui#> .
|
||||
@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 epp: <http://lv2plug.in/ns/ext/port-props#> .
|
||||
@prefix pg: <http://lv2plug.in/ns/ext/port-groups#> .
|
||||
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
|
||||
<http://ourpad.casa/plugins/oplabs-band-bus>
|
||||
a lv2:Plugin , lv2:UtilityPlugin ;
|
||||
doap:name "OPLabs Band Bus" ;
|
||||
doap:license <http://opensource.org/licenses/MIT> ;
|
||||
lv2:project <http://ourpad.casa/plugins/oplabs-band-bus> ;
|
||||
mod:brand "OPLabs" ;
|
||||
mod:label "Band Bus" ;
|
||||
mod:model "Band Bus" ;
|
||||
|
||||
# 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 (62 total)
|
||||
# ============================
|
||||
lv2:port
|
||||
|
||||
## --- AUDIO INPUT PORTS (16) ---
|
||||
## Channel 1
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 0 ; lv2:symbol "ch1L" ; lv2:name "Ch 1 L" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch1> ] ,
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 1 ; lv2:symbol "ch1R" ; lv2:name "Ch 1 R" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch1> ] ,
|
||||
## Channel 2
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 2 ; lv2:symbol "ch2L" ; lv2:name "Ch 2 L" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch2> ] ,
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 3 ; lv2:symbol "ch2R" ; lv2:name "Ch 2 R" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch2> ] ,
|
||||
## Channel 3
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 4 ; lv2:symbol "ch3L" ; lv2:name "Ch 3 L" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch3> ] ,
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 5 ; lv2:symbol "ch3R" ; lv2:name "Ch 3 R" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch3> ] ,
|
||||
## Channel 4
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 6 ; lv2:symbol "ch4L" ; lv2:name "Ch 4 L" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch4> ] ,
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 7 ; lv2:symbol "ch4R" ; lv2:name "Ch 4 R" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch4> ] ,
|
||||
## Channel 5
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 8 ; lv2:symbol "ch5L" ; lv2:name "Ch 5 L" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch5> ] ,
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 9 ; lv2:symbol "ch5R" ; lv2:name "Ch 5 R" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch5> ] ,
|
||||
## Channel 6
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 10 ; lv2:symbol "ch6L" ; lv2:name "Ch 6 L" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch6> ] ,
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 11 ; lv2:symbol "ch6R" ; lv2:name "Ch 6 R" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch6> ] ,
|
||||
## Channel 7
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 12 ; lv2:symbol "ch7L" ; lv2:name "Ch 7 L" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch7> ] ,
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 13 ; lv2:symbol "ch7R" ; lv2:name "Ch 7 R" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch7> ] ,
|
||||
## Channel 8
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 14 ; lv2:symbol "ch8L" ; lv2:name "Ch 8 L" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch8> ] ,
|
||||
[ a lv2:AudioPort , lv2:InputPort ; lv2:index 15 ; lv2:symbol "ch8R" ; lv2:name "Ch 8 R" ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch8> ] ,
|
||||
|
||||
## --- AUDIO OUTPUT PORTS (2) ---
|
||||
[ a lv2:AudioPort , lv2:OutputPort ; lv2:index 16 ; lv2:symbol "outL" ; lv2:name "Out L" ] ,
|
||||
[ a lv2:AudioPort , lv2:OutputPort ; lv2:index 17 ; lv2:symbol "outR" ; lv2:name "Out R" ] ,
|
||||
|
||||
## --- CONTROL INPUT PORTS — PER CHANNEL (24) ---
|
||||
|
||||
## Ch 1
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 18 ; lv2:symbol "ch1Vol" ; lv2:name "Ch 1 Vol" ; lv2:default 0.0 ; lv2:minimum -60.0 ; lv2:maximum 6.0 ; units:unit units:db ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch1> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 19 ; lv2:symbol "ch1Pan" ; lv2:name "Ch 1 Pan" ; lv2:default 0.0 ; lv2:minimum -1.0 ; lv2:maximum 1.0 ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch1> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 20 ; lv2:symbol "ch1Mute" ; lv2:name "Ch 1 Mute" ; lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0 ; lv2:portProperty lv2:toggled ; lv2:scalePoint [ rdfs:label "Off" ; rdf:value 0.0 ] , [ rdfs:label "On" ; rdf:value 1.0 ] ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch1> ] ,
|
||||
## Ch 2
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 21 ; lv2:symbol "ch2Vol" ; lv2:name "Ch 2 Vol" ; lv2:default 0.0 ; lv2:minimum -60.0 ; lv2:maximum 6.0 ; units:unit units:db ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch2> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 22 ; lv2:symbol "ch2Pan" ; lv2:name "Ch 2 Pan" ; lv2:default 0.0 ; lv2:minimum -1.0 ; lv2:maximum 1.0 ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch2> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 23 ; lv2:symbol "ch2Mute" ; lv2:name "Ch 2 Mute" ; lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0 ; lv2:portProperty lv2:toggled ; lv2:scalePoint [ rdfs:label "Off" ; rdf:value 0.0 ] , [ rdfs:label "On" ; rdf:value 1.0 ] ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch2> ] ,
|
||||
## Ch 3
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 24 ; lv2:symbol "ch3Vol" ; lv2:name "Ch 3 Vol" ; lv2:default 0.0 ; lv2:minimum -60.0 ; lv2:maximum 6.0 ; units:unit units:db ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch3> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 25 ; lv2:symbol "ch3Pan" ; lv2:name "Ch 3 Pan" ; lv2:default 0.0 ; lv2:minimum -1.0 ; lv2:maximum 1.0 ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch3> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 26 ; lv2:symbol "ch3Mute" ; lv2:name "Ch 3 Mute" ; lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0 ; lv2:portProperty lv2:toggled ; lv2:scalePoint [ rdfs:label "Off" ; rdf:value 0.0 ] , [ rdfs:label "On" ; rdf:value 1.0 ] ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch3> ] ,
|
||||
## Ch 4
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 27 ; lv2:symbol "ch4Vol" ; lv2:name "Ch 4 Vol" ; lv2:default 0.0 ; lv2:minimum -60.0 ; lv2:maximum 6.0 ; units:unit units:db ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch4> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 28 ; lv2:symbol "ch4Pan" ; lv2:name "Ch 4 Pan" ; lv2:default 0.0 ; lv2:minimum -1.0 ; lv2:maximum 1.0 ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch4> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 29 ; lv2:symbol "ch4Mute" ; lv2:name "Ch 4 Mute" ; lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0 ; lv2:portProperty lv2:toggled ; lv2:scalePoint [ rdfs:label "Off" ; rdf:value 0.0 ] , [ rdfs:label "On" ; rdf:value 1.0 ] ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch4> ] ,
|
||||
## Ch 5
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 30 ; lv2:symbol "ch5Vol" ; lv2:name "Ch 5 Vol" ; lv2:default 0.0 ; lv2:minimum -60.0 ; lv2:maximum 6.0 ; units:unit units:db ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch5> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 31 ; lv2:symbol "ch5Pan" ; lv2:name "Ch 5 Pan" ; lv2:default 0.0 ; lv2:minimum -1.0 ; lv2:maximum 1.0 ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch5> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 32 ; lv2:symbol "ch5Mute" ; lv2:name "Ch 5 Mute" ; lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0 ; lv2:portProperty lv2:toggled ; lv2:scalePoint [ rdfs:label "Off" ; rdf:value 0.0 ] , [ rdfs:label "On" ; rdf:value 1.0 ] ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch5> ] ,
|
||||
## Ch 6
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 33 ; lv2:symbol "ch6Vol" ; lv2:name "Ch 6 Vol" ; lv2:default 0.0 ; lv2:minimum -60.0 ; lv2:maximum 6.0 ; units:unit units:db ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch6> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 34 ; lv2:symbol "ch6Pan" ; lv2:name "Ch 6 Pan" ; lv2:default 0.0 ; lv2:minimum -1.0 ; lv2:maximum 1.0 ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch6> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 35 ; lv2:symbol "ch6Mute" ; lv2:name "Ch 6 Mute" ; lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0 ; lv2:portProperty lv2:toggled ; lv2:scalePoint [ rdfs:label "Off" ; rdf:value 0.0 ] , [ rdfs:label "On" ; rdf:value 1.0 ] ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch6> ] ,
|
||||
## Ch 7
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 36 ; lv2:symbol "ch7Vol" ; lv2:name "Ch 7 Vol" ; lv2:default 0.0 ; lv2:minimum -60.0 ; lv2:maximum 6.0 ; units:unit units:db ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch7> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 37 ; lv2:symbol "ch7Pan" ; lv2:name "Ch 7 Pan" ; lv2:default 0.0 ; lv2:minimum -1.0 ; lv2:maximum 1.0 ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch7> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 38 ; lv2:symbol "ch7Mute" ; lv2:name "Ch 7 Mute" ; lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0 ; lv2:portProperty lv2:toggled ; lv2:scalePoint [ rdfs:label "Off" ; rdf:value 0.0 ] , [ rdfs:label "On" ; rdf:value 1.0 ] ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch7> ] ,
|
||||
## Ch 8
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 39 ; lv2:symbol "ch8Vol" ; lv2:name "Ch 8 Vol" ; lv2:default 0.0 ; lv2:minimum -60.0 ; lv2:maximum 6.0 ; units:unit units:db ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch8> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 40 ; lv2:symbol "ch8Pan" ; lv2:name "Ch 8 Pan" ; lv2:default 0.0 ; lv2:minimum -1.0 ; lv2:maximum 1.0 ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch8> ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 41 ; lv2:symbol "ch8Mute" ; lv2:name "Ch 8 Mute" ; lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0 ; lv2:portProperty lv2:toggled ; lv2:scalePoint [ rdfs:label "Off" ; rdf:value 0.0 ] , [ rdfs:label "On" ; rdf:value 1.0 ] ; pg:group <http://ourpad.casa/plugins/oplabs-band-bus#ch8> ] ,
|
||||
|
||||
## --- MASTER CONTROL INPUT PORTS (2) ---
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 42 ; lv2:symbol "masterVol" ; lv2:name "Master Vol" ; lv2:default 0.0 ; lv2:minimum -60.0 ; lv2:maximum 6.0 ; units:unit units:db ] ,
|
||||
[ a lv2:InputPort , lv2:ControlPort ; lv2:index 43 ; lv2:symbol "masterMute" ; lv2:name "Master Mute" ; lv2:default 0.0 ; lv2:minimum 0.0 ; lv2:maximum 1.0 ; lv2:portProperty lv2:toggled ; lv2:scalePoint [ rdfs:label "Off" ; rdf:value 0.0 ] , [ rdfs:label "On" ; rdf:value 1.0 ] ] ,
|
||||
|
||||
## --- CONTROL OUTPUT PORTS — PER-CHANNEL LEVEL METERS (16) ---
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 44 ; lv2:symbol "ch1LevelL" ; lv2:name "Ch 1 Lvl L" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 45 ; lv2:symbol "ch1LevelR" ; lv2:name "Ch 1 Lvl R" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 46 ; lv2:symbol "ch2LevelL" ; lv2:name "Ch 2 Lvl L" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 47 ; lv2:symbol "ch2LevelR" ; lv2:name "Ch 2 Lvl R" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 48 ; lv2:symbol "ch3LevelL" ; lv2:name "Ch 3 Lvl L" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 49 ; lv2:symbol "ch3LevelR" ; lv2:name "Ch 3 Lvl R" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 50 ; lv2:symbol "ch4LevelL" ; lv2:name "Ch 4 Lvl L" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 51 ; lv2:symbol "ch4LevelR" ; lv2:name "Ch 4 Lvl R" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 52 ; lv2:symbol "ch5LevelL" ; lv2:name "Ch 5 Lvl L" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 53 ; lv2:symbol "ch5LevelR" ; lv2:name "Ch 5 Lvl R" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 54 ; lv2:symbol "ch6LevelL" ; lv2:name "Ch 6 Lvl L" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 55 ; lv2:symbol "ch6LevelR" ; lv2:name "Ch 6 Lvl R" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 56 ; lv2:symbol "ch7LevelL" ; lv2:name "Ch 7 Lvl L" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 57 ; lv2:symbol "ch7LevelR" ; lv2:name "Ch 7 Lvl R" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 58 ; lv2:symbol "ch8LevelL" ; lv2:name "Ch 8 Lvl L" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 59 ; lv2:symbol "ch8LevelR" ; lv2:name "Ch 8 Lvl R" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
|
||||
## --- MASTER LEVEL METER OUTPUT PORTS (2) ---
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 60 ; lv2:symbol "masterLevelL" ; lv2:name "Master Lvl L" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ] ,
|
||||
[ a lv2:OutputPort , lv2:ControlPort ; lv2:index 61 ; lv2:symbol "masterLevelR" ; lv2:name "Master Lvl R" ; lv2:default -70.0 ; lv2:minimum -70.0 ; lv2:maximum 6.0 ; units:unit units:db ; lv2:portProperty epp:notOnGUI ]
|
||||
.
|
||||
|
||||
# Port group definitions
|
||||
<http://ourpad.casa/plugins/oplabs-band-bus#ch1> a pg:InputGroup ; lv2:name "Channel 1" ; lv2:symbol "ch1" .
|
||||
<http://ourpad.casa/plugins/oplabs-band-bus#ch2> a pg:InputGroup ; lv2:name "Channel 2" ; lv2:symbol "ch2" .
|
||||
<http://ourpad.casa/plugins/oplabs-band-bus#ch3> a pg:InputGroup ; lv2:name "Channel 3" ; lv2:symbol "ch3" .
|
||||
<http://ourpad.casa/plugins/oplabs-band-bus#ch4> a pg:InputGroup ; lv2:name "Channel 4" ; lv2:symbol "ch4" .
|
||||
<http://ourpad.casa/plugins/oplabs-band-bus#ch5> a pg:InputGroup ; lv2:name "Channel 5" ; lv2:symbol "ch5" .
|
||||
<http://ourpad.casa/plugins/oplabs-band-bus#ch6> a pg:InputGroup ; lv2:name "Channel 6" ; lv2:symbol "ch6" .
|
||||
<http://ourpad.casa/plugins/oplabs-band-bus#ch7> a pg:InputGroup ; lv2:name "Channel 7" ; lv2:symbol "ch7" .
|
||||
<http://ourpad.casa/plugins/oplabs-band-bus#ch8> a pg:InputGroup ; lv2:name "Channel 8" ; lv2:symbol "ch8" .
|
||||
@@ -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-bus>
|
||||
a lv2:Plugin ;
|
||||
lv2:binary <OPLabsBandBus.so> ;
|
||||
rdfs:seeAlso <OPLabsBandBus.ttl> .
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
BIN
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"
|
||||
]
|
||||
|
||||
.
|
||||
@@ -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> .
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user