diff --git a/src/dsp/ir_loader.py b/src/dsp/ir_loader.py index cc6b44e..0005e6a 100644 --- a/src/dsp/ir_loader.py +++ b/src/dsp/ir_loader.py @@ -43,6 +43,54 @@ class IRFile: length_ms: float channels: int = 1 + @property + def display_name(self) -> str: + """Return a clean human-readable name by stripping common IR prefixes/suffixes. + + Strips leading/trailing markers commonly found in IR filenames + (e.g. ``IR_Marshall_1960A.wav`` → ``Marshall 1960A``, + ``cab_Vox_AC30_48k.wav`` → ``Vox AC30``). + """ + name = self.name + + # Common IR prefixes (case-insensitive) + _prefixes = [ + "IR_", "ir_", "IR-", "ir-", + "Impulse_Response_", "impulse_response_", + "Impulse-Response-", "impulse-response-", + "Cab_", "cab_", "Cab-", "cab-", + ] + + # Common IR suffixes (case-insensitive) + _suffixes = [ + "_IR", "_ir", "-IR", "-ir", + "_Impulse", "_impulse", "-Impulse", "-impulse", + "_Cab", "_cab", "-Cab", "-cab", + "_Stereo", "_stereo", "-Stereo", "-stereo", + "_Mono", "_mono", "-Mono", "-mono", + "_48k", "_48K", "-48k", "-48K", + "_44100", "-44100", + "_48kHz", "-48kHz", + "_16bit", "_24bit", "-16bit", "-24bit", + "_LD", "_SD", "-LD", "-SD", + ] + + for prefix in _prefixes: + if name.lower().startswith(prefix.lower()): + name = name[len(prefix):] + break + + for suffix in _suffixes: + if name.lower().endswith(suffix.lower()): + name = name[:-len(suffix)] + break + + # Replace separators with spaces and clean up + name = name.replace("_", " ").replace("-", " ") + name = " ".join(name.split()).strip() + + return name if name else self.name + def _next_pow2(n: int) -> int: """Return the smallest power of 2 >= n.""" diff --git a/src/presets/manager.py b/src/presets/manager.py index f8fe1fd..9509101 100644 --- a/src/presets/manager.py +++ b/src/presets/manager.py @@ -104,6 +104,7 @@ def _preset_from_dict(data: dict) -> Preset: params=dict(block_data.get("params", {})), nam_model_path=block_data.get("nam_model_path", ""), ir_file_path=block_data.get("ir_file_path", ""), + block_id=block_data.get("block_id", ""), ) ) @@ -128,6 +129,7 @@ def _preset_from_dict(data: dict) -> Preset: params=dict(bd.get("params", {})), nam_model_path=bd.get("nam_model_path", ""), ir_file_path=bd.get("ir_file_path", ""), + block_id=bd.get("block_id", ""), ) ) snapshots[int(slot_str)] = Snapshot( @@ -191,6 +193,10 @@ class PresetManager: # Migrate legacy flat presets → gtr channel on first boot self._migrate_legacy_presets() + # Restore last active preset after boot so the pedal comes back + # to the same (channel, bank, program) it was on before shutdown. + self.restore_state() + logger.info("PresetManager root: %s (channel=%s)", self._dir, self._current_channel.value) # ── Public navigation API ─────────────────────────────────────────────── diff --git a/src/web/ui-v2-dist/index.html b/src/web/ui-v2-dist/index.html index 4063459..5d6929b 100644 --- a/src/web/ui-v2-dist/index.html +++ b/src/web/ui-v2-dist/index.html @@ -945,6 +945,22 @@ async function pollTuner(){ function startPolling(){stopPolling();loadState();pollTimer=setInterval(loadState,2000);} function stopPolling(){if(pollTimer){clearInterval(pollTimer);pollTimer=null;}} +// ═══ WebSocket — real-time updates ═══ +let ws=null; +function setupWebSocket(){ + if(ws)try{ws.close()}catch(e){} + const proto=location.protocol==='https:'?'wss:':'ws:'; + ws=new WebSocket(proto+'//'+location.host+'/ws'); + ws.onmessage=(ev)=>{ + try{ + const msg=JSON.parse(ev.data); + if(msg.type==='block_toggled'){loadState();} + }catch(e){} + }; + ws.onclose=()=>{setTimeout(setupWebSocket,3000);}; + ws.onerror=()=>{}; +} + // ═══ Init! ═══ function init(){ // Restore saved channel @@ -1243,8 +1259,9 @@ function init(){ document.getElementById('dlSearchBtn').onclick=doDlSearch; document.getElementById('dlSearch').onkeydown=(e)=>{if(e.key==='Enter')doDlSearch();}; - // Start polling + // Start polling + WebSocket startPolling(); + setupWebSocket(); loadPresets(); loadSnapshots(); }