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

165 lines
7.0 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RP2350 Pedal Controller</title>
<link rel="manifest" href="manifest.json">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="apple-touch-icon" href="icon-192.png">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app-container">
<header class="glass-panel app-header">
<h1 id="app-title" class="title-disconnected">RP2350 Pedal</h1>
<div class="header-controls">
<!-- Which scene you're on is worth seeing without opening
anything. Acting on it is not: Save overwrites the eeprom
and Load throws away what you have, so both sit behind the
menu where you can't reach them by accident. -->
<div class="header-scenes">
<select id="global-scene-select" title="Scene">
<!-- Options injected via JS -->
</select>
</div>
<button id="tuner-btn" class="action-btn" title="Tuner Mode">🎵</button>
<label class="switch" title="Global Enable">
<input type="checkbox" id="global-enable">
<span class="slider round"></span>
</label>
<div class="burger-wrap">
<button id="burger-btn" class="action-btn" title="Menu"
aria-haspopup="true" aria-expanded="false"></button>
<div id="global-menu" class="menu-dropdown hidden" role="menu">
<!-- The scene labels are filled in from the picker, so it
always says which one it is about to overwrite. -->
<button id="global-save-scene-btn" role="menuitem">💾&nbsp;&nbsp;Save Scene</button>
<button id="global-load-scene-btn" role="menuitem">📂&nbsp;&nbsp;Load Scene</button>
<button id="global-unroute-btn" role="menuitem">&nbsp;&nbsp;Unroute All Effects</button>
<hr>
<button id="update-app-btn" role="menuitem">⬇️&nbsp;&nbsp;Update App</button>
<button id="open-settings-btn" role="menuitem">🔌&nbsp;&nbsp;MIDI Connection…</button>
<hr>
<button id="global-program-btn" class="danger-item" role="menuitem">⚠️&nbsp;&nbsp;Reboot to Programming Mode</button>
</div>
</div>
</div>
</header>
<!-- Tuner Panel -->
<div id="tuner-panel" class="glass-panel hidden">
<div class="tuner-header">
<h2>Tuner</h2>
<div style="display: flex; gap: 15px; align-items: center;">
<label style="color: var(--text-muted); display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="tuner-synth-toggle"> Play Tones
</label>
<button id="close-tuner-btn" class="action-btn" title="Exit Tuner"></button>
</div>
</div>
<div class="tuner-chromatic-display">
<div class="tuner-note" id="tuner-chromatic-note">--</div>
<div class="tuner-cents" id="tuner-chromatic-cents"></div>
<div class="tuner-vol" id="tuner-chromatic-vol">V: 0</div>
</div>
<div class="tuner-meter-container">
<div class="tuner-meter-center"></div>
<div id="tuner-needle" class="tuner-needle"></div>
</div>
<div class="tuner-strings" id="tuner-poly-strings">
<!-- Strings dynamically added here -->
</div>
</div>
<main id="effects-container">
<!-- Effect cards populated dynamically -->
</main>
<!-- Panel Backdrop -->
<div id="panel-backdrop" class="hidden"></div>
<!-- Reached from the menu. Set once and then left alone, which is
why it isn't in the menu itself. -->
<div id="settings-panel" class="glass-panel dialog-panel hidden" role="dialog" aria-modal="true">
<div class="panel-header">
<h2>MIDI Connection</h2>
<button id="close-settings" class="action-btn" title="Close"></button>
</div>
<div class="dialog-body">
<div class="menu-section">
<h3>Devices</h3>
<div class="menu-field">
<label for="midi-input-select">Input</label>
<select id="midi-input-select" class="menu-select"></select>
</div>
<div class="menu-field">
<label for="midi-output-select">Output</label>
<select id="midi-output-select" class="menu-select"></select>
</div>
</div>
<div class="menu-section">
<h3>Channel</h3>
<!-- The same value as the Settings card's "MIDI Ch" pot, and
the same control underneath - see bindMidiChannel(). It
is repeated here because this is where you come looking
when Control Change stops working, and the channel is the
usual reason. -->
<div class="menu-field">
<select id="midi-channel-select" class="menu-select"></select>
</div>
<p class="menu-hint">The pedal filters Control Change and Program
Change by this, so bypass, the tuner and scene changes need it to
match. Parameter edits are SysEx and work either way — which is
how this dialog can still fix it when it is wrong.</p>
</div>
</div>
</div>
<!-- Active Pot Panel -->
<div id="active-pot-panel" class="glass-panel bottom-panel hidden">
<div class="panel-header">
<div class="active-pot-info">
<span id="active-pot-title">Effect - Pot</span>
<span id="active-pot-value" class="pot-value">0</span>
</div>
<button id="close-active-pot" class="action-btn" title="Close"></button>
</div>
<div class="active-pot-slider-container">
<input type="range" id="active-pot-slider" class="large-slider" min="0" max="120" value="60">
</div>
</div>
</div>
<script src="effects.js"></script>
<script src="app.js"></script>
<script>
// Keep the service worker away from local development. Unregistering
// one does not stop it controlling the page - the active worker stays
// in charge of already-controlled clients until they all go away - so
// a stale app.js can happily survive several reloads and an "Update
// App" or three. The PWA only matters for the deployed copy anyway.
//
// IS_LOCAL_DEV comes from app.js, which is loaded above.
if ('serviceWorker' in navigator) {
if (IS_LOCAL_DEV) {
// And clean up after any worker registered here previously
navigator.serviceWorker.getRegistrations()
.then(regs => regs.forEach(reg => reg.unregister()));
} else {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./service-worker.js')
.then(registration => {
console.log('ServiceWorker registration successful');
})
.catch(err => {
console.log('ServiceWorker registration failed: ', err);
});
});
}
}
</script>
</body>
</html>