feat: Snapshots — 8 per preset save/recall
Adds snapshot system to Helix Stadium: Backend: - Snapshot data model (8 slots per preset, captures block bypass + params + master volume) - Snapshot API endpoints: list, save, recall, rename, delete - Integrated into preset serialization/deserialization - Tracks current_snapshot in state / WebSocket broadcasts Frontend: - Camera icon in header bar with current snapshot number - SnapshotPanel as slide-up overlay - 8 snapshot slots in a horizontal row - Tap to recall filled snapshot, tap empty slot to save - Press-and-hold (600ms) to save current state - Inline name editing on tap - Discard Edits toggle - Green flash indicator on save - Delete button for active snapshot
This commit is contained in:
+43
-1
@@ -19,7 +19,7 @@ import threading
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .types import Bank, FXBlock, FXType, MIDIMapping, Preset
|
||||
from .types import Bank, FXBlock, FXType, MIDIMapping, Preset, Snapshot
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -70,6 +70,24 @@ def _preset_to_dict(preset: Preset) -> dict:
|
||||
}
|
||||
for key, mapping in preset.midi_mappings.items()
|
||||
},
|
||||
"snapshots": {
|
||||
str(slot): {
|
||||
"name": snap.name,
|
||||
"master_volume": snap.master_volume,
|
||||
"chain": [
|
||||
{
|
||||
"fx_type": block.fx_type.value,
|
||||
"enabled": block.enabled,
|
||||
"bypass": block.bypass,
|
||||
"params": dict(block.params),
|
||||
"nam_model_path": block.nam_model_path,
|
||||
"ir_file_path": block.ir_file_path,
|
||||
}
|
||||
for block in snap.chain
|
||||
],
|
||||
}
|
||||
for slot, snap in preset.snapshots.items()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +115,26 @@ def _preset_from_dict(data: dict) -> Preset:
|
||||
max_val=md.get("max_val", 1.0),
|
||||
)
|
||||
|
||||
snapshots = {}
|
||||
for slot_str, snap_data in data.get("snapshots", {}).items():
|
||||
snap_chain = []
|
||||
for bd in snap_data.get("chain", []):
|
||||
snap_chain.append(
|
||||
FXBlock(
|
||||
fx_type=FXType(bd["fx_type"]),
|
||||
enabled=bd.get("enabled", True),
|
||||
bypass=bd.get("bypass", False),
|
||||
params=dict(bd.get("params", {})),
|
||||
nam_model_path=bd.get("nam_model_path", ""),
|
||||
ir_file_path=bd.get("ir_file_path", ""),
|
||||
)
|
||||
)
|
||||
snapshots[int(slot_str)] = Snapshot(
|
||||
name=snap_data.get("name", f"Snapshot {slot_str}"),
|
||||
chain=snap_chain,
|
||||
master_volume=snap_data.get("master_volume", 0.8),
|
||||
)
|
||||
|
||||
return Preset(
|
||||
name=data["name"],
|
||||
bank=data.get("bank", 0),
|
||||
@@ -107,6 +145,7 @@ def _preset_from_dict(data: dict) -> Preset:
|
||||
routing_mode=data.get("routing_mode", "mono"),
|
||||
routing_breakpoint=data.get("routing_breakpoint", 7),
|
||||
tuner_enabled=data.get("tuner_enabled", False),
|
||||
snapshots=snapshots,
|
||||
)
|
||||
|
||||
|
||||
@@ -143,6 +182,9 @@ class PresetManager:
|
||||
# Auto-save debounce timer
|
||||
self._auto_save_timer: Optional[threading.Timer] = None
|
||||
|
||||
# Current snapshot slot (0 = none selected)
|
||||
self._current_snapshot: int = 0
|
||||
|
||||
logger.info("PresetManager root: %s", self._dir)
|
||||
|
||||
# ── Public navigation API ───────────────────────────────────────────────
|
||||
|
||||
@@ -89,6 +89,14 @@ class MIDIMapping:
|
||||
max_val: float = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Snapshot:
|
||||
"""A saved snapshot of block states and parameters within a preset."""
|
||||
name: str
|
||||
chain: list[FXBlock] = field(default_factory=list)
|
||||
master_volume: float = 0.8
|
||||
|
||||
|
||||
@dataclass
|
||||
class Preset:
|
||||
"""A complete pedal preset — full signal chain state."""
|
||||
@@ -104,6 +112,10 @@ class Preset:
|
||||
routing_mode: str = "mono" # "mono" or "4cm"
|
||||
routing_breakpoint: int = 7 # chain index where pre/post split occurs
|
||||
|
||||
# ── Snapshots (8 per preset) ──
|
||||
snapshots: dict[int, Snapshot] = field(default_factory=dict)
|
||||
"""Snapshot slots 1-8. Empty dict means no snapshots saved."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Bank:
|
||||
|
||||
Reference in New Issue
Block a user