diff --git a/Cargo.lock b/Cargo.lock index baaa227..5e37cc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -790,12 +790,6 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" -[[package]] -name = "cache-padded" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" - [[package]] name = "cairo-rs" version = "0.18.5" @@ -4675,15 +4669,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ringbuf" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65af18d50f789e74aaf23bbb3f65dcd22a3cb6e029b5bced149f6bd57c5c2a2" -dependencies = [ - "cache-padded", -] - [[package]] name = "ringbuf" version = "0.4.8" @@ -5819,7 +5804,7 @@ dependencies = [ "muda", "open", "reqwest", - "ringbuf 0.4.8", + "ringbuf", "semver", "serde", "serde_json", @@ -5848,7 +5833,7 @@ version = "0.9.0" dependencies = [ "log", "lv2", - "ringbuf 0.2.8", + "ringbuf", "voidmic_core", ] @@ -5861,7 +5846,7 @@ dependencies = [ "egui", "nih_plug", "nih_plug_egui", - "ringbuf 0.2.8", + "ringbuf", "voidmic_core", "voidmic_ui", ] diff --git a/README.md b/README.md index 9aa59a6..4f040b1 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,18 @@ cargo build --release --no-default-features ``` ### 🪟 Windows -1. Install **Rust**. -2. Install **BSVC** (C++ Build Tools). +1. **Install Virtual Audio**: Download [VB-Cable](https://vb-audio.com/Cable/) (free) and reboot. +2. Install **Rust** and **BSVC** (C++ Build Tools). 3. `cargo build --release` 4. Run `.\target\release\voidmic.exe` +5. In VoidMic, select "CABLE Input" as Output. + +### 🍎 macOS +1. **Install Virtual Audio**: `brew install blackhole-2ch` and reboot. +2. Install **Rust** via [rustup.rs](https://rustup.rs). +3. `cargo build --release` +4. Run `./target/release/voidmic` +5. In VoidMic, select "BlackHole 2ch" as Output. ## 🎮 Usage Guide diff --git a/crates/app/src/gui.rs b/crates/app/src/gui.rs index 022f91e..af1a88c 100644 --- a/crates/app/src/gui.rs +++ b/crates/app/src/gui.rs @@ -496,6 +496,7 @@ impl VoidMicApp { ui.horizontal(|ui| { if ui .checkbox(&mut self.config.dynamic_threshold_enabled, "Auto-Gate") + .on_hover_text("Automatically adjusts gate based on ambient noise floor") .changed() { self.config.preset = "Custom".to_string(); @@ -607,53 +608,38 @@ impl VoidMicApp { ui.separator(); // VAD Controls + const VAD_MODES: &[(i32, &str, &str)] = &[ + (0, "Quality", "Quality (Likely Speech)"), + (1, "Low Bitrate", "Low Bitrate"), + (2, "Aggressive", "Aggressive"), + (3, "Very Aggressive", "Very Aggressive"), + ]; ui.horizontal(|ui| { ui.label("VAD Sensitivity:"); + let current_label = VAD_MODES + .iter() + .find(|(v, _, _)| *v == self.config.vad_sensitivity) + .map(|(_, _, full)| *full) + .unwrap_or("Unknown"); egui::ComboBox::from_id_salt("vad_combo") - .selected_text(match self.config.vad_sensitivity { - 0 => "Quality (Likely Speech)", - 1 => "Low Bitrate", - 2 => "Aggressive", - 3 => "Very Aggressive", - _ => "Unknown", - }) + .selected_text(current_label) .show_ui(ui, |ui| { - let mut changed = false; - if ui - .selectable_value(&mut self.config.vad_sensitivity, 0, "Quality") - .clicked() - { - changed = true; - } - if ui - .selectable_value(&mut self.config.vad_sensitivity, 1, "Low Bitrate") - .clicked() - { - changed = true; - } - if ui - .selectable_value(&mut self.config.vad_sensitivity, 2, "Aggressive") - .clicked() - { - changed = true; - } - if ui - .selectable_value(&mut self.config.vad_sensitivity, 3, "Very Aggressive") - .clicked() - { - changed = true; - } - - if changed { - self.mark_config_dirty(); - if let Some(engine) = &self.engine { - engine - .vad_sensitivity - .store(self.config.vad_sensitivity as u32, Ordering::Relaxed); + for (value, label, _) in VAD_MODES { + if ui + .selectable_value(&mut self.config.vad_sensitivity, *value, *label) + .clicked() + { + self.mark_config_dirty(); + if let Some(engine) = &self.engine { + engine + .vad_sensitivity + .store(self.config.vad_sensitivity as u32, Ordering::Relaxed); + } } } }); - ui.label(egui::RichText::new("ℹ️ WebRTC Voice Activity Detection").size(10.0)); + ui.label(egui::RichText::new("ℹ️ WebRTC VAD").size(10.0)) + .on_hover_text("Voice Activity Detection - filters non-speech sounds"); }); ui.separator(); @@ -669,7 +655,7 @@ impl VoidMicApp { }); if self.config.eq_enabled { - ui.horizontal(|ui| { + egui::Grid::new("eq_grid").num_columns(2).show(ui, |ui| { ui.label("Low (Bass):"); if ui .add(egui::Slider::new(&mut self.config.eq_low_gain, -10.0..=10.0).text("dB")) @@ -682,8 +668,8 @@ impl VoidMicApp { .store(self.config.eq_low_gain.to_bits(), Ordering::Relaxed); } } - }); - ui.horizontal(|ui| { + ui.end_row(); + ui.label("Mid (Voice):"); if ui .add(egui::Slider::new(&mut self.config.eq_mid_gain, -10.0..=10.0).text("dB")) @@ -696,8 +682,8 @@ impl VoidMicApp { .store(self.config.eq_mid_gain.to_bits(), Ordering::Relaxed); } } - }); - ui.horizontal(|ui| { + ui.end_row(); + ui.label("High (Treble):"); if ui .add(egui::Slider::new(&mut self.config.eq_high_gain, -10.0..=10.0).text("dB")) @@ -710,6 +696,7 @@ impl VoidMicApp { .store(self.config.eq_high_gain.to_bits(), Ordering::Relaxed); } } + ui.end_row(); }); } @@ -719,6 +706,7 @@ impl VoidMicApp { ui.horizontal(|ui| { if ui .checkbox(&mut self.config.agc_enabled, "Automatic Gain Control (AGC)") + .on_hover_text("Normalizes volume to prevent clipping and boost quiet speech") .changed() { self.mark_config_dirty(); @@ -728,7 +716,6 @@ impl VoidMicApp { .store(self.config.agc_enabled, Ordering::Relaxed); } } - ui.label(egui::RichText::new("ℹ️ Keeps volume consistent").size(10.0)); }); ui.add_space(5.0); @@ -769,6 +756,8 @@ impl VoidMicApp { self.render_spectrum(ui); // Jitter Monitor (Phase 6) + const JITTER_GOOD_US: u32 = 1000; + const JITTER_WARN_US: u32 = 5000; let jitter = self .engine .as_ref() @@ -778,14 +767,15 @@ impl VoidMicApp { ui.add_space(5.0); ui.horizontal(|ui| { ui.label("Latency Health:"); - let color = if jitter < 1000 { + let color = if jitter < JITTER_GOOD_US { egui::Color32::GREEN - } else if jitter < 5000 { + } else if jitter < JITTER_WARN_US { egui::Color32::YELLOW } else { egui::Color32::RED }; - ui.colored_label(color, format!("{} µs jitter (Max)", jitter)); + ui.colored_label(color, format!("{} µs jitter", jitter)) + .on_hover_text("< 1ms = excellent | 1-5ms = acceptable | > 5ms = may cause audio glitches"); }); } } diff --git a/crates/app/src/virtual_device.rs b/crates/app/src/virtual_device.rs index 9819e32..02d772a 100644 --- a/crates/app/src/virtual_device.rs +++ b/crates/app/src/virtual_device.rs @@ -68,16 +68,13 @@ pub fn create_virtual_sink() -> Result { #[cfg(target_os = "windows")] { // On Windows, we can't auto-create. Return instruction to install VB-Cable. - Err("Windows requires VB-Cable. Please install from vb-audio.com/Cable/".to_string()) + Err("Windows requires VB-Cable. Install from: https://vb-audio.com/Cable/".to_string()) } #[cfg(target_os = "macos")] { // On macOS, we can't auto-create. Return instruction to install BlackHole. - Err( - "macOS requires BlackHole. Please install from github.com/ExistentialAudio/BlackHole" - .to_string(), - ) + Err("macOS requires BlackHole. Install via: brew install blackhole-2ch".to_string()) } #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))] diff --git a/crates/core/src/echo_cancel.rs b/crates/core/src/echo_cancel.rs index 5be6e7b..2c6a8ef 100644 --- a/crates/core/src/echo_cancel.rs +++ b/crates/core/src/echo_cancel.rs @@ -2,12 +2,13 @@ //! //! Uses the aec3 crate (Rust port of WebRTC AEC3) for acoustic echo cancellation. -use crate::constants::SAMPLE_RATE; +use crate::constants::{FRAME_SIZE, SAMPLE_RATE}; use aec3::voip::VoipAec3; /// Echo canceller wrapper pub struct EchoCanceller { aec: VoipAec3, + output_buffer: [f32; FRAME_SIZE], // Pre-allocated to avoid heap allocation } impl EchoCanceller { @@ -17,7 +18,10 @@ impl EchoCanceller { let aec = VoipAec3::builder(SAMPLE_RATE as usize, 1, 1) .build() .expect("Failed to create AEC3"); - Self { aec } + Self { + aec, + output_buffer: [0.0; FRAME_SIZE], + } } /// Processes a frame of audio with echo cancellation. @@ -25,23 +29,27 @@ impl EchoCanceller { /// # Arguments /// * `mic_input` - The microphone input (may contain echo). Expected length: 480 (10ms at 48kHz) /// * `speaker_ref` - The reference signal from speakers. Expected length: 480 + /// * `output` - Output buffer to write echo-cancelled signal to /// /// # Returns - /// The echo-cancelled microphone signal - pub fn process_frame(&mut self, mic_input: &[f32], speaker_ref: &[f32]) -> Vec { - let mut out = vec![0.0; mic_input.len()]; + /// `true` if processing succeeded, `false` if fallback to raw input was used + pub fn process_frame(&mut self, mic_input: &[f32], speaker_ref: &[f32], output: &mut [f32]) -> bool { + // Clear output buffer + self.output_buffer.fill(0.0); // Process with AEC3 // level_change = false (we don't track volume changes yet) if let Err(e) = self .aec - .process(mic_input, Some(speaker_ref), false, &mut out) + .process(mic_input, Some(speaker_ref), false, &mut self.output_buffer) { eprintln!("AEC error: {:?}", e); - return mic_input.to_vec(); // Fallback to raw input + output.copy_from_slice(mic_input); // Fallback to raw input + return false; } - out + output.copy_from_slice(&self.output_buffer); + true } /// Resets the echo canceller state. @@ -58,3 +66,4 @@ impl Default for EchoCanceller { Self::new() } } + diff --git a/crates/core/src/processor.rs b/crates/core/src/processor.rs index f1129b9..f9f40a6 100644 --- a/crates/core/src/processor.rs +++ b/crates/core/src/processor.rs @@ -406,8 +406,9 @@ impl VoidProcessor { if let Some(refs) = ref_frames { // Try to match channel, or use channel 0 if fewer refs if let Some(ref_ch) = refs.get(i).or(refs.first()) { - let processed = aec_instance.process_frame(&temp_input, ref_ch); - temp_input.copy_from_slice(&processed); + let mut aec_output = [0.0f32; FRAME_SIZE]; + aec_instance.process_frame(&temp_input, ref_ch, &mut aec_output); + temp_input.copy_from_slice(&aec_output); } } } diff --git a/crates/lv2/Cargo.toml b/crates/lv2/Cargo.toml index 78ce2b3..2dc0bc5 100644 --- a/crates/lv2/Cargo.toml +++ b/crates/lv2/Cargo.toml @@ -9,5 +9,6 @@ crate-type = ["cdylib"] [dependencies] lv2 = "0.6" voidmic_core = { path = "../core" } -ringbuf = "0.2.8" +ringbuf = "0.4.7" log = "0.4" + diff --git a/crates/lv2/src/lib.rs b/crates/lv2/src/lib.rs index f63abd9..45f3ba1 100644 --- a/crates/lv2/src/lib.rs +++ b/crates/lv2/src/lib.rs @@ -1,5 +1,6 @@ use lv2::prelude::*; -use ringbuf::{Consumer, Producer, RingBuffer}; +use ringbuf::traits::{Consumer, Observer, Producer}; +use ringbuf::HeapRb; use std::sync::atomic::Ordering; use voidmic_core::constants::FRAME_SIZE; use voidmic_core::VoidProcessor; @@ -18,10 +19,8 @@ struct VoidMicPorts { #[uri("https://github.com/Detair/voidvoice/lv2/voidmic")] struct VoidMic { processor: VoidProcessor, - rb_in_prod: Producer, - rb_in_cons: Consumer, - rb_out_prod: Producer, - rb_out_cons: Consumer, + rb_in: HeapRb, + rb_out: HeapRb, } impl Plugin for VoidMic { @@ -39,17 +38,13 @@ impl Plugin for VoidMic { ); let buffer_size = FRAME_SIZE * 4 * 2; - let rb_in = RingBuffer::::new(buffer_size); - let (prod_in, cons_in) = rb_in.split(); - let rb_out = RingBuffer::::new(buffer_size); - let (prod_out, cons_out) = rb_out.split(); + let rb_in = HeapRb::::new(buffer_size); + let rb_out = HeapRb::::new(buffer_size); Some(Self { processor, - rb_in_prod: prod_in, - rb_in_cons: cons_in, - rb_out_prod: prod_out, - rb_out_cons: cons_out, + rb_in, + rb_out, }) } @@ -69,8 +64,8 @@ impl Plugin for VoidMic { let input_r = ports.input_r.iter(); for (l, r) in input_l.zip(input_r) { - let _ = self.rb_in_prod.push(*l); - let _ = self.rb_in_prod.push(*r); + let _ = self.rb_in.try_push(*l); + let _ = self.rb_in.try_push(*r); } // 3. Process Blocks @@ -79,10 +74,10 @@ impl Plugin for VoidMic { let mut left_out = [0.0f32; FRAME_SIZE]; let mut right_out = [0.0f32; FRAME_SIZE]; - while self.rb_in_cons.len() >= FRAME_SIZE * 2 { + while self.rb_in.occupied_len() >= FRAME_SIZE * 2 { for j in 0..FRAME_SIZE { - left_in[j] = self.rb_in_cons.pop().unwrap_or(0.0); - right_in[j] = self.rb_in_cons.pop().unwrap_or(0.0); + left_in[j] = self.rb_in.try_pop().unwrap_or(0.0); + right_in[j] = self.rb_in.try_pop().unwrap_or(0.0); } self.processor.process_frame( @@ -95,8 +90,8 @@ impl Plugin for VoidMic { ); for j in 0..FRAME_SIZE { - let _ = self.rb_out_prod.push(left_out[j]); - let _ = self.rb_out_prod.push(right_out[j]); + let _ = self.rb_out.try_push(left_out[j]); + let _ = self.rb_out.try_push(right_out[j]); } } @@ -105,9 +100,9 @@ impl Plugin for VoidMic { let output_r = ports.output_r.iter_mut(); for (l, r) in output_l.zip(output_r) { - if self.rb_out_cons.len() >= 2 { - *l = self.rb_out_cons.pop().unwrap_or(0.0); - *r = self.rb_out_cons.pop().unwrap_or(0.0); + if self.rb_out.occupied_len() >= 2 { + *l = self.rb_out.try_pop().unwrap_or(0.0); + *r = self.rb_out.try_pop().unwrap_or(0.0); } else { *l = 0.0; *r = 0.0; @@ -117,3 +112,4 @@ impl Plugin for VoidMic { } lv2_descriptors!(VoidMic); + diff --git a/crates/plugin/Cargo.toml b/crates/plugin/Cargo.toml index 0cc2d65..dadbbb9 100644 --- a/crates/plugin/Cargo.toml +++ b/crates/plugin/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib"] [dependencies] voidmic_core = { path = "../core" } nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git" } -ringbuf = "0.2.8" +ringbuf = "0.4.7" anyhow = "1.0" nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug.git" } egui = "0.31" diff --git a/crates/plugin/src/lib.rs b/crates/plugin/src/lib.rs index 7f26429..1e028cb 100644 --- a/crates/plugin/src/lib.rs +++ b/crates/plugin/src/lib.rs @@ -1,7 +1,8 @@ use crossbeam_channel::Receiver; use nih_plug::prelude::*; use nih_plug_egui::{create_egui_editor, widgets, EguiState}; -use ringbuf::{Consumer, Producer, RingBuffer}; +use ringbuf::traits::{Consumer, Observer, Producer}; +use ringbuf::HeapRb; use std::num::NonZeroU32; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; @@ -17,10 +18,8 @@ struct VoidMicPlugin { processor: Option, // Ring Buffers (Audio I/O) - rb_in_prod: Option>, - rb_in_cons: Option>, - rb_out_prod: Option>, - rb_out_cons: Option>, + rb_in: Option>, + rb_out: Option>, // GUI Data Bridging volume_level: Arc, @@ -57,10 +56,8 @@ impl Default for VoidMicPlugin { Self { params: Arc::new(VoidMicParams::default()), processor: None, - rb_in_prod: None, - rb_in_cons: None, - rb_out_prod: None, - rb_out_cons: None, + rb_in: None, + rb_out: None, volume_level: Arc::new(AtomicU32::new(0)), spectrum_receiver: None, } @@ -99,7 +96,7 @@ impl Plugin for VoidMicPlugin { const NAME: &'static str = "VoidMic"; const VENDOR: &'static str = "Detair"; const URL: &'static str = "https://github.com/Detair/voidvoice"; - const EMAIL: &'static str = "user@example.com"; + const EMAIL: &'static str = ""; const VERSION: &'static str = env!("CARGO_PKG_VERSION"); @@ -198,9 +195,10 @@ impl Plugin for VoidMicPlugin { ) -> bool { if buffer_config.sample_rate != SAMPLE_RATE as f32 { nih_log!( - "VoidMic only supports 48kHz currently. Host is using {:.0}Hz", + "VoidMic requires 48kHz sample rate. Host is using {:.0}Hz. Plugin initialization rejected.", buffer_config.sample_rate ); + return false; } let (tx, rx) = crossbeam_channel::bounded(2); @@ -222,16 +220,9 @@ impl Plugin for VoidMicPlugin { let buffer_size = FRAME_SIZE * 4 * 2; // * 2 for Stereo - // Ringbuf 0.2.8 - let rb_in = RingBuffer::::new(buffer_size); - let (prod_in, cons_in) = rb_in.split(); - self.rb_in_prod = Some(prod_in); - self.rb_in_cons = Some(cons_in); - - let rb_out = RingBuffer::::new(buffer_size); - let (prod_out, cons_out) = rb_out.split(); - self.rb_out_prod = Some(prod_out); - self.rb_out_cons = Some(cons_out); + // Ringbuf 0.4 + self.rb_in = Some(HeapRb::::new(buffer_size)); + self.rb_out = Some(HeapRb::::new(buffer_size)); true } @@ -264,32 +255,32 @@ impl Plugin for VoidMicPlugin { let num_samples = channel_data[0].len(); // 1. Push Input (Interleaved) - if let Some(prod_in) = &mut self.rb_in_prod { + if let Some(rb_in) = &mut self.rb_in { for i in 0..num_samples { if num_channels == 2 { - let _ = prod_in.push(channel_data[0][i]); - let _ = prod_in.push(channel_data[1][i]); + let _ = rb_in.try_push(channel_data[0][i]); + let _ = rb_in.try_push(channel_data[1][i]); } else if num_channels == 1 { // Duplicate Mono to Stereo let val = channel_data[0][i]; - let _ = prod_in.push(val); - let _ = prod_in.push(val); + let _ = rb_in.try_push(val); + let _ = rb_in.try_push(val); } } } // 2. Process chunks - if let (Some(cons_in), Some(prod_out)) = (&mut self.rb_in_cons, &mut self.rb_out_prod) { + if let (Some(rb_in), Some(rb_out)) = (&mut self.rb_in, &mut self.rb_out) { let mut left_in = [0.0f32; FRAME_SIZE]; let mut right_in = [0.0f32; FRAME_SIZE]; let mut left_out = [0.0f32; FRAME_SIZE]; let mut right_out = [0.0f32; FRAME_SIZE]; // Need 2 * FRAME_SIZE samples for a full stereo frame - while cons_in.len() >= FRAME_SIZE * 2 { + while rb_in.occupied_len() >= FRAME_SIZE * 2 { for j in 0..FRAME_SIZE { - left_in[j] = cons_in.pop().unwrap_or(0.0); - right_in[j] = cons_in.pop().unwrap_or(0.0); + left_in[j] = rb_in.try_pop().unwrap_or(0.0); + right_in[j] = rb_in.try_pop().unwrap_or(0.0); } processor.process_frame( @@ -302,18 +293,18 @@ impl Plugin for VoidMicPlugin { ); for j in 0..FRAME_SIZE { - let _ = prod_out.push(left_out[j]); - let _ = prod_out.push(right_out[j]); + let _ = rb_out.try_push(left_out[j]); + let _ = rb_out.try_push(right_out[j]); } } } // 3. Output - if let Some(cons_out) = &mut self.rb_out_cons { + if let Some(rb_out) = &mut self.rb_out { for i in 0..num_samples { - if cons_out.len() >= 2 { - let l = cons_out.pop().unwrap_or(0.0); - let r = cons_out.pop().unwrap_or(0.0); + if rb_out.occupied_len() >= 2 { + let l = rb_out.try_pop().unwrap_or(0.0); + let r = rb_out.try_pop().unwrap_or(0.0); if num_channels >= 1 { channel_data[0][i] = l; diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 1f14ff8..e5f1314 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -1,5 +1,11 @@ use egui::{Color32, CornerRadius, Stroke, Visuals}; +// Theme color constants for consistent styling +pub const ACCENT_BLUE: Color32 = Color32::from_rgb(88, 166, 255); +pub const SUCCESS_GREEN: Color32 = Color32::from_rgb(46, 160, 67); +pub const WARNING_YELLOW: Color32 = Color32::from_rgb(255, 193, 7); +pub const DANGER_RED: Color32 = Color32::from_rgb(248, 81, 73); + pub fn setup_custom_style(ctx: &egui::Context, dark_mode: bool) { if dark_mode { let mut visuals = Visuals::dark(); @@ -7,7 +13,6 @@ pub fn setup_custom_style(ctx: &egui::Context, dark_mode: bool) { // Premium Dark Palette (Deep Void Blue) let bg_color = Color32::from_rgb(13, 17, 23); // Extremely dark blue-grey let panel_color = Color32::from_rgb(22, 27, 34); // Slightly lighter - let _accent_color = Color32::from_rgb(88, 166, 255); // Vibrant blue let text_color = Color32::from_rgb(240, 246, 252); visuals.window_fill = bg_color; @@ -24,7 +29,7 @@ pub fn setup_custom_style(ctx: &egui::Context, dark_mode: bool) { visuals.widgets.hovered.bg_fill = Color32::from_rgb(48, 54, 61); visuals.widgets.hovered.corner_radius = CornerRadius::same(6); - visuals.widgets.active.bg_fill = Color32::from_rgb(88, 166, 255); + visuals.widgets.active.bg_fill = ACCENT_BLUE; visuals.widgets.active.fg_stroke = Stroke::new(1.0, Color32::BLACK); visuals.widgets.active.corner_radius = CornerRadius::same(6); @@ -34,9 +39,26 @@ pub fn setup_custom_style(ctx: &egui::Context, dark_mode: bool) { } else { // Clean Light Mode let mut visuals = Visuals::light(); + + let panel_color = Color32::from_rgb(248, 249, 250); + let text_color = Color32::from_rgb(36, 41, 47); + + visuals.panel_fill = panel_color; + visuals.override_text_color = Some(text_color); + + visuals.widgets.inactive.bg_fill = Color32::from_rgb(235, 237, 240); visuals.widgets.inactive.corner_radius = CornerRadius::same(6); + + visuals.widgets.hovered.bg_fill = Color32::from_rgb(220, 223, 228); visuals.widgets.hovered.corner_radius = CornerRadius::same(6); + + visuals.widgets.active.bg_fill = Color32::from_rgb(0, 120, 215); + visuals.widgets.active.fg_stroke = Stroke::new(1.0, Color32::WHITE); visuals.widgets.active.corner_radius = CornerRadius::same(6); + + visuals.selection.bg_fill = Color32::from_rgb(0, 120, 215); + ctx.set_visuals(visuals); } } + diff --git a/crates/ui/src/visualizer.rs b/crates/ui/src/visualizer.rs index fb6ae07..806649c 100644 --- a/crates/ui/src/visualizer.rs +++ b/crates/ui/src/visualizer.rs @@ -7,8 +7,8 @@ pub fn render_spectrum(ui: &mut egui::Ui, input_data: &[f32], output_data: &[f32 } let red_line = Line::new(PlotPoints::from_ys_f32(input_data)) - .color(egui::Color32::from_rgba_premultiplied(100, 0, 0, 100)) - .fill(0.0); // Fill input (noise) with red/grey + .color(egui::Color32::from_rgba_unmultiplied(220, 53, 69, 180)) // Clearer red + .fill(0.0); // Fill input (noise) let green_line = Line::new(PlotPoints::from_ys_f32(output_data)) .color(egui::Color32::GREEN)