Files
Linus Torvalds 75c5246eac WebMIDI: follow the pedal's MIDI channel
The pedal filters Control Change and Program Change by
settings.midi_channel and lets SysEx past regardless.  The app
transmitted on channel 1 always: activeTransmitChannel only ever moved
in response to CC 107, and no firmware has ever sent or handled CC 107.

So setting "MIDI Ch" to anything but Omni broke global bypass, the tuner
toggle, scene loads and the reboot, while parameter edits carried on
working - a failure mode that looks like half the app coming loose
rather than like one setting.

Take the channel from the schema instead, where it has been all along.
It arrives in the state dump like any other pot, and it is picked up
from three directions: the control on the Settings card, a PARAM_UPDATE
from the pedal, or the new copy in the dialog.  The pot is found by the
effect's 'base' - the header's filename - rather than by its display
name, which is the steadier of the two handles.  The dead CC 107 paths
go.

Note that startup was never actually stuck: REQ_SCHEMA and REQ_STATE are
SysEx, handle_midi_packet() dispatches SysEx and returns above the
channel check, and sysex_send_state_dump() walks every effect including
settings.  So the state always arrived; nothing read the channel out of
it.

Rename the dialog to "MIDI Connection" while here, since devices and
channel are all it holds, and give the channel a control there as well
as on the Settings card.  It is a view rather than a second setting -
changing it drives the card's control, so one place still decides what
gets sent - and it is where you go looking when Control Change appears
dead, which is the moment you most need to be able to fix it.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2026-07-28 11:24:43 -07:00

946 lines
18 KiB
CSS

