diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index 51087be..4a4ed02 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -315,7 +315,7 @@ impl Forth { Op::Dup => { ensure(stack, 1)?; - let v = stack.last().unwrap().clone(); + let v = stack.last().expect("stack non-empty after ensure").clone(); stack.push(v); } Op::Dupn => { @@ -560,7 +560,7 @@ impl Forth { ensure(stack, 1)?; let values = std::mem::take(stack); let val = if values.len() == 1 { - values.into_iter().next().unwrap() + values.into_iter().next().expect("single value after len check") } else { Value::CycleList(Arc::from(values)) }; @@ -570,7 +570,7 @@ impl Forth { ensure(stack, 1)?; let values = std::mem::take(stack); let val = if values.len() == 1 { - values.into_iter().next().unwrap() + values.into_iter().next().expect("single value after len check") } else { Value::CycleList(Arc::from(values)) }; @@ -1804,8 +1804,8 @@ fn euclidean_rhythm(k: usize, n: usize, rotation: usize) -> Vec { groups.into_iter().partition(|g| g[0]); for _ in 0..min_count { - let mut one = ones.pop().unwrap(); - one.extend(zeros.pop().unwrap()); + let mut one = ones.pop().expect("ones sufficient for min_count"); + one.extend(zeros.pop().expect("zeros sufficient for min_count")); new_groups.push(one); } new_groups.extend(ones); diff --git a/crates/project/src/share.rs b/crates/project/src/share.rs index 804583f..51ce2dc 100644 --- a/crates/project/src/share.rs +++ b/crates/project/src/share.rs @@ -96,9 +96,9 @@ mod tests { #[test] fn roundtrip_empty() { let pattern = Pattern::default(); - let encoded = export(&pattern).unwrap(); + let encoded = export(&pattern).expect("export pattern"); 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.steps.len(), pattern.steps.len()); } @@ -127,8 +127,8 @@ mod tests { pattern.length = 8; pattern.name = Some("Test".to_string()); - let encoded = export(&pattern).unwrap(); - let decoded = import(&encoded).unwrap(); + let encoded = export(&pattern).expect("export pattern"); + let decoded = import(&encoded).expect("import pattern"); assert_eq!(decoded.length, 8); assert_eq!(decoded.name.as_deref(), Some("Test")); @@ -152,9 +152,9 @@ mod tests { #[test] fn whitespace_trimming() { let pattern = Pattern::default(); - let encoded = export(&pattern).unwrap(); + let encoded = export(&pattern).expect("export pattern"); let padded = format!(" {encoded} \n"); - let decoded = import(&padded).unwrap(); + let decoded = import(&padded).expect("import padded pattern"); assert_eq!(decoded.length, pattern.length); } @@ -172,15 +172,15 @@ mod tests { pattern.length = 16; // Current (msgpack+brotli) - let new_encoded = export(&pattern).unwrap(); + let new_encoded = export(&pattern).expect("export pattern"); // Old pipeline (json+deflate) for comparison 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 = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best()); - encoder.write_all(&json).unwrap(); - let old_compressed = encoder.finish().unwrap(); + encoder.write_all(&json).expect("write to encoder"); + let old_compressed = encoder.finish().expect("finish encoder"); let old_encoded = format!("cgr:{}", URL_SAFE_NO_PAD.encode(&old_compressed)); assert!( @@ -203,9 +203,9 @@ mod tests { bank.patterns[0].length = 8; 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:")); - 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.patterns[0].length, 8); diff --git a/crates/ratatui/src/editor.rs b/crates/ratatui/src/editor.rs index 32c2e51..71780ad 100644 --- a/crates/ratatui/src/editor.rs +++ b/crates/ratatui/src/editor.rs @@ -487,7 +487,7 @@ impl Editor { if is_cursor { cursor_style } else if is_selected { - base_style.bg(selection_style.bg.unwrap()) + base_style.bg(selection_style.bg.expect("selection style has bg")) } else { base_style } diff --git a/plugins/cagire-plugins/src/lib.rs b/plugins/cagire-plugins/src/lib.rs index 11211dc..d55cd5c 100644 --- a/plugins/cagire-plugins/src/lib.rs +++ b/plugins/cagire-plugins/src/lib.rs @@ -219,7 +219,6 @@ impl Plugin for CagirePlugin { source: s.source, }) .collect(), - quantization: pat.quantization, sync_mode: pat.sync_mode, follow_up: pat.follow_up, }; diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index 02f6ff6..356cbc9 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -70,8 +70,8 @@ impl App { pub fn shift_patterns_up(&mut self) { let bank = self.patterns_nav.bank_cursor; let patterns = self.patterns_nav.selected_patterns(); - let start = *patterns.first().unwrap(); - let end = *patterns.last().unwrap(); + let start = *patterns.first().expect("selected_patterns non-empty"); + let end = *patterns.last().expect("selected_patterns non-empty"); if let Some(dirty) = clipboard::shift_patterns_up( &mut self.project_state.project, bank, @@ -90,8 +90,8 @@ impl App { pub fn shift_patterns_down(&mut self) { let bank = self.patterns_nav.bank_cursor; let patterns = self.patterns_nav.selected_patterns(); - let start = *patterns.first().unwrap(); - let end = *patterns.last().unwrap(); + let start = *patterns.first().expect("selected_patterns non-empty"); + let end = *patterns.last().expect("selected_patterns non-empty"); if let Some(dirty) = clipboard::shift_patterns_down( &mut self.project_state.project, bank, diff --git a/src/app/dispatch.rs b/src/app/dispatch.rs index 5a25c7f..4cccac4 100644 --- a/src/app/dispatch.rs +++ b/src/app/dispatch.rs @@ -296,12 +296,10 @@ impl App { 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(); + self.playback.clear_mutes(); } AppCommand::ClearSolos => { - self.playback.clear_staged_solos(); - self.mute.clear_solo(); + self.playback.clear_solos(); } // UI state diff --git a/src/app/mod.rs b/src/app/mod.rs index e8ada44..73f9de2 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -24,7 +24,7 @@ use crate::midi::MidiState; use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, Variables}; use crate::page::Page; use crate::state::{ - undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal, MuteState, + undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState, ProjectState, ScriptEditorState, UiState, }; @@ -45,7 +45,6 @@ pub struct App { pub project_state: ProjectState, pub ui: UiState, pub playback: PlaybackState, - pub mute: MuteState, pub page: Page, pub editor_ctx: EditorContext, @@ -100,7 +99,6 @@ impl App { project_state: ProjectState::default(), ui: UiState::default(), playback: PlaybackState::default(), - mute: MuteState::default(), page: Page::default(), editor_ctx: EditorContext::default(), diff --git a/src/app/sequencer.rs b/src/app/sequencer.rs index d3901ed..6d40d59 100644 --- a/src/app/sequencer.rs +++ b/src/app/sequencer.rs @@ -32,8 +32,8 @@ impl App { pub fn send_mute_state(&self, cmd_tx: &Sender) { let _ = cmd_tx.send(SeqCommand::SetMuteState { - muted: self.mute.muted.clone(), - soloed: self.mute.soloed.clone(), + muted: self.playback.muted.clone(), + soloed: self.playback.soloed.clone(), }); } @@ -68,7 +68,6 @@ impl App { source: s.source, }) .collect(), - quantization: pat.quantization, sync_mode: pat.sync_mode, follow_up: pat.follow_up, }; diff --git a/src/app/staging.rs b/src/app/staging.rs index b624a30..fc407d6 100644 --- a/src/app/staging.rs +++ b/src/app/staging.rs @@ -63,13 +63,14 @@ impl App { } 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 { crate::state::StagedMuteChange::ToggleMute { bank, pattern } => { - self.mute.toggle_mute(bank, pattern); + self.playback.toggle_mute(bank, pattern); } crate::state::StagedMuteChange::ToggleSolo { bank, pattern } => { - self.mute.toggle_solo(bank, pattern); + self.playback.toggle_solo(bank, pattern); } } } diff --git a/src/bin/desktop/main.rs b/src/bin/desktop/main.rs index cf6850e..fb88657 100644 --- a/src/bin/desktop/main.rs +++ b/src/bin/desktop/main.rs @@ -158,6 +158,7 @@ struct CagireDesktop { audio_sample_pos: Arc, sample_rate_shared: Arc, _stream: Option, + _input_stream: Option, _analysis_handle: Option, midi_rx: Receiver, stream_error_rx: crossbeam_channel::Receiver, @@ -203,6 +204,7 @@ impl CagireDesktop { audio_sample_pos: b.audio_sample_pos, sample_rate_shared: b.sample_rate_shared, _stream: b.stream, + _input_stream: b.input_stream, _analysis_handle: b.analysis_handle, midi_rx: b.midi_rx, stream_error_rx: b.stream_error_rx, @@ -226,6 +228,7 @@ impl CagireDesktop { self.app.audio.restart_pending = false; self._stream = None; + self._input_stream = None; self._analysis_handle = None; let Some(ref sequencer) = self.sequencer else { @@ -236,6 +239,7 @@ impl CagireDesktop { let new_config = AudioStreamConfig { output_device: self.app.audio.config.output_device.clone(), + input_device: self.app.audio.config.input_device.clone(), channels: self.app.audio.config.channels, buffer_size: self.app.audio.config.buffer_size, max_voices: self.app.audio.config.max_voices, @@ -270,8 +274,9 @@ impl CagireDesktop { new_error_tx, &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._input_stream = new_input; self._analysis_handle = Some(new_analysis); self.app.audio.config.sample_rate = info.sample_rate; self.app.audio.config.host_name = info.host_name; diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 0315ca0..7b53205 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -264,6 +264,10 @@ use cpal::Stream; use crossbeam_channel::{Receiver, Sender}; #[cfg(feature = "cli")] use doux::{Engine, EngineMetrics}; +#[cfg(feature = "cli")] +use std::collections::VecDeque; +#[cfg(feature = "cli")] +use std::sync::Mutex; #[cfg(feature = "cli")] use super::AudioCommand; @@ -271,6 +275,7 @@ use super::AudioCommand; #[cfg(feature = "cli")] pub struct AudioStreamConfig { pub output_device: Option, + pub input_device: Option, pub channels: u16, pub buffer_size: u32, pub max_voices: usize, @@ -283,6 +288,15 @@ pub struct AudioStreamInfo { pub channels: u16, } +#[cfg(feature = "cli")] +type BuildStreamResult = ( + Stream, + Option, + AudioStreamInfo, + AnalysisHandle, + Arc, +); + #[cfg(feature = "cli")] #[allow(clippy::too_many_arguments)] pub fn build_stream( @@ -295,15 +309,7 @@ pub fn build_stream( audio_sample_pos: Arc, error_tx: Sender, sample_paths: &[std::path::PathBuf], -) -> Result< - ( - Stream, - AudioStreamInfo, - AnalysisHandle, - Arc, - ), - String, -> { +) -> Result { let device = match &config.output_device { Some(name) => doux::audio::find_output_device(name) .ok_or_else(|| format!("Device not found: {name}"))?, @@ -352,10 +358,72 @@ pub fn build_stream( let registry = Arc::clone(&engine.sample_registry); + const INPUT_BUFFER_SIZE: usize = 8192; + let input_buffer: Arc>> = + 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 cmd_buffer = String::with_capacity(256); let mut rt_set = false; + let mut live_scratch = vec![0.0f32; 4096]; + let input_buf_clone = Arc::clone(&input_buffer); let stream = device .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.process_block(data, &[], &[]); + engine.process_block(data, &[], &live_scratch[..stereo_len]); scope_buffer.write(data); // Feed mono mix to analysis thread via ring buffer (non-blocking) @@ -425,5 +534,5 @@ pub fn build_stream( host_name, channels: effective_channels, }; - Ok((stream, info, analysis_handle, registry)) + Ok((stream, input_stream, info, analysis_handle, registry)) } diff --git a/src/engine/dispatcher.rs b/src/engine/dispatcher.rs index cef41ad..cde9d4f 100644 --- a/src/engine/dispatcher.rs +++ b/src/engine/dispatcher.rs @@ -85,7 +85,7 @@ pub fn dispatcher_loop( let current_us = link.clock_micros() as SyncTime; while let Some(cmd) = queue.peek() { 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); dispatch_midi(cmd.command, &midi_tx); } else { @@ -149,8 +149,8 @@ mod tests { target_time_us: 200, }); - assert_eq!(heap.pop().unwrap().target_time_us, 100); - assert_eq!(heap.pop().unwrap().target_time_us, 200); - assert_eq!(heap.pop().unwrap().target_time_us, 300); + assert_eq!(heap.pop().expect("heap non-empty").target_time_us, 100); + assert_eq!(heap.pop().expect("heap non-empty").target_time_us, 200); + assert_eq!(heap.pop().expect("heap non-empty").target_time_us, 300); } } diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 58acf25..ce1a9ee 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -141,8 +141,6 @@ pub struct PatternSnapshot { pub speed: crate::model::PatternSpeed, pub length: usize, pub steps: Vec, - #[allow(dead_code)] - pub quantization: LaunchQuantization, pub sync_mode: SyncMode, pub follow_up: FollowUp, } @@ -544,7 +542,7 @@ struct StepResult { fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str { use std::fmt::Write; buf.clear(); - write!(buf, "__speed_{bank}_{pattern}__").unwrap(); + write!(buf, "__speed_{bank}_{pattern}__").expect("write to String"); buf } @@ -553,7 +551,7 @@ pub struct SequencerState { pattern_cache: PatternCache, pending_updates: HashMap<(usize, usize), PatternSnapshot>, runs_counter: RunsCounter, - step_traces: Arc, + step_traces: StepTracesMap, event_count: usize, script_engine: ScriptEngine, variables: Variables, @@ -590,7 +588,7 @@ impl SequencerState { pattern_cache: PatternCache::new(), pending_updates: HashMap::new(), runs_counter: RunsCounter::new(), - step_traces: Arc::new(HashMap::new()), + step_traces: HashMap::new(), event_count: 0, script_engine, variables, @@ -709,7 +707,7 @@ impl SequencerState { self.audio_state.active_patterns.clear(); self.audio_state.pending_starts.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.audio_state.flush_midi_notes = true; } @@ -727,7 +725,7 @@ impl SequencerState { self.speed_overrides.clear(); self.script_engine.clear_global_params(); self.runs_counter.counts.clear(); - Arc::make_mut(&mut self.step_traces).clear(); + self.step_traces.clear(); self.audio_state.flush_midi_notes = true; } SeqCommand::ResetScriptState => { @@ -807,7 +805,7 @@ impl SequencerState { fn tick_paused(&mut self) -> TickOutput { for pending in self.audio_state.pending_stops.drain(..) { 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 }); let key = (pending.id.bank, pending.id.pattern); @@ -889,7 +887,7 @@ impl SequencerState { for pending in &self.audio_state.pending_stops { if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) { 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 }); // Flush pending update so cache stays current for future launches @@ -1009,7 +1007,7 @@ impl SequencerState { .script_engine .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), std::mem::take(&mut trace), ); @@ -1198,7 +1196,7 @@ impl SequencerState { last_step_beat: a.last_step_beat, }) .collect(), - step_traces: Arc::clone(&self.step_traces), + step_traces: Arc::new(self.step_traces.clone()), event_count: self.event_count, tempo: self.last_tempo, beat: self.last_beat, @@ -1490,7 +1488,6 @@ mod tests { source: None, }) .collect(), - quantization: LaunchQuantization::Immediate, sync_mode: SyncMode::Reset, 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 // 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.iter, 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)); - 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.iter, 1); // 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)); - 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.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 // 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); // beat_int at 0.625 is (0.625*4*2)=5, prev is 4, fires 1 step 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); } @@ -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 - 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); // beat_int at 0.75 is 3, prev is 2, fires 1 step (2), step_index=3 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); // 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)); - 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); // Update pattern to length 2 while running — deferred until iteration boundary @@ -1890,7 +1887,7 @@ mod tests { }], 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 // 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. // 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(); + let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set"); assert_eq!(ap.step_index, 1); // 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(); + let ap = state.audio_state.active_patterns.get(&pid(0, 0)).expect("pattern in active set"); assert_eq!(ap.step_index, 0); } @@ -2056,7 +2053,6 @@ mod tests { source: None, }) .collect(), - quantization: LaunchQuantization::Immediate, sync_mode: SyncMode::Reset, follow_up: FollowUp::default(), } diff --git a/src/init.rs b/src/init.rs index 9f3bf3f..7feb6a7 100644 --- a/src/init.rs +++ b/src/init.rs @@ -40,6 +40,7 @@ pub struct Init { pub audio_sample_pos: Arc, pub sample_rate_shared: Arc, pub stream: Option, + pub input_stream: Option, pub analysis_handle: Option, pub midi_rx: Receiver, pub stream_error_rx: crossbeam_channel::Receiver, @@ -200,12 +201,13 @@ pub fn init(args: InitArgs) -> Init { let stream_config = AudioStreamConfig { output_device: app.audio.config.output_device.clone(), + input_device: app.audio.config.input_device.clone(), channels: app.audio.config.channels, buffer_size: app.audio.config.buffer_size, 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, initial_audio_rx, Arc::clone(&scope_buffer), @@ -216,7 +218,7 @@ pub fn init(args: InitArgs) -> Init { stream_error_tx, &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.host_name = info.host_name; app.audio.config.channels = info.channels; @@ -233,12 +235,12 @@ pub fn init(args: InitArgs) -> Init { .expect("failed to spawn preload thread"); } - (Some(s), Some(analysis)) + (Some(s), input, Some(analysis)) } Err(e) => { app.ui.set_status(format!("Audio failed: {e}")); app.audio.error = Some(e); - (None, None) + (None, None, None) } }; @@ -257,6 +259,7 @@ pub fn init(args: InitArgs) -> Init { audio_sample_pos, sample_rate_shared, stream, + input_stream, analysis_handle, midi_rx, stream_error_rx, diff --git a/src/input/help_page.rs b/src/input/help_page.rs index de9eb2b..9a92745 100644 --- a/src/input/help_page.rs +++ b/src/input/help_page.rs @@ -115,7 +115,7 @@ fn execute_focused_block(ctx: &mut InputContext) { let Some(parsed) = cache[ctx.app.ui.help_topic].as_ref() else { 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 { return; }; diff --git a/src/input/main_page.rs b/src/input/main_page.rs index fa3d5ed..057ea5e 100644 --- a/src/input/main_page.rs +++ b/src/input/main_page.rs @@ -34,46 +34,14 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool ctx.playing .store(ctx.app.playback.playing, Ordering::Relaxed); } - KeyCode::Left if shift && !ctrl => { - if ctx.app.editor_ctx.selection_anchor.is_none() { - ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step)); - } - ctx.dispatch(AppCommand::PrevStep); - } - KeyCode::Right if shift && !ctrl => { - if ctx.app.editor_ctx.selection_anchor.is_none() { - 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::Left if shift && !ctrl => shift_navigate(ctx, AppCommand::PrevStep), + KeyCode::Right if shift && !ctrl => shift_navigate(ctx, AppCommand::NextStep), + KeyCode::Up if shift && !ctrl => shift_navigate(ctx, AppCommand::StepUp), + KeyCode::Down if shift && !ctrl => shift_navigate(ctx, AppCommand::StepDown), + KeyCode::Left => navigate(ctx, AppCommand::PrevStep), + KeyCode::Right => navigate(ctx, AppCommand::NextStep), + KeyCode::Up => navigate(ctx, AppCommand::StepUp), + KeyCode::Down => navigate(ctx, AppCommand::StepDown), KeyCode::Esc => { 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') => { 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); } KeyCode::Char('x') => { 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); } KeyCode::Char('M') => { @@ -245,3 +213,15 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool } 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); +} diff --git a/src/main.rs b/src/main.rs index 46f31ea..3356dff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -100,6 +100,7 @@ fn main() -> io::Result<()> { let audio_sample_pos = b.audio_sample_pos; let sample_rate_shared = b.sample_rate_shared; let mut _stream = b.stream; + let mut _input_stream = b.input_stream; let mut _analysis_handle = b.analysis_handle; let mut midi_rx = b.midi_rx; let mut stream_error_rx = b.stream_error_rx; @@ -118,6 +119,7 @@ fn main() -> io::Result<()> { if app.audio.restart_pending { app.audio.restart_pending = false; _stream = None; + _input_stream = None; _analysis_handle = None; let new_audio_rx = sequencer.swap_audio_channel(); @@ -125,6 +127,7 @@ fn main() -> io::Result<()> { let new_config = AudioStreamConfig { output_device: app.audio.config.output_device.clone(), + input_device: app.audio.config.input_device.clone(), channels: app.audio.config.channels, buffer_size: app.audio.config.buffer_size, max_voices: app.audio.config.max_voices, @@ -159,8 +162,9 @@ fn main() -> io::Result<()> { new_error_tx, &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); + _input_stream = new_input; _analysis_handle = Some(new_analysis); app.audio.config.sample_rate = info.sample_rate; app.audio.config.host_name = info.host_name; diff --git a/src/services/clipboard.rs b/src/services/clipboard.rs index 19ff65b..e1e35b2 100644 --- a/src/services/clipboard.rs +++ b/src/services/clipboard.rs @@ -252,7 +252,7 @@ pub fn duplicate_steps( ) -> PasteResult { let pat = project.pattern_at(bank, pattern); 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)> = indices .iter() diff --git a/src/state/mod.rs b/src/state/mod.rs index 2c9122d..d394bc6 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -20,7 +20,6 @@ pub mod effects; pub mod file_browser; pub mod live_keys; pub mod modal; -pub mod mute; pub mod options; pub mod panel; pub mod patterns_nav; @@ -41,7 +40,6 @@ pub use modal::{ConfirmAction, Modal, RenameTarget}; 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, StagedPropChange}; pub use project::ProjectState; pub use sample_browser::{SampleBrowserState, SampleTree}; diff --git a/src/state/mute.rs b/src/state/mute.rs deleted file mode 100644 index 9569049..0000000 --- a/src/state/mute.rs +++ /dev/null @@ -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 - } -} diff --git a/src/state/playback.rs b/src/state/playback.rs index fb5308e..b7c67fc 100644 --- a/src/state/playback.rs +++ b/src/state/playback.rs @@ -31,6 +31,8 @@ pub struct PlaybackState { pub queued_changes: Vec, pub staged_mute_changes: HashSet, pub staged_prop_changes: HashMap<(usize, usize), StagedPropChange>, + pub muted: HashSet<(usize, usize)>, + pub soloed: HashSet<(usize, usize)>, } impl Default for PlaybackState { @@ -41,6 +43,8 @@ impl Default for PlaybackState { queued_changes: Vec::new(), staged_mute_changes: HashSet::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 { .. })); } + 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 { 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 { 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)) + } } diff --git a/src/views/help_view.rs b/src/views/help_view.rs index 3be3354..bcb38e6 100644 --- a/src/views/help_view.rs +++ b/src/views/help_view.rs @@ -194,7 +194,7 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) { } } 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 content_area = if has_search_bar { diff --git a/src/views/main_view.rs b/src/views/main_view.rs index 9b775a9..68addea 100644 --- a/src/views/main_view.rs +++ b/src/views/main_view.rs @@ -192,7 +192,7 @@ fn render_viz_area( VizPanel::Spectrum => render_spectrum(frame, app, *panel_area), VizPanel::Lissajous => render_lissajous(frame, app, *panel_area), 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(); if has_prelude { 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, false, _, _, _) => (theme.tile.playing_inactive_bg, theme.tile.playing_inactive_fg), (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) } (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, 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) } (false, true, false, false, _) => { diff --git a/src/views/patterns_view.rs b/src/views/patterns_view.rs index c91e40d..a0df9be 100644 --- a/src/views/patterns_view.rs +++ b/src/views/patterns_view.rs @@ -155,8 +155,8 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area .bank_selection_range() .is_some_and(|r| r.contains(&idx)); - 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)); + let has_muted = (0..MAX_PATTERNS).any(|p| app.playback.is_muted(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_solo = (0..MAX_PATTERNS).any(|p| app.playback.has_staged_solo(idx, p)); 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() .is_some_and(|r| r.contains(&idx)); - let is_muted = app.mute.is_muted(bank, idx); - let is_soloed = app.mute.is_soloed(bank, idx); + let is_muted = app.playback.is_muted(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_solo = app.playback.has_staged_solo(bank, idx); let has_staged_props = app.playback.has_staged_props(bank, idx); 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 is_effectively_muted = app.playback.is_effectively_muted(bank, idx); let (bg, fg, prefix) = if is_cursor { (theme.selection.cursor, theme.selection.cursor_fg, "")