Feat: tweak and fix from last night workshop
This commit is contained in:
@@ -10,6 +10,9 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Fixed
|
||||
- PatternProps and EuclideanDistribution modals now use the global theme background instead of the terminal default.
|
||||
- Changing pattern properties is now a stage/commit operation.
|
||||
- Changing pattern speed only happens at pattern boundaries.
|
||||
- `mlockall` warning no longer appears on macOS; memory locking is now Linux-only.
|
||||
|
||||
## [0.0.5] - Unreleased
|
||||
|
||||
|
||||
64
src/app.rs
64
src/app.rs
@@ -20,7 +20,7 @@ use crate::settings::Settings;
|
||||
use crate::state::{
|
||||
AudioSettings, CyclicEnum, DictFocus, EditorContext, FlashKind, LiveKeyState, Metrics, Modal,
|
||||
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||
PlaybackState, ProjectState, StagedChange, UiState,
|
||||
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
|
||||
};
|
||||
use crate::views::{dict_view, help_view};
|
||||
|
||||
@@ -499,13 +499,14 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Commits staged pattern and mute/solo changes.
|
||||
/// Commits staged pattern, mute/solo, and prop 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();
|
||||
let prop_count = self.playback.staged_prop_changes.len();
|
||||
|
||||
if pattern_count == 0 && mute_count == 0 {
|
||||
if pattern_count == 0 && mute_count == 0 && prop_count == 0 {
|
||||
self.ui.set_status("No changes to commit".to_string());
|
||||
return false;
|
||||
}
|
||||
@@ -530,11 +531,21 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
};
|
||||
// Apply staged prop changes
|
||||
for ((bank, pattern), props) in self.playback.staged_prop_changes.drain() {
|
||||
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
||||
pat.name = props.name;
|
||||
if let Some(len) = props.length {
|
||||
pat.set_length(len);
|
||||
}
|
||||
pat.speed = props.speed;
|
||||
pat.quantization = props.quantization;
|
||||
pat.sync_mode = props.sync_mode;
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
|
||||
let total = pattern_count + mute_count + prop_count;
|
||||
let status = format!("Committed {total} changes");
|
||||
self.ui.set_status(status);
|
||||
|
||||
mute_changed
|
||||
@@ -543,19 +554,18 @@ impl App {
|
||||
pub fn clear_staged_changes(&mut self) {
|
||||
let pattern_count = self.playback.staged_changes.len();
|
||||
let mute_count = self.playback.staged_mute_changes.len();
|
||||
let prop_count = self.playback.staged_prop_changes.len();
|
||||
|
||||
if pattern_count == 0 && mute_count == 0 {
|
||||
if pattern_count == 0 && mute_count == 0 && prop_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.playback.staged_changes.clear();
|
||||
self.playback.staged_mute_changes.clear();
|
||||
self.playback.staged_prop_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"),
|
||||
};
|
||||
let total = pattern_count + mute_count + prop_count;
|
||||
let status = format!("Cleared {total} staged changes");
|
||||
self.ui.set_status(status);
|
||||
}
|
||||
|
||||
@@ -1189,7 +1199,7 @@ impl App {
|
||||
AppCommand::OpenPatternPropsModal { bank, pattern } => {
|
||||
self.open_pattern_props_modal(bank, pattern);
|
||||
}
|
||||
AppCommand::SetPatternProps {
|
||||
AppCommand::StagePatternProps {
|
||||
bank,
|
||||
pattern,
|
||||
name,
|
||||
@@ -1198,15 +1208,21 @@ impl App {
|
||||
quantization,
|
||||
sync_mode,
|
||||
} => {
|
||||
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
||||
pat.name = name;
|
||||
if let Some(len) = length {
|
||||
pat.set_length(len);
|
||||
}
|
||||
pat.speed = speed;
|
||||
pat.quantization = quantization;
|
||||
pat.sync_mode = sync_mode;
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
self.playback.staged_prop_changes.insert(
|
||||
(bank, pattern),
|
||||
StagedPropChange {
|
||||
name,
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
},
|
||||
);
|
||||
self.ui.set_status(format!(
|
||||
"B{:02}:P{:02} props staged",
|
||||
bank + 1,
|
||||
pattern + 1
|
||||
));
|
||||
}
|
||||
|
||||
// Page navigation
|
||||
|
||||
@@ -106,7 +106,7 @@ pub enum AppCommand {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
},
|
||||
SetPatternProps {
|
||||
StagePatternProps {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
name: Option<String>,
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
#[cfg(target_os = "linux")]
|
||||
mod memory {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
static MLOCKALL_CALLED: AtomicBool = AtomicBool::new(false);
|
||||
static MLOCKALL_SUCCESS: AtomicBool = AtomicBool::new(false);
|
||||
static MLOCKALL_CALLED: AtomicBool = AtomicBool::new(false);
|
||||
static MLOCKALL_SUCCESS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Locks all current and future memory pages to prevent page faults during RT execution.
|
||||
/// Must be called BEFORE spawning any threads for maximum effectiveness.
|
||||
/// Returns true if mlockall succeeded, false otherwise (which is common without rtprio).
|
||||
#[cfg(unix)]
|
||||
pub fn lock_memory() -> bool {
|
||||
if MLOCKALL_CALLED.swap(true, Ordering::SeqCst) {
|
||||
return MLOCKALL_SUCCESS.load(Ordering::SeqCst);
|
||||
/// Locks all current and future memory pages to prevent page faults during RT execution.
|
||||
/// Must be called BEFORE spawning any threads for maximum effectiveness.
|
||||
pub fn lock_memory() -> bool {
|
||||
if MLOCKALL_CALLED.swap(true, Ordering::SeqCst) {
|
||||
return MLOCKALL_SUCCESS.load(Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let result = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) };
|
||||
|
||||
if result == 0 {
|
||||
MLOCKALL_SUCCESS.store(true, Ordering::SeqCst);
|
||||
true
|
||||
} else {
|
||||
let errno = std::io::Error::last_os_error();
|
||||
eprintln!("[cagire] mlockall failed: {errno}");
|
||||
eprintln!("[cagire] Memory locking disabled. For best RT performance on Linux:");
|
||||
eprintln!("[cagire] 1. Add user to 'audio' group: sudo usermod -aG audio $USER");
|
||||
eprintln!("[cagire] 2. Add to /etc/security/limits.conf:");
|
||||
eprintln!("[cagire] @audio - memlock unlimited");
|
||||
eprintln!("[cagire] 3. Log out and back in");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
let result = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) };
|
||||
|
||||
if result == 0 {
|
||||
MLOCKALL_SUCCESS.store(true, Ordering::SeqCst);
|
||||
true
|
||||
} else {
|
||||
// Get the actual error for better diagnostics
|
||||
let errno = std::io::Error::last_os_error();
|
||||
eprintln!("[cagire] mlockall failed: {errno}");
|
||||
eprintln!("[cagire] Memory locking disabled. For best RT performance on Linux:");
|
||||
eprintln!("[cagire] 1. Add user to 'audio' group: sudo usermod -aG audio $USER");
|
||||
eprintln!("[cagire] 2. Add to /etc/security/limits.conf:");
|
||||
eprintln!("[cagire] @audio - memlock unlimited");
|
||||
eprintln!("[cagire] 3. Log out and back in");
|
||||
false
|
||||
#[allow(dead_code)]
|
||||
pub fn is_memory_locked() -> bool {
|
||||
MLOCKALL_SUCCESS.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use memory::{is_memory_locked, lock_memory};
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn lock_memory() -> bool {
|
||||
// Windows: VirtualLock exists but isn't typically needed for audio
|
||||
true
|
||||
}
|
||||
|
||||
/// Check if memory locking is active.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[allow(dead_code)]
|
||||
pub fn is_memory_locked() -> bool {
|
||||
MLOCKALL_SUCCESS.load(Ordering::Relaxed)
|
||||
false
|
||||
}
|
||||
|
||||
/// Attempts to set realtime scheduling priority for the current thread.
|
||||
|
||||
@@ -531,6 +531,7 @@ impl KeyCache {
|
||||
pub(crate) struct SequencerState {
|
||||
audio_state: AudioState,
|
||||
pattern_cache: PatternCache,
|
||||
pending_updates: HashMap<(usize, usize), PatternSnapshot>,
|
||||
runs_counter: RunsCounter,
|
||||
step_traces: Arc<StepTracesMap>,
|
||||
event_count: usize,
|
||||
@@ -559,6 +560,7 @@ impl SequencerState {
|
||||
Self {
|
||||
audio_state: AudioState::new(),
|
||||
pattern_cache: PatternCache::new(),
|
||||
pending_updates: HashMap::new(),
|
||||
runs_counter: RunsCounter::new(),
|
||||
step_traces: Arc::new(HashMap::new()),
|
||||
event_count: 0,
|
||||
@@ -596,7 +598,14 @@ impl SequencerState {
|
||||
pattern,
|
||||
data,
|
||||
} => {
|
||||
self.pattern_cache.set(bank, pattern, data);
|
||||
let id = PatternId { bank, pattern };
|
||||
let is_active = self.audio_state.active_patterns.contains_key(&id);
|
||||
let has_cache = self.pattern_cache.get(bank, pattern).is_some();
|
||||
if is_active && has_cache {
|
||||
self.pending_updates.insert((bank, pattern), data);
|
||||
} else {
|
||||
self.pattern_cache.set(bank, pattern, data);
|
||||
}
|
||||
}
|
||||
SeqCommand::PatternStart {
|
||||
bank,
|
||||
@@ -652,6 +661,10 @@ impl SequencerState {
|
||||
}
|
||||
}
|
||||
SeqCommand::StopAll => {
|
||||
// Flush pending updates so cache stays current for future launches
|
||||
for ((bank, pattern), snapshot) in self.pending_updates.drain() {
|
||||
self.pattern_cache.set(bank, pattern, snapshot);
|
||||
}
|
||||
self.audio_state.active_patterns.clear();
|
||||
self.audio_state.pending_starts.clear();
|
||||
self.audio_state.pending_stops.clear();
|
||||
@@ -715,6 +728,10 @@ impl SequencerState {
|
||||
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||
bank != pending.id.bank || pattern != pending.id.pattern
|
||||
});
|
||||
let key = (pending.id.bank, pending.id.pattern);
|
||||
if let Some(snapshot) = self.pending_updates.remove(&key) {
|
||||
self.pattern_cache.set(key.0, key.1, snapshot);
|
||||
}
|
||||
}
|
||||
self.audio_state.pending_starts.clear();
|
||||
self.audio_state.prev_beat = -1.0;
|
||||
@@ -773,6 +790,11 @@ impl SequencerState {
|
||||
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||
bank != pending.id.bank || pattern != pending.id.pattern
|
||||
});
|
||||
// Flush pending update so cache stays current for future launches
|
||||
let key = (pending.id.bank, pending.id.pattern);
|
||||
if let Some(snapshot) = self.pending_updates.remove(&key) {
|
||||
self.pattern_cache.set(key.0, key.1, snapshot);
|
||||
}
|
||||
self.buf_stopped.push(pending.id);
|
||||
}
|
||||
}
|
||||
@@ -921,6 +943,14 @@ impl SequencerState {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply deferred updates for patterns that just completed an iteration
|
||||
for completed_id in &self.buf_completed_iterations {
|
||||
let key = (completed_id.bank, completed_id.pattern);
|
||||
if let Some(snapshot) = self.pending_updates.remove(&key) {
|
||||
self.pattern_cache.set(key.0, key.1, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1872,8 +1902,9 @@ mod tests {
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 0);
|
||||
|
||||
// Update pattern to length 2 while running — step_index wraps via modulo
|
||||
// beat=1.25: beat_int=5, prev=4, fires 1 step. step_index=0%2=0 fires, advances to 1
|
||||
// Update pattern to length 2 while running — deferred until iteration boundary
|
||||
// beat=1.25: update is deferred (pattern active), still length 4
|
||||
// step_index=0 fires, advances to 1
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
@@ -1883,10 +1914,21 @@ mod tests {
|
||||
1.25,
|
||||
));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 1); // still length 4
|
||||
|
||||
// Advance through remaining steps of original length-4 pattern
|
||||
state.tick(tick_at(1.5, true)); // step 1→2
|
||||
state.tick(tick_at(1.75, true)); // step 2→3
|
||||
state.tick(tick_at(2.0, true)); // step 3→wraps to 0, iteration completes, update applies
|
||||
|
||||
// Now length=2 is applied. Next tick uses new length.
|
||||
// beat=2.25: step 0 fires, advances to 1
|
||||
state.tick(tick_at(2.25, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 1);
|
||||
|
||||
// beat=1.5: beat_int=6, prev=5, step fires. step_index=1 fires, wraps to 0
|
||||
state.tick(tick_at(1.5, true));
|
||||
// beat=2.5: step 1 fires, wraps to 0 (length 2)
|
||||
state.tick(tick_at(2.5, true));
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
assert_eq!(ap.step_index, 0);
|
||||
}
|
||||
|
||||
@@ -615,7 +615,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let speed_val = *speed;
|
||||
let quant_val = *quantization;
|
||||
let sync_val = *sync_mode;
|
||||
ctx.dispatch(AppCommand::SetPatternProps {
|
||||
ctx.dispatch(AppCommand::StagePatternProps {
|
||||
bank,
|
||||
pattern,
|
||||
name: name_val,
|
||||
@@ -1096,6 +1096,7 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
KeyCode::Esc => {
|
||||
if !ctx.app.playback.staged_changes.is_empty()
|
||||
|| !ctx.app.playback.staged_mute_changes.is_empty()
|
||||
|| !ctx.app.playback.staged_prop_changes.is_empty()
|
||||
{
|
||||
ctx.dispatch(AppCommand::ClearStagedChanges);
|
||||
} else {
|
||||
|
||||
@@ -41,7 +41,7 @@ pub use options::{OptionsFocus, OptionsState};
|
||||
pub use panel::{PanelFocus, PanelState, SidePanel};
|
||||
pub use patterns_nav::{PatternsColumn, PatternsNav};
|
||||
pub use mute::MuteState;
|
||||
pub use playback::{PlaybackState, StagedChange, StagedMuteChange};
|
||||
pub use playback::{PlaybackState, StagedChange, StagedMuteChange, StagedPropChange};
|
||||
pub use project::ProjectState;
|
||||
pub use sample_browser::SampleBrowserState;
|
||||
pub use ui::{DictFocus, FlashKind, HelpFocus, UiState};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::engine::PatternChange;
|
||||
use crate::model::{LaunchQuantization, SyncMode};
|
||||
use std::collections::HashSet;
|
||||
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StagedChange {
|
||||
@@ -15,11 +15,20 @@ pub enum StagedMuteChange {
|
||||
ToggleSolo { bank: usize, pattern: usize },
|
||||
}
|
||||
|
||||
pub struct StagedPropChange {
|
||||
pub name: Option<String>,
|
||||
pub length: Option<usize>,
|
||||
pub speed: PatternSpeed,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
pub struct PlaybackState {
|
||||
pub playing: bool,
|
||||
pub staged_changes: Vec<StagedChange>,
|
||||
pub queued_changes: Vec<StagedChange>,
|
||||
pub staged_mute_changes: HashSet<StagedMuteChange>,
|
||||
pub staged_prop_changes: HashMap<(usize, usize), StagedPropChange>,
|
||||
}
|
||||
|
||||
impl Default for PlaybackState {
|
||||
@@ -29,6 +38,7 @@ impl Default for PlaybackState {
|
||||
staged_changes: Vec::new(),
|
||||
queued_changes: Vec::new(),
|
||||
staged_mute_changes: HashSet::new(),
|
||||
staged_prop_changes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +51,11 @@ impl PlaybackState {
|
||||
pub fn clear_queues(&mut self) {
|
||||
self.staged_changes.clear();
|
||||
self.queued_changes.clear();
|
||||
self.staged_prop_changes.clear();
|
||||
}
|
||||
|
||||
pub fn has_staged_props(&self, bank: usize, pattern: usize) -> bool {
|
||||
self.staged_prop_changes.contains_key(&(bank, pattern))
|
||||
}
|
||||
|
||||
pub fn stage_mute(&mut self, bank: usize, pattern: usize) {
|
||||
|
||||
@@ -285,6 +285,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
// 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);
|
||||
let has_staged_props = app.playback.has_staged_props(bank, idx);
|
||||
|
||||
// Preview state (what it will be after commit)
|
||||
let preview_muted = is_muted ^ has_staged_mute;
|
||||
@@ -386,13 +387,14 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
spans.push(Span::styled(format!(" {name}"), name_style));
|
||||
}
|
||||
|
||||
// Right-aligned info: length and speed
|
||||
// Right-aligned info: length, speed, and staged props indicator
|
||||
let speed_str = if speed != PatternSpeed::NORMAL {
|
||||
format!(" {}", speed.label())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let right_info = format!("{length}{speed_str}");
|
||||
let props_indicator = if has_staged_props { "~" } else { "" };
|
||||
let right_info = format!("{props_indicator}{length}{speed_str}");
|
||||
let left_width: usize = spans.iter().map(|s| s.content.chars().count()).sum();
|
||||
let right_width = right_info.chars().count();
|
||||
let padding = (text_area.width as usize).saturating_sub(left_width + right_width + 1);
|
||||
|
||||
Reference in New Issue
Block a user