more fixes
All checks were successful
Deploy Website / deploy (push) Has been skipped

This commit is contained in:
2026-03-01 03:33:22 +01:00
parent b72c782b2b
commit 11cc925faf
24 changed files with 269 additions and 189 deletions

View File

@@ -315,7 +315,7 @@ impl Forth {
Op::Dup => { Op::Dup => {
ensure(stack, 1)?; ensure(stack, 1)?;
let v = stack.last().unwrap().clone(); let v = stack.last().expect("stack non-empty after ensure").clone();
stack.push(v); stack.push(v);
} }
Op::Dupn => { Op::Dupn => {
@@ -560,7 +560,7 @@ impl Forth {
ensure(stack, 1)?; ensure(stack, 1)?;
let values = std::mem::take(stack); let values = std::mem::take(stack);
let val = if values.len() == 1 { let val = if values.len() == 1 {
values.into_iter().next().unwrap() values.into_iter().next().expect("single value after len check")
} else { } else {
Value::CycleList(Arc::from(values)) Value::CycleList(Arc::from(values))
}; };
@@ -570,7 +570,7 @@ impl Forth {
ensure(stack, 1)?; ensure(stack, 1)?;
let values = std::mem::take(stack); let values = std::mem::take(stack);
let val = if values.len() == 1 { let val = if values.len() == 1 {
values.into_iter().next().unwrap() values.into_iter().next().expect("single value after len check")
} else { } else {
Value::CycleList(Arc::from(values)) Value::CycleList(Arc::from(values))
}; };
@@ -1804,8 +1804,8 @@ fn euclidean_rhythm(k: usize, n: usize, rotation: usize) -> Vec<i64> {
groups.into_iter().partition(|g| g[0]); groups.into_iter().partition(|g| g[0]);
for _ in 0..min_count { for _ in 0..min_count {
let mut one = ones.pop().unwrap(); let mut one = ones.pop().expect("ones sufficient for min_count");
one.extend(zeros.pop().unwrap()); one.extend(zeros.pop().expect("zeros sufficient for min_count"));
new_groups.push(one); new_groups.push(one);
} }
new_groups.extend(ones); new_groups.extend(ones);

View File

@@ -96,9 +96,9 @@ mod tests {
#[test] #[test]
fn roundtrip_empty() { fn roundtrip_empty() {
let pattern = Pattern::default(); let pattern = Pattern::default();
let encoded = export(&pattern).unwrap(); let encoded = export(&pattern).expect("export pattern");
assert!(encoded.starts_with("cgr:")); assert!(encoded.starts_with("cgr:"));
let decoded = import(&encoded).unwrap(); let decoded = import(&encoded).expect("import pattern");
assert_eq!(decoded.length, pattern.length); assert_eq!(decoded.length, pattern.length);
assert_eq!(decoded.steps.len(), pattern.steps.len()); assert_eq!(decoded.steps.len(), pattern.steps.len());
} }
@@ -127,8 +127,8 @@ mod tests {
pattern.length = 8; pattern.length = 8;
pattern.name = Some("Test".to_string()); pattern.name = Some("Test".to_string());
let encoded = export(&pattern).unwrap(); let encoded = export(&pattern).expect("export pattern");
let decoded = import(&encoded).unwrap(); let decoded = import(&encoded).expect("import pattern");
assert_eq!(decoded.length, 8); assert_eq!(decoded.length, 8);
assert_eq!(decoded.name.as_deref(), Some("Test")); assert_eq!(decoded.name.as_deref(), Some("Test"));
@@ -152,9 +152,9 @@ mod tests {
#[test] #[test]
fn whitespace_trimming() { fn whitespace_trimming() {
let pattern = Pattern::default(); let pattern = Pattern::default();
let encoded = export(&pattern).unwrap(); let encoded = export(&pattern).expect("export pattern");
let padded = format!(" {encoded} \n"); let padded = format!(" {encoded} \n");
let decoded = import(&padded).unwrap(); let decoded = import(&padded).expect("import padded pattern");
assert_eq!(decoded.length, pattern.length); assert_eq!(decoded.length, pattern.length);
} }
@@ -172,15 +172,15 @@ mod tests {
pattern.length = 16; pattern.length = 16;
// Current (msgpack+brotli) // Current (msgpack+brotli)
let new_encoded = export(&pattern).unwrap(); let new_encoded = export(&pattern).expect("export pattern");
// Old pipeline (json+deflate) for comparison // Old pipeline (json+deflate) for comparison
use std::io::Write; use std::io::Write;
let json = serde_json::to_vec(&pattern).unwrap(); let json = serde_json::to_vec(&pattern).expect("serialize json");
let mut encoder = let mut encoder =
flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best()); flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best());
encoder.write_all(&json).unwrap(); encoder.write_all(&json).expect("write to encoder");
let old_compressed = encoder.finish().unwrap(); let old_compressed = encoder.finish().expect("finish encoder");
let old_encoded = format!("cgr:{}", URL_SAFE_NO_PAD.encode(&old_compressed)); let old_encoded = format!("cgr:{}", URL_SAFE_NO_PAD.encode(&old_compressed));
assert!( assert!(
@@ -203,9 +203,9 @@ mod tests {
bank.patterns[0].length = 8; bank.patterns[0].length = 8;
bank.name = Some("Drums".to_string()); bank.name = Some("Drums".to_string());
let encoded = export_bank(&bank).unwrap(); let encoded = export_bank(&bank).expect("export bank");
assert!(encoded.starts_with("cgrb:")); assert!(encoded.starts_with("cgrb:"));
let decoded = import_bank(&encoded).unwrap(); let decoded = import_bank(&encoded).expect("import bank");
assert_eq!(decoded.name.as_deref(), Some("Drums")); assert_eq!(decoded.name.as_deref(), Some("Drums"));
assert_eq!(decoded.patterns[0].length, 8); assert_eq!(decoded.patterns[0].length, 8);

View File

@@ -487,7 +487,7 @@ impl Editor {
if is_cursor { if is_cursor {
cursor_style cursor_style
} else if is_selected { } else if is_selected {
base_style.bg(selection_style.bg.unwrap()) base_style.bg(selection_style.bg.expect("selection style has bg"))
} else { } else {
base_style base_style
} }

View File

@@ -219,7 +219,6 @@ impl Plugin for CagirePlugin {
source: s.source, source: s.source,
}) })
.collect(), .collect(),
quantization: pat.quantization,
sync_mode: pat.sync_mode, sync_mode: pat.sync_mode,
follow_up: pat.follow_up, follow_up: pat.follow_up,
}; };

View File

@@ -70,8 +70,8 @@ impl App {
pub fn shift_patterns_up(&mut self) { pub fn shift_patterns_up(&mut self) {
let bank = self.patterns_nav.bank_cursor; let bank = self.patterns_nav.bank_cursor;
let patterns = self.patterns_nav.selected_patterns(); let patterns = self.patterns_nav.selected_patterns();
let start = *patterns.first().unwrap(); let start = *patterns.first().expect("selected_patterns non-empty");
let end = *patterns.last().unwrap(); let end = *patterns.last().expect("selected_patterns non-empty");
if let Some(dirty) = clipboard::shift_patterns_up( if let Some(dirty) = clipboard::shift_patterns_up(
&mut self.project_state.project, &mut self.project_state.project,
bank, bank,
@@ -90,8 +90,8 @@ impl App {
pub fn shift_patterns_down(&mut self) { pub fn shift_patterns_down(&mut self) {
let bank = self.patterns_nav.bank_cursor; let bank = self.patterns_nav.bank_cursor;
let patterns = self.patterns_nav.selected_patterns(); let patterns = self.patterns_nav.selected_patterns();
let start = *patterns.first().unwrap(); let start = *patterns.first().expect("selected_patterns non-empty");
let end = *patterns.last().unwrap(); let end = *patterns.last().expect("selected_patterns non-empty");
if let Some(dirty) = clipboard::shift_patterns_down( if let Some(dirty) = clipboard::shift_patterns_down(
&mut self.project_state.project, &mut self.project_state.project,
bank, bank,

View File

@@ -296,12 +296,10 @@ impl App {
AppCommand::StageMute { bank, pattern } => self.playback.stage_mute(bank, pattern), AppCommand::StageMute { bank, pattern } => self.playback.stage_mute(bank, pattern),
AppCommand::StageSolo { bank, pattern } => self.playback.stage_solo(bank, pattern), AppCommand::StageSolo { bank, pattern } => self.playback.stage_solo(bank, pattern),
AppCommand::ClearMutes => { AppCommand::ClearMutes => {
self.playback.clear_staged_mutes(); self.playback.clear_mutes();
self.mute.clear_mute();
} }
AppCommand::ClearSolos => { AppCommand::ClearSolos => {
self.playback.clear_staged_solos(); self.playback.clear_solos();
self.mute.clear_solo();
} }
// UI state // UI state

View File

@@ -24,7 +24,7 @@ use crate::midi::MidiState;
use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, Variables}; use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, Variables};
use crate::page::Page; use crate::page::Page;
use crate::state::{ use crate::state::{
undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal, MuteState, undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal,
OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
ProjectState, ScriptEditorState, UiState, ProjectState, ScriptEditorState, UiState,
}; };
@@ -45,7 +45,6 @@ pub struct App {
pub project_state: ProjectState, pub project_state: ProjectState,
pub ui: UiState, pub ui: UiState,
pub playback: PlaybackState, pub playback: PlaybackState,
pub mute: MuteState,
pub page: Page, pub page: Page,
pub editor_ctx: EditorContext, pub editor_ctx: EditorContext,
@@ -100,7 +99,6 @@ impl App {
project_state: ProjectState::default(), project_state: ProjectState::default(),
ui: UiState::default(), ui: UiState::default(),
playback: PlaybackState::default(), playback: PlaybackState::default(),
mute: MuteState::default(),
page: Page::default(), page: Page::default(),
editor_ctx: EditorContext::default(), editor_ctx: EditorContext::default(),

View File

@@ -32,8 +32,8 @@ impl App {
pub fn send_mute_state(&self, cmd_tx: &Sender<SeqCommand>) { pub fn send_mute_state(&self, cmd_tx: &Sender<SeqCommand>) {
let _ = cmd_tx.send(SeqCommand::SetMuteState { let _ = cmd_tx.send(SeqCommand::SetMuteState {
muted: self.mute.muted.clone(), muted: self.playback.muted.clone(),
soloed: self.mute.soloed.clone(), soloed: self.playback.soloed.clone(),
}); });
} }
@@ -68,7 +68,6 @@ impl App {
source: s.source, source: s.source,
}) })
.collect(), .collect(),
quantization: pat.quantization,
sync_mode: pat.sync_mode, sync_mode: pat.sync_mode,
follow_up: pat.follow_up, follow_up: pat.follow_up,
}; };

View File

@@ -63,13 +63,14 @@ impl App {
} }
let mute_changed = mute_count > 0; let mute_changed = mute_count > 0;
for change in self.playback.staged_mute_changes.drain() { let mute_changes: Vec<_> = self.playback.staged_mute_changes.drain().collect();
for change in mute_changes {
match change { match change {
crate::state::StagedMuteChange::ToggleMute { bank, pattern } => { crate::state::StagedMuteChange::ToggleMute { bank, pattern } => {
self.mute.toggle_mute(bank, pattern); self.playback.toggle_mute(bank, pattern);
} }
crate::state::StagedMuteChange::ToggleSolo { bank, pattern } => { crate::state::StagedMuteChange::ToggleSolo { bank, pattern } => {
self.mute.toggle_solo(bank, pattern); self.playback.toggle_solo(bank, pattern);
} }
} }
} }

View File

@@ -158,6 +158,7 @@ struct CagireDesktop {
audio_sample_pos: Arc<AtomicU64>, audio_sample_pos: Arc<AtomicU64>,
sample_rate_shared: Arc<AtomicU32>, sample_rate_shared: Arc<AtomicU32>,
_stream: Option<cpal::Stream>, _stream: Option<cpal::Stream>,
_input_stream: Option<cpal::Stream>,
_analysis_handle: Option<AnalysisHandle>, _analysis_handle: Option<AnalysisHandle>,
midi_rx: Receiver<MidiCommand>, midi_rx: Receiver<MidiCommand>,
stream_error_rx: crossbeam_channel::Receiver<String>, stream_error_rx: crossbeam_channel::Receiver<String>,
@@ -203,6 +204,7 @@ impl CagireDesktop {
audio_sample_pos: b.audio_sample_pos, audio_sample_pos: b.audio_sample_pos,
sample_rate_shared: b.sample_rate_shared, sample_rate_shared: b.sample_rate_shared,
_stream: b.stream, _stream: b.stream,
_input_stream: b.input_stream,
_analysis_handle: b.analysis_handle, _analysis_handle: b.analysis_handle,
midi_rx: b.midi_rx, midi_rx: b.midi_rx,
stream_error_rx: b.stream_error_rx, stream_error_rx: b.stream_error_rx,
@@ -226,6 +228,7 @@ impl CagireDesktop {
self.app.audio.restart_pending = false; self.app.audio.restart_pending = false;
self._stream = None; self._stream = None;
self._input_stream = None;
self._analysis_handle = None; self._analysis_handle = None;
let Some(ref sequencer) = self.sequencer else { let Some(ref sequencer) = self.sequencer else {
@@ -236,6 +239,7 @@ impl CagireDesktop {
let new_config = AudioStreamConfig { let new_config = AudioStreamConfig {
output_device: self.app.audio.config.output_device.clone(), output_device: self.app.audio.config.output_device.clone(),
input_device: self.app.audio.config.input_device.clone(),
channels: self.app.audio.config.channels, channels: self.app.audio.config.channels,
buffer_size: self.app.audio.config.buffer_size, buffer_size: self.app.audio.config.buffer_size,
max_voices: self.app.audio.config.max_voices, max_voices: self.app.audio.config.max_voices,
@@ -270,8 +274,9 @@ impl CagireDesktop {
new_error_tx, new_error_tx,
&self.app.audio.config.sample_paths, &self.app.audio.config.sample_paths,
) { ) {
Ok((new_stream, info, new_analysis, registry)) => { Ok((new_stream, new_input, info, new_analysis, registry)) => {
self._stream = Some(new_stream); self._stream = Some(new_stream);
self._input_stream = new_input;
self._analysis_handle = Some(new_analysis); self._analysis_handle = Some(new_analysis);
self.app.audio.config.sample_rate = info.sample_rate; self.app.audio.config.sample_rate = info.sample_rate;
self.app.audio.config.host_name = info.host_name; self.app.audio.config.host_name = info.host_name;

View File

@@ -264,6 +264,10 @@ use cpal::Stream;
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
use doux::{Engine, EngineMetrics}; use doux::{Engine, EngineMetrics};
#[cfg(feature = "cli")]
use std::collections::VecDeque;
#[cfg(feature = "cli")]
use std::sync::Mutex;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
use super::AudioCommand; use super::AudioCommand;
@@ -271,6 +275,7 @@ use super::AudioCommand;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
pub struct AudioStreamConfig { pub struct AudioStreamConfig {
pub output_device: Option<String>, pub output_device: Option<String>,
pub input_device: Option<String>,
pub channels: u16, pub channels: u16,
pub buffer_size: u32, pub buffer_size: u32,
pub max_voices: usize, pub max_voices: usize,
@@ -283,6 +288,15 @@ pub struct AudioStreamInfo {
pub channels: u16, pub channels: u16,
} }
#[cfg(feature = "cli")]
type BuildStreamResult = (
Stream,
Option<Stream>,
AudioStreamInfo,
AnalysisHandle,
Arc<doux::SampleRegistry>,
);
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn build_stream( pub fn build_stream(
@@ -295,15 +309,7 @@ pub fn build_stream(
audio_sample_pos: Arc<AtomicU64>, audio_sample_pos: Arc<AtomicU64>,
error_tx: Sender<String>, error_tx: Sender<String>,
sample_paths: &[std::path::PathBuf], sample_paths: &[std::path::PathBuf],
) -> Result< ) -> Result<BuildStreamResult, String> {
(
Stream,
AudioStreamInfo,
AnalysisHandle,
Arc<doux::SampleRegistry>,
),
String,
> {
let device = match &config.output_device { let device = match &config.output_device {
Some(name) => doux::audio::find_output_device(name) Some(name) => doux::audio::find_output_device(name)
.ok_or_else(|| format!("Device not found: {name}"))?, .ok_or_else(|| format!("Device not found: {name}"))?,
@@ -352,10 +358,72 @@ pub fn build_stream(
let registry = Arc::clone(&engine.sample_registry); let registry = Arc::clone(&engine.sample_registry);
const INPUT_BUFFER_SIZE: usize = 8192;
let input_buffer: Arc<Mutex<VecDeque<f32>>> =
Arc::new(Mutex::new(VecDeque::with_capacity(INPUT_BUFFER_SIZE)));
let input_device = config
.input_device
.as_ref()
.and_then(|name| {
let dev = doux::audio::find_input_device(name);
if dev.is_none() {
eprintln!("input device not found: {name}");
}
dev
});
let input_channels: usize = input_device
.as_ref()
.and_then(|dev| dev.default_input_config().ok())
.map_or(0, |cfg| cfg.channels() as usize);
let input_stream = input_device.and_then(|dev| {
let input_cfg = match dev.default_input_config() {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("input config error: {e}");
return None;
}
};
if input_cfg.sample_rate() != default_config.sample_rate() {
eprintln!(
"warning: input sample rate ({}Hz) differs from output ({}Hz)",
input_cfg.sample_rate(),
default_config.sample_rate()
);
}
eprintln!(
"opening input: {}ch @ {}Hz",
input_cfg.channels(),
input_cfg.sample_rate()
);
let buf = Arc::clone(&input_buffer);
let stream = dev
.build_input_stream(
&input_cfg.into(),
move |data: &[f32], _| {
let mut b = buf.lock().unwrap();
b.extend(data.iter().copied());
let excess = b.len().saturating_sub(INPUT_BUFFER_SIZE);
if excess > 0 {
drop(b.drain(..excess));
}
},
|err| eprintln!("input stream error: {err}"),
None,
)
.ok()?;
stream.play().ok()?;
Some(stream)
});
let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer); let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer);
let mut cmd_buffer = String::with_capacity(256); let mut cmd_buffer = String::with_capacity(256);
let mut rt_set = false; let mut rt_set = false;
let mut live_scratch = vec![0.0f32; 4096];
let input_buf_clone = Arc::clone(&input_buffer);
let stream = device let stream = device
.build_output_stream( .build_output_stream(
@@ -402,8 +470,49 @@ pub fn build_stream(
} }
} }
// doux expects stereo interleaved live_input (CHANNELS=2)
let stereo_len = buffer_samples * 2;
if live_scratch.len() < stereo_len {
live_scratch.resize(stereo_len, 0.0);
}
let mut buf = input_buf_clone.lock().unwrap();
match input_channels {
0 => {
live_scratch[..stereo_len].fill(0.0);
}
1 => {
for i in 0..buffer_samples {
let s = buf.pop_front().unwrap_or(0.0);
live_scratch[i * 2] = s;
live_scratch[i * 2 + 1] = s;
}
}
2 => {
for sample in &mut live_scratch[..stereo_len] {
*sample = buf.pop_front().unwrap_or(0.0);
}
}
_ => {
for i in 0..buffer_samples {
let l = buf.pop_front().unwrap_or(0.0);
let r = buf.pop_front().unwrap_or(0.0);
for _ in 2..input_channels {
buf.pop_front();
}
live_scratch[i * 2] = l;
live_scratch[i * 2 + 1] = r;
}
}
}
// Discard excess if input produced more than we consumed
let excess = buf.len().saturating_sub(INPUT_BUFFER_SIZE / 2);
if excess > 0 {
drop(buf.drain(..excess));
}
drop(buf);
engine.metrics.load.set_buffer_time(buffer_time_ns); engine.metrics.load.set_buffer_time(buffer_time_ns);
engine.process_block(data, &[], &[]); engine.process_block(data, &[], &live_scratch[..stereo_len]);
scope_buffer.write(data); scope_buffer.write(data);
// Feed mono mix to analysis thread via ring buffer (non-blocking) // Feed mono mix to analysis thread via ring buffer (non-blocking)
@@ -425,5 +534,5 @@ pub fn build_stream(
host_name, host_name,
channels: effective_channels, channels: effective_channels,
}; };
Ok((stream, info, analysis_handle, registry)) Ok((stream, input_stream, info, analysis_handle, registry))
} }

View File

@@ -85,7 +85,7 @@ pub fn dispatcher_loop(
let current_us = link.clock_micros() as SyncTime; let current_us = link.clock_micros() as SyncTime;
while let Some(cmd) = queue.peek() { while let Some(cmd) = queue.peek() {
if cmd.target_time_us <= current_us + SPIN_THRESHOLD_US { if cmd.target_time_us <= current_us + SPIN_THRESHOLD_US {
let cmd = queue.pop().unwrap(); let cmd = queue.pop().expect("pop after peek");
wait_until_dispatch(cmd.target_time_us, &link, has_rt); wait_until_dispatch(cmd.target_time_us, &link, has_rt);
dispatch_midi(cmd.command, &midi_tx); dispatch_midi(cmd.command, &midi_tx);
} else { } else {
@@ -149,8 +149,8 @@ mod tests {
target_time_us: 200, target_time_us: 200,
}); });
assert_eq!(heap.pop().unwrap().target_time_us, 100); assert_eq!(heap.pop().expect("heap non-empty").target_time_us, 100);
assert_eq!(heap.pop().unwrap().target_time_us, 200); assert_eq!(heap.pop().expect("heap non-empty").target_time_us, 200);
assert_eq!(heap.pop().unwrap().target_time_us, 300); assert_eq!(heap.pop().expect("heap non-empty").target_time_us, 300);
} }
} }

View File

@@ -141,8 +141,6 @@ pub struct PatternSnapshot {
pub speed: crate::model::PatternSpeed, pub speed: crate::model::PatternSpeed,
pub length: usize, pub length: usize,
pub steps: Vec<StepSnapshot>, pub steps: Vec<StepSnapshot>,
#[allow(dead_code)]
pub quantization: LaunchQuantization,
pub sync_mode: SyncMode, pub sync_mode: SyncMode,
pub follow_up: FollowUp, pub follow_up: FollowUp,
} }
@@ -544,7 +542,7 @@ struct StepResult {
fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str { fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
use std::fmt::Write; use std::fmt::Write;
buf.clear(); buf.clear();
write!(buf, "__speed_{bank}_{pattern}__").unwrap(); write!(buf, "__speed_{bank}_{pattern}__").expect("write to String");
buf buf
} }
@@ -553,7 +551,7 @@ pub struct SequencerState {
pattern_cache: PatternCache, pattern_cache: PatternCache,
pending_updates: HashMap<(usize, usize), PatternSnapshot>, pending_updates: HashMap<(usize, usize), PatternSnapshot>,
runs_counter: RunsCounter, runs_counter: RunsCounter,
step_traces: Arc<StepTracesMap>, step_traces: StepTracesMap,
event_count: usize, event_count: usize,
script_engine: ScriptEngine, script_engine: ScriptEngine,
variables: Variables, variables: Variables,
@@ -590,7 +588,7 @@ impl SequencerState {
pattern_cache: PatternCache::new(), pattern_cache: PatternCache::new(),
pending_updates: HashMap::new(), pending_updates: HashMap::new(),
runs_counter: RunsCounter::new(), runs_counter: RunsCounter::new(),
step_traces: Arc::new(HashMap::new()), step_traces: HashMap::new(),
event_count: 0, event_count: 0,
script_engine, script_engine,
variables, variables,
@@ -709,7 +707,7 @@ impl SequencerState {
self.audio_state.active_patterns.clear(); self.audio_state.active_patterns.clear();
self.audio_state.pending_starts.clear(); self.audio_state.pending_starts.clear();
self.audio_state.pending_stops.clear(); self.audio_state.pending_stops.clear();
Arc::make_mut(&mut self.step_traces).clear(); self.step_traces.clear();
self.runs_counter.counts.clear(); self.runs_counter.counts.clear();
self.audio_state.flush_midi_notes = true; self.audio_state.flush_midi_notes = true;
} }
@@ -727,7 +725,7 @@ impl SequencerState {
self.speed_overrides.clear(); self.speed_overrides.clear();
self.script_engine.clear_global_params(); self.script_engine.clear_global_params();
self.runs_counter.counts.clear(); self.runs_counter.counts.clear();
Arc::make_mut(&mut self.step_traces).clear(); self.step_traces.clear();
self.audio_state.flush_midi_notes = true; self.audio_state.flush_midi_notes = true;
} }
SeqCommand::ResetScriptState => { SeqCommand::ResetScriptState => {
@@ -807,7 +805,7 @@ impl SequencerState {
fn tick_paused(&mut self) -> TickOutput { fn tick_paused(&mut self) -> TickOutput {
for pending in self.audio_state.pending_stops.drain(..) { for pending in self.audio_state.pending_stops.drain(..) {
self.audio_state.active_patterns.remove(&pending.id); self.audio_state.active_patterns.remove(&pending.id);
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| { self.step_traces.retain(|&(bank, pattern, _), _| {
bank != pending.id.bank || pattern != pending.id.pattern bank != pending.id.bank || pattern != pending.id.pattern
}); });
let key = (pending.id.bank, pending.id.pattern); let key = (pending.id.bank, pending.id.pattern);
@@ -889,7 +887,7 @@ impl SequencerState {
for pending in &self.audio_state.pending_stops { for pending in &self.audio_state.pending_stops {
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) { if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
self.audio_state.active_patterns.remove(&pending.id); self.audio_state.active_patterns.remove(&pending.id);
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| { self.step_traces.retain(|&(bank, pattern, _), _| {
bank != pending.id.bank || pattern != pending.id.pattern bank != pending.id.bank || pattern != pending.id.pattern
}); });
// Flush pending update so cache stays current for future launches // Flush pending update so cache stays current for future launches
@@ -1009,7 +1007,7 @@ impl SequencerState {
.script_engine .script_engine
.evaluate_with_trace(script, &ctx, &mut trace) .evaluate_with_trace(script, &ctx, &mut trace)
{ {
Arc::make_mut(&mut self.step_traces).insert( self.step_traces.insert(
(active.bank, active.pattern, source_idx), (active.bank, active.pattern, source_idx),
std::mem::take(&mut trace), std::mem::take(&mut trace),
); );
@@ -1198,7 +1196,7 @@ impl SequencerState {
last_step_beat: a.last_step_beat, last_step_beat: a.last_step_beat,
}) })
.collect(), .collect(),
step_traces: Arc::clone(&self.step_traces), step_traces: Arc::new(self.step_traces.clone()),
event_count: self.event_count, event_count: self.event_count,
tempo: self.last_tempo, tempo: self.last_tempo,
beat: self.last_beat, beat: self.last_beat,
@@ -1490,7 +1488,6 @@ mod tests {
source: None, source: None,
}) })
.collect(), .collect(),
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
follow_up: FollowUp::default(), follow_up: FollowUp::default(),
} }
@@ -1709,19 +1706,19 @@ mod tests {
// beat_int at 0.5 is 2, prev_beat_int at 0.0 is 0 // beat_int at 0.5 is 2, prev_beat_int at 0.0 is 0
// steps_to_fire = 2-0 = 2, firing steps 0 and 1, wrapping to 0 // steps_to_fire = 2-0 = 2, firing steps 0 and 1, wrapping to 0
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 0); assert_eq!(ap.step_index, 0);
assert_eq!(ap.iter, 1); assert_eq!(ap.iter, 1);
// beat_int at 0.75 is 3, prev is 2, fires 1 step (step 0), advances to 1 // beat_int at 0.75 is 3, prev is 2, fires 1 step (step 0), advances to 1
state.tick(tick_at(0.75, true)); state.tick(tick_at(0.75, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 1); assert_eq!(ap.step_index, 1);
assert_eq!(ap.iter, 1); assert_eq!(ap.iter, 1);
// beat_int at 1.0 is 4, prev is 3, fires 1 step (step 1), wraps to 0 // beat_int at 1.0 is 4, prev is 3, fires 1 step (step 1), wraps to 0
state.tick(tick_at(1.0, true)); state.tick(tick_at(1.0, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 0); assert_eq!(ap.step_index, 0);
assert_eq!(ap.iter, 2); assert_eq!(ap.iter, 2);
} }
@@ -1754,12 +1751,12 @@ mod tests {
// At 2x speed: beat_int at 0.5 is (0.5*4*2)=4, prev at 0.0 is 0 // At 2x speed: beat_int at 0.5 is (0.5*4*2)=4, prev at 0.0 is 0
// Fires 4 steps (0,1,2,3), advancing to step 4 // Fires 4 steps (0,1,2,3), advancing to step 4
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 4); assert_eq!(ap.step_index, 4);
// beat_int at 0.625 is (0.625*4*2)=5, prev is 4, fires 1 step // beat_int at 0.625 is (0.625*4*2)=5, prev is 4, fires 1 step
state.tick(tick_at(0.625, true)); state.tick(tick_at(0.625, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 5); assert_eq!(ap.step_index, 5);
} }
@@ -1866,17 +1863,17 @@ mod tests {
)); ));
// beat_int at 0.5 is 2, prev at 0.0 is 0, fires 2 steps (0,1), step_index=2 // beat_int at 0.5 is 2, prev at 0.0 is 0, fires 2 steps (0,1), step_index=2
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 2); assert_eq!(ap.step_index, 2);
// beat_int at 0.75 is 3, prev is 2, fires 1 step (2), step_index=3 // beat_int at 0.75 is 3, prev is 2, fires 1 step (2), step_index=3
state.tick(tick_at(0.75, true)); state.tick(tick_at(0.75, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 3); assert_eq!(ap.step_index, 3);
// beat_int at 1.0 is 4, prev is 3, fires 1 step (3), wraps to step_index=0 // beat_int at 1.0 is 4, prev is 3, fires 1 step (3), wraps to step_index=0
state.tick(tick_at(1.0, true)); state.tick(tick_at(1.0, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 0); assert_eq!(ap.step_index, 0);
// Update pattern to length 2 while running — deferred until iteration boundary // Update pattern to length 2 while running — deferred until iteration boundary
@@ -1890,7 +1887,7 @@ mod tests {
}], }],
1.25, 1.25,
)); ));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 1); // still length 4 assert_eq!(ap.step_index, 1); // still length 4
// Advance through remaining steps of original length-4 pattern // Advance through remaining steps of original length-4 pattern
@@ -1901,12 +1898,12 @@ mod tests {
// Now length=2 is applied. Next tick uses new length. // Now length=2 is applied. Next tick uses new length.
// beat=2.25: step 0 fires, advances to 1 // beat=2.25: step 0 fires, advances to 1
state.tick(tick_at(2.25, true)); state.tick(tick_at(2.25, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 1); assert_eq!(ap.step_index, 1);
// beat=2.5: step 1 fires, wraps to 0 (length 2) // beat=2.5: step 1 fires, wraps to 0 (length 2)
state.tick(tick_at(2.5, true)); state.tick(tick_at(2.5, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set");
assert_eq!(ap.step_index, 0); assert_eq!(ap.step_index, 0);
} }
@@ -2056,7 +2053,6 @@ mod tests {
source: None, source: None,
}) })
.collect(), .collect(),
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset, sync_mode: SyncMode::Reset,
follow_up: FollowUp::default(), follow_up: FollowUp::default(),
} }

View File

@@ -40,6 +40,7 @@ pub struct Init {
pub audio_sample_pos: Arc<AtomicU64>, pub audio_sample_pos: Arc<AtomicU64>,
pub sample_rate_shared: Arc<AtomicU32>, pub sample_rate_shared: Arc<AtomicU32>,
pub stream: Option<cpal::Stream>, pub stream: Option<cpal::Stream>,
pub input_stream: Option<cpal::Stream>,
pub analysis_handle: Option<AnalysisHandle>, pub analysis_handle: Option<AnalysisHandle>,
pub midi_rx: Receiver<MidiCommand>, pub midi_rx: Receiver<MidiCommand>,
pub stream_error_rx: crossbeam_channel::Receiver<String>, pub stream_error_rx: crossbeam_channel::Receiver<String>,
@@ -200,12 +201,13 @@ pub fn init(args: InitArgs) -> Init {
let stream_config = AudioStreamConfig { let stream_config = AudioStreamConfig {
output_device: app.audio.config.output_device.clone(), output_device: app.audio.config.output_device.clone(),
input_device: app.audio.config.input_device.clone(),
channels: app.audio.config.channels, channels: app.audio.config.channels,
buffer_size: app.audio.config.buffer_size, buffer_size: app.audio.config.buffer_size,
max_voices: app.audio.config.max_voices, max_voices: app.audio.config.max_voices,
}; };
let (stream, analysis_handle) = match build_stream( let (stream, input_stream, analysis_handle) = match build_stream(
&stream_config, &stream_config,
initial_audio_rx, initial_audio_rx,
Arc::clone(&scope_buffer), Arc::clone(&scope_buffer),
@@ -216,7 +218,7 @@ pub fn init(args: InitArgs) -> Init {
stream_error_tx, stream_error_tx,
&app.audio.config.sample_paths, &app.audio.config.sample_paths,
) { ) {
Ok((s, info, analysis, registry)) => { Ok((s, input, info, analysis, registry)) => {
app.audio.config.sample_rate = info.sample_rate; app.audio.config.sample_rate = info.sample_rate;
app.audio.config.host_name = info.host_name; app.audio.config.host_name = info.host_name;
app.audio.config.channels = info.channels; app.audio.config.channels = info.channels;
@@ -233,12 +235,12 @@ pub fn init(args: InitArgs) -> Init {
.expect("failed to spawn preload thread"); .expect("failed to spawn preload thread");
} }
(Some(s), Some(analysis)) (Some(s), input, Some(analysis))
} }
Err(e) => { Err(e) => {
app.ui.set_status(format!("Audio failed: {e}")); app.ui.set_status(format!("Audio failed: {e}"));
app.audio.error = Some(e); app.audio.error = Some(e);
(None, None) (None, None, None)
} }
}; };
@@ -257,6 +259,7 @@ pub fn init(args: InitArgs) -> Init {
audio_sample_pos, audio_sample_pos,
sample_rate_shared, sample_rate_shared,
stream, stream,
input_stream,
analysis_handle, analysis_handle,
midi_rx, midi_rx,
stream_error_rx, stream_error_rx,

View File

@@ -115,7 +115,7 @@ fn execute_focused_block(ctx: &mut InputContext) {
let Some(parsed) = cache[ctx.app.ui.help_topic].as_ref() else { let Some(parsed) = cache[ctx.app.ui.help_topic].as_ref() else {
return; return;
}; };
let idx = ctx.app.ui.help_focused_block.unwrap(); let idx = ctx.app.ui.help_focused_block.expect("block focused in code nav");
let Some(block) = parsed.code_blocks.get(idx) else { let Some(block) = parsed.code_blocks.get(idx) else {
return; return;
}; };

View File

@@ -34,46 +34,14 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
ctx.playing ctx.playing
.store(ctx.app.playback.playing, Ordering::Relaxed); .store(ctx.app.playback.playing, Ordering::Relaxed);
} }
KeyCode::Left if shift && !ctrl => { KeyCode::Left if shift && !ctrl => shift_navigate(ctx, AppCommand::PrevStep),
if ctx.app.editor_ctx.selection_anchor.is_none() { KeyCode::Right if shift && !ctrl => shift_navigate(ctx, AppCommand::NextStep),
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step)); KeyCode::Up if shift && !ctrl => shift_navigate(ctx, AppCommand::StepUp),
} KeyCode::Down if shift && !ctrl => shift_navigate(ctx, AppCommand::StepDown),
ctx.dispatch(AppCommand::PrevStep); KeyCode::Left => navigate(ctx, AppCommand::PrevStep),
} KeyCode::Right => navigate(ctx, AppCommand::NextStep),
KeyCode::Right if shift && !ctrl => { KeyCode::Up => navigate(ctx, AppCommand::StepUp),
if ctx.app.editor_ctx.selection_anchor.is_none() { KeyCode::Down => navigate(ctx, AppCommand::StepDown),
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
}
ctx.dispatch(AppCommand::NextStep);
}
KeyCode::Up if shift && !ctrl => {
if ctx.app.editor_ctx.selection_anchor.is_none() {
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
}
ctx.dispatch(AppCommand::StepUp);
}
KeyCode::Down if shift && !ctrl => {
if ctx.app.editor_ctx.selection_anchor.is_none() {
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
}
ctx.dispatch(AppCommand::StepDown);
}
KeyCode::Left => {
ctx.app.editor_ctx.clear_selection();
ctx.dispatch(AppCommand::PrevStep);
}
KeyCode::Right => {
ctx.app.editor_ctx.clear_selection();
ctx.dispatch(AppCommand::NextStep);
}
KeyCode::Up => {
ctx.app.editor_ctx.clear_selection();
ctx.dispatch(AppCommand::StepUp);
}
KeyCode::Down => {
ctx.app.editor_ctx.clear_selection();
ctx.dispatch(AppCommand::StepDown);
}
KeyCode::Esc => { KeyCode::Esc => {
ctx.app.editor_ctx.clear_selection(); ctx.app.editor_ctx.clear_selection();
} }
@@ -214,12 +182,12 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
} }
KeyCode::Char('m') => { KeyCode::Char('m') => {
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern); let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
ctx.app.mute.toggle_mute(bank, pattern); ctx.app.playback.toggle_mute(bank, pattern);
ctx.app.send_mute_state(ctx.seq_cmd_tx); ctx.app.send_mute_state(ctx.seq_cmd_tx);
} }
KeyCode::Char('x') => { KeyCode::Char('x') => {
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern); let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
ctx.app.mute.toggle_solo(bank, pattern); ctx.app.playback.toggle_solo(bank, pattern);
ctx.app.send_mute_state(ctx.seq_cmd_tx); ctx.app.send_mute_state(ctx.seq_cmd_tx);
} }
KeyCode::Char('M') => { KeyCode::Char('M') => {
@@ -245,3 +213,15 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
} }
InputResult::Continue InputResult::Continue
} }
fn shift_navigate(ctx: &mut InputContext, cmd: AppCommand) {
if ctx.app.editor_ctx.selection_anchor.is_none() {
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
}
ctx.dispatch(cmd);
}
fn navigate(ctx: &mut InputContext, cmd: AppCommand) {
ctx.app.editor_ctx.clear_selection();
ctx.dispatch(cmd);
}

View File

@@ -100,6 +100,7 @@ fn main() -> io::Result<()> {
let audio_sample_pos = b.audio_sample_pos; let audio_sample_pos = b.audio_sample_pos;
let sample_rate_shared = b.sample_rate_shared; let sample_rate_shared = b.sample_rate_shared;
let mut _stream = b.stream; let mut _stream = b.stream;
let mut _input_stream = b.input_stream;
let mut _analysis_handle = b.analysis_handle; let mut _analysis_handle = b.analysis_handle;
let mut midi_rx = b.midi_rx; let mut midi_rx = b.midi_rx;
let mut stream_error_rx = b.stream_error_rx; let mut stream_error_rx = b.stream_error_rx;
@@ -118,6 +119,7 @@ fn main() -> io::Result<()> {
if app.audio.restart_pending { if app.audio.restart_pending {
app.audio.restart_pending = false; app.audio.restart_pending = false;
_stream = None; _stream = None;
_input_stream = None;
_analysis_handle = None; _analysis_handle = None;
let new_audio_rx = sequencer.swap_audio_channel(); let new_audio_rx = sequencer.swap_audio_channel();
@@ -125,6 +127,7 @@ fn main() -> io::Result<()> {
let new_config = AudioStreamConfig { let new_config = AudioStreamConfig {
output_device: app.audio.config.output_device.clone(), output_device: app.audio.config.output_device.clone(),
input_device: app.audio.config.input_device.clone(),
channels: app.audio.config.channels, channels: app.audio.config.channels,
buffer_size: app.audio.config.buffer_size, buffer_size: app.audio.config.buffer_size,
max_voices: app.audio.config.max_voices, max_voices: app.audio.config.max_voices,
@@ -159,8 +162,9 @@ fn main() -> io::Result<()> {
new_error_tx, new_error_tx,
&app.audio.config.sample_paths, &app.audio.config.sample_paths,
) { ) {
Ok((new_stream, info, new_analysis, registry)) => { Ok((new_stream, new_input, info, new_analysis, registry)) => {
_stream = Some(new_stream); _stream = Some(new_stream);
_input_stream = new_input;
_analysis_handle = Some(new_analysis); _analysis_handle = Some(new_analysis);
app.audio.config.sample_rate = info.sample_rate; app.audio.config.sample_rate = info.sample_rate;
app.audio.config.host_name = info.host_name; app.audio.config.host_name = info.host_name;

View File

@@ -252,7 +252,7 @@ pub fn duplicate_steps(
) -> PasteResult { ) -> PasteResult {
let pat = project.pattern_at(bank, pattern); let pat = project.pattern_at(bank, pattern);
let pat_len = pat.length; let pat_len = pat.length;
let paste_at = *indices.last().unwrap() + 1; let paste_at = *indices.last().expect("indices non-empty") + 1;
let dupe_data: Vec<(bool, String, Option<u8>)> = indices let dupe_data: Vec<(bool, String, Option<u8>)> = indices
.iter() .iter()

View File

@@ -20,7 +20,6 @@ pub mod effects;
pub mod file_browser; pub mod file_browser;
pub mod live_keys; pub mod live_keys;
pub mod modal; pub mod modal;
pub mod mute;
pub mod options; pub mod options;
pub mod panel; pub mod panel;
pub mod patterns_nav; pub mod patterns_nav;
@@ -41,7 +40,6 @@ pub use modal::{ConfirmAction, Modal, RenameTarget};
pub use options::{OptionsFocus, OptionsState}; pub use options::{OptionsFocus, OptionsState};
pub use panel::{PanelFocus, PanelState, SidePanel}; pub use panel::{PanelFocus, PanelState, SidePanel};
pub use patterns_nav::{PatternsColumn, PatternsNav}; pub use patterns_nav::{PatternsColumn, PatternsNav};
pub use mute::MuteState;
pub use playback::{PlaybackState, StagedChange, StagedMuteChange, StagedPropChange}; pub use playback::{PlaybackState, StagedChange, StagedMuteChange, StagedPropChange};
pub use project::ProjectState; pub use project::ProjectState;
pub use sample_browser::{SampleBrowserState, SampleTree}; pub use sample_browser::{SampleBrowserState, SampleTree};

View File

@@ -1,53 +0,0 @@
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
}
}

View File

@@ -31,6 +31,8 @@ pub struct PlaybackState {
pub queued_changes: Vec<StagedChange>, pub queued_changes: Vec<StagedChange>,
pub staged_mute_changes: HashSet<StagedMuteChange>, pub staged_mute_changes: HashSet<StagedMuteChange>,
pub staged_prop_changes: HashMap<(usize, usize), StagedPropChange>, pub staged_prop_changes: HashMap<(usize, usize), StagedPropChange>,
pub muted: HashSet<(usize, usize)>,
pub soloed: HashSet<(usize, usize)>,
} }
impl Default for PlaybackState { impl Default for PlaybackState {
@@ -41,6 +43,8 @@ impl Default for PlaybackState {
queued_changes: Vec::new(), queued_changes: Vec::new(),
staged_mute_changes: HashSet::new(), staged_mute_changes: HashSet::new(),
staged_prop_changes: HashMap::new(), staged_prop_changes: HashMap::new(),
muted: HashSet::new(),
soloed: HashSet::new(),
} }
} }
} }
@@ -86,6 +90,16 @@ impl PlaybackState {
self.staged_mute_changes.retain(|c| !matches!(c, StagedMuteChange::ToggleSolo { .. })); self.staged_mute_changes.retain(|c| !matches!(c, StagedMuteChange::ToggleSolo { .. }));
} }
pub fn clear_mutes(&mut self) {
self.clear_staged_mutes();
self.muted.clear();
}
pub fn clear_solos(&mut self) {
self.clear_staged_solos();
self.soloed.clear();
}
pub fn has_staged_mute(&self, bank: usize, pattern: usize) -> bool { pub fn has_staged_mute(&self, bank: usize, pattern: usize) -> bool {
self.staged_mute_changes.contains(&StagedMuteChange::ToggleMute { bank, pattern }) self.staged_mute_changes.contains(&StagedMuteChange::ToggleMute { bank, pattern })
} }
@@ -93,4 +107,33 @@ impl PlaybackState {
pub fn has_staged_solo(&self, bank: usize, pattern: usize) -> bool { pub fn has_staged_solo(&self, bank: usize, pattern: usize) -> bool {
self.staged_mute_changes.contains(&StagedMuteChange::ToggleSolo { bank, pattern }) self.staged_mute_changes.contains(&StagedMuteChange::ToggleSolo { bank, pattern })
} }
pub fn toggle_mute(&mut self, bank: usize, pattern: usize) {
let key = (bank, pattern);
if !self.muted.remove(&key) {
self.muted.insert(key);
}
}
pub fn toggle_solo(&mut self, bank: usize, pattern: usize) {
let key = (bank, pattern);
if !self.soloed.remove(&key) {
self.soloed.insert(key);
}
}
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;
}
!self.soloed.is_empty() && !self.soloed.contains(&(bank, pattern))
}
} }

View File

@@ -194,7 +194,7 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
} }
} }
let cache = app.ui.help_parsed.borrow(); let cache = app.ui.help_parsed.borrow();
let parsed = cache[app.ui.help_topic].as_ref().unwrap(); let parsed = cache[app.ui.help_topic].as_ref().expect("help topic parsed");
let has_search_bar = app.ui.help_search_active || has_query; let has_search_bar = app.ui.help_search_active || has_query;
let content_area = if has_search_bar { let content_area = if has_search_bar {

View File

@@ -192,7 +192,7 @@ fn render_viz_area(
VizPanel::Spectrum => render_spectrum(frame, app, *panel_area), VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
VizPanel::Lissajous => render_lissajous(frame, app, *panel_area), VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
VizPanel::Preview => { VizPanel::Preview => {
let user_words = user_words_once.as_ref().unwrap(); let user_words = user_words_once.as_ref().expect("user_words initialized");
let has_prelude = !app.project_state.project.prelude.trim().is_empty(); let has_prelude = !app.project_state.project.prelude.trim().is_empty();
if has_prelude { if has_prelude {
let [script_area, prelude_area] = if is_vertical_layout { let [script_area, prelude_area] = if is_vertical_layout {
@@ -396,13 +396,13 @@ fn render_tile(
(true, true, _, _, _) => (theme.tile.playing_active_bg, theme.tile.playing_active_fg), (true, true, _, _, _) => (theme.tile.playing_active_bg, theme.tile.playing_active_fg),
(true, false, _, _, _) => (theme.tile.playing_inactive_bg, theme.tile.playing_inactive_fg), (true, false, _, _, _) => (theme.tile.playing_inactive_bg, theme.tile.playing_inactive_fg),
(false, true, true, true, _) => { (false, true, true, true, _) => {
let (r, g, b) = link_color.unwrap().0; let (r, g, b) = link_color.expect("link_color set in this branch").0;
(Color::Rgb(r, g, b), theme.selection.cursor_fg) (Color::Rgb(r, g, b), theme.selection.cursor_fg)
} }
(false, true, true, false, _) => (theme.tile.active_selected_bg, theme.selection.cursor_fg), (false, true, true, false, _) => (theme.tile.active_selected_bg, theme.selection.cursor_fg),
(false, true, _, _, true) => (theme.tile.active_in_range_bg, theme.selection.cursor_fg), (false, true, _, _, true) => (theme.tile.active_in_range_bg, theme.selection.cursor_fg),
(false, true, false, true, _) => { (false, true, false, true, _) => {
let (r, g, b) = link_color.unwrap().1; let (r, g, b) = link_color.expect("link_color set in this branch").1;
(Color::Rgb(r, g, b), theme.tile.active_fg) (Color::Rgb(r, g, b), theme.tile.active_fg)
} }
(false, true, false, false, _) => { (false, true, false, false, _) => {

View File

@@ -155,8 +155,8 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
.bank_selection_range() .bank_selection_range()
.is_some_and(|r| r.contains(&idx)); .is_some_and(|r| r.contains(&idx));
let has_muted = (0..MAX_PATTERNS).any(|p| app.mute.is_muted(idx, p)); let has_muted = (0..MAX_PATTERNS).any(|p| app.playback.is_muted(idx, p));
let has_soloed = (0..MAX_PATTERNS).any(|p| app.mute.is_soloed(idx, p)); let has_soloed = (0..MAX_PATTERNS).any(|p| app.playback.is_soloed(idx, p));
let has_staged_mute = (0..MAX_PATTERNS).any(|p| app.playback.has_staged_mute(idx, p)); 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_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 has_staged_mute_solo = has_staged_mute || has_staged_solo;
@@ -353,14 +353,14 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
.pattern_selection_range() .pattern_selection_range()
.is_some_and(|r| r.contains(&idx)); .is_some_and(|r| r.contains(&idx));
let is_muted = app.mute.is_muted(bank, idx); let is_muted = app.playback.is_muted(bank, idx);
let is_soloed = app.mute.is_soloed(bank, idx); let is_soloed = app.playback.is_soloed(bank, idx);
let has_staged_mute = app.playback.has_staged_mute(bank, idx); 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_solo = app.playback.has_staged_solo(bank, idx);
let has_staged_props = app.playback.has_staged_props(bank, idx); let has_staged_props = app.playback.has_staged_props(bank, idx);
let preview_muted = is_muted ^ has_staged_mute; let preview_muted = is_muted ^ has_staged_mute;
let preview_soloed = is_soloed ^ has_staged_solo; let preview_soloed = is_soloed ^ has_staged_solo;
let is_effectively_muted = app.mute.is_effectively_muted(bank, idx); let is_effectively_muted = app.playback.is_effectively_muted(bank, idx);
let (bg, fg, prefix) = if is_cursor { let (bg, fg, prefix) = if is_cursor {
(theme.selection.cursor, theme.selection.cursor_fg, "") (theme.selection.cursor, theme.selection.cursor_fg, "")