Replace the current per-stem effect chain with a MultibandHost (renamed from MultibandHost) as the universal container. Every stem gets a multiband container that:
- Defaults to 1 band (passthrough mode - behaves like current system)
- Allows users to add bands and define crossover frequencies
- Accepts any effect implementing the
Effecttrait (PD, CLAP, native Rust, future types) - Shows all effects in a lane-based UI with per-band plugin chains
- Effect-agnostic: The multiband container works with
Box<dyn Effect>, not specific effect types - Unified interface: PD effects, CLAP plugins, and native effects all appear in the same picker
- Extensible: Future effect types (VST3, LV2, etc.) automatically work if they implement
Effect
┌─────────────────────────────────────────────────────────────────────────┐
│ MULTIBAND EDITOR UI │
├─────────────────────────────────────────────────────────────────────────┤
│ [Load Preset ▾] [Save Preset] Deck 1 - Drums [×] │
├─────────────────────────────────────────────────────────────────────────┤
│ Crossover Visualization (20Hz ────────────────────────────── 20kHz) │
│ ════════════════╪════════════════════╪══════════════════════════════ │
│ 200Hz 2kHz │
│ ↕ drag ↕ drag │
├─────────────────────────────────────────────────────────────────────────┤
│ Band 1: 20-200Hz (Sub) [S] [M] ────────────────────────────────── │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Compressor │ │ │ [+] │
│ │ ○○○○○○○○ │ │ │ │
│ │ Thr Rat Atk │ │ │ │
│ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ Band 2: 200Hz-2kHz (Mid) [S] [M] ────────────────────────────────── │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Saturation │ │ EQ │ │ │ [+] │
│ │ ○○○○○○○○ │ │ ○○○○○○○○ │ │ │ │
│ │ Drv Mix Tone│ │ Lo Mid Hi │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ Band 3: 2kHz-20kHz (High) [S] [M] ────────────────────────────────── │
│ ┌──────────────┐ │
│ │ De-esser │ [+] │
│ │ ○○○○○○○○ │ │
│ │ Freq Thr Rat│ │
│ └──────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ [+ Add Band] │
├─────────────────────────────────────────────────────────────────────────┤
│ Macros: [1:Attack] [2:Drive] [3:______] [4:______] │
│ [5:______] [6:______] [7:______] [8:______] │
└─────────────────────────────────────────────────────────────────────────┘
mesh-widgets/ # NEW CRATE (reusable for mesh-cue)
├── Cargo.toml
└── src/
├── lib.rs
└── multiband/
├── mod.rs # MultibandEditor widget
├── state.rs # MultibandEditorState
├── message.rs # MultibandEditorMessage enum
├── crossover_bar.rs # Draggable frequency dividers
├── band_lane.rs # Single band row with effects
├── effect_card.rs # Individual effect with 8 knobs (any Effect type)
└── macro_bar.rs # 8 macro knobs at bottom
mesh-core/src/effect/
├── multiband.rs # MOVE from clap/ - now effect-agnostic
└── preset.rs # NEW: MultibandPreset save/load
Note: MultibandHost moves from clap/ to effect/ since it's no longer CLAP-specific.
It holds Vec<Box<dyn Effect>> per band, accepting PD, CLAP, or any Effect impl.
1.1 Enhance MultibandHost API
- Add
get_band_count(),get_crossover_freqs()accessor methods - Add
get_band_effects(band_idx)to enumerate effects per band - Add
get_effect_info(band_idx, effect_idx)for UI display - Add
get_band_state(idx) -> BandState { muted, soloed, gain }
1.2 Create Preset System
// crates/mesh-core/src/effect/preset.rs
#[derive(Serialize, Deserialize)]
pub struct MultibandPreset {
pub name: String,
pub crossover_freqs: Vec<f32>,
pub bands: Vec<BandPreset>,
pub macros: Vec<MacroPreset>,
}
#[derive(Serialize, Deserialize)]
pub struct BandPreset {
pub gain: f32,
pub muted: bool,
pub effects: Vec<EffectPreset>,
}
/// Effect preset - works for ANY effect type implementing Effect trait
#[derive(Serialize, Deserialize)]
pub struct EffectPreset {
pub effect_id: String, // CLAP ID, PD folder name, or native effect ID
pub source: EffectSource, // Pd, Clap, or Native (extensible enum)
pub params: [f32; 8], // Normalized parameter values
pub bypassed: bool,
}
#[derive(Serialize, Deserialize)]
pub enum EffectSource {
Pd, // Pure Data patch
Clap, // CLAP plugin
Native, // Built-in Rust effect
// Future: Vst3, Lv2, etc.
}The preset system stores EffectSource to know which manager to use when recreating:
Pd→pd_manager.create_effect(id)Clap→clap_manager.create_effect(id)Native→native_manager.create_effect(id)
1.3 Add Preset Directory
- Location:
~/Music/mesh-collection/presets/multiband/ - Format: JSON files with
.mbpresetextension
2.1 Create mesh-widgets crate
# crates/mesh-widgets/Cargo.toml
[package]
name = "mesh-widgets"
version = "0.1.0"
[dependencies]
iced = { workspace = true }
mesh-core = { path = "../mesh-core" }2.2 MultibandEditorState
pub struct MultibandEditorState {
/// Which deck/stem this editor is for
pub deck: usize,
pub stem: usize,
/// Crossover frequencies (N-1 for N bands)
pub crossover_freqs: Vec<f32>,
/// Dragging state for crossover dividers
pub dragging_crossover: Option<usize>,
/// Per-band state
pub bands: Vec<BandUiState>,
/// Currently selected effect (for parameter focus)
pub selected_effect: Option<(usize, usize)>, // (band_idx, effect_idx)
/// Macro names and values
pub macros: [(String, f32); 8],
/// Preset browser state
pub preset_browser_open: bool,
pub available_presets: Vec<String>,
}
pub struct BandUiState {
pub name: String, // "Sub", "Low", "Mid", "High", etc.
pub freq_low: f32, // Hz
pub freq_high: f32, // Hz
pub muted: bool,
pub soloed: bool,
pub effects: Vec<EffectUiState>,
}
/// UI state for any effect (PD, CLAP, native, etc.)
pub struct EffectUiState {
pub id: String, // Effect identifier for recreation
pub name: String, // Display name from effect.info().name
pub source: EffectSource, // Pd, Clap, or Native
pub bypassed: bool,
pub param_names: [String; 8], // From effect.info().params
pub param_values: [f32; 8], // Current normalized values
}2.3 MultibandEditorMessage
pub enum MultibandEditorMessage {
// Crossover
StartDragCrossover(usize),
DragCrossover(f32), // New frequency in Hz
EndDragCrossover,
// Bands
AddBand,
RemoveBand(usize),
SetBandMute(usize, bool),
SetBandSolo(usize, bool),
// Effects within bands
OpenEffectPicker(usize), // band_idx - opens picker to add effect
RemoveEffect(usize, usize), // band_idx, effect_idx
BypassEffect(usize, usize, bool),
SelectEffect(usize, usize), // Focus for parameter editing
SetEffectParam(usize, usize, usize, f32), // band, effect, param, value
// Macros
SetMacro(usize, f32),
RenameMacro(usize, String),
OpenMacroMapper(usize), // Opens dialog to map macro to params
// Presets
OpenPresetBrowser,
LoadPreset(String),
SavePreset(String),
ClosePresetBrowser,
// Modal
Close,
}2.4 CrossoverBar Widget
- Horizontal bar showing 20Hz to 20kHz (log scale)
- Draggable divider lines at crossover frequencies
- Band labels between dividers
- Mouse interaction: drag to adjust crossover
2.5 BandLane Widget
- Horizontal row for one frequency band
- Header: band name, freq range, [S]olo [M]ute buttons
- Content: horizontal list of PluginCard widgets
- Footer: [+] button to add effect
2.6 PluginCard Widget
- Compact card showing one effect
- Header: effect name, bypass toggle, remove [×]
- Content: 8 small rotary knobs in 2 rows of 4
- Knob labels underneath
2.7 MacroBar Widget
- 8 macro knobs in a row
- Editable name labels
- Click knob to open macro mapping dialog
3.1 Replace Effect Chain Display
- Current:
[Effect1●]──[Effect2◯]──[+] - New:
[Multiband ▾]button that opens MultibandEditor
3.2 Modify DeckView
// In deck_view.rs
pub struct DeckView {
// Replace:
// stem_effect_names: [Vec<String>; 4],
// stem_effect_bypassed: [Vec<bool>; 4],
// With:
multiband_editors: [MultibandEditorState; 4], // One per stem
}3.3 Update App Message Routing
- Add
MultibandEditorMessageto main Message enum - Route to appropriate deck/stem's MultibandHost
- Sync UI state after backend changes
3.4 Engine Command Additions
// In command.rs
pub enum EngineCommand {
// Existing...
// New multiband commands
SetCrossoverFreq { deck: usize, stem: Stem, crossover_idx: usize, freq: f32 },
AddBandEffect { deck: usize, stem: Stem, band_idx: usize, effect: Box<dyn Effect> },
RemoveBandEffect { deck: usize, stem: Stem, band_idx: usize, effect_idx: usize },
SetBandMute { deck: usize, stem: Stem, band_idx: usize, muted: bool },
SetBandSolo { deck: usize, stem: Stem, band_idx: usize, soloed: bool },
SetBandEffectParam { deck: usize, stem: Stem, band_idx: usize, effect_idx: usize, param_idx: usize, value: f32 },
SetMacro { deck: usize, stem: Stem, macro_idx: usize, value: f32 },
AddBand { deck: usize, stem: Stem },
RemoveBand { deck: usize, stem: Stem, band_idx: usize },
}3.5 Initialize Stems with Multiband Wrapper
- When deck loads track, create MultibandHost per stem
- Default: 1 band, no crossover, empty effect chain
- Store reference for UI sync
4.1 Preset Save
- Serialize MultibandHost state to MultibandPreset
- Write to
~/Music/mesh-collection/presets/multiband/{name}.mbpreset
4.2 Preset Load
- Parse JSON file
- Create MultibandHost from preset
- Load each effect (PD or CLAP) with saved parameters
4.3 Preset Browser
- Simple modal with list of available presets
- Preview info (band count, effects)
- Load/Delete buttons
crates/mesh-widgets/Cargo.toml
crates/mesh-widgets/src/lib.rs
crates/mesh-widgets/src/multiband/mod.rs
crates/mesh-widgets/src/multiband/state.rs
crates/mesh-widgets/src/multiband/message.rs
crates/mesh-widgets/src/multiband/crossover_bar.rs
crates/mesh-widgets/src/multiband/band_lane.rs
crates/mesh-widgets/src/multiband/effect_card.rs
crates/mesh-widgets/src/multiband/macro_bar.rs
crates/mesh-core/src/effect/multiband.rs # MOVED from clap/multiband.rs
crates/mesh-core/src/effect/preset.rs # Effect-agnostic preset system
Cargo.toml # Add mesh-widgets workspace member
crates/mesh-core/src/effect/mod.rs # Export multiband, preset modules
crates/mesh-core/src/clap/mod.rs # Remove multiband (moved to effect/)
crates/mesh-core/src/engine/command.rs # Add multiband commands
crates/mesh-core/src/engine/engine.rs # Handle multiband commands
crates/mesh-player/Cargo.toml # Depend on mesh-widgets
crates/mesh-player/src/ui/deck_view.rs # Replace effect chain with multiband
crates/mesh-player/src/ui/app.rs # Route multiband messages
crates/mesh-player/src/ui/message.rs # Add multiband message variant
crates/mesh-player/src/domain/mod.rs # Add multiband domain methods
- User clicks
[Multiband ▾]on stem - MultibandEditor modal opens (default: 1 band)
- User clicks
[+]in band lane - Effect picker opens with unified list:
- PD effects (from
~/Music/mesh-collection/effects/) - CLAP plugins (from
~/.clap/,/usr/lib/clap/) - Native effects (built-in filters, etc.)
- Filter buttons: [All] [PD] [CLAP] [Native]
- PD effects (from
- User selects any effect type
- Effect card appears in band lane with 8 knobs (from
effect.info().params)
- User clicks
[+ Add Band]at bottom - New band lane appears
- Crossover divider appears at default frequency (e.g., midpoint)
- User drags divider to set crossover frequency
- User clicks
[Save Preset] - Name input dialog appears
- User enters name, clicks Save
- Preset saved to
~/Music/mesh-collection/presets/multiband/
- User clicks
[Load Preset ▾] - Dropdown/modal shows available presets
- User clicks preset name
- All bands, effects, and mappings restored
-
Crossover Implementation: Should we bundle a built-in crossover or use external plugin?
- Recommendation: Built-in Linkwitz-Riley crossover for reliability (no external dependency)
- Falls back gracefully if user wants to use LSP Crossover instead
-
Max Bands: Currently 8, is this sufficient?
- Recommendation: Keep at 8, matches pro tools like Multipass
-
Macro Mapping UI: How to map macros to parameters?
- Recommendation: Click macro → Click parameter on effect card → Mapped
-
Effect Reordering: Drag-and-drop within band?
- Recommendation: Phase 2 feature, not MVP
-
Mixed Effect Types: Can a band have both PD and CLAP effects?
- Recommendation: Yes! Each band is
Vec<Box<dyn Effect>>, effect type doesn't matter
- Recommendation: Yes! Each band is
| Phase | Description | Effort |
|---|---|---|
| 1 | Backend Enhancements | 2-3 hours |
| 2 | Widget Crate | 6-8 hours |
| 3 | Integration | 4-5 hours |
| 4 | Preset System | 2-3 hours |
| Total | 14-19 hours |
- User can open multiband editor for any stem
- User can add/remove frequency bands with draggable crossovers
- User can add PD or CLAP effects to any band
- User can adjust effect parameters via rotary knobs
- User can save/load multiband presets
- Single-band mode (default) behaves like current effect chain
- Widget is reusable for mesh-cue integration