From 872f933506e519f196548e0cda0c6893ef1cfa4c Mon Sep 17 00:00:00 2001 From: Niels Geens Date: Tue, 5 Aug 2025 12:03:26 +0200 Subject: [PATCH] Added tap tempo --- audio_engine/src/column.rs | 8 ++ audio_engine/src/main.rs | 2 + audio_engine/src/metronome.rs | 11 +- audio_engine/src/midi.rs | 2 +- audio_engine/src/persistence_manager.rs | 22 +++- audio_engine/src/process_handler.rs | 30 ++++- audio_engine/src/tap_tempo.rs | 65 ++++++++++ audio_engine/src/track.rs | 4 + audio_engine/src/track_matrix.rs | 12 ++ mapper/src/app.rs | 71 ++++++----- mapper/src/data.rs | 53 ++++---- mapper/src/input_form.rs | 77 ++++++++---- mapper/src/main.rs | 16 ++- mapper/src/midi.rs | 2 +- mapper/src/ui.rs | 160 ++++++++++++++++-------- xtask/src/collect.rs | 23 ++-- xtask/src/mapper.rs | 18 +-- 17 files changed, 416 insertions(+), 160 deletions(-) create mode 100644 audio_engine/src/tap_tempo.rs diff --git a/audio_engine/src/column.rs b/audio_engine/src/column.rs index 0af32de..dd37064 100644 --- a/audio_engine/src/column.rs +++ b/audio_engine/src/column.rs @@ -90,6 +90,14 @@ impl Column { self.tracks[row].set_consolidated_buffer(buffer) } + pub fn is_all_tracks_cleared(&self) -> bool { + self.tracks.iter().all(|track| track.is_empty()) + } + + pub fn set_frames_per_beat(&mut self, new_frames_per_beat: usize) { + self.frames_per_beat = new_frames_per_beat; + } + pub fn handle_xrun( &mut self, timing: &BufferTiming, diff --git a/audio_engine/src/main.rs b/audio_engine/src/main.rs index 02e3d96..b2d3b23 100644 --- a/audio_engine/src/main.rs +++ b/audio_engine/src/main.rs @@ -16,6 +16,7 @@ mod persistence_manager; mod post_record_handler; mod process_handler; mod state; +mod tap_tempo; mod track; mod track_matrix; @@ -47,6 +48,7 @@ use post_record_handler::PostRecordHandler; use post_record_handler::PostRecordResponse; use process_handler::ProcessHandler; use state::State; +use tap_tempo::TapTempo; use track::Track; use track::TrackState; use track_matrix::TrackMatrix; diff --git a/audio_engine/src/metronome.rs b/audio_engine/src/metronome.rs index d8cdfb0..eda34ad 100644 --- a/audio_engine/src/metronome.rs +++ b/audio_engine/src/metronome.rs @@ -68,13 +68,22 @@ impl Metronome { Ok(timing) } - pub fn set_click_volume(&mut self, click_volume: f32, persistence: &mut PersistenceManagerController) -> Result<()> { + pub fn set_click_volume( + &mut self, + click_volume: f32, + persistence: &mut PersistenceManagerController, + ) -> Result<()> { let clamped = click_volume.clamp(0.0, 1.0); persistence.update_metronome_volume(clamped)?; self.click_volume = clamped; Ok(()) } + pub fn set_frames_per_beat(&mut self, frames_per_beat: usize) { + self.frames_per_beat = frames_per_beat; + self.frames_since_last_beat = self.frames_since_last_beat % self.frames_per_beat; + } + fn process_ppqn_ticks( &mut self, ps: &jack::ProcessScope, diff --git a/audio_engine/src/midi.rs b/audio_engine/src/midi.rs index 716e263..d3bad44 100644 --- a/audio_engine/src/midi.rs +++ b/audio_engine/src/midi.rs @@ -59,7 +59,7 @@ pub fn process_events( process_handler.handle_button_4()?; } 24 if value_num > 0 => { - process_handler.handle_button_5()?; + process_handler.handle_button_5(ps)?; } 25 if value_num > 0 => { process_handler.handle_button_6()?; diff --git a/audio_engine/src/persistence_manager.rs b/audio_engine/src/persistence_manager.rs index 16a8b82..a1acf19 100644 --- a/audio_engine/src/persistence_manager.rs +++ b/audio_engine/src/persistence_manager.rs @@ -16,7 +16,10 @@ pub enum PersistenceUpdate { }, UpdateMetronomeVolume { volume: f32, - } + }, + UpdateMetronomeFramesPerBeat { + frames_per_beat: usize, + }, } impl PersistenceManagerController { @@ -34,7 +37,16 @@ impl PersistenceManagerController { } pub fn update_metronome_volume(&self, volume: f32) -> Result<()> { - let update = PersistenceUpdate::UpdateMetronomeVolume { volume: volume }; + let update = PersistenceUpdate::UpdateMetronomeVolume { volume }; + match self.sender.try_send(update) { + Ok(true) => Ok(()), + Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full + Err(_) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel closed + } + } + + pub fn update_metronome_frames_per_beat(&self, frames_per_beat: usize) -> Result<()> { + let update = PersistenceUpdate::UpdateMetronomeFramesPerBeat { frames_per_beat }; match self.sender.try_send(update) { Ok(true) => Ok(()), Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full @@ -120,6 +132,12 @@ impl PersistenceManager { PersistenceUpdate::UpdateMetronomeVolume { volume } => { self.state .send_modify(|state| state.metronome.click_volume = volume); + self.save_to_disk()?; + } + PersistenceUpdate::UpdateMetronomeFramesPerBeat { frames_per_beat } => { + self.state + .send_modify(|state| state.metronome.frames_per_beat = frames_per_beat); + self.save_to_disk()?; } } diff --git a/audio_engine/src/process_handler.rs b/audio_engine/src/process_handler.rs index 3b00fb9..14b33b0 100644 --- a/audio_engine/src/process_handler.rs +++ b/audio_engine/src/process_handler.rs @@ -11,6 +11,7 @@ pub struct ProcessHandler selected_column: usize, last_volume_setting: f32, sample_rate: usize, + tap_tempo: TapTempo, } impl ProcessHandler { @@ -36,6 +37,7 @@ impl ProcessHandler ProcessHandler Result<()> { - self.metronome.set_click_volume(value as f32 / 127.0, &mut self.persistence) + self.metronome + .set_click_volume(value as f32 / 127.0, &mut self.persistence) } pub fn handle_button_1(&mut self) -> Result<()> { @@ -92,10 +95,6 @@ impl ProcessHandler Result<()> { - Ok(()) - } - - pub fn handle_button_5(&mut self) -> Result<()> { let controllers = TrackControllers::new( &self.post_record_controller, &self.osc, @@ -107,6 +106,27 @@ impl ProcessHandler Result<()> { + if !self.track_matrix.is_all_tracks_cleared() { + // Ignore tap if not all tracks are clear + return Ok(()); + } + + let current_frame_time = ps.last_frame_time(); + + let tempo_updated = self.tap_tempo.handle_tap(current_frame_time); + + if let Some(frames_per_beat) = tempo_updated { + // If tempo was updated, we need to update all columns with new frames_per_beat + self.metronome.set_frames_per_beat(frames_per_beat); + self.persistence + .update_metronome_frames_per_beat(frames_per_beat)?; + self.track_matrix.set_frames_per_beat(frames_per_beat); + } + + Ok(()) + } + pub fn handle_button_6(&mut self) -> Result<()> { self.selected_column = 0; self.osc.selected_column_changed(self.selected_column)?; diff --git a/audio_engine/src/tap_tempo.rs b/audio_engine/src/tap_tempo.rs new file mode 100644 index 0000000..ac86589 --- /dev/null +++ b/audio_engine/src/tap_tempo.rs @@ -0,0 +1,65 @@ +/// Collects tap timestamps and calculates tempo when enough taps are received. +#[derive(Debug)] +pub struct TapTempo { + sample_rate: usize, + tap_times: [u32; Self::MAX_TAPS], + tap_count: usize, +} + +impl TapTempo { + const MAX_TAPS: usize = 8; + const MIN_TAPS: usize = 4; + const MIN_INTERVAL_MS: usize = 100; + const MAX_INTERVAL_MS: usize = 2000; + + pub fn new(sample_rate: usize) -> Self { + Self { + sample_rate, + tap_times: [0; Self::MAX_TAPS], + tap_count: 0, + } + } + + pub fn handle_tap(&mut self, current_frame_time: u32) -> Option { + self.record_tap(current_frame_time); + self.calculate_tempo() + } + + fn record_tap(&mut self, frame_time: u32) { + // Check if valid interval range + if self.tap_count > 0 { + let last_tap = self.tap_times[self.tap_count - 1]; + if frame_time - last_tap < (Self::MIN_INTERVAL_MS * self.sample_rate / 1000) as u32 { + return; + } + if frame_time - last_tap > (Self::MAX_INTERVAL_MS * self.sample_rate / 1000) as u32 { + self.tap_count = 0; + } + } + // Add new entry + if self.tap_count < Self::MAX_TAPS { + self.tap_times[self.tap_count] = frame_time; + self.tap_count += 1; + } else { + self.tap_times[0] = frame_time; + self.tap_times.rotate_left(1); + } + } + + fn calculate_tempo(&self) -> Option { + if self.tap_count < Self::MIN_TAPS { + return None; + } + + let mut total_interval = 0u64; + let interval_count = self.tap_count - 1; + + for i in 1..self.tap_count { + let interval = self.tap_times[i] - self.tap_times[i - 1]; + total_interval += interval as u64; + } + + let average_interval = (total_interval / interval_count as u64) as usize; + Some(average_interval) + } +} diff --git a/audio_engine/src/track.rs b/audio_engine/src/track.rs index 3fb7c6e..622cad7 100644 --- a/audio_engine/src/track.rs +++ b/audio_engine/src/track.rs @@ -107,6 +107,10 @@ impl Track { self.audio_data.set_consolidated_buffer(buffer) } + pub fn is_empty(&self) -> bool { + matches!(self.current_state, TrackState::Empty) && self.audio_data.is_empty() + } + pub fn handle_xrun( &mut self, beat_in_missed: Option, diff --git a/audio_engine/src/track_matrix.rs b/audio_engine/src/track_matrix.rs index 2e39aec..34f813a 100644 --- a/audio_engine/src/track_matrix.rs +++ b/audio_engine/src/track_matrix.rs @@ -40,6 +40,18 @@ impl TrackMatrix bool { + self.columns + .iter() + .all(|column| column.is_all_tracks_cleared()) + } + + pub fn set_frames_per_beat(&mut self, new_frames_per_beat: usize) { + for column in &mut self.columns { + column.set_frames_per_beat(new_frames_per_beat); + } + } + pub fn process( &mut self, ps: &jack::ProcessScope, diff --git a/mapper/src/app.rs b/mapper/src/app.rs index f35074c..a9034bc 100644 --- a/mapper/src/app.rs +++ b/mapper/src/app.rs @@ -43,7 +43,6 @@ impl SelectedRegister { } } - pub struct App { pub structured_notes: Vec, pub register_values: HashMap, @@ -67,9 +66,9 @@ impl App { map.insert("IC11".to_string(), 8); map }; - + let structured_form = StructuredInputForm::new(register_values.clone()); - + Self { structured_notes: Vec::new(), register_values, @@ -79,7 +78,8 @@ impl App { cursor_position: 0, notes_list_state: ListState::default(), command_sender, - status_message: "Ready - Press 'n' for new note, ←→ for registers, 'h' for help".to_string(), + status_message: "Ready - Press 'n' for new note, ←→ for registers, 'h' for help" + .to_string(), show_help: false, structured_form, } @@ -87,13 +87,15 @@ impl App { pub fn add_structured_note(&mut self) { let note = self.structured_form.finalize_note(); - + // Check if a note already exists for this register state let current_values = self.register_values.clone(); - - if let Some(existing_note) = self.structured_notes.iter_mut().find(|n| { - n.register_values == current_values - }) { + + if let Some(existing_note) = self + .structured_notes + .iter_mut() + .find(|n| n.register_values == current_values) + { *existing_note = note; self.status_message = "Note updated".to_string(); } else { @@ -104,35 +106,40 @@ impl App { pub fn start_structured_input(&mut self) { self.structured_form.reset(self.register_values.clone()); - + // Check if a note already exists for this register state and pre-load it let current_values = self.register_values.clone(); - if let Some(existing_note) = self.structured_notes.iter().find(|n| { - n.register_values == current_values - }) { + if let Some(existing_note) = self + .structured_notes + .iter() + .find(|n| n.register_values == current_values) + { self.structured_form.current_note = existing_note.clone(); } - + self.input_mode = InputMode::StructuredInput; } pub fn get_current_value(&self) -> u8 { - *self.register_values.get(self.selected_register.as_str()).unwrap_or(&0) + *self + .register_values + .get(self.selected_register.as_str()) + .unwrap_or(&0) } pub fn set_current_value(&mut self, value: u8) { - self.register_values.insert(self.selected_register.as_str().to_string(), value); + self.register_values + .insert(self.selected_register.as_str().to_string(), value); self.send_midi_value(value); } pub fn send_midi_value(&self, value: u8) { let (cc_low, cc_high) = self.selected_register.cc_pair(); - + if value <= 127 { - let _ = self.command_sender.send(MidiCommand::ControlChange { - cc: cc_low, - value, - }); + let _ = self + .command_sender + .send(MidiCommand::ControlChange { cc: cc_low, value }); } else { let _ = self.command_sender.send(MidiCommand::ControlChange { cc: cc_high, @@ -146,15 +153,19 @@ impl App { let mask = 1 << bit; let new_value = current ^ mask; self.set_current_value(new_value); - self.status_message = format!("Toggled {}:bit{} ({})", - self.selected_register.as_str(), bit, new_value); + self.status_message = format!( + "Toggled {}:bit{} ({})", + self.selected_register.as_str(), + bit, + new_value + ); } pub fn save_mapping(&self, filename: &str) -> Result<(), Box> { // Convert structured notes to legacy format for saving let legacy_data = MappingData::from_structured_notes( self.structured_notes.clone(), - self.register_values.clone() + self.register_values.clone(), ); let json = serde_json::to_string_pretty(&legacy_data)?; fs::write(filename, json)?; @@ -164,11 +175,11 @@ impl App { pub fn load_mapping(&mut self, filename: &str) -> Result<(), Box> { let content = fs::read_to_string(filename)?; let legacy_data: MappingData = serde_json::from_str(&content)?; - + // Convert legacy format to structured notes for editing self.structured_notes = legacy_data.to_structured_notes(); self.register_values = legacy_data.register_values.clone(); - + // Send current register values to hardware for (register, &value) in &legacy_data.register_values { match register.as_str() { @@ -193,7 +204,7 @@ impl App { _ => {} } } - + Ok(()) } @@ -252,7 +263,7 @@ impl App { fn handle_text_input(&mut self, key: KeyCode) { let current_field = self.structured_form.current_field; let cursor_pos = self.structured_form.cursor_position; - + if let Some(current_text) = self.structured_form.get_text_value_mut(¤t_field) { match key { KeyCode::Char(c) => { @@ -271,7 +282,7 @@ impl App { _ => {} } } - + // Update cursor position after text operations match key { KeyCode::Char(_) => { @@ -303,4 +314,4 @@ impl App { _ => {} } } -} \ No newline at end of file +} diff --git a/mapper/src/data.rs b/mapper/src/data.rs index dd2ba80..d917c9b 100644 --- a/mapper/src/data.rs +++ b/mapper/src/data.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; pub struct StructuredNote { pub description: String, pub register_values: HashMap, - + // LED states (0/1) pub switch_1: Option, pub switch_2: Option, @@ -20,12 +20,12 @@ pub struct StructuredNote { pub config: Option, pub expression_pedal_a: Option, pub expression_pedal_b: Option, - + // Display segments (strings of active segments) pub display_1: String, pub display_2: String, pub display_3: String, - + // Button states (strings of active button numbers) pub button_leds: String, pub button_presses: String, @@ -87,13 +87,13 @@ impl Default for MappingData { impl StructuredNote { pub fn is_empty(&self) -> bool { - self.switch_1.is_none() && - self.switch_2.is_none() && - self.display_1.is_empty() && - self.display_2.is_empty() && - self.display_3.is_empty() && - self.button_leds.is_empty() && - self.button_presses.is_empty() + self.switch_1.is_none() + && self.switch_2.is_none() + && self.display_1.is_empty() + && self.display_2.is_empty() + && self.display_3.is_empty() + && self.button_leds.is_empty() + && self.button_presses.is_empty() } pub fn generate_description(&self) -> String { @@ -101,11 +101,15 @@ impl StructuredNote { if text.is_empty() { String::new() } else { - text.chars().map(|c| c.to_string()).collect::>().join(", ") + text.chars() + .map(|c| c.to_string()) + .collect::>() + .join(", ") } }; - - format!("{}{} {}{}{}{}{} {}{}{}{} {}{} [{}] [{}] [{}] [{}] [{}]", + + format!( + "{}{} {}{}{}{}{} {}{}{}{} {}{} [{}] [{}] [{}] [{}] [{}]", self.switch_1.unwrap_or(0), self.switch_2.unwrap_or(0), self.switches.unwrap_or(0), @@ -146,7 +150,7 @@ impl StructuredNote { // Split the description into parts let parts: Vec<&str> = description.split_whitespace().collect(); - + if parts.len() >= 4 { // Parse LED states from first 4 parts // Part 0: switch1+switch2 (e.g., "01") @@ -160,7 +164,7 @@ impl StructuredNote { } } } - + // Part 1: switches+select+number+value1+value2 (e.g., "10000") if let Some(middle_bits) = parts.get(1) { let chars: Vec = middle_bits.chars().collect(); @@ -172,7 +176,7 @@ impl StructuredNote { structured_note.value_2 = chars[4].to_digit(10).map(|d| d as u8); } } - + // Part 2: direct_select+midi_function+midi_chan+config (e.g., "1101") if let Some(last_bits) = parts.get(2) { let chars: Vec = last_bits.chars().collect(); @@ -183,7 +187,7 @@ impl StructuredNote { structured_note.config = chars[3].to_digit(10).map(|d| d as u8); } } - + // Part 3: expression_pedal_a+expression_pedal_b (e.g., "00") if let Some(pedals) = parts.get(3) { let chars: Vec = pedals.chars().collect(); @@ -193,7 +197,7 @@ impl StructuredNote { } } } - + // Parse the bracketed sections for text fields let bracket_sections = Self::extract_bracket_sections(description); if bracket_sections.len() >= 5 { @@ -206,12 +210,12 @@ impl StructuredNote { Some(structured_note) } - + fn extract_bracket_sections(text: &str) -> Vec { let mut sections = Vec::new(); let mut current_section = String::new(); let mut inside_brackets = false; - + for ch in text.chars() { match ch { '[' => { @@ -238,13 +242,16 @@ impl StructuredNote { } } } - + sections } } impl MappingData { - pub fn from_structured_notes(notes: Vec, register_values: HashMap) -> Self { + pub fn from_structured_notes( + notes: Vec, + register_values: HashMap, + ) -> Self { let legacy_notes = notes.iter().map(|n| n.to_register_note()).collect(); Self { notes: legacy_notes, @@ -258,4 +265,4 @@ impl MappingData { .filter_map(StructuredNote::from_register_note) .collect() } -} \ No newline at end of file +} diff --git a/mapper/src/input_form.rs b/mapper/src/input_form.rs index 6001209..d1c9b27 100644 --- a/mapper/src/input_form.rs +++ b/mapper/src/input_form.rs @@ -101,19 +101,32 @@ impl InputField { } pub fn is_led_field(&self) -> bool { - matches!(self, - InputField::Switch1 | InputField::Switch2 | InputField::Switches | - InputField::Select | InputField::Number | InputField::Value1 | - InputField::Value2 | InputField::DirectSelect | InputField::MidiFunction | - InputField::MidiChan | InputField::Config | InputField::ExpressionPedalA | - InputField::ExpressionPedalB + matches!( + self, + InputField::Switch1 + | InputField::Switch2 + | InputField::Switches + | InputField::Select + | InputField::Number + | InputField::Value1 + | InputField::Value2 + | InputField::DirectSelect + | InputField::MidiFunction + | InputField::MidiChan + | InputField::Config + | InputField::ExpressionPedalA + | InputField::ExpressionPedalB ) } pub fn is_text_field(&self) -> bool { - matches!(self, - InputField::Display1 | InputField::Display2 | InputField::Display3 | - InputField::ButtonLeds | InputField::ButtonPresses + matches!( + self, + InputField::Display1 + | InputField::Display2 + | InputField::Display3 + | InputField::ButtonLeds + | InputField::ButtonPresses ) } } @@ -128,7 +141,7 @@ impl StructuredInputForm { pub fn new(register_values: HashMap) -> Self { let mut current_note = StructuredNote::default(); current_note.register_values = register_values; - + Self { current_note, current_field: InputField::Switch1, @@ -211,21 +224,30 @@ impl StructuredInputForm { pub fn draw_structured_input_form(f: &mut Frame, form: &StructuredInputForm, area: Rect) { let mut lines = Vec::new(); - + // Helper function to create field line let create_field_line = |field: &InputField, form: &StructuredInputForm| -> Line { let is_current = form.current_field == *field; let prefix = if is_current { "→ " } else { " " }; - let style = if is_current { - Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) - } else { - Style::default() + let style = if is_current { + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD) + } else { + Style::default() }; - + if field.is_led_field() { let value = form.get_led_value(field); - let value_str = match value { Some(1) => "1", Some(0) => "0", _ => "─" }; - Line::from(vec![Span::styled(format!("{}{}: {}", prefix, field.name().to_lowercase(), value_str), style)]) + let value_str = match value { + Some(1) => "1", + Some(0) => "0", + _ => "─", + }; + Line::from(vec![Span::styled( + format!("{}{}: {}", prefix, field.name().to_lowercase(), value_str), + style, + )]) } else { let text = form.get_text_value(field); let display_text = if is_current { @@ -239,7 +261,15 @@ pub fn draw_structured_input_form(f: &mut Frame, form: &StructuredInputForm, are } else { text.to_string() }; - Line::from(vec![Span::styled(format!("{}{} (list of active segments): {}", prefix, field.name().to_lowercase(), display_text), style)]) + Line::from(vec![Span::styled( + format!( + "{}{} (list of active segments): {}", + prefix, + field.name().to_lowercase(), + display_text + ), + style, + )]) } }; @@ -278,7 +308,10 @@ pub fn draw_structured_input_form(f: &mut Frame, form: &StructuredInputForm, are lines.push(create_field_line(&InputField::ButtonLeds, form)); lines.push(create_field_line(&InputField::ButtonPresses, form)); - let block = Paragraph::new(lines) - .block(Block::default().borders(Borders::ALL).title("Structured Note Input")); + let block = Paragraph::new(lines).block( + Block::default() + .borders(Borders::ALL) + .title("Structured Note Input"), + ); f.render_widget(block, area); -} \ No newline at end of file +} diff --git a/mapper/src/main.rs b/mapper/src/main.rs index 909ba84..03970e0 100644 --- a/mapper/src/main.rs +++ b/mapper/src/main.rs @@ -1,27 +1,25 @@ use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - event::{DisableMouseCapture, EnableMouseCapture}, -}; -use ratatui::{ - backend::CrosstermBackend, - Terminal, }; use kanal::{Receiver, Sender}; +use ratatui::{backend::CrosstermBackend, Terminal}; use std::{error::Error, io}; -mod midi; +mod app; mod data; mod input_form; +mod midi; mod ui; -mod app; use app::App; use midi::{MidiCommand, MidiSender}; use ui::run_app; fn main() -> Result<(), Box> { // Create JACK client - let (client, status) = jack::Client::new("FCB1010_Mapper", jack::ClientOptions::NO_START_SERVER)?; + let (client, status) = + jack::Client::new("FCB1010_Mapper", jack::ClientOptions::NO_START_SERVER)?; if !status.is_empty() { eprintln!("JACK client status: {:?}", status); } @@ -61,4 +59,4 @@ fn main() -> Result<(), Box> { } Ok(()) -} \ No newline at end of file +} diff --git a/mapper/src/midi.rs b/mapper/src/midi.rs index 43c67a0..f8af4c4 100644 --- a/mapper/src/midi.rs +++ b/mapper/src/midi.rs @@ -50,4 +50,4 @@ impl jack::ProcessHandler for MidiSender { jack::Control::Continue } -} \ No newline at end of file +} diff --git a/mapper/src/ui.rs b/mapper/src/ui.rs index 764bc94..54274e6 100644 --- a/mapper/src/ui.rs +++ b/mapper/src/ui.rs @@ -7,9 +7,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, - widgets::{ - Block, Borders, Clear, List, ListItem, Paragraph, Wrap, - }, + widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap}, Frame, Terminal, }; use std::error::Error; @@ -23,7 +21,10 @@ pub fn ui(f: &mut Frame, app: &mut App) { draw_structured_ui(f, app); // Input popup if needed - if matches!(app.input_mode, InputMode::EditValue | InputMode::SaveFile | InputMode::LoadFile) { + if matches!( + app.input_mode, + InputMode::EditValue | InputMode::SaveFile | InputMode::LoadFile + ) { draw_input_popup(f, app); } } @@ -41,7 +42,11 @@ fn draw_structured_ui(f: &mut Frame, app: &mut App) { // Title let title = Paragraph::new("FCB1010 Structured Note Mapper") - .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) + .style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ) .alignment(Alignment::Center) .block(Block::default().borders(Borders::ALL)); f.render_widget(title, chunks[0]); @@ -76,24 +81,35 @@ fn draw_structured_notes_list(f: &mut Frame, app: &mut App, area: Rect) { .enumerate() .map(|(i, note)| { let mut content = String::new(); - + // Add register values in binary format first let ic03 = note.register_values.get("IC03").unwrap_or(&0); let ic10 = note.register_values.get("IC10").unwrap_or(&0); let ic11 = note.register_values.get("IC11").unwrap_or(&0); - content.push_str(&format!("[IC03:{:08b}, IC10:{:08b}, IC11:{:08b}]", ic03, ic10, ic11)); - + content.push_str(&format!( + "[IC03:{:08b}, IC10:{:08b}, IC11:{:08b}]", + ic03, ic10, ic11 + )); + // Add dash separator and note description content.push_str(&format!(" - {}. {}", i + 1, note.description)); - + ListItem::new(content) }) .collect(); let mut notes_list_state = app.notes_list_state.clone(); let notes_list = List::new(items) - .block(Block::default().borders(Borders::ALL).title("Structured Notes (Enter to restore register values, Del to remove)")) - .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .block( + Block::default() + .borders(Borders::ALL) + .title("Structured Notes (Enter to restore register values, Del to remove)"), + ) + .highlight_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) .highlight_symbol("→ "); f.render_stateful_widget(notes_list, area, &mut notes_list_state); @@ -102,7 +118,11 @@ fn draw_structured_notes_list(f: &mut Frame, app: &mut App, area: Rect) { fn draw_register_values(f: &mut Frame, app: &App, area: Rect) { let chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(33), Constraint::Percentage(33), Constraint::Percentage(34)]) + .constraints([ + Constraint::Percentage(33), + Constraint::Percentage(33), + Constraint::Percentage(34), + ]) .split(area); let registers = [ @@ -117,7 +137,10 @@ fn draw_register_values(f: &mut Frame, app: &App, area: Rect) { let mut lines = Vec::new(); lines.push(Line::from(vec![ - Span::styled(format!("{}: ", name), Style::default().add_modifier(Modifier::BOLD)), + Span::styled( + format!("{}: ", name), + Style::default().add_modifier(Modifier::BOLD), + ), Span::raw(format!("{:08b} = {}", value, value)), ])); @@ -126,18 +149,26 @@ fn draw_register_values(f: &mut Frame, app: &App, area: Rect) { for bit in (0..8).rev() { let is_set = (value >> bit) & 1 == 1; let style = if is_set { - Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD) } else { Style::default().fg(Color::DarkGray) }; - bit_spans.push(Span::styled(format!("[{}]", if is_set { "■" } else { "□" }), style)); + bit_spans.push(Span::styled( + format!("[{}]", if is_set { "■" } else { "□" }), + style, + )); } lines.push(Line::from(bit_spans)); // Bit numbers let mut bit_num_spans = Vec::new(); for bit in (0..8).rev() { - bit_num_spans.push(Span::styled(format!(" {} ", bit), Style::default().fg(Color::Yellow))); + bit_num_spans.push(Span::styled( + format!(" {} ", bit), + Style::default().fg(Color::Yellow), + )); } lines.push(Line::from(bit_num_spans)); @@ -150,14 +181,17 @@ fn draw_register_values(f: &mut Frame, app: &App, area: Rect) { let block = Block::default() .borders(Borders::ALL) .style(border_style) - .title(if is_selected { format!("→ {}", name) } else { name.to_string() }); + .title(if is_selected { + format!("→ {}", name) + } else { + name.to_string() + }); let paragraph = Paragraph::new(lines).block(block); f.render_widget(paragraph, chunks[i]); } } - fn draw_input_popup(f: &mut Frame, app: &App) { let area = centered_rect(60, 20, f.size()); f.render_widget(Clear, area); @@ -177,30 +211,44 @@ fn draw_input_popup(f: &mut Frame, app: &App) { // Create text with cursor using char-aware operations let mut spans = Vec::new(); let chars: Vec = app.input_buffer.chars().collect(); - + if chars.is_empty() { - spans.push(Span::styled("█", Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); + spans.push(Span::styled( + "█", + Style::default() + .fg(Color::White) + .add_modifier(Modifier::REVERSED), + )); } else { if app.cursor_position > 0 { let before_cursor: String = chars.iter().take(app.cursor_position).collect(); spans.push(Span::raw(before_cursor)); } - + if app.cursor_position < chars.len() { let cursor_char = chars[app.cursor_position]; - spans.push(Span::styled(cursor_char.to_string(), Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); - + spans.push(Span::styled( + cursor_char.to_string(), + Style::default() + .fg(Color::White) + .add_modifier(Modifier::REVERSED), + )); + if app.cursor_position + 1 < chars.len() { let after_cursor: String = chars.iter().skip(app.cursor_position + 1).collect(); spans.push(Span::raw(after_cursor)); } } else { - spans.push(Span::styled("█", Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); + spans.push(Span::styled( + "█", + Style::default() + .fg(Color::White) + .add_modifier(Modifier::REVERSED), + )); } } - let paragraph = Paragraph::new(Line::from(spans)) - .block(block); + let paragraph = Paragraph::new(Line::from(spans)).block(block); f.render_widget(paragraph, area); } @@ -254,12 +302,11 @@ fn draw_help(f: &mut Frame) { "Press 'h' again to return to main view", ]; - let help_paragraph = Paragraph::new( help_text .iter() .map(|&line| Line::from(line)) - .collect::>() + .collect::>(), ) .block(Block::default().title("Help").borders(Borders::ALL)) .wrap(Wrap { trim: true }); @@ -287,10 +334,7 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { .split(popup_layout[1])[1] } -pub fn run_app( - terminal: &mut Terminal, - mut app: App, -) -> Result<(), Box> { +pub fn run_app(terminal: &mut Terminal, mut app: App) -> Result<(), Box> { loop { terminal.draw(|f| ui(f, &mut app))?; @@ -312,7 +356,10 @@ pub fn handle_input(app: &mut App, key: crossterm::event::KeyCode) -> Result Result> { +fn handle_legacy_input( + app: &mut App, + key: crossterm::event::KeyCode, +) -> Result> { match app.input_mode { InputMode::Normal => handle_normal_input(app, key), InputMode::EditValue => handle_edit_value_input(app, key), @@ -322,9 +369,12 @@ fn handle_legacy_input(app: &mut App, key: crossterm::event::KeyCode) -> Result< } } -fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result> { +fn handle_normal_input( + app: &mut App, + key: crossterm::event::KeyCode, +) -> Result> { use crossterm::event::KeyCode; - + match key { KeyCode::Char('q') => { let _ = app.command_sender.send(crate::midi::MidiCommand::Quit); @@ -378,7 +428,7 @@ fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result< } KeyCode::Up => { let notes_len = app.structured_notes.len(); - + if let Some(selected) = app.notes_list_state.selected() { if selected > 0 { app.notes_list_state.select(Some(selected - 1)); @@ -389,7 +439,7 @@ fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result< } KeyCode::Down => { let notes_len = app.structured_notes.len(); - + if let Some(selected) = app.notes_list_state.selected() { if selected < notes_len.saturating_sub(1) { app.notes_list_state.select(Some(selected + 1)); @@ -404,7 +454,7 @@ fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result< // Restore register values from note app.register_values = note.register_values.clone(); app.status_message = "Register values restored from note".to_string(); - + // Send MIDI updates for all registers for (register_name, &value) in &app.register_values { match register_name.as_str() { @@ -439,7 +489,8 @@ fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result< if app.structured_notes.is_empty() { app.notes_list_state.select(None); } else if selected >= app.structured_notes.len() { - app.notes_list_state.select(Some(app.structured_notes.len() - 1)); + app.notes_list_state + .select(Some(app.structured_notes.len() - 1)); } app.status_message = "Note deleted".to_string(); } @@ -452,16 +503,19 @@ fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result< // Implement other input handlers (edit_value, save_file, load_file) -fn handle_edit_value_input(app: &mut App, key: crossterm::event::KeyCode) -> Result> { +fn handle_edit_value_input( + app: &mut App, + key: crossterm::event::KeyCode, +) -> Result> { use crossterm::event::KeyCode; - + match key { KeyCode::Enter => { match app.input_buffer.parse::() { Ok(value) => { app.set_current_value(value); - app.status_message = format!("{} set to {}", - app.selected_register.as_str(), value); + app.status_message = + format!("{} set to {}", app.selected_register.as_str(), value); } Err(_) => { app.status_message = "Invalid value (0-255)".to_string(); @@ -510,7 +564,9 @@ fn handle_edit_value_input(app: &mut App, key: crossterm::event::KeyCode) -> Res } } KeyCode::Char(c) => { - if let Ok(potential_value) = format!("{}{}", &app.input_buffer[..app.cursor_position], c).parse::() { + if let Ok(potential_value) = + format!("{}{}", &app.input_buffer[..app.cursor_position], c).parse::() + { if potential_value <= 255 { let mut chars: Vec = app.input_buffer.chars().collect(); chars.insert(app.cursor_position, c); @@ -529,9 +585,12 @@ fn handle_edit_value_input(app: &mut App, key: crossterm::event::KeyCode) -> Res Ok(false) } -fn handle_save_file_input(app: &mut App, key: crossterm::event::KeyCode) -> Result> { +fn handle_save_file_input( + app: &mut App, + key: crossterm::event::KeyCode, +) -> Result> { use crossterm::event::KeyCode; - + match key { KeyCode::Enter => { match app.save_mapping(&app.input_buffer) { @@ -591,9 +650,12 @@ fn handle_save_file_input(app: &mut App, key: crossterm::event::KeyCode) -> Resu Ok(false) } -fn handle_load_file_input(app: &mut App, key: crossterm::event::KeyCode) -> Result> { +fn handle_load_file_input( + app: &mut App, + key: crossterm::event::KeyCode, +) -> Result> { use crossterm::event::KeyCode; - + match key { KeyCode::Enter => { let filename = app.input_buffer.clone(); @@ -648,4 +710,4 @@ fn handle_load_file_input(app: &mut App, key: crossterm::event::KeyCode) -> Resu _ => {} } Ok(false) -} \ No newline at end of file +} diff --git a/xtask/src/collect.rs b/xtask/src/collect.rs index d884ba3..ce1d0bd 100644 --- a/xtask/src/collect.rs +++ b/xtask/src/collect.rs @@ -27,33 +27,33 @@ pub async fn collect_dir_output(dir: &str, output: &str) { pub async fn collect_image_output(output: &str) { use std::fs; - + let mut content = String::new(); content.push_str("File Contents:\n"); - + // Collect specific files mentioned by user let files_to_collect = vec![ "image/conf/bblayers.conf", - "image/conf/local.conf", + "image/conf/local.conf", "image/conf/templateconf.cfg", "image/docker-compose.yml", "image/Dockerfile", "image/implementation_plan.md", "image/run", ]; - + for file_path in files_to_collect { if let Ok(file_content) = fs::read_to_string(file_path) { content.push_str(&format!("\n===== FILE: {} =====\n", file_path)); content.push_str(&file_content); } } - + // Collect meta-fcb-looper files if fs::read_dir("image/meta-layers/meta-fcb-looper").is_ok() { collect_directory_recursive("image/meta-layers/meta-fcb-looper", &mut content); } - + // Write to output file if let Err(e) = fs::write(output, content) { eprintln!("Failed to write output file: {}", e); @@ -64,15 +64,18 @@ pub async fn collect_image_output(output: &str) { fn collect_directory_recursive(dir_path: &str, content: &mut String) { use std::fs; - + if let Ok(entries) = fs::read_dir(dir_path) { for entry in entries.flatten() { let path = entry.path(); if path.is_file() { let file_path = path.to_string_lossy(); - if file_path.ends_with(".bb") || file_path.ends_with(".bbappend") || - file_path.ends_with(".conf") || file_path.ends_with(".wks") || - file_path.ends_with(".service") { + if file_path.ends_with(".bb") + || file_path.ends_with(".bbappend") + || file_path.ends_with(".conf") + || file_path.ends_with(".wks") + || file_path.ends_with(".service") + { if let Ok(file_content) = fs::read_to_string(&path) { content.push_str(&format!("\n===== FILE: {} =====\n", file_path)); content.push_str(&file_content); diff --git a/xtask/src/mapper.rs b/xtask/src/mapper.rs index 8788f72..0aa3c3b 100644 --- a/xtask/src/mapper.rs +++ b/xtask/src/mapper.rs @@ -26,10 +26,10 @@ pub async fn mapper() { println!("Starting qjackctl..."); let qjackctl = spawn_qjackctl().await; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - + println!("Starting firmware debugger in new terminal..."); let firmware = spawn_firmware_in_terminal().await; - + println!("Starting mapper..."); let mapper = spawn_mapper().await; @@ -108,7 +108,10 @@ async fn spawn_firmware_in_terminal() -> Child { #[cfg(target_os = "windows")] { Command::new("C:\\Program Files\\Git\\git-bash.exe") - .args(["-c", "cd firmware && cargo run; read -p 'Press Enter to close...'"]) + .args([ + "-c", + "cd firmware && cargo run; read -p 'Press Enter to close...'", + ]) .spawn() .expect("Could not start firmware debugger in git-bash") } @@ -143,11 +146,12 @@ async fn kill_process(child: &mut Child, name: &str) { Err(e) => { // Don't print error if process already exited let error_msg = e.to_string().to_lowercase(); - if !error_msg.contains("no such process") && - !error_msg.contains("not found") && - !error_msg.contains("invalid argument") { + if !error_msg.contains("no such process") + && !error_msg.contains("not found") + && !error_msg.contains("invalid argument") + { eprintln!("Failed to stop {}: {}", name, e); } } } -} \ No newline at end of file +}