diff --git a/src/app.rs b/src/app.rs index 904397d..36b5f36 100644 --- a/src/app.rs +++ b/src/app.rs @@ -98,6 +98,7 @@ impl App { show_scope: self.audio.config.show_scope, show_spectrum: self.audio.config.show_spectrum, show_completion: self.ui.show_completion, + flash_brightness: self.ui.flash_brightness, }, link: crate::settings::LinkSettings { enabled: link.is_enabled(), diff --git a/src/input.rs b/src/input.rs index eb4aeb6..c770a01 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1163,6 +1163,11 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { OptionsFocus::ShowCompletion => { ctx.app.ui.show_completion = !ctx.app.ui.show_completion } + OptionsFocus::FlashBrightness => { + let delta = if key.code == KeyCode::Left { -0.1 } else { 0.1 }; + ctx.app.ui.flash_brightness = + (ctx.app.ui.flash_brightness + delta).clamp(0.0, 1.0); + } OptionsFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()), OptionsFocus::StartStopSync => ctx .link diff --git a/src/main.rs b/src/main.rs index 85e5f85..2f02f53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,6 +94,7 @@ fn main() -> io::Result<()> { app.audio.config.show_scope = settings.display.show_scope; app.audio.config.show_spectrum = settings.display.show_spectrum; app.ui.show_completion = settings.display.show_completion; + app.ui.flash_brightness = settings.display.flash_brightness; let metrics = Arc::new(EngineMetrics::default()); let scope_buffer = Arc::new(ScopeBuffer::new()); @@ -212,6 +213,13 @@ fn main() -> io::Result<()> { app.metrics.event_count = seq_snapshot.event_count; app.metrics.dropped_events = seq_snapshot.dropped_events; + app.ui.event_flash = (app.ui.event_flash - 0.1).max(0.0); + let new_events = app.metrics.event_count.saturating_sub(app.ui.last_event_count); + if new_events > 0 { + app.ui.event_flash = (new_events as f32 * 0.4).min(1.0); + } + app.ui.last_event_count = app.metrics.event_count; + app.flush_queued_changes(&sequencer.cmd_tx); app.flush_dirty_patterns(&sequencer.cmd_tx); diff --git a/src/settings.rs b/src/settings.rs index d111a7f..f5b987d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -29,8 +29,12 @@ pub struct DisplaySettings { pub show_spectrum: bool, #[serde(default = "default_true")] pub show_completion: bool, + #[serde(default = "default_flash_brightness")] + pub flash_brightness: f32, } +fn default_flash_brightness() -> f32 { 1.0 } + #[derive(Debug, Serialize, Deserialize)] pub struct LinkSettings { pub enabled: bool, @@ -60,6 +64,7 @@ impl Default for DisplaySettings { show_scope: true, show_spectrum: true, show_completion: true, + flash_brightness: 1.0, } } } diff --git a/src/state/options.rs b/src/state/options.rs index d21309d..d0191a8 100644 --- a/src/state/options.rs +++ b/src/state/options.rs @@ -6,6 +6,7 @@ pub enum OptionsFocus { ShowScope, ShowSpectrum, ShowCompletion, + FlashBrightness, LinkEnabled, StartStopSync, Quantum, @@ -23,7 +24,8 @@ impl OptionsState { OptionsFocus::RuntimeHighlight => OptionsFocus::ShowScope, OptionsFocus::ShowScope => OptionsFocus::ShowSpectrum, OptionsFocus::ShowSpectrum => OptionsFocus::ShowCompletion, - OptionsFocus::ShowCompletion => OptionsFocus::LinkEnabled, + OptionsFocus::ShowCompletion => OptionsFocus::FlashBrightness, + OptionsFocus::FlashBrightness => OptionsFocus::LinkEnabled, OptionsFocus::LinkEnabled => OptionsFocus::StartStopSync, OptionsFocus::StartStopSync => OptionsFocus::Quantum, OptionsFocus::Quantum => OptionsFocus::RefreshRate, @@ -37,7 +39,8 @@ impl OptionsState { OptionsFocus::ShowScope => OptionsFocus::RuntimeHighlight, OptionsFocus::ShowSpectrum => OptionsFocus::ShowScope, OptionsFocus::ShowCompletion => OptionsFocus::ShowSpectrum, - OptionsFocus::LinkEnabled => OptionsFocus::ShowCompletion, + OptionsFocus::FlashBrightness => OptionsFocus::ShowCompletion, + OptionsFocus::LinkEnabled => OptionsFocus::FlashBrightness, OptionsFocus::StartStopSync => OptionsFocus::LinkEnabled, OptionsFocus::Quantum => OptionsFocus::StartStopSync, }; diff --git a/src/state/ui.rs b/src/state/ui.rs index 54049b5..974f0ef 100644 --- a/src/state/ui.rs +++ b/src/state/ui.rs @@ -38,6 +38,9 @@ pub struct UiState { pub runtime_highlight: bool, pub show_completion: bool, pub minimap_until: Option, + pub last_event_count: usize, + pub event_flash: f32, + pub flash_brightness: f32, } impl Default for UiState { @@ -61,6 +64,9 @@ impl Default for UiState { runtime_highlight: false, show_completion: true, minimap_until: None, + last_event_count: 0, + event_flash: 0.0, + flash_brightness: 1.0, } } } diff --git a/src/views/main_view.rs b/src/views/main_view.rs index f8d69c9..14053be 100644 --- a/src/views/main_view.rs +++ b/src/views/main_view.rs @@ -41,7 +41,24 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: } render_sequencer(frame, app, snapshot, sequencer_area); - render_vu_meter(frame, app, vu_area); + + // Calculate actual grid height to align VU meter + let pattern = app.current_edit_pattern(); + let page = app.editor_ctx.step / STEPS_PER_PAGE; + let page_start = page * STEPS_PER_PAGE; + let steps_on_page = (page_start + STEPS_PER_PAGE).min(pattern.length) - page_start; + let num_rows = steps_on_page.div_ceil(8); + let spacing = num_rows.saturating_sub(1) as u16; + let row_height = sequencer_area.height.saturating_sub(spacing) / num_rows as u16; + let actual_grid_height = row_height * num_rows as u16 + spacing; + + let aligned_vu_area = Rect { + y: sequencer_area.y, + height: actual_grid_height, + ..vu_area + }; + + render_vu_meter(frame, app, aligned_vu_area); } const STEPS_PER_PAGE: usize = 32; diff --git a/src/views/options_view.rs b/src/views/options_view.rs index 83f5ed7..73ef57b 100644 --- a/src/views/options_view.rs +++ b/src/views/options_view.rs @@ -29,7 +29,7 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { }; let [display_area, _, link_area, _, session_area] = Layout::vertical([ - Constraint::Length(7), + Constraint::Length(8), Constraint::Length(1), Constraint::Length(5), Constraint::Length(1), @@ -63,6 +63,7 @@ fn render_display_section(frame: &mut Frame, app: &App, area: Rect) { ); let focus = app.options.focus; + let flash_str = format!("{:.0}%", app.ui.flash_brightness * 100.0); let lines = vec![ render_option_line( "Refresh rate", @@ -93,6 +94,11 @@ fn render_display_section(frame: &mut Frame, app: &App, area: Rect) { if app.ui.show_completion { "On" } else { "Off" }, focus == OptionsFocus::ShowCompletion, ), + render_option_line( + "Flash brightness", + &flash_str, + focus == OptionsFocus::FlashBrightness, + ), ]; frame.render_widget(Paragraph::new(lines), content_area); diff --git a/src/views/render.rs b/src/views/render.rs index c84f710..374ed74 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -41,9 +41,20 @@ fn adjust_spans_for_line( pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot) { let term = frame.area(); + + let bg_color = if app.ui.event_flash > 0.0 { + let i = (app.ui.event_flash * app.ui.flash_brightness * 60.0) as u8; + Color::Rgb(i, i, i) + } else { + Color::Reset + }; + let blank = " ".repeat(term.width as usize); let lines: Vec = (0..term.height).map(|_| Line::raw(&blank)).collect(); - frame.render_widget(Paragraph::new(lines), term); + frame.render_widget( + Paragraph::new(lines).style(Style::default().bg(bg_color)), + term, + ); if app.ui.show_title { title_view::render(frame, term, &app.ui); diff --git a/website/cagire.png b/website/cagire.png new file mode 100644 index 0000000..5aaff02 Binary files /dev/null and b/website/cagire.png differ diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..f5269d7 --- /dev/null +++ b/website/index.html @@ -0,0 +1,162 @@ + + + + + + Cagire + + + +
+
+
+  ██████╗ █████╗  ██████╗ ██╗██████╗ ███████╗
+ ██╔════╝██╔══██╗██╔════╝ ██║██╔══██╗██╔════╝
+ ██║     ███████║██║  ███╗██║██████╔╝█████╗
+ ██║     ██╔══██║██║   ██║██║██╔══██╗██╔══╝
+ ╚██████╗██║  ██║╚██████╔╝██║██║  ██║███████╗
+  ╚═════╝╚═╝  ╚═╝ ╚═════╝ ╚═╝╚═╝  ╚═╝╚══════╝
+
+ +
+ +
+ Cagire screenshot +

Cagire is a terminal-based step sequencer for live coding music. Each step in a pattern contains a Forth script that produces sound and creates events. Synchronize with other musicians using Ableton Link. Cagire uses its own audio engine for audio synthesis and sampling!

+
+ +
+

Releases

+
+ macOS + Windows + Linux +
+
+ +
+

Credits

+

Cagire is built by BuboBubo (Raphael Maurice Forment).

+

Doux (audio engine) is a Rust port of Dough, originally written in C by Felix Roos.

+

mi-plaits-dsp-rs by Oliver Rockstedt, based on Mutable Instruments Plaits by Emilie Gillet.

+
+ +
+

Support

+

Report issues and contribute on GitHub.

+

Support the project on Ko-fi.

+
+ +
+ +