Pattern mute and so on
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.0.5] - Unreleased
|
||||
|
||||
### Added
|
||||
- Mute/solo for patterns: stage with `m`/`x`, commit with `c`. Solo mutes all other patterns. Clear with `M`/`X`.
|
||||
|
||||
## [0.0.4] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
@@ -3,8 +3,17 @@ use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MuteStatus {
|
||||
Normal,
|
||||
Muted,
|
||||
Soloed,
|
||||
EffectivelyMuted, // Solo active on another pattern
|
||||
}
|
||||
|
||||
pub struct ActivePatterns<'a> {
|
||||
patterns: &'a [(usize, usize, usize)], // (bank, pattern, iter)
|
||||
mute_status: Option<&'a [MuteStatus]>,
|
||||
current_step: Option<(usize, usize)>, // (current_step, total_steps)
|
||||
}
|
||||
|
||||
@@ -12,6 +21,7 @@ impl<'a> ActivePatterns<'a> {
|
||||
pub fn new(patterns: &'a [(usize, usize, usize)]) -> Self {
|
||||
Self {
|
||||
patterns,
|
||||
mute_status: None,
|
||||
current_step: None,
|
||||
}
|
||||
}
|
||||
@@ -20,6 +30,11 @@ impl<'a> ActivePatterns<'a> {
|
||||
self.current_step = Some((current, total));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_mute_status(mut self, status: &'a [MuteStatus]) -> Self {
|
||||
self.mute_status = Some(status);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ActivePatterns<'_> {
|
||||
@@ -39,20 +54,36 @@ impl Widget for ActivePatterns<'_> {
|
||||
if row >= max_pattern_rows {
|
||||
break;
|
||||
}
|
||||
let text = format!("B{:02}:{:02} ({:02})", bank + 1, pattern + 1, iter.min(99));
|
||||
let y = area.y + row as u16;
|
||||
let bg = if row % 2 == 0 {
|
||||
theme.table.row_even
|
||||
} else {
|
||||
theme.table.row_odd
|
||||
|
||||
let mute_status = self
|
||||
.mute_status
|
||||
.and_then(|s| s.get(row))
|
||||
.copied()
|
||||
.unwrap_or(MuteStatus::Normal);
|
||||
|
||||
let (prefix, fg, bg) = match mute_status {
|
||||
MuteStatus::Soloed => ("S", theme.list.soloed_fg, theme.list.soloed_bg),
|
||||
MuteStatus::Muted => ("M", theme.list.muted_fg, theme.list.muted_bg),
|
||||
MuteStatus::EffectivelyMuted => (" ", theme.list.muted_fg, theme.list.muted_bg),
|
||||
MuteStatus::Normal => {
|
||||
let bg = if row % 2 == 0 {
|
||||
theme.table.row_even
|
||||
} else {
|
||||
theme.table.row_odd
|
||||
};
|
||||
(" ", theme.ui.text_primary, bg)
|
||||
}
|
||||
};
|
||||
|
||||
let text = format!("{}B{:02}:{:02}({:02})", prefix, bank + 1, pattern + 1, iter.min(99));
|
||||
let y = area.y + row as u16;
|
||||
|
||||
let mut chars = text.chars();
|
||||
for col in 0..area.width as usize {
|
||||
let ch = chars.next().unwrap_or(' ');
|
||||
buf[(area.x + col as u16, y)]
|
||||
.set_char(ch)
|
||||
.set_fg(theme.ui.text_primary)
|
||||
.set_fg(fg)
|
||||
.set_bg(bg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ mod text_input;
|
||||
pub mod theme;
|
||||
mod vu_meter;
|
||||
|
||||
pub use active_patterns::ActivePatterns;
|
||||
pub use active_patterns::{ActivePatterns, MuteStatus};
|
||||
pub use confirm::ConfirmModal;
|
||||
pub use editor::{CompletionCandidate, Editor};
|
||||
pub use file_browser::FileBrowserModal;
|
||||
|
||||
@@ -123,6 +123,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: teal,
|
||||
hover_bg: surface1,
|
||||
hover_fg: text,
|
||||
muted_bg: Color::Rgb(215, 215, 225),
|
||||
muted_fg: overlay0,
|
||||
soloed_bg: Color::Rgb(250, 235, 200),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
|
||||
@@ -123,6 +123,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: teal,
|
||||
hover_bg: surface1,
|
||||
hover_fg: text,
|
||||
muted_bg: Color::Rgb(40, 40, 50),
|
||||
muted_fg: overlay0,
|
||||
soloed_bg: Color::Rgb(60, 55, 35),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
|
||||
@@ -117,6 +117,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: cyan,
|
||||
hover_bg: lighter_bg,
|
||||
hover_fg: foreground,
|
||||
muted_bg: Color::Rgb(50, 52, 65),
|
||||
muted_fg: comment,
|
||||
soloed_bg: Color::Rgb(70, 70, 50),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
|
||||
@@ -118,6 +118,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: mint,
|
||||
hover_bg: bg_lighter,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(75, 70, 95),
|
||||
muted_fg: muted,
|
||||
soloed_bg: Color::Rgb(100, 95, 65),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: coral,
|
||||
|
||||
@@ -119,6 +119,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: aqua,
|
||||
hover_bg: bg2,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(50, 50, 55),
|
||||
muted_fg: fg4,
|
||||
soloed_bg: Color::Rgb(70, 65, 40),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
|
||||
@@ -114,6 +114,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: yellow,
|
||||
hover_bg: Color::Rgb(230, 50, 50),
|
||||
hover_fg: yellow,
|
||||
muted_bg: darker_red,
|
||||
muted_fg: dim_yellow,
|
||||
soloed_bg: Color::Rgb(200, 200, 0),
|
||||
soloed_fg: black,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: white,
|
||||
|
||||
@@ -119,6 +119,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: crystal_blue,
|
||||
hover_bg: bg_lighter,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(38, 38, 48),
|
||||
muted_fg: comment,
|
||||
soloed_bg: Color::Rgb(60, 55, 45),
|
||||
soloed_fg: carp_yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: autumn_red,
|
||||
|
||||
@@ -187,6 +187,10 @@ pub struct ListColors {
|
||||
pub edit_fg: Color,
|
||||
pub hover_bg: Color,
|
||||
pub hover_fg: Color,
|
||||
pub muted_bg: Color,
|
||||
pub muted_fg: Color,
|
||||
pub soloed_bg: Color,
|
||||
pub soloed_fg: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -116,6 +116,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: bright,
|
||||
hover_bg: surface2,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(22, 22, 22),
|
||||
muted_fg: dark,
|
||||
soloed_bg: Color::Rgb(60, 60, 60),
|
||||
soloed_fg: bright,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: dim,
|
||||
|
||||
@@ -116,6 +116,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: dark,
|
||||
hover_bg: surface2,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(235, 235, 235),
|
||||
muted_fg: light,
|
||||
soloed_bg: Color::Rgb(190, 190, 190),
|
||||
soloed_fg: dark,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: dim,
|
||||
|
||||
@@ -117,6 +117,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: blue,
|
||||
hover_bg: bg_lighter,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(48, 50, 45),
|
||||
muted_fg: comment,
|
||||
soloed_bg: Color::Rgb(70, 65, 45),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: pink,
|
||||
|
||||
@@ -117,6 +117,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: frost0,
|
||||
hover_bg: polar_night2,
|
||||
hover_fg: snow_storm2,
|
||||
muted_bg: Color::Rgb(55, 60, 70),
|
||||
muted_fg: polar_night3,
|
||||
soloed_bg: Color::Rgb(70, 65, 50),
|
||||
soloed_fg: aurora_yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: aurora_red,
|
||||
|
||||
@@ -118,6 +118,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: cyan,
|
||||
hover_bg: surface2,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(15, 15, 15),
|
||||
muted_fg: fg_muted,
|
||||
soloed_bg: Color::Rgb(45, 40, 15),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
|
||||
@@ -118,6 +118,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: foam,
|
||||
hover_bg: bg_lighter,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(32, 30, 42),
|
||||
muted_fg: muted,
|
||||
soloed_bg: Color::Rgb(60, 50, 40),
|
||||
soloed_fg: gold,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: love,
|
||||
|
||||
@@ -118,6 +118,10 @@ pub fn theme() -> ThemeColors {
|
||||
edit_fg: blue,
|
||||
hover_bg: bg_lighter,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(35, 38, 50),
|
||||
muted_fg: comment,
|
||||
soloed_bg: Color::Rgb(60, 55, 40),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
|
||||
@@ -183,6 +183,10 @@ pub fn rotate_theme(theme: ThemeColors, degrees: f32) -> ThemeColors {
|
||||
edit_fg: rotate_color(theme.list.edit_fg, degrees),
|
||||
hover_bg: rotate_color(theme.list.hover_bg, degrees),
|
||||
hover_fg: rotate_color(theme.list.hover_fg, degrees),
|
||||
muted_bg: rotate_color(theme.list.muted_bg, degrees),
|
||||
muted_fg: rotate_color(theme.list.muted_fg, degrees),
|
||||
soloed_bg: rotate_color(theme.list.soloed_bg, degrees),
|
||||
soloed_fg: rotate_color(theme.list.soloed_fg, degrees),
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: rotate_color(theme.link_status.disabled, degrees),
|
||||
|
||||
94
src/app.rs
94
src/app.rs
@@ -17,8 +17,8 @@ use crate::services::pattern_editor;
|
||||
use crate::settings::Settings;
|
||||
use crate::state::{
|
||||
AudioSettings, CyclicEnum, DictFocus, EditorContext, FlashKind, LiveKeyState, Metrics, Modal,
|
||||
OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
|
||||
ProjectState, StagedChange, UiState,
|
||||
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||
PlaybackState, ProjectState, StagedChange, UiState,
|
||||
};
|
||||
use crate::views::{dict_view, help_view};
|
||||
|
||||
@@ -28,6 +28,7 @@ pub struct App {
|
||||
pub project_state: ProjectState,
|
||||
pub ui: UiState,
|
||||
pub playback: PlaybackState,
|
||||
pub mute: MuteState,
|
||||
|
||||
pub page: Page,
|
||||
pub editor_ctx: EditorContext,
|
||||
@@ -69,6 +70,7 @@ impl App {
|
||||
project_state: ProjectState::default(),
|
||||
ui: UiState::default(),
|
||||
playback: PlaybackState::default(),
|
||||
mute: MuteState::default(),
|
||||
|
||||
page: Page::default(),
|
||||
editor_ctx: EditorContext::default(),
|
||||
@@ -494,26 +496,64 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commit_staged_changes(&mut self) {
|
||||
if self.playback.staged_changes.is_empty() {
|
||||
/// Commits staged pattern and mute/solo changes.
|
||||
/// Returns true if mute state changed (caller should send to sequencer).
|
||||
pub fn commit_staged_changes(&mut self) -> bool {
|
||||
let pattern_count = self.playback.staged_changes.len();
|
||||
let mute_count = self.playback.staged_mute_changes.len();
|
||||
|
||||
if pattern_count == 0 && mute_count == 0 {
|
||||
self.ui.set_status("No changes to commit".to_string());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
let count = self.playback.staged_changes.len();
|
||||
self.playback
|
||||
.queued_changes
|
||||
.append(&mut self.playback.staged_changes);
|
||||
self.ui.set_status(format!("Committed {count} changes"));
|
||||
|
||||
// Commit pattern changes (queued for quantization)
|
||||
if pattern_count > 0 {
|
||||
self.playback
|
||||
.queued_changes
|
||||
.append(&mut self.playback.staged_changes);
|
||||
}
|
||||
|
||||
// Apply mute/solo changes immediately
|
||||
let mute_changed = mute_count > 0;
|
||||
for change in self.playback.staged_mute_changes.drain() {
|
||||
match change {
|
||||
crate::state::StagedMuteChange::ToggleMute { bank, pattern } => {
|
||||
self.mute.toggle_mute(bank, pattern);
|
||||
}
|
||||
crate::state::StagedMuteChange::ToggleSolo { bank, pattern } => {
|
||||
self.mute.toggle_solo(bank, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status = match (pattern_count, mute_count) {
|
||||
(0, m) => format!("Applied {m} mute/solo changes"),
|
||||
(p, 0) => format!("Committed {p} pattern changes"),
|
||||
(p, m) => format!("Committed {p} pattern + {m} mute/solo changes"),
|
||||
};
|
||||
self.ui.set_status(status);
|
||||
|
||||
mute_changed
|
||||
}
|
||||
|
||||
pub fn clear_staged_changes(&mut self) {
|
||||
if self.playback.staged_changes.is_empty() {
|
||||
let pattern_count = self.playback.staged_changes.len();
|
||||
let mute_count = self.playback.staged_mute_changes.len();
|
||||
|
||||
if pattern_count == 0 && mute_count == 0 {
|
||||
return;
|
||||
}
|
||||
let count = self.playback.staged_changes.len();
|
||||
|
||||
self.playback.staged_changes.clear();
|
||||
self.ui
|
||||
.set_status(format!("Cleared {count} staged changes"));
|
||||
self.playback.staged_mute_changes.clear();
|
||||
|
||||
let status = match (pattern_count, mute_count) {
|
||||
(0, m) => format!("Cleared {m} staged mute/solo"),
|
||||
(p, 0) => format!("Cleared {p} staged patterns"),
|
||||
(p, m) => format!("Cleared {p} patterns + {m} mute/solo"),
|
||||
};
|
||||
self.ui.set_status(status);
|
||||
}
|
||||
|
||||
pub fn select_edit_pattern(&mut self, pattern: usize) {
|
||||
@@ -1094,9 +1134,6 @@ impl App {
|
||||
AppCommand::DuplicateSteps => self.duplicate_steps(link),
|
||||
|
||||
// Pattern playback (staging)
|
||||
AppCommand::CommitStagedChanges => {
|
||||
self.commit_staged_changes();
|
||||
}
|
||||
AppCommand::ClearStagedChanges => {
|
||||
self.clear_staged_changes();
|
||||
}
|
||||
@@ -1300,6 +1337,22 @@ impl App {
|
||||
self.stage_pattern_toggle(bank, pattern, snapshot);
|
||||
}
|
||||
|
||||
// Mute/Solo (staged)
|
||||
AppCommand::StageMute { bank, pattern } => {
|
||||
self.playback.stage_mute(bank, pattern);
|
||||
}
|
||||
AppCommand::StageSolo { bank, pattern } => {
|
||||
self.playback.stage_solo(bank, pattern);
|
||||
}
|
||||
AppCommand::ClearMutes => {
|
||||
self.playback.clear_staged_mutes();
|
||||
self.mute.clear_mute();
|
||||
}
|
||||
AppCommand::ClearSolos => {
|
||||
self.playback.clear_staged_solos();
|
||||
self.mute.clear_solo();
|
||||
}
|
||||
|
||||
// UI state
|
||||
AppCommand::ClearMinimap => {
|
||||
self.ui.minimap_until = None;
|
||||
@@ -1522,6 +1575,13 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_mute_state(&self, cmd_tx: &Sender<SeqCommand>) {
|
||||
let _ = cmd_tx.send(SeqCommand::SetMuteState {
|
||||
muted: self.mute.muted.clone(),
|
||||
soloed: self.mute.soloed.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender<SeqCommand>) {
|
||||
for (bank, pattern) in self.project_state.take_dirty() {
|
||||
let pat = self.project_state.project.pattern_at(bank, pattern);
|
||||
|
||||
@@ -75,7 +75,6 @@ pub enum AppCommand {
|
||||
DuplicateSteps,
|
||||
|
||||
// Pattern playback (staging)
|
||||
CommitStagedChanges,
|
||||
ClearStagedChanges,
|
||||
|
||||
// Project
|
||||
@@ -156,6 +155,12 @@ pub enum AppCommand {
|
||||
PatternsBack,
|
||||
PatternsTogglePlay,
|
||||
|
||||
// Mute/Solo (staged)
|
||||
StageMute { bank: usize, pattern: usize },
|
||||
StageSolo { bank: usize, pattern: usize },
|
||||
ClearMutes, // Clears both staged and applied mutes
|
||||
ClearSolos, // Clears both staged and applied solos
|
||||
|
||||
// UI state
|
||||
ClearMinimap,
|
||||
HideTitle,
|
||||
|
||||
@@ -115,6 +115,10 @@ pub enum SeqCommand {
|
||||
pattern: usize,
|
||||
quantization: LaunchQuantization,
|
||||
},
|
||||
SetMuteState {
|
||||
muted: std::collections::HashSet<(usize, usize)>,
|
||||
soloed: std::collections::HashSet<(usize, usize)>,
|
||||
},
|
||||
StopAll,
|
||||
Shutdown,
|
||||
}
|
||||
@@ -551,6 +555,8 @@ pub(crate) struct SequencerState {
|
||||
buf_audio_commands: Vec<TimestampedCommand>,
|
||||
cc_access: Option<Arc<dyn CcAccess>>,
|
||||
active_notes: HashMap<(u8, u8, u8), ActiveNote>,
|
||||
muted: std::collections::HashSet<(usize, usize)>,
|
||||
soloed: std::collections::HashSet<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl SequencerState {
|
||||
@@ -575,9 +581,22 @@ impl SequencerState {
|
||||
buf_audio_commands: Vec::new(),
|
||||
cc_access,
|
||||
active_notes: HashMap::new(),
|
||||
muted: std::collections::HashSet::new(),
|
||||
soloed: std::collections::HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_effectively_muted(&self, bank: usize, pattern: usize) -> bool {
|
||||
let key = (bank, pattern);
|
||||
if self.muted.contains(&key) {
|
||||
return true;
|
||||
}
|
||||
if !self.soloed.is_empty() && !self.soloed.contains(&key) {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn process_commands(&mut self, commands: Vec<SeqCommand>) {
|
||||
for cmd in commands {
|
||||
match cmd {
|
||||
@@ -619,6 +638,28 @@ impl SequencerState {
|
||||
});
|
||||
}
|
||||
}
|
||||
SeqCommand::SetMuteState { muted, soloed } => {
|
||||
let newly_muted: Vec<(usize, usize)> = self
|
||||
.audio_state
|
||||
.active_patterns
|
||||
.keys()
|
||||
.filter(|id| {
|
||||
let key = (id.bank, id.pattern);
|
||||
let was_muted = self.is_effectively_muted(id.bank, id.pattern);
|
||||
let now_muted = muted.contains(&key)
|
||||
|| (!soloed.is_empty() && !soloed.contains(&key));
|
||||
!was_muted && now_muted
|
||||
})
|
||||
.map(|id| (id.bank, id.pattern))
|
||||
.collect();
|
||||
|
||||
self.muted = muted;
|
||||
self.soloed = soloed;
|
||||
|
||||
if !newly_muted.is_empty() {
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
}
|
||||
}
|
||||
SeqCommand::StopAll => {
|
||||
self.audio_state.active_patterns.clear();
|
||||
self.audio_state.pending_starts.clear();
|
||||
@@ -783,6 +824,9 @@ impl SequencerState {
|
||||
}
|
||||
}
|
||||
|
||||
let muted_snapshot = self.muted.clone();
|
||||
let soloed_snapshot = self.soloed.clone();
|
||||
|
||||
for (_id, active) in self.audio_state.active_patterns.iter_mut() {
|
||||
let Some(pattern) = self.pattern_cache.get(active.bank, active.pattern) else {
|
||||
continue;
|
||||
@@ -807,6 +851,10 @@ impl SequencerState {
|
||||
.unwrap_or(false);
|
||||
|
||||
if step.active && has_script {
|
||||
let key = (active.bank, active.pattern);
|
||||
let is_muted = muted_snapshot.contains(&key)
|
||||
|| (!soloed_snapshot.is_empty() && !soloed_snapshot.contains(&key));
|
||||
|
||||
let source_idx = pattern.resolve_source(step_idx);
|
||||
let runs = self.runs_counter.get_and_increment(
|
||||
active.bank,
|
||||
@@ -845,18 +893,20 @@ impl SequencerState {
|
||||
std::mem::take(&mut trace),
|
||||
);
|
||||
|
||||
let event_time = if lookahead_secs > 0.0 {
|
||||
Some(engine_time + lookahead_secs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if !is_muted {
|
||||
let event_time = if lookahead_secs > 0.0 {
|
||||
Some(engine_time + lookahead_secs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for cmd in cmds {
|
||||
self.event_count += 1;
|
||||
self.buf_audio_commands.push(TimestampedCommand {
|
||||
cmd,
|
||||
time: event_time,
|
||||
});
|
||||
for cmd in cmds {
|
||||
self.event_count += 1;
|
||||
self.buf_audio_commands.push(TimestampedCommand {
|
||||
cmd,
|
||||
time: event_time,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
src/input.rs
45
src/input.rs
@@ -1054,6 +1054,22 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
||||
rotation: "0".to_string(),
|
||||
}));
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
ctx.dispatch(AppCommand::StageMute { bank, pattern });
|
||||
}
|
||||
KeyCode::Char('x') => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
|
||||
}
|
||||
KeyCode::Char('M') => {
|
||||
ctx.dispatch(AppCommand::ClearMutes);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('X') => {
|
||||
ctx.dispatch(AppCommand::ClearSolos);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
@@ -1070,7 +1086,9 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
KeyCode::Up => ctx.dispatch(AppCommand::PatternsCursorUp),
|
||||
KeyCode::Down => ctx.dispatch(AppCommand::PatternsCursorDown),
|
||||
KeyCode::Esc => {
|
||||
if !ctx.app.playback.staged_changes.is_empty() {
|
||||
if !ctx.app.playback.staged_changes.is_empty()
|
||||
|| !ctx.app.playback.staged_mute_changes.is_empty()
|
||||
{
|
||||
ctx.dispatch(AppCommand::ClearStagedChanges);
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::PatternsBack);
|
||||
@@ -1082,7 +1100,12 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
ctx.dispatch(AppCommand::PatternsTogglePlay);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('c') if !ctrl => ctx.dispatch(AppCommand::CommitStagedChanges),
|
||||
KeyCode::Char('c') if !ctrl => {
|
||||
let mute_changed = ctx.app.commit_staged_changes();
|
||||
if mute_changed {
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||
selected: false,
|
||||
@@ -1165,6 +1188,24 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
ctx.dispatch(AppCommand::OpenPatternPropsModal { bank, pattern });
|
||||
}
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
ctx.dispatch(AppCommand::StageMute { bank, pattern });
|
||||
}
|
||||
KeyCode::Char('x') => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
|
||||
}
|
||||
KeyCode::Char('M') => {
|
||||
ctx.dispatch(AppCommand::ClearMutes);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('X') => {
|
||||
ctx.dispatch(AppCommand::ClearSolos);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ pub mod editor;
|
||||
pub mod file_browser;
|
||||
pub mod live_keys;
|
||||
pub mod modal;
|
||||
pub mod mute;
|
||||
pub mod options;
|
||||
pub mod panel;
|
||||
pub mod patterns_nav;
|
||||
@@ -38,7 +39,8 @@ pub use modal::Modal;
|
||||
pub use options::{OptionsFocus, OptionsState};
|
||||
pub use panel::{PanelFocus, PanelState, SidePanel};
|
||||
pub use patterns_nav::{PatternsColumn, PatternsNav};
|
||||
pub use playback::{PlaybackState, StagedChange};
|
||||
pub use mute::MuteState;
|
||||
pub use playback::{PlaybackState, StagedChange, StagedMuteChange};
|
||||
pub use project::ProjectState;
|
||||
pub use sample_browser::SampleBrowserState;
|
||||
pub use ui::{DictFocus, FlashKind, HelpFocus, UiState};
|
||||
|
||||
53
src/state/mute.rs
Normal file
53
src/state/mute.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MuteState {
|
||||
pub muted: HashSet<(usize, usize)>,
|
||||
pub soloed: HashSet<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl MuteState {
|
||||
pub fn toggle_mute(&mut self, bank: usize, pattern: usize) {
|
||||
let key = (bank, pattern);
|
||||
if self.muted.contains(&key) {
|
||||
self.muted.remove(&key);
|
||||
} else {
|
||||
self.muted.insert(key);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_solo(&mut self, bank: usize, pattern: usize) {
|
||||
let key = (bank, pattern);
|
||||
if self.soloed.contains(&key) {
|
||||
self.soloed.remove(&key);
|
||||
} else {
|
||||
self.soloed.insert(key);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_mute(&mut self) {
|
||||
self.muted.clear();
|
||||
}
|
||||
|
||||
pub fn clear_solo(&mut self) {
|
||||
self.soloed.clear();
|
||||
}
|
||||
|
||||
pub fn is_muted(&self, bank: usize, pattern: usize) -> bool {
|
||||
self.muted.contains(&(bank, pattern))
|
||||
}
|
||||
|
||||
pub fn is_soloed(&self, bank: usize, pattern: usize) -> bool {
|
||||
self.soloed.contains(&(bank, pattern))
|
||||
}
|
||||
|
||||
pub fn is_effectively_muted(&self, bank: usize, pattern: usize) -> bool {
|
||||
if self.muted.contains(&(bank, pattern)) {
|
||||
return true;
|
||||
}
|
||||
if !self.soloed.is_empty() && !self.soloed.contains(&(bank, pattern)) {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::engine::PatternChange;
|
||||
use crate::model::{LaunchQuantization, SyncMode};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StagedChange {
|
||||
@@ -8,10 +9,17 @@ pub struct StagedChange {
|
||||
pub sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum StagedMuteChange {
|
||||
ToggleMute { bank: usize, pattern: usize },
|
||||
ToggleSolo { bank: usize, pattern: usize },
|
||||
}
|
||||
|
||||
pub struct PlaybackState {
|
||||
pub playing: bool,
|
||||
pub staged_changes: Vec<StagedChange>,
|
||||
pub queued_changes: Vec<StagedChange>,
|
||||
pub staged_mute_changes: HashSet<StagedMuteChange>,
|
||||
}
|
||||
|
||||
impl Default for PlaybackState {
|
||||
@@ -20,6 +28,7 @@ impl Default for PlaybackState {
|
||||
playing: true,
|
||||
staged_changes: Vec::new(),
|
||||
queued_changes: Vec::new(),
|
||||
staged_mute_changes: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,4 +42,38 @@ impl PlaybackState {
|
||||
self.staged_changes.clear();
|
||||
self.queued_changes.clear();
|
||||
}
|
||||
|
||||
pub fn stage_mute(&mut self, bank: usize, pattern: usize) {
|
||||
let change = StagedMuteChange::ToggleMute { bank, pattern };
|
||||
if self.staged_mute_changes.contains(&change) {
|
||||
self.staged_mute_changes.remove(&change);
|
||||
} else {
|
||||
self.staged_mute_changes.insert(change);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage_solo(&mut self, bank: usize, pattern: usize) {
|
||||
let change = StagedMuteChange::ToggleSolo { bank, pattern };
|
||||
if self.staged_mute_changes.contains(&change) {
|
||||
self.staged_mute_changes.remove(&change);
|
||||
} else {
|
||||
self.staged_mute_changes.insert(change);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_staged_mutes(&mut self) {
|
||||
self.staged_mute_changes.retain(|c| !matches!(c, StagedMuteChange::ToggleMute { .. }));
|
||||
}
|
||||
|
||||
pub fn clear_staged_solos(&mut self) {
|
||||
self.staged_mute_changes.retain(|c| !matches!(c, StagedMuteChange::ToggleSolo { .. }));
|
||||
}
|
||||
|
||||
pub fn has_staged_mute(&self, bank: usize, pattern: usize) -> bool {
|
||||
self.staged_mute_changes.contains(&StagedMuteChange::ToggleMute { bank, pattern })
|
||||
}
|
||||
|
||||
pub fn has_staged_solo(&self, bank: usize, pattern: usize) -> bool {
|
||||
self.staged_mute_changes.contains(&StagedMuteChange::ToggleSolo { bank, pattern })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +323,8 @@ fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
|
||||
}
|
||||
|
||||
fn render_active_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||
use crate::widgets::MuteStatus;
|
||||
|
||||
let theme = theme::get();
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
@@ -336,11 +338,27 @@ fn render_active_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnap
|
||||
.map(|p| (p.bank, p.pattern, p.iter))
|
||||
.collect();
|
||||
|
||||
let mute_status: Vec<MuteStatus> = snapshot
|
||||
.active_patterns
|
||||
.iter()
|
||||
.map(|p| {
|
||||
if app.mute.is_soloed(p.bank, p.pattern) {
|
||||
MuteStatus::Soloed
|
||||
} else if app.mute.is_muted(p.bank, p.pattern) {
|
||||
MuteStatus::Muted
|
||||
} else if app.mute.is_effectively_muted(p.bank, p.pattern) {
|
||||
MuteStatus::EffectivelyMuted
|
||||
} else {
|
||||
MuteStatus::Normal
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let step_info = snapshot
|
||||
.get_step(app.editor_ctx.bank, app.editor_ctx.pattern)
|
||||
.map(|step| (step, app.current_edit_pattern().length));
|
||||
|
||||
let mut widget = ActivePatterns::new(&patterns);
|
||||
let mut widget = ActivePatterns::new(&patterns).with_mute_status(&mute_status);
|
||||
if let Some((step, total)) = step_info {
|
||||
widget = widget.with_step(step, total);
|
||||
}
|
||||
|
||||
@@ -91,13 +91,41 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
|
||||
let is_playing = banks_with_playback.contains(&idx);
|
||||
let is_staged = banks_with_staged.contains(&idx);
|
||||
|
||||
let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged) {
|
||||
(true, _, _) => (theme.selection.cursor, theme.selection.cursor_fg, ""),
|
||||
(false, true, _) => (theme.list.playing_bg, theme.list.playing_fg, "> "),
|
||||
(false, false, true) => (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ "),
|
||||
(false, false, false) if is_selected => (theme.list.hover_bg, theme.list.hover_fg, ""),
|
||||
(false, false, false) if is_edit => (theme.list.edit_bg, theme.list.edit_fg, ""),
|
||||
(false, false, false) => (theme.ui.bg, theme.ui.text_muted, ""),
|
||||
// Check if any pattern in this bank is muted/soloed (applied)
|
||||
let has_muted = (0..MAX_PATTERNS).any(|p| app.mute.is_muted(idx, p));
|
||||
let has_soloed = (0..MAX_PATTERNS).any(|p| app.mute.is_soloed(idx, p));
|
||||
|
||||
// Check if any pattern in this bank has staged mute/solo
|
||||
let has_staged_mute = (0..MAX_PATTERNS).any(|p| app.playback.has_staged_mute(idx, p));
|
||||
let has_staged_solo = (0..MAX_PATTERNS).any(|p| app.playback.has_staged_solo(idx, p));
|
||||
let has_staged_mute_solo = has_staged_mute || has_staged_solo;
|
||||
|
||||
let (bg, fg, prefix) = if is_cursor {
|
||||
(theme.selection.cursor, theme.selection.cursor_fg, "")
|
||||
} else if is_playing {
|
||||
if has_staged_mute_solo {
|
||||
(theme.list.staged_play_bg, theme.list.staged_play_fg, ">*")
|
||||
} else if has_soloed {
|
||||
(theme.list.soloed_bg, theme.list.soloed_fg, ">S")
|
||||
} else if has_muted {
|
||||
(theme.list.muted_bg, theme.list.muted_fg, ">M")
|
||||
} else {
|
||||
(theme.list.playing_bg, theme.list.playing_fg, "> ")
|
||||
}
|
||||
} else if is_staged {
|
||||
(theme.list.staged_play_bg, theme.list.staged_play_fg, "+ ")
|
||||
} else if has_staged_mute_solo {
|
||||
(theme.list.staged_play_bg, theme.list.staged_play_fg, " *")
|
||||
} else if has_soloed && is_selected {
|
||||
(theme.list.soloed_bg, theme.list.soloed_fg, " S")
|
||||
} else if has_muted && is_selected {
|
||||
(theme.list.muted_bg, theme.list.muted_fg, " M")
|
||||
} else if is_selected {
|
||||
(theme.list.hover_bg, theme.list.hover_fg, "")
|
||||
} else if is_edit {
|
||||
(theme.list.edit_bg, theme.list.edit_fg, "")
|
||||
} else {
|
||||
(theme.ui.bg, theme.ui.text_muted, "")
|
||||
};
|
||||
|
||||
let name = app.project_state.project.banks[idx]
|
||||
@@ -250,14 +278,74 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
let is_staged_play = staged_to_play.contains(&idx);
|
||||
let is_staged_stop = staged_to_stop.contains(&idx);
|
||||
|
||||
let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged_play, is_staged_stop) {
|
||||
(true, _, _, _) => (theme.selection.cursor, theme.selection.cursor_fg, ""),
|
||||
(false, true, _, true) => (theme.list.staged_stop_bg, theme.list.staged_stop_fg, "- "),
|
||||
(false, true, _, false) => (theme.list.playing_bg, theme.list.playing_fg, "> "),
|
||||
(false, false, true, _) => (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ "),
|
||||
(false, false, false, _) if is_selected => (theme.list.hover_bg, theme.list.hover_fg, ""),
|
||||
(false, false, false, _) if is_edit => (theme.list.edit_bg, theme.list.edit_fg, ""),
|
||||
(false, false, false, _) => (theme.ui.bg, theme.ui.text_muted, ""),
|
||||
// Current applied mute/solo state
|
||||
let is_muted = app.mute.is_muted(bank, idx);
|
||||
let is_soloed = app.mute.is_soloed(bank, idx);
|
||||
|
||||
// Staged mute/solo (will toggle on commit)
|
||||
let has_staged_mute = app.playback.has_staged_mute(bank, idx);
|
||||
let has_staged_solo = app.playback.has_staged_solo(bank, idx);
|
||||
|
||||
// Preview state (what it will be after commit)
|
||||
let preview_muted = is_muted ^ has_staged_mute;
|
||||
let preview_soloed = is_soloed ^ has_staged_solo;
|
||||
let is_effectively_muted = app.mute.is_effectively_muted(bank, idx);
|
||||
|
||||
let (bg, fg, prefix) = if is_cursor {
|
||||
(theme.selection.cursor, theme.selection.cursor_fg, "")
|
||||
} else if is_playing {
|
||||
// Playing patterns
|
||||
if is_staged_stop {
|
||||
(theme.list.staged_stop_bg, theme.list.staged_stop_fg, "- ")
|
||||
} else if has_staged_solo {
|
||||
// Staged solo toggle on playing pattern
|
||||
if preview_soloed {
|
||||
(theme.list.soloed_bg, theme.list.soloed_fg, "+S")
|
||||
} else {
|
||||
(theme.list.playing_bg, theme.list.playing_fg, "-S")
|
||||
}
|
||||
} else if has_staged_mute {
|
||||
// Staged mute toggle on playing pattern
|
||||
if preview_muted {
|
||||
(theme.list.muted_bg, theme.list.muted_fg, "+M")
|
||||
} else {
|
||||
(theme.list.playing_bg, theme.list.playing_fg, "-M")
|
||||
}
|
||||
} else if is_soloed {
|
||||
(theme.list.soloed_bg, theme.list.soloed_fg, ">S")
|
||||
} else if is_muted {
|
||||
(theme.list.muted_bg, theme.list.muted_fg, ">M")
|
||||
} else if is_effectively_muted {
|
||||
(theme.list.muted_bg, theme.list.muted_fg, "> ")
|
||||
} else {
|
||||
(theme.list.playing_bg, theme.list.playing_fg, "> ")
|
||||
}
|
||||
} else if is_staged_play {
|
||||
(theme.list.staged_play_bg, theme.list.staged_play_fg, "+ ")
|
||||
} else if has_staged_solo {
|
||||
// Staged solo on non-playing pattern
|
||||
if preview_soloed {
|
||||
(theme.list.soloed_bg, theme.list.soloed_fg, "+S")
|
||||
} else {
|
||||
(theme.ui.bg, theme.ui.text_muted, "-S")
|
||||
}
|
||||
} else if has_staged_mute {
|
||||
// Staged mute on non-playing pattern
|
||||
if preview_muted {
|
||||
(theme.list.muted_bg, theme.list.muted_fg, "+M")
|
||||
} else {
|
||||
(theme.ui.bg, theme.ui.text_muted, "-M")
|
||||
}
|
||||
} else if is_soloed {
|
||||
(theme.list.soloed_bg, theme.list.soloed_fg, " S")
|
||||
} else if is_muted {
|
||||
(theme.list.muted_bg, theme.list.muted_fg, " M")
|
||||
} else if is_selected {
|
||||
(theme.list.hover_bg, theme.list.hover_fg, "")
|
||||
} else if is_edit {
|
||||
(theme.list.edit_bg, theme.list.edit_fg, "")
|
||||
} else {
|
||||
(theme.ui.bg, theme.ui.text_muted, "")
|
||||
};
|
||||
|
||||
let pattern = &app.project_state.project.banks[bank].patterns[idx];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub use cagire_ratatui::{
|
||||
ActivePatterns, ConfirmModal, FileBrowserModal, ModalFrame, NavMinimap, NavTile, Orientation,
|
||||
SampleBrowser, Scope, Spectrum, TextInputModal, VuMeter,
|
||||
ActivePatterns, ConfirmModal, FileBrowserModal, ModalFrame, MuteStatus, NavMinimap, NavTile,
|
||||
Orientation, SampleBrowser, Scope, Spectrum, TextInputModal, VuMeter,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user