:root {
--bg-color: #0f1115;
--panel-bg: rgba(255, 255, 255, 0.03);
--panel-border: rgba(255, 255, 255, 0.05);
--text-main: #e2e8f0;
--text-muted: #94a3b8;
--accent: #3b82f6;
--accent-hover: #60a5fa;
--danger: #ef4444;
--success: #10b981;
/* A locally served copy, in place of --success/--text-muted */
--dev: #f59e0b;
--dev-muted: #8a7040;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
line-height: 1.5;
padding: 20px;
}
.app-container {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.glass-panel {
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 12px;
padding: 20px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
header {
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
font-size: 1.5rem;
font-weight: 600;
}
h2 {
font-size: 1.2rem;
margin-bottom: 15px;
color: var(--text-muted);
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
/*
* The menu hangs out of the bottom of this, and has to be able to
* cover the cards below. It can't do that on its own z-index:
* .glass-panel sets backdrop-filter, which creates a stacking
* context, so the menu's z-index only ranks it inside this header.
* The cards are .glass-panel too and come later in the document, so
* without this they paint over the menu - and being backdrop
* filters, they blur it while they're at it.
*
* Stays well below the backdrop (900) and the panels above it.
*/
position: relative;
z-index: 20;
}
.header-controls {
display: flex;
align-items: center;
gap: 15px;
}
#app-title {
transition: color 0.3s;
}
/*
* The title is the pedal-state light, and it carries where the page
* came from as well: hue says which copy this is, brightness says
* whether the pedal is answering. Two signals, one word, no badge.
*/
.title-connected {
color: var(--success);
}
.title-disconnected {
color: var(--text-muted);
}
body.local-dev .title-connected {
color: var(--dev);
}
body.local-dev .title-disconnected {
color: var(--dev-muted);
}
.control-group label {
font-size: 0.9rem;
color: var(--text-muted);
}
select {
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid var(--panel-border);
color: var(--text-main);
padding: 10px 15px;
border-radius: 8px;
font-size: 1rem;
outline: none;
cursor: pointer;
appearance: none;
}
select:focus {
border-color: var(--accent);
}
option {
background-color: var(--bg-color);
color: var(--text-main);
}
/* Toggle Switch */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(255, 255, 255, 0.1);
transition: .3s;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .3s;
}
input:checked + .slider {
background-color: var(--accent);
}
input:checked + .slider:before {
transform: translateX(22px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.enable-group {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.action-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--panel-border);
color: var(--text-muted);
font-size: 1.1rem;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
padding-bottom: 2px;
}
.action-btn:hover {
background: rgba(255, 255, 255, 0.15);
color: var(--text-main);
transform: scale(1.1);
}
.action-btn:active {
transform: scale(0.95);
}
/* Pots */
.pots-section {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
}
.pot-control {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.pot-label {
font-size: 0.9rem;
color: var(--text-muted);
text-align: center;
}
.pot-value {
font-size: 1.1rem;
font-weight: 500;
}
input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
margin-top: -6px;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
/*
* Focus still needs to be visible - it's the only cue a keyboard user
* gets - but the default outline looks wrong on a round thumb, so put a
* ring on the thumb instead.
*/
input[type=range]:focus {
outline: none;
}
input[type=range]:focus::-webkit-slider-thumb {
box-shadow: 0 0 0 5px rgba(59, 130, 246, 0.35);
}
input[type=range]:focus::-moz-range-thumb {
box-shadow: 0 0 0 5px rgba(59, 130, 246, 0.35);
}
/* Diagnostics */
.diag-grid {
display: flex;
gap: 30px;
}
.diag-item {
display: flex;
align-items: center;
gap: 10px;
}
.diag-label {
font-size: 0.9rem;
color: var(--text-muted);
}
.indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.1);
}
.indicator.active {
background-color: var(--danger);
box-shadow: 0 0 10px var(--danger);
}
.value {
font-family: monospace;
font-size: 1.1rem;
background: rgba(0, 0, 0, 0.2);
padding: 4px 8px;
border-radius: 4px;
}
/* Effect Cards */
#effects-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.effect-card {
display: flex;
flex-direction: column;
gap: 15px;
}
.effect-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--panel-border);
padding-bottom: 10px;
/*
* The header is what you grab to reorder, so none of it may behave
* like text. A long press on text is the browser's select gesture,
* which on a phone is what you got instead of a drag, and iOS puts
* its callout menu up on top of that. This has to be here rather
* than switched on once a drag is recognised: by then the selection
* has already started.
*/
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}
/*
* Dragging grabs the whole header rather than the handle glyph alone,
* which makes the target finger-sized by construction rather than by
* padding out a ~20px character to the ~44px a thumb wants. The
* handle stays as the affordance that says the header can be grabbed.
*
* touch-action has to be on whatever the touch lands on: the browser
* decides whether a gesture may pan when it begins, from the element
* hit and its ancestors.
*/
.effect-header.draggable {
cursor: grab;
touch-action: none;
}
.drag-handle {
margin-right: 12px;
font-size: 1.4em;
opacity: 0.7;
}
.effect-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--accent);
}
.effect-controls {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
}
/* Graphic EQ specific styles */
.effect-controls.eq-container {
display: block;
position: relative;
padding-bottom: 20px;
}
.eq-curve-wrapper {
position: relative;
width: 100%;
height: 300px;
margin-top: 20px;
z-index: 1;
pointer-events: auto;
cursor: crosshair;
}
.eq-sliders-hidden {
display: none !important;
}
.eq-canvas {
width: 100%;
height: 100%;
display: block;
}
.eq-curve-svg {
width: 100%;
height: 100%;
overflow: visible;
}
.eq-path {
fill: rgba(59, 130, 246, 0.15); /* var(--accent) transparent */
stroke: var(--accent);
stroke-width: 3px;
vector-effect: non-scaling-stroke;
}
.eq-sliders {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1;
margin-top: 10px;
}
.eq-pot {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: 260px; /* enough space for val, slider, label */
width: 10%;
}
.eq-slider-wrapper {
width: 20px;
height: 180px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
input[type=range].eq-range {
transform: rotate(-90deg);
width: 180px; /* this becomes the visual height */
margin: 0;
}
/* Bottom Panels (Generic) */
/* Panel Backdrop */
#panel-backdrop {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
z-index: 900;
transition: opacity 0.3s;
}
#panel-backdrop.hidden {
display: none !important;
}
.bottom-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
margin: 0;
border-radius: 20px 20px 0 0;
border-bottom: none;
z-index: 1000;
padding: 20px 30px;
background: rgba(15, 17, 21, 0.95);
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.8);
transform: translateY(110%);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.bottom-panel:not(.hidden) {
transform: translateY(0);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
}
.panel-header h2 {
margin: 0;
color: var(--text-main);
}
/*
* The menu proper: a dropdown hung off the burger, holding actions and
* nothing else. It deliberately does not dim the page behind it - it
* isn't modal, and a menu that darkens the whole app to offer four
* items reads as heavier than it is.
*/
.burger-wrap {
position: relative;
}
.menu-dropdown {
position: absolute;
z-index: 1000;
top: calc(100% + 8px);
right: 0;
/*
* Sized to whatever the longest item happens to be, rather than to a
* number that was right until someone added a word. "Reboot to
* Programming Mode" wrapped at 260px, and the scene items change
* width as you pick different scenes, so nothing here has a length
* worth hard-coding. The floor keeps a short menu from looking
* mean; the ceiling keeps a long one on the screen.
*/
width: max-content;
min-width: 260px;
max-width: calc(100vw - 32px);
padding: 6px;
background: rgba(20, 23, 29, 0.98);
border: 1px solid var(--panel-border);
border-radius: 12px;
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.65);
}
/* The bottom sheet hid itself by sliding off the bottom edge, so there
has never been a general .hidden rule to inherit. */
.menu-dropdown.hidden,
.dialog-panel.hidden {
display: none;
}
.menu-dropdown button {
display: block;
width: 100%;
text-align: left;
background: none;
border: 0;
color: var(--text-main);
padding: 11px 14px;
border-radius: 8px;
cursor: pointer;
font: inherit;
font-size: 0.98rem;
white-space: nowrap;
}
.menu-dropdown button:hover {
background: rgba(255, 255, 255, 0.08);
}
.menu-dropdown button.danger-item {
color: var(--danger);
}
.menu-dropdown button.danger-item:hover {
background: rgba(239, 68, 68, 0.12);
}
.menu-dropdown hr {
border: 0;
border-top: 1px solid var(--panel-border);
margin: 6px 8px;
}
/* Which scene you're on, promoted out of the menu into the header.
Saving and loading stayed behind the menu - see index.html. */
.header-scenes {
display: flex;
align-items: center;
margin-right: 4px;
}
.header-scenes select {
background: rgba(255, 255, 255, 0.06);
color: var(--text-main);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 6px 10px;
font: inherit;
font-size: 0.88rem;
}
/*
* Settings, which is a dialog rather than a sheet: it is centred and
* sized to its content instead of anchored to the bottom edge, because
* nothing about it is phone-shaped any more.
*/
.dialog-panel {
position: fixed;
z-index: 1000;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(460px, calc(100vw - 40px));
max-height: calc(100vh - 40px);
overflow-y: auto;
padding: 0;
background: rgba(15, 17, 21, 0.97);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.7);
}
.dialog-panel .panel-header {
margin: 0;
padding: 18px 22px;
border-bottom: 1px solid var(--panel-border);
position: sticky;
top: 0;
background: rgba(15, 17, 21, 0.97);
}
.dialog-body {
padding: 6px 22px 22px;
}
.menu-section {
margin-top: 22px;
}
.menu-section > h3 {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--text-muted);
margin: 0 0 10px;
padding-bottom: 6px;
border-bottom: 1px solid var(--panel-border);
}
.menu-field {
margin-bottom: 10px;
}
.menu-field > label {
display: block;
font-size: 0.82rem;
color: var(--text-muted);
margin-bottom: 4px;
}
.menu-hint {
font-size: 0.8rem;
color: var(--text-muted);
margin-top: 6px;
}
.menu-select {
width: 100%;
background: #1a1d23;
color: var(--text-main);
padding: 9px 10px;
border-radius: 8px;
border: 1px solid var(--panel-border);
font: inherit;
font-size: 0.95rem;
}
/*
* showButtonSuccess()/showButtonError() swap a button's label for a
* moment and add one of these. They aren't tied to a particular kind
* of button, because the buttons that report back like this are now
* both menu items and header controls - hence !important, which beats
* the two-selector rules those live under without having to enumerate
* every context a reporting button might turn up in.
*/
.success {
background: rgba(16, 185, 129, 0.2) !important;
color: var(--success) !important;
border-color: var(--success) !important;
}
.error {
background: rgba(239, 68, 68, 0.2) !important;
color: var(--danger) !important;
border-color: var(--danger) !important;
}
.active-pot-info {
display: flex;
align-items: center;
gap: 15px;
}
#active-pot-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-main);
}
#active-pot-value {
font-size: 1.3rem;
color: var(--accent);
background: rgba(0, 0, 0, 0.3);
padding: 4px 12px;
border-radius: 8px;
}
.active-pot-slider-container {
padding-bottom: 20px;
}
/* Larger slider for fine adjustments */
input[type=range].large-slider {
height: 40px;
}
input[type=range].large-slider::-webkit-slider-thumb {
height: 32px;
width: 32px;
margin-top: -14px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
input[type=range].large-slider::-webkit-slider-runnable-track {
height: 8px;
border-radius: 4px;
}
/* Make pot-controls clickable */
.pot-control, .pot-control * {
cursor: pointer;
}
/* Tuner Overlay */
#tuner-panel {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color);
z-index: 2000;
display: flex;
flex-direction: column;
padding: 30px;
border-radius: 0;
border: none;
justify-content: center;
gap: 30px;
}
#tuner-panel.hidden {
display: none !important;
}
.tuner-header {
position: absolute;
top: 30px;
left: 30px;
right: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.tuner-chromatic-display {
display: flex;
justify-content: center;
align-items: baseline;
gap: 15px;
margin-top: 50px;
}
.tuner-note {
font-size: 5rem;
font-weight: bold;
color: var(--accent);
text-align: right;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.tuner-cents {
font-size: 2.5rem;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
line-height: 1;
}
.tuner-vol {
font-size: 1.2rem;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
line-height: 1;
opacity: 0.7;
}
.tuner-meter-container {
position: relative;
height: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
overflow: hidden;
margin-top: 10px;
}
.tuner-meter-center {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2px;
background-color: rgba(255, 255, 255, 0.2);
transform: translateX(-50%);
}
.tuner-needle {
position: absolute;
top: 0;
bottom: 0;
width: 6px;
background-color: var(--success);
left: 50%;
transform: translateX(-50%);
transition: left 0.1s linear, background-color 0.2s;
border-radius: 3px;
}
.tuner-strings {
display: flex;
justify-content: space-around;
margin-top: 40px;
}
.tuner-string {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
width: 60px;
}
.tuner-string-name {
font-weight: bold;
font-size: 1.5rem;
color: var(--text-muted);
}
.tuner-string-name.active {
color: var(--text-main);
}
.tuner-string-arrow {
width: 0;
height: 0;
border-left: 15px solid transparent;
border-right: 15px solid transparent;
transition: all 0.1s;
}
.tuner-string-arrow.up {
border-bottom: 20px solid var(--danger);
border-top: none;
background: transparent;
width: 0; height: 0; border-radius: 0;
}
.tuner-string-arrow.down {
border-top: 20px solid var(--danger);
border-bottom: none;
background: transparent;
width: 0; height: 0; border-radius: 0;
}
.tuner-string-arrow.perfect {
border: none;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: var(--success);
}
.tuner-string-arrow.inactive {
border: none;
width: 15px;
height: 15px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.1);
}
.tuner-string-vol {
font-size: 0.9rem;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
opacity: 0.7;
}
#tuner-btn.active {
background: rgba(59, 130, 246, 0.5);
color: white;
border-color: var(--accent);
}
.unrouted-divider {
grid-column: 1 / -1;
text-align: center;
color: var(--text-color);
font-weight: bold;
margin-top: 2rem;
margin-bottom: 1rem;
position: relative;
opacity: 0.7;
}
.unrouted-divider::before,
.unrouted-divider::after {
content: '';
position: absolute;
top: 50%;
width: 30%;
height: 1px;
background-color: var(--text-color);
opacity: 0.3;
}
.unrouted-divider::before { left: 5%; }
.unrouted-divider::after { right: 5%; }
.effect-card.unrouted {
opacity: 0.5;
filter: grayscale(80%);
transition: opacity 0.3s, filter 0.3s;
}
.effect-card.unrouted:hover {
opacity: 0.8;
filter: grayscale(40%);
